rust_i18n_support/
backend.rs

1use std::collections::HashMap;
2
3/// I18n backend trait
4pub trait Backend: Send + Sync + 'static {
5    /// Return the available locales
6    fn available_locales(&self) -> Vec<&str>;
7    /// Get the translation for the given locale and key
8    fn translate(&self, locale: &str, key: &str) -> Option<&str>;
9}
10
11pub trait BackendExt: Backend {
12    /// Extend backend to add more translations
13    fn extend<T: Backend>(self, other: T) -> CombinedBackend<Self, T>
14    where
15        Self: Sized,
16    {
17        CombinedBackend(self, other)
18    }
19}
20
21pub struct CombinedBackend<A, B>(A, B);
22
23impl<A, B> Backend for CombinedBackend<A, B>
24where
25    A: Backend,
26    B: Backend,
27{
28    fn available_locales(&self) -> Vec<&str> {
29        let mut available_locales = self.0.available_locales();
30        for locale in self.1.available_locales() {
31            if !available_locales.contains(&locale) {
32                available_locales.push(locale);
33            }
34        }
35        available_locales
36    }
37
38    #[inline]
39    fn translate(&self, locale: &str, key: &str) -> Option<&str> {
40        self.1
41            .translate(locale, key)
42            .or_else(|| self.0.translate(locale, key))
43    }
44}
45
46/// Simple KeyValue storage backend
47pub struct SimpleBackend {
48    /// All translations key is flatten key, like `en.hello.world`
49    translations: HashMap<String, HashMap<String, String>>,
50}
51
52impl SimpleBackend {
53    /// Create a new SimpleBackend.
54    pub fn new() -> Self {
55        SimpleBackend {
56            translations: HashMap::new(),
57        }
58    }
59
60    /// Add more translations for the given locale.
61    ///
62    /// ```no_run
63    /// # use std::collections::HashMap;
64    /// # use rust_i18n_support::SimpleBackend;
65    /// # let mut backend = SimpleBackend::new();
66    /// let mut trs = HashMap::<&str, &str>::new();
67    /// trs.insert("hello", "Hello");
68    /// trs.insert("foo", "Foo bar");
69    /// backend.add_translations("en", &trs);
70    /// ```
71    pub fn add_translations(&mut self, locale: &str, data: &HashMap<&str, &str>) {
72        let data = data
73            .iter()
74            .map(|(k, v)| ((*k).into(), (*v).into()))
75            .collect::<HashMap<_, _>>();
76
77        let trs = self.translations.entry(locale.into()).or_default();
78        trs.extend(data);
79    }
80}
81
82impl Backend for SimpleBackend {
83    fn available_locales(&self) -> Vec<&str> {
84        let mut locales = self
85            .translations
86            .keys()
87            .map(|k| k.as_str())
88            .collect::<Vec<_>>();
89        locales.sort();
90        locales
91    }
92
93    fn translate(&self, locale: &str, key: &str) -> Option<&str> {
94        if let Some(trs) = self.translations.get(locale) {
95            return trs.get(key).map(|s| s.as_str());
96        }
97
98        None
99    }
100}
101
102impl BackendExt for SimpleBackend {}
103
104impl Default for SimpleBackend {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use std::collections::HashMap;
113
114    use super::SimpleBackend;
115    use super::{Backend, BackendExt};
116
117    #[test]
118    fn test_simple_backend() {
119        let mut backend = SimpleBackend::new();
120        let mut data = HashMap::<&str, &str>::new();
121        data.insert("hello", "Hello");
122        data.insert("foo", "Foo bar");
123        backend.add_translations("en", &data);
124
125        let mut data_cn = HashMap::<&str, &str>::new();
126        data_cn.insert("hello", "你好");
127        data_cn.insert("foo", "Foo 测试");
128        backend.add_translations("zh-CN", &data_cn);
129
130        assert_eq!(backend.translate("en", "hello"), Some("Hello"));
131        assert_eq!(backend.translate("en", "foo"), Some("Foo bar"));
132        assert_eq!(backend.translate("zh-CN", "hello"), Some("你好"));
133        assert_eq!(backend.translate("zh-CN", "foo"), Some("Foo 测试"));
134
135        assert_eq!(backend.available_locales(), vec!["en", "zh-CN"]);
136    }
137
138    #[test]
139    fn test_combined_backend() {
140        let mut backend = SimpleBackend::new();
141        let mut data = HashMap::<&str, &str>::new();
142        data.insert("hello", "Hello");
143        data.insert("foo", "Foo bar");
144        backend.add_translations("en", &data);
145
146        let mut data_cn = HashMap::<&str, &str>::new();
147        data_cn.insert("hello", "你好");
148        data_cn.insert("foo", "Foo 测试");
149        backend.add_translations("zh-CN", &data_cn);
150
151        let mut backend2 = SimpleBackend::new();
152        let mut data2 = HashMap::<&str, &str>::new();
153        data2.insert("hello", "Hello2");
154        backend2.add_translations("en", &data2);
155
156        let mut data_cn2 = HashMap::<&str, &str>::new();
157        data_cn2.insert("hello", "你好2");
158        backend2.add_translations("zh-CN", &data_cn2);
159
160        let combined = backend.extend(backend2);
161        assert_eq!(combined.translate("en", "hello"), Some("Hello2"));
162        assert_eq!(combined.translate("zh-CN", "hello"), Some("你好2"));
163
164        assert_eq!(combined.available_locales(), vec!["en", "zh-CN"]);
165    }
166}