rustbasic_translatable/
translator.rs1use std::collections::HashMap;
2use std::sync::RwLock;
3use std::fs;
4use std::path::Path;
5use std::sync::LazyLock;
6use rustbasic_core::serde_json;
7
8pub struct Translator {
10 translations: RwLock<HashMap<String, HashMap<String, String>>>,
11 default_locale: RwLock<String>,
12}
13
14pub static TRANSLATOR: LazyLock<Translator> = LazyLock::new(|| Translator {
16 translations: RwLock::new(HashMap::new()),
17 default_locale: RwLock::new("id".to_string()),
18});
19
20impl Translator {
21 pub fn init<P: AsRef<Path>>(&self, lang_dir: P) -> Result<(), String> {
23 let path = lang_dir.as_ref();
24 if !path.exists() || !path.is_dir() {
25 return Err(format!("Direktori bahasa tidak ditemukan atau bukan folder: {:?}", path));
26 }
27
28 let mut all_translations = HashMap::new();
29
30 let entries = fs::read_dir(path)
31 .map_err(|e| format!("Gagal membaca direktori bahasa: {}", e))?;
32
33 for entry in entries.filter_map(|e| e.ok()) {
34 let file_path = entry.path();
35 if file_path.is_file() && file_path.extension().and_then(|s| s.to_str()) == Some("json") {
36 let locale = file_path
37 .file_stem()
38 .and_then(|s| s.to_str())
39 .ok_or_else(|| format!("Nama file tidak valid: {:?}", file_path))?
40 .to_string();
41
42 let content = fs::read_to_string(&file_path)
43 .map_err(|e| format!("Gagal membaca file {:?}: {}", file_path, e))?;
44
45 let json_value: serde_json::Value = serde_json::from_str(&content)
46 .map_err(|e| format!("Gagal mem-parse JSON di {:?}: {}", file_path, e))?;
47
48 let mut flat_map = HashMap::new();
49 flatten_json("", &json_value, &mut flat_map);
50
51 all_translations.insert(locale, flat_map);
52 }
53 }
54
55 let mut write_lock = self.translations.write().unwrap();
56 *write_lock = all_translations;
57
58 Ok(())
59 }
60
61 pub fn set_default_locale(&self, locale: &str) {
63 let mut default_lock = self.default_locale.write().unwrap();
64 *default_lock = locale.to_string();
65 }
66
67 pub fn get_default_locale(&self) -> String {
69 let default_lock = self.default_locale.read().unwrap();
70 default_lock.clone()
71 }
72
73 pub fn trans(&self, key: &str, locale: &str) -> String {
75 let translations = self.translations.read().unwrap();
76
77 if let Some(val) = translations.get(locale).and_then(|m| m.get(key)) {
79 return val.clone();
80 }
81
82 let default_loc = self.get_default_locale();
84 if locale != default_loc {
85 let opt = translations.get(&default_loc).and_then(|m| m.get(key));
86 if let Some(val) = opt {
87 return val.clone();
88 }
89 }
90
91 key.to_string()
93 }
94
95 pub fn trans_with(&self, key: &str, locale: &str, params: &[(&str, &str)]) -> String {
97 let mut translated = self.trans(key, locale);
98 for &(param, val) in params {
99 let placeholder = format!("{{{}}}", param);
100 translated = translated.replace(&placeholder, val);
101 }
102 translated
103 }
104}
105
106fn flatten_json(prefix: &str, value: &serde_json::Value, flat_map: &mut HashMap<String, String>) {
108 match value {
109 serde_json::Value::Object(obj) => {
110 for (k, v) in obj {
111 let new_prefix = if prefix.is_empty() {
112 k.clone()
113 } else {
114 format!("{}.{}", prefix, k)
115 };
116 flatten_json(&new_prefix, v, flat_map);
117 }
118 }
119 serde_json::Value::String(s) => {
120 flat_map.insert(prefix.to_string(), s.clone());
121 }
122 serde_json::Value::Number(n) => {
123 flat_map.insert(prefix.to_string(), n.to_string());
124 }
125 serde_json::Value::Bool(b) => {
126 flat_map.insert(prefix.to_string(), b.to_string());
127 }
128 serde_json::Value::Null => {
129 flat_map.insert(prefix.to_string(), String::new());
130 }
131 serde_json::Value::Array(arr) => {
132 flat_map.insert(prefix.to_string(), serde_json::Value::Array(arr.clone()).to_string());
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use std::fs::File;
141 use std::io::Write;
142
143 #[test]
144 fn test_flatten_json() {
145 let json_str = r#"{
146 "welcome": "Selamat datang",
147 "nested": {
148 "level1": {
149 "level2": "Sukses"
150 }
151 }
152 }"#;
153
154 let val: serde_json::Value = serde_json::from_str(json_str).unwrap();
155 let mut map = HashMap::new();
156 flatten_json("", &val, &mut map);
157
158 assert_eq!(map.get("welcome").unwrap(), "Selamat datang");
159 assert_eq!(map.get("nested.level1.level2").unwrap(), "Sukses");
160 }
161
162 #[test]
163 fn test_translator_flow() {
164 let temp_dir = std::env::temp_dir().join("lang_test_dir");
166 fs::create_dir_all(&temp_dir).ok();
167
168 let mut id_file = File::create(temp_dir.join("id.json")).unwrap();
170 id_file.write_all(r#"{
171 "welcome": "Selamat datang!",
172 "greet": "Halo, {name}!",
173 "auth": {
174 "failed": "Gagal masuk."
175 }
176 }"#.as_bytes()).unwrap();
177
178 let mut en_file = File::create(temp_dir.join("en.json")).unwrap();
180 en_file.write_all(r#"{
181 "welcome": "Welcome!",
182 "greet": "Hello, {name}!"
183 }"#.as_bytes()).unwrap();
184
185 let translator = Translator {
187 translations: RwLock::new(HashMap::new()),
188 default_locale: RwLock::new("id".to_string()),
189 };
190 translator.init(&temp_dir).unwrap();
191
192 assert_eq!(translator.trans("welcome", "id"), "Selamat datang!");
194 assert_eq!(translator.trans("welcome", "en"), "Welcome!");
195
196 assert_eq!(translator.trans("auth.failed", "id"), "Gagal masuk.");
198
199 assert_eq!(translator.trans("auth.failed", "en"), "Gagal masuk.");
201
202 assert_eq!(translator.trans("missing.key", "en"), "missing.key");
204
205 assert_eq!(translator.trans_with("greet", "id", &[("name", "Hendra")]), "Halo, Hendra!");
207 assert_eq!(translator.trans_with("greet", "en", &[("name", "John")]), "Hello, John!");
208
209 fs::remove_dir_all(&temp_dir).ok();
211 }
212}