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::{Mutex, RwLock};
7
8pub const DEFAULT_LANGUAGE: &str = "en";
9
10#[derive(Debug)]
11pub enum TranslationError {
12    InvalidLanguage(String),
13    LoadError(String),
14    ConfigError(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, "Ungültige Sprache: {}", lang),
21            Self::LoadError(msg) => write!(f, "Ladefehler: {}", msg),
22            Self::ConfigError(msg) => write!(f, "Konfigurationsfehler: {}", msg),
23        }
24    }
25}
26
27#[derive(RustEmbed)]
28#[folder = "src/i18n/langs/"]
29pub struct Langs;
30
31fn get_language_file(lang: &str) -> Option<String> {
32    let filename = format!("{}.json", lang.to_lowercase());
33    Langs::get(&filename).and_then(|file| {
34        std::str::from_utf8(file.data.as_ref())
35            .ok()
36            .map(|s| s.to_owned())
37    })
38}
39
40#[derive(Debug, Clone)]
41pub struct TranslationEntry {
42    pub text: String,
43    pub color_category: String,
44    pub display_category: String,
45}
46
47impl TranslationEntry {
48    pub fn get_color(&self) -> AppColor {
49        AppColor::from_any(&self.color_category)
50    }
51
52    pub fn format(&self, params: &[&str]) -> (String, AppColor) {
53        let mut text = self.text.clone();
54        for param in params {
55            text = text.replacen("{}", param, 1);
56        }
57        (text, self.get_color())
58    }
59
60    pub fn format_for_command(&self, params: &[&str]) -> String {
61        let (text, _) = self.format(params);
62        format!("[{}] {}", self.display_category.to_uppercase(), text)
63    }
64}
65
66#[derive(Debug, Clone, Default)]
67struct TranslationConfig {
68    entries: HashMap<String, TranslationEntry>,
69    // ✅ GLOBALES Display-Mapping - wird ERWEITERT statt ersetzt
70    global_display_to_color_map: HashMap<String, String>,
71}
72
73impl TranslationConfig {
74    fn load(lang: &str) -> Result<Self> {
75        let translation_str = get_language_file(lang).ok_or_else(|| {
76            AppError::Translation(TranslationError::LoadError(format!(
77                "Language file for '{}' not found",
78                lang
79            )))
80        })?;
81
82        let raw_entries: HashMap<String, String> =
83            serde_json::from_str(&translation_str).map_err(|e| {
84                AppError::Translation(TranslationError::LoadError(format!(
85                    "Error parsing language file: {}",
86                    e
87                )))
88            })?;
89
90        let mut entries = HashMap::new();
91        let mut new_display_mappings = HashMap::new();
92
93        for (key, value) in raw_entries.iter() {
94            if key.ends_with(".text") {
95                let base_key = &key[0..key.len() - 5];
96                let color_category = raw_entries
97                    .get(&format!("{}.category", base_key))
98                    .cloned()
99                    .unwrap_or_else(|| "info".to_string());
100                let display_category = raw_entries
101                    .get(&format!("{}.display_category", base_key))
102                    .cloned()
103                    .unwrap_or_else(|| color_category.clone());
104
105                entries.insert(
106                    base_key.to_string(),
107                    TranslationEntry {
108                        text: value.clone(),
109                        color_category: color_category.clone(),
110                        display_category: display_category.clone(),
111                    },
112                );
113
114                new_display_mappings.insert(
115                    display_category.to_lowercase(),
116                    color_category.to_lowercase(),
117                );
118            }
119        }
120
121        Ok(Self {
122            entries,
123            global_display_to_color_map: new_display_mappings,
124        })
125    }
126
127    fn get_entry(&self, key: &str) -> Option<&TranslationEntry> {
128        self.entries.get(key)
129    }
130
131    fn get_color_category_for_display(&self, display_category: &str) -> String {
132        self.global_display_to_color_map
133            .get(&display_category.to_lowercase())
134            .cloned()
135            .unwrap_or_else(|| {
136                // ✅ SMART FALLBACK: Versuche ähnliche Display-Categories zu finden
137                self.find_similar_color_category(display_category)
138            })
139    }
140
141    // ✅ SMART FALLBACK: Finde ähnliche Color-Categories
142    fn find_similar_color_category(&self, display_category: &str) -> String {
143        let display_lower = display_category.to_lowercase();
144
145        // Versuche bekannte Patterns
146        if display_lower.contains("error") || display_lower.contains("fehler") {
147            return "error".to_string();
148        }
149        if display_lower.contains("warn") || display_lower.contains("warnung") {
150            return "warning".to_string();
151        }
152        if display_lower.contains("info") {
153            return "info".to_string();
154        }
155        if display_lower.contains("debug") {
156            return "debug".to_string();
157        }
158        if display_lower.contains("lang")
159            || display_lower.contains("sprache")
160            || display_lower.contains("language")
161        {
162            return "lang".to_string();
163        }
164        if display_lower.contains("version") {
165            return "version".to_string();
166        }
167
168        // Fallback
169        "info".to_string()
170    }
171
172    // ✅ MERGE neue Display-Mappings mit bestehenden
173    fn merge_display_mappings(&mut self, new_mappings: HashMap<String, String>) {
174        for (display, color) in new_mappings {
175            self.global_display_to_color_map.insert(display, color);
176        }
177    }
178}
179
180struct TranslationService {
181    current_language: String,
182    config: TranslationConfig,
183    cache: Mutex<HashMap<String, (String, AppColor)>>,
184}
185
186impl TranslationService {
187    fn new() -> Self {
188        Self {
189            current_language: DEFAULT_LANGUAGE.to_string(),
190            config: TranslationConfig::default(),
191            cache: Mutex::new(HashMap::new()),
192        }
193    }
194
195    fn get_translation_readonly(&self, key: &str, params: &[&str]) -> (String, AppColor) {
196        let cache_key = if params.is_empty() {
197            key.to_string()
198        } else {
199            format!("{}:{}", key, params.join(":"))
200        };
201
202        if let Ok(cache) = self.cache.lock() {
203            if let Some(cached) = cache.get(&cache_key) {
204                return cached.clone();
205            }
206        }
207
208        let (text, color) = if let Some(entry) = self.config.get_entry(key) {
209            entry.format(params)
210        } else {
211            (
212                format!("⚠️ Translation key not found: {}", key),
213                AppColor::from_any("warning"),
214            )
215        };
216
217        if let Ok(mut cache) = self.cache.lock() {
218            if cache.len() >= 1000 {
219                cache.clear();
220            }
221            cache.insert(cache_key, (text.clone(), color));
222        }
223
224        (text, color)
225    }
226
227    fn clear_cache(&self) {
228        if let Ok(mut cache) = self.cache.lock() {
229            cache.clear();
230        }
231    }
232
233    // ✅ NEUE METHODE: Merge Display-Mappings statt ersetzen
234    fn update_language(&mut self, new_config: TranslationConfig) {
235        // ✅ MERGE alte + neue Display-Mappings
236        self.config
237            .merge_display_mappings(new_config.global_display_to_color_map.clone());
238
239        // Update entries
240        self.config.entries = new_config.entries;
241
242        // NUR Text-Cache leeren, Display-Mapping bleibt
243        self.clear_cache();
244    }
245}
246
247lazy_static! {
248    static ref INSTANCE: RwLock<TranslationService> = RwLock::new(TranslationService::new());
249}
250
251pub async fn init() -> Result<()> {
252    set_language_internal(DEFAULT_LANGUAGE, false)
253}
254
255pub fn set_language(lang: &str) -> Result<()> {
256    set_language_internal(lang, true)
257}
258
259fn set_language_internal(lang: &str, _save_config: bool) -> Result<()> {
260    let lang = lang.to_lowercase();
261
262    if !get_available_languages()
263        .iter()
264        .any(|l| l.to_lowercase() == lang)
265    {
266        return Err(AppError::Translation(TranslationError::InvalidLanguage(
267            lang.to_uppercase(),
268        )));
269    }
270
271    let config = TranslationConfig::load(&lang).unwrap_or_default();
272    let mut service = INSTANCE.write().unwrap();
273    service.current_language = lang;
274
275    // ✅ SMART UPDATE: Merge statt Replace
276    service.update_language(config);
277
278    Ok(())
279}
280
281pub fn get_translation(key: &str, params: &[&str]) -> String {
282    INSTANCE
283        .read()
284        .unwrap()
285        .get_translation_readonly(key, params)
286        .0
287}
288
289pub fn get_command_translation(key: &str, params: &[&str]) -> String {
290    let service = INSTANCE.read().unwrap();
291    if let Some(entry) = service.config.get_entry(key) {
292        entry.format_for_command(params)
293    } else {
294        format!("[WARNING] ⚠️ Translation key not found: {}", key)
295    }
296}
297
298pub fn get_current_language() -> String {
299    INSTANCE.read().unwrap().current_language.to_uppercase()
300}
301
302pub fn get_available_languages() -> Vec<String> {
303    Langs::iter()
304        .filter_map(|f| f.as_ref().strip_suffix(".json").map(|s| s.to_uppercase()))
305        .collect()
306}
307
308pub fn get_color_category_for_display(display_category: &str) -> String {
309    INSTANCE
310        .read()
311        .unwrap()
312        .config
313        .get_color_category_for_display(display_category)
314}
315
316pub fn clear_translation_cache() {
317    INSTANCE.read().unwrap().clear_cache();
318}