Skip to main content

robinpath_modules/modules/
i18n_mod.rs

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