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