rush_sync_server/i18n/
mod.rs1use crate::core::prelude::*;
3use crate::ui::color::AppColor;
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}
15
16impl std::fmt::Display for TranslationError {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 Self::InvalidLanguage(lang) => write!(f, "Invalid language: {}", lang),
20 Self::LoadError(msg) => write!(f, "Load error: {}", msg),
21 }
22 }
23}
24
25#[derive(RustEmbed)]
26#[folder = "src/i18n/langs/"]
27pub struct Langs;
28
29#[derive(Debug, Clone)]
30struct Entry {
31 text: String,
32 display: String,
33 category: String,
34}
35
36impl Entry {
37 fn format(&self, params: &[&str]) -> String {
38 params
39 .iter()
40 .enumerate()
41 .fold(self.text.clone(), |mut text, (i, param)| {
42 text = text.replace(&format!("{{{}}}", i), param);
43 if text.contains("{}") {
44 text = text.replacen("{}", param, 1);
45 }
46 text
47 })
48 }
49}
50
51struct I18nService {
52 language: String,
53 entries: HashMap<String, Entry>,
54 fallback: HashMap<String, Entry>,
55 cache: HashMap<String, String>,
56}
57
58impl I18nService {
59 fn new() -> Self {
60 Self {
61 language: DEFAULT_LANGUAGE.into(),
62 entries: HashMap::new(),
63 fallback: HashMap::new(),
64 cache: HashMap::new(),
65 }
66 }
67
68 fn load_language(&mut self, lang: &str) -> Result<()> {
69 if !Self::available_languages()
71 .iter()
72 .any(|l| l.eq_ignore_ascii_case(lang))
73 {
74 return Err(AppError::Translation(TranslationError::InvalidLanguage(
75 lang.into(),
76 )));
77 }
78
79 self.entries = Self::load_entries(lang)?;
81
82 self.fallback.clear();
84 for available_lang in Self::available_languages() {
85 if available_lang.to_lowercase() != lang.to_lowercase() {
86 if let Ok(other_entries) = Self::load_entries(&available_lang.to_lowercase()) {
87 for (key, entry) in other_entries {
88 self.fallback.entry(key).or_insert(entry);
89 }
90 }
91 }
92 }
93
94 self.cache.clear();
95 self.language = lang.into();
96 Ok(())
97 }
98
99 fn load_entries(lang: &str) -> Result<HashMap<String, Entry>> {
100 let filename = format!("{}.json", lang.to_lowercase());
101 let content = Langs::get(&filename).ok_or_else(|| {
102 AppError::Translation(TranslationError::LoadError(format!(
103 "File not found: {}",
104 filename
105 )))
106 })?;
107
108 let content_str = std::str::from_utf8(content.data.as_ref())
109 .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
110
111 let raw: HashMap<String, String> = serde_json::from_str(content_str)
112 .map_err(|e| AppError::Translation(TranslationError::LoadError(e.to_string())))?;
113
114 Ok(raw
115 .iter()
116 .filter_map(|(key, value)| {
117 key.strip_suffix(".text").map(|base_key| {
118 let display = raw
119 .get(&format!("{}.display_text", base_key))
120 .unwrap_or(&base_key.to_uppercase())
121 .clone();
122 let category = raw
123 .get(&format!("{}.category", base_key))
124 .unwrap_or(&"info".to_string())
125 .clone();
126
127 (
128 base_key.into(),
129 Entry {
130 text: value.clone(),
131 display,
132 category,
133 },
134 )
135 })
136 })
137 .collect())
138 }
139
140 fn get_translation(&mut self, key: &str, params: &[&str]) -> String {
141 let cache_key = if params.is_empty() {
143 key.into()
144 } else {
145 format!("{}:{}", key, params.join(":"))
146 };
147
148 if let Some(cached) = self.cache.get(&cache_key) {
149 return cached.clone();
150 }
151
152 let text = match self.entries.get(key).or_else(|| self.fallback.get(key)) {
154 Some(entry) => entry.format(params),
155 None => format!("Missing: {}", key),
156 };
157
158 if self.cache.len() >= 1000 {
160 self.cache.clear();
161 }
162 self.cache.insert(cache_key, text.clone());
163 text
164 }
165
166 fn get_command_translation(&mut self, key: &str, params: &[&str]) -> String {
167 match self.entries.get(key).or_else(|| self.fallback.get(key)) {
168 Some(entry) => format!("[{}] {}", entry.display, entry.format(params)),
169 None => format!("[WARNING] Missing: {}", key),
170 }
171 }
172
173 fn get_display_color(&self, display_text: &str) -> AppColor {
178 for entry in self.entries.values() {
180 if entry.display.to_uppercase() == display_text.to_uppercase() {
181 return AppColor::from_category(&entry.category);
183 }
184 }
185
186 AppColor::from_any("info")
188 }
189
190 fn available_languages() -> Vec<String> {
191 Langs::iter()
192 .filter_map(|f| {
193 let filename = f.as_ref();
194 filename.strip_suffix(".json").map(|s| s.to_uppercase())
195 })
196 .collect()
197 }
198}
199
200static SERVICE: std::sync::LazyLock<Arc<RwLock<I18nService>>> =
202 std::sync::LazyLock::new(|| Arc::new(RwLock::new(I18nService::new())));
203
204pub async fn init() -> Result<()> {
206 set_language(DEFAULT_LANGUAGE)
207}
208
209pub fn set_language(lang: &str) -> Result<()> {
210 SERVICE.write().unwrap().load_language(lang)
211}
212
213pub fn get_translation(key: &str, params: &[&str]) -> String {
214 SERVICE.write().unwrap().get_translation(key, params)
215}
216
217pub fn get_command_translation(key: &str, params: &[&str]) -> String {
218 SERVICE
219 .write()
220 .unwrap()
221 .get_command_translation(key, params)
222}
223
224pub fn get_color_for_display_text(display_text: &str) -> AppColor {
226 SERVICE.read().unwrap().get_display_color(display_text)
227}
228
229pub fn get_color_category_for_display(display: &str) -> String {
231 match display.to_lowercase().as_str() {
233 "theme" => "theme".to_string(),
234 "lang" | "sprache" => "lang".to_string(),
235 _ => "info".to_string(),
236 }
237}
238
239pub fn get_current_language() -> String {
240 SERVICE.read().unwrap().language.to_uppercase()
241}
242
243pub fn get_available_languages() -> Vec<String> {
244 I18nService::available_languages()
245}
246
247pub fn has_translation(key: &str) -> bool {
248 let service = SERVICE.read().unwrap();
249 service.entries.contains_key(key) || service.fallback.contains_key(key)
250}
251
252pub fn clear_translation_cache() {
253 SERVICE.write().unwrap().cache.clear();
254}
255
256#[macro_export]
258macro_rules! t {
259 ($key:expr) => { $crate::i18n::get_translation($key, &[]) };
260 ($key:expr, $($arg:expr),+) => { $crate::i18n::get_translation($key, &[$($arg),+]) };
261}
262
263#[macro_export]
264macro_rules! tc {
265 ($key:expr) => { $crate::i18n::get_command_translation($key, &[]) };
266 ($key:expr, $($arg:expr),+) => { $crate::i18n::get_command_translation($key, &[$($arg),+]) };
267}