Skip to main content

ssh_cli/
locale.rs

1//! Detecção e resolução de idioma cross-platform.
2//!
3//! Precedência de seleção de idioma (do mais para o menos prioritário):
4//! 1. Flag `--lang` da CLI
5//! 2. Variável de ambiente `SSH_CLI_LANG`
6//! 3. Locale do sistema via `sys_locale::get_locale()`
7//! 4. Fallback: `Idioma::English`
8
9use std::sync::OnceLock;
10
11use crate::i18n::Idioma;
12
13/// Estado global do idioma — definido uma única vez na inicialização.
14static IDIOMA_GLOBAL: OnceLock<Idioma> = OnceLock::new();
15
16/// Resolve o idioma aplicando a hierarquia de precedência em 4 camadas.
17///
18/// Retorna o primeiro idioma válido encontrado na ordem:
19/// flag CLI > env SSH_CLI_LANG > sys_locale > English.
20pub fn resolver_idioma(forcar: Option<&str>) -> Idioma {
21    // Camada 1: flag --lang da CLI
22    if let Some(codigo) = forcar {
23        if let Some(idioma) = codigo_para_idioma(codigo) {
24            return idioma;
25        }
26    }
27
28    // Camada 2: variável de ambiente SSH_CLI_LANG
29    if let Ok(env_lang) = std::env::var("SSH_CLI_LANG") {
30        if let Some(idioma) = codigo_para_idioma(&env_lang) {
31            return idioma;
32        }
33    }
34
35    // Camada 3: locale do sistema via sys_locale
36    if let Some(locale) = sys_locale::get_locale() {
37        if let Some(idioma) = codigo_para_idioma(&locale) {
38            return idioma;
39        }
40    }
41
42    // Camada 4: fallback incondicional
43    Idioma::English
44}
45
46/// Define o idioma global (chamada única na inicialização do processo).
47///
48/// Chamadas subsequentes são silenciosamente ignoradas — o `OnceLock`
49/// garante que o idioma é imutável após a primeira definição.
50pub fn definir_idioma(idioma: Idioma) {
51    let _ = IDIOMA_GLOBAL.set(idioma);
52}
53
54/// Retorna o idioma global atual.
55///
56/// Se `definir_idioma` ainda não foi chamado, retorna `Idioma::English`
57/// como fallback seguro para código executado antes da inicialização.
58pub fn idioma_atual() -> Idioma {
59    IDIOMA_GLOBAL.get().copied().unwrap_or(Idioma::English)
60}
61
62/// Converte código textual de idioma para `Idioma`.
63///
64/// Reconhece prefixos "pt" e "en" com qualquer sufixo de região,
65/// sem distinção entre maiúsculas e minúsculas.
66fn codigo_para_idioma(codigo: &str) -> Option<Idioma> {
67    let normalizado = codigo.to_lowercase();
68    match normalizado.as_str() {
69        "pt" | "pt-br" | "pt_br" => Some(Idioma::Portugues),
70        "en" | "en-us" | "en_us" => Some(Idioma::English),
71        outro => {
72            if outro.starts_with("pt") {
73                Some(Idioma::Portugues)
74            } else if outro.starts_with("en") {
75                Some(Idioma::English)
76            } else {
77                None
78            }
79        }
80    }
81}
82
83#[cfg(test)]
84mod testes {
85    use super::*;
86
87    #[test]
88    fn codigo_pt_retorna_portugues() {
89        assert_eq!(codigo_para_idioma("pt"), Some(Idioma::Portugues));
90    }
91
92    #[test]
93    fn codigo_pt_br_retorna_portugues() {
94        assert_eq!(codigo_para_idioma("pt-BR"), Some(Idioma::Portugues));
95    }
96
97    #[test]
98    fn codigo_pt_br_underscore_retorna_portugues() {
99        assert_eq!(codigo_para_idioma("pt_BR"), Some(Idioma::Portugues));
100    }
101
102    #[test]
103    fn codigo_en_retorna_english() {
104        assert_eq!(codigo_para_idioma("en"), Some(Idioma::English));
105    }
106
107    #[test]
108    fn codigo_en_us_retorna_english() {
109        assert_eq!(codigo_para_idioma("en-US"), Some(Idioma::English));
110    }
111
112    #[test]
113    fn codigo_en_gb_retorna_english_por_prefixo() {
114        assert_eq!(codigo_para_idioma("en-GB"), Some(Idioma::English));
115    }
116
117    #[test]
118    fn codigo_desconhecido_retorna_none() {
119        assert_eq!(codigo_para_idioma("fr-FR"), None);
120    }
121
122    #[test]
123    fn codigo_vazio_retorna_none() {
124        assert_eq!(codigo_para_idioma(""), None);
125    }
126
127    #[test]
128    fn codigo_maiusculo_normalizado() {
129        assert_eq!(codigo_para_idioma("PT"), Some(Idioma::Portugues));
130        assert_eq!(codigo_para_idioma("EN"), Some(Idioma::English));
131    }
132
133    #[test]
134    fn resolver_com_forcar_pt_retorna_portugues() {
135        let resultado = resolver_idioma(Some("pt-BR"));
136        assert_eq!(resultado, Idioma::Portugues);
137    }
138
139    #[test]
140    fn resolver_com_forcar_en_retorna_english() {
141        let resultado = resolver_idioma(Some("en-US"));
142        assert_eq!(resultado, Idioma::English);
143    }
144
145    #[test]
146    fn resolver_com_forcar_invalido_usa_camadas_seguintes() {
147        // Código inválido não resolve na camada 1; deve cair em sys_locale ou fallback.
148        std::env::remove_var("SSH_CLI_LANG");
149        let resultado = resolver_idioma(Some("xx-YY"));
150        // Deve retornar English ou Portugues — não pode ser um valor inválido.
151        assert!(
152            resultado == Idioma::English || resultado == Idioma::Portugues,
153            "resolver_idioma deve retornar idioma válido mesmo com código inválido"
154        );
155    }
156
157    #[test]
158    fn resolver_sem_forcar_retorna_idioma_valido() {
159        std::env::remove_var("SSH_CLI_LANG");
160        let resultado = resolver_idioma(None);
161        assert!(
162            resultado == Idioma::English || resultado == Idioma::Portugues,
163            "resolver_idioma deve retornar idioma válido"
164        );
165    }
166
167    #[test]
168    fn idioma_atual_retorna_fallback_english_antes_de_definir() {
169        // Não chamamos definir_idioma — o OnceLock pode já estar setado em outros testes,
170        // mas o resultado DEVE ser um idioma válido.
171        let resultado = idioma_atual();
172        assert!(
173            resultado == Idioma::English || resultado == Idioma::Portugues,
174            "idioma_atual deve retornar idioma válido"
175        );
176    }
177}