Skip to main content

browser_automation_cli/i18n/
idioma.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Typed supported UI locales (`Idioma`) — single source of truth.
3
4use unic_langid::{langid, LanguageIdentifier};
5
6/// Text direction for terminal/UI rendering.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8#[non_exhaustive]
9pub enum Direcao {
10    /// Left-to-right (Latin, CJK layout LTR, etc.).
11    Ltr,
12    /// Right-to-left (Arabic, Hebrew) — only with `i18n-rtl` packs.
13    Rtl,
14}
15
16/// Writing system tag used for documentation and future CJK/RTL packs.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[non_exhaustive]
19pub enum ScriptEscrita {
20    /// Latin script (en, pt-BR, …).
21    Latn,
22    /// Simplified Chinese (feature `i18n-cjk`).
23    Hans,
24    /// Traditional Chinese (feature `i18n-cjk`).
25    Hant,
26    /// Japanese (feature `i18n-cjk`).
27    Jpan,
28    /// Korean (feature `i18n-cjk`).
29    Kore,
30    /// Arabic (feature `i18n-rtl`).
31    Arab,
32    /// Hebrew (feature `i18n-rtl`).
33    Hebr,
34}
35
36/// Supported UI locale for human-facing suggestions.
37///
38/// Machine JSON (`error.message`, envelopes) stays English regardless of [`Idioma`].
39/// Default build: [`Idioma::En`] + [`Idioma::PtBr`] only.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41#[non_exhaustive]
42pub enum Idioma {
43    /// Neutral English (`en`) — technical validation locale.
44    En,
45    /// Brazilian Portuguese (`pt-BR`) — development / accent validation locale.
46    PtBr,
47}
48
49impl Idioma {
50    /// Locales compiled into this binary (MVP: en + pt-BR).
51    pub const DISPONIVEIS: &'static [Idioma] = &[Idioma::En, Idioma::PtBr];
52
53    /// BCP 47 tag used in diagnostics and FTL paths.
54    pub const fn bcp47(self) -> &'static str {
55        match self {
56            Idioma::En => "en",
57            Idioma::PtBr => "pt-BR",
58        }
59    }
60
61    /// Legacy two-letter token used by older call sites (`en` / `pt`).
62    pub const fn legacy_token(self) -> &'static str {
63        match self {
64            Idioma::En => "en",
65            Idioma::PtBr => "pt",
66        }
67    }
68
69    /// Primary language subtag (ISO 639).
70    pub const fn language(self) -> &'static str {
71        match self {
72            Idioma::En => "en",
73            Idioma::PtBr => "pt",
74        }
75    }
76
77    /// Regional fallback (pt-BR → still PtBr as base pack; en is neutral).
78    pub const fn fallback(self) -> Idioma {
79        match self {
80            Idioma::En => Idioma::En,
81            Idioma::PtBr => Idioma::PtBr,
82        }
83    }
84
85    /// Text direction for this locale.
86    pub const fn direcao(self) -> Direcao {
87        Direcao::Ltr
88    }
89
90    /// Writing system.
91    pub const fn script(self) -> ScriptEscrita {
92        ScriptEscrita::Latn
93    }
94
95    /// Convert to `unic_langid::LanguageIdentifier`.
96    pub fn language_identifier(self) -> LanguageIdentifier {
97        match self {
98            Idioma::En => langid!("en"),
99            Idioma::PtBr => langid!("pt-BR"),
100        }
101    }
102
103    /// Map a parsed language id onto a compiled pack (language + region aware).
104    pub fn from_langid(id: &LanguageIdentifier) -> Option<Idioma> {
105        let lang = id.language.as_str();
106        match lang {
107            "en" => Some(Idioma::En),
108            "pt" => {
109                // MVP pack is pt-BR only. Bare `pt` and `pt-BR` → PtBr.
110                // `pt-PT` has no pack in default binary → None (negotiator falls to en).
111                match id.region.as_ref().map(|r| r.as_str()) {
112                    Some("PT") => None,
113                    Some("BR") | None => Some(Idioma::PtBr),
114                    _ => Some(Idioma::PtBr),
115                }
116            }
117            _ => None,
118        }
119    }
120
121    /// Parse a user/CLI/env token into a supported idioma when unambiguous.
122    pub fn parse_token(raw: &str) -> Option<Idioma> {
123        let s = raw.trim().replace('_', "-");
124        if s.is_empty() {
125            return None;
126        }
127        // Accept legacy `pt` as pt-BR (MVP rule: no bare-pt pack, CLI accepts pt → pt-BR).
128        let lower = s.to_ascii_lowercase();
129        if lower == "pt" || lower.starts_with("pt-br") || lower == "pt-br" {
130            return Some(Idioma::PtBr);
131        }
132        if lower == "en" || lower.starts_with("en-") {
133            return Some(Idioma::En);
134        }
135        let id: LanguageIdentifier = s.parse().ok()?;
136        Self::from_langid(&id)
137    }
138}
139
140impl std::fmt::Display for Idioma {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        f.write_str(self.bcp47())
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn parse_tokens_accept_regional_and_legacy() {
152        assert_eq!(Idioma::parse_token("pt-BR"), Some(Idioma::PtBr));
153        assert_eq!(Idioma::parse_token("pt_BR.UTF-8"), Some(Idioma::PtBr));
154        assert_eq!(Idioma::parse_token("pt"), Some(Idioma::PtBr));
155        assert_eq!(Idioma::parse_token("EN-us"), Some(Idioma::En));
156        assert_eq!(Idioma::parse_token("de-DE"), None);
157    }
158
159    #[test]
160    fn disponiveis_is_mvp_bilingual() {
161        assert_eq!(Idioma::DISPONIVEIS, &[Idioma::En, Idioma::PtBr]);
162    }
163}