1use once_cell::sync::Lazy;
7use serde_json::Value;
8use std::collections::HashMap;
9use std::sync::RwLock;
10
11const EMBEDDED_LOCALES: &[(&str, &str)] = &[
13 (
14 "cs",
15 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/cs.json")),
16 ),
17 (
18 "de",
19 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/de.json")),
20 ),
21 (
22 "en",
23 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/en.json")),
24 ),
25 (
26 "es",
27 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/es.json")),
28 ),
29 (
30 "fr",
31 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/fr.json")),
32 ),
33 (
34 "it",
35 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/it.json")),
36 ),
37 (
38 "ja",
39 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/ja.json")),
40 ),
41 (
42 "ko",
43 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/ko.json")),
44 ),
45 (
46 "pt-BR",
47 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/pt-BR.json")),
48 ),
49 (
50 "ru",
51 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/ru.json")),
52 ),
53 (
54 "th",
55 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/th.json")),
56 ),
57 (
58 "uk",
59 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/uk.json")),
60 ),
61 (
62 "vi",
63 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/vi.json")),
64 ),
65 (
66 "zh-CN",
67 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/zh-CN.json")),
68 ),
69];
70
71static TRANSLATIONS: Lazy<RwLock<HashMap<String, HashMap<&'static str, &'static str>>>> =
74 Lazy::new(|| RwLock::new(HashMap::new()));
75
76fn parse_locale(json_str: &str) -> HashMap<&'static str, &'static str> {
78 let value: Value = serde_json::from_str(json_str).expect("Valid JSON");
79 let mut flat = HashMap::new();
80 flatten_json(&value, String::new(), &mut flat);
81 flat
82}
83
84fn flatten_json(value: &Value, prefix: String, output: &mut HashMap<&'static str, &'static str>) {
86 match value {
87 Value::Object(map) => {
88 for (key, val) in map {
89 if key.starts_with('_') {
90 continue; }
92 let new_prefix = if prefix.is_empty() {
93 key.clone()
94 } else {
95 format!("{}.{}", prefix, key)
96 };
97 flatten_json(val, new_prefix, output);
98 }
99 }
100 Value::String(s) => {
101 let key_static: &'static str = Box::leak(prefix.into_boxed_str());
107 let val_static: &'static str = Box::leak(s.clone().into_boxed_str());
108 output.insert(key_static, val_static);
109 }
110 _ => {}
111 }
112}
113
114fn ensure_loaded(locale: &str) {
116 if TRANSLATIONS.read().unwrap().contains_key(locale) {
117 return;
118 }
119
120 let mut translations = TRANSLATIONS.write().unwrap();
121 if translations.contains_key(locale) {
123 return;
124 }
125
126 if let Some((_, json)) = EMBEDDED_LOCALES.iter().find(|(l, _)| *l == locale) {
127 translations.insert(locale.to_string(), parse_locale(json));
128 }
129}
130
131pub struct RuntimeBackend;
133
134impl RuntimeBackend {
135 pub fn new() -> Self {
136 Self
137 }
138}
139
140impl rust_i18n::Backend for RuntimeBackend {
141 fn available_locales(&self) -> Vec<&str> {
142 EMBEDDED_LOCALES.iter().map(|(l, _)| *l).collect()
143 }
144
145 fn translate(&self, locale: &str, key: &str) -> Option<&str> {
146 ensure_loaded(locale);
147 let translations = TRANSLATIONS.read().unwrap();
148 translations.get(locale)?.get(key).copied()
149 }
150}
151
152impl rust_i18n::BackendExt for RuntimeBackend {}
153
154impl Default for RuntimeBackend {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use rust_i18n::Backend;
164
165 #[test]
166 fn test_parse_all_locales() {
167 for (locale, json) in EMBEDDED_LOCALES {
168 let parsed = parse_locale(json);
169 assert!(
170 !parsed.is_empty(),
171 "Locale {} should have translations",
172 locale
173 );
174 }
175 }
176
177 #[test]
178 fn test_flatten_nested_json() {
179 let json = r#"{
180 "action": {
181 "copy": "Copy",
182 "paste": "Paste"
183 },
184 "simple": "value"
185 }"#;
186 let parsed = parse_locale(json);
187 assert_eq!(parsed.get("action.copy").copied(), Some("Copy"));
188 assert_eq!(parsed.get("action.paste").copied(), Some("Paste"));
189 assert_eq!(parsed.get("simple").copied(), Some("value"));
190 }
191
192 #[test]
193 fn test_skip_metadata_keys() {
194 let json = r#"{
195 "_version": "1.0",
196 "key": "value"
197 }"#;
198 let parsed = parse_locale(json);
199 assert!(!parsed.contains_key("_version"));
200 assert_eq!(parsed.get("key").copied(), Some("value"));
201 }
202
203 #[test]
204 fn test_backend_available_locales() {
205 let backend = RuntimeBackend::new();
206 let locales = backend.available_locales();
207 assert_eq!(locales.len(), 14);
208 assert!(locales.iter().any(|l| *l == "en"));
209 assert!(locales.iter().any(|l| *l == "es"));
210 }
211
212 #[test]
213 fn test_backend_translate() {
214 let backend = RuntimeBackend::new();
215
216 let result = backend.translate("en", "action.copy");
218 assert!(result.is_some());
219
220 let result = backend.translate("en", "nonexistent.key");
222 assert!(result.is_none());
223 }
224
225 #[test]
226 fn test_lazy_loading() {
227 let backend = RuntimeBackend::new();
228
229 backend.translate("en", "action.copy");
231 assert!(TRANSLATIONS.read().unwrap().contains_key("en"));
232
233 backend.translate("en", "action.paste");
235 }
236}