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