Skip to main content

i_slint_compiler/
translations.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use crate::llr::Expression;
5use smol_str::{SmolStr, ToSmolStr};
6use std::collections::HashMap;
7use std::collections::hash_map::Entry;
8use std::path::Path;
9use std::rc::Rc;
10
11#[derive(Clone, Debug)]
12pub struct Translations {
13    /// An array with all the array of string
14    /// The first vector index is stored in the LLR.
15    /// The inner vector index is the language id. (The first is the original)
16    /// Only contains the string that are not having plural forms
17    pub strings: Vec<Vec<Option<SmolStr>>>,
18    /// An array with all the strings that are used in a plural form.
19    /// The first vector index is stored in the LLR.
20    /// The inner vector index is the language. (The first is the original string)
21    /// The last vector contains each form
22    pub plurals: Vec<Vec<Option<Vec<SmolStr>>>>,
23
24    /// Expression is a function that maps its first and only argument (an integer)
25    /// to the plural form index (an integer)
26    /// It can only do basic mathematical operations.
27    /// The expression cannot reference properties or variable.
28    /// Only builtin math functions, and its first argument
29    pub plural_rules: Vec<Option<Expression>>,
30
31    /// The "names" of the languages
32    pub languages: Vec<SmolStr>,
33}
34
35#[derive(Clone)]
36pub struct TranslationsBuilder {
37    result: Translations,
38    /// Maps (msgid, msgid_plural, msgctx) to the index in the result
39    /// (the index is in strings or plurals depending if there is a plural)
40    map: HashMap<(SmolStr, SmolStr, SmolStr), usize>,
41
42    /// The catalog containing the translations
43    catalogs: Rc<Vec<rspolib::POFile>>,
44}
45
46impl TranslationsBuilder {
47    pub fn load_translations(path: &Path, domain: &str) -> std::io::Result<Self> {
48        let mut languages = vec!["".into()];
49        let mut catalogs = Vec::new();
50        let mut plural_rules =
51            vec![Some(plural_rule_parser::parse_rule_expression("n!=1").unwrap())];
52        for l in std::fs::read_dir(path)
53            .map_err(|e| std::io::Error::other(format!("Error reading directory {path:?}: {e}")))?
54        {
55            let l = l?;
56            let path = l.path().join("LC_MESSAGES").join(format!("{domain}.po"));
57            if path.exists() {
58                let catalog = rspolib::pofile(path.as_path()).map_err(|e| {
59                    std::io::Error::other(format!("Error parsing {}: {e}", path.display()))
60                })?;
61                languages.push(l.file_name().to_string_lossy().into());
62
63                let expr = if let Some(header) = catalog.metadata.get("Plural-Forms") {
64                    let plural_expr = header.split(';').find_map(|sub_entry| {
65                        let (key, expression) = sub_entry.split_once('=')?;
66                        (key.trim() == "plural").then(|| expression)
67                    });
68                    plural_expr.ok_or_else(|| {
69                        std::io::Error::other(format!(
70                            "Error parsing plural rules in {}",
71                            path.display()
72                        ))
73                    })?
74                } else {
75                    "n != 1"
76                };
77                plural_rules.push(Some(plural_rule_parser::parse_rule_expression(&expr).map_err(
78                    |_| {
79                        std::io::Error::other(format!(
80                            "Error parsing plural rules in {}",
81                            path.display()
82                        ))
83                    },
84                )?));
85
86                catalogs.push(catalog);
87            }
88        }
89        if catalogs.is_empty() {
90            return Err(std::io::Error::other(format!(
91                "No translations found. We look for files in '{}/<lang>/LC_MESSAGES/{domain}.po",
92                path.display()
93            )));
94        }
95        Ok(Self {
96            result: Translations {
97                strings: Vec::new(),
98                plurals: Vec::new(),
99                plural_rules,
100                languages,
101            },
102            map: HashMap::new(),
103            catalogs: Rc::new(catalogs),
104        })
105    }
106
107    pub fn lower_translate_call(&mut self, args: Vec<Expression>) -> Expression {
108        let [original, contextid, _domain, format_args, n, plural] = args
109            .try_into()
110            .expect("The resolving pass should have ensured that the arguments are correct");
111        let original = get_string(original).expect("original must be a string");
112        let contextid = get_string(contextid).expect("contextid must be a string");
113        let plural = get_string(plural).expect("plural must be a string");
114
115        let is_plural =
116            !plural.is_empty() || !matches!(n, Expression::NumberLiteral(f) if f == 1.0);
117
118        match self.map.entry((original.clone(), plural.clone(), contextid.clone())) {
119            Entry::Occupied(entry) => Expression::TranslationReference {
120                format_args: format_args.into(),
121                string_index: *entry.get(),
122                plural: is_plural.then(|| n.into()),
123            },
124            Entry::Vacant(entry) => {
125                let messages = self.catalogs.iter().map(|catalog| {
126                    catalog.find_by_msgid_msgctxt(original.as_str(), contextid.as_str())
127                });
128                let idx = if is_plural {
129                    let messages = std::iter::once(Some(vec![original.clone(), plural.clone()]))
130                        .chain(messages.map(|opt_entry| {
131                            opt_entry.and_then(|entry| {
132                                if entry.msgstr_plural.is_empty() {
133                                    None
134                                } else {
135                                    Some(
136                                        entry
137                                            .msgstr_plural
138                                            .iter()
139                                            .map(|s| s.to_smolstr())
140                                            .collect(),
141                                    )
142                                }
143                            })
144                        }))
145                        .collect();
146                    self.result.plurals.push(messages);
147                    self.result.plurals.len() - 1
148                } else {
149                    let messages = std::iter::once(Some(original.clone()))
150                        .chain(messages.map(|opt_entry| {
151                            opt_entry.and_then(|entry| entry.msgstr.map(|s| s.to_smolstr()))
152                        }))
153                        .collect::<Vec<_>>();
154                    self.result.strings.push(messages);
155                    self.result.strings.len() - 1
156                };
157                Expression::TranslationReference {
158                    format_args: format_args.into(),
159                    string_index: *entry.insert(idx),
160                    plural: is_plural.then(|| n.into()),
161                }
162            }
163        }
164    }
165
166    pub fn result(self) -> Translations {
167        self.result
168    }
169
170    pub fn collect_characters_seen(&self, characters_seen: &mut impl Extend<char>) {
171        characters_seen.extend(
172            self.catalogs
173                .iter()
174                .flat_map(|catalog| {
175                    catalog.entries.iter().flat_map(|entry| {
176                        entry
177                            .msgstr
178                            .iter()
179                            .map(|s| s.as_str())
180                            .chain(entry.msgstr_plural.iter().map(|s| s.as_str()))
181                    })
182                })
183                .flat_map(|str| str.chars()),
184        );
185    }
186}
187
188fn get_string(plural: Expression) -> Option<SmolStr> {
189    match plural {
190        Expression::StringLiteral(s) => Some(s),
191        _ => None,
192    }
193}
194
195mod plural_rule_parser {
196    use super::Expression;
197    pub struct ParseError<'a>(&'static str, &'a [u8]);
198    impl std::fmt::Debug for ParseError<'_> {
199        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200            write!(f, "ParseError({}, rest={:?})", self.0, std::str::from_utf8(self.1).unwrap())
201        }
202    }
203    pub fn parse_rule_expression(string: &str) -> Result<Expression, ParseError<'_>> {
204        let ascii = string.as_bytes();
205        let s = parse_expression(ascii)?;
206        if !s.rest.is_empty() {
207            return Err(ParseError("extra character in string", s.rest));
208        }
209        match s.ty {
210            Ty::Number => Ok(s.expr),
211            Ty::Boolean => Ok(Expression::Condition {
212                condition: s.expr.into(),
213                true_expr: Expression::NumberLiteral(1.).into(),
214                false_expr: Expression::NumberLiteral(0.).into(),
215            }),
216        }
217    }
218
219    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
220    enum Ty {
221        Number,
222        Boolean,
223    }
224
225    struct ParsingState<'a> {
226        expr: Expression,
227        rest: &'a [u8],
228        ty: Ty,
229    }
230
231    impl ParsingState<'_> {
232        fn skip_whitespace(self) -> Self {
233            let rest = skip_whitespace(self.rest);
234            Self { rest, ..self }
235        }
236    }
237
238    /// `<condition> ('?' <expr> : <expr> )?`
239    fn parse_expression(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
240        let string = skip_whitespace(string);
241        let state = parse_condition(string)?.skip_whitespace();
242        if state.ty != Ty::Boolean {
243            return Ok(state);
244        }
245        if let Some(rest) = state.rest.strip_prefix(b"?") {
246            let s1 = parse_expression(rest)?.skip_whitespace();
247            let rest = s1.rest.strip_prefix(b":").ok_or(ParseError("expected ':'", s1.rest))?;
248            let s2 = parse_expression(rest)?;
249            if s1.ty != s2.ty {
250                return Err(ParseError("incompatible types in ternary operator", s2.rest));
251            }
252            Ok(ParsingState {
253                expr: Expression::Condition {
254                    condition: state.expr.into(),
255                    true_expr: s1.expr.into(),
256                    false_expr: s2.expr.into(),
257                },
258                rest: skip_whitespace(s2.rest),
259                ty: s2.ty,
260            })
261        } else {
262            Ok(state)
263        }
264    }
265
266    /// `<and_expr> ("||" <condition>)?`
267    fn parse_condition(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
268        let string = skip_whitespace(string);
269        let state = parse_and_expr(string)?.skip_whitespace();
270        if state.rest.is_empty() {
271            return Ok(state);
272        }
273        if let Some(rest) = state.rest.strip_prefix(b"||") {
274            let state2 = parse_condition(rest)?;
275            if state.ty != Ty::Boolean || state2.ty != Ty::Boolean {
276                return Err(ParseError("incompatible types in || operator", state2.rest));
277            }
278            Ok(ParsingState {
279                expr: Expression::BinaryExpression {
280                    lhs: state.expr.into(),
281                    rhs: state2.expr.into(),
282                    op: '|',
283                },
284                ty: Ty::Boolean,
285                rest: skip_whitespace(state2.rest),
286            })
287        } else {
288            Ok(state)
289        }
290    }
291
292    /// `<cmp_expr> ("&&" <and_expr>)?`
293    fn parse_and_expr(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
294        let string = skip_whitespace(string);
295        let state = parse_cmp_expr(string)?.skip_whitespace();
296        if state.rest.is_empty() {
297            return Ok(state);
298        }
299        if let Some(rest) = state.rest.strip_prefix(b"&&") {
300            let state2 = parse_and_expr(rest)?;
301            if state.ty != Ty::Boolean || state2.ty != Ty::Boolean {
302                return Err(ParseError("incompatible types in || operator", state2.rest));
303            }
304            Ok(ParsingState {
305                expr: Expression::BinaryExpression {
306                    lhs: state.expr.into(),
307                    rhs: state2.expr.into(),
308                    op: '&',
309                },
310                ty: Ty::Boolean,
311                rest: skip_whitespace(state2.rest),
312            })
313        } else {
314            Ok(state)
315        }
316    }
317
318    /// `<value> ('=='|'!='|'<'|'>'|'<='|'>=' <cmp_expr>)?`
319    fn parse_cmp_expr(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
320        let string = skip_whitespace(string);
321        let mut state = parse_value(string)?;
322        state.rest = skip_whitespace(state.rest);
323        if state.rest.is_empty() {
324            return Ok(state);
325        }
326        for (token, op) in [
327            (b"==" as &[u8], '='),
328            (b"!=", '!'),
329            (b"<=", '≤'),
330            (b">=", '≥'),
331            (b"<", '<'),
332            (b">", '>'),
333        ] {
334            if let Some(rest) = state.rest.strip_prefix(token) {
335                let state2 = parse_cmp_expr(rest)?;
336                if state.ty != Ty::Number || state2.ty != Ty::Number {
337                    return Err(ParseError("incompatible types in comparison", state2.rest));
338                }
339                return Ok(ParsingState {
340                    expr: Expression::BinaryExpression {
341                        lhs: state.expr.into(),
342                        rhs: state2.expr.into(),
343                        op,
344                    },
345                    ty: Ty::Boolean,
346                    rest: skip_whitespace(state2.rest),
347                });
348            }
349        }
350        Ok(state)
351    }
352
353    /// `<term> ('%' <term>)?`
354    fn parse_value(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
355        let string = skip_whitespace(string);
356        let mut state = parse_term(string)?;
357        state.rest = skip_whitespace(state.rest);
358        if state.rest.is_empty() {
359            return Ok(state);
360        }
361        if let Some(rest) = state.rest.strip_prefix(b"%") {
362            let state2 = parse_term(rest)?;
363            if state.ty != Ty::Number || state2.ty != Ty::Number {
364                return Err(ParseError("incompatible types in % operator", state2.rest));
365            }
366            Ok(ParsingState {
367                expr: Expression::BuiltinFunctionCall {
368                    function: crate::expression_tree::BuiltinFunction::Mod,
369                    arguments: vec![state.expr.into(), state2.expr.into()],
370                },
371                ty: Ty::Number,
372                rest: skip_whitespace(state2.rest),
373            })
374        } else {
375            Ok(state)
376        }
377    }
378
379    fn parse_term(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
380        let string = skip_whitespace(string);
381        let state = match string.first().ok_or(ParseError("unexpected end of string", string))? {
382            b'n' => ParsingState {
383                expr: Expression::FunctionParameterReference { index: 0 },
384                rest: &string[1..],
385                ty: Ty::Number,
386            },
387            b'(' => {
388                let mut s = parse_expression(&string[1..])?;
389                s.rest = s.rest.strip_prefix(b")").ok_or(ParseError("expected ')'", s.rest))?;
390                s
391            }
392            x if x.is_ascii_digit() => {
393                let (n, rest) = parse_number(string)?;
394                ParsingState { expr: Expression::NumberLiteral(n as _), rest, ty: Ty::Number }
395            }
396            _ => return Err(ParseError("unexpected token", string)),
397        };
398        Ok(state)
399    }
400    fn parse_number(string: &[u8]) -> Result<(i32, &[u8]), ParseError<'_>> {
401        let end = string.iter().position(|&c| !c.is_ascii_digit()).unwrap_or(string.len());
402        let n = std::str::from_utf8(&string[..end])
403            .expect("string is valid utf-8")
404            .parse()
405            .map_err(|_| ParseError("can't parse number", string))?;
406        Ok((n, &string[end..]))
407    }
408    fn skip_whitespace(mut string: &[u8]) -> &[u8] {
409        // slice::trim_ascii_start when MSRV >= 1.80
410        while !string.is_empty() && string[0].is_ascii_whitespace() {
411            string = &string[1..];
412        }
413        string
414    }
415
416    #[test]
417    fn test_parse_rule_expression() {
418        #[track_caller]
419        fn p(string: &str) -> String {
420            let ctx = crate::llr::EvaluationContext {
421                compilation_unit: &crate::llr::CompilationUnit {
422                    public_components: Default::default(),
423                    sub_components: Default::default(),
424                    used_sub_components: Default::default(),
425                    globals: Default::default(),
426                    has_debug_info: false,
427                    translations: None,
428                    popup_menu: None,
429                },
430                current_scope: crate::llr::EvaluationScope::Global(0.into()),
431                generator_state: (),
432                argument_types: &[crate::langtype::Type::Int32],
433            };
434            crate::llr::pretty_print::DisplayExpression(
435                &parse_rule_expression(string).expect("parse error"),
436                &ctx,
437            )
438            .to_string()
439        }
440
441        // en
442        assert_eq!(p("n != 1"), "((arg_0 ! 1.0) ? 1.0 : 0.0)");
443        // fr
444        assert_eq!(p("n > 1"), "((arg_0 > 1.0) ? 1.0 : 0.0)");
445        // ar
446        assert_eq!(
447            p("(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5)"),
448            "((arg_0 = 0.0) ? 0.0 : ((arg_0 = 1.0) ? 1.0 : ((arg_0 = 2.0) ? 2.0 : (((Mod(arg_0, 100.0) ≥ 3.0) & (Mod(arg_0, 100.0) ≤ 10.0)) ? 3.0 : ((Mod(arg_0, 100.0) ≥ 11.0) ? 4.0 : 5.0)))))"
449        );
450        // ga
451        assert_eq!(
452            p("n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :(n>6 && n<11) ? 3 : 4"),
453            "((arg_0 = 1.0) ? 0.0 : ((arg_0 = 2.0) ? 1.0 : (((arg_0 > 2.0) & (arg_0 < 7.0)) ? 2.0 : (((arg_0 > 6.0) & (arg_0 < 11.0)) ? 3.0 : 4.0))))"
454        );
455        // ja
456        assert_eq!(p("0"), "0.0");
457        // pl
458        assert_eq!(
459            p("(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)"),
460            "((arg_0 = 1.0) ? 0.0 : (((Mod(arg_0, 10.0) ≥ 2.0) & ((Mod(arg_0, 10.0) ≤ 4.0) & ((Mod(arg_0, 100.0) < 10.0) | (Mod(arg_0, 100.0) ≥ 20.0)))) ? 1.0 : 2.0))",
461        );
462
463        // ru
464        assert_eq!(
465            p("(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)"),
466            "(((Mod(arg_0, 10.0) = 1.0) & (Mod(arg_0, 100.0) ! 11.0)) ? 0.0 : (((Mod(arg_0, 10.0) ≥ 2.0) & ((Mod(arg_0, 10.0) ≤ 4.0) & ((Mod(arg_0, 100.0) < 10.0) | (Mod(arg_0, 100.0) ≥ 20.0)))) ? 1.0 : 2.0))",
467        );
468    }
469}