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