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::{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        let mut text = self.text.clone();
41        for (i, param) in params.iter().enumerate() {
42            text = text.replace(&format!("{{{}}}", i), param);
43        }
44        for param in params {
45            if text.contains("{}") {
46                text = text.replacen("{}", param, 1);
47            }
48        }
49        text
50    }
51
52    fn get_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_to_category: HashMap<String, String>,
63}
64
65impl I18nService {
66    fn new() -> Self {
67        Self {
68            language: DEFAULT_LANGUAGE.to_string(),
69            entries: HashMap::new(),
70            fallback: HashMap::new(),
71            cache: HashMap::new(),
72            display_to_category: HashMap::new(),
73        }
74    }
75
76    fn load_language(&mut self, lang: &str) -> Result<()> {
77        if !Self::get_available_languages()
78            .iter()
79            .any(|l| l.to_lowercase() == lang.to_lowercase())
80        {
81            return Err(AppError::Translation(TranslationError::InvalidLanguage(
82                lang.to_string(),
83            )));
84        }
85
86        self.entries = Self::load_entries(lang)?;
87
88        if lang != DEFAULT_LANGUAGE {
89            self.fallback = Self::load_entries(DEFAULT_LANGUAGE).unwrap_or_default();
90        }
91
92        self.build_display_to_category_mapping();
93        self.cache.clear();
94        self.language = lang.to_string();
95        Ok(())
96    }
97
98    fn build_display_to_category_mapping(&mut self) {
99        self.display_to_category.clear();
100
101        for entry in self.entries.values() {
102            let display_key = entry.display.to_lowercase();
103            self.display_to_category
104                .insert(display_key, entry.category.clone());
105        }
106
107        for entry in self.fallback.values() {
108            let display_key = entry.display.to_lowercase();
109            self.display_to_category
110                .entry(display_key)
111                .or_insert_with(|| entry.category.clone());
112        }
113    }
114
115    fn load_entries(lang: &str) -> Result<HashMap<String, Entry>> {
116        let filename = format!("{}.json", lang.to_lowercase());
117        let content = Langs::get(&filename).ok_or_else(|| {
118            AppError::Translation(TranslationError::LoadError(format!(
119                "File not found: {}",
120                filename
121            )))
122        })?;
123
124        let content_str = std::str::from_utf8(content.data.as_ref())
125            .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
126
127        let raw: HashMap<String, String> = serde_json::from_str(content_str)
128            .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
129
130        let mut entries = HashMap::new();
131
132        for (key, value) in raw.iter() {
133            if key.ends_with(".text") {
134                let base_key = &key[0..key.len() - 5];
135                let display = raw
136                    .get(&format!("{}.display_text", base_key))
137                    .unwrap_or(&base_key.to_uppercase())
138                    .clone();
139                let category = raw
140                    .get(&format!("{}.category", base_key))
141                    .unwrap_or(&"info".to_string())
142                    .clone();
143
144                entries.insert(
145                    base_key.to_string(),
146                    Entry {
147                        text: value.clone(),
148                        display,
149                        category,
150                    },
151                );
152            }
153        }
154
155        Ok(entries)
156    }
157
158    fn get_translation(&mut self, key: &str, params: &[&str]) -> (String, AppColor) {
159        let cache_key = if params.is_empty() {
160            key.to_string()
161        } else {
162            format!("{}:{}", key, params.join(":"))
163        };
164
165        if let Some(cached) = self.cache.get(&cache_key) {
166            return cached.clone();
167        }
168
169        let entry = self.entries.get(key).or_else(|| self.fallback.get(key));
170
171        let (text, color) = match entry {
172            Some(e) => (e.format(params), e.get_color()),
173            None => {
174                log::warn!("Missing key: {}", key);
175                (format!("Missing: {}", key), AppColor::from_any("warning"))
176            }
177        };
178
179        if self.cache.len() >= 1000 {
180            self.cache.clear();
181        }
182        self.cache.insert(cache_key, (text.clone(), color));
183
184        (text, color)
185    }
186
187    fn get_command_translation(&mut self, key: &str, params: &[&str]) -> String {
188        if let Some(entry) = self.entries.get(key).or_else(|| self.fallback.get(key)) {
189            format!("[{}] {}", entry.display, entry.format(params))
190        } else {
191            format!("[WARNING] Missing: {}", key)
192        }
193    }
194
195    fn get_category_for_display(&self, display_text: &str) -> String {
196        let display_lower = display_text.to_lowercase();
197
198        if let Some(category) = self.display_to_category.get(&display_lower) {
199            return category.clone();
200        }
201
202        "info".to_string()
203    }
204
205    fn get_available_languages() -> Vec<String> {
206        Langs::iter()
207            .filter_map(|f| f.as_ref().strip_suffix(".json").map(|s| s.to_uppercase()))
208            .collect()
209    }
210
211    fn clear_cache(&mut self) {
212        self.cache.clear();
213    }
214}
215
216lazy_static! {
217    static ref SERVICE: Arc<RwLock<I18nService>> = Arc::new(RwLock::new(I18nService::new()));
218}
219
220// Öffentliche API
221pub async fn init() -> Result<()> {
222    set_language(DEFAULT_LANGUAGE)
223}
224
225pub fn set_language(lang: &str) -> Result<()> {
226    SERVICE.write().unwrap().load_language(lang)
227}
228
229pub fn get_translation(key: &str, params: &[&str]) -> String {
230    SERVICE.write().unwrap().get_translation(key, params).0
231}
232
233pub fn get_command_translation(key: &str, params: &[&str]) -> String {
234    SERVICE
235        .write()
236        .unwrap()
237        .get_command_translation(key, params)
238}
239
240pub fn get_color_category_for_display(display_category: &str) -> String {
241    SERVICE
242        .read()
243        .unwrap()
244        .get_category_for_display(display_category)
245}
246
247pub fn get_current_language() -> String {
248    SERVICE.read().unwrap().language.to_uppercase()
249}
250
251pub fn get_available_languages() -> Vec<String> {
252    I18nService::get_available_languages()
253}
254
255pub fn has_translation(key: &str) -> bool {
256    let service = SERVICE.read().unwrap();
257    service.entries.contains_key(key) || service.fallback.contains_key(key)
258}
259
260pub fn clear_translation_cache() {
261    SERVICE.write().unwrap().clear_cache();
262}
263
264pub fn get_all_translation_keys() -> Vec<String> {
265    let service = SERVICE.read().unwrap();
266    let mut keys: Vec<String> = service.entries.keys().cloned().collect();
267    keys.extend(service.fallback.keys().cloned());
268    keys.sort();
269    keys.dedup();
270    keys
271}
272
273pub fn get_translation_stats() -> HashMap<String, usize> {
274    let service = SERVICE.read().unwrap();
275    let mut stats = HashMap::new();
276    stats.insert("total_keys".to_string(), service.entries.len());
277    stats.insert("fallback_keys".to_string(), service.fallback.len());
278    stats.insert("cached_entries".to_string(), service.cache.len());
279    stats.insert(
280        "display_mappings".to_string(),
281        service.display_to_category.len(),
282    );
283    stats
284}
285
286pub fn get_missing_keys_report() -> Vec<String> {
287    Vec::new()
288}
289
290#[macro_export]
291macro_rules! t {
292    ($key:expr) => {
293        $crate::i18n::get_translation($key, &[])
294    };
295    ($key:expr, $($arg:expr),+) => {
296        $crate::i18n::get_translation($key, &[$($arg),+])
297    };
298}
299
300#[macro_export]
301macro_rules! tc {
302    ($key:expr) => {
303        $crate::i18n::get_command_translation($key, &[])
304    };
305    ($key:expr, $($arg:expr),+) => {
306        $crate::i18n::get_command_translation($key, &[$($arg),+])
307    };
308}