Skip to main content

itools_localization/
lib.rs

1#![warn(missing_docs)]
2
3//! iTools 国际化支持模块
4//!
5//! 提供完整的国际化功能,包括翻译管理、语言切换和参数化翻译。
6
7use std::{collections::HashMap, num::NonZeroUsize, path::Path};
8use tracing::info;
9
10/// 国际化错误类型
11#[derive(Debug)]
12pub enum I18nError {
13    /// 文件读取错误
14    FileRead(std::io::Error),
15    /// 解析错误
16    ParseError(String),
17    /// 不支持的文件格式
18    UnsupportedFormat(String),
19    /// 特性未启用
20    FeatureNotEnabled(String),
21    /// 翻译未找到
22    TranslationNotFound(String),
23}
24
25impl std::error::Error for I18nError {
26    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
27        match self {
28            I18nError::FileRead(err) => Some(err),
29            _ => None,
30        }
31    }
32}
33
34impl std::fmt::Display for I18nError {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            I18nError::FileRead(err) => write!(f, "Failed to read file: {}", err),
38            I18nError::ParseError(err) => write!(f, "Failed to parse content: {}", err),
39            I18nError::UnsupportedFormat(format) => write!(f, "Unsupported file format: {}", format),
40            I18nError::FeatureNotEnabled(feature) => write!(f, "Feature not enabled: {}", feature),
41            I18nError::TranslationNotFound(key) => write!(f, "Translation not found: {}", key),
42        }
43    }
44}
45
46impl From<std::io::Error> for I18nError {
47    fn from(err: std::io::Error) -> Self {
48        I18nError::FileRead(err)
49    }
50}
51
52// 导入 oak-fluent 库
53use oak_fluent::{Translator as OakFluentTranslatorCore, parse};
54
55/// 翻译提供者 trait
56pub trait TranslationProvider {
57    /// 翻译一个键
58    fn translate(&self, key: &str, args: Option<&HashMap<String, String>>, locale: &str) -> Result<String, I18nError>;
59
60    /// 获取当前语言
61    fn get_locale(&self) -> &str;
62
63    /// 设置当前语言
64    fn set_locale(&mut self, locale: &str);
65
66    /// 克隆翻译提供者
67    fn clone_box(&self) -> Box<dyn TranslationProvider + Send + Sync>;
68}
69
70/// 简单内存翻译提供者
71#[derive(Debug, Clone)]
72pub struct SimpleTranslator {
73    translations: HashMap<String, HashMap<String, String>>,
74    current_locale: String,
75    default_locale: String,
76}
77
78impl SimpleTranslator {
79    /// 创建新的简单翻译提供者
80    pub fn new(current_locale: &str, default_locale: &str) -> Self {
81        Self {
82            translations: HashMap::new(),
83            current_locale: current_locale.to_string(),
84            default_locale: default_locale.to_string(),
85        }
86    }
87
88    /// 添加翻译
89    pub fn add_translations(&mut self, locale: &str, translations: HashMap<String, String>) {
90        self.translations.insert(locale.to_string(), translations);
91    }
92}
93
94impl TranslationProvider for SimpleTranslator {
95    fn translate(&self, key: &str, args: Option<&HashMap<String, String>>, locale: &str) -> Result<String, I18nError> {
96        // 查找翻译
97        let translation = self
98            .translations
99            .get(locale)
100            .and_then(|lang| lang.get(key))
101            .or_else(|| {
102                if locale != self.default_locale {
103                    self.translations.get(&self.default_locale).and_then(|lang| lang.get(key))
104                }
105                else {
106                    None
107                }
108            })
109            .unwrap_or(&key.to_string())
110            .to_string();
111
112        // 处理参数
113        if let Some(args) = args {
114            let mut result = translation;
115            for (name, value) in args {
116                result = result.replace(&format!("{{{}}}", name), value);
117            }
118            Ok(result)
119        }
120        else {
121            Ok(translation)
122        }
123    }
124
125    fn get_locale(&self) -> &str {
126        &self.current_locale
127    }
128
129    fn set_locale(&mut self, locale: &str) {
130        self.current_locale = locale.to_string();
131    }
132
133    fn clone_box(&self) -> Box<dyn TranslationProvider + Send + Sync> {
134        Box::new(self.clone())
135    }
136}
137
138/// Oak Fluent 翻译提供者
139pub struct OakFluentTranslator {
140    translators: HashMap<String, OakFluentTranslatorCore>,
141    current_locale: String,
142    default_locale: String,
143}
144
145impl OakFluentTranslator {
146    /// 创建新的 Oak Fluent 翻译提供者
147    pub fn new(current_locale: &str, default_locale: &str) -> Self {
148        Self {
149            translators: HashMap::new(),
150            current_locale: current_locale.to_string(),
151            default_locale: default_locale.to_string(),
152        }
153    }
154
155    /// 从文件加载翻译
156    pub fn add_translations_from_file(&mut self, locale: &str, file_path: &Path) -> Result<(), I18nError> {
157        let content = std::fs::read_to_string(file_path)?;
158
159        // 根据文件扩展名选择解析器
160        match file_path.extension().and_then(|ext| ext.to_str()) {
161            Some("ftl") => self.add_translations_from_string(locale, &content),
162            Some("json") => {
163                #[cfg(feature = "json")]
164                {
165                    let translations: HashMap<String, String> =
166                        oak_json::from_str(&content).map_err(|e| I18nError::ParseError(e.to_string()))?;
167                    self.add_simple_translations(locale, translations)
168                }
169                #[cfg(not(feature = "json"))]
170                {
171                    Err(I18nError::FeatureNotEnabled("json".to_string()))
172                }
173            }
174            Some("toml") => {
175                #[cfg(feature = "toml")]
176                {
177                    let translations: HashMap<String, String> =
178                        oak_toml::from_str(&content).map_err(|e| I18nError::ParseError(e.to_string()))?;
179                    self.add_simple_translations(locale, translations)
180                }
181                #[cfg(not(feature = "toml"))]
182                {
183                    Err(I18nError::FeatureNotEnabled("toml".to_string()))
184                }
185            }
186            Some("yaml") | Some("yml") => {
187                #[cfg(feature = "yaml")]
188                {
189                    let translations: HashMap<String, String> =
190                        oak_yaml::from_str(&content).map_err(|e| I18nError::ParseError(e.to_string()))?;
191                    self.add_simple_translations(locale, translations)
192                }
193                #[cfg(not(feature = "yaml"))]
194                {
195                    Err(I18nError::FeatureNotEnabled("yaml".to_string()))
196                }
197            }
198            Some(ext) => Err(I18nError::UnsupportedFormat(ext.to_string())),
199            None => Err(I18nError::UnsupportedFormat("unknown".to_string())),
200        }
201    }
202
203    /// 添加简单翻译(从 HashMap 加载)
204    pub fn add_simple_translations(&mut self, locale: &str, translations: HashMap<String, String>) -> Result<(), I18nError> {
205        // 这里我们需要将 HashMap 转换为 Fluent 格式,或者添加对简单键值对的支持
206        // 为了简单起见,我们暂时将其添加到 SimpleTranslator 中
207        // 后续可以考虑更复杂的实现
208
209        // 这里我们使用一个临时的 SimpleTranslator 来处理简单的键值对翻译
210        // 实际实现中,我们可能需要将这些翻译转换为 Fluent 格式
211
212        // 由于 OakFluentTranslator 目前只支持 Fluent 格式,我们需要添加对简单键值对的支持
213        // 这里我们使用一个简单的方法,将键值对转换为 Fluent 格式的字符串
214        let mut ftl_content = String::new();
215        for (key, value) in translations {
216            ftl_content.push_str(&format!("{} = {}", key, value));
217            ftl_content.push('\n');
218        }
219
220        self.add_translations_from_string(locale, &ftl_content)
221    }
222
223    /// 从字符串加载翻译
224    pub fn add_translations_from_string(&mut self, locale: &str, ftl_content: &str) -> Result<(), I18nError> {
225        match parse(ftl_content) {
226            Ok(root) => {
227                let translator = OakFluentTranslatorCore::new(root);
228                self.translators.insert(locale.to_string(), translator);
229                Ok(())
230            }
231            Err(err) => Err(I18nError::ParseError(format!("{:?}", err))),
232        }
233    }
234}
235
236impl TranslationProvider for OakFluentTranslator {
237    fn translate(&self, key: &str, args: Option<&HashMap<String, String>>, locale: &str) -> Result<String, I18nError> {
238        // 尝试从当前语言获取翻译
239        if let Some(translator) = self.translators.get(locale)
240            && let Some(translation) = translator.translate(key, args.unwrap_or(&HashMap::new())) {
241            return Ok(translation);
242        }
243
244        // 回退到默认语言
245        if locale != self.default_locale
246            && let Some(translator) = self.translators.get(&self.default_locale)
247            && let Some(translation) = translator.translate(key, args.unwrap_or(&HashMap::new())) {
248            return Ok(translation);
249        }
250
251        // 回退到键本身
252        Ok(key.to_string())
253    }
254
255    fn get_locale(&self) -> &str {
256        &self.current_locale
257    }
258
259    fn set_locale(&mut self, locale: &str) {
260        self.current_locale = locale.to_string();
261    }
262
263    fn clone_box(&self) -> Box<dyn TranslationProvider + Send + Sync> {
264        // 创建一个新的 OakFluentTranslator 实例
265        let mut new_translator = OakFluentTranslator::new(&self.current_locale, &self.default_locale);
266
267        // 复制所有翻译器
268        for (locale, translator) in &self.translators {
269            // 由于 OakFluentTranslatorCore 实现了 Clone,我们可以直接克隆
270            new_translator.translators.insert(locale.clone(), translator.clone());
271        }
272
273        Box::new(new_translator)
274    }
275}
276
277// 实现 Send + Sync 因为 OakFluentTranslatorCore 是可克隆的且线程安全的
278unsafe impl Send for OakFluentTranslator {}
279
280unsafe impl Sync for OakFluentTranslator {}
281
282/// 翻译缓存键
283#[derive(Debug, Hash, PartialEq, Eq)]
284struct TranslationCacheKey {
285    key: String,
286    locale: String,
287    args: Option<Vec<(String, String)>>,
288}
289
290impl TranslationCacheKey {
291    /// 创建新的缓存键
292    fn new(key: &str, locale: &str, args: Option<&HashMap<String, String>>) -> Self {
293        let args_vec = args.map(|a| {
294            let mut vec: Vec<(String, String)> = a.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
295            vec.sort_by(|(a, _), (b, _)| a.cmp(b));
296            vec
297        });
298
299        Self { key: key.to_string(), locale: locale.to_string(), args: args_vec }
300    }
301}
302
303/// 国际化上下文
304pub struct I18nContext {
305    translator: Box<dyn TranslationProvider + Send + Sync>,
306    translation_cache: lru::LruCache<TranslationCacheKey, String>,
307    cache_capacity: usize,
308}
309
310impl Clone for I18nContext {
311    fn clone(&self) -> Self {
312        Self {
313            translator: self.translator.clone_box(),
314            translation_cache: lru::LruCache::new(NonZeroUsize::new(self.cache_capacity).unwrap()),
315            cache_capacity: self.cache_capacity,
316        }
317    }
318}
319
320impl I18nContext {
321    /// 创建新的国际化上下文
322    pub fn new(translator: Box<dyn TranslationProvider + Send + Sync>) -> Self {
323        Self { translator, translation_cache: lru::LruCache::new(NonZeroUsize::new(1000).unwrap()), cache_capacity: 1000 }
324    }
325
326    /// 创建带有自定义缓存容量的国际化上下文
327    pub fn with_cache_capacity(translator: Box<dyn TranslationProvider + Send + Sync>, capacity: usize) -> Self {
328        Self {
329            translator,
330            translation_cache: lru::LruCache::new(NonZeroUsize::new(capacity).unwrap()),
331            cache_capacity: capacity,
332        }
333    }
334
335    /// 翻译一个键
336    pub fn t(&mut self, key: &str) -> String {
337        self.t_with_args(key, None)
338    }
339
340    /// 翻译带参数的键
341    pub fn t_with_args(&mut self, key: &str, args: Option<&HashMap<String, String>>) -> String {
342        let locale = self.translator.get_locale();
343
344        // 构建缓存键
345        let cache_key = TranslationCacheKey::new(key, locale, args);
346
347        // 检查缓存
348        if let Some(cached) = self.translation_cache.get(&cache_key) {
349            return cached.clone();
350        }
351
352        // 翻译
353        let result = match self.translator.translate(key, args, locale) {
354            Ok(translation) => translation,
355            Err(e) => {
356                tracing::debug!("Translation error: {}, falling back to key", e);
357                key.to_string()
358            }
359        };
360
361        // 缓存结果
362        self.translation_cache.put(cache_key, result.clone());
363
364        result
365    }
366
367    /// 批量翻译
368    pub fn t_batch(&mut self, keys: &[(String, Option<HashMap<String, String>>)]) -> Vec<String> {
369        keys.iter().map(|(key, args)| self.t_with_args(key, args.as_ref())).collect()
370    }
371
372    /// 获取当前语言
373    pub fn get_locale(&self) -> &str {
374        self.translator.get_locale()
375    }
376
377    /// 设置当前语言
378    pub fn set_locale(&mut self, locale: &str) {
379        self.translator.set_locale(locale);
380        self.clear_cache(); // 语言切换时清除缓存
381    }
382
383    /// 清除翻译缓存
384    pub fn clear_cache(&mut self) {
385        self.translation_cache.clear();
386    }
387
388    /// 获取缓存大小
389    pub fn cache_size(&self) -> usize {
390        self.translation_cache.len()
391    }
392}
393
394lazy_static::lazy_static! {
395    /// 全局国际化实例
396    pub static ref I18N: std::sync::Mutex<I18nContext> = {
397        // 创建默认的 Oak Fluent 翻译提供者
398        let mut translator = OakFluentTranslator::new("en", "en");
399
400        // 尝试加载默认语言资源
401        let default_locale = std::env::var("LANG").unwrap_or_else(|_| "en".to_string());
402
403        // 加载英文资源
404        if let Ok(english_path) = std::env::var("ITOOLS_LOCALE_EN") {
405            let path = Path::new(&english_path);
406            if path.exists() && let Err(e) = translator.add_translations_from_file("en", path) {
407                info!("Failed to load English translations: {}", e);
408            }
409        }
410
411        // 加载当前语言资源
412        if default_locale != "en" && let Ok(locale_path) = std::env::var(format!("ITOOLS_LOCALE_{}", default_locale.to_uppercase().replace('-', "_"))) {
413            let path = Path::new(&locale_path);
414            if path.exists() && let Err(e) = translator.add_translations_from_file(&default_locale, path) {
415                info!("Failed to load {} translations: {}", default_locale, e);
416            }
417        }
418
419        std::sync::Mutex::new(I18nContext::new(Box::new(translator)))
420    };
421}
422
423/// 获取翻译
424pub fn t(key: &str) -> String {
425    let mut i18n = I18N.lock().unwrap();
426    i18n.t(key)
427}
428
429/// 获取带参数的翻译
430pub fn t_with_args(key: &str, args: &[(&str, &str)]) -> String {
431    let mut i18n = I18N.lock().unwrap();
432    let args_map: HashMap<String, String> = args.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
433    i18n.t_with_args(key, Some(&args_map))
434}
435
436/// 设置当前语言
437pub fn set_locale(locale: &str) {
438    let mut i18n = I18N.lock().unwrap();
439    i18n.set_locale(locale);
440}
441
442/// 获取当前语言
443pub fn get_locale() -> String {
444    let i18n = I18N.lock().unwrap();
445    i18n.get_locale().to_string()
446}