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