Skip to main content

sqlite_graphrag/i18n/validation/
messages_embedding.rs

1//! Embedding error message catalog (GAP-SG-132).
2//!
3//! EN+PT user-facing strings for `AppError::Embedding(String)`.
4//!
5//! `EmbeddingErrorKind::classify` matches English substrings case-insensitively:
6//! `oauth`, `quota`, `slot exhausted`, `backend mismatch`, and (`dim` + `zero`).
7//! Portuguese templates MUST retain those English markers so classification
8//! continues to work under either locale.
9
10use crate::i18n::{current, Language};
11use std::fmt::Display;
12
13// ── generic wrapper ──────────────────────────────────────────────────────────
14
15/// Wrap an external / dynamic error detail as an embedding payload.
16pub fn embedding_detail(err: impl Display) -> String {
17    match current() {
18        Language::English => format!("{err}"),
19        Language::Portuguese => format!("{err}"),
20    }
21}
22
23// ── OpenRouter HTTP / client ─────────────────────────────────────────────────
24
25/// Failed to build the reqwest HTTP client for OpenRouter embeddings.
26pub fn embedding_http_client_build_failed(err: impl Display) -> String {
27    match current() {
28        Language::English => format!("failed to build HTTP client: {err}"),
29        Language::Portuguese => format!("falha ao construir cliente HTTP: {err}"),
30    }
31}
32
33/// Empty `data` array in a successful OpenRouter embedding response.
34pub fn embedding_empty_response_from_openrouter() -> String {
35    match current() {
36        Language::English => "empty response from OpenRouter".to_string(),
37        Language::Portuguese => "resposta vazia do OpenRouter".to_string(),
38    }
39}
40
41/// Batch response length mismatch.
42pub fn embedding_expected_count(expected: usize, got: usize) -> String {
43    match current() {
44        Language::English => format!("expected {expected} embeddings, got {got}"),
45        Language::Portuguese => format!("esperados {expected} embeddings, obtidos {got}"),
46    }
47}
48
49/// Provider returned fewer dimensions than requested.
50pub fn embedding_dimension_less_than_requested(got: usize, requested: usize) -> String {
51    match current() {
52        Language::English => format!("embedding dimension {got} < requested {requested}"),
53        Language::Portuguese => {
54            format!("dimensão de embedding {got} < solicitada {requested}")
55        }
56    }
57}
58
59/// OpenRouter embedding request timed out.
60pub fn embedding_openrouter_request_timed_out() -> String {
61    match current() {
62        Language::English => "OpenRouter request timed out".to_string(),
63        Language::Portuguese => "requisição OpenRouter expirou (timeout)".to_string(),
64    }
65}
66
67/// Transient network failure during embedding HTTP.
68pub fn embedding_http_request_failed(err: impl Display) -> String {
69    match current() {
70        Language::English => format!("HTTP request failed: {err}"),
71        Language::Portuguese => format!("requisição HTTP falhou: {err}"),
72    }
73}
74
75/// Failed to read embedding response body.
76pub fn embedding_failed_to_read_response_body(err: impl Display) -> String {
77    match current() {
78        Language::English => format!("failed to read response body: {err}"),
79        Language::Portuguese => format!("falha ao ler corpo da resposta: {err}"),
80    }
81}
82
83/// HTTP 200 body had neither `data` nor `error`.
84pub fn embedding_openrouter_200_neither_data_nor_error() -> String {
85    match current() {
86        Language::English => {
87            "OpenRouter 200 response had neither data nor error".to_string()
88        }
89        Language::Portuguese => {
90            "resposta OpenRouter 200 sem data nem error".to_string()
91        }
92    }
93}
94
95/// Failed to parse embedding JSON body.
96pub fn embedding_failed_to_parse_response(err: impl Display) -> String {
97    match current() {
98        Language::English => format!("failed to parse embedding response: {err}"),
99        Language::Portuguese => format!("falha ao analisar resposta de embedding: {err}"),
100    }
101}
102
103/// Invalid OpenRouter API key (HTTP 401).
104pub fn embedding_openrouter_invalid_api_key_401() -> String {
105    match current() {
106        Language::English => "invalid OpenRouter API key (HTTP 401)".to_string(),
107        Language::Portuguese => "chave de API OpenRouter inválida (HTTP 401)".to_string(),
108    }
109}
110
111/// OpenRouter hard-failure status with body.
112pub fn embedding_openrouter_returned(status: impl Display, body: &str) -> String {
113    match current() {
114        Language::English => format!("OpenRouter returned {status}: {body}"),
115        Language::Portuguese => format!("OpenRouter retornou {status}: {body}"),
116    }
117}
118
119/// OpenRouter 5xx during embedding.
120pub fn embedding_openrouter_server_error(status: impl Display) -> String {
121    match current() {
122        Language::English => format!("OpenRouter server error: {status}"),
123        Language::Portuguese => format!("erro de servidor OpenRouter: {status}"),
124    }
125}
126
127/// Unexpected HTTP status with body snippet.
128pub fn embedding_unexpected_http(status: impl Display, body: &str) -> String {
129    match current() {
130        Language::English => format!("unexpected HTTP {status}: {body}"),
131        Language::Portuguese => format!("HTTP inesperado {status}: {body}"),
132    }
133}
134
135/// Max retries exhausted for OpenRouter embedding request.
136pub fn embedding_openrouter_max_retries() -> String {
137    match current() {
138        Language::English => {
139            "max retries exceeded for OpenRouter request".to_string()
140        }
141        Language::Portuguese => {
142            "número máximo de tentativas excedido para requisição OpenRouter".to_string()
143        }
144    }
145}
146
147// ── runtime / singleton getters ──────────────────────────────────────────────
148
149/// Tokio multi-thread runtime failed to initialise.
150pub fn embedding_tokio_runtime_init_failed(err: impl Display) -> String {
151    match current() {
152        Language::English => format!("tokio runtime init failed: {err}"),
153        Language::Portuguese => format!("falha na inicialização do runtime tokio: {err}"),
154    }
155}
156
157/// Tokio runtime missing after set.
158pub fn embedding_tokio_runtime_unavailable() -> String {
159    match current() {
160        Language::English => {
161            "tokio runtime unavailable after initialisation".to_string()
162        }
163        Language::Portuguese => {
164            "runtime tokio indisponível após inicialização".to_string()
165        }
166    }
167}
168
169/// Default embedder singleton missing after set.
170pub fn embedding_embedder_unavailable() -> String {
171    match current() {
172        Language::English => {
173            "embedder unavailable after initialisation".to_string()
174        }
175        Language::Portuguese => {
176            "embedder indisponível após inicialização".to_string()
177        }
178    }
179}
180
181/// Claude embedder singleton missing after set.
182pub fn embedding_claude_embedder_unavailable() -> String {
183    match current() {
184        Language::English => {
185            "claude embedder unavailable after initialisation".to_string()
186        }
187        Language::Portuguese => {
188            "embedder claude indisponível após inicialização".to_string()
189        }
190    }
191}
192
193/// OpenCode embedder singleton missing after set.
194pub fn embedding_opencode_embedder_unavailable() -> String {
195    match current() {
196        Language::English => {
197            "opencode embedder unavailable after initialisation".to_string()
198        }
199        Language::Portuguese => {
200            "embedder opencode indisponível após inicialização".to_string()
201        }
202    }
203}
204
205/// OpenRouter embed client singleton missing after set.
206pub fn embedding_openrouter_client_unavailable() -> String {
207    match current() {
208        Language::English => {
209            "openrouter client unavailable after initialisation".to_string()
210        }
211        Language::Portuguese => {
212            "cliente openrouter indisponível após inicialização".to_string()
213        }
214    }
215}
216
217/// OpenRouter chat client singleton missing after set.
218pub fn embedding_openrouter_chat_client_unavailable() -> String {
219    match current() {
220        Language::English => {
221            "openrouter chat client unavailable after initialisation".to_string()
222        }
223        Language::Portuguese => {
224            "cliente de chat openrouter indisponível após inicialização".to_string()
225        }
226    }
227}
228
229// ── backend probes ───────────────────────────────────────────────────────────
230
231/// OpenRouter probe: client not initialised.
232pub fn embedding_openrouter_probe_not_initialised() -> String {
233    match current() {
234        Language::English => {
235            "openrouter probe: client not initialised (skip)".to_string()
236        }
237        Language::Portuguese => {
238            "probe openrouter: cliente não inicializado (skip)".to_string()
239        }
240    }
241}
242
243/// Codex probe: binary missing from PATH.
244pub fn embedding_codex_probe_binary_not_on_path() -> String {
245    match current() {
246        Language::English => "codex probe: binary not on PATH (skip)".to_string(),
247        Language::Portuguese => "probe codex: binário não está no PATH (skip)".to_string(),
248    }
249}
250
251/// Codex probe: auth.json missing.
252pub fn embedding_codex_probe_auth_missing() -> String {
253    match current() {
254        Language::English => {
255            "codex probe: auth.json missing (skip; use --llm-backend none or login)"
256                .to_string()
257        }
258        Language::Portuguese => {
259            "probe codex: auth.json ausente (skip; use --llm-backend none ou login)"
260                .to_string()
261        }
262    }
263}
264
265/// Claude probe: binary missing from PATH.
266pub fn embedding_claude_probe_binary_not_on_path() -> String {
267    match current() {
268        Language::English => "claude probe: binary not on PATH (skip)".to_string(),
269        Language::Portuguese => "probe claude: binário não está no PATH (skip)".to_string(),
270    }
271}
272
273/// OpenCode probe: binary missing from PATH.
274pub fn embedding_opencode_probe_binary_not_on_path() -> String {
275    match current() {
276        Language::English => "opencode probe: binary not on PATH (skip)".to_string(),
277        Language::Portuguese => {
278            "probe opencode: binário não está no PATH (skip)".to_string()
279        }
280    }
281}
282
283/// OpenRouter client not initialised for embed path.
284pub fn embedding_openrouter_client_not_initialised() -> String {
285    match current() {
286        Language::English => {
287            "OpenRouter client not initialised; call get_openrouter_embedder first"
288                .to_string()
289        }
290        Language::Portuguese => {
291            "cliente OpenRouter não inicializado; chame get_openrouter_embedder primeiro"
292                .to_string()
293        }
294    }
295}
296
297// ── dimension / validation ───────────────────────────────────────────────────
298
299/// Vector length diverges from configured embedding dim (G42/C5).
300pub fn embedding_has_dims_expected(got: usize, expected: usize) -> String {
301    match current() {
302        Language::English => format!(
303            "embedding has {got} dims, expected {expected}; \
304             refusing to truncate or pad silently (G42/C5)"
305        ),
306        Language::Portuguese => format!(
307            "embedding tem {got} dims, esperado {expected}; \
308             recusando truncar ou preencher silenciosamente (G42/C5)"
309        ),
310    }
311}
312
313/// LLM returned wrong dim for a specific fan-out item.
314pub fn embedding_llm_item_dims(got: usize, idx: usize, expected: usize) -> String {
315    match current() {
316        Language::English => format!(
317            "LLM returned {got} dims for item {idx}, expected {expected}; \
318             refusing to truncate or pad silently (G42/C5)"
319        ),
320        Language::Portuguese => format!(
321            "LLM retornou {got} dims para item {idx}, esperado {expected}; \
322             recusando truncar ou preencher silenciosamente (G42/C5)"
323        ),
324    }
325}
326
327/// KNN search dim mismatch (memories / entities).
328pub fn embedding_knn_search_dim_mismatch(got: usize, expected: usize) -> String {
329    match current() {
330        Language::English => {
331            format!("knn_search embedding has {got} dims, expected {expected}")
332        }
333        Language::Portuguese => {
334            format!("embedding knn_search tem {got} dims, esperado {expected}")
335        }
336    }
337}
338
339/// KNN search dim mismatch (chunks).
340pub fn embedding_knn_search_chunks_dim_mismatch(got: usize, expected: usize) -> String {
341    match current() {
342        Language::English => {
343            format!("knn_search_chunks embedding has {got} dims, expected {expected}")
344        }
345        Language::Portuguese => {
346            format!("embedding knn_search_chunks tem {got} dims, esperado {expected}")
347        }
348    }
349}
350
351// ── slot / fan-out / batch ───────────────────────────────────────────────────
352
353/// LLM slot semaphore exhausted (marker: `slot exhausted`).
354pub fn embedding_slot_exhausted(err: impl Display) -> String {
355    match current() {
356        Language::English => format!("slot exhausted: {err} (fall back to FTS5)"),
357        // Keep English marker "slot exhausted" for EmbeddingErrorKind::classify.
358        Language::Portuguese => {
359            format!("slot exhausted: {err} (fall back para FTS5)")
360        }
361    }
362}
363
364/// JoinSet join error while embedding.
365pub fn embedding_task_join_error(err: impl Display) -> String {
366    match current() {
367        Language::English => format!("embedding task join error: {err}"),
368        Language::Portuguese => format!("erro de join na tarefa de embedding: {err}"),
369    }
370}
371
372/// Entity embed cache produced a null slot.
373pub fn embedding_entity_cache_null() -> String {
374    match current() {
375        Language::English => {
376            "entity embed cache produced null result".to_string()
377        }
378        Language::Portuguese => {
379            "cache de embed de entidade produziu resultado nulo".to_string()
380        }
381    }
382}
383
384/// Fan-out lost an item index.
385pub fn embedding_fanout_lost_index(idx: usize) -> String {
386    match current() {
387        Language::English => format!("embedding fan-out lost item index {idx}"),
388        Language::Portuguese => {
389            format!("fan-out de embedding perdeu índice do item {idx}")
390        }
391    }
392}
393
394/// Semaphore closed mid fan-out.
395pub fn embedding_semaphore_closed() -> String {
396    match current() {
397        Language::English => "semaphore closed".to_string(),
398        Language::Portuguese => "semáforo fechado".to_string(),
399    }
400}
401
402/// Embedding cancelled by shutdown signal (keep "cancelled" for batch matcher).
403pub fn embedding_cancelled_by_shutdown() -> String {
404    match current() {
405        Language::English => {
406            "embedding cancelled by shutdown signal".to_string()
407        }
408        // Keep English "cancelled" so batch collector still matches.
409        Language::Portuguese => {
410            "embedding cancelled por sinal de shutdown".to_string()
411        }
412    }
413}
414
415/// Embedding JoinSet task panicked.
416pub fn embedding_task_panicked(err: impl Display) -> String {
417    match current() {
418        Language::English => format!("embedding task panicked: {err}"),
419        Language::Portuguese => format!("tarefa de embedding entrou em pânico: {err}"),
420    }
421}
422
423// ── LLM subprocess embedding ─────────────────────────────────────────────────
424
425/// No codex/claude/opencode on PATH.
426pub fn embedding_no_llm_cli_on_path() -> String {
427    match current() {
428        Language::English => {
429            "no LLM CLI found on PATH: install `codex` (0.130+), `claude` (Claude Code 2.1+), or `opencode` (1.17+)"
430                .to_string()
431        }
432        Language::Portuguese => {
433            "nenhum CLI de LLM encontrado no PATH: instale `codex` (0.130+), `claude` (Claude Code 2.1+) ou `opencode` (1.17+)"
434                .to_string()
435        }
436    }
437}
438
439/// Named binary not found on PATH.
440pub fn embedding_binary_not_found_on_path(which_name: &str) -> String {
441    match current() {
442        Language::English => format!("`{which_name}` not found on PATH"),
443        Language::Portuguese => format!("`{which_name}` não encontrado no PATH"),
444    }
445}
446
447/// LLM batch embedding JSON parse failed.
448pub fn embedding_llm_batch_parse_failed(err: impl Display, raw: &str) -> String {
449    match current() {
450        Language::English => {
451            format!("LLM batch embedding response parse failed: {err}; raw={raw}")
452        }
453        Language::Portuguese => {
454            format!("falha ao analisar resposta de embedding em lote LLM: {err}; raw={raw}")
455        }
456    }
457}
458
459/// LLM batch returned wrong item count.
460pub fn embedding_llm_batch_item_count(got: usize, expected: usize) -> String {
461    match current() {
462        Language::English => format!(
463            "LLM batch returned {got} items, expected {expected} (G42/S2 coverage check)"
464        ),
465        Language::Portuguese => format!(
466            "lote LLM retornou {got} itens, esperados {expected} (verificação de cobertura G42/S2)"
467        ),
468    }
469}
470
471/// LLM batch item index out of range.
472pub fn embedding_llm_batch_index_out_of_range(i: usize, max: usize) -> String {
473    match current() {
474        Language::English => {
475            format!("LLM batch item index {i} out of range 1..={max}")
476        }
477        Language::Portuguese => {
478            format!("índice de item do lote LLM {i} fora do intervalo 1..={max}")
479        }
480    }
481}
482
483/// LLM batch item dimension mismatch (G42/C5).
484pub fn embedding_llm_batch_item_dims(i: usize, got: usize, expected: usize) -> String {
485    match current() {
486        Language::English => format!(
487            "LLM batch item {i} returned {got} dims, expected {expected}; \
488             refusing to truncate or pad silently (G42/C5)"
489        ),
490        Language::Portuguese => format!(
491            "item {i} do lote LLM retornou {got} dims, esperado {expected}; \
492             recusando truncar ou preencher silenciosamente (G42/C5)"
493        ),
494    }
495}
496
497/// LLM batch response missing an item index.
498pub fn embedding_llm_batch_missing_item(index: usize) -> String {
499    match current() {
500        Language::English => format!(
501            "LLM batch response is missing item index {index} (G42/S2 coverage check)"
502        ),
503        Language::Portuguese => format!(
504            "resposta do lote LLM sem índice de item {index} (verificação de cobertura G42/S2)"
505        ),
506    }
507}
508
509/// Single LLM embedding JSON parse failed.
510pub fn embedding_llm_parse_failed(err: impl Display, raw: &str) -> String {
511    match current() {
512        Language::English => {
513            format!("LLM embedding response parse failed: {err}; raw={raw}")
514        }
515        Language::Portuguese => {
516            format!("falha ao analisar resposta de embedding LLM: {err}; raw={raw}")
517        }
518    }
519}
520
521/// Single LLM embedding dimension mismatch (G42/C5).
522pub fn embedding_llm_returned_dims(got: usize, expected: usize) -> String {
523    match current() {
524        Language::English => format!(
525            "LLM returned {got} dims, expected {expected}; \
526             refusing to truncate or pad silently (G42/C5)"
527        ),
528        Language::Portuguese => format!(
529            "LLM retornou {got} dims, esperado {expected}; \
530             recusando truncar ou preencher silenciosamente (G42/C5)"
531        ),
532    }
533}
534
535/// Schema tempfile create failed.
536pub fn embedding_schema_tempfile_create_failed(err: impl Display) -> String {
537    match current() {
538        Language::English => format!("schema tempfile create failed: {err}"),
539        Language::Portuguese => {
540            format!("falha ao criar tempfile de schema: {err}")
541        }
542    }
543}
544
545/// Schema tempfile write failed.
546pub fn embedding_schema_tempfile_write_failed(err: impl Display) -> String {
547    match current() {
548        Language::English => format!("schema tempfile write failed: {err}"),
549        Language::Portuguese => {
550            format!("falha ao escrever tempfile de schema: {err}")
551        }
552    }
553}
554
555/// Claude OAuth usage quota exhausted (markers: `OAuth`, `quota`).
556pub fn embedding_oauth_usage_quota_exhausted_claude(snippet: &str) -> String {
557    match current() {
558        // Keep English "OAuth" + "quota" markers for EmbeddingErrorKind::classify.
559        Language::English => format!(
560            "OAuth usage quota exhausted: claude rate_limit detected in stdout: {snippet}"
561        ),
562        Language::Portuguese => format!(
563            "OAuth usage quota exhausted: rate_limit do claude detectado no stdout: {snippet}"
564        ),
565    }
566}
567
568/// Codex stdin write failed.
569pub fn embedding_codex_stdin_write_failed(err: impl Display) -> String {
570    match current() {
571        Language::English => format!("codex stdin write failed: {err}"),
572        Language::Portuguese => format!("falha ao escrever no stdin do codex: {err}"),
573    }
574}
575
576// ── dry-run backend guards ───────────────────────────────────────────────────
577
578/// `--llm-backend codex` but codex missing (claude detected).
579pub fn embedding_dry_run_codex_not_on_path() -> String {
580    match current() {
581        Language::English => {
582            "`--llm-backend codex` requested but `codex` was not found on PATH \
583             (a `claude` binary was detected; refusing silent fallback per ADR-0042). \
584             Install `codex` (>= 0.130) or pass `--llm-backend claude` explicitly."
585                .to_string()
586        }
587        Language::Portuguese => {
588            "`--llm-backend codex` solicitado mas `codex` não encontrado no PATH \
589             (binário `claude` detectado; recusando fallback silencioso por ADR-0042). \
590             Instale `codex` (>= 0.130) ou passe `--llm-backend claude` explicitamente."
591                .to_string()
592        }
593    }
594}
595
596/// `--llm-backend claude` but claude missing (codex detected).
597pub fn embedding_dry_run_claude_not_on_path() -> String {
598    match current() {
599        Language::English => {
600            "`--llm-backend claude` requested but `claude` was not found on PATH \
601             (a `codex` binary was detected; refusing silent fallback per ADR-0042). \
602             Install `claude` (Claude Code >= 2.1) or pass `--llm-backend codex` explicitly."
603                .to_string()
604        }
605        Language::Portuguese => {
606            "`--llm-backend claude` solicitado mas `claude` não encontrado no PATH \
607             (binário `codex` detectado; recusando fallback silencioso por ADR-0042). \
608             Instale `claude` (Claude Code >= 2.1) ou passe `--llm-backend codex` explicitamente."
609                .to_string()
610        }
611    }
612}
613
614/// `--llm-backend opencode` but auto-detect resolved another flavour.
615pub fn embedding_dry_run_opencode_resolved_other(flavour: &str) -> String {
616    match current() {
617        Language::English => format!(
618            "`--llm-backend opencode` requested but auto-detect resolved `{flavour}` \
619             (opencode has lower priority than codex/claude in detect_available). \
620             Pass `--llm-backend auto`, `--opencode-binary <path>`, or \
621             `config set llm.opencode_binary <path>`."
622        ),
623        Language::Portuguese => format!(
624            "`--llm-backend opencode` solicitado mas auto-detect resolveu `{flavour}` \
625             (opencode tem prioridade menor que codex/claude em detect_available). \
626             Passe `--llm-backend auto`, `--opencode-binary <path>`, ou \
627             `config set llm.opencode_binary <path>`."
628        ),
629    }
630}
631
632/// `--llm-backend opencode` but opencode not on PATH.
633pub fn embedding_dry_run_opencode_not_on_path() -> String {
634    match current() {
635        Language::English => {
636            "`--llm-backend opencode` requested but `opencode` was not found on PATH. \
637             Install `opencode` (>= 1.17) or pass `--llm-backend auto` to auto-detect."
638                .to_string()
639        }
640        Language::Portuguese => {
641            "`--llm-backend opencode` solicitado mas `opencode` não encontrado no PATH. \
642             Instale `opencode` (>= 1.17) ou passe `--llm-backend auto` para auto-detect."
643                .to_string()
644        }
645    }
646}
647
648// ── opencode runner ──────────────────────────────────────────────────────────
649
650/// OpenCode NDJSON had no text events.
651pub fn embedding_opencode_no_text_events() -> String {
652    match current() {
653        Language::English => {
654            "opencode returned no text events in NDJSON output".to_string()
655        }
656        Language::Portuguese => {
657            "opencode não retornou eventos de texto na saída NDJSON".to_string()
658        }
659    }
660}
661
662/// OpenCode timed out.
663pub fn embedding_opencode_timed_out(timeout_secs: u64) -> String {
664    match current() {
665        Language::English => format!("opencode timed out after {timeout_secs}s"),
666        Language::Portuguese => {
667            format!("opencode expirou (timeout) após {timeout_secs}s")
668        }
669    }
670}
671
672/// Failed to spawn opencode.
673pub fn embedding_failed_to_spawn_opencode(err: impl Display) -> String {
674    match current() {
675        Language::English => format!("failed to spawn opencode: {err}"),
676        Language::Portuguese => format!("falha ao iniciar opencode: {err}"),
677    }
678}
679
680/// OpenCode non-zero exit with tails.
681pub fn embedding_opencode_exited(
682    status: impl Display,
683    stderr: &str,
684    stdout: &str,
685) -> String {
686    match current() {
687        Language::English => format!(
688            "opencode exited with {status}: stderr={stderr}, stdout={stdout}"
689        ),
690        Language::Portuguese => format!(
691            "opencode saiu com {status}: stderr={stderr}, stdout={stdout}"
692        ),
693    }
694}
695
696/// OpenCode JSON parse failed.
697pub fn embedding_opencode_json_parse_failed(err: impl Display) -> String {
698    match current() {
699        Language::English => format!("opencode JSON parse failed: {err}"),
700        Language::Portuguese => format!("falha ao analisar JSON do opencode: {err}"),
701    }
702}