1use std::borrow::Cow;
2use std::collections::HashMap;
3
4pub trait Backend: Send + Sync + 'static {
6 fn available_locales(&self) -> Vec<Cow<'_, str>>;
8 fn translate(&self, locale: &str, key: &str) -> Option<Cow<'_, str>>;
10 fn messages_for_locale(&self, locale: &str) -> Option<Vec<(Cow<'_, str>, Cow<'_, str>)>>;
12}
13
14pub trait BackendExt: Backend {
15 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
68pub struct SimpleBackend {
70 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 pub fn new() -> Self {
99 SimpleBackend {
100 translations: HashMap::new(),
101 }
102 }
103
104 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}