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 AVAILABLE_LANGUAGES: &[&str] = &["de", "en"];
9pub const DEFAULT_LANGUAGE: &str = "en";
10
11#[derive(Debug)]
12pub enum TranslationError {
13    InvalidLanguage(String),
14    LoadError(String),
15    ConfigError(String),
16}
17
18impl std::fmt::Display for TranslationError {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            Self::InvalidLanguage(lang) => write!(f, "Ungültige Sprache: {}", lang),
22            Self::LoadError(msg) => write!(f, "Ladefehler: {}", msg),
23            Self::ConfigError(msg) => write!(f, "Konfigurationsfehler: {}", msg),
24        }
25    }
26}
27
28#[derive(RustEmbed)]
29#[folder = "src/i18n/langs/"]
30pub struct Langs;
31
32fn get_language_file(lang: &str) -> Option<&'static str> {
33    match lang {
34        "de" => Some(include_str!("langs/de.json")),
35        "en" => Some(include_str!("langs/en.json")),
36        _ => None,
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                // ✅ SAMMLE neue Display-Mappings
115                new_display_mappings.insert(
116                    display_category.to_lowercase(),
117                    color_category.to_lowercase(),
118                );
119            }
120        }
121
122        Ok(Self {
123            entries,
124            global_display_to_color_map: new_display_mappings,
125        })
126    }
127
128    fn get_entry(&self, key: &str) -> Option<&TranslationEntry> {
129        self.entries.get(key)
130    }
131
132    fn get_color_category_for_display(&self, display_category: &str) -> String {
133        self.global_display_to_color_map
134            .get(&display_category.to_lowercase())
135            .cloned()
136            .unwrap_or_else(|| {
137                // ✅ SMART FALLBACK: Versuche ähnliche Display-Categories zu finden
138                self.find_similar_color_category(display_category)
139            })
140    }
141
142    // ✅ SMART FALLBACK: Finde ähnliche Color-Categories
143    fn find_similar_color_category(&self, display_category: &str) -> String {
144        let display_lower = display_category.to_lowercase();
145
146        // Versuche bekannte Patterns
147        if display_lower.contains("error") || display_lower.contains("fehler") {
148            return "error".to_string();
149        }
150        if display_lower.contains("warn") || display_lower.contains("warnung") {
151            return "warning".to_string();
152        }
153        if display_lower.contains("info") {
154            return "info".to_string();
155        }
156        if display_lower.contains("debug") {
157            return "debug".to_string();
158        }
159        if display_lower.contains("lang")
160            || display_lower.contains("sprache")
161            || display_lower.contains("language")
162        {
163            return "lang".to_string();
164        }
165        if display_lower.contains("version") {
166            return "version".to_string();
167        }
168
169        // Fallback
170        "info".to_string()
171    }
172
173    // ✅ MERGE neue Display-Mappings mit bestehenden
174    fn merge_display_mappings(&mut self, new_mappings: HashMap<String, String>) {
175        for (display, color) in new_mappings {
176            self.global_display_to_color_map.insert(display, color);
177        }
178    }
179}
180
181struct TranslationService {
182    current_language: String,
183    config: TranslationConfig,
184    cache: Mutex<HashMap<String, (String, AppColor)>>,
185}
186
187impl TranslationService {
188    fn new() -> Self {
189        Self {
190            current_language: DEFAULT_LANGUAGE.to_string(),
191            config: TranslationConfig::default(),
192            cache: Mutex::new(HashMap::new()),
193        }
194    }
195
196    fn get_translation_readonly(&self, key: &str, params: &[&str]) -> (String, AppColor) {
197        let cache_key = if params.is_empty() {
198            key.to_string()
199        } else {
200            format!("{}:{}", key, params.join(":"))
201        };
202
203        if let Ok(cache) = self.cache.lock() {
204            if let Some(cached) = cache.get(&cache_key) {
205                return cached.clone();
206            }
207        }
208
209        let (text, color) = if let Some(entry) = self.config.get_entry(key) {
210            entry.format(params)
211        } else {
212            (
213                format!("⚠️ Translation key not found: {}", key),
214                AppColor::from_any("warning"),
215            )
216        };
217
218        if let Ok(mut cache) = self.cache.lock() {
219            if cache.len() >= 1000 {
220                cache.clear();
221            }
222            cache.insert(cache_key, (text.clone(), color));
223        }
224
225        (text, color)
226    }
227
228    fn clear_cache(&self) {
229        if let Ok(mut cache) = self.cache.lock() {
230            cache.clear();
231        }
232    }
233
234    // ✅ NEUE METHODE: Merge Display-Mappings statt ersetzen
235    fn update_language(&mut self, new_config: TranslationConfig) {
236        // ✅ MERGE alte + neue Display-Mappings
237        self.config
238            .merge_display_mappings(new_config.global_display_to_color_map.clone());
239
240        // Update entries
241        self.config.entries = new_config.entries;
242
243        // NUR Text-Cache leeren, Display-Mapping bleibt
244        self.clear_cache();
245    }
246}
247
248lazy_static! {
249    static ref INSTANCE: RwLock<TranslationService> = RwLock::new(TranslationService::new());
250}
251
252pub async fn init() -> Result<()> {
253    set_language_internal(DEFAULT_LANGUAGE, false)
254}
255
256pub fn set_language(lang: &str) -> Result<()> {
257    set_language_internal(lang, true)
258}
259
260fn set_language_internal(lang: &str, _save_config: bool) -> Result<()> {
261    let lang = lang.to_lowercase();
262    if !AVAILABLE_LANGUAGES.iter().any(|&l| l == lang) {
263        return Err(AppError::Translation(TranslationError::InvalidLanguage(
264            lang.to_uppercase(),
265        )));
266    }
267
268    let config = TranslationConfig::load(&lang).unwrap_or_default();
269    let mut service = INSTANCE.write().unwrap();
270    service.current_language = lang;
271
272    // ✅ SMART UPDATE: Merge statt Replace
273    service.update_language(config);
274
275    Ok(())
276}
277
278pub fn get_translation(key: &str, params: &[&str]) -> String {
279    INSTANCE
280        .read()
281        .unwrap()
282        .get_translation_readonly(key, params)
283        .0
284}
285
286pub fn get_command_translation(key: &str, params: &[&str]) -> String {
287    let service = INSTANCE.read().unwrap();
288    if let Some(entry) = service.config.get_entry(key) {
289        entry.format_for_command(params)
290    } else {
291        format!("[WARNING] ⚠️ Translation key not found: {}", key)
292    }
293}
294
295pub fn get_current_language() -> String {
296    INSTANCE.read().unwrap().current_language.to_uppercase()
297}
298
299pub fn get_available_languages() -> Vec<String> {
300    AVAILABLE_LANGUAGES
301        .iter()
302        .map(|&s| s.to_uppercase())
303        .collect()
304}
305
306pub fn get_color_category_for_display(display_category: &str) -> String {
307    INSTANCE
308        .read()
309        .unwrap()
310        .config
311        .get_color_category_for_display(display_category)
312}
313
314pub fn clear_translation_cache() {
315    INSTANCE.read().unwrap().clear_cache();
316}