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 pub fn edit_recomputing_embedding() -> &'static str {
570 "Recalculando embedding da memória editada..."
571 }
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use serial_test::serial;
579
580 #[test]
581 #[serial]
582 fn fallback_english_when_env_absent() {
583 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
584 std::env::set_var("LC_ALL", "C");
585 std::env::set_var("LANG", "C");
586 assert_eq!(Language::from_env_or_locale(), Language::English);
587 std::env::remove_var("LC_ALL");
588 std::env::remove_var("LANG");
589 }
590
591 #[test]
592 #[serial]
593 fn env_pt_selects_portuguese() {
594 std::env::remove_var("LC_ALL");
595 std::env::remove_var("LANG");
596 std::env::set_var("SQLITE_GRAPHRAG_LANG", "pt");
597 assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
598 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
599 }
600
601 #[test]
602 #[serial]
603 fn env_pt_br_selects_portuguese() {
604 std::env::remove_var("LC_ALL");
605 std::env::remove_var("LANG");
606 std::env::set_var("SQLITE_GRAPHRAG_LANG", "pt-BR");
607 assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
608 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
609 }
610
611 #[test]
612 #[serial]
613 fn locale_ptbr_utf8_selects_portuguese() {
614 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
615 std::env::set_var("LC_ALL", "pt_BR.UTF-8");
616 assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
617 std::env::remove_var("LC_ALL");
618 }
619
620 #[test]
621 #[serial]
622 fn posix_precedence_lc_all_overrides_lang() {
623 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
624 std::env::remove_var("LC_MESSAGES");
625 std::env::set_var("LC_ALL", "en_US.UTF-8");
626 std::env::set_var("LANG", "pt_BR.UTF-8");
627 assert_eq!(
628 Language::from_env_or_locale(),
629 Language::English,
630 "LC_ALL=en_US must override LANG=pt_BR per POSIX"
631 );
632 std::env::remove_var("LC_ALL");
633 std::env::remove_var("LANG");
634 }
635
636 #[test]
637 #[serial]
638 fn posix_precedence_lc_all_unrecognized_stops_iteration() {
639 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
640 std::env::remove_var("LC_MESSAGES");
641 std::env::set_var("LC_ALL", "ja_JP.UTF-8");
642 std::env::set_var("LANG", "pt_BR.UTF-8");
643 assert_eq!(
644 Language::from_env_or_locale(),
645 Language::English,
646 "LC_ALL=ja_JP set must stop iteration; falls back to English default"
647 );
648 std::env::remove_var("LC_ALL");
649 std::env::remove_var("LANG");
650 }
651
652 #[test]
653 #[serial]
654 fn lang_pt_selects_portuguese_when_lc_all_unset() {
655 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
656 std::env::remove_var("LC_ALL");
657 std::env::remove_var("LC_MESSAGES");
658 std::env::set_var("LANG", "pt_BR.UTF-8");
659 assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
660 std::env::remove_var("LANG");
661 }
662
663 mod validation_tests {
664 use super::*;
665
666 #[test]
667 fn name_length_en() {
668 let msg = match Language::English {
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!(msg.contains("name must be 1-80 chars"), "obtido: {msg}");
673 }
674
675 #[test]
676 fn name_length_pt() {
677 let msg = match Language::Portuguese {
678 Language::English => format!("name must be 1-{} chars", 80),
679 Language::Portuguese => format!("nome deve ter entre 1 e {} caracteres", 80),
680 };
681 assert!(
682 msg.contains("nome deve ter entre 1 e 80 caracteres"),
683 "obtido: {msg}"
684 );
685 }
686
687 #[test]
688 fn name_kebab_en() {
689 let nome = "Invalid_Name";
690 let msg = match Language::English {
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 slug"), "obtido: {msg}");
699 assert!(msg.contains("Invalid_Name"), "obtido: {msg}");
700 }
701
702 #[test]
703 fn name_kebab_pt() {
704 let nome = "Invalid_Name";
705 let msg = match Language::Portuguese {
706 Language::English => format!(
707 "name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
708 ),
709 Language::Portuguese => {
710 format!("nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'")
711 }
712 };
713 assert!(msg.contains("kebab-case"), "obtido: {msg}");
714 assert!(msg.contains("minúsculas"), "obtido: {msg}");
715 assert!(msg.contains("Invalid_Name"), "obtido: {msg}");
716 }
717
718 #[test]
719 fn description_exceeds_en() {
720 let msg = match Language::English {
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!(msg.contains("description must be <= 500"), "obtido: {msg}");
725 }
726
727 #[test]
728 fn description_exceeds_pt() {
729 let msg = match Language::Portuguese {
730 Language::English => format!("description must be <= {} chars", 500),
731 Language::Portuguese => format!("descrição deve ter no máximo {} caracteres", 500),
732 };
733 assert!(
734 msg.contains("descrição deve ter no máximo 500"),
735 "obtido: {msg}"
736 );
737 }
738
739 #[test]
740 fn body_exceeds_en() {
741 let limite = crate::constants::MAX_MEMORY_BODY_LEN;
742 let msg = match Language::English {
743 Language::English => format!("body exceeds {limite} bytes"),
744 Language::Portuguese => format!("corpo excede {limite} bytes"),
745 };
746 assert!(msg.contains("body exceeds 512000"), "obtido: {msg}");
747 }
748
749 #[test]
750 fn body_exceeds_pt() {
751 let limite = crate::constants::MAX_MEMORY_BODY_LEN;
752 let msg = match Language::Portuguese {
753 Language::English => format!("body exceeds {limite} bytes"),
754 Language::Portuguese => format!("corpo excede {limite} bytes"),
755 };
756 assert!(msg.contains("corpo excede 512000"), "obtido: {msg}");
757 }
758
759 #[test]
760 fn new_name_length_en() {
761 let msg = match Language::English {
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!(msg.contains("new-name must be 1-80"), "obtido: {msg}");
766 }
767
768 #[test]
769 fn new_name_length_pt() {
770 let msg = match Language::Portuguese {
771 Language::English => format!("new-name must be 1-{} chars", 80),
772 Language::Portuguese => format!("novo nome deve ter entre 1 e {} caracteres", 80),
773 };
774 assert!(
775 msg.contains("novo nome deve ter entre 1 e 80"),
776 "obtido: {msg}"
777 );
778 }
779
780 #[test]
781 fn new_name_kebab_en() {
782 let nome = "Bad Name";
783 let msg = match Language::English {
784 Language::English => format!(
785 "new-name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
786 ),
787 Language::Portuguese => format!(
788 "novo nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'"
789 ),
790 };
791 assert!(msg.contains("new-name must be kebab-case"), "obtido: {msg}");
792 }
793
794 #[test]
795 fn new_name_kebab_pt() {
796 let nome = "Bad Name";
797 let msg = match Language::Portuguese {
798 Language::English => format!(
799 "new-name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
800 ),
801 Language::Portuguese => format!(
802 "novo nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'"
803 ),
804 };
805 assert!(
806 msg.contains("novo nome deve estar em kebab-case"),
807 "obtido: {msg}"
808 );
809 }
810
811 #[test]
812 fn reserved_name_en() {
813 let msg = match Language::English {
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("reserved for internal use"), "obtido: {msg}");
824 }
825
826 #[test]
827 fn reserved_name_pt() {
828 let msg = match Language::Portuguese {
829 Language::English => {
830 "names and namespaces starting with __ are reserved for internal use"
831 .to_string()
832 }
833 Language::Portuguese => {
834 "nomes e namespaces iniciados com __ são reservados para uso interno"
835 .to_string()
836 }
837 };
838 assert!(msg.contains("reservados para uso interno"), "obtido: {msg}");
839 }
840 }
841
842 mod app_error_pt_translation_tests {
843 use crate::errors::AppError;
844
845 #[test]
846 fn localized_message_pt_not_found_fully_translated() {
847 let err =
848 AppError::NotFound("memory 'test-mem' not found in namespace 'global'".into());
849 let pt = err.localized_message_for(crate::i18n::Language::Portuguese);
850 assert!(
851 pt.contains("memória"),
852 "PT must translate 'memory' to 'memória': {pt}"
853 );
854 assert!(
855 pt.contains("não encontrada no namespace"),
856 "PT must translate full phrase: {pt}"
857 );
858 assert!(
859 !pt.contains("not found in namespace"),
860 "PT must not contain English phrase: {pt}"
861 );
862 }
863
864 #[test]
865 fn localized_message_pt_duplicate_fully_translated() {
866 let err = AppError::Duplicate(
867 "memory 'x' already exists in namespace 'global'. Use --force-merge to update."
868 .into(),
869 );
870 let pt = err.localized_message_for(crate::i18n::Language::Portuguese);
871 assert!(pt.contains("memória"), "PT must translate 'memory': {pt}");
872 assert!(
873 pt.contains("já existe no namespace"),
874 "PT must translate 'already exists': {pt}"
875 );
876 }
877 }
878}