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