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 low_memory(available_mb: u64, required_mb: u64) -> String {
486 format!(
487 "memória disponível ({available_mb}MB) abaixo do mínimo requerido ({required_mb}MB) \
488 para carregar o modelo; aborte outras cargas ou use --skip-memory-guard (exit 77)"
489 )
490 }
491
492 pub fn binary_not_found(name: &str) -> String {
493 format!("binário não encontrado: {name} — instale e adicione ao PATH")
494 }
495
496 pub fn rate_limited(detail: &str) -> String {
497 format!("taxa de requisição excedida: {detail}")
498 }
499
500 pub fn timeout(operation: &str, secs: u64) -> String {
501 format!("timeout após {secs}s: {operation}")
502 }
503 }
504
505 pub mod runtime_pt {
511 pub fn embedding_heavy_must_measure_ram() -> String {
512 "comando intensivo em embedding precisa medir RAM disponível".to_string()
513 }
514
515 pub fn heavy_command_detected(available_mb: u64, safe_concurrency: usize) -> String {
516 format!(
517 "Comando pesado detectado; memória disponível: {available_mb} MB; \
518 concorrência segura: {safe_concurrency}"
519 )
520 }
521
522 pub fn reducing_concurrency(
523 requested_concurrency: usize,
524 effective_concurrency: usize,
525 ) -> String {
526 format!(
527 "Reduzindo a concorrência solicitada de {requested_concurrency} para \
528 {effective_concurrency} para evitar oversubscription de memória"
529 )
530 }
531
532 pub fn initializing_embedding_model() -> &'static str {
533 "Inicializando modelo de embedding (pode baixar na primeira execução)..."
534 }
535
536 pub fn embedding_chunks_serially(count: usize) -> String {
537 format!("Embedando {count} chunks serialmente para manter memória limitada...")
538 }
539
540 pub fn remember_step_input_validated(available_mb: u64) -> String {
541 format!("Etapa remember: entrada validada; memória disponível {available_mb} MB")
542 }
543
544 pub fn remember_step_chunking_completed(
545 total_passage_tokens: usize,
546 model_max_length: usize,
547 chunks_count: usize,
548 rss_mb: u64,
549 ) -> String {
550 format!(
551 "Etapa remember: tokenizer contou {total_passage_tokens} tokens de passagem \
552 (máximo do modelo {model_max_length}); chunking gerou {chunks_count} chunks; \
553 RSS do processo {rss_mb} MB"
554 )
555 }
556
557 pub fn remember_step_embeddings_completed(rss_mb: u64) -> String {
558 format!("Etapa remember: embeddings dos chunks concluídos; RSS do processo {rss_mb} MB")
559 }
560
561 pub fn restore_recomputing_embedding() -> &'static str {
562 "Recalculando embedding da memória restaurada..."
563 }
564
565 pub fn edit_recomputing_embedding() -> &'static str {
566 "Recalculando embedding da memória editada..."
567 }
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574 use serial_test::serial;
575
576 #[test]
577 #[serial]
578 fn fallback_english_when_env_absent() {
579 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
580 std::env::set_var("LC_ALL", "C");
581 std::env::set_var("LANG", "C");
582 assert_eq!(Language::from_env_or_locale(), Language::English);
583 std::env::remove_var("LC_ALL");
584 std::env::remove_var("LANG");
585 }
586
587 #[test]
588 #[serial]
589 fn env_pt_selects_portuguese() {
590 std::env::remove_var("LC_ALL");
591 std::env::remove_var("LANG");
592 std::env::set_var("SQLITE_GRAPHRAG_LANG", "pt");
593 assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
594 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
595 }
596
597 #[test]
598 #[serial]
599 fn env_pt_br_selects_portuguese() {
600 std::env::remove_var("LC_ALL");
601 std::env::remove_var("LANG");
602 std::env::set_var("SQLITE_GRAPHRAG_LANG", "pt-BR");
603 assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
604 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
605 }
606
607 #[test]
608 #[serial]
609 fn locale_ptbr_utf8_selects_portuguese() {
610 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
611 std::env::set_var("LC_ALL", "pt_BR.UTF-8");
612 assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
613 std::env::remove_var("LC_ALL");
614 }
615
616 #[test]
617 #[serial]
618 fn posix_precedence_lc_all_overrides_lang() {
619 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
620 std::env::remove_var("LC_MESSAGES");
621 std::env::set_var("LC_ALL", "en_US.UTF-8");
622 std::env::set_var("LANG", "pt_BR.UTF-8");
623 assert_eq!(
624 Language::from_env_or_locale(),
625 Language::English,
626 "LC_ALL=en_US must override LANG=pt_BR per POSIX"
627 );
628 std::env::remove_var("LC_ALL");
629 std::env::remove_var("LANG");
630 }
631
632 #[test]
633 #[serial]
634 fn posix_precedence_lc_all_unrecognized_stops_iteration() {
635 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
636 std::env::remove_var("LC_MESSAGES");
637 std::env::set_var("LC_ALL", "ja_JP.UTF-8");
638 std::env::set_var("LANG", "pt_BR.UTF-8");
639 assert_eq!(
640 Language::from_env_or_locale(),
641 Language::English,
642 "LC_ALL=ja_JP set must stop iteration; falls back to English default"
643 );
644 std::env::remove_var("LC_ALL");
645 std::env::remove_var("LANG");
646 }
647
648 #[test]
649 #[serial]
650 fn lang_pt_selects_portuguese_when_lc_all_unset() {
651 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
652 std::env::remove_var("LC_ALL");
653 std::env::remove_var("LC_MESSAGES");
654 std::env::set_var("LANG", "pt_BR.UTF-8");
655 assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
656 std::env::remove_var("LANG");
657 }
658
659 mod validation_tests {
660 use super::*;
661
662 #[test]
663 fn name_length_en() {
664 let msg = match Language::English {
665 Language::English => format!("name must be 1-{} chars", 80),
666 Language::Portuguese => format!("nome deve ter entre 1 e {} caracteres", 80),
667 };
668 assert!(msg.contains("name must be 1-80 chars"), "obtido: {msg}");
669 }
670
671 #[test]
672 fn name_length_pt() {
673 let msg = match Language::Portuguese {
674 Language::English => format!("name must be 1-{} chars", 80),
675 Language::Portuguese => format!("nome deve ter entre 1 e {} caracteres", 80),
676 };
677 assert!(
678 msg.contains("nome deve ter entre 1 e 80 caracteres"),
679 "obtido: {msg}"
680 );
681 }
682
683 #[test]
684 fn name_kebab_en() {
685 let nome = "Invalid_Name";
686 let msg = match Language::English {
687 Language::English => format!(
688 "name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
689 ),
690 Language::Portuguese => {
691 format!("nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'")
692 }
693 };
694 assert!(msg.contains("kebab-case slug"), "obtido: {msg}");
695 assert!(msg.contains("Invalid_Name"), "obtido: {msg}");
696 }
697
698 #[test]
699 fn name_kebab_pt() {
700 let nome = "Invalid_Name";
701 let msg = match Language::Portuguese {
702 Language::English => format!(
703 "name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
704 ),
705 Language::Portuguese => {
706 format!("nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'")
707 }
708 };
709 assert!(msg.contains("kebab-case"), "obtido: {msg}");
710 assert!(msg.contains("minúsculas"), "obtido: {msg}");
711 assert!(msg.contains("Invalid_Name"), "obtido: {msg}");
712 }
713
714 #[test]
715 fn description_exceeds_en() {
716 let msg = match Language::English {
717 Language::English => format!("description must be <= {} chars", 500),
718 Language::Portuguese => format!("descrição deve ter no máximo {} caracteres", 500),
719 };
720 assert!(msg.contains("description must be <= 500"), "obtido: {msg}");
721 }
722
723 #[test]
724 fn description_exceeds_pt() {
725 let msg = match Language::Portuguese {
726 Language::English => format!("description must be <= {} chars", 500),
727 Language::Portuguese => format!("descrição deve ter no máximo {} caracteres", 500),
728 };
729 assert!(
730 msg.contains("descrição deve ter no máximo 500"),
731 "obtido: {msg}"
732 );
733 }
734
735 #[test]
736 fn body_exceeds_en() {
737 let limite = crate::constants::MAX_MEMORY_BODY_LEN;
738 let msg = match Language::English {
739 Language::English => format!("body exceeds {limite} bytes"),
740 Language::Portuguese => format!("corpo excede {limite} bytes"),
741 };
742 assert!(msg.contains("body exceeds 512000"), "obtido: {msg}");
743 }
744
745 #[test]
746 fn body_exceeds_pt() {
747 let limite = crate::constants::MAX_MEMORY_BODY_LEN;
748 let msg = match Language::Portuguese {
749 Language::English => format!("body exceeds {limite} bytes"),
750 Language::Portuguese => format!("corpo excede {limite} bytes"),
751 };
752 assert!(msg.contains("corpo excede 512000"), "obtido: {msg}");
753 }
754
755 #[test]
756 fn new_name_length_en() {
757 let msg = match Language::English {
758 Language::English => format!("new-name must be 1-{} chars", 80),
759 Language::Portuguese => format!("novo nome deve ter entre 1 e {} caracteres", 80),
760 };
761 assert!(msg.contains("new-name must be 1-80"), "obtido: {msg}");
762 }
763
764 #[test]
765 fn new_name_length_pt() {
766 let msg = match Language::Portuguese {
767 Language::English => format!("new-name must be 1-{} chars", 80),
768 Language::Portuguese => format!("novo nome deve ter entre 1 e {} caracteres", 80),
769 };
770 assert!(
771 msg.contains("novo nome deve ter entre 1 e 80"),
772 "obtido: {msg}"
773 );
774 }
775
776 #[test]
777 fn new_name_kebab_en() {
778 let nome = "Bad Name";
779 let msg = match Language::English {
780 Language::English => format!(
781 "new-name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
782 ),
783 Language::Portuguese => format!(
784 "novo nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'"
785 ),
786 };
787 assert!(msg.contains("new-name must be kebab-case"), "obtido: {msg}");
788 }
789
790 #[test]
791 fn new_name_kebab_pt() {
792 let nome = "Bad Name";
793 let msg = match Language::Portuguese {
794 Language::English => format!(
795 "new-name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
796 ),
797 Language::Portuguese => format!(
798 "novo nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'"
799 ),
800 };
801 assert!(
802 msg.contains("novo nome deve estar em kebab-case"),
803 "obtido: {msg}"
804 );
805 }
806
807 #[test]
808 fn reserved_name_en() {
809 let msg = match Language::English {
810 Language::English => {
811 "names and namespaces starting with __ are reserved for internal use"
812 .to_string()
813 }
814 Language::Portuguese => {
815 "nomes e namespaces iniciados com __ são reservados para uso interno"
816 .to_string()
817 }
818 };
819 assert!(msg.contains("reserved for internal use"), "obtido: {msg}");
820 }
821
822 #[test]
823 fn reserved_name_pt() {
824 let msg = match Language::Portuguese {
825 Language::English => {
826 "names and namespaces starting with __ are reserved for internal use"
827 .to_string()
828 }
829 Language::Portuguese => {
830 "nomes e namespaces iniciados com __ são reservados para uso interno"
831 .to_string()
832 }
833 };
834 assert!(msg.contains("reservados para uso interno"), "obtido: {msg}");
835 }
836 }
837
838 mod app_error_pt_translation_tests {
839 use crate::errors::AppError;
840
841 #[test]
842 fn localized_message_pt_not_found_fully_translated() {
843 let err =
844 AppError::NotFound("memory 'test-mem' not found in namespace 'global'".into());
845 let pt = err.localized_message_for(crate::i18n::Language::Portuguese);
846 assert!(
847 pt.contains("memória"),
848 "PT must translate 'memory' to 'memória': {pt}"
849 );
850 assert!(
851 pt.contains("não encontrada no namespace"),
852 "PT must translate full phrase: {pt}"
853 );
854 assert!(
855 !pt.contains("not found in namespace"),
856 "PT must not contain English phrase: {pt}"
857 );
858 }
859
860 #[test]
861 fn localized_message_pt_duplicate_fully_translated() {
862 let err = AppError::Duplicate(
863 "memory 'x' already exists in namespace 'global'. Use --force-merge to update."
864 .into(),
865 );
866 let pt = err.localized_message_for(crate::i18n::Language::Portuguese);
867 assert!(pt.contains("memória"), "PT must translate 'memory': {pt}");
868 assert!(
869 pt.contains("já existe no namespace"),
870 "PT must translate 'already exists': {pt}"
871 );
872 }
873 }
874}