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