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") {
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!(
45 value = %v,
46 "SQLITE_GRAPHRAG_LANG value not recognized, falling back to locale detection"
47 );
48 }
49 for var in &["LC_ALL", "LANG"] {
50 if let Ok(v) = std::env::var(var) {
51 if v.to_lowercase().starts_with("pt") {
52 return Language::Portuguese;
53 }
54 }
55 }
56 Language::English
57 }
58}
59
60static GLOBAL_LANGUAGE: OnceLock<Language> = OnceLock::new();
61
62pub fn init(explicit: Option<Language>) {
65 let resolved = explicit.unwrap_or_else(Language::from_env_or_locale);
66 let _ = GLOBAL_LANGUAGE.set(resolved);
67}
68
69pub fn current() -> Language {
71 *GLOBAL_LANGUAGE.get_or_init(Language::from_env_or_locale)
72}
73
74pub fn tr(en: &str, pt: &str) -> &'static str {
76 match current() {
81 Language::English => Box::leak(en.to_string().into_boxed_str()),
82 Language::Portuguese => Box::leak(pt.to_string().into_boxed_str()),
83 }
84}
85
86pub fn error_prefix() -> &'static str {
88 match current() {
89 Language::English => "Error",
90 Language::Portuguese => "Erro",
91 }
92}
93
94pub mod errors_msg {
96 use super::current;
97 use crate::i18n::Language;
98
99 pub fn memory_not_found(nome: &str, namespace: &str) -> String {
100 match current() {
101 Language::English => {
102 format!("memory '{nome}' not found in namespace '{namespace}'")
103 }
104 Language::Portuguese => {
105 format!("memória '{nome}' não encontrada no namespace '{namespace}'")
106 }
107 }
108 }
109
110 pub fn database_not_found(path: &str) -> String {
111 match current() {
112 Language::English => {
113 format!("database not found at {path}. Run 'sqlite-graphrag init' first.")
114 }
115 Language::Portuguese => format!(
116 "banco de dados não encontrado em {path}. Execute 'sqlite-graphrag init' primeiro."
117 ),
118 }
119 }
120
121 pub fn entity_not_found(nome: &str, namespace: &str) -> String {
122 match current() {
123 Language::English => {
124 format!("entity \"{nome}\" does not exist in namespace \"{namespace}\"")
125 }
126 Language::Portuguese => {
127 format!("entidade \"{nome}\" não existe no namespace \"{namespace}\"")
128 }
129 }
130 }
131
132 pub fn relationship_not_found(de: &str, rel: &str, para: &str, namespace: &str) -> String {
133 match current() {
134 Language::English => format!(
135 "relationship \"{de}\" --[{rel}]--> \"{para}\" does not exist in namespace \"{namespace}\""
136 ),
137 Language::Portuguese => format!(
138 "relacionamento \"{de}\" --[{rel}]--> \"{para}\" não existe no namespace \"{namespace}\""
139 ),
140 }
141 }
142
143 pub fn duplicate_memory(nome: &str, namespace: &str) -> String {
144 match current() {
145 Language::English => format!(
146 "memory '{nome}' already exists in namespace '{namespace}'. Use --force-merge to update."
147 ),
148 Language::Portuguese => format!(
149 "memória '{nome}' já existe no namespace '{namespace}'. Use --force-merge para atualizar."
150 ),
151 }
152 }
153
154 pub fn optimistic_lock_conflict(expected: i64, current_ts: i64) -> String {
155 match current() {
156 Language::English => format!(
157 "optimistic lock conflict: expected updated_at={expected}, but current is {current_ts}"
158 ),
159 Language::Portuguese => format!(
160 "conflito de optimistic lock: esperava updated_at={expected}, mas atual é {current_ts}"
161 ),
162 }
163 }
164
165 pub fn version_not_found(versao: i64, nome: &str) -> String {
166 match current() {
167 Language::English => format!("version {versao} not found for memory '{nome}'"),
168 Language::Portuguese => {
169 format!("versão {versao} não encontrada para a memória '{nome}'")
170 }
171 }
172 }
173
174 pub fn no_recall_results(max_distance: f32, query: &str, namespace: &str) -> String {
175 match current() {
176 Language::English => format!(
177 "no results within --max-distance {max_distance} for query '{query}' in namespace '{namespace}'"
178 ),
179 Language::Portuguese => format!(
180 "nenhum resultado dentro de --max-distance {max_distance} para a consulta '{query}' no namespace '{namespace}'"
181 ),
182 }
183 }
184
185 pub fn soft_deleted_memory_not_found(nome: &str, namespace: &str) -> String {
186 match current() {
187 Language::English => {
188 format!("soft-deleted memory '{nome}' not found in namespace '{namespace}'")
189 }
190 Language::Portuguese => {
191 format!("memória soft-deleted '{nome}' não encontrada no namespace '{namespace}'")
192 }
193 }
194 }
195
196 pub fn concurrent_process_conflict() -> String {
197 match current() {
198 Language::English => {
199 "optimistic lock conflict: memory was modified by another process".to_string()
200 }
201 Language::Portuguese => {
202 "conflito de optimistic lock: memória foi modificada por outro processo".to_string()
203 }
204 }
205 }
206
207 pub fn entity_limit_exceeded(max: usize) -> String {
208 match current() {
209 Language::English => format!("entities exceed limit of {max}"),
210 Language::Portuguese => format!("entidades excedem o limite de {max}"),
211 }
212 }
213
214 pub fn relationship_limit_exceeded(max: usize) -> String {
215 match current() {
216 Language::English => format!("relationships exceed limit of {max}"),
217 Language::Portuguese => format!("relacionamentos excedem o limite de {max}"),
218 }
219 }
220}
221
222pub mod validation {
224 use super::current;
225 use crate::i18n::Language;
226
227 pub fn name_length(max: usize) -> String {
228 match current() {
229 Language::English => format!("name must be 1-{max} chars"),
230 Language::Portuguese => format!("nome deve ter entre 1 e {max} caracteres"),
231 }
232 }
233
234 pub fn reserved_name() -> String {
235 match current() {
236 Language::English => {
237 "names and namespaces starting with __ are reserved for internal use".to_string()
238 }
239 Language::Portuguese => {
240 "nomes e namespaces iniciados com __ são reservados para uso interno".to_string()
241 }
242 }
243 }
244
245 pub fn name_kebab(nome: &str) -> String {
246 match current() {
247 Language::English => format!(
248 "name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
249 ),
250 Language::Portuguese => {
251 format!("nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'")
252 }
253 }
254 }
255
256 pub fn description_exceeds(max: usize) -> String {
257 match current() {
258 Language::English => format!("description must be <= {max} chars"),
259 Language::Portuguese => format!("descrição deve ter no máximo {max} caracteres"),
260 }
261 }
262
263 pub fn body_exceeds(max: usize) -> String {
264 match current() {
265 Language::English => format!("body exceeds {max} bytes"),
266 Language::Portuguese => format!("corpo excede {max} bytes"),
267 }
268 }
269
270 pub fn new_name_length(max: usize) -> String {
271 match current() {
272 Language::English => format!("new-name must be 1-{max} chars"),
273 Language::Portuguese => format!("novo nome deve ter entre 1 e {max} caracteres"),
274 }
275 }
276
277 pub fn new_name_kebab(nome: &str) -> String {
278 match current() {
279 Language::English => format!(
280 "new-name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
281 ),
282 Language::Portuguese => format!(
283 "novo nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'"
284 ),
285 }
286 }
287
288 pub fn namespace_length() -> String {
289 match current() {
290 Language::English => "namespace must be 1-80 chars".to_string(),
291 Language::Portuguese => "namespace deve ter entre 1 e 80 caracteres".to_string(),
292 }
293 }
294
295 pub fn namespace_format() -> String {
296 match current() {
297 Language::English => "namespace must be alphanumeric + hyphens/underscores".to_string(),
298 Language::Portuguese => {
299 "namespace deve ser alfanumérico com hífens/sublinhados".to_string()
300 }
301 }
302 }
303
304 pub fn path_traversal(p: &str) -> String {
305 match current() {
306 Language::English => format!("path traversal rejected: {p}"),
307 Language::Portuguese => format!("traversal de caminho rejeitado: {p}"),
308 }
309 }
310
311 pub fn invalid_tz(v: &str) -> String {
312 match current() {
313 Language::English => format!(
314 "SQLITE_GRAPHRAG_DISPLAY_TZ invalid: '{v}'; use an IANA name like 'America/Sao_Paulo'"
315 ),
316 Language::Portuguese => format!(
317 "SQLITE_GRAPHRAG_DISPLAY_TZ inválido: '{v}'; use um nome IANA como 'America/Sao_Paulo'"
318 ),
319 }
320 }
321
322 pub fn empty_query() -> String {
323 match current() {
324 Language::English => "query cannot be empty".to_string(),
325 Language::Portuguese => "a consulta não pode estar vazia".to_string(),
326 }
327 }
328
329 pub fn empty_body() -> String {
330 match current() {
331 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(),
332 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(),
333 }
334 }
335
336 pub fn invalid_namespace_config(path: &str, err: &str) -> String {
337 match current() {
338 Language::English => {
339 format!("invalid project namespace config '{path}': {err}")
340 }
341 Language::Portuguese => {
342 format!("configuração de namespace de projeto inválida '{path}': {err}")
343 }
344 }
345 }
346
347 pub fn invalid_projects_mapping(path: &str, err: &str) -> String {
348 match current() {
349 Language::English => format!("invalid projects mapping '{path}': {err}"),
350 Language::Portuguese => format!("mapeamento de projetos inválido '{path}': {err}"),
351 }
352 }
353
354 pub fn self_referential_link() -> String {
355 match current() {
356 Language::English => "--from and --to must be different entities — self-referential relationships are not supported".to_string(),
357 Language::Portuguese => "--from e --to devem ser entidades diferentes — relacionamentos auto-referenciais não são suportados".to_string(),
358 }
359 }
360
361 pub fn invalid_link_weight(weight: f64) -> String {
362 match current() {
363 Language::English => {
364 format!("--weight: must be between 0.0 and 1.0 (actual: {weight})")
365 }
366 Language::Portuguese => {
367 format!("--weight: deve estar entre 0.0 e 1.0 (atual: {weight})")
368 }
369 }
370 }
371
372 pub fn sync_destination_equals_source() -> String {
373 match current() {
374 Language::English => {
375 "destination path must differ from the source database path".to_string()
376 }
377 Language::Portuguese => {
378 "caminho de destino deve ser diferente do caminho do banco de dados fonte"
379 .to_string()
380 }
381 }
382 }
383
384 pub mod app_error_pt {
390 pub fn validation(msg: &str) -> String {
391 format!("erro de validação: {msg}")
392 }
393
394 pub fn duplicate(msg: &str) -> String {
395 format!("duplicata detectada: {msg}")
396 }
397
398 pub fn conflict(msg: &str) -> String {
399 format!("conflito: {msg}")
400 }
401
402 pub fn not_found(msg: &str) -> String {
403 format!("não encontrado: {msg}")
404 }
405
406 pub fn namespace_error(msg: &str) -> String {
407 format!("namespace não resolvido: {msg}")
408 }
409
410 pub fn limit_exceeded(msg: &str) -> String {
411 format!("limite excedido: {msg}")
412 }
413
414 pub fn database(err: &str) -> String {
415 format!("erro de banco de dados: {err}")
416 }
417
418 pub fn embedding(msg: &str) -> String {
419 format!("erro de embedding: {msg}")
420 }
421
422 pub fn vec_extension(msg: &str) -> String {
423 format!("extensão sqlite-vec falhou: {msg}")
424 }
425
426 pub fn db_busy(msg: &str) -> String {
427 format!("banco ocupado: {msg}")
428 }
429
430 pub fn batch_partial_failure(total: usize, failed: usize) -> String {
431 format!("falha parcial em batch: {failed} de {total} itens falharam")
432 }
433
434 pub fn io(err: &str) -> String {
435 format!("erro de I/O: {err}")
436 }
437
438 pub fn internal(err: &str) -> String {
439 format!("erro interno: {err}")
440 }
441
442 pub fn json(err: &str) -> String {
443 format!("erro de JSON: {err}")
444 }
445
446 pub fn lock_busy(msg: &str) -> String {
447 format!("lock ocupado: {msg}")
448 }
449
450 pub fn all_slots_full(max: usize, waited_secs: u64) -> String {
451 format!(
452 "todos os {max} slots de concorrência ocupados após aguardar {waited_secs}s \
453 (exit 75); use --max-concurrency ou aguarde outras invocações terminarem"
454 )
455 }
456
457 pub fn low_memory(available_mb: u64, required_mb: u64) -> String {
458 format!(
459 "memória disponível ({available_mb}MB) abaixo do mínimo requerido ({required_mb}MB) \
460 para carregar o modelo; aborte outras cargas ou use --skip-memory-guard (exit 77)"
461 )
462 }
463 }
464
465 pub mod runtime_pt {
471 pub fn embedding_heavy_must_measure_ram() -> String {
472 "comando intensivo em embedding precisa medir RAM disponível".to_string()
473 }
474
475 pub fn heavy_command_detected(available_mb: u64, safe_concurrency: usize) -> String {
476 format!(
477 "Comando pesado detectado; memória disponível: {available_mb} MB; \
478 concorrência segura: {safe_concurrency}"
479 )
480 }
481
482 pub fn reducing_concurrency(
483 requested_concurrency: usize,
484 effective_concurrency: usize,
485 ) -> String {
486 format!(
487 "Reduzindo a concorrência solicitada de {requested_concurrency} para \
488 {effective_concurrency} para evitar oversubscription de memória"
489 )
490 }
491
492 pub fn downloading_ner_model() -> &'static str {
493 "Baixando modelo NER (primeira execução, ~676 MB)..."
494 }
495
496 pub fn initializing_embedding_model() -> &'static str {
497 "Inicializando modelo de embedding (pode baixar na primeira execução)..."
498 }
499
500 pub fn embedding_chunks_serially(count: usize) -> String {
501 format!("Embedando {count} chunks serialmente para manter memória limitada...")
502 }
503
504 pub fn remember_step_input_validated(available_mb: u64) -> String {
505 format!("Etapa remember: entrada validada; memória disponível {available_mb} MB")
506 }
507
508 pub fn remember_step_chunking_completed(
509 total_passage_tokens: usize,
510 model_max_length: usize,
511 chunks_count: usize,
512 rss_mb: u64,
513 ) -> String {
514 format!(
515 "Etapa remember: tokenizer contou {total_passage_tokens} tokens de passagem \
516 (máximo do modelo {model_max_length}); chunking gerou {chunks_count} chunks; \
517 RSS do processo {rss_mb} MB"
518 )
519 }
520
521 pub fn remember_step_embeddings_completed(rss_mb: u64) -> String {
522 format!("Etapa remember: embeddings dos chunks concluídos; RSS do processo {rss_mb} MB")
523 }
524
525 pub fn restore_recomputing_embedding() -> &'static str {
526 "Recalculando embedding da memória restaurada..."
527 }
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use serial_test::serial;
535
536 #[test]
537 #[serial]
538 fn fallback_english_when_env_absent() {
539 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
540 std::env::set_var("LC_ALL", "C");
541 std::env::set_var("LANG", "C");
542 assert_eq!(Language::from_env_or_locale(), Language::English);
543 std::env::remove_var("LC_ALL");
544 std::env::remove_var("LANG");
545 }
546
547 #[test]
548 #[serial]
549 fn env_pt_selects_portuguese() {
550 std::env::remove_var("LC_ALL");
551 std::env::remove_var("LANG");
552 std::env::set_var("SQLITE_GRAPHRAG_LANG", "pt");
553 assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
554 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
555 }
556
557 #[test]
558 #[serial]
559 fn env_pt_br_selects_portuguese() {
560 std::env::remove_var("LC_ALL");
561 std::env::remove_var("LANG");
562 std::env::set_var("SQLITE_GRAPHRAG_LANG", "pt-BR");
563 assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
564 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
565 }
566
567 #[test]
568 #[serial]
569 fn locale_ptbr_utf8_selects_portuguese() {
570 std::env::remove_var("SQLITE_GRAPHRAG_LANG");
571 std::env::set_var("LC_ALL", "pt_BR.UTF-8");
572 assert_eq!(Language::from_env_or_locale(), Language::Portuguese);
573 std::env::remove_var("LC_ALL");
574 }
575
576 mod validation_tests {
577 use super::*;
578
579 #[test]
580 fn name_length_en() {
581 let msg = match Language::English {
582 Language::English => format!("name must be 1-{} chars", 80),
583 Language::Portuguese => format!("nome deve ter entre 1 e {} caracteres", 80),
584 };
585 assert!(msg.contains("name must be 1-80 chars"), "obtido: {msg}");
586 }
587
588 #[test]
589 fn name_length_pt() {
590 let msg = match Language::Portuguese {
591 Language::English => format!("name must be 1-{} chars", 80),
592 Language::Portuguese => format!("nome deve ter entre 1 e {} caracteres", 80),
593 };
594 assert!(
595 msg.contains("nome deve ter entre 1 e 80 caracteres"),
596 "obtido: {msg}"
597 );
598 }
599
600 #[test]
601 fn name_kebab_en() {
602 let nome = "Invalid_Name";
603 let msg = match Language::English {
604 Language::English => format!(
605 "name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
606 ),
607 Language::Portuguese => {
608 format!("nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'")
609 }
610 };
611 assert!(msg.contains("kebab-case slug"), "obtido: {msg}");
612 assert!(msg.contains("Invalid_Name"), "obtido: {msg}");
613 }
614
615 #[test]
616 fn name_kebab_pt() {
617 let nome = "Invalid_Name";
618 let msg = match Language::Portuguese {
619 Language::English => format!(
620 "name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
621 ),
622 Language::Portuguese => {
623 format!("nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'")
624 }
625 };
626 assert!(msg.contains("kebab-case"), "obtido: {msg}");
627 assert!(msg.contains("minúsculas"), "obtido: {msg}");
628 assert!(msg.contains("Invalid_Name"), "obtido: {msg}");
629 }
630
631 #[test]
632 fn description_exceeds_en() {
633 let msg = match Language::English {
634 Language::English => format!("description must be <= {} chars", 500),
635 Language::Portuguese => format!("descrição deve ter no máximo {} caracteres", 500),
636 };
637 assert!(msg.contains("description must be <= 500"), "obtido: {msg}");
638 }
639
640 #[test]
641 fn description_exceeds_pt() {
642 let msg = match Language::Portuguese {
643 Language::English => format!("description must be <= {} chars", 500),
644 Language::Portuguese => format!("descrição deve ter no máximo {} caracteres", 500),
645 };
646 assert!(
647 msg.contains("descrição deve ter no máximo 500"),
648 "obtido: {msg}"
649 );
650 }
651
652 #[test]
653 fn body_exceeds_en() {
654 let limite = crate::constants::MAX_MEMORY_BODY_LEN;
655 let msg = match Language::English {
656 Language::English => format!("body exceeds {limite} bytes"),
657 Language::Portuguese => format!("corpo excede {limite} bytes"),
658 };
659 assert!(msg.contains("body exceeds 512000"), "obtido: {msg}");
660 }
661
662 #[test]
663 fn body_exceeds_pt() {
664 let limite = crate::constants::MAX_MEMORY_BODY_LEN;
665 let msg = match Language::Portuguese {
666 Language::English => format!("body exceeds {limite} bytes"),
667 Language::Portuguese => format!("corpo excede {limite} bytes"),
668 };
669 assert!(msg.contains("corpo excede 512000"), "obtido: {msg}");
670 }
671
672 #[test]
673 fn new_name_length_en() {
674 let msg = match Language::English {
675 Language::English => format!("new-name must be 1-{} chars", 80),
676 Language::Portuguese => format!("novo nome deve ter entre 1 e {} caracteres", 80),
677 };
678 assert!(msg.contains("new-name must be 1-80"), "obtido: {msg}");
679 }
680
681 #[test]
682 fn new_name_length_pt() {
683 let msg = match Language::Portuguese {
684 Language::English => format!("new-name must be 1-{} chars", 80),
685 Language::Portuguese => format!("novo nome deve ter entre 1 e {} caracteres", 80),
686 };
687 assert!(
688 msg.contains("novo nome deve ter entre 1 e 80"),
689 "obtido: {msg}"
690 );
691 }
692
693 #[test]
694 fn new_name_kebab_en() {
695 let nome = "Bad Name";
696 let msg = match Language::English {
697 Language::English => format!(
698 "new-name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
699 ),
700 Language::Portuguese => format!(
701 "novo nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'"
702 ),
703 };
704 assert!(msg.contains("new-name must be kebab-case"), "obtido: {msg}");
705 }
706
707 #[test]
708 fn new_name_kebab_pt() {
709 let nome = "Bad Name";
710 let msg = match Language::Portuguese {
711 Language::English => format!(
712 "new-name must be kebab-case slug (lowercase letters, digits, hyphens): '{nome}'"
713 ),
714 Language::Portuguese => format!(
715 "novo nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{nome}'"
716 ),
717 };
718 assert!(
719 msg.contains("novo nome deve estar em kebab-case"),
720 "obtido: {msg}"
721 );
722 }
723
724 #[test]
725 fn reserved_name_en() {
726 let msg = match Language::English {
727 Language::English => {
728 "names and namespaces starting with __ are reserved for internal use"
729 .to_string()
730 }
731 Language::Portuguese => {
732 "nomes e namespaces iniciados com __ são reservados para uso interno"
733 .to_string()
734 }
735 };
736 assert!(msg.contains("reserved for internal use"), "obtido: {msg}");
737 }
738
739 #[test]
740 fn reserved_name_pt() {
741 let msg = match Language::Portuguese {
742 Language::English => {
743 "names and namespaces starting with __ are reserved for internal use"
744 .to_string()
745 }
746 Language::Portuguese => {
747 "nomes e namespaces iniciados com __ são reservados para uso interno"
748 .to_string()
749 }
750 };
751 assert!(msg.contains("reservados para uso interno"), "obtido: {msg}");
752 }
753 }
754}