1use crate::core::prelude::*;
2use crate::ui::color::AppColor;
3use rust_embed::RustEmbed;
4use std::collections::HashMap;
5use std::sync::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: RwLock<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: RwLock::new(HashMap::new()),
64 }
65 }
66
67 fn load_language(&mut self, lang: &str) -> Result<()> {
68 if !Self::available_languages()
69 .iter()
70 .any(|l| l.eq_ignore_ascii_case(lang))
71 {
72 return Err(AppError::Translation(TranslationError::InvalidLanguage(
73 lang.into(),
74 )));
75 }
76
77 self.entries = Self::load_entries(lang)?;
78
79 self.fallback.clear();
81 for available_lang in Self::available_languages() {
82 if available_lang.to_lowercase() != lang.to_lowercase() {
83 if let Ok(other_entries) = Self::load_entries(&available_lang.to_lowercase()) {
84 for (key, entry) in other_entries {
85 self.fallback.entry(key).or_insert(entry);
86 }
87 }
88 }
89 }
90
91 if let Ok(mut cache) = self.cache.write() {
92 cache.clear();
93 }
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 let category_files: Vec<String> = Langs::iter()
103 .filter_map(|file| {
104 let filename = file.as_ref();
105 let prefix = format!("{}/", lang_lower);
106
107 if filename.starts_with(&prefix) && filename.ends_with(".json") {
108 Some(filename.to_string())
109 } else {
110 None
111 }
112 })
113 .collect();
114
115 let mut found_modular = false;
116 for filename in category_files {
117 if let Some(content) = Langs::get(&filename) {
118 if let Ok(content_str) = std::str::from_utf8(content.data.as_ref()) {
119 if let Ok(raw) = serde_json::from_str::<HashMap<String, String>>(content_str) {
120 merged_raw.extend(raw);
121 found_modular = true;
122 }
123 }
124 }
125 }
126
127 if !found_modular {
129 let filename = format!("{}.json", lang_lower);
130 let content = Langs::get(&filename).ok_or_else(|| {
131 AppError::Translation(TranslationError::LoadError(format!(
132 "File not found: {}",
133 filename
134 )))
135 })?;
136
137 let content_str = std::str::from_utf8(content.data.as_ref())
138 .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
139
140 merged_raw = serde_json::from_str(content_str)
141 .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
142 }
143
144 Ok(merged_raw
145 .iter()
146 .filter_map(|(key, value)| {
147 key.strip_suffix(".text").map(|base_key| {
148 let display = merged_raw
149 .get(&format!("{}.display_text", base_key))
150 .unwrap_or(&base_key.to_uppercase())
151 .clone();
152 let category = merged_raw
153 .get(&format!("{}.category", base_key))
154 .unwrap_or(&"info".to_string())
155 .clone();
156
157 (
158 base_key.into(),
159 Entry {
160 text: value.clone(),
161 display,
162 category,
163 },
164 )
165 })
166 })
167 .collect())
168 }
169
170 fn get_translation(&self, key: &str, params: &[&str]) -> String {
172 let cache_key = if params.is_empty() {
173 key.into()
174 } else {
175 format!("{}:{}", key, params.join(":"))
176 };
177
178 if let Ok(cache) = self.cache.read() {
180 if let Some(cached) = cache.get(&cache_key) {
181 return cached.clone();
182 }
183 }
184
185 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 if let Ok(mut cache) = self.cache.write() {
192 if cache.len() >= 1000 {
193 cache.clear();
194 }
195 cache.insert(cache_key, text.clone());
196 }
197 text
198 }
199
200 fn get_command_translation(&self, key: &str, params: &[&str]) -> String {
201 match self.entries.get(key).or_else(|| self.fallback.get(key)) {
202 Some(entry) => format!("[{}] {}", entry.display, entry.format(params)),
203 None => format!("[WARNING] Missing: {}", key),
204 }
205 }
206
207 fn get_display_color(&self, display_text: &str) -> AppColor {
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 AppColor::from_any("info")
214 }
215
216 fn available_languages() -> Vec<String> {
217 let mut languages = std::collections::HashSet::new();
218
219 for file in Langs::iter() {
220 let filename = file.as_ref();
221
222 if filename.ends_with(".json") {
223 if let Some(lang) = filename.strip_suffix(".json") {
224 if !lang.contains('/') {
225 languages.insert(lang.to_uppercase());
226 }
227 }
228
229 if let Some(slash_pos) = filename.find('/') {
230 let lang = &filename[..slash_pos];
231 languages.insert(lang.to_uppercase());
232 }
233 }
234 }
235
236 languages.into_iter().collect()
237 }
238}
239
240static SERVICE: std::sync::LazyLock<RwLock<I18nService>> =
241 std::sync::LazyLock::new(|| RwLock::new(I18nService::new()));
242
243pub async fn init() -> Result<()> {
244 set_language(DEFAULT_LANGUAGE)
245}
246
247pub fn set_language(lang: &str) -> Result<()> {
248 match SERVICE.write() {
249 Ok(mut service) => service.load_language(lang),
250 Err(e) => Err(AppError::Validation(format!("i18n lock poisoned: {}", e))),
251 }
252}
253
254pub fn get_translation(key: &str, params: &[&str]) -> String {
255 match SERVICE.read() {
256 Ok(service) => service.get_translation(key, params),
257 Err(_) => format!("Missing: {}", key),
258 }
259}
260
261pub fn get_command_translation(key: &str, params: &[&str]) -> String {
262 match SERVICE.read() {
263 Ok(service) => service.get_command_translation(key, params),
264 Err(_) => format!("[WARNING] Missing: {}", key),
265 }
266}
267
268pub fn get_color_for_display_text(display_text: &str) -> AppColor {
269 match SERVICE.read() {
270 Ok(service) => service.get_display_color(display_text),
271 Err(_) => AppColor::from_any("info"),
272 }
273}
274
275pub fn get_color_category_for_display(display: &str) -> String {
276 match display.to_lowercase().as_str() {
277 "theme" => "theme".to_string(),
278 "lang" | "sprache" => "lang".to_string(),
279 _ => "info".to_string(),
280 }
281}
282
283pub fn get_current_language() -> String {
284 match SERVICE.read() {
285 Ok(service) => service.language.to_uppercase(),
286 Err(_) => DEFAULT_LANGUAGE.to_uppercase(),
287 }
288}
289
290pub fn get_available_languages() -> Vec<String> {
291 I18nService::available_languages()
292}
293
294pub fn has_translation(key: &str) -> bool {
295 match SERVICE.read() {
296 Ok(service) => service.entries.contains_key(key) || service.fallback.contains_key(key),
297 Err(_) => false,
298 }
299}
300
301pub fn clear_translation_cache() {
302 if let Ok(service) = SERVICE.read() {
303 if let Ok(mut cache) = service.cache.write() {
304 cache.clear();
305 }
306 }
307}
308
309#[macro_export]
310macro_rules! t {
311 ($key:expr) => { $crate::i18n::get_translation($key, &[]) };
312 ($key:expr, $($arg:expr),+) => { $crate::i18n::get_translation($key, &[$($arg),+]) };
313}
314
315#[macro_export]
316macro_rules! tc {
317 ($key:expr) => { $crate::i18n::get_command_translation($key, &[]) };
318 ($key:expr, $($arg:expr),+) => { $crate::i18n::get_command_translation($key, &[$($arg),+]) };
319}