Skip to main content

context7_cli/
i18n.rs

1/// Bilingual internationalisation (EN / PT-BR) for user-facing messages.
2///
3/// Language resolution order:
4/// 1. CLI flag `--lang en|pt`
5/// 2. Environment variable `CONTEXT7_LANG`
6/// 3. `sys_locale::get_locale()` — locale starting with `"pt"` → Portuguese
7/// 4. Default: English
8///
9/// Call [`definir_idioma`] once at startup (in `run()`), then call
10/// [`idioma_atual`] or [`t`] anywhere to retrieve localised strings.
11use std::sync::OnceLock;
12
13// ─── IDIOMA ───────────────────────────────────────────────────────────────────
14
15/// Supported display languages.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Idioma {
18    /// English output.
19    English,
20    /// Brazilian Portuguese output.
21    Portugues,
22}
23
24/// Global language setting — written once at startup, read-only thereafter.
25static IDIOMA_GLOBAL: OnceLock<Idioma> = OnceLock::new();
26
27/// Returns the currently configured language.
28///
29/// Defaults to [`Idioma::English`] if [`definir_idioma`] has not been called.
30pub fn idioma_atual() -> Idioma {
31    *IDIOMA_GLOBAL.get().unwrap_or(&Idioma::English)
32}
33
34/// Sets the global language. Silently ignored if already set (OnceLock semantics).
35pub fn definir_idioma(idioma: Idioma) {
36    let _ = IDIOMA_GLOBAL.set(idioma);
37}
38
39/// Resolves the language from CLI flag, env var, or system locale.
40///
41/// Resolution order:
42/// 1. `cli_lang` — value of `--lang` flag (if provided)
43/// 2. `CONTEXT7_LANG` environment variable
44/// 3. `sys_locale::get_locale()` — BCP 47 locale (e.g. `"pt-BR"`)
45/// 4. Default: English
46pub fn resolver_idioma(cli_lang: Option<&str>) -> Idioma {
47    // 1. CLI flag
48    if let Some(lang) = cli_lang {
49        return parse_lang_str(lang);
50    }
51
52    // 2. Environment variable
53    if let Ok(env_lang) = std::env::var("CONTEXT7_LANG") {
54        return parse_lang_str(&env_lang);
55    }
56
57    // 3. System locale
58    if let Some(locale) = sys_locale::get_locale() {
59        if locale.to_lowercase().starts_with("pt") {
60            return Idioma::Portugues;
61        }
62    }
63
64    // 4. Default
65    Idioma::English
66}
67
68fn parse_lang_str(s: &str) -> Idioma {
69    match s.to_lowercase().as_str() {
70        "pt" | "pt-br" | "pt_br" | "portugues" | "português" => Idioma::Portugues,
71        _ => Idioma::English,
72    }
73}
74
75// ─── MENSAGEM ─────────────────────────────────────────────────────────────────
76
77/// All user-facing messages indexed by variant.
78///
79/// Each variant maps to a pair of `(English, Portuguese)` strings.
80#[derive(Debug, Clone, Copy)]
81pub enum Mensagem {
82    // Keys subcommand (14 variants)
83    /// "Key added successfully at: {path}"
84    ChaveAdicionada,
85    /// "No key stored."
86    NenhumaChaveArmazenada,
87    /// "Use `context7 keys add <KEY>` to add a key."
88    UsarKeysAdd,
89    /// "{n} key(s) stored:"
90    ContadorChaves,
91    /// "No key stored to remove."
92    NenhumaChaveParaRemover,
93    /// "Index {i} invalid. Use a number between 1 and {n}."
94    IndiceInvalido,
95    /// "Key {masked} removed successfully."
96    ChaveRemovidaSucesso,
97    /// "Operation cancelled."
98    OperacaoCancelada,
99    /// "All keys removed."
100    TodasChavesRemovidas,
101    /// "System does not support XDG directories."
102    SistemaXdgNaoSuportado,
103    /// "{imported}/{total} key(s) imported successfully."
104    ChavesImportadasSucesso,
105    /// "No CONTEXT7_API= key found in: {file}"
106    NenhumaChaveContext7NoArquivo,
107    /// "Are you sure you want to remove ALL keys? [y/N] " / "[s/N] "
108    ConfirmarRemoverTodas,
109    /// Accepted confirmation responses: "y"/"yes" or "s"/"sim"
110    RespostaConfirmacao,
111
112    // Library / Docs (8 variants)
113    /// "No library found."
114    NenhumaBibliotecaEncontrada,
115    /// "Libraries found:"
116    BibliotecasEncontradas,
117    /// "Trust:"
118    ConfiancaScore,
119    /// "No documentation found."
120    NenhumaDocumentacaoEncontrada,
121    /// "Documentation:"
122    TituloDocumentacao,
123    /// "Sources:"
124    TituloFontes,
125    /// "No content available."
126    SemConteudoDisponivel,
127    /// "Searching library: {name}"
128    BuscandoBiblioteca,
129
130    // HTTP / Network errors (8 variants)
131    /// "Network error searching library: {err}"
132    ErroDeRede,
133    /// "Network error fetching documentation: {err}"
134    ErroDeRedeDocs,
135    /// "Failed to deserialise JSON response: {err}"
136    FalhaDesserializar,
137    /// "Rate limit reached (429), waiting for retry…"
138    RateLimitAtingido,
139    /// "Server error ({status}), retrying…"
140    ErrodoServidor,
141    /// "Invalid API key (401/403), trying next…"
142    ChaveApiInvalida,
143    /// "Attempt {n}/{max}"
144    Tentativa,
145    /// "Waiting {ms}ms before retrying…"
146    AguardandoRetry,
147
148    // Config / XDG (7 variants)
149    /// "System does not support XDG — cannot save configuration"
150    ErroCaminhoXdg,
151    /// "Failed to read XDG config at: {path}"
152    FalhaLerConfig,
153    /// "Invalid TOML at: {path}"
154    FalhaTomlInvalido,
155    /// "Failed to write config at: {path}"
156    FalhaEscreverConfig,
157    /// "Failed to create directory: {path}"
158    FalhaCriarDiretorio,
159    /// "No API key configured. Set CONTEXT7_API_KEYS or use `keys add`."
160    NenhumaChaveConfigurada,
161    /// "Failed to serialise configuration to TOML"
162    FalhaSerializarToml,
163
164    // Logging / info (8 variants)
165    /// "Keys loaded from CONTEXT7_API_KEYS environment variable"
166    ChavesCarregadasEnvVar,
167    /// "Keys loaded from XDG configuration"
168    ChavesCarregadasXdg,
169    /// "Failed to read XDG configuration (continuing): {err}"
170    FalhaLerXdgContinuando,
171    /// "Starting context7 with {n} API keys available"
172    IniciandoComChaves,
173    /// "Keys loaded from compile-time CONTEXT7_API_KEYS"
174    ChavesCarregadasCompileTime,
175    /// "Failed to serialise results to JSON"
176    FalhaSerializarJson,
177    /// "Failed to serialise documentation to JSON"
178    FalhaSerializarDocs,
179    /// "Failed to search library '{name}'"
180    FalhaBuscarBiblioteca,
181
182    // Permissions / IO (2 variants)
183    /// "Failed to read metadata of: {path}"
184    FalhaLerMetadados,
185    /// "Failed to set permissions on: {path}"
186    FalhaDefinirPermissoes,
187}
188
189/// Returns the localised string for a message in the current language.
190///
191/// For parameterised messages use `format!("{} {}", t(Mensagem::Foo), param)`.
192pub fn t(msg: Mensagem) -> &'static str {
193    match idioma_atual() {
194        Idioma::English => en(msg),
195        Idioma::Portugues => pt(msg),
196    }
197}
198
199fn en(msg: Mensagem) -> &'static str {
200    match msg {
201        // Keys
202        Mensagem::ChaveAdicionada => "Key added successfully at:",
203        Mensagem::NenhumaChaveArmazenada => "No key stored.",
204        Mensagem::UsarKeysAdd => "Use `context7 keys add <KEY>` to add a key.",
205        Mensagem::ContadorChaves => "key(s) stored:",
206        Mensagem::NenhumaChaveParaRemover => "No key stored to remove.",
207        Mensagem::IndiceInvalido => "Invalid index. Use a number between 1 and",
208        Mensagem::ChaveRemovidaSucesso => "Key removed successfully.",
209        Mensagem::OperacaoCancelada => "Operation cancelled.",
210        Mensagem::TodasChavesRemovidas => "All keys removed.",
211        Mensagem::SistemaXdgNaoSuportado => "System does not support XDG directories.",
212        Mensagem::ChavesImportadasSucesso => "key(s) imported successfully.",
213        Mensagem::NenhumaChaveContext7NoArquivo => "No CONTEXT7_API= key found in:",
214        Mensagem::ConfirmarRemoverTodas => "Are you sure you want to remove ALL keys? [y/N] ",
215        Mensagem::RespostaConfirmacao => "y|yes",
216
217        // Library / Docs
218        Mensagem::NenhumaBibliotecaEncontrada => "No library found.",
219        Mensagem::BibliotecasEncontradas => "Libraries found:",
220        Mensagem::ConfiancaScore => "Trust:",
221        Mensagem::NenhumaDocumentacaoEncontrada => "No documentation found.",
222        Mensagem::TituloDocumentacao => "Documentation:",
223        Mensagem::TituloFontes => "Sources:",
224        Mensagem::SemConteudoDisponivel => "No content available.",
225        Mensagem::BuscandoBiblioteca => "Searching library:",
226
227        // HTTP / Network
228        Mensagem::ErroDeRede => "Network error searching library:",
229        Mensagem::ErroDeRedeDocs => "Network error fetching documentation:",
230        Mensagem::FalhaDesserializar => "Failed to deserialise JSON response:",
231        Mensagem::RateLimitAtingido => "Rate limit reached (429), waiting for retry…",
232        Mensagem::ErrodoServidor => "Server error, retrying…",
233        Mensagem::ChaveApiInvalida => "Invalid API key (401/403), trying next…",
234        Mensagem::Tentativa => "Attempt",
235        Mensagem::AguardandoRetry => "Waiting before retrying…",
236
237        // Config / XDG
238        Mensagem::ErroCaminhoXdg => "System does not support XDG — cannot save configuration",
239        Mensagem::FalhaLerConfig => "Failed to read XDG config at:",
240        Mensagem::FalhaTomlInvalido => "Invalid TOML at:",
241        Mensagem::FalhaEscreverConfig => "Failed to write config at:",
242        Mensagem::FalhaCriarDiretorio => "Failed to create directory:",
243        Mensagem::NenhumaChaveConfigurada => {
244            "No API key configured. Set CONTEXT7_API_KEYS or use `context7 keys add <KEY>`."
245        }
246        Mensagem::FalhaSerializarToml => "Failed to serialise configuration to TOML",
247
248        // Logging / info
249        Mensagem::ChavesCarregadasEnvVar => {
250            "Keys loaded from CONTEXT7_API_KEYS environment variable"
251        }
252        Mensagem::ChavesCarregadasXdg => "Keys loaded from XDG configuration",
253        Mensagem::FalhaLerXdgContinuando => "Failed to read XDG configuration (continuing):",
254        Mensagem::IniciandoComChaves => "Starting context7 with",
255        Mensagem::ChavesCarregadasCompileTime => "Keys loaded from compile-time CONTEXT7_API_KEYS",
256        Mensagem::FalhaSerializarJson => "Failed to serialise results to JSON",
257        Mensagem::FalhaSerializarDocs => "Failed to serialise documentation to JSON",
258        Mensagem::FalhaBuscarBiblioteca => "Failed to search library",
259
260        // Permissions / IO
261        Mensagem::FalhaLerMetadados => "Failed to read metadata of:",
262        Mensagem::FalhaDefinirPermissoes => "Failed to set permissions on:",
263    }
264}
265
266fn pt(msg: Mensagem) -> &'static str {
267    match msg {
268        // Keys
269        Mensagem::ChaveAdicionada => "Chave adicionada com sucesso em:",
270        Mensagem::NenhumaChaveArmazenada => "Nenhuma chave armazenada.",
271        Mensagem::UsarKeysAdd => "Use `context7 keys add <CHAVE>` para adicionar uma chave.",
272        Mensagem::ContadorChaves => "chave(s) armazenada(s):",
273        Mensagem::NenhumaChaveParaRemover => "Nenhuma chave armazenada para remover.",
274        Mensagem::IndiceInvalido => "Índice inválido. Use um número entre 1 e",
275        Mensagem::ChaveRemovidaSucesso => "Chave removida com sucesso.",
276        Mensagem::OperacaoCancelada => "Operação cancelada.",
277        Mensagem::TodasChavesRemovidas => "Todas as chaves foram removidas.",
278        Mensagem::SistemaXdgNaoSuportado => "Sistema não suporta diretórios XDG.",
279        Mensagem::ChavesImportadasSucesso => "chave(s) importada(s) com sucesso.",
280        Mensagem::NenhumaChaveContext7NoArquivo => "Nenhuma chave CONTEXT7_API= encontrada em:",
281        Mensagem::ConfirmarRemoverTodas => "Tem certeza que deseja remover TODAS as chaves? [s/N] ",
282        Mensagem::RespostaConfirmacao => "s|sim",
283
284        // Library / Docs
285        Mensagem::NenhumaBibliotecaEncontrada => "Nenhuma biblioteca encontrada.",
286        Mensagem::BibliotecasEncontradas => "Bibliotecas encontradas:",
287        Mensagem::ConfiancaScore => "Confiança:",
288        Mensagem::NenhumaDocumentacaoEncontrada => "Nenhuma documentação encontrada.",
289        Mensagem::TituloDocumentacao => "Documentação:",
290        Mensagem::TituloFontes => "Fontes:",
291        Mensagem::SemConteudoDisponivel => "Sem conteúdo disponível.",
292        Mensagem::BuscandoBiblioteca => "Buscando biblioteca:",
293
294        // HTTP / Network
295        Mensagem::ErroDeRede => "Erro de rede ao buscar biblioteca:",
296        Mensagem::ErroDeRedeDocs => "Erro de rede ao buscar documentação:",
297        Mensagem::FalhaDesserializar => "Falha ao desserializar resposta JSON:",
298        Mensagem::RateLimitAtingido => "Rate limit atingido (429), aguardando retry…",
299        Mensagem::ErrodoServidor => "Erro do servidor, tentando novamente…",
300        Mensagem::ChaveApiInvalida => "Chave de API inválida (401/403), tentando próxima…",
301        Mensagem::Tentativa => "Tentativa",
302        Mensagem::AguardandoRetry => "Aguardando antes de tentar novamente…",
303
304        // Config / XDG
305        Mensagem::ErroCaminhoXdg => {
306            "Sistema não suporta diretórios XDG — impossível salvar configuração"
307        }
308        Mensagem::FalhaLerConfig => "Falha ao ler configuração XDG em:",
309        Mensagem::FalhaTomlInvalido => "TOML inválido em:",
310        Mensagem::FalhaEscreverConfig => "Falha ao escrever config em:",
311        Mensagem::FalhaCriarDiretorio => "Falha ao criar diretório:",
312        Mensagem::NenhumaChaveConfigurada => {
313            "Nenhuma chave de API encontrada. Configure CONTEXT7_API_KEYS ou use `context7 keys add <CHAVE>`."
314        }
315        Mensagem::FalhaSerializarToml => "Falha ao serializar configuração para TOML",
316
317        // Logging / info
318        Mensagem::ChavesCarregadasEnvVar => {
319            "Chaves carregadas via variável de ambiente CONTEXT7_API_KEYS"
320        }
321        Mensagem::ChavesCarregadasXdg => "Chaves carregadas via configuração XDG",
322        Mensagem::FalhaLerXdgContinuando => "Falha ao ler configuração XDG (continuando):",
323        Mensagem::IniciandoComChaves => "Iniciando context7 com",
324        Mensagem::ChavesCarregadasCompileTime => {
325            "Chaves carregadas via compile-time CONTEXT7_API_KEYS"
326        }
327        Mensagem::FalhaSerializarJson => "Falha ao serializar resultados para JSON",
328        Mensagem::FalhaSerializarDocs => "Falha ao serializar documentação para JSON",
329        Mensagem::FalhaBuscarBiblioteca => "Falha ao buscar biblioteca",
330
331        // Permissions / IO
332        Mensagem::FalhaLerMetadados => "Falha ao ler metadados de:",
333        Mensagem::FalhaDefinirPermissoes => "Falha ao definir permissões em:",
334    }
335}
336
337// ─── TESTES ───────────────────────────────────────────────────────────────────
338
339#[cfg(test)]
340mod testes {
341    use super::*;
342
343    #[test]
344    fn testa_idioma_atual_padrao_e_english() {
345        // Se OnceLock ainda não foi setado neste processo de teste, deve ser English
346        // (pode ter sido setado por outro teste — verificamos apenas que não panics)
347        let _ = idioma_atual();
348    }
349
350    #[test]
351    fn testa_resolver_idioma_cli_flag_pt() {
352        assert_eq!(resolver_idioma(Some("pt")), Idioma::Portugues);
353        assert_eq!(resolver_idioma(Some("pt-BR")), Idioma::Portugues);
354        assert_eq!(resolver_idioma(Some("PT_BR")), Idioma::Portugues);
355    }
356
357    #[test]
358    fn testa_resolver_idioma_cli_flag_en() {
359        assert_eq!(resolver_idioma(Some("en")), Idioma::English);
360        assert_eq!(resolver_idioma(Some("en-US")), Idioma::English);
361    }
362
363    #[test]
364    fn testa_resolver_idioma_sem_flag_nem_env_retorna_english_ou_pt() {
365        // Sem flag e sem env, deve retornar English ou Portugues (dependendo do sistema)
366        let idioma = resolver_idioma(None);
367        assert!(idioma == Idioma::English || idioma == Idioma::Portugues);
368    }
369
370    #[test]
371    fn testa_t_mensagem_nenhuma_chave_en() {
372        let msg_en = en(Mensagem::NenhumaChaveArmazenada);
373        assert!(!msg_en.is_empty());
374        assert!(
375            msg_en.to_lowercase().contains("no") || msg_en.to_lowercase().contains("key"),
376            "EN deve conter 'no' ou 'key', obteve: {}",
377            msg_en
378        );
379    }
380
381    #[test]
382    fn testa_t_mensagem_nenhuma_chave_pt() {
383        let msg_pt = pt(Mensagem::NenhumaChaveArmazenada);
384        assert!(!msg_pt.is_empty());
385        assert!(
386            msg_pt.to_lowercase().contains("nenhuma") || msg_pt.to_lowercase().contains("chave"),
387            "PT deve conter 'nenhuma' ou 'chave', obteve: {}",
388            msg_pt
389        );
390    }
391
392    #[test]
393    fn testa_confirmacao_resposta_en_contem_y() {
394        let confirmacao = en(Mensagem::RespostaConfirmacao);
395        assert!(
396            confirmacao.contains('y'),
397            "EN: resposta confirmação deve conter 'y'"
398        );
399    }
400
401    #[test]
402    fn testa_confirmacao_resposta_pt_contem_s() {
403        let confirmacao = pt(Mensagem::RespostaConfirmacao);
404        assert!(
405            confirmacao.contains('s'),
406            "PT: resposta confirmação deve conter 's'"
407        );
408    }
409
410    #[test]
411    fn testa_todas_variantes_en_nao_sao_vazias() {
412        let variantes = [
413            Mensagem::ChaveAdicionada,
414            Mensagem::NenhumaChaveArmazenada,
415            Mensagem::UsarKeysAdd,
416            Mensagem::ContadorChaves,
417            Mensagem::NenhumaChaveParaRemover,
418            Mensagem::IndiceInvalido,
419            Mensagem::ChaveRemovidaSucesso,
420            Mensagem::OperacaoCancelada,
421            Mensagem::TodasChavesRemovidas,
422            Mensagem::SistemaXdgNaoSuportado,
423            Mensagem::ChavesImportadasSucesso,
424            Mensagem::NenhumaChaveContext7NoArquivo,
425            Mensagem::ConfirmarRemoverTodas,
426            Mensagem::RespostaConfirmacao,
427            Mensagem::NenhumaBibliotecaEncontrada,
428            Mensagem::BibliotecasEncontradas,
429            Mensagem::ConfiancaScore,
430            Mensagem::NenhumaDocumentacaoEncontrada,
431            Mensagem::TituloDocumentacao,
432            Mensagem::TituloFontes,
433            Mensagem::SemConteudoDisponivel,
434            Mensagem::BuscandoBiblioteca,
435            Mensagem::ErroDeRede,
436            Mensagem::ErroDeRedeDocs,
437            Mensagem::FalhaDesserializar,
438            Mensagem::RateLimitAtingido,
439            Mensagem::ErrodoServidor,
440            Mensagem::ChaveApiInvalida,
441            Mensagem::Tentativa,
442            Mensagem::AguardandoRetry,
443            Mensagem::ErroCaminhoXdg,
444            Mensagem::FalhaLerConfig,
445            Mensagem::FalhaTomlInvalido,
446            Mensagem::FalhaEscreverConfig,
447            Mensagem::FalhaCriarDiretorio,
448            Mensagem::NenhumaChaveConfigurada,
449            Mensagem::FalhaSerializarToml,
450            Mensagem::ChavesCarregadasEnvVar,
451            Mensagem::ChavesCarregadasXdg,
452            Mensagem::FalhaLerXdgContinuando,
453            Mensagem::IniciandoComChaves,
454            Mensagem::ChavesCarregadasCompileTime,
455            Mensagem::FalhaSerializarJson,
456            Mensagem::FalhaSerializarDocs,
457            Mensagem::FalhaBuscarBiblioteca,
458            Mensagem::FalhaLerMetadados,
459            Mensagem::FalhaDefinirPermissoes,
460        ];
461
462        for v in &variantes {
463            let msg_en = en(*v);
464            let msg_pt = pt(*v);
465            assert!(
466                !msg_en.is_empty(),
467                "EN mensagem vazia para variante {:?}",
468                v
469            );
470            assert!(
471                !msg_pt.is_empty(),
472                "PT mensagem vazia para variante {:?}",
473                v
474            );
475        }
476    }
477}