1use 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 pub strings: Vec<Vec<Option<SmolStr>>>,
19 pub plurals: Vec<Vec<Option<Vec<SmolStr>>>>,
24
25 pub plural_rules: Vec<Option<Expression>>,
31
32 pub languages: Vec<(SmolStr, char)>,
34}
35
36#[derive(Clone)]
37pub struct TranslationsBuilder {
38 result: Translations,
39 map: HashMap<(SmolStr, SmolStr, SmolStr), usize>,
42
43 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 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 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 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 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 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 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 assert_eq!(p("n != 1"), "((arg_0 ! 1.0) ? 1.0 : 0.0)");
443 assert_eq!(p("n > 1"), "((arg_0 > 1.0) ? 1.0 : 0.0)");
445 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 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 assert_eq!(p("0"), "0.0");
457 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 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}