1#[allow(dead_code)]
4pub struct LocaleString {
5 pub key: String,
6 pub value: String,
7 pub context: Option<String>,
8}
9
10#[allow(dead_code)]
11pub struct LocaleTable {
12 pub locale_id: String,
13 pub name: String,
14 pub entries: Vec<LocaleString>,
15}
16
17#[allow(dead_code)]
18pub struct LocalizationSystem {
19 pub tables: Vec<LocaleTable>,
20 pub active_locale: String,
21 pub fallback_locale: String,
22}
23
24#[allow(dead_code)]
25pub fn new_localization(fallback: &str) -> LocalizationSystem {
26 LocalizationSystem {
27 tables: Vec::new(),
28 active_locale: fallback.to_string(),
29 fallback_locale: fallback.to_string(),
30 }
31}
32
33#[allow(dead_code)]
34pub fn add_locale_table(sys: &mut LocalizationSystem, table: LocaleTable) {
35 sys.tables.push(table);
36}
37
38#[allow(dead_code)]
39pub fn set_active_locale(sys: &mut LocalizationSystem, locale_id: &str) {
40 sys.active_locale = locale_id.to_string();
41}
42
43fn find_in_table<'a>(tables: &'a [LocaleTable], locale_id: &str, key: &str) -> Option<&'a str> {
45 tables
46 .iter()
47 .find(|t| t.locale_id == locale_id)
48 .and_then(|t| t.entries.iter().find(|e| e.key == key))
49 .map(|e| e.value.as_str())
50}
51
52#[allow(dead_code)]
53pub fn translate<'a>(sys: &'a LocalizationSystem, key: &'a str) -> &'a str {
54 if let Some(v) = find_in_table(&sys.tables, &sys.active_locale, key) {
55 return v;
56 }
57 if let Some(v) = find_in_table(&sys.tables, &sys.fallback_locale, key) {
58 return v;
59 }
60 key
61}
62
63#[allow(dead_code)]
64pub fn translate_with_context<'a>(
65 sys: &'a LocalizationSystem,
66 key: &'a str,
67 context: &str,
68) -> &'a str {
69 let find_with_ctx = |tables: &'a [LocaleTable], locale: &str| -> Option<&'a str> {
70 tables
71 .iter()
72 .find(|t| t.locale_id == locale)
73 .and_then(|t| {
74 t.entries
75 .iter()
76 .find(|e| {
77 e.key == key && e.context.as_deref().map(|c| c == context).unwrap_or(false)
78 })
79 .or_else(|| t.entries.iter().find(|e| e.key == key))
80 })
81 .map(|e| e.value.as_str())
82 };
83
84 if let Some(v) = find_with_ctx(&sys.tables, &sys.active_locale) {
85 return v;
86 }
87 if let Some(v) = find_with_ctx(&sys.tables, &sys.fallback_locale) {
88 return v;
89 }
90 key
91}
92
93#[allow(dead_code)]
94pub fn has_key(sys: &LocalizationSystem, locale_id: &str, key: &str) -> bool {
95 sys.tables
96 .iter()
97 .find(|t| t.locale_id == locale_id)
98 .map(|t| t.entries.iter().any(|e| e.key == key))
99 .unwrap_or(false)
100}
101
102#[allow(dead_code)]
103pub fn locale_count(sys: &LocalizationSystem) -> usize {
104 sys.tables.len()
105}
106
107#[allow(dead_code)]
108pub fn key_count(table: &LocaleTable) -> usize {
109 table.entries.len()
110}
111
112#[allow(dead_code)]
113pub fn new_locale_table(locale_id: &str, name: &str) -> LocaleTable {
114 LocaleTable {
115 locale_id: locale_id.to_string(),
116 name: name.to_string(),
117 entries: Vec::new(),
118 }
119}
120
121#[allow(dead_code)]
122pub fn add_locale_string(table: &mut LocaleTable, key: &str, value: &str) {
123 table.entries.push(LocaleString {
124 key: key.to_string(),
125 value: value.to_string(),
126 context: None,
127 });
128}
129
130#[allow(dead_code)]
131pub fn export_locale_json(table: &LocaleTable) -> String {
132 let mut json = String::from("{");
133 for (i, entry) in table.entries.iter().enumerate() {
134 if i > 0 {
135 json.push(',');
136 }
137 json.push('"');
138 json.push_str(&entry.key);
139 json.push_str("\":\"");
140 json.push_str(&entry.value);
141 json.push('"');
142 }
143 json.push('}');
144 json
145}
146
147#[allow(dead_code)]
148pub fn import_locale_strings(table: &mut LocaleTable, data: &[(String, String)]) {
149 for (key, value) in data {
150 table.entries.push(LocaleString {
151 key: key.clone(),
152 value: value.clone(),
153 context: None,
154 });
155 }
156}
157
158#[allow(dead_code)]
159pub fn missing_keys(
160 sys: &LocalizationSystem,
161 reference_locale: &str,
162 target_locale: &str,
163) -> Vec<String> {
164 let ref_keys: Vec<&str> = sys
165 .tables
166 .iter()
167 .find(|t| t.locale_id == reference_locale)
168 .map(|t| t.entries.iter().map(|e| e.key.as_str()).collect())
169 .unwrap_or_default();
170
171 ref_keys
172 .into_iter()
173 .filter(|k| !has_key(sys, target_locale, k))
174 .map(|k| k.to_string())
175 .collect()
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 fn make_en_table() -> LocaleTable {
183 let mut t = new_locale_table("en-US", "English");
184 add_locale_string(&mut t, "greeting", "Hello");
185 add_locale_string(&mut t, "farewell", "Goodbye");
186 t
187 }
188
189 fn make_ja_table() -> LocaleTable {
190 let mut t = new_locale_table("ja-JP", "Japanese");
191 add_locale_string(&mut t, "greeting", "こんにちは");
192 t
193 }
194
195 #[test]
196 fn test_new_localization() {
197 let sys = new_localization("en-US");
198 assert_eq!(sys.fallback_locale, "en-US");
199 assert_eq!(sys.active_locale, "en-US");
200 assert!(sys.tables.is_empty());
201 }
202
203 #[test]
204 fn test_add_locale_table() {
205 let mut sys = new_localization("en-US");
206 add_locale_table(&mut sys, make_en_table());
207 assert_eq!(locale_count(&sys), 1);
208 }
209
210 #[test]
211 fn test_locale_count() {
212 let mut sys = new_localization("en-US");
213 add_locale_table(&mut sys, make_en_table());
214 add_locale_table(&mut sys, make_ja_table());
215 assert_eq!(locale_count(&sys), 2);
216 }
217
218 #[test]
219 fn test_key_count() {
220 let table = make_en_table();
221 assert_eq!(key_count(&table), 2);
222 }
223
224 #[test]
225 fn test_translate_known_key() {
226 let mut sys = new_localization("en-US");
227 add_locale_table(&mut sys, make_en_table());
228 assert_eq!(translate(&sys, "greeting"), "Hello");
229 }
230
231 #[test]
232 fn test_translate_unknown_falls_back_to_key() {
233 let mut sys = new_localization("en-US");
234 add_locale_table(&mut sys, make_en_table());
235 assert_eq!(translate(&sys, "unknown_key"), "unknown_key");
236 }
237
238 #[test]
239 fn test_translate_active_locale_priority() {
240 let mut sys = new_localization("en-US");
241 add_locale_table(&mut sys, make_en_table());
242 add_locale_table(&mut sys, make_ja_table());
243 set_active_locale(&mut sys, "ja-JP");
244 assert_eq!(translate(&sys, "greeting"), "こんにちは");
245 }
246
247 #[test]
248 fn test_translate_fallback_when_key_missing_in_active() {
249 let mut sys = new_localization("en-US");
250 add_locale_table(&mut sys, make_en_table());
251 add_locale_table(&mut sys, make_ja_table());
252 set_active_locale(&mut sys, "ja-JP");
253 assert_eq!(translate(&sys, "farewell"), "Goodbye");
255 }
256
257 #[test]
258 fn test_has_key_true() {
259 let mut sys = new_localization("en-US");
260 add_locale_table(&mut sys, make_en_table());
261 assert!(has_key(&sys, "en-US", "greeting"));
262 }
263
264 #[test]
265 fn test_has_key_false() {
266 let mut sys = new_localization("en-US");
267 add_locale_table(&mut sys, make_en_table());
268 assert!(!has_key(&sys, "en-US", "nonexistent"));
269 }
270
271 #[test]
272 fn test_export_locale_json_non_empty() {
273 let table = make_en_table();
274 let json = export_locale_json(&table);
275 assert!(!json.is_empty());
276 assert!(json.contains("greeting"));
277 assert!(json.contains("Hello"));
278 }
279
280 #[test]
281 fn test_import_locale_strings() {
282 let mut table = new_locale_table("en-US", "English");
283 let data = vec![
284 ("key1".to_string(), "val1".to_string()),
285 ("key2".to_string(), "val2".to_string()),
286 ];
287 import_locale_strings(&mut table, &data);
288 assert_eq!(key_count(&table), 2);
289 }
290
291 #[test]
292 fn test_missing_keys_detects_gap() {
293 let mut sys = new_localization("en-US");
294 add_locale_table(&mut sys, make_en_table());
295 add_locale_table(&mut sys, make_ja_table());
296 let missing = missing_keys(&sys, "en-US", "ja-JP");
297 assert!(missing.contains(&"farewell".to_string()));
298 assert!(!missing.contains(&"greeting".to_string()));
299 }
300
301 #[test]
302 fn test_missing_keys_empty_when_complete() {
303 let mut sys = new_localization("en-US");
304 add_locale_table(&mut sys, make_en_table());
305 let mut full = make_en_table();
306 full.locale_id = "fr-FR".to_string();
307 add_locale_table(&mut sys, full);
308 let missing = missing_keys(&sys, "en-US", "fr-FR");
309 assert!(missing.is_empty());
310 }
311
312 #[test]
313 fn test_translate_with_context() {
314 let mut sys = new_localization("en-US");
315 let mut table = new_locale_table("en-US", "English");
316 table.entries.push(LocaleString {
317 key: "action".to_string(),
318 value: "File (verb)".to_string(),
319 context: Some("verb".to_string()),
320 });
321 table.entries.push(LocaleString {
322 key: "action".to_string(),
323 value: "File (noun)".to_string(),
324 context: Some("noun".to_string()),
325 });
326 add_locale_table(&mut sys, table);
327 let v = translate_with_context(&sys, "action", "verb");
328 assert_eq!(v, "File (verb)");
329 }
330
331 #[test]
332 fn test_new_locale_table() {
333 let table = new_locale_table("de-DE", "German");
334 assert_eq!(table.locale_id, "de-DE");
335 assert_eq!(table.name, "German");
336 assert!(table.entries.is_empty());
337 }
338}