Skip to main content

rust_i18n_support/
backend.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3
4/// A view of another backend restricted to a single namespace.
5pub struct NamespacedBackend {
6    backend: &'static dyn Backend,
7    namespace: &'static str,
8}
9
10impl NamespacedBackend {
11    /// Create a backend that exposes only keys below `namespace`.
12    pub fn new(backend: &'static dyn Backend, namespace: &'static str) -> Self {
13        Self { backend, namespace }
14    }
15
16    fn namespaced_key(&self, key: &str) -> String {
17        format!("{}.{key}", self.namespace)
18    }
19}
20
21impl Backend for NamespacedBackend {
22    fn available_locales(&self) -> Vec<Cow<'_, str>> {
23        self.backend
24            .available_locales()
25            .into_iter()
26            .filter(|locale| self.messages_for_locale(locale).is_some())
27            .collect()
28    }
29
30    fn translate(&self, locale: &str, key: &str) -> Option<Cow<'_, str>> {
31        self.backend.translate(locale, &self.namespaced_key(key))
32    }
33
34    fn messages_for_locale(&self, locale: &str) -> Option<Vec<(Cow<'_, str>, Cow<'_, str>)>> {
35        let prefix = format!("{}.", self.namespace);
36        let messages = self
37            .backend
38            .messages_for_locale(locale)?
39            .into_iter()
40            .filter_map(|(key, value)| {
41                key.strip_prefix(&prefix)
42                    .map(|key| (Cow::Owned(key.to_string()), value))
43            })
44            .collect::<Vec<_>>();
45
46        (!messages.is_empty()).then_some(messages)
47    }
48}
49
50/// I18n backend trait
51pub trait Backend: Send + Sync + 'static {
52    /// Return the available locales
53    fn available_locales(&self) -> Vec<Cow<'_, str>>;
54    /// Get the translation for the given locale and key
55    fn translate(&self, locale: &str, key: &str) -> Option<Cow<'_, str>>;
56    /// Get all translations for the given locale
57    fn messages_for_locale(&self, locale: &str) -> Option<Vec<(Cow<'_, str>, Cow<'_, str>)>>;
58}
59
60pub trait BackendExt: Backend {
61    /// Extend backend to add more translations
62    fn extend<T: Backend>(self, other: T) -> CombinedBackend<Self, T>
63    where
64        Self: Sized,
65    {
66        CombinedBackend(self, other)
67    }
68}
69
70pub struct CombinedBackend<A, B>(A, B);
71
72impl<A, B> Backend for CombinedBackend<A, B>
73where
74    A: Backend,
75    B: Backend,
76{
77    fn available_locales(&self) -> Vec<Cow<'_, str>> {
78        let mut available_locales = self.0.available_locales();
79        for locale in self.1.available_locales() {
80            if !available_locales.contains(&locale) {
81                available_locales.push(locale);
82            }
83        }
84        available_locales
85    }
86
87    #[inline]
88    fn translate(&self, locale: &str, key: &str) -> Option<Cow<'_, str>> {
89        self.1
90            .translate(locale, key)
91            .or_else(|| self.0.translate(locale, key))
92    }
93
94    fn messages_for_locale(&self, locale: &str) -> Option<Vec<(Cow<'_, str>, Cow<'_, str>)>> {
95        match (
96            self.1.messages_for_locale(locale),
97            self.0.messages_for_locale(locale),
98        ) {
99            (None, None) => None,
100            (None, a) => a,
101            (b, None) => b,
102            (Some(b), Some(a)) => Some(
103                b.into_iter()
104                    .chain(
105                        a.into_iter()
106                            .filter(|(k, _)| self.1.translate(locale, k).is_none()),
107                    )
108                    .collect(),
109            ),
110        }
111    }
112}
113
114/// Simple KeyValue storage backend
115pub struct SimpleBackend {
116    /// All translations key is flatten key, like `en.hello.world`
117    translations: HashMap<Cow<'static, str>, HashMap<Cow<'static, str>, Cow<'static, str>>>,
118}
119
120impl
121    FromIterator<(
122        Cow<'static, str>,
123        HashMap<Cow<'static, str>, Cow<'static, str>>,
124    )> for SimpleBackend
125{
126    fn from_iter<
127        I: IntoIterator<
128            Item = (
129                Cow<'static, str>,
130                HashMap<Cow<'static, str>, Cow<'static, str>>,
131            ),
132        >,
133    >(
134        iter: I,
135    ) -> Self {
136        Self {
137            translations: iter.into_iter().collect(),
138        }
139    }
140}
141
142impl SimpleBackend {
143    /// Create a new SimpleBackend.
144    pub fn new() -> Self {
145        SimpleBackend {
146            translations: HashMap::new(),
147        }
148    }
149
150    /// Add more translations for the given locale.
151    ///
152    /// ```no_run
153    /// # use std::collections::HashMap;
154    /// # use rust_i18n_support::SimpleBackend;
155    /// # let mut backend = SimpleBackend::new();
156    /// let mut trs = HashMap::new();
157    /// trs.insert("hello".into(), "Hello".into());
158    /// trs.insert("foo".into(), "Foo bar".into());
159    /// backend.add_translations("en".into(), trs);
160    /// ```
161    pub fn add_translations(
162        &mut self,
163        locale: Cow<'static, str>,
164        data: HashMap<Cow<'static, str>, Cow<'static, str>>,
165    ) {
166        let trs = self.translations.entry(locale.into()).or_default();
167        trs.extend(data);
168    }
169}
170
171impl Backend for SimpleBackend {
172    fn available_locales(&self) -> Vec<Cow<'_, str>> {
173        let mut locales = self.translations.keys().cloned().collect::<Vec<_>>();
174        locales.sort();
175        locales
176    }
177
178    fn translate(&self, locale: &str, key: &str) -> Option<Cow<'_, str>> {
179        if let Some(trs) = self.translations.get(locale) {
180            return trs.get(key).cloned();
181        }
182
183        None
184    }
185
186    fn messages_for_locale(&self, locale: &str) -> Option<Vec<(Cow<'_, str>, Cow<'_, str>)>> {
187        self.translations
188            .get(locale)
189            .map(|trs| trs.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
190    }
191}
192
193impl BackendExt for SimpleBackend {}
194
195impl Default for SimpleBackend {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use std::borrow::Cow;
204    use std::collections::HashMap;
205
206    use super::SimpleBackend;
207    use super::{Backend, BackendExt, NamespacedBackend};
208
209    #[test]
210    fn test_simple_backend() {
211        let mut backend = SimpleBackend::new();
212        let mut data = HashMap::new();
213        data.insert("hello".into(), "Hello".into());
214        data.insert("foo".into(), "Foo bar".into());
215        backend.add_translations("en".into(), data);
216
217        let mut data_cn = HashMap::new();
218        data_cn.insert("hello".into(), "你好".into());
219        data_cn.insert("foo".into(), "Foo 测试".into());
220        backend.add_translations("zh-CN".into(), data_cn);
221
222        assert_eq!(backend.translate("en", "hello"), Some(Cow::from("Hello")));
223        assert_eq!(backend.translate("en", "foo"), Some(Cow::from("Foo bar")));
224        assert_eq!(backend.translate("zh-CN", "hello"), Some(Cow::from("你好")));
225        assert_eq!(
226            backend.translate("zh-CN", "foo"),
227            Some(Cow::from("Foo 测试"))
228        );
229
230        assert_eq!(backend.available_locales(), vec!["en", "zh-CN"]);
231    }
232
233    #[test]
234    fn test_combined_backend() {
235        let mut backend = SimpleBackend::new();
236        let mut data = HashMap::new();
237        data.insert("hello".into(), "Hello".into());
238        data.insert("foo".into(), "Foo bar".into());
239        backend.add_translations("en".into(), data);
240
241        let mut data_cn = HashMap::new();
242        data_cn.insert("hello".into(), "你好".into());
243        data_cn.insert("foo".into(), "Foo 测试".into());
244        backend.add_translations("zh-CN".into(), data_cn);
245
246        let mut backend2 = SimpleBackend::new();
247        let mut data2 = HashMap::new();
248        data2.insert("hello".into(), "Hello2".into());
249        backend2.add_translations("en".into(), data2);
250
251        let mut data_cn2 = HashMap::new();
252        data_cn2.insert("hello".into(), "你好2".into());
253        backend2.add_translations("zh-CN".into(), data_cn2);
254
255        let combined = backend.extend(backend2);
256        assert_eq!(combined.translate("en", "hello"), Some(Cow::from("Hello2")));
257        assert_eq!(
258            combined.translate("zh-CN", "hello"),
259            Some(Cow::from("你好2"))
260        );
261
262        assert_eq!(combined.available_locales(), vec!["en", "zh-CN"]);
263    }
264
265    #[test]
266    fn test_namespaced_backend() {
267        let mut backend = SimpleBackend::new();
268        let mut data = HashMap::new();
269        data.insert("ui_component.title".into(), "Custom title".into());
270        data.insert("title".into(), "Unrelated title".into());
271        backend.add_translations("en".into(), data);
272
273        let backend = Box::leak(Box::new(backend));
274        let namespaced = NamespacedBackend::new(backend, "ui_component");
275
276        assert_eq!(
277            namespaced.translate("en", "title"),
278            Some(Cow::Borrowed("Custom title"))
279        );
280        assert_eq!(
281            namespaced.messages_for_locale("en"),
282            Some(vec![(
283                Cow::Owned("title".to_string()),
284                Cow::Borrowed("Custom title")
285            )])
286        );
287    }
288}