1#![warn(missing_docs)]
2
3use std::{collections::HashMap, path::Path};
8use tracing::info;
9
10#[cfg(feature = "oak-fluent")]
12use oak_fluent::{Translator as OakFluentTranslatorCore, parse};
13
14pub trait TranslationProvider {
16 fn translate(
18 &self,
19 key: &str,
20 args: Option<&HashMap<String, String>>,
21 locale: &str,
22 ) -> Result<String, Box<dyn std::error::Error>>;
23
24 fn get_locale(&self) -> &str;
26
27 fn set_locale(&mut self, locale: &str);
29
30 fn clone_box(&self) -> Box<dyn TranslationProvider + Send + Sync>;
32}
33
34#[derive(Debug, Clone)]
36pub struct SimpleTranslator {
37 translations: HashMap<String, HashMap<String, String>>,
38 current_locale: String,
39 default_locale: String,
40}
41
42impl SimpleTranslator {
43 pub fn new(current_locale: &str, default_locale: &str) -> Self {
45 Self {
46 translations: HashMap::new(),
47 current_locale: current_locale.to_string(),
48 default_locale: default_locale.to_string(),
49 }
50 }
51
52 pub fn add_translations(&mut self, locale: &str, translations: HashMap<String, String>) {
54 self.translations.insert(locale.to_string(), translations);
55 }
56}
57
58impl TranslationProvider for SimpleTranslator {
59 fn translate(
60 &self,
61 key: &str,
62 args: Option<&HashMap<String, String>>,
63 locale: &str,
64 ) -> Result<String, Box<dyn std::error::Error>> {
65 let translation = self
67 .translations
68 .get(locale)
69 .and_then(|lang| lang.get(key))
70 .or_else(|| {
71 if locale != self.default_locale {
72 self.translations.get(&self.default_locale).and_then(|lang| lang.get(key))
73 }
74 else {
75 None
76 }
77 })
78 .unwrap_or(&key.to_string())
79 .to_string();
80
81 if let Some(args) = args {
83 let mut result = translation;
84 for (name, value) in args {
85 result = result.replace(&format!("{{{}}}", name), value);
86 }
87 Ok(result)
88 }
89 else {
90 Ok(translation)
91 }
92 }
93
94 fn get_locale(&self) -> &str {
95 &self.current_locale
96 }
97
98 fn set_locale(&mut self, locale: &str) {
99 self.current_locale = locale.to_string();
100 }
101
102 fn clone_box(&self) -> Box<dyn TranslationProvider + Send + Sync> {
103 Box::new(self.clone())
104 }
105}
106
107pub struct FluentTranslator {
109 bundles: HashMap<String, fluent_bundle::FluentBundle<fluent_bundle::FluentResource>>,
110 current_locale: String,
111 default_locale: String,
112}
113
114impl FluentTranslator {
115 pub fn new(current_locale: &str, default_locale: &str) -> Self {
117 Self { bundles: HashMap::new(), current_locale: current_locale.to_string(), default_locale: default_locale.to_string() }
118 }
119
120 pub fn add_translations_from_file(&mut self, locale: &str, file_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
122 let content = std::fs::read_to_string(file_path)?;
123 self.add_translations_from_string(locale, &content)
124 }
125
126 pub fn add_translations_from_string(&mut self, locale: &str, ftl_content: &str) -> Result<(), Box<dyn std::error::Error>> {
128 match fluent_bundle::FluentResource::try_new(ftl_content.to_string()) {
129 Ok(resource) => {
130 let langid = match locale {
131 "zh-CN" => unic_langid::langid!("zh-CN"),
132 "en" => unic_langid::langid!("en"),
133 _ => unic_langid::langid!("en"),
134 };
135
136 let mut bundle = fluent_bundle::FluentBundle::new(vec![langid]);
137 match bundle.add_resource(resource) {
138 Ok(_) => {
139 self.bundles.insert(locale.to_string(), bundle);
140 Ok(())
141 }
142 Err(errors) => Err(format!("Failed to add resource: {:?}", errors).into()),
143 }
144 }
145 Err((_, errors)) => Err(format!("Failed to parse FTL content: {:?}", errors).into()),
146 }
147 }
148}
149
150impl TranslationProvider for FluentTranslator {
151 fn translate(
152 &self,
153 key: &str,
154 args: Option<&HashMap<String, String>>,
155 locale: &str,
156 ) -> Result<String, Box<dyn std::error::Error>> {
157 if let Some(bundle) = self.bundles.get(locale) {
159 if let Some(message) = bundle.get_message(key) {
160 if let Some(value) = message.value() {
161 let mut errors = vec![];
162 let result = if let Some(args) = args {
163 let mut fluent_args = fluent_bundle::FluentArgs::new();
164 for (name, value) in args {
165 fluent_args.set(name, fluent_bundle::FluentValue::from(value));
166 }
167 bundle.format_pattern(value, Some(&fluent_args), &mut errors)
168 }
169 else {
170 bundle.format_pattern(value, None, &mut errors)
171 };
172 return Ok(result.to_string());
173 }
174 }
175 }
176
177 if locale != self.default_locale {
179 if let Some(bundle) = self.bundles.get(&self.default_locale) {
180 if let Some(message) = bundle.get_message(key) {
181 if let Some(value) = message.value() {
182 let mut errors = vec![];
183 let result = if let Some(args) = args {
184 let mut fluent_args = fluent_bundle::FluentArgs::new();
185 for (name, value) in args {
186 fluent_args.set(name, fluent_bundle::FluentValue::from(value));
187 }
188 bundle.format_pattern(value, Some(&fluent_args), &mut errors)
189 }
190 else {
191 bundle.format_pattern(value, None, &mut errors)
192 };
193 return Ok(result.to_string());
194 }
195 }
196 }
197 }
198
199 Ok(key.to_string())
201 }
202
203 fn get_locale(&self) -> &str {
204 &self.current_locale
205 }
206
207 fn set_locale(&mut self, locale: &str) {
208 self.current_locale = locale.to_string();
209 }
210
211 fn clone_box(&self) -> Box<dyn TranslationProvider + Send + Sync> {
212 let new_translator = FluentTranslator::new(&self.current_locale, &self.default_locale);
214
215 for (_locale, _bundle) in &self.bundles {
217 }
221
222 Box::new(new_translator)
223 }
224}
225
226unsafe impl Send for FluentTranslator {}
229unsafe impl Sync for FluentTranslator {}
230
231#[cfg(feature = "oak-fluent")]
233pub struct OakFluentTranslator {
234 translators: HashMap<String, OakFluentTranslatorCore>,
235 current_locale: String,
236 default_locale: String,
237}
238
239#[cfg(feature = "oak-fluent")]
240impl OakFluentTranslator {
241 pub fn new(current_locale: &str, default_locale: &str) -> Self {
243 Self {
244 translators: HashMap::new(),
245 current_locale: current_locale.to_string(),
246 default_locale: default_locale.to_string(),
247 }
248 }
249
250 pub fn add_translations_from_file(&mut self, locale: &str, file_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
252 let content = std::fs::read_to_string(file_path)?;
253 self.add_translations_from_string(locale, &content)
254 }
255
256 pub fn add_translations_from_string(&mut self, locale: &str, ftl_content: &str) -> Result<(), Box<dyn std::error::Error>> {
258 match parse(ftl_content) {
259 Ok(root) => {
260 let translator = OakFluentTranslatorCore::new(root);
261 self.translators.insert(locale.to_string(), translator);
262 Ok(())
263 }
264 Err(err) => Err(format!("Failed to parse FTL content: {:?}", err).into()),
265 }
266 }
267}
268
269#[cfg(feature = "oak-fluent")]
270impl TranslationProvider for OakFluentTranslator {
271 fn translate(
272 &self,
273 key: &str,
274 args: Option<&HashMap<String, String>>,
275 locale: &str,
276 ) -> Result<String, Box<dyn std::error::Error>> {
277 if let Some(translator) = self.translators.get(locale) {
279 if let Some(translation) = translator.translate(key, &args.unwrap_or(&HashMap::new())) {
280 return Ok(translation);
281 }
282 }
283
284 if locale != self.default_locale {
286 if let Some(translator) = self.translators.get(&self.default_locale) {
287 if let Some(translation) = translator.translate(key, &args.unwrap_or(&HashMap::new())) {
288 return Ok(translation);
289 }
290 }
291 }
292
293 Ok(key.to_string())
295 }
296
297 fn get_locale(&self) -> &str {
298 &self.current_locale
299 }
300
301 fn set_locale(&mut self, locale: &str) {
302 self.current_locale = locale.to_string();
303 }
304
305 fn clone_box(&self) -> Box<dyn TranslationProvider + Send + Sync> {
306 let mut new_translator = OakFluentTranslator::new(&self.current_locale, &self.default_locale);
308
309 for (locale, translator) in &self.translators {
311 new_translator.translators.insert(locale.clone(), translator.clone());
313 }
314
315 Box::new(new_translator)
316 }
317}
318
319#[cfg(feature = "oak-fluent")]
320unsafe impl Send for OakFluentTranslator {}
322
323#[cfg(feature = "oak-fluent")]
324unsafe impl Sync for OakFluentTranslator {}
325
326#[derive(Debug, Hash, PartialEq, Eq)]
328struct TranslationCacheKey {
329 key: String,
330 locale: String,
331 args: Option<Vec<(String, String)>>,
332}
333
334impl TranslationCacheKey {
335 fn new(key: &str, locale: &str, args: Option<&HashMap<String, String>>) -> Self {
337 let args_vec = args.map(|a| {
338 let mut vec: Vec<(String, String)> = a.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
339 vec.sort_by(|(a, _), (b, _)| a.cmp(b));
340 vec
341 });
342
343 Self { key: key.to_string(), locale: locale.to_string(), args: args_vec }
344 }
345}
346
347pub struct I18nContext {
349 translator: Box<dyn TranslationProvider + Send + Sync>,
350 translation_cache: lru::LruCache<TranslationCacheKey, String>,
351 cache_capacity: usize,
352}
353
354impl Clone for I18nContext {
355 fn clone(&self) -> Self {
356 Self {
357 translator: self.translator.clone_box(),
358 translation_cache: lru::LruCache::new(self.cache_capacity),
359 cache_capacity: self.cache_capacity,
360 }
361 }
362}
363
364impl I18nContext {
365 pub fn new(translator: Box<dyn TranslationProvider + Send + Sync>) -> Self {
367 Self { translator, translation_cache: lru::LruCache::new(1000), cache_capacity: 1000 }
368 }
369
370 pub fn with_cache_capacity(translator: Box<dyn TranslationProvider + Send + Sync>, capacity: usize) -> Self {
372 Self { translator, translation_cache: lru::LruCache::new(capacity), cache_capacity: capacity }
373 }
374
375 pub fn t(&mut self, key: &str) -> String {
377 self.t_with_args(key, None)
378 }
379
380 pub fn t_with_args(&mut self, key: &str, args: Option<&HashMap<String, String>>) -> String {
382 let locale = self.translator.get_locale();
383
384 let cache_key = TranslationCacheKey::new(key, locale, args);
386
387 if let Some(cached) = self.translation_cache.get(&cache_key) {
389 return cached.clone();
390 }
391
392 let result = self.translator.translate(key, args, locale).unwrap_or_else(|_| key.to_string());
394
395 self.translation_cache.put(cache_key, result.clone());
397
398 result
399 }
400
401 pub fn t_batch(&mut self, keys: &[(String, Option<HashMap<String, String>>)]) -> Vec<String> {
403 keys.iter().map(|(key, args)| self.t_with_args(key, args.as_ref())).collect()
404 }
405
406 pub fn get_locale(&self) -> &str {
408 self.translator.get_locale()
409 }
410
411 pub fn set_locale(&mut self, locale: &str) {
413 self.translator.set_locale(locale);
414 self.clear_cache(); }
416
417 pub fn clear_cache(&mut self) {
419 self.translation_cache.clear();
420 }
421
422 pub fn cache_size(&self) -> usize {
424 self.translation_cache.len()
425 }
426}
427
428lazy_static::lazy_static! {
429 pub static ref I18N: std::sync::Mutex<I18nContext> = {
431 #[cfg(feature = "oak-fluent")]
432 {
433 let mut translator = OakFluentTranslator::new("en", "en");
435
436 let default_locale = std::env::var("LANG").unwrap_or_else(|_| "en".to_string());
438
439 if let Ok(english_path) = std::env::var("ITOOLS_LOCALE_EN") {
441 let path = Path::new(&english_path);
442 if path.exists() {
443 if let Err(e) = translator.add_translations_from_file("en", path) {
444 info!("Failed to load English translations: {}", e);
445 }
446 }
447 }
448
449 if default_locale != "en" {
451 if let Ok(locale_path) = std::env::var(format!("ITOOLS_LOCALE_{}", default_locale.to_uppercase().replace('-', "_"))) {
452 let path = Path::new(&locale_path);
453 if path.exists() {
454 if let Err(e) = translator.add_translations_from_file(&default_locale, path) {
455 info!("Failed to load {} translations: {}", default_locale, e);
456 }
457 }
458 }
459 }
460
461 std::sync::Mutex::new(I18nContext::new(Box::new(translator)))
462 }
463
464 #[cfg(not(feature = "oak-fluent"))]
465 {
466 let mut translator = FluentTranslator::new("en", "en");
468
469 let default_locale = std::env::var("LANG").unwrap_or_else(|_| "en".to_string());
471
472 if let Ok(english_path) = std::env::var("ITOOLS_LOCALE_EN") {
474 let path = Path::new(&english_path);
475 if path.exists() {
476 if let Err(e) = translator.add_translations_from_file("en", path) {
477 info!("Failed to load English translations: {}", e);
478 }
479 }
480 }
481
482 if default_locale != "en" {
484 if let Ok(locale_path) = std::env::var(format!("ITOOLS_LOCALE_{}", default_locale.to_uppercase().replace('-', "_"))) {
485 let path = Path::new(&locale_path);
486 if path.exists() {
487 if let Err(e) = translator.add_translations_from_file(&default_locale, path) {
488 info!("Failed to load {} translations: {}", default_locale, e);
489 }
490 }
491 }
492 }
493
494 std::sync::Mutex::new(I18nContext::new(Box::new(translator)))
495 }
496 };
497}
498
499pub fn t(key: &str) -> String {
501 let mut i18n = I18N.lock().unwrap();
502 i18n.t(key)
503}
504
505pub fn t_with_args(key: &str, args: &[(&str, &str)]) -> String {
507 let mut i18n = I18N.lock().unwrap();
508 let args_map: HashMap<String, String> = args.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
509 i18n.t_with_args(key, Some(&args_map))
510}
511
512pub fn set_locale(locale: &str) {
514 let mut i18n = I18N.lock().unwrap();
515 i18n.set_locale(locale);
516}
517
518pub fn get_locale() -> String {
520 let i18n = I18N.lock().unwrap();
521 i18n.get_locale().to_string()
522}