Skip to main content

browser_automation_cli/
i18n.rs

1//! Minimal en/pt-BR message helpers for critical errors.
2//!
3//! Technical `message` fields stay in English (agent-stable).
4//! Human `suggestion` strings are localized via catalog keys or kind fallback.
5
6use std::sync::OnceLock;
7
8/// Process-wide effective language (`en` or `pt`), set once at CLI boot.
9static EFFECTIVE_LANG: OnceLock<&'static str> = OnceLock::new();
10
11/// Normalize lang token to "en" or "pt".
12pub fn normalize_lang(lang: Option<&str>) -> &'static str {
13    match lang.map(|s| s.trim().to_ascii_lowercase()) {
14        Some(ref s) if s.starts_with("pt") => "pt",
15        _ => "en",
16    }
17}
18
19/// Resolve language from CLI flag, then XDG config, then OS locale hints.
20pub fn resolve_lang(cli_lang: Option<&str>) -> &'static str {
21    if let Some(l) = cli_lang {
22        if !l.trim().is_empty() {
23            return normalize_lang(Some(l));
24        }
25    }
26    if let Ok(cfg) = crate::xdg::load_config() {
27        if let Some(ref l) = cfg.lang {
28            if !l.trim().is_empty() {
29                return normalize_lang(Some(l));
30            }
31        }
32    }
33    // OS locale without product env vars: read glibc/setlocale-style via std only if available.
34    // Prefer directories / LANG is OS concern — use libc setlocale is heavy; fall back to en.
35    // On Unix, try reading /etc/locale.conf is overkill; use `sys_locale` free path:
36    #[cfg(unix)]
37    {
38        // Minimal: inspect POSIX locale without treating as product config.
39        // SAFETY: reading environment for OS locale is an OS concern (not product settings).
40        // Plan forbids product env; OS locale detection is explicitly allowed as platform.
41        if let Ok(v) = std::env::var("LANG") {
42            if v.to_ascii_lowercase().starts_with("pt") {
43                return "pt";
44            }
45        }
46        if let Ok(v) = std::env::var("LC_ALL") {
47            if v.to_ascii_lowercase().starts_with("pt") {
48                return "pt";
49            }
50        }
51        if let Ok(v) = std::env::var("LC_MESSAGES") {
52            if v.to_ascii_lowercase().starts_with("pt") {
53                return "pt";
54            }
55        }
56    }
57    "en"
58}
59
60/// Store effective language for the process (call once from `run()`).
61pub fn set_effective_lang(lang: &'static str) {
62    let _ = EFFECTIVE_LANG.set(lang);
63}
64
65/// Current effective language (defaults to `en` if unset).
66pub fn effective_lang() -> &'static str {
67    EFFECTIVE_LANG.get().copied().unwrap_or("en")
68}
69
70/// Localized suggestion for a known kind key.
71pub fn suggestion_for(kind: &str, lang: Option<&str>) -> Option<&'static str> {
72    let l = normalize_lang(lang.or(Some(effective_lang())));
73    match (kind, l) {
74        ("usage", "pt") => Some("Confira --help e os argumentos obrigatorios"),
75        ("usage", _) => Some("Check --help and required arguments"),
76        ("broken-pipe", "pt") => {
77            Some("Nao pipe stdout para consumidor fechado; exit 141 e esperado")
78        }
79        ("broken-pipe", _) => Some("Do not pipe stdout to a closed consumer; exit 141 is expected"),
80        ("unavailable", "pt") => {
81            Some("Instale Chrome/Chromium no PATH ou use: browser-automation-cli config set chrome_path <path>")
82        }
83        ("unavailable", _) => {
84            Some("Install Chrome/Chromium on PATH or: browser-automation-cli config set chrome_path <path>")
85        }
86        ("data", "pt") => Some("Verifique robots.txt ou o payload JSON/NDJSON"),
87        ("data", _) => Some("Check robots.txt or the JSON/NDJSON payload"),
88        ("browser", "pt") => Some("Verifique URL e se o Chrome ainda esta vivo no one-shot"),
89        ("browser", _) => Some("Check the URL and whether Chrome stayed alive in this one-shot"),
90        _ => None,
91    }
92}
93
94/// Catalog of stable suggestion keys (preferred over hard-coded English).
95pub fn suggestion_key(key: &str, lang: Option<&str>) -> &'static str {
96    let l = normalize_lang(lang.or(Some(effective_lang())));
97    match (key, l) {
98        ("vision_required", "pt") => "Passe --experimental-vision na mesma invocacao",
99        ("vision_required", _) => "Pass --experimental-vision on the same invocation",
100        ("robots_dual", "pt") => {
101            "Passe as duas flags juntas quando ignorar robots.txt de proposito"
102        }
103        ("robots_dual", _) => "Pass both flags together when you intentionally skip robots.txt",
104        ("category_memory", "pt") => {
105            "Passe --category-memory (heap take/summary/close funcionam sem ops de grafo profundo)"
106        }
107        ("category_memory", _) => {
108            "Pass --category-memory (heap take/summary/close work without deep graph ops)"
109        }
110        ("category_extensions", "pt") => "Passe --category-extensions na mesma invocacao",
111        ("category_extensions", _) => "Pass --category-extensions on the same invocation",
112        ("screencast_flag", "pt") => "Passe --experimental-screencast na mesma invocacao",
113        ("screencast_flag", _) => "Pass --experimental-screencast on the same invocation",
114        ("webmcp_flag", "pt") => "Passe --category-webmcp na mesma invocacao",
115        ("webmcp_flag", _) => "Pass --category-webmcp on the same invocation",
116        ("third_party_flag", "pt") => "Passe --category-third-party na mesma invocacao",
117        ("third_party_flag", _) => "Pass --category-third-party on the same invocation",
118        ("capture_network", "pt") => "Passe --capture-network antes de run/net",
119        ("capture_network", _) => "Pass --capture-network before run/net",
120        ("capture_console", "pt") => "Passe --capture-console antes de run/console",
121        ("capture_console", _) => "Pass --capture-console before run/console",
122        ("run_fail_fast", "pt") => {
123            "Corrija o passo com falha; os passos seguintes nao foram executados"
124        }
125        ("run_fail_fast", _) => "Fix the failing step; subsequent steps were not executed",
126        ("lighthouse_missing", "pt") => {
127            "Instale lighthouse ou: browser-automation-cli config set lighthouse_path <path>"
128        }
129        ("lighthouse_missing", _) => {
130            "Install lighthouse or: browser-automation-cli config set lighthouse_path <path>"
131        }
132        (_, "pt") => "Confira --help e os argumentos obrigatorios",
133        _ => "Check --help and required arguments",
134    }
135}
136
137/// Apply kind-based localized suggestion when none is set, or re-map known EN strings.
138pub fn localize_error_suggestion(err: &crate::error::CliError) -> crate::error::CliError {
139    let lang = effective_lang();
140    if lang == "en" {
141        return err.clone();
142    }
143    // Prefer catalog by kind when suggestion is missing.
144    if err.suggestion().is_none() {
145        if let Some(s) = suggestion_for(err.kind().as_str(), Some(lang)) {
146            let mut out = crate::error::CliError::with_suggestion(err.kind(), err.message(), s);
147            if let Some(d) = err.data() {
148                out = out.with_data(d.clone());
149            }
150            return out;
151        }
152        return err.clone();
153    }
154    let s = err.suggestion().unwrap_or("");
155    // Map common English suggestions to Portuguese.
156    let mapped = match s {
157        "Pass --experimental-vision on the same invocation" => {
158            suggestion_key("vision_required", Some(lang))
159        }
160        "Pass both flags together when you intentionally skip robots.txt" => {
161            suggestion_key("robots_dual", Some(lang))
162        }
163        "Pass --category-memory (heap take/summary/close work without deep graph ops)" => {
164            suggestion_key("category_memory", Some(lang))
165        }
166        "Pass --category-extensions on the same invocation" => {
167            suggestion_key("category_extensions", Some(lang))
168        }
169        "Pass --experimental-screencast on the same invocation" => {
170            suggestion_key("screencast_flag", Some(lang))
171        }
172        "Pass --category-webmcp on the same invocation" => {
173            suggestion_key("webmcp_flag", Some(lang))
174        }
175        "Pass --category-third-party on the same invocation" => {
176            suggestion_key("third_party_flag", Some(lang))
177        }
178        "Pass --capture-network before run/net" => suggestion_key("capture_network", Some(lang)),
179        "Pass --capture-console before run/console" => {
180            suggestion_key("capture_console", Some(lang))
181        }
182        "Fix the failing step; subsequent steps were not executed" => {
183            suggestion_key("run_fail_fast", Some(lang))
184        }
185        other if other.contains("lighthouse") && other.contains("npm") => {
186            suggestion_key("lighthouse_missing", Some(lang))
187        }
188        _ => {
189            return err.clone();
190        }
191    };
192    let mut out = crate::error::CliError::with_suggestion(err.kind(), err.message(), mapped);
193    if let Some(d) = err.data() {
194        out = out.with_data(d.clone());
195    }
196    out
197}