1use crate::core::prelude::*;
3use crate::ui::color::AppColor;
4use rust_embed::RustEmbed;
5use std::collections::HashMap;
6use std::sync::{Arc, RwLock};
7
8pub const DEFAULT_LANGUAGE: &str = "en";
9
10#[derive(Debug)]
11pub enum TranslationError {
12 InvalidLanguage(String),
13 LoadError(String),
14 KeyNotFound(String),
15}
16
17impl std::fmt::Display for TranslationError {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 Self::InvalidLanguage(lang) => write!(f, "Invalid language: {}", lang),
21 Self::LoadError(msg) => write!(f, "Load error: {}", msg),
22 Self::KeyNotFound(key) => write!(f, "Translation key not found: {}", key),
23 }
24 }
25}
26
27#[derive(RustEmbed)]
28#[folder = "src/i18n/langs/"]
29pub struct Langs;
30
31#[derive(Debug, Clone)]
32struct Entry {
33 text: String,
34 display: String,
35 category: String,
36}
37
38impl Entry {
39 fn format(&self, params: &[&str]) -> String {
40 params
41 .iter()
42 .enumerate()
43 .fold(self.text.clone(), |mut text, (i, param)| {
44 text = text.replace(&format!("{{{}}}", i), param);
45 if text.contains("{}") {
46 text = text.replacen("{}", param, 1);
47 }
48 text
49 })
50 }
51
52 fn color(&self) -> AppColor {
53 AppColor::from_any(&self.category)
54 }
55}
56
57struct I18nService {
58 language: String,
59 entries: HashMap<String, Entry>,
60 fallback: HashMap<String, Entry>,
61 cache: HashMap<String, (String, AppColor)>,
62 display_map: HashMap<String, String>,
63}
64
65impl I18nService {
66 fn new() -> Self {
67 Self {
68 language: DEFAULT_LANGUAGE.into(),
69 entries: HashMap::new(),
70 fallback: HashMap::new(),
71 cache: HashMap::new(),
72 display_map: HashMap::new(),
73 }
74 }
75
76 fn load_language(&mut self, lang: &str) -> Result<()> {
77 if !Self::available_languages()
79 .iter()
80 .any(|l| l.eq_ignore_ascii_case(lang))
81 {
82 return Err(AppError::Translation(TranslationError::InvalidLanguage(
83 lang.into(),
84 )));
85 }
86
87 self.entries = Self::load_entries(lang)?;
89 if lang != DEFAULT_LANGUAGE {
90 self.fallback = Self::load_entries(DEFAULT_LANGUAGE).unwrap_or_default();
91 }
92
93 self.build_display_map();
94 self.cache.clear();
95 self.language = lang.into();
96 Ok(())
97 }
98
99 fn build_display_map(&mut self) {
100 self.display_map.clear();
101 for entry in self.entries.values().chain(self.fallback.values()) {
102 self.display_map
103 .entry(entry.display.to_lowercase())
104 .or_insert_with(|| entry.category.clone());
105 }
106 }
107
108 fn load_entries(lang: &str) -> Result<HashMap<String, Entry>> {
109 let filename = format!("{}.json", lang.to_lowercase());
110 let content = Langs::get(&filename).ok_or_else(|| {
111 AppError::Translation(TranslationError::LoadError(format!(
112 "File not found: {}",
113 filename
114 )))
115 })?;
116
117 let content_str = std::str::from_utf8(content.data.as_ref())
118 .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
119
120 let raw: HashMap<String, String> = serde_json::from_str(content_str)
121 .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
122
123 Ok(raw
124 .iter()
125 .filter_map(|(key, value)| {
126 key.strip_suffix(".text").map(|base_key| {
127 let display = raw
128 .get(&format!("{}.display_text", base_key))
129 .unwrap_or(&base_key.to_uppercase())
130 .clone();
131 let category = raw
132 .get(&format!("{}.category", base_key))
133 .unwrap_or(&"info".to_string())
134 .clone();
135
136 (
137 base_key.into(),
138 Entry {
139 text: value.clone(),
140 display,
141 category,
142 },
143 )
144 })
145 })
146 .collect())
147 }
148
149 fn get_translation(&mut self, key: &str, params: &[&str]) -> (String, AppColor) {
150 let cache_key = if params.is_empty() {
152 key.into()
153 } else {
154 format!("{}:{}", key, params.join(":"))
155 };
156
157 if let Some(cached) = self.cache.get(&cache_key) {
159 return cached.clone();
160 }
161
162 let (text, color) = match self.entries.get(key).or_else(|| self.fallback.get(key)) {
164 Some(entry) => (entry.format(params), entry.color()),
165 None => (format!("Missing: {}", key), AppColor::from_any("warning")),
166 };
167
168 if self.cache.len() >= 1000 {
170 self.cache.clear();
171 }
172 self.cache.insert(cache_key, (text.clone(), color));
173 (text, color)
174 }
175
176 fn get_command_translation(&mut self, key: &str, params: &[&str]) -> String {
177 match self.entries.get(key).or_else(|| self.fallback.get(key)) {
178 Some(entry) => format!("[{}] {}", entry.display, entry.format(params)),
179 None => format!("[WARNING] Missing: {}", key),
180 }
181 }
182
183 fn get_category_for_display(&self, display: &str) -> String {
184 self.display_map
185 .get(&display.to_lowercase())
186 .cloned()
187 .unwrap_or_else(|| "info".into())
188 }
189
190 fn available_languages() -> Vec<String> {
191 Langs::iter()
192 .filter_map(|f| {
193 let filename = f.as_ref();
194 filename.strip_suffix(".json").map(|s| s.to_uppercase())
195 })
196 .collect()
197 }
198}
199
200static SERVICE: std::sync::LazyLock<Arc<RwLock<I18nService>>> =
202 std::sync::LazyLock::new(|| Arc::new(RwLock::new(I18nService::new())));
203
204pub async fn init() -> Result<()> {
206 set_language(DEFAULT_LANGUAGE)
207}
208
209pub fn set_language(lang: &str) -> Result<()> {
210 SERVICE.write().unwrap().load_language(lang)
211}
212
213pub fn get_translation(key: &str, params: &[&str]) -> String {
214 SERVICE.write().unwrap().get_translation(key, params).0
215}
216
217pub fn get_command_translation(key: &str, params: &[&str]) -> String {
218 SERVICE
219 .write()
220 .unwrap()
221 .get_command_translation(key, params)
222}
223
224pub fn get_color_category_for_display(display: &str) -> String {
225 SERVICE.read().unwrap().get_category_for_display(display)
226}
227
228pub fn get_current_language() -> String {
229 SERVICE.read().unwrap().language.to_uppercase()
230}
231
232pub fn get_available_languages() -> Vec<String> {
233 I18nService::available_languages()
234}
235
236pub fn has_translation(key: &str) -> bool {
237 let service = SERVICE.read().unwrap();
238 service.entries.contains_key(key) || service.fallback.contains_key(key)
239}
240
241pub fn clear_translation_cache() {
242 SERVICE.write().unwrap().cache.clear();
243}
244
245pub fn get_all_translation_keys() -> Vec<String> {
247 let service = SERVICE.read().unwrap();
248 let mut keys: Vec<String> = service
249 .entries
250 .keys()
251 .chain(service.fallback.keys())
252 .cloned()
253 .collect();
254 keys.sort_unstable();
255 keys.dedup();
256 keys
257}
258
259pub fn get_translation_stats() -> HashMap<String, usize> {
260 let service = SERVICE.read().unwrap();
261 [
262 ("total_keys", service.entries.len()),
263 ("fallback_keys", service.fallback.len()),
264 ("cached_entries", service.cache.len()),
265 ("display_mappings", service.display_map.len()),
266 ]
267 .into_iter()
268 .map(|(k, v)| (k.into(), v))
269 .collect()
270}
271
272pub fn get_missing_keys_report() -> Vec<String> {
273 Vec::new()
274}
275
276#[macro_export]
278macro_rules! t {
279 ($key:expr) => { $crate::i18n::get_translation($key, &[]) };
280 ($key:expr, $($arg:expr),+) => { $crate::i18n::get_translation($key, &[$($arg),+]) };
281}
282
283#[macro_export]
284macro_rules! tc {
285 ($key:expr) => { $crate::i18n::get_command_translation($key, &[]) };
286 ($key:expr, $($arg:expr),+) => { $crate::i18n::get_command_translation($key, &[$($arg),+]) };
287}