Skip to main content

cargo_zng/l10n/
generate_util.rs

1use std::{borrow::Cow, fmt::Write as _, fs, path::Path};
2
3use fluent_syntax::ast::{Attribute, CallArguments, Entry, Expression, Identifier, InlineExpression, Pattern, PatternElement, VariantKey};
4
5use crate::util;
6
7pub fn transform_dir(
8    dir: &str,
9    to_name: &str,
10    file_header: &str,
11    filter: &dyn Fn(&str, &str) -> bool,
12    transform: &dyn Fn(&str) -> Cow<str>,
13    check: bool,
14    verbose: bool,
15) {
16    let dir_path = Path::new(dir);
17    let pattern = dir_path.join("**/*.ftl");
18    let to_dir = dir_path.with_file_name(to_name);
19    for entry in glob::glob(&pattern.display().to_string()).unwrap_or_else(|e| fatal!("cannot read `{dir}`, {e}")) {
20        let entry = entry.unwrap_or_else(|e| fatal!("cannot read `{dir}` entry, {e}"));
21        let relative_entry = entry.strip_prefix(dir_path).unwrap();
22        let to_file = to_dir.join(relative_entry);
23        let _ = util::check_or_create_dir_all(check, to_file.parent().unwrap());
24        let ok = transform_file(&entry, &to_file, file_header, filter, transform, check, verbose);
25        if ok {
26            let display_to = to_file.strip_prefix(to_dir.parent().unwrap()).unwrap();
27            println!("  generated {}", display_to.display());
28        }
29    }
30}
31
32#[allow(clippy::too_many_arguments)]
33pub fn transform_file(
34    from: &Path,
35    to: &Path,
36    file_header: &str,
37    filter: &dyn Fn(&str, &str) -> bool,
38    transform: &dyn Fn(&str) -> Cow<str>,
39    check: bool,
40    verbose: bool,
41) -> bool {
42    let source = match fs::read_to_string(from) {
43        Ok(s) => s,
44        Err(e) => {
45            error!("cannot read `{}`, {e}", from.display());
46            return false;
47        }
48    };
49
50    let mut all_ok = true;
51    let source = match fluent_syntax::parser::parse(source) {
52        Ok(s) => s,
53        Err((s, e)) => {
54            all_ok = false;
55            error!(
56                "cannot parse `{}`\n{}",
57                from.display(),
58                e.into_iter().map(|e| format!("    {e}")).collect::<Vec<_>>().join("\n")
59            );
60            s
61        }
62    };
63
64    let mut output = file_header.to_owned();
65
66    for entry in source.body {
67        match entry {
68            Entry::Message(m) => write_entry(&mut output, false, &m.id, m.value.as_ref(), &m.attributes, filter, transform),
69            Entry::Term(t) => write_entry(&mut output, true, &t.id, Some(&t.value), &t.attributes, filter, transform),
70            Entry::Comment(_) | Entry::GroupComment(_) | Entry::ResourceComment(_) | Entry::Junk { .. } => {}
71        }
72    }
73
74    if let Err(e) = util::check_or_write(check, to, output.trim().as_bytes(), verbose) {
75        all_ok = false;
76        error!("cannot write `{}`, {e}", to.display());
77    }
78
79    all_ok
80}
81
82fn write_entry(
83    output: &mut String,
84    is_term: bool,
85    id: &Identifier<String>,
86    value: Option<&Pattern<String>>,
87    attributes: &[Attribute<String>],
88    filter: &dyn Fn(&str, &str) -> bool,
89    transform: &dyn Fn(&str) -> Cow<str>,
90) {
91    if !is_term && !filter(&id.name, "") {
92        return;
93    }
94
95    write!(output, "\n\n{}{} = ", if is_term { "-" } else { "" }, id.name).unwrap();
96    if let Some(value) = value {
97        write_pattern(output, value, transform, 1);
98    }
99    for attr in attributes {
100        if !is_term && !filter(&id.name, &attr.id.name) {
101            return;
102        }
103
104        write!(output, "\n    .{} = ", attr.id.name).unwrap();
105        write_pattern(output, &attr.value, transform, 2);
106    }
107}
108
109fn write_pattern(output: &mut String, pattern: &Pattern<String>, transform: &dyn Fn(&str) -> Cow<str>, depth: usize) {
110    for el in &pattern.elements {
111        match el {
112            PatternElement::TextElement { value } => {
113                let mut prefix = String::new();
114                for line in value.split('\n') {
115                    // not .lines() because is consumes trailing empty lines
116                    write!(output, "{prefix}{}", transform(line)).unwrap();
117                    prefix = format!("\n{}", " ".repeat(depth * 4));
118                }
119            }
120            PatternElement::Placeable { expression } => write_expression(output, expression, transform, depth),
121        }
122    }
123}
124
125fn write_expression(output: &mut String, expr: &Expression<String>, transform: &dyn Fn(&str) -> Cow<str>, depth: usize) {
126    match expr {
127        Expression::Select { selector, variants } => {
128            write!(output, "{{").unwrap();
129            write_inline_expression_inner(output, selector, transform, depth);
130            writeln!(output, " ->").unwrap();
131
132            for v in variants {
133                write!(output, "{}", " ".repeat((depth + 1) * 4)).unwrap();
134                if v.default {
135                    write!(output, "*").unwrap();
136                }
137                let key = match &v.key {
138                    VariantKey::Identifier { name } => name,
139                    VariantKey::NumberLiteral { value } => value,
140                };
141                write!(output, "[{key}] ").unwrap();
142
143                write_pattern(output, &v.value, transform, depth + 2);
144                writeln!(output).unwrap();
145            }
146
147            writeln!(output, "}}").unwrap();
148        }
149        Expression::Inline(e) => write_inline_expression(output, e, transform, depth),
150    }
151}
152fn write_inline_expression(output: &mut String, expr: &InlineExpression<String>, transform: &dyn Fn(&str) -> Cow<str>, depth: usize) {
153    write!(output, "{{ ").unwrap();
154    write_inline_expression_inner(output, expr, transform, depth);
155    write!(output, " }} ").unwrap();
156}
157fn write_inline_expression_inner(output: &mut String, expr: &InlineExpression<String>, transform: &dyn Fn(&str) -> Cow<str>, depth: usize) {
158    match expr {
159        InlineExpression::StringLiteral { value } => {
160            let value = transform(value);
161            let value = value.replace('\\', "\\\\").replace('"', "\\\"");
162            write!(output, "\"{value}\"").unwrap()
163        }
164        InlineExpression::NumberLiteral { value } => write!(output, "{value}").unwrap(),
165        InlineExpression::FunctionReference { id, arguments } => {
166            write!(output, "{}", id.name).unwrap();
167            write_arguments(output, arguments, transform, depth);
168        }
169        InlineExpression::MessageReference { id, attribute } => {
170            write!(output, "{}", id.name).unwrap();
171            if let Some(a) = attribute {
172                write!(output, ".{}", a.name).unwrap();
173            }
174        }
175        InlineExpression::TermReference { id, attribute, arguments } => {
176            write!(output, "-{}", id.name).unwrap();
177            if let Some(a) = attribute {
178                write!(output, ".{}", a.name).unwrap();
179            }
180            if let Some(args) = arguments {
181                write_arguments(output, args, transform, depth);
182            }
183        }
184        InlineExpression::VariableReference { id } => write!(output, "${}", id.name).unwrap(),
185        InlineExpression::Placeable { expression } => {
186            write_expression(output, expression, transform, depth);
187        }
188    }
189}
190
191fn write_arguments(output: &mut String, arguments: &CallArguments<String>, transform: &dyn Fn(&str) -> Cow<str>, depth: usize) {
192    write!(output, "(").unwrap();
193    let mut sep = "";
194    for a in &arguments.positional {
195        write!(output, "{sep}").unwrap();
196        write_inline_expression_inner(output, a, transform, depth);
197        sep = ", ";
198    }
199    for a in &arguments.named {
200        write!(output, "{sep}{}:", a.name.name).unwrap();
201        write_inline_expression_inner(output, &a.value, transform, depth);
202        sep = ", ";
203    }
204    write!(output, ")").unwrap();
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn basic_write_entry() {
213        let source = r#"
214-lang = en-US
215
216button = Button
217
218window = 
219    .title = Localize Example ({-lang})
220
221click-count = {$n ->
222    [one] Clicked {$n} time
223    *[other] Clicked {$n} times
224}
225key-count = {NUMBER($n) ->
226    [one] Clicked {$n} time
227    *[other] Clicked {$n} times
228}
229        "#;
230        let source = fluent_syntax::parser::parse(source.to_owned()).unwrap();
231
232        let mut output = String::new();
233        for entry in &source.body {
234            match entry {
235                Entry::Message(m) => write_entry(&mut output, false, &m.id, m.value.as_ref(), &m.attributes, &|_, _| true, &|a| {
236                    Cow::Borrowed(a)
237                }),
238                Entry::Term(t) => write_entry(&mut output, true, &t.id, Some(&t.value), &t.attributes, &|_, _| true, &|a| {
239                    Cow::Borrowed(a)
240                }),
241                _ => {}
242            }
243        }
244
245        let _ =
246            fluent_syntax::parser::parse(output.clone()).unwrap_or_else(|e| panic!("write_entry output invalid\n{}\n{output}", &e.1[0]));
247    }
248}