rush_sync_server/i18n/
mod.rs

1// ## FILE: src/i18n/mod.rs - SUPER VEREINFACHT!
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}
15
16impl std::fmt::Display for TranslationError {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match self {
19            Self::InvalidLanguage(lang) => write!(f, "Invalid language: {}", lang),
20            Self::LoadError(msg) => write!(f, "Load error: {}", msg),
21        }
22    }
23}
24
25#[derive(RustEmbed)]
26#[folder = "src/i18n/langs/"]
27pub struct Langs;
28
29#[derive(Debug, Clone)]
30struct Entry {
31    text: String,
32    display: String,
33    category: String,
34}
35
36impl Entry {
37    fn format(&self, params: &[&str]) -> String {
38        params
39            .iter()
40            .enumerate()
41            .fold(self.text.clone(), |mut text, (i, param)| {
42                text = text.replace(&format!("{{{}}}", i), param);
43                if text.contains("{}") {
44                    text = text.replacen("{}", param, 1);
45                }
46                text
47            })
48    }
49}
50
51struct I18nService {
52    language: String,
53    entries: HashMap<String, Entry>,
54    fallback: HashMap<String, Entry>,
55    cache: HashMap<String, String>,
56}
57
58impl I18nService {
59    fn new() -> Self {
60        Self {
61            language: DEFAULT_LANGUAGE.into(),
62            entries: HashMap::new(),
63            fallback: HashMap::new(),
64            cache: HashMap::new(),
65        }
66    }
67
68    fn load_language(&mut self, lang: &str) -> Result<()> {
69        // Validate
70        if !Self::available_languages()
71            .iter()
72            .any(|l| l.eq_ignore_ascii_case(lang))
73        {
74            return Err(AppError::Translation(TranslationError::InvalidLanguage(
75                lang.into(),
76            )));
77        }
78
79        // Load entries
80        self.entries = Self::load_entries(lang)?;
81
82        // Load fallback (andere Sprachen)
83        self.fallback.clear();
84        for available_lang in Self::available_languages() {
85            if available_lang.to_lowercase() != lang.to_lowercase() {
86                if let Ok(other_entries) = Self::load_entries(&available_lang.to_lowercase()) {
87                    for (key, entry) in other_entries {
88                        self.fallback.entry(key).or_insert(entry);
89                    }
90                }
91            }
92        }
93
94        self.cache.clear();
95        self.language = lang.into();
96        Ok(())
97    }
98
99    fn load_entries(lang: &str) -> Result<HashMap<String, Entry>> {
100        let filename = format!("{}.json", lang.to_lowercase());
101        let content = Langs::get(&filename).ok_or_else(|| {
102            AppError::Translation(TranslationError::LoadError(format!(
103                "File not found: {}",
104                filename
105            )))
106        })?;
107
108        let content_str = std::str::from_utf8(content.data.as_ref())
109            .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
110
111        let raw: HashMap<String, String> = serde_json::from_str(content_str)
112            .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
113
114        Ok(raw
115            .iter()
116            .filter_map(|(key, value)| {
117                key.strip_suffix(".text").map(|base_key| {
118                    let display = raw
119                        .get(&format!("{}.display_text", base_key))
120                        .unwrap_or(&base_key.to_uppercase())
121                        .clone();
122                    let category = raw
123                        .get(&format!("{}.category", base_key))
124                        .unwrap_or(&"info".to_string())
125                        .clone();
126
127                    (
128                        base_key.into(),
129                        Entry {
130                            text: value.clone(),
131                            display,
132                            category,
133                        },
134                    )
135                })
136            })
137            .collect())
138    }
139
140    fn get_translation(&mut self, key: &str, params: &[&str]) -> String {
141        // Cache check
142        let cache_key = if params.is_empty() {
143            key.into()
144        } else {
145            format!("{}:{}", key, params.join(":"))
146        };
147
148        if let Some(cached) = self.cache.get(&cache_key) {
149            return cached.clone();
150        }
151
152        // Get entry
153        let text = match self.entries.get(key).or_else(|| self.fallback.get(key)) {
154            Some(entry) => entry.format(params),
155            None => format!("Missing: {}", key),
156        };
157
158        // Cache with size limit
159        if self.cache.len() >= 1000 {
160            self.cache.clear();
161        }
162        self.cache.insert(cache_key, text.clone());
163        text
164    }
165
166    fn get_command_translation(&mut self, key: &str, params: &[&str]) -> String {
167        match self.entries.get(key).or_else(|| self.fallback.get(key)) {
168            Some(entry) => format!("[{}] {}", entry.display, entry.format(params)),
169            None => format!("[WARNING] Missing: {}", key),
170        }
171    }
172
173    // fn get_display_color(&self, display_text: &str) -> AppColor {
174    //     AppColor::from_display_text(display_text) // ← NUR EINE ZEILE!
175    // }
176
177    fn get_display_color(&self, display_text: &str) -> AppColor {
178        // Suche Entry mit matching display_text
179        for entry in self.entries.values() {
180            if entry.display.to_uppercase() == display_text.to_uppercase() {
181                // ✅ VERWENDE category für Farbe!
182                return AppColor::from_category(&entry.category);
183            }
184        }
185
186        // Fallback...
187        AppColor::from_any("info")
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// ✅ SINGLETON (unverändert)
201static SERVICE: std::sync::LazyLock<Arc<RwLock<I18nService>>> =
202    std::sync::LazyLock::new(|| Arc::new(RwLock::new(I18nService::new())));
203
204// ✅ PUBLIC API - VEREINFACHT!
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)
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
224// ✅ NEUE VEREINFACHTE FUNKTION: Direkte Farb-Zuordnung!
225pub fn get_color_for_display_text(display_text: &str) -> AppColor {
226    SERVICE.read().unwrap().get_display_color(display_text)
227}
228
229// ✅ LEGACY SUPPORT (um bestehenden Code nicht zu brechen)
230pub fn get_color_category_for_display(display: &str) -> String {
231    // Gib einfach die Display-Text als Kategorie zurück für AppColor::from_any()
232    match display.to_lowercase().as_str() {
233        "theme" => "theme".to_string(),
234        "lang" | "sprache" => "lang".to_string(),
235        _ => "info".to_string(),
236    }
237}
238
239pub fn get_current_language() -> String {
240    SERVICE.read().unwrap().language.to_uppercase()
241}
242
243pub fn get_available_languages() -> Vec<String> {
244    I18nService::available_languages()
245}
246
247pub fn has_translation(key: &str) -> bool {
248    let service = SERVICE.read().unwrap();
249    service.entries.contains_key(key) || service.fallback.contains_key(key)
250}
251
252pub fn clear_translation_cache() {
253    SERVICE.write().unwrap().cache.clear();
254}
255
256// ✅ MACROS (unverändert)
257#[macro_export]
258macro_rules! t {
259    ($key:expr) => { $crate::i18n::get_translation($key, &[]) };
260    ($key:expr, $($arg:expr),+) => { $crate::i18n::get_translation($key, &[$($arg),+]) };
261}
262
263#[macro_export]
264macro_rules! tc {
265    ($key:expr) => { $crate::i18n::get_command_translation($key, &[]) };
266    ($key:expr, $($arg:expr),+) => { $crate::i18n::get_command_translation($key, &[$($arg),+]) };
267}