rush_sync_server/i18n/
mod.rs

1use crate::core::prelude::*;
2use crate::ui::color::AppColor;
3use rust_embed::RustEmbed;
4use std::collections::HashMap;
5use std::sync::{Arc, RwLock};
6
7pub const DEFAULT_LANGUAGE: &str = "en";
8
9#[derive(Debug)]
10pub enum TranslationError {
11    InvalidLanguage(String),
12    LoadError(String),
13}
14
15impl std::fmt::Display for TranslationError {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        match self {
18            Self::InvalidLanguage(lang) => write!(f, "Invalid language: {}", lang),
19            Self::LoadError(msg) => write!(f, "Load error: {}", msg),
20        }
21    }
22}
23
24#[derive(RustEmbed)]
25#[folder = "src/i18n/langs/"]
26pub struct Langs;
27
28#[derive(Debug, Clone)]
29struct Entry {
30    text: String,
31    display: String,
32    category: String,
33}
34
35impl Entry {
36    fn format(&self, params: &[&str]) -> String {
37        params
38            .iter()
39            .enumerate()
40            .fold(self.text.clone(), |mut text, (i, param)| {
41                text = text.replace(&format!("{{{}}}", i), param);
42                if text.contains("{}") {
43                    text = text.replacen("{}", param, 1);
44                }
45                text
46            })
47    }
48}
49
50struct I18nService {
51    language: String,
52    entries: HashMap<String, Entry>,
53    fallback: HashMap<String, Entry>,
54    cache: HashMap<String, String>,
55}
56
57impl I18nService {
58    fn new() -> Self {
59        Self {
60            language: DEFAULT_LANGUAGE.into(),
61            entries: HashMap::new(),
62            fallback: HashMap::new(),
63            cache: HashMap::new(),
64        }
65    }
66
67    fn load_language(&mut self, lang: &str) -> Result<()> {
68        // Validate
69        if !Self::available_languages()
70            .iter()
71            .any(|l| l.eq_ignore_ascii_case(lang))
72        {
73            return Err(AppError::Translation(TranslationError::InvalidLanguage(
74                lang.into(),
75            )));
76        }
77
78        // Load entries
79        self.entries = Self::load_entries(lang)?;
80
81        // Load fallback (andere Sprachen)
82        self.fallback.clear();
83        for available_lang in Self::available_languages() {
84            if available_lang.to_lowercase() != lang.to_lowercase() {
85                if let Ok(other_entries) = Self::load_entries(&available_lang.to_lowercase()) {
86                    for (key, entry) in other_entries {
87                        self.fallback.entry(key).or_insert(entry);
88                    }
89                }
90            }
91        }
92
93        self.cache.clear();
94        self.language = lang.into();
95        Ok(())
96    }
97
98    fn load_entries(lang: &str) -> Result<HashMap<String, Entry>> {
99        let lang_lower = lang.to_lowercase();
100        let mut merged_raw: HashMap<String, String> = HashMap::new();
101
102        // Dynamisch alle Kategorien für diese Sprache finden
103        let category_files: Vec<String> = Langs::iter()
104            .filter_map(|file| {
105                let filename = file.as_ref();
106                let prefix = format!("{}/", lang_lower);
107
108                if filename.starts_with(&prefix) && filename.ends_with(".json") {
109                    Some(filename.to_string())
110                } else {
111                    None
112                }
113            })
114            .collect();
115
116        // Lade alle gefundenen Kategorie-Dateien
117        let mut found_modular = false;
118        for filename in category_files {
119            if let Some(content) = Langs::get(&filename) {
120                if let Ok(content_str) = std::str::from_utf8(content.data.as_ref()) {
121                    if let Ok(raw) = serde_json::from_str::<HashMap<String, String>>(content_str) {
122                        merged_raw.extend(raw);
123                        found_modular = true;
124                    }
125                }
126            }
127        }
128
129        // Fallback: Alte Einzeldatei
130        if !found_modular {
131            let filename = format!("{}.json", lang_lower);
132            let content = Langs::get(&filename).ok_or_else(|| {
133                AppError::Translation(TranslationError::LoadError(format!(
134                    "File not found: {}",
135                    filename
136                )))
137            })?;
138
139            let content_str = std::str::from_utf8(content.data.as_ref())
140                .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
141
142            merged_raw = serde_json::from_str(content_str)
143                .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
144        }
145
146        // DEINE ORIGINALLOGIK - UNVERÄNDERT!
147        Ok(merged_raw
148            .iter()
149            .filter_map(|(key, value)| {
150                key.strip_suffix(".text").map(|base_key| {
151                    let display = merged_raw
152                        .get(&format!("{}.display_text", base_key))
153                        .unwrap_or(&base_key.to_uppercase())
154                        .clone();
155                    let category = merged_raw
156                        .get(&format!("{}.category", base_key))
157                        .unwrap_or(&"info".to_string())
158                        .clone();
159
160                    (
161                        base_key.into(),
162                        Entry {
163                            text: value.clone(),
164                            display,
165                            category,
166                        },
167                    )
168                })
169            })
170            .collect())
171    }
172
173    fn get_translation(&mut self, key: &str, params: &[&str]) -> String {
174        // Cache check
175        let cache_key = if params.is_empty() {
176            key.into()
177        } else {
178            format!("{}:{}", key, params.join(":"))
179        };
180
181        if let Some(cached) = self.cache.get(&cache_key) {
182            return cached.clone();
183        }
184
185        // Get entry
186        let text = match self.entries.get(key).or_else(|| self.fallback.get(key)) {
187            Some(entry) => entry.format(params),
188            None => format!("Missing: {}", key),
189        };
190
191        // Cache with size limit
192        if self.cache.len() >= 1000 {
193            self.cache.clear();
194        }
195        self.cache.insert(cache_key, text.clone());
196        text
197    }
198
199    fn get_command_translation(&mut self, key: &str, params: &[&str]) -> String {
200        match self.entries.get(key).or_else(|| self.fallback.get(key)) {
201            Some(entry) => format!("[{}] {}", entry.display, entry.format(params)),
202            None => format!("[WARNING] Missing: {}", key),
203        }
204    }
205
206    fn get_display_color(&self, display_text: &str) -> AppColor {
207        // Suche Entry mit matching display_text
208        for entry in self.entries.values() {
209            if entry.display.to_uppercase() == display_text.to_uppercase() {
210                return AppColor::from_category(&entry.category);
211            }
212        }
213
214        // Fallback...
215        AppColor::from_any("info")
216    }
217
218    // ✅ KORRIGIERTE FUNKTION: Erkennt beide Strukturen
219    fn available_languages() -> Vec<String> {
220        let mut languages = std::collections::HashSet::new();
221
222        for file in Langs::iter() {
223            let filename = file.as_ref();
224
225            if filename.ends_with(".json") {
226                if let Some(lang) = filename.strip_suffix(".json") {
227                    // Alte Struktur: de.json -> de
228                    if !lang.contains('/') {
229                        languages.insert(lang.to_uppercase());
230                    }
231                }
232
233                if let Some(slash_pos) = filename.find('/') {
234                    // Neue Struktur: de/common.json -> de
235                    let lang = &filename[..slash_pos];
236                    languages.insert(lang.to_uppercase());
237                }
238            }
239        }
240
241        languages.into_iter().collect()
242    }
243}
244
245// ✅ SINGLETON (unverändert)
246static SERVICE: std::sync::LazyLock<Arc<RwLock<I18nService>>> =
247    std::sync::LazyLock::new(|| Arc::new(RwLock::new(I18nService::new())));
248
249// ✅ PUBLIC API - VEREINFACHT!
250pub async fn init() -> Result<()> {
251    set_language(DEFAULT_LANGUAGE)
252}
253
254pub fn set_language(lang: &str) -> Result<()> {
255    SERVICE.write().unwrap().load_language(lang)
256}
257
258pub fn get_translation(key: &str, params: &[&str]) -> String {
259    SERVICE.write().unwrap().get_translation(key, params)
260}
261
262pub fn get_command_translation(key: &str, params: &[&str]) -> String {
263    SERVICE
264        .write()
265        .unwrap()
266        .get_command_translation(key, params)
267}
268
269pub fn get_color_for_display_text(display_text: &str) -> AppColor {
270    SERVICE.read().unwrap().get_display_color(display_text)
271}
272
273pub fn get_color_category_for_display(display: &str) -> String {
274    match display.to_lowercase().as_str() {
275        "theme" => "theme".to_string(),
276        "lang" | "sprache" => "lang".to_string(),
277        _ => "info".to_string(),
278    }
279}
280
281pub fn get_current_language() -> String {
282    SERVICE.read().unwrap().language.to_uppercase()
283}
284
285pub fn get_available_languages() -> Vec<String> {
286    I18nService::available_languages()
287}
288
289pub fn has_translation(key: &str) -> bool {
290    let service = SERVICE.read().unwrap();
291    service.entries.contains_key(key) || service.fallback.contains_key(key)
292}
293
294pub fn clear_translation_cache() {
295    SERVICE.write().unwrap().cache.clear();
296}
297
298#[macro_export]
299macro_rules! t {
300    ($key:expr) => { $crate::i18n::get_translation($key, &[]) };
301    ($key:expr, $($arg:expr),+) => { $crate::i18n::get_translation($key, &[$($arg),+]) };
302}
303
304#[macro_export]
305macro_rules! tc {
306    ($key:expr) => { $crate::i18n::get_command_translation($key, &[]) };
307    ($key:expr, $($arg:expr),+) => { $crate::i18n::get_command_translation($key, &[$($arg),+]) };
308}