Skip to main content

i18n_again_support/
lib.rs

1use fs::File;
2use fs_err as fs;
3use glob::glob;
4use std::collections::HashMap;
5use std::io::prelude::*;
6use std::io::Write;
7
8mod prepare;
9pub mod errors;
10
11pub use crate::errors::*;
12pub use crate::prepare::*;
13
14pub type Locale = String;
15pub type Value = serde_json::Value;
16pub type Translations = HashMap<Locale, Value>;
17
18/// Init I18n translations from `build.rs`.
19///
20/// This will load all translations by glob `**/*.yml` from the
21/// given path and prepare a file to be included in the compiled proc macro.
22pub fn load_from_dirs(locale_dir: impl AsRef<std::path::Path>) -> Result<()> {
23    let locale_path = locale_dir.as_ref();
24    let translations = locales_yaml_files_to_translation_map(locale_path)?;
25    let translations = serialize(translations)?;
26
27    fs::write("foo-bar-baz", translations)?;
28
29    Ok(())
30}
31
32/// Path to an translation item
33///
34/// f the for `a.b.c` analogous to `json` addressing.
35pub type TranslationPath = String;
36
37/// Optimize for proc-macro parsing ease,
38/// that's called 1 vs n times more often!
39pub type TranslationMap = HashMap<TranslationPath, HashMap<Locale, String>>;
40
41pub fn deserialize(bytes: &[u8]) -> Result<TranslationMap> {
42    let tmap: TranslationMap = postcard::from_bytes(bytes)?;
43    Ok(tmap)
44}
45
46pub fn serialize(text2translations: TranslationMap) -> Result<Vec<u8>> {
47    let bytes = postcard::to_allocvec(&text2translations)?;
48    Ok(bytes)
49}
50
51/// Merge JSON Values, merge b into a
52///
53/// Overrides values of `a` with values of `b`
54/// and recurses into all objects.
55pub fn merge_value(a: &mut Value, b: &Value) {
56    match (a, b) {
57        (&mut Value::Object(ref mut a), &Value::Object(ref b)) => {
58            for (k, v) in b {
59                merge_value(a.entry(k.clone()).or_insert(Value::Null), v);
60            }
61        }
62        (a, b) => {
63            *a = b.clone();
64        }
65    }
66}
67
68fn extract_yaml_content(
69    yaml_content: impl AsRef<str>,
70    trans_map: &mut HashMap<TranslationPath, serde_json::Value>,
71) -> Result<()> {
72    // All translation items per language
73    let trs: Translations = serde_yaml::from_str(yaml_content.as_ref())?;
74
75    eprintln!("cargo:warning: foo: -- {:?}", &trs);
76
77    trs.into_iter().for_each(|(tp, translations)| {
78        trans_map
79            .entry(tp)
80            .and_modify(|translations_old| merge_value(translations_old, &translations))
81            .or_insert(translations);
82    });
83    Ok(())
84}
85
86fn trans_map_voodoo(trans_map: HashMap<TranslationPath, serde_json::Value>) -> TranslationMap {
87    let mut tp2trans_per_locale = TranslationMap::new();
88
89    // let mut locale_vars = HashMap::<String, String>::new();
90    // translations.iter().for_each(|(locale, trs)| {
91    //     let new_vars = extract_vars(locale.as_str(), &trs);
92    //     locale_vars.extend(new_vars);
93    // });
94
95    // locale_vars
96
97    trans_map.into_iter().for_each(|(ref locale, trs)| {
98        let new_vars = extract_vars(locale.as_str(), &trs);
99        let new_vars_iter = new_vars.into_iter().filter_map(|(k, v)| {
100            k.strip_prefix(&(locale.to_owned() + "."))
101                .map(move |k| (k.to_string(), v))
102        });
103        for (tp, translation) in new_vars_iter {
104            tp2trans_per_locale
105                .entry(tp)
106                .or_default()
107                .insert(locale.clone(), translation);
108        }
109    });
110    tp2trans_per_locale
111}
112
113// Load locales into flatten key,value HashMap
114pub fn locales_yaml_files_to_translation_map(
115    locales_dir: &std::path::Path,
116) -> Result<TranslationMap> {
117    let mut trans_map = Translations::new();
118
119    let path_pattern = format!("{}/**/*.yml", locales_dir.display());
120
121    println!("cargo:i18n-locale={}", &path_pattern);
122
123    let paths = glob(&path_pattern).expect("Failed to read glob pattern");
124    for maybe_path in paths {
125        let path = if let Ok(path) = maybe_path {
126            path
127        } else {
128            continue;
129        };
130        println!("cargo:i18n-load={}", &path.display());
131
132        let file = File::open(path).expect("Failed to open the YAML file");
133        let mut reader = std::io::BufReader::new(file);
134        let mut content = String::new();
135
136        reader.read_to_string(&mut content)?;
137        extract_yaml_content(content, &mut trans_map)?;
138    }
139
140    let tp2trans_per_locale = trans_map_voodoo(trans_map);
141
142    Ok(tp2trans_per_locale)
143}
144
145/// Find the value based on it's path aka prefix `a.b.c`
146///
147/// Returns a `prefix`:`value` set.
148pub fn extract_vars(prefix: &str, trs: &Value) -> HashMap<String, String> {
149    let mut v = HashMap::<String, String>::new();
150    let prefix = prefix.to_string();
151
152    match &trs {
153        serde_json::Value::String(s) => {
154            v.insert(prefix, s.to_string());
155        }
156        serde_json::Value::Object(o) => {
157            for (k, vv) in o {
158                let key = format!("{}.{}", prefix, k);
159                v.extend(extract_vars(key.as_str(), vv));
160            }
161        }
162        serde_json::Value::Null => {
163            v.insert(prefix, "".into());
164        }
165        serde_json::Value::Bool(s) => {
166            v.insert(prefix, format!("{}", s));
167        }
168        serde_json::Value::Number(s) => {
169            v.insert(prefix, format!("{}", s));
170        }
171        serde_json::Value::Array(_) => {
172            v.insert(prefix, "".into());
173        }
174    }
175
176    v
177}
178
179#[cfg(test)]
180mod tests;