rush_sync_server/i18n/
mod.rs

1// ## FILE: src/i18n/mod.rs - KOMPRIMIERTE VERSION
2use 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        // Validate
78        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        // Load entries
88        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        // Cache key
151        let cache_key = if params.is_empty() {
152            key.into()
153        } else {
154            format!("{}:{}", key, params.join(":"))
155        };
156
157        // Check cache
158        if let Some(cached) = self.cache.get(&cache_key) {
159            return cached.clone();
160        }
161
162        // Get entry
163        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        // Cache with size limit
169        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
200// ✅ KOMPRIMIERTE SINGLETON
201static SERVICE: std::sync::LazyLock<Arc<RwLock<I18nService>>> =
202    std::sync::LazyLock::new(|| Arc::new(RwLock::new(I18nService::new())));
203
204// ✅ KOMPRIMIERTE PUBLIC API
205pub 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
245// ✅ KOMPRIMIERTE STATS & DEBUG
246pub 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// ✅ MACROS (unverändert)
277#[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}