Skip to main content

rust_i18n_support/
backend.rs

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