rush_sync_server/i18n/
mod.rs

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
226// Öffentliche API
227pub 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}