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 and the decimal separator
33    pub languages: Vec<(SmolStr, char)>,
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(), i_slint_common::DEFAULT_DECIMAL_SEPARATOR)];
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                let language_name = l.file_name().to_string_lossy().to_smolstr();
63                languages.push((
64                    language_name.clone(),
65                    i_slint_common::decimal_separator_for_locale(language_name.as_str()),
66                ));
67
68                let expr = if let Some(header) = catalog.metadata.get("Plural-Forms") {
69                    let plural_expr = header.split(';').find_map(|sub_entry| {
70                        let (key, expression) = sub_entry.split_once('=')?;
71                        (key.trim() == "plural").then_some(expression)
72                    });
73                    plural_expr.ok_or_else(|| {
74                        std::io::Error::other(format!(
75                            "Error parsing plural rules in {}",
76                            path.display()
77                        ))
78                    })?
79                } else {
80                    "n != 1"
81                };
82                plural_rules.push(Some(plural_rule_parser::parse_rule_expression(expr).map_err(
83                    |_| {
84                        std::io::Error::other(format!(
85                            "Error parsing plural rules in {}",
86                            path.display()
87                        ))
88                    },
89                )?));
90
91                catalogs.push(catalog);
92            }
93        }
94        if catalogs.is_empty() {
95            return Err(std::io::Error::other(format!(
96                "No translations found. We look for files in '{}/<lang>/LC_MESSAGES/{domain}.po",
97                path.display()
98            )));
99        }
100        Ok(Self {
101            result: Translations {
102                strings: Vec::new(),
103                plurals: Vec::new(),
104                plural_rules,
105                languages,
106            },
107            map: HashMap::new(),
108            catalogs: Rc::new(catalogs),
109        })
110    }
111
112    pub fn lower_translate_call(&mut self, args: Vec<Expression>) -> Expression {
113        let [original, contextid, _domain, format_args, n, plural] = args
114            .try_into()
115            .expect("The resolving pass should have ensured that the arguments are correct");
116        let original = get_string(original).expect("original must be a string");
117        let contextid = get_string(contextid).expect("contextid must be a string");
118        let plural = get_string(plural).expect("plural must be a string");
119
120        let is_plural =
121            !plural.is_empty() || !matches!(n, Expression::NumberLiteral(f) if f == 1.0);
122
123        match self.map.entry((original.clone(), plural.clone(), contextid.clone())) {
124            Entry::Occupied(entry) => Expression::TranslationReference {
125                format_args: format_args.into(),
126                string_index: *entry.get(),
127                plural: is_plural.then(|| n.into()),
128            },
129            Entry::Vacant(entry) => {
130                let messages = self.catalogs.iter().map(|catalog| {
131                    catalog
132                        .find_by_msgid_msgctxt(original.as_str(), contextid.as_str())
133                        .filter(|entry| entry.translated())
134                });
135                let idx = if is_plural {
136                    let messages = std::iter::once(Some(vec![original.clone(), plural.clone()]))
137                        .chain(messages.map(|opt_entry| {
138                            opt_entry.and_then(|entry| {
139                                if entry.msgstr_plural.is_empty() {
140                                    None
141                                } else {
142                                    Some(
143                                        entry
144                                            .msgstr_plural
145                                            .iter()
146                                            .map(|s| s.to_smolstr())
147                                            .collect(),
148                                    )
149                                }
150                            })
151                        }))
152                        .collect();
153                    self.result.plurals.push(messages);
154                    self.result.plurals.len() - 1
155                } else {
156                    let messages = std::iter::once(Some(original.clone()))
157                        .chain(messages.map(|opt_entry| {
158                            opt_entry.and_then(|entry| entry.msgstr.map(|s| s.to_smolstr()))
159                        }))
160                        .collect::<Vec<_>>();
161                    self.result.strings.push(messages);
162                    self.result.strings.len() - 1
163                };
164                Expression::TranslationReference {
165                    format_args: format_args.into(),
166                    string_index: *entry.insert(idx),
167                    plural: is_plural.then(|| n.into()),
168                }
169            }
170        }
171    }
172
173    pub fn result(self) -> Translations {
174        self.result
175    }
176
177    /// Add all characters in any po file to `characters_seen` if they are not yet there
178    pub fn collect_characters_seen(&self, characters_seen: &mut impl Extend<char>) {
179        characters_seen.extend(
180            self.catalogs
181                .iter()
182                .flat_map(|catalog| {
183                    catalog.entries.iter().flat_map(|entry| {
184                        entry
185                            .msgstr
186                            .iter()
187                            .map(|s| s.as_str())
188                            .chain(entry.msgstr_plural.iter().map(|s| s.as_str()))
189                    })
190                })
191                .flat_map(|str| str.chars()),
192        );
193    }
194}
195
196fn get_string(plural: Expression) -> Option<SmolStr> {
197    match plural {
198        Expression::StringLiteral(s) => Some(s),
199        _ => None,
200    }
201}
202
203mod plural_rule_parser {
204    use super::Expression;
205    pub struct ParseError<'a>(&'static str, &'a [u8]);
206    impl std::fmt::Debug for ParseError<'_> {
207        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208            write!(f, "ParseError({}, rest={:?})", self.0, std::str::from_utf8(self.1).unwrap())
209        }
210    }
211    pub fn parse_rule_expression(string: &str) -> Result<Expression, ParseError<'_>> {
212        let ascii = string.as_bytes();
213        let s = parse_expression(ascii)?;
214        if !s.rest.is_empty() {
215            return Err(ParseError("extra character in string", s.rest));
216        }
217        match s.ty {
218            Ty::Number => Ok(s.expr),
219            Ty::Boolean => Ok(Expression::Condition {
220                condition: s.expr.into(),
221                true_expr: Expression::NumberLiteral(1.).into(),
222                false_expr: Expression::NumberLiteral(0.).into(),
223            }),
224        }
225    }
226
227    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
228    enum Ty {
229        Number,
230        Boolean,
231    }
232
233    struct ParsingState<'a> {
234        expr: Expression,
235        rest: &'a [u8],
236        ty: Ty,
237    }
238
239    impl ParsingState<'_> {
240        fn skip_whitespace(self) -> Self {
241            let rest = self.rest.trim_ascii_start();
242            Self { rest, ..self }
243        }
244    }
245
246    /// `<condition> ('?' <expr> : <expr> )?`
247    fn parse_expression(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
248        let string = string.trim_ascii_start();
249        let state = parse_condition(string)?.skip_whitespace();
250        if state.ty != Ty::Boolean {
251            return Ok(state);
252        }
253        if let Some(rest) = state.rest.strip_prefix(b"?") {
254            let s1 = parse_expression(rest)?.skip_whitespace();
255            let rest = s1.rest.strip_prefix(b":").ok_or(ParseError("expected ':'", s1.rest))?;
256            let s2 = parse_expression(rest)?;
257            if s1.ty != s2.ty {
258                return Err(ParseError("incompatible types in ternary operator", s2.rest));
259            }
260            Ok(ParsingState {
261                expr: Expression::Condition {
262                    condition: state.expr.into(),
263                    true_expr: s1.expr.into(),
264                    false_expr: s2.expr.into(),
265                },
266                rest: s2.rest.trim_ascii_start(),
267                ty: s2.ty,
268            })
269        } else {
270            Ok(state)
271        }
272    }
273
274    /// `<and_expr> ("||" <condition>)?`
275    fn parse_condition(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
276        let string = string.trim_ascii_start();
277        let state = parse_and_expr(string)?.skip_whitespace();
278        if state.rest.is_empty() {
279            return Ok(state);
280        }
281        if let Some(rest) = state.rest.strip_prefix(b"||") {
282            let state2 = parse_condition(rest)?;
283            if state.ty != Ty::Boolean || state2.ty != Ty::Boolean {
284                return Err(ParseError("incompatible types in || operator", state2.rest));
285            }
286            Ok(ParsingState {
287                expr: Expression::BinaryExpression {
288                    lhs: state.expr.into(),
289                    rhs: state2.expr.into(),
290                    op: '|',
291                },
292                ty: Ty::Boolean,
293                rest: state2.rest.trim_ascii_start(),
294            })
295        } else {
296            Ok(state)
297        }
298    }
299
300    /// `<cmp_expr> ("&&" <and_expr>)?`
301    fn parse_and_expr(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
302        let string = string.trim_ascii_start();
303        let state = parse_cmp_expr(string)?.skip_whitespace();
304        if state.rest.is_empty() {
305            return Ok(state);
306        }
307        if let Some(rest) = state.rest.strip_prefix(b"&&") {
308            let state2 = parse_and_expr(rest)?;
309            if state.ty != Ty::Boolean || state2.ty != Ty::Boolean {
310                return Err(ParseError("incompatible types in || operator", state2.rest));
311            }
312            Ok(ParsingState {
313                expr: Expression::BinaryExpression {
314                    lhs: state.expr.into(),
315                    rhs: state2.expr.into(),
316                    op: '&',
317                },
318                ty: Ty::Boolean,
319                rest: state2.rest.trim_ascii_start(),
320            })
321        } else {
322            Ok(state)
323        }
324    }
325
326    /// `<value> ('=='|'!='|'<'|'>'|'<='|'>=' <cmp_expr>)?`
327    fn parse_cmp_expr(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
328        let string = string.trim_ascii_start();
329        let mut state = parse_value(string)?;
330        state.rest = state.rest.trim_ascii_start();
331        if state.rest.is_empty() {
332            return Ok(state);
333        }
334        for (token, op) in [
335            (b"==" as &[u8], '='),
336            (b"!=", '!'),
337            (b"<=", '≤'),
338            (b">=", '≥'),
339            (b"<", '<'),
340            (b">", '>'),
341        ] {
342            if let Some(rest) = state.rest.strip_prefix(token) {
343                let state2 = parse_cmp_expr(rest)?;
344                if state.ty != Ty::Number || state2.ty != Ty::Number {
345                    return Err(ParseError("incompatible types in comparison", state2.rest));
346                }
347                return Ok(ParsingState {
348                    expr: Expression::BinaryExpression {
349                        lhs: state.expr.into(),
350                        rhs: state2.expr.into(),
351                        op,
352                    },
353                    ty: Ty::Boolean,
354                    rest: state2.rest.trim_ascii_start(),
355                });
356            }
357        }
358        Ok(state)
359    }
360
361    /// `<term> ('%' <term>)?`
362    fn parse_value(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
363        let string = string.trim_ascii_start();
364        let mut state = parse_term(string)?;
365        state.rest = state.rest.trim_ascii_start();
366        if state.rest.is_empty() {
367            return Ok(state);
368        }
369        if let Some(rest) = state.rest.strip_prefix(b"%") {
370            let state2 = parse_term(rest)?;
371            if state.ty != Ty::Number || state2.ty != Ty::Number {
372                return Err(ParseError("incompatible types in % operator", state2.rest));
373            }
374            Ok(ParsingState {
375                expr: Expression::BuiltinFunctionCall {
376                    function: crate::expression_tree::BuiltinFunction::Mod,
377                    arguments: vec![state.expr, state2.expr],
378                },
379                ty: Ty::Number,
380                rest: state2.rest.trim_ascii_start(),
381            })
382        } else {
383            Ok(state)
384        }
385    }
386
387    fn parse_term(string: &[u8]) -> Result<ParsingState<'_>, ParseError<'_>> {
388        let string = string.trim_ascii_start();
389        let state = match string.first().ok_or(ParseError("unexpected end of string", string))? {
390            b'n' => ParsingState {
391                expr: Expression::FunctionParameterReference { index: 0 },
392                rest: &string[1..],
393                ty: Ty::Number,
394            },
395            b'(' => {
396                let mut s = parse_expression(&string[1..])?;
397                s.rest = s.rest.strip_prefix(b")").ok_or(ParseError("expected ')'", s.rest))?;
398                s
399            }
400            x if x.is_ascii_digit() => {
401                let (n, rest) = parse_number(string)?;
402                ParsingState { expr: Expression::NumberLiteral(n as _), rest, ty: Ty::Number }
403            }
404            _ => return Err(ParseError("unexpected token", string)),
405        };
406        Ok(state)
407    }
408    fn parse_number(string: &[u8]) -> Result<(i32, &[u8]), ParseError<'_>> {
409        let end = string.iter().position(|&c| !c.is_ascii_digit()).unwrap_or(string.len());
410        let n = std::str::from_utf8(&string[..end])
411            .expect("string is valid utf-8")
412            .parse()
413            .map_err(|_| ParseError("can't parse number", string))?;
414        Ok((n, &string[end..]))
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}