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