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