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