1use 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 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") {
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 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", "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 break;
72 }
73 }
74 Language::English
75 }
76}
77
78static GLOBAL_LANGUAGE: OnceLock<Language> = OnceLock::new();
79
80pub 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
96pub fn current() -> Language {
98 *GLOBAL_LANGUAGE.get_or_init(Language::from_env_or_locale)
99}
100
101pub fn tr(en: &'static str, pt: &'static str) -> &'static str {
109 match current() {
110 Language::English => en,
111 Language::Portuguese => pt,
112 }
113}
114
115pub fn error_prefix() -> &'static str {
117 match current() {
118 Language::English => "Error",
119 Language::Portuguese => "Erro",
120 }
121}
122
123pub 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
251pub 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 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 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}