gettr 0.0.2

i18n framework for Rust and yew
Documentation
use regex::Regex;
use std::{collections::HashMap, fs, io::Write, path::PathBuf, sync::RwLock};

pub use once_cell::sync::Lazy;

#[cfg(feature = "yew")]
mod yew_integration;
#[cfg(feature = "yew")]
pub use yew_integration::*;

#[macro_export]
macro_rules! gettr {
    ($s:literal $(, $($arg:expr)+)?) => {{
        let mut s = $crate::get($s);
        $(
        $(
        s.replacen("{}", $arg, 1);
        )+
        )?
        s
    }};
}

#[derive(Debug, Default)]
struct State {
    current_language: String,
    translations: HashMap<String, String>,
}

static STATE: Lazy<RwLock<State>> = Lazy::new(|| Default::default());

pub fn get(key: &str) -> String {
    let state = STATE.read();
    if state.is_err() {
        return key.into();
    }
    let state = state.unwrap();
    get_translation(&state, key, &state.current_language)
}

fn get_translation(state: &State, key: &str, lang: &str) -> String {
    let translations = state.translations.get(lang);
    if translations.is_none() {
        return key.into();
    }
    let translations = translations.unwrap();
    let translations = read(&translations);
    if translations.is_err() {
        return key.into();
    }
    let translations = translations.unwrap();
    let t = translations.get(key).cloned().unwrap_or_else(|| key.into());
    if t.is_empty() {
        key.into()
    } else {
        t
    }
}

pub fn get_for(lang: &str, key: &str) -> String {
    let state = STATE.read();
    if state.is_err() {
        return key.into();
    }
    let state = state.unwrap();
    get_translation(&state, key, lang)
}

pub fn init(language: &'static str, translations: Vec<(&'static str, &'static str)>) {
    if let Ok(mut w) = STATE.write() {
        let mut t = HashMap::new();
        for tr in translations {
            t.insert(tr.0.into(), tr.1.into());
        }
        let s = State {
            current_language: language.into(),
            translations: t,
        };
        *w = s;
    }
}

pub fn update(language: &str) {
    if let Ok(mut w) = STATE.write() {
        w.current_language = language.into();
    }
}

pub fn read(content: &str) -> std::io::Result<HashMap<String, String>> {
    let mut t = HashMap::new();
    for line in content.lines() {
        if let Some((key, value)) = line.split_once("=") {
            let key = key.trim();
            let value = value.trim();
            t.insert(
                key[1..key.len() - 1].to_string(),
                value[1..value.len() - 1].to_string(),
            );
        }
    }
    Ok(t)
}

pub fn update_translations(
    languages: Vec<&str>,
    searchdir: PathBuf,
    outdir: PathBuf,
) -> std::io::Result<()> {
    let mut translations = HashMap::<String, Vec<String>>::new();
    let regex = regex();
    search(&mut translations, searchdir, &regex)?;

    for lang in languages {
        let path = outdir.join(format!("{}", lang));

        let current = if path.exists() {
            read(&fs::read_to_string(&path)?)?
        } else {
            HashMap::new()
        };

        let mut file = fs::File::create(path)?;
        writeln!(
            file,
            "// This file is generated and automatically updated by gettr."
        )?;
        writeln!(file, "// Values set to keys will be kept on updates.\n")?;
        let mut translations = translations
            .iter()
            .collect::<Vec<(&String, &Vec<String>)>>();
        translations.sort_by(|a, b| a.0.cmp(b.0));
        for (msg, codepointers) in translations {
            for codepointer in codepointers {
                writeln!(file, r#"// {}"#, codepointer)?;
            }
            match current.get(msg) {
                Some(value) => {
                    writeln!(file, r#""{}" = "{}""#, msg, value)?;
                }
                None => {
                    writeln!(file, r#""{}" = """#, msg)?;
                }
            }
        }
    }

    Ok(())
}

fn search(
    translations: &mut HashMap<String, Vec<String>>,
    path: PathBuf,
    regex: &Regex,
) -> std::io::Result<()> {
    for entry in fs::read_dir(&path)? {
        let entry = entry?;
        let path = entry.path().to_str().unwrap_or("unknown_path").to_string();
        if entry.path().is_file() {
            let content = fs::read_to_string(entry.path())?;
            find_in_file(translations, &path, &content, regex);
        } else if entry.path().is_dir() {
            search(translations, entry.path(), regex)?;
        }
    }
    Ok(())
}

fn find_in_file(
    translations: &mut HashMap<String, Vec<String>>,
    path: &str,
    file: &str,
    regex: &Regex,
) {
    for (i, line) in file.lines().enumerate() {
        if let Some(caps) = regex.captures(line) {
            let codepointer = format!("{}#{}", path, i);
            let message = &caps[2];
            match translations.get_mut(message) {
                Some(codepointers) => {
                    codepointers.push(codepointer);
                }
                None => {
                    translations.insert(message.into(), vec![codepointer]);
                }
            }
        }
    }
}

fn regex() -> Regex {
    Regex::new(r#".*gettr(!)?\("([^"]*)"(,.*)?\).*"#).expect("gettr regex is not valid")
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use crate::{find_in_file, regex};

    #[test]
    fn test_code_search() {
        let regex = regex();
        let mut translations = HashMap::new();
        let path = "/foo/bar.rs";
        let code = r#"use stuff;

pub struct Stru {
    f: u8
}
impl Stru {
    pub fn this_is_not_gettr() {
        let s = gettr!("Hello World: {}", "foo bar");
        println!("{}", s);
        let s = gettr!("Hello World: {}", "foo bar");
    }
}
"#;
        find_in_file(&mut translations, path, code, &regex);

        assert_eq!(
            translations.get("Hello World: {}"),
            Some(&vec![
                "/foo/bar.rs#7".to_string(),
                "/foo/bar.rs#9".to_string()
            ])
        )
    }
}