1use crate::core::prelude::*;
2use crate::ui::color::AppColor;
3use lazy_static::lazy_static;
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 let mut text = self.text.clone();
41 for (i, param) in params.iter().enumerate() {
42 text = text.replace(&format!("{{{}}}", i), param);
43 }
44 for param in params {
45 if text.contains("{}") {
46 text = text.replacen("{}", param, 1);
47 }
48 }
49 text
50 }
51
52 fn get_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_to_category: HashMap<String, String>,
63}
64
65impl I18nService {
66 fn new() -> Self {
67 Self {
68 language: DEFAULT_LANGUAGE.to_string(),
69 entries: HashMap::new(),
70 fallback: HashMap::new(),
71 cache: HashMap::new(),
72 display_to_category: HashMap::new(),
73 }
74 }
75
76 fn load_language(&mut self, lang: &str) -> Result<()> {
77 if !Self::get_available_languages()
78 .iter()
79 .any(|l| l.to_lowercase() == lang.to_lowercase())
80 {
81 return Err(AppError::Translation(TranslationError::InvalidLanguage(
82 lang.to_string(),
83 )));
84 }
85
86 self.entries = Self::load_entries(lang)?;
87
88 if lang != DEFAULT_LANGUAGE {
89 self.fallback = Self::load_entries(DEFAULT_LANGUAGE).unwrap_or_default();
90 }
91
92 self.build_display_to_category_mapping();
93 self.cache.clear();
94 self.language = lang.to_string();
95 Ok(())
96 }
97
98 fn build_display_to_category_mapping(&mut self) {
99 self.display_to_category.clear();
100
101 for entry in self.entries.values() {
102 let display_key = entry.display.to_lowercase();
103 self.display_to_category
104 .insert(display_key, entry.category.clone());
105 }
106
107 for entry in self.fallback.values() {
108 let display_key = entry.display.to_lowercase();
109 self.display_to_category
110 .entry(display_key)
111 .or_insert_with(|| entry.category.clone());
112 }
113 }
114
115 fn load_entries(lang: &str) -> Result<HashMap<String, Entry>> {
116 let filename = format!("{}.json", lang.to_lowercase());
117 let content = Langs::get(&filename).ok_or_else(|| {
118 AppError::Translation(TranslationError::LoadError(format!(
119 "File not found: {}",
120 filename
121 )))
122 })?;
123
124 let content_str = std::str::from_utf8(content.data.as_ref())
125 .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
126
127 let raw: HashMap<String, String> = serde_json::from_str(content_str)
128 .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
129
130 let mut entries = HashMap::new();
131
132 for (key, value) in raw.iter() {
133 if key.ends_with(".text") {
134 let base_key = &key[0..key.len() - 5];
135 let display = raw
136 .get(&format!("{}.display_text", base_key))
137 .unwrap_or(&base_key.to_uppercase())
138 .clone();
139 let category = raw
140 .get(&format!("{}.category", base_key))
141 .unwrap_or(&"info".to_string())
142 .clone();
143
144 entries.insert(
145 base_key.to_string(),
146 Entry {
147 text: value.clone(),
148 display,
149 category,
150 },
151 );
152 }
153 }
154
155 Ok(entries)
156 }
157
158 fn get_translation(&mut self, key: &str, params: &[&str]) -> (String, AppColor) {
159 let cache_key = if params.is_empty() {
160 key.to_string()
161 } else {
162 format!("{}:{}", key, params.join(":"))
163 };
164
165 if let Some(cached) = self.cache.get(&cache_key) {
166 return cached.clone();
167 }
168
169 let entry = self.entries.get(key).or_else(|| self.fallback.get(key));
170
171 let (text, color) = match entry {
172 Some(e) => (e.format(params), e.get_color()),
173 None => {
174 log::warn!("Missing key: {}", key);
175 (format!("Missing: {}", key), AppColor::from_any("warning"))
176 }
177 };
178
179 if self.cache.len() >= 1000 {
180 self.cache.clear();
181 }
182 self.cache.insert(cache_key, (text.clone(), color));
183
184 (text, color)
185 }
186
187 fn get_command_translation(&mut self, key: &str, params: &[&str]) -> String {
188 if let Some(entry) = self.entries.get(key).or_else(|| self.fallback.get(key)) {
189 format!("[{}] {}", entry.display, entry.format(params))
190 } else {
191 format!("[WARNING] Missing: {}", key)
192 }
193 }
194
195 fn get_category_for_display(&self, display_text: &str) -> String {
196 let display_lower = display_text.to_lowercase();
197
198 if let Some(category) = self.display_to_category.get(&display_lower) {
199 return category.clone();
200 }
201
202 "info".to_string()
203 }
204
205 fn get_available_languages() -> Vec<String> {
206 Langs::iter()
207 .filter_map(|f| f.as_ref().strip_suffix(".json").map(|s| s.to_uppercase()))
208 .collect()
209 }
210
211 fn clear_cache(&mut self) {
212 self.cache.clear();
213 }
214}
215
216lazy_static! {
217 static ref SERVICE: Arc<RwLock<I18nService>> = Arc::new(RwLock::new(I18nService::new()));
218}
219
220pub async fn init() -> Result<()> {
222 set_language(DEFAULT_LANGUAGE)
223}
224
225pub fn set_language(lang: &str) -> Result<()> {
226 SERVICE.write().unwrap().load_language(lang)
227}
228
229pub fn get_translation(key: &str, params: &[&str]) -> String {
230 SERVICE.write().unwrap().get_translation(key, params).0
231}
232
233pub fn get_command_translation(key: &str, params: &[&str]) -> String {
234 SERVICE
235 .write()
236 .unwrap()
237 .get_command_translation(key, params)
238}
239
240pub fn get_color_category_for_display(display_category: &str) -> String {
241 SERVICE
242 .read()
243 .unwrap()
244 .get_category_for_display(display_category)
245}
246
247pub fn get_current_language() -> String {
248 SERVICE.read().unwrap().language.to_uppercase()
249}
250
251pub fn get_available_languages() -> Vec<String> {
252 I18nService::get_available_languages()
253}
254
255pub fn has_translation(key: &str) -> bool {
256 let service = SERVICE.read().unwrap();
257 service.entries.contains_key(key) || service.fallback.contains_key(key)
258}
259
260pub fn clear_translation_cache() {
261 SERVICE.write().unwrap().clear_cache();
262}
263
264pub fn get_all_translation_keys() -> Vec<String> {
265 let service = SERVICE.read().unwrap();
266 let mut keys: Vec<String> = service.entries.keys().cloned().collect();
267 keys.extend(service.fallback.keys().cloned());
268 keys.sort();
269 keys.dedup();
270 keys
271}
272
273pub fn get_translation_stats() -> HashMap<String, usize> {
274 let service = SERVICE.read().unwrap();
275 let mut stats = HashMap::new();
276 stats.insert("total_keys".to_string(), service.entries.len());
277 stats.insert("fallback_keys".to_string(), service.fallback.len());
278 stats.insert("cached_entries".to_string(), service.cache.len());
279 stats.insert(
280 "display_mappings".to_string(),
281 service.display_to_category.len(),
282 );
283 stats
284}
285
286pub fn get_missing_keys_report() -> Vec<String> {
287 Vec::new()
288}
289
290#[macro_export]
291macro_rules! t {
292 ($key:expr) => {
293 $crate::i18n::get_translation($key, &[])
294 };
295 ($key:expr, $($arg:expr),+) => {
296 $crate::i18n::get_translation($key, &[$($arg),+])
297 };
298}
299
300#[macro_export]
301macro_rules! tc {
302 ($key:expr) => {
303 $crate::i18n::get_command_translation($key, &[])
304 };
305 ($key:expr, $($arg:expr),+) => {
306 $crate::i18n::get_command_translation($key, &[$($arg),+])
307 };
308}