1use std::borrow::Cow;
2use std::collections::HashMap;
3
4pub struct NamespacedBackend {
6 backend: &'static dyn Backend,
7 namespace: &'static str,
8}
9
10impl NamespacedBackend {
11 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
50pub trait Backend: Send + Sync + 'static {
52 fn available_locales(&self) -> Vec<Cow<'_, str>>;
54 fn translate(&self, locale: &str, key: &str) -> Option<Cow<'_, str>>;
56 fn messages_for_locale(&self, locale: &str) -> Option<Vec<(Cow<'_, str>, Cow<'_, str>)>>;
58}
59
60pub trait BackendExt: Backend {
61 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
114pub struct SimpleBackend {
116 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 pub fn new() -> Self {
145 SimpleBackend {
146 translations: HashMap::new(),
147 }
148 }
149
150 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}