Skip to main content

sqlite_graphrag/
i18n.rs

1//! Bilingual human-readable message layer.
2//!
3//! The CLI uses `--lang en|pt` (global flag) or `SQLITE_GRAPHRAG_LANG` (env var) to choose
4//! the language of stderr progress messages. JSON stdout is deterministic and identical
5//! across languages — only strings intended for humans pass through this module.
6//!
7//! Detection (highest to lowest priority):
8//! 1. Explicit `--lang` flag
9//! 2. Env var `SQLITE_GRAPHRAG_LANG`
10//! 3. OS locale (`LANG`, `LC_ALL`) with `pt` prefix
11//! 4. Fallback `English`
12
13use std::sync::OnceLock;
14
15#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
16pub enum Language {
17    #[value(name = "en", aliases = ["english", "EN"])]
18    English,
19    #[value(name = "pt", aliases = ["portugues", "portuguese", "pt-BR", "pt-br", "PT"])]
20    Portuguese,
21}
22
23impl Language {
24    /// Parses a command-line string into a `Language` without relying on clap.
25    /// Accepts the same aliases defined in `#[value(...)]`: "en", "pt", etc.
26    pub fn from_str_opt(s: &str) -> Option<Self> {
27        match s.to_lowercase().as_str() {
28            "en" | "english" => Some(Language::English),
29            "pt" | "pt-br" | "portugues" | "portuguese" => Some(Language::Portuguese),
30            _ => None,
31        }
32    }
33
34    pub fn from_env_or_locale() -> Self {
35        // v1.0.36 (L5): empty `SQLITE_GRAPHRAG_LANG` is treated as unset (no warning),
36        // matching POSIX convention where empty-string env vars are equivalent to
37        // missing ones for locale detection.
38        if let Ok(v) = std::env::var("SQLITE_GRAPHRAG_LANG") {
39            if !v.is_empty() {
40                let lower = v.to_lowercase();
41                if lower.starts_with("pt") {
42                    return Language::Portuguese;
43                }
44                if lower.starts_with("en") {
45                    return Language::English;
46                }
47                // Unrecognized non-empty value: warn and fall through to locale detection.
48                tracing::warn!(
49                    value = %v,
50                    "SQLITE_GRAPHRAG_LANG value not recognized, falling back to locale detection"
51                );
52            }
53        }
54        // POSIX locale precedence: LC_ALL > LC_MESSAGES > LANG.
55        // If LC_ALL is set, LANG must be IGNORED regardless of value.
56        // Previous implementation iterated both and returned PT on any "pt" prefix,
57        // violating POSIX semantics (e.g. `LC_ALL=en_US LANG=pt_BR` returned PT).
58        // Now: stop at first set var; recognize both "pt" and "en" prefixes; fall
59        // through to English default only when no locale var is set.
60        for var in &["LC_ALL", "LC_MESSAGES", "LANG"] {
61            if let Ok(v) = std::env::var(var) {
62                if v.is_empty() {
63                    // POSIX: empty-string env var is equivalent to unset; fall
64                    // through to the next variable in the precedence chain.
65                    continue;
66                }
67                let lower = v.to_lowercase();
68                if lower.starts_with("pt") {
69                    return Language::Portuguese;
70                }
71                if lower.starts_with("en") {
72                    return Language::English;
73                }
74                // Found non-empty var with unrecognized prefix: respect POSIX
75                // precedence by stopping iteration here. Do NOT fall through
76                // to the next var.
77                break;
78            }
79        }
80        Language::English
81    }
82}
83
84static GLOBAL_LANGUAGE: OnceLock<Language> = OnceLock::new();
85
86/// Initializes the global language. Subsequent calls are silently ignored
87/// (OnceLock semantics) — guaranteeing thread-safety and determinism.
88///
89/// v1.0.36 (L6): early-return when already initialized so the env-fallback
90/// resolver (`from_env_or_locale`) does not run a second time. Without this
91/// guard, calling `init(None)` after `current()` already populated the
92/// OnceLock causes `from_env_or_locale` to fire its `tracing::warn!` twice
93/// for unrecognized `SQLITE_GRAPHRAG_LANG` values.
94pub fn init(explicit: Option<Language>) {
95    if GLOBAL_LANGUAGE.get().is_some() {
96        return;
97    }
98    let resolved = explicit.unwrap_or_else(Language::from_env_or_locale);
99    let _ = GLOBAL_LANGUAGE.set(resolved);
100}
101
102/// Returns the active language, or fallback English if `init` was never called.
103pub fn current() -> Language {
104    *GLOBAL_LANGUAGE.get_or_init(Language::from_env_or_locale)
105}
106
107/// Translates a bilingual message by selecting the active variant.
108///
109/// v1.0.36 (M4): inputs are constrained to `&'static str` so the function
110/// can return one of them directly without `Box::leak`. The previous
111/// implementation leaked one allocation per call which accumulated in
112/// long-running pipelines; this version is allocation-free. All in-tree
113/// callers already pass string literals, which are `&'static str`.
114pub fn tr(en: &'static str, pt: &'static str) -> &'static str {
115    match current() {
116        Language::English => en,
117        Language::Portuguese => pt,
118    }
119}
120
121/// Localized prefix for error messages displayed to the end user.
122pub fn error_prefix() -> &'static str {
123    match current() {
124        Language::English => "Error",
125        Language::Portuguese => "Erro",
126    }
127}
128
129/// Localized error messages for `AppError` variants.
130pub mod errors_msg {
131    use super::current;
132    use crate::i18n::Language;
133
134    pub fn memory_not_found(nome: &str, namespace: &str) -> String {
135        match current() {
136            Language::English => {
137                format!("memory '{nome}' not found in namespace '{namespace}'")
138            }
139            Language::Portuguese => {
140                format!("memória '{nome}' não encontrada no namespace '{namespace}'")
141            }
142        }
143    }
144
145    pub fn memory_or_entity_not_found(name: &str, namespace: &str) -> String {
146        match current() {
147            Language::English => {
148                format!("memory or entity '{name}' not found in namespace '{namespace}'")
149            }
150            Language::Portuguese => {
151                format!("memória ou entidade '{name}' não encontrada no namespace '{namespace}'")
152            }
153        }
154    }
155
156    pub fn database_not_found(path: &str) -> String {
157        match current() {
158            Language::English => {
159                format!("database not found at {path}. Run 'sqlite-graphrag init' first.")
160            }
161            Language::Portuguese => format!(
162                "banco de dados não encontrado em {path}. Execute 'sqlite-graphrag init' primeiro."
163            ),
164        }
165    }
166
167    pub fn entity_not_found(nome: &str, namespace: &str) -> String {
168        match current() {
169            Language::English => {
170                format!("entity \"{nome}\" does not exist in namespace \"{namespace}\"")
171            }
172            Language::Portuguese => {
173                format!("entidade \"{nome}\" não existe no namespace \"{namespace}\"")
174            }
175        }
176    }
177
178    pub fn relationship_not_found(de: &str, rel: &str, para: &str, namespace: &str) -> String {
179        match current() {
180            Language::English => format!(
181                "relationship \"{de}\" --[{rel}]--> \"{para}\" does not exist in namespace \"{namespace}\""
182            ),
183            Language::Portuguese => format!(
184                "relacionamento \"{de}\" --[{rel}]--> \"{para}\" não existe no namespace \"{namespace}\""
185            ),
186        }
187    }
188
189    pub fn duplicate_memory(nome: &str, namespace: &str) -> String {
190        match current() {
191            Language::English => format!(
192                "memory '{nome}' already exists in namespace '{namespace}'. Use --force-merge to update."
193            ),
194            Language::Portuguese => format!(
195                "memória '{nome}' já existe no namespace '{namespace}'. Use --force-merge para atualizar."
196            ),
197        }
198    }
199
200    pub fn optimistic_lock_conflict(expected: i64, current_ts: i64) -> String {
201        match current() {
202            Language::English => format!(
203                "optimistic lock conflict: expected updated_at={expected}, but current is {current_ts}"
204            ),
205            Language::Portuguese => format!(
206                "conflito de optimistic lock: esperava updated_at={expected}, mas atual é {current_ts}"
207            ),
208        }
209    }
210
211    pub fn version_not_found(versao: i64, nome: &str) -> String {
212        match current() {
213            Language::English => format!("version {versao} not found for memory '{nome}'"),
214            Language::Portuguese => {
215                format!("versão {versao} não encontrada para a memória '{nome}'")
216            }
217        }
218    }
219
220    pub fn no_recall_results(max_distance: f32, query: &str, namespace: &str) -> String {
221        match current() {
222            Language::English => format!(
223                "no results within --max-distance {max_distance} for query '{query}' in namespace '{namespace}'"
224            ),
225            Language::Portuguese => format!(
226                "nenhum resultado dentro de --max-distance {max_distance} para a consulta '{query}' no namespace '{namespace}'"
227            ),
228        }
229    }
230
231    pub fn soft_deleted_memory_not_found(nome: &str, namespace: &str) -> String {
232        match current() {
233            Language::English => {
234                format!("soft-deleted memory '{nome}' not found in namespace '{namespace}'")
235            }
236            Language::Portuguese => {
237                format!("memória soft-deleted '{nome}' não encontrada no namespace '{namespace}'")
238            }
239        }
240    }
241
242    pub fn concurrent_process_conflict() -> String {
243        match current() {
244            Language::English => {
245                "optimistic lock conflict: memory was modified by another process".to_string()
246            }
247            Language::Portuguese => {
248                "conflito de optimistic lock: memória foi modificada por outro processo".to_string()
249            }
250        }
251    }
252
253    pub fn entity_limit_exceeded(max: usize) -> String {
254        match current() {
255            Language::English => format!("entities exceed limit of {max}"),
256            Language::Portuguese => format!("entidades excedem o limite de {max}"),
257        }
258    }
259
260    pub fn relationship_limit_exceeded(max: usize) -> String {
261        match current() {
262            Language::English => format!("relationships exceed limit of {max}"),
263            Language::Portuguese => format!("relacionamentos excedem o limite de {max}"),
264        }
265    }
266}
267
268/// Localized validation messages for memory fields.
269pub mod validation {
270    use super::current;
271    use crate::i18n::Language;
272
273    pub fn name_length(max: usize) -> String {
274        match current() {
275            Language::English => format!("name must be 1-{max} chars"),
276            Language::Portuguese => format!("nome deve ter entre 1 e {max} caracteres"),
277        }
278    }
279
280    pub fn reserved_name() -> String {
281        match current() {
282            Language::English => {
283                "names and namespaces starting with __ are reserved for internal use".to_string()
284            }
285            Language::Portuguese => {
286                "nomes e namespaces iniciados com __ são reservados para uso interno".to_string()
287            }
288        }
289    }
290
291    pub fn name_kebab(nome: &str) -> String {
292        match current() {
293            Language::English => format!(
294                "name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
295            ),
296            Language::Portuguese => {
297                format!("nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'")
298            }
299        }
300    }
301
302    pub fn description_exceeds(max: usize) -> String {
303        match current() {
304            Language::English => format!("description must be <= {max} chars"),
305            Language::Portuguese => format!("descrição deve ter no máximo {max} caracteres"),
306        }
307    }
308
309    pub fn body_exceeds(max: usize) -> String {
310        match current() {
311            Language::English => format!("body exceeds {max} bytes"),
312            Language::Portuguese => format!("corpo excede {max} bytes"),
313        }
314    }
315
316    pub fn new_name_length(max: usize) -> String {
317        match current() {
318            Language::English => format!("new-name must be 1-{max} chars"),
319            Language::Portuguese => format!("novo nome deve ter entre 1 e {max} caracteres"),
320        }
321    }
322
323    pub fn new_name_kebab(nome: &str) -> String {
324        match current() {
325            Language::English => format!(
326                "new-name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
327            ),
328            Language::Portuguese => format!(
329                "novo nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'"
330            ),
331        }
332    }
333
334    pub fn namespace_length() -> String {
335        match current() {
336            Language::English => "namespace must be 1-80 chars".to_string(),
337            Language::Portuguese => "namespace deve ter entre 1 e 80 caracteres".to_string(),
338        }
339    }
340
341    pub fn namespace_format() -> String {
342        match current() {
343            Language::English => "namespace must be alphanumeric + hyphens/underscores".to_string(),
344            Language::Portuguese => {
345                "namespace deve ser alfanumérico com hífens/sublinhados".to_string()
346            }
347        }
348    }
349
350    pub fn path_traversal(p: &str) -> String {
351        match current() {
352            Language::English => format!("path traversal rejected: {p}"),
353            Language::Portuguese => format!("traversal de caminho rejeitado: {p}"),
354        }
355    }
356
357    pub fn invalid_tz(v: &str) -> String {
358        match current() {
359            Language::English => format!(
360                "SQLITE_GRAPHRAG_DISPLAY_TZ invalid: '{v}'; use an IANA name like 'America/Sao_Paulo'"
361            ),
362            Language::Portuguese => format!(
363                "SQLITE_GRAPHRAG_DISPLAY_TZ inválido: '{v}'; use um nome IANA como 'America/Sao_Paulo'"
364            ),
365        }
366    }
367
368    pub fn empty_query() -> String {
369        match current() {
370            Language::English => "query cannot be empty".to_string(),
371            Language::Portuguese => "a consulta não pode estar vazia".to_string(),
372        }
373    }
374
375    pub fn empty_body() -> String {
376        match current() {
377            Language::English => "body cannot be empty: provide --body, --body-file, or --body-stdin with content, or supply a graph via --entities-file/--graph-stdin".to_string(),
378            Language::Portuguese => "o corpo não pode estar vazio: forneça --body, --body-file ou --body-stdin com conteúdo, ou um grafo via --entities-file/--graph-stdin".to_string(),
379        }
380    }
381
382    pub fn invalid_namespace_config(path: &str, err: &str) -> String {
383        match current() {
384            Language::English => {
385                format!("invalid project namespace config '{path}': {err}")
386            }
387            Language::Portuguese => {
388                format!("configuração de namespace de projeto inválida '{path}': {err}")
389            }
390        }
391    }
392
393    pub fn invalid_projects_mapping(path: &str, err: &str) -> String {
394        match current() {
395            Language::English => format!("invalid projects mapping '{path}': {err}"),
396            Language::Portuguese => format!("mapeamento de projetos inválido '{path}': {err}"),
397        }
398    }
399
400    pub fn self_referential_link() -> String {
401        match current() {
402            Language::English => "--from and --to must be different entities — self-referential relationships are not supported".to_string(),
403            Language::Portuguese => "--from e --to devem ser entidades diferentes — relacionamentos auto-referenciais não são suportados".to_string(),
404        }
405    }
406
407    pub fn invalid_link_weight(weight: f64) -> String {
408        match current() {
409            Language::English => {
410                format!("--weight: must be between 0.0 and 1.0 (actual: {weight})")
411            }
412            Language::Portuguese => {
413                format!("--weight: deve estar entre 0.0 e 1.0 (atual: {weight})")
414            }
415        }
416    }
417
418    pub fn sync_destination_equals_source() -> String {
419        match current() {
420            Language::English => {
421                "destination path must differ from the source database path".to_string()
422            }
423            Language::Portuguese => {
424                "caminho de destino deve ser diferente do caminho do banco de dados fonte"
425                    .to_string()
426            }
427        }
428    }
429
430    /// Portuguese translations for `AppError` Display messages.
431    ///
432    /// Each helper mirrors a single `AppError` variant's `#[error(...)]` text in
433    /// Portuguese, keeping the language barrier confined to this module. The
434    /// English source of truth lives in `src/errors.rs` via `thiserror`.
435    pub mod app_error_pt {
436        pub fn validation(msg: &str) -> String {
437            format!("erro de validação: {msg}")
438        }
439
440        pub fn duplicate(msg: &str) -> String {
441            format!("duplicata detectada: {msg}")
442        }
443
444        pub fn conflict(msg: &str) -> String {
445            format!("conflito: {msg}")
446        }
447
448        pub fn not_found(msg: &str) -> String {
449            format!("não encontrado: {msg}")
450        }
451
452        pub fn namespace_error(msg: &str) -> String {
453            format!("namespace não resolvido: {msg}")
454        }
455
456        pub fn limit_exceeded(msg: &str) -> String {
457            format!("limite excedido: {msg}")
458        }
459
460        pub fn database(err: &str) -> String {
461            format!("erro de banco de dados: {err}")
462        }
463
464        pub fn embedding(msg: &str) -> String {
465            format!("erro de embedding: {msg}")
466        }
467
468        pub fn vec_extension(msg: &str) -> String {
469            format!("extensão sqlite-vec falhou: {msg}")
470        }
471
472        pub fn db_busy(msg: &str) -> String {
473            format!("banco ocupado: {msg}")
474        }
475
476        pub fn batch_partial_failure(total: usize, failed: usize) -> String {
477            format!("falha parcial em batch: {failed} de {total} itens falharam")
478        }
479
480        pub fn io(err: &str) -> String {
481            format!("erro de I/O: {err}")
482        }
483
484        pub fn internal(err: &str) -> String {
485            format!("erro interno: {err}")
486        }
487
488        pub fn json(err: &str) -> String {
489            format!("erro de JSON: {err}")
490        }
491
492        pub fn lock_busy(msg: &str) -> String {
493            format!("lock ocupado: {msg}")
494        }
495
496        pub fn all_slots_full(max: usize, waited_secs: u64) -> String {
497            format!(
498                "todos os {max} slots de concorrência ocupados após aguardar {waited_secs}s \
499                 (exit 75); use --max-concurrency ou aguarde outras invocações terminarem"
500            )
501        }
502
503        pub fn low_memory(available_mb: u64, required_mb: u64) -> String {
504            format!(
505                "memória disponível ({available_mb}MB) abaixo do mínimo requerido ({required_mb}MB) \
506                 para carregar o modelo; aborte outras cargas ou use --skip-memory-guard (exit 77)"
507            )
508        }
509    }
510
511    /// Portuguese translations for runtime startup messages emitted from `main.rs`.
512    ///
513    /// These mirror the English text supplied alongside each call to
514    /// `output::emit_progress_i18n` / `output::emit_error_i18n`, keeping the PT
515    /// strings confined to this module per the language policy.
516    pub mod runtime_pt {
517        pub fn embedding_heavy_must_measure_ram() -> String {
518            "comando intensivo em embedding precisa medir RAM disponível".to_string()
519        }
520
521        pub fn heavy_command_detected(available_mb: u64, safe_concurrency: usize) -> String {
522            format!(
523                "Comando pesado detectado; memória disponível: {available_mb} MB; \
524                 concorrência segura: {safe_concurrency}"
525            )
526        }
527
528        pub fn reducing_concurrency(
529            requested_concurrency: usize,
530            effective_concurrency: usize,
531        ) -> String {
532            format!(
533                "Reduzindo a concorrência solicitada de {requested_concurrency} para \
534                 {effective_concurrency} para evitar oversubscription de memória"
535            )
536        }
537
538        pub fn downloading_ner_model() -> &'static str {
539            "Baixando modelo NER (primeira execução, ~676 MB)..."
540        }
541
542        pub fn initializing_embedding_model() -> &'static str {
543            "Inicializando modelo de embedding (pode baixar na primeira execução)..."
544        }
545
546        pub fn embedding_chunks_serially(count: usize) -> String {
547            format!("Embedando {count} chunks serialmente para manter memória limitada...")
548        }
549
550        pub fn remember_step_input_validated(available_mb: u64) -> String {
551            format!("Etapa remember: entrada validada; memória disponível {available_mb} MB")
552        }
553
554        pub fn remember_step_chunking_completed(
555            total_passage_tokens: usize,
556            model_max_length: usize,
557            chunks_count: usize,
558            rss_mb: u64,
559        ) -> String {
560            format!(
561                "Etapa remember: tokenizer contou {total_passage_tokens} tokens de passagem \
562                 (máximo do modelo {model_max_length}); chunking gerou {chunks_count} chunks; \
563                 RSS do processo {rss_mb} MB"
564            )
565        }
566
567        pub fn remember_step_embeddings_completed(rss_mb: u64) -> String {
568            format!("Etapa remember: embeddings dos chunks concluídos; RSS do processo {rss_mb} MB")
569        }
570
571        pub fn restore_recomputing_embedding() -> &'static str {
572            "Recalculando embedding da memória restaurada..."
573        }
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use serial_test::serial;
581
582    #[test]
583    #[serial]
584    fn fallback_english_when_env_absent() {
585        std::env::remove_var("SQLITE_GRAPHRAG_LANG");
586        std::env::set_var("LC_ALL", "C");
587        std::env::set_var("LANG", "C");
588        assert_eq!(Language::from_env_or_locale(), Language::English);
589        std::env::remove_var("LC_ALL");
590        std::env::remove_var("LANG");
591    }
592
593    #[test]
594    #[serial]
595    fn env_pt_selects_portuguese() {
596        std::env::remove_var("LC_ALL");
597        std::env::remove_var("LANG");
598        std::env::set_var("SQLITE_GRAPHRAG_LANG", "pt");
599        assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
600        std::env::remove_var("SQLITE_GRAPHRAG_LANG");
601    }
602
603    #[test]
604    #[serial]
605    fn env_pt_br_selects_portuguese() {
606        std::env::remove_var("LC_ALL");
607        std::env::remove_var("LANG");
608        std::env::set_var("SQLITE_GRAPHRAG_LANG", "pt-BR");
609        assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
610        std::env::remove_var("SQLITE_GRAPHRAG_LANG");
611    }
612
613    #[test]
614    #[serial]
615    fn locale_ptbr_utf8_selects_portuguese() {
616        std::env::remove_var("SQLITE_GRAPHRAG_LANG");
617        std::env::set_var("LC_ALL", "pt_BR.UTF-8");
618        assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
619        std::env::remove_var("LC_ALL");
620    }
621
622    #[test]
623    #[serial]
624    fn posix_precedence_lc_all_overrides_lang() {
625        std::env::remove_var("SQLITE_GRAPHRAG_LANG");
626        std::env::remove_var("LC_MESSAGES");
627        std::env::set_var("LC_ALL", "en_US.UTF-8");
628        std::env::set_var("LANG", "pt_BR.UTF-8");
629        assert_eq!(
630            Language::from_env_or_locale(),
631            Language::English,
632            "LC_ALL=en_US must override LANG=pt_BR per POSIX"
633        );
634        std::env::remove_var("LC_ALL");
635        std::env::remove_var("LANG");
636    }
637
638    #[test]
639    #[serial]
640    fn posix_precedence_lc_all_unrecognized_stops_iteration() {
641        std::env::remove_var("SQLITE_GRAPHRAG_LANG");
642        std::env::remove_var("LC_MESSAGES");
643        std::env::set_var("LC_ALL", "ja_JP.UTF-8");
644        std::env::set_var("LANG", "pt_BR.UTF-8");
645        assert_eq!(
646            Language::from_env_or_locale(),
647            Language::English,
648            "LC_ALL=ja_JP set must stop iteration; falls back to English default"
649        );
650        std::env::remove_var("LC_ALL");
651        std::env::remove_var("LANG");
652    }
653
654    #[test]
655    #[serial]
656    fn lang_pt_selects_portuguese_when_lc_all_unset() {
657        std::env::remove_var("SQLITE_GRAPHRAG_LANG");
658        std::env::remove_var("LC_ALL");
659        std::env::remove_var("LC_MESSAGES");
660        std::env::set_var("LANG", "pt_BR.UTF-8");
661        assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
662        std::env::remove_var("LANG");
663    }
664
665    mod validation_tests {
666        use super::*;
667
668        #[test]
669        fn name_length_en() {
670            let msg = match Language::English {
671                Language::English => format!("name must be 1-{} chars", 80),
672                Language::Portuguese => format!("nome deve ter entre 1 e {} caracteres", 80),
673            };
674            assert!(msg.contains("name must be 1-80 chars"), "obtido: {msg}");
675        }
676
677        #[test]
678        fn name_length_pt() {
679            let msg = match Language::Portuguese {
680                Language::English => format!("name must be 1-{} chars", 80),
681                Language::Portuguese => format!("nome deve ter entre 1 e {} caracteres", 80),
682            };
683            assert!(
684                msg.contains("nome deve ter entre 1 e 80 caracteres"),
685                "obtido: {msg}"
686            );
687        }
688
689        #[test]
690        fn name_kebab_en() {
691            let nome = "Invalid_Name";
692            let msg = match Language::English {
693                Language::English => format!(
694                    "name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
695                ),
696                Language::Portuguese => {
697                    format!("nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'")
698                }
699            };
700            assert!(msg.contains("kebab-case slug"), "obtido: {msg}");
701            assert!(msg.contains("Invalid_Name"), "obtido: {msg}");
702        }
703
704        #[test]
705        fn name_kebab_pt() {
706            let nome = "Invalid_Name";
707            let msg = match Language::Portuguese {
708                Language::English => format!(
709                    "name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
710                ),
711                Language::Portuguese => {
712                    format!("nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'")
713                }
714            };
715            assert!(msg.contains("kebab-case"), "obtido: {msg}");
716            assert!(msg.contains("minúsculas"), "obtido: {msg}");
717            assert!(msg.contains("Invalid_Name"), "obtido: {msg}");
718        }
719
720        #[test]
721        fn description_exceeds_en() {
722            let msg = match Language::English {
723                Language::English => format!("description must be <= {} chars", 500),
724                Language::Portuguese => format!("descrição deve ter no máximo {} caracteres", 500),
725            };
726            assert!(msg.contains("description must be <= 500"), "obtido: {msg}");
727        }
728
729        #[test]
730        fn description_exceeds_pt() {
731            let msg = match Language::Portuguese {
732                Language::English => format!("description must be <= {} chars", 500),
733                Language::Portuguese => format!("descrição deve ter no máximo {} caracteres", 500),
734            };
735            assert!(
736                msg.contains("descrição deve ter no máximo 500"),
737                "obtido: {msg}"
738            );
739        }
740
741        #[test]
742        fn body_exceeds_en() {
743            let limite = crate::constants::MAX_MEMORY_BODY_LEN;
744            let msg = match Language::English {
745                Language::English => format!("body exceeds {limite} bytes"),
746                Language::Portuguese => format!("corpo excede {limite} bytes"),
747            };
748            assert!(msg.contains("body exceeds 512000"), "obtido: {msg}");
749        }
750
751        #[test]
752        fn body_exceeds_pt() {
753            let limite = crate::constants::MAX_MEMORY_BODY_LEN;
754            let msg = match Language::Portuguese {
755                Language::English => format!("body exceeds {limite} bytes"),
756                Language::Portuguese => format!("corpo excede {limite} bytes"),
757            };
758            assert!(msg.contains("corpo excede 512000"), "obtido: {msg}");
759        }
760
761        #[test]
762        fn new_name_length_en() {
763            let msg = match Language::English {
764                Language::English => format!("new-name must be 1-{} chars", 80),
765                Language::Portuguese => format!("novo nome deve ter entre 1 e {} caracteres", 80),
766            };
767            assert!(msg.contains("new-name must be 1-80"), "obtido: {msg}");
768        }
769
770        #[test]
771        fn new_name_length_pt() {
772            let msg = match Language::Portuguese {
773                Language::English => format!("new-name must be 1-{} chars", 80),
774                Language::Portuguese => format!("novo nome deve ter entre 1 e {} caracteres", 80),
775            };
776            assert!(
777                msg.contains("novo nome deve ter entre 1 e 80"),
778                "obtido: {msg}"
779            );
780        }
781
782        #[test]
783        fn new_name_kebab_en() {
784            let nome = "Bad Name";
785            let msg = match Language::English {
786                Language::English => format!(
787                    "new-name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
788                ),
789                Language::Portuguese => format!(
790                    "novo nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'"
791                ),
792            };
793            assert!(msg.contains("new-name must be kebab-case"), "obtido: {msg}");
794        }
795
796        #[test]
797        fn new_name_kebab_pt() {
798            let nome = "Bad Name";
799            let msg = match Language::Portuguese {
800                Language::English => format!(
801                    "new-name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
802                ),
803                Language::Portuguese => format!(
804                    "novo nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'"
805                ),
806            };
807            assert!(
808                msg.contains("novo nome deve estar em kebab-case"),
809                "obtido: {msg}"
810            );
811        }
812
813        #[test]
814        fn reserved_name_en() {
815            let msg = match Language::English {
816                Language::English => {
817                    "names and namespaces starting with __ are reserved for internal use"
818                        .to_string()
819                }
820                Language::Portuguese => {
821                    "nomes e namespaces iniciados com __ são reservados para uso interno"
822                        .to_string()
823                }
824            };
825            assert!(msg.contains("reserved for internal use"), "obtido: {msg}");
826        }
827
828        #[test]
829        fn reserved_name_pt() {
830            let msg = match Language::Portuguese {
831                Language::English => {
832                    "names and namespaces starting with __ are reserved for internal use"
833                        .to_string()
834                }
835                Language::Portuguese => {
836                    "nomes e namespaces iniciados com __ são reservados para uso interno"
837                        .to_string()
838                }
839            };
840            assert!(msg.contains("reservados para uso interno"), "obtido: {msg}");
841        }
842    }
843}