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