1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
use fs::File;
use fs_err as fs;
use glob::glob;
use std::collections::HashMap;
use std::io::prelude::*;
use std::io::Write;

mod prepare;
pub mod errors;

pub use crate::errors::*;
pub use crate::prepare::*;

pub type Locale = String;
pub type Value = serde_json::Value;
pub type Translations = HashMap<Locale, Value>;

/// Init I18n translations from `build.rs`.
///
/// This will load all translations by glob `**/*.yml` from the
/// given path and prepare a file to be included in the compiled proc macro.
pub fn load_from_dirs(locale_dir: impl AsRef<std::path::Path>) -> Result<()> {
    let locale_path = locale_dir.as_ref();
    let translations = locales_yaml_files_to_translation_map(locale_path)?;
    let translations = serialize(translations)?;

    fs::write("foo-bar-baz", translations)?;

    Ok(())
}

/// Path to an translation item
///
/// f the for `a.b.c` analogous to `json` addressing.
pub type TranslationPath = String;

/// Optimize for proc-macro parsing ease,
/// that's called 1 vs n times more often!
pub type TranslationMap = HashMap<TranslationPath, HashMap<Locale, String>>;

pub fn deserialize(bytes: &[u8]) -> Result<TranslationMap> {
    let tmap: TranslationMap = postcard::from_bytes(bytes)?;
    Ok(tmap)
}

pub fn serialize(text2translations: TranslationMap) -> Result<Vec<u8>> {
    let bytes = postcard::to_allocvec(&text2translations)?;
    Ok(bytes)
}

/// Merge JSON Values, merge b into a
///
/// Overrides values of `a` with values of `b`
/// and recurses into all objects.
pub fn merge_value(a: &mut Value, b: &Value) {
    match (a, b) {
        (&mut Value::Object(ref mut a), &Value::Object(ref b)) => {
            for (k, v) in b {
                merge_value(a.entry(k.clone()).or_insert(Value::Null), v);
            }
        }
        (a, b) => {
            *a = b.clone();
        }
    }
}

fn extract_yaml_content(
    yaml_content: impl AsRef<str>,
    trans_map: &mut HashMap<TranslationPath, serde_json::Value>,
) -> Result<()> {
    // All translation items per language
    let trs: Translations = serde_yaml::from_str(yaml_content.as_ref())?;

    eprintln!("cargo:warning: foo: -- {:?}", &trs);

    trs.into_iter().for_each(|(tp, translations)| {
        trans_map
            .entry(tp)
            .and_modify(|translations_old| merge_value(translations_old, &translations))
            .or_insert(translations);
    });
    Ok(())
}

fn trans_map_voodoo(trans_map: HashMap<TranslationPath, serde_json::Value>) -> TranslationMap {
    let mut tp2trans_per_locale = TranslationMap::new();

    // let mut locale_vars = HashMap::<String, String>::new();
    // translations.iter().for_each(|(locale, trs)| {
    //     let new_vars = extract_vars(locale.as_str(), &trs);
    //     locale_vars.extend(new_vars);
    // });

    // locale_vars

    trans_map.into_iter().for_each(|(ref locale, trs)| {
        let new_vars = extract_vars(locale.as_str(), &trs);
        let new_vars_iter = new_vars.into_iter().filter_map(|(k, v)| {
            k.strip_prefix(&(locale.to_owned() + "."))
                .map(move |k| (k.to_string(), v))
        });
        for (tp, translation) in new_vars_iter {
            tp2trans_per_locale
                .entry(tp)
                .or_default()
                .insert(locale.clone(), translation);
        }
    });
    tp2trans_per_locale
}

// Load locales into flatten key,value HashMap
pub fn locales_yaml_files_to_translation_map(
    locales_dir: &std::path::Path,
) -> Result<TranslationMap> {
    let mut trans_map = Translations::new();

    let path_pattern = format!("{}/**/*.yml", locales_dir.display());

    println!("cargo:i18n-locale={}", &path_pattern);

    let paths = glob(&path_pattern).expect("Failed to read glob pattern");
    for maybe_path in paths {
        let path = if let Ok(path) = maybe_path {
            path
        } else {
            continue;
        };
        println!("cargo:i18n-load={}", &path.display());

        let file = File::open(path).expect("Failed to open the YAML file");
        let mut reader = std::io::BufReader::new(file);
        let mut content = String::new();

        reader.read_to_string(&mut content)?;
        extract_yaml_content(content, &mut trans_map)?;
    }

    let tp2trans_per_locale = trans_map_voodoo(trans_map);

    Ok(tp2trans_per_locale)
}

/// Find the value based on it's path aka prefix `a.b.c`
///
/// Returns a `prefix`:`value` set.
pub fn extract_vars(prefix: &str, trs: &Value) -> HashMap<String, String> {
    let mut v = HashMap::<String, String>::new();
    let prefix = prefix.to_string();

    match &trs {
        serde_json::Value::String(s) => {
            v.insert(prefix, s.to_string());
        }
        serde_json::Value::Object(o) => {
            for (k, vv) in o {
                let key = format!("{}.{}", prefix, k);
                v.extend(extract_vars(key.as_str(), vv));
            }
        }
        serde_json::Value::Null => {
            v.insert(prefix, "".into());
        }
        serde_json::Value::Bool(s) => {
            v.insert(prefix, format!("{}", s));
        }
        serde_json::Value::Number(s) => {
            v.insert(prefix, format!("{}", s));
        }
        serde_json::Value::Array(_) => {
            v.insert(prefix, "".into());
        }
    }

    v
}

#[cfg(test)]
mod tests;