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