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