rush_sync_server/i18n/
mod.rs1use 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 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 self.find_similar_color_category(display_category)
138 })
139 }
140
141 fn find_similar_color_category(&self, display_category: &str) -> String {
143 let display_lower = display_category.to_lowercase();
144
145 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 "info".to_string()
170 }
171
172 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 fn update_language(&mut self, new_config: TranslationConfig) {
235 self.config
237 .merge_display_mappings(new_config.global_display_to_color_map.clone());
238
239 self.config.entries = new_config.entries;
241
242 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 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}