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