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(
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 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 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 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 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 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 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 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 assert_eq!(p("n != 1"), "((arg_0 ! 1.0) ? 1.0 : 0.0)");
455 assert_eq!(p("n > 1"), "((arg_0 > 1.0) ? 1.0 : 0.0)");
457 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 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 assert_eq!(p("0"), "0.0");
469 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 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}