Skip to main content

browser_automation_cli/i18n/
ftl.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Embedded Fluent (FTL) catalogs — parity source for translators + optional runtime check.
3
4use fluent::{FluentBundle, FluentResource};
5use unic_langid::LanguageIdentifier;
6
7use super::idioma::Idioma;
8use super::mensagem::{ftl_id, Mensagem};
9
10const EN_FTL: &str = include_str!("../../locales/en.ftl");
11const PT_BR_FTL: &str = include_str!("../../locales/pt-BR.ftl");
12
13/// Embedded FTL source for a compiled idioma.
14pub fn ftl_source(idioma: Idioma) -> &'static str {
15    match idioma {
16        Idioma::En => EN_FTL,
17        Idioma::PtBr => PT_BR_FTL,
18    }
19}
20
21/// Build a Fluent bundle for `idioma` from the embedded FTL (for tests / diagnostics).
22pub fn bundle_for(idioma: Idioma) -> Result<FluentBundle<FluentResource>, String> {
23    let lang: LanguageIdentifier = idioma.language_identifier();
24    let mut bundle = FluentBundle::new(vec![lang]);
25    // One-shot CLI: no need for concurrent memoizer (single-threaded format at boot/tests).
26    bundle.set_use_isolating(false);
27    let res = FluentResource::try_new(ftl_source(idioma).to_string())
28        .map_err(|(_, errs)| format!("FTL parse errors for {}: {errs:?}", idioma.bcp47()))?;
29    bundle
30        .add_resource(res)
31        .map_err(|errs| format!("FTL add_resource {}: {errs:?}", idioma.bcp47()))?;
32    Ok(bundle)
33}
34
35/// Format a message id from the embedded FTL; falls back to enum catalog on miss.
36pub fn format_ftl(idioma: Idioma, msg: Mensagem) -> String {
37    let id = ftl_id(msg);
38    match bundle_for(idioma) {
39        Ok(bundle) => {
40            if let Some(message) = bundle.get_message(id) {
41                if let Some(pattern) = message.value() {
42                    let mut errors = vec![];
43                    let s = bundle.format_pattern(pattern, None, &mut errors);
44                    if errors.is_empty() && !s.is_empty() {
45                        return s.to_string();
46                    }
47                }
48            }
49            msg.texto(idioma).to_string()
50        }
51        Err(_) => msg.texto(idioma).to_string(),
52    }
53}
54
55/// Extract bare message ids from an FTL source (lines `key = value`, skip comments/blank).
56pub fn ftl_keys(source: &str) -> Vec<String> {
57    let mut keys = Vec::new();
58    for line in source.lines() {
59        let t = line.trim();
60        if t.is_empty() || t.starts_with('#') {
61            continue;
62        }
63        if let Some((key, _)) = t.split_once('=') {
64            let key = key.trim();
65            if !key.is_empty() && !key.starts_with('-') {
66                keys.push(key.to_string());
67            }
68        }
69    }
70    keys.sort();
71    keys.dedup();
72    keys
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn ftl_en_pt_key_parity() {
81        let en = ftl_keys(EN_FTL);
82        let pt = ftl_keys(PT_BR_FTL);
83        assert_eq!(en, pt, "en.ftl and pt-BR.ftl key sets must match");
84        assert!(!en.is_empty());
85    }
86
87    #[test]
88    fn ftl_keys_cover_all_mensagem() {
89        let en = ftl_keys(EN_FTL);
90        for m in Mensagem::ALL {
91            let id = ftl_id(*m);
92            assert!(
93                en.iter().any(|k| k == id),
94                "missing FTL key {id} for {m:?}"
95            );
96        }
97    }
98
99    #[test]
100    fn fluent_parses_both_packs() {
101        bundle_for(Idioma::En).expect("en FTL");
102        bundle_for(Idioma::PtBr).expect("pt-BR FTL");
103    }
104
105    #[test]
106    fn ftl_format_matches_enum_for_usage() {
107        let en = format_ftl(Idioma::En, Mensagem::UsageSuggestion);
108        assert_eq!(en, Mensagem::UsageSuggestion.texto(Idioma::En));
109        let pt = format_ftl(Idioma::PtBr, Mensagem::VisionRequired);
110        assert!(pt.contains("invocação"), "{pt}");
111    }
112}