browser_automation_cli/i18n/
detect.rs1use fluent_langneg::negotiate::NegotiationStrategy;
5use fluent_langneg::negotiate_languages;
6use unic_langid::LanguageIdentifier;
7
8use super::idioma::Idioma;
9
10pub const LANG_ENV: &str = "BROWSER_AUTOMATION_CLI_LANG";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum LocaleSource {
17 Flag,
19 Env,
21 Xdg,
23 System,
25 Default,
27}
28
29impl LocaleSource {
30 pub const fn as_str(self) -> &'static str {
32 match self {
33 LocaleSource::Flag => "flag",
34 LocaleSource::Env => "env",
35 LocaleSource::Xdg => "xdg",
36 LocaleSource::System => "system",
37 LocaleSource::Default => "default",
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct ResolvedLocale {
51 pub idioma: Idioma,
53 pub source: LocaleSource,
55 pub system_raw: Option<String>,
57}
58
59fn available_langids() -> Vec<LanguageIdentifier> {
61 Idioma::DISPONIVEIS
62 .iter()
63 .map(|i| i.language_identifier())
64 .collect()
65}
66
67pub fn parse_langid(raw: &str) -> Option<LanguageIdentifier> {
71 let mut s = raw.trim().replace('_', "-");
72 if s.is_empty() {
73 return None;
74 }
75 if let Some(idx) = s.find(['.', '@']) {
77 s.truncate(idx);
78 }
79 let lower = s.to_ascii_lowercase();
81 if lower == "c" || lower == "posix" {
82 return None;
83 }
84 s.parse().ok()
85}
86
87pub fn negotiate(requested: &[LanguageIdentifier]) -> Idioma {
89 let available = available_langids();
90 let default = Idioma::En.language_identifier();
91 let matched = negotiate_languages(
92 requested,
93 &available,
94 Some(&default),
95 NegotiationStrategy::Filtering,
96 );
97 matched
98 .first()
99 .and_then(|id| Idioma::from_langid(id))
100 .or_else(|| {
101 requested.iter().find_map(Idioma::from_langid)
104 })
105 .unwrap_or(Idioma::En)
106}
107
108pub fn detect_system_langid() -> Option<LanguageIdentifier> {
110 let raw = sys_locale::get_locale()?;
111 parse_langid(&raw)
112}
113
114pub fn resolve(
124 cli_lang: Option<&str>,
125 xdg_lang: Option<&str>,
126) -> ResolvedLocale {
127 if let Some(raw) = cli_lang.map(str::trim).filter(|s| !s.is_empty()) {
129 if let Some(idioma) = Idioma::parse_token(raw) {
130 return ResolvedLocale {
131 idioma,
132 source: LocaleSource::Flag,
133 system_raw: None,
134 };
135 }
136 tracing::warn!(value = raw, "invalid --lang value; continuing resolution chain");
138 }
139
140 if let Ok(raw) = std::env::var(LANG_ENV) {
142 let t = raw.trim();
143 if !t.is_empty() {
144 if let Some(idioma) = Idioma::parse_token(t) {
145 return ResolvedLocale {
146 idioma,
147 source: LocaleSource::Env,
148 system_raw: None,
149 };
150 }
151 tracing::warn!(
152 env = LANG_ENV,
153 value = t,
154 "invalid BROWSER_AUTOMATION_CLI_LANG; continuing resolution chain"
155 );
156 }
157 }
158
159 if let Some(raw) = xdg_lang.map(str::trim).filter(|s| !s.is_empty()) {
161 if let Some(idioma) = Idioma::parse_token(raw) {
162 return ResolvedLocale {
163 idioma,
164 source: LocaleSource::Xdg,
165 system_raw: None,
166 };
167 }
168 tracing::warn!(value = raw, "invalid XDG lang; continuing resolution chain");
169 }
170
171 match sys_locale::get_locale() {
173 Some(raw) => {
174 if let Some(id) = parse_langid(&raw) {
176 let idioma = negotiate(std::slice::from_ref(&id));
177 return ResolvedLocale {
179 idioma,
180 source: LocaleSource::System,
181 system_raw: Some(raw),
182 };
183 }
184 tracing::debug!(
185 raw = %raw,
186 "OS locale unparsable; falling back to default en"
187 );
188 ResolvedLocale {
189 idioma: Idioma::En,
190 source: LocaleSource::Default,
191 system_raw: Some(raw),
192 }
193 }
194 None => {
195 tracing::debug!("sys-locale returned None; using default en");
197 ResolvedLocale {
198 idioma: Idioma::En,
199 source: LocaleSource::Default,
200 system_raw: None,
201 }
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn parse_strips_encoding_and_underscore() {
212 let id = parse_langid("pt_BR.UTF-8").expect("parse");
213 assert_eq!(id.language.as_str(), "pt");
214 assert_eq!(id.region.as_ref().map(|r| r.as_str()), Some("BR"));
215 }
216
217 #[test]
218 fn reject_c_locale() {
219 assert!(parse_langid("C").is_none());
220 assert!(parse_langid("POSIX").is_none());
221 }
222
223 #[test]
224 fn negotiate_pt_br_prefers_pack() {
225 let id: LanguageIdentifier = "pt-BR".parse().unwrap();
226 assert_eq!(negotiate(&[id]), Idioma::PtBr);
227 }
228
229 #[test]
230 fn negotiate_unknown_falls_to_en() {
231 let id: LanguageIdentifier = "ja-JP".parse().unwrap();
232 assert_eq!(negotiate(&[id]), Idioma::En);
233 }
234
235 #[test]
236 fn flag_layer_wins() {
237 let r = resolve(Some("pt-BR"), Some("en"));
238 assert_eq!(r.idioma, Idioma::PtBr);
239 assert_eq!(r.source, LocaleSource::Flag);
240 }
241}