mas_i18n/
translations.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    ops::Deref,
10};
11
12use icu_plurals::PluralCategory;
13use serde::{
14    Deserialize, Deserializer, Serialize, Serializer,
15    de::{MapAccess, Visitor},
16    ser::SerializeMap,
17};
18
19use crate::sprintf::Message;
20
21fn plural_category_as_str(category: PluralCategory) -> &'static str {
22    match category {
23        PluralCategory::Zero => "zero",
24        PluralCategory::One => "one",
25        PluralCategory::Two => "two",
26        PluralCategory::Few => "few",
27        PluralCategory::Many => "many",
28        PluralCategory::Other => "other",
29    }
30}
31
32pub type TranslationTree = Tree;
33
34#[derive(Debug, Clone, Deserialize, Default)]
35pub struct Metadata {
36    #[serde(skip)]
37    // We don't want to deserialize it, as we're resetting it every time
38    // This then generates the `context` field when serializing
39    pub context_locations: BTreeSet<String>,
40    pub description: Option<String>,
41}
42
43impl Serialize for Metadata {
44    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
45    where
46        S: Serializer,
47    {
48        let context = self
49            .context_locations
50            .iter()
51            .map(String::as_str)
52            .collect::<Vec<&str>>()
53            .join(", ");
54
55        let mut map = serializer.serialize_map(None)?;
56
57        if !context.is_empty() {
58            map.serialize_entry("context", &context)?;
59        }
60
61        if let Some(description) = &self.description {
62            map.serialize_entry("description", description)?;
63        }
64
65        map.end()
66    }
67}
68
69impl Metadata {
70    fn add_location(&mut self, location: String) {
71        self.context_locations.insert(location);
72    }
73}
74
75#[derive(Debug, Clone, Default)]
76pub struct Tree {
77    inner: BTreeMap<String, Node>,
78}
79
80#[derive(Debug, Clone)]
81pub struct Node {
82    metadata: Option<Metadata>,
83    value: Value,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87#[serde(untagged)]
88pub enum Value {
89    Tree(Tree),
90    Leaf(Message),
91}
92
93impl<'de> Deserialize<'de> for Tree {
94    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95    where
96        D: Deserializer<'de>,
97    {
98        struct TreeVisitor;
99
100        impl<'de> Visitor<'de> for TreeVisitor {
101            type Value = Tree;
102
103            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
104                formatter.write_str("map")
105            }
106
107            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
108            where
109                A: MapAccess<'de>,
110            {
111                let mut tree: BTreeMap<String, Node> = BTreeMap::new();
112                let mut metadata_map: BTreeMap<String, Metadata> = BTreeMap::new();
113
114                while let Some(key) = map.next_key::<String>()? {
115                    if let Some(name) = key.strip_prefix('@') {
116                        let metadata = map.next_value::<Metadata>()?;
117                        metadata_map.insert(name.to_owned(), metadata);
118                    } else {
119                        let value = map.next_value::<Value>()?;
120                        tree.insert(
121                            key,
122                            Node {
123                                metadata: None,
124                                value,
125                            },
126                        );
127                    }
128                }
129
130                for (key, meta) in metadata_map {
131                    if let Some(node) = tree.get_mut(&key) {
132                        node.metadata = Some(meta);
133                    }
134                }
135
136                Ok(Tree { inner: tree })
137            }
138        }
139
140        deserializer.deserialize_any(TreeVisitor)
141    }
142}
143
144impl Serialize for Tree {
145    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
146    where
147        S: Serializer,
148    {
149        let mut map = serializer.serialize_map(None)?;
150
151        for (key, value) in &self.inner {
152            map.serialize_entry(key, &value.value)?;
153            if let Some(meta) = &value.metadata {
154                map.serialize_entry(&format!("@{key}"), meta)?;
155            }
156        }
157
158        map.end()
159    }
160}
161
162impl Tree {
163    /// Get a message from the tree by key.
164    ///
165    /// Returns `None` if the requested key is not found.
166    #[must_use]
167    pub fn message(&self, key: &str) -> Option<&Message> {
168        let keys = key.split('.');
169        let node = self.walk_path(keys)?;
170        let message = node.value.as_message()?;
171        Some(message)
172    }
173
174    /// Get a pluralized message from the tree by key and plural category.
175    ///
176    /// If the key doesn't have plural variants, this will return the message
177    /// itself. Returns the "other" category if the requested category is
178    /// not found. Returns `None` if the requested key is not found.
179    #[must_use]
180    pub fn pluralize(&self, key: &str, category: PluralCategory) -> Option<&Message> {
181        let keys = key.split('.');
182        let node = self.walk_path(keys)?;
183
184        let subtree = match &node.value {
185            Value::Leaf(message) => return Some(message),
186            Value::Tree(tree) => tree,
187        };
188
189        let node = if let Some(node) = subtree.inner.get(plural_category_as_str(category)) {
190            node
191        } else {
192            // Fallback to the "other" category
193            subtree.inner.get("other")?
194        };
195
196        let message = node.value.as_message()?;
197        Some(message)
198    }
199
200    #[doc(hidden)]
201    pub fn set_if_not_defined<K: Deref<Target = str>, I: IntoIterator<Item = K>>(
202        &mut self,
203        path: I,
204        value: Message,
205        location: Option<String>,
206    ) -> bool {
207        // We're temporarily moving the tree out of the struct to be able to nicely
208        // iterate on it
209        let mut fake_root = Node {
210            metadata: None,
211            value: Value::Tree(Tree {
212                inner: std::mem::take(&mut self.inner),
213            }),
214        };
215
216        let mut node = &mut fake_root;
217        for key in path {
218            match &mut node.value {
219                Value::Tree(tree) => {
220                    node = tree.inner.entry(key.deref().to_owned()).or_insert(Node {
221                        metadata: None,
222                        value: Value::Tree(Tree::default()),
223                    });
224                }
225                Value::Leaf(_) => {
226                    panic!()
227                }
228            }
229        }
230
231        let replaced = match &node.value {
232            Value::Tree(tree) => {
233                assert!(
234                    tree.inner.is_empty(),
235                    "Trying to overwrite a non-empty tree"
236                );
237
238                node.value = Value::Leaf(value);
239                true
240            }
241            Value::Leaf(_) => {
242                // Do not overwrite existing values
243                false
244            }
245        };
246
247        if let Some(location) = location {
248            node.metadata
249                .get_or_insert(Metadata::default())
250                .add_location(location);
251        }
252
253        // Restore the original tree at the end of the function
254        match fake_root {
255            Node {
256                value: Value::Tree(tree),
257                ..
258            } => self.inner = tree.inner,
259            _ => panic!("Tried to replace the root node"),
260        }
261
262        replaced
263    }
264
265    fn walk_path<K: Deref<Target = str>, I: IntoIterator<Item = K>>(
266        &self,
267        path: I,
268    ) -> Option<&Node> {
269        let mut iterator = path.into_iter();
270        let next = iterator.next()?;
271        self.walk_path_inner(next, iterator)
272    }
273
274    fn walk_path_inner<K: Deref<Target = str>, I: Iterator<Item = K>>(
275        &self,
276        next_key: K,
277        mut path: I,
278    ) -> Option<&Node> {
279        let next = self.inner.get(&*next_key)?;
280
281        match path.next() {
282            Some(next_key) => match &next.value {
283                Value::Tree(tree) => tree.walk_path_inner(next_key, path),
284                Value::Leaf(_) => None,
285            },
286            None => Some(next),
287        }
288    }
289}
290
291impl Value {
292    fn as_message(&self) -> Option<&Message> {
293        match self {
294            Value::Leaf(message) => Some(message),
295            Value::Tree(_) => None,
296        }
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use crate::sprintf::{ArgumentList, arg_list};
304
305    #[test]
306    fn test_it_works() {
307        let tree = serde_json::json!({
308            "hello": "world",
309            "damals": {
310              "about_x_hours_ago": {
311                "one":   "about one hour ago",
312                "other": "about %(count)s hours ago"
313              }
314            }
315        });
316
317        let result: Result<TranslationTree, _> = serde_json::from_value(tree);
318        assert!(result.is_ok());
319        let tree = result.unwrap();
320        let message = tree.message("hello");
321        assert!(message.is_some());
322        let message = message.unwrap();
323        assert_eq!(message.format(&ArgumentList::default()).unwrap(), "world");
324
325        let message = tree.message("damals.about_x_hours_ago.one");
326        assert!(message.is_some());
327        let message = message.unwrap();
328        assert_eq!(message.format(&arg_list!()).unwrap(), "about one hour ago");
329
330        let message = tree.pluralize("damals.about_x_hours_ago", PluralCategory::Other);
331        assert!(message.is_some());
332        let message = message.unwrap();
333        assert_eq!(
334            message.format(&arg_list!(count = 2)).unwrap(),
335            "about 2 hours ago"
336        );
337    }
338}