Skip to main content

robinpath_modules/modules/
i18n_mod.rs

1use robinpath::{RobinPath, Value};
2use std::collections::HashMap;
3use std::sync::{Arc, Mutex};
4
5struct I18nState {
6    locale: String,
7    translations: HashMap<String, HashMap<String, String>>,
8}
9
10impl I18nState {
11    fn new() -> Self {
12        I18nState {
13            locale: "en".to_string(),
14            translations: HashMap::new(),
15        }
16    }
17}
18
19pub fn register(rp: &mut RobinPath) {
20    let state = Arc::new(Mutex::new(I18nState::new()));
21
22    // i18n.setLocale locale → bool
23    let s = state.clone();
24    rp.register_builtin("i18n.setLocale", move |args, _| {
25        let locale = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "en".to_string());
26        s.lock().unwrap().locale = locale;
27        Ok(Value::Bool(true))
28    });
29
30    // i18n.getLocale → string
31    let s = state.clone();
32    rp.register_builtin("i18n.getLocale", move |_args, _| {
33        Ok(Value::String(s.lock().unwrap().locale.clone()))
34    });
35
36    // i18n.loadTranslations locale translations → bool
37    let s = state.clone();
38    rp.register_builtin("i18n.loadTranslations", move |args, _| {
39        let locale = args.first().map(|v| v.to_display_string()).unwrap_or_default();
40        let translations = args.get(1).cloned().unwrap_or(Value::Null);
41        if let Value::Object(obj) = translations {
42            let mut st = s.lock().unwrap();
43            let entry = st.translations.entry(locale).or_default();
44            for (key, val) in obj {
45                entry.insert(key, val.to_display_string());
46            }
47            Ok(Value::Bool(true))
48        } else {
49            Err("Translations must be an object".to_string())
50        }
51    });
52
53    // i18n.t key locale? interpolation? → translated string
54    let s = state.clone();
55    rp.register_builtin("i18n.t", move |args, _| {
56        let key = args.first().map(|v| v.to_display_string()).unwrap_or_default();
57        let locale = args.get(1).map(|v| v.to_display_string());
58        let interpolation = args.get(2).cloned().unwrap_or(Value::Null);
59        let st = s.lock().unwrap();
60        let loc = locale.as_deref().unwrap_or(&st.locale);
61        let translated = st.translations.get(loc)
62            .and_then(|t| t.get(&key))
63            .cloned()
64            .unwrap_or(key.clone());
65        // Apply interpolation {{var}}
66        let mut result = translated;
67        if let Value::Object(obj) = &interpolation {
68            for (k, v) in obj {
69                let placeholder = format!("{{{{{}}}}}", k);
70                result = result.replace(&placeholder, &v.to_display_string());
71            }
72        }
73        Ok(Value::String(result))
74    });
75
76    // i18n.formatNumber number locale? → formatted string
77    let s = state.clone();
78    rp.register_builtin("i18n.formatNumber", move |args, _| {
79        let num = args.first().map(|v| v.to_number()).unwrap_or(0.0);
80        let locale = args.get(1).map(|v| v.to_display_string())
81            .unwrap_or_else(|| s.lock().unwrap().locale.clone());
82        let (thousands_sep, decimal_sep) = get_number_separators(&locale);
83        let formatted = format_number(num, thousands_sep, decimal_sep);
84        Ok(Value::String(formatted))
85    });
86
87    // i18n.formatCurrency amount currency locale? → formatted string
88    let s = state.clone();
89    rp.register_builtin("i18n.formatCurrency", move |args, _| {
90        let amount = args.first().map(|v| v.to_number()).unwrap_or(0.0);
91        let currency = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "USD".to_string());
92        let locale = args.get(2).map(|v| v.to_display_string())
93            .unwrap_or_else(|| s.lock().unwrap().locale.clone());
94        let symbol = currency_symbol(&currency);
95        let (thousands_sep, decimal_sep) = get_number_separators(&locale);
96        let formatted = format_number(amount, thousands_sep, decimal_sep);
97        Ok(Value::String(format!("{}{}", symbol, formatted)))
98    });
99
100    // i18n.pluralize count singular plural → string
101    rp.register_builtin("i18n.pluralize", |args, _| {
102        let count = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
103        let singular = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
104        let plural = args.get(2).map(|v| v.to_display_string())
105            .unwrap_or_else(|| format!("{}s", singular));
106        if count == 1 {
107            Ok(Value::String(format!("{} {}", count, singular)))
108        } else {
109            Ok(Value::String(format!("{} {}", count, plural)))
110        }
111    });
112
113    // i18n.direction locale → "ltr" or "rtl"
114    let s = state.clone();
115    rp.register_builtin("i18n.direction", move |args, _| {
116        let locale = args.first().map(|v| v.to_display_string())
117            .unwrap_or_else(|| s.lock().unwrap().locale.clone());
118        let lang = locale.split('-').next().unwrap_or(&locale);
119        let rtl = matches!(lang, "ar" | "he" | "fa" | "ur" | "ps" | "yi" | "sd" | "ug");
120        Ok(Value::String(if rtl { "rtl" } else { "ltr" }.to_string()))
121    });
122
123    // i18n.listLocales → array of locale strings
124    let s = state.clone();
125    rp.register_builtin("i18n.listLocales", move |_args, _| {
126        let st = s.lock().unwrap();
127        let locales: Vec<Value> = st.translations.keys()
128            .map(|k| Value::String(k.clone()))
129            .collect();
130        Ok(Value::Array(locales))
131    });
132
133    // i18n.hasTranslation key locale? → bool
134    let s = state.clone();
135    rp.register_builtin("i18n.hasTranslation", move |args, _| {
136        let key = args.first().map(|v| v.to_display_string()).unwrap_or_default();
137        let locale = args.get(1).map(|v| v.to_display_string());
138        let st = s.lock().unwrap();
139        let loc = locale.as_deref().unwrap_or(&st.locale);
140        let has = st.translations.get(loc)
141            .map(|t| t.contains_key(&key))
142            .unwrap_or(false);
143        Ok(Value::Bool(has))
144    });
145
146    // i18n.formatList items locale? → "a, b, and c"
147    rp.register_builtin("i18n.formatList", |args, _| {
148        let items = args.first().cloned().unwrap_or(Value::Null);
149        if let Value::Array(arr) = items {
150            let strings: Vec<String> = arr.iter().map(|v| v.to_display_string()).collect();
151            let result = match strings.len() {
152                0 => String::new(),
153                1 => strings[0].clone(),
154                2 => format!("{} and {}", strings[0], strings[1]),
155                _ => {
156                    let last = strings.last().unwrap();
157                    let rest = &strings[..strings.len() - 1];
158                    format!("{}, and {}", rest.join(", "), last)
159                }
160            };
161            Ok(Value::String(result))
162        } else {
163            Ok(Value::String(items.to_display_string()))
164        }
165    });
166}
167
168fn get_number_separators(locale: &str) -> (char, char) {
169    let lang = locale.split('-').next().unwrap_or(locale);
170    match lang {
171        "de" | "fr" | "es" | "it" | "pt" | "nl" | "pl" | "cs" | "sk" | "hu" | "ro" | "bg" | "hr" | "sl" | "et" | "lv" | "lt" => ('.', ','),
172        _ => (',', '.'),
173    }
174}
175
176fn format_number(num: f64, thousands_sep: char, decimal_sep: char) -> String {
177    let is_negative = num < 0.0;
178    let abs = num.abs();
179    let integer_part = abs as u64;
180    let frac = abs - integer_part as f64;
181    // Format integer part with thousands separator
182    let int_str = integer_part.to_string();
183    let mut formatted = String::new();
184    for (i, c) in int_str.chars().rev().enumerate() {
185        if i > 0 && i % 3 == 0 { formatted.push(thousands_sep); }
186        formatted.push(c);
187    }
188    let formatted: String = formatted.chars().rev().collect();
189    // Add decimal part if needed
190    let result = if frac > 0.0001 {
191        let decimal_str = format!("{:.2}", frac);
192        format!("{}{}{}", formatted, decimal_sep, &decimal_str[2..])
193    } else {
194        formatted
195    };
196    if is_negative { format!("-{}", result) } else { result }
197}
198
199fn currency_symbol(code: &str) -> &'static str {
200    match code.to_uppercase().as_str() {
201        "USD" => "$", "EUR" => "\u{20ac}", "GBP" => "\u{00a3}", "JPY" => "\u{00a5}",
202        "CNY" | "RMB" => "\u{00a5}", "KRW" => "\u{20a9}", "INR" => "\u{20b9}",
203        "BRL" => "R$", "RUB" => "\u{20bd}", "TRY" => "\u{20ba}", "CHF" => "CHF ",
204        "CAD" => "CA$", "AUD" => "A$", "MXN" => "MX$", "SEK" => "kr",
205        _ => "$",
206    }
207}