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