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 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(
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 self.find_similar_color_category(display_category)
139 })
140 }
141
142 fn find_similar_color_category(&self, display_category: &str) -> String {
144 let display_lower = display_category.to_lowercase();
145
146 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 "info".to_string()
171 }
172
173 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 fn update_language(&mut self, new_config: TranslationConfig) {
236 self.config
238 .merge_display_mappings(new_config.global_display_to_color_map.clone());
239
240 self.config.entries = new_config.entries;
242
243 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 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}