robinpath_modules/modules/
i18n_mod.rs1use 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 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 rp.register_builtin("i18n.getLocale", |_args, _| {
25 Ok(Value::String(STATE.lock().unwrap().locale.clone()))
26 });
27
28 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 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 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 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 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(¤cy);
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 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 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 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 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 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 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 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}