robinpath_modules/modules/
i18n_mod.rs1use 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 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 let s = state.clone();
32 rp.register_builtin("i18n.getLocale", move |_args, _| {
33 Ok(Value::String(s.lock().unwrap().locale.clone()))
34 });
35
36 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 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 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 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 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(¤cy);
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 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 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 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 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 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 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 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}