Skip to main content

sqlite_graphrag/i18n/validation/
messages_cli.rs

1//! Messages about CLI ARGUMENTS and flag combinations (GAP-SG-146).
2//!
3//! Missing, conflicting or malformed flags, and the explicit confirmations a
4//! destructive subcommand refuses to run without.
5
6use crate::i18n::{current, Language};
7
8/// A concurrency ceiling of zero was handed to the LLM slot acquisition.
9///
10/// GAP-SG-262: this message lived as a Portuguese string literal inside
11/// `llm_slots::acquire_llm_slot`, so it was the one refusal on that path that
12/// an English-speaking caller received in Portuguese. It never reached
13/// `refusal_message_call_site_gate` either, because that gate walks THIS module
14/// and the message was not in it.
15///
16/// Zero is rejected rather than treated as unlimited: a zero ceiling would make
17/// every slot comparison vacuous and authorise unbounded concurrency, which is
18/// the opposite of what the caller asked for by naming a limit at all.
19pub fn llm_slot_ceiling_must_be_positive() -> String {
20    match current() {
21        Language::English => {
22            "max_concurrent must be >= 1 for acquire_llm_slot: a ceiling of 0 would \
23             authorise unbounded concurrency rather than none"
24                .to_string()
25        }
26        Language::Portuguese => "max_concurrent deve ser >= 1 para acquire_llm_slot: um teto de 0 \
27             autorizaria concorrência ilimitada em vez de nenhuma"
28            .to_string(),
29    }
30}
31
32/// Invalid JSON for a CLI flag that takes a file path (e.g. `--entities-file`).
33pub fn invalid_json_in_flag(flag: &str, err: &impl std::fmt::Display) -> String {
34    match current() {
35        Language::English => format!("invalid JSON in {flag}: {err}"),
36        Language::Portuguese => format!("JSON inválido em {flag}: {err}"),
37    }
38}
39
40/// Invalid JSON payload on a stdin flag (e.g. `--graph-stdin`).
41pub fn invalid_json_payload_on_flag(flag: &str, err: &impl std::fmt::Display) -> String {
42    match current() {
43        Language::English => format!("invalid JSON payload on {flag}: {err}"),
44        Language::Portuguese => format!("payload JSON inválido em {flag}: {err}"),
45    }
46}
47
48/// Invalid pending-embeddings status filter.
49pub fn invalid_status_filter(other: &str) -> String {
50    match current() {
51        Language::English => {
52            format!("invalid status filter: {other} (expected pending|in_progress|done|abandoned)")
53        }
54        Language::Portuguese => format!(
55            "filtro de status inválido: {other} (esperado pending|in_progress|done|abandoned)"
56        ),
57    }
58}
59
60/// Localized message for `invalid_tz`.
61pub fn invalid_tz(v: &str) -> String {
62    match current() {
63        Language::English => {
64            format!("display.tz invalid: '{v}'; use an IANA name like 'America/Sao_Paulo'")
65        }
66        Language::Portuguese => {
67            format!("display.tz inválido: '{v}'; use um nome IANA como 'America/Sao_Paulo'")
68        }
69    }
70}
71
72/// `--enable-ner` and `--skip-extraction` conflict.
73pub fn enable_ner_skip_extraction_exclusive() -> String {
74    match current() {
75        Language::English => {
76            "--enable-ner and --skip-extraction are mutually exclusive; remove one".to_string()
77        }
78        Language::Portuguese => {
79            "--enable-ner e --skip-extraction são mutuamente exclusivos; remova um".to_string()
80        }
81    }
82}
83
84/// --entity required when --memory is used.
85pub fn entity_required_when_memory() -> String {
86    match current() {
87        Language::English => "--entity is required when --memory is used".to_string(),
88        Language::Portuguese => "--entity é obrigatório quando --memory é usado".to_string(),
89    }
90}
91
92/// --from required when not using --entity/--all.
93pub fn from_required_without_entity_all() -> String {
94    match current() {
95        Language::English => "--from is required when --entity/--all is not used".to_string(),
96        Language::Portuguese => {
97            "--from é obrigatório quando --entity/--all não é usado".to_string()
98        }
99    }
100}
101
102/// Batch reclassify missing --from-type.
103pub fn from_type_required_batch() -> String {
104    match current() {
105        Language::English => "--from-type is required in batch mode".to_string(),
106        Language::Portuguese => "--from-type é obrigatório no modo batch".to_string(),
107    }
108}
109
110/// --to required when not using --entity/--all.
111pub fn to_required_without_entity_all() -> String {
112    match current() {
113        Language::English => "--to is required when --entity/--all is not used".to_string(),
114        Language::Portuguese => "--to é obrigatório quando --entity/--all não é usado".to_string(),
115    }
116}
117
118/// Batch reclassify missing --to-type.
119pub fn to_type_required_batch() -> String {
120    match current() {
121        Language::English => "--to-type is required in batch mode".to_string(),
122        Language::Portuguese => "--to-type é obrigatório no modo batch".to_string(),
123    }
124}
125
126/// Memory/entity name required as positional or --name.
127pub fn name_required_positional_or_flag() -> String {
128    match current() {
129        Language::English => "name required: pass as positional argument or via --name".to_string(),
130        Language::Portuguese => {
131            "nome obrigatório: passe como argumento posicional ou via --name".to_string()
132        }
133    }
134}
135
136/// `split-body`: neither a name nor `--batch` was supplied.
137///
138/// Distinct from [`name_required_positional_or_flag`] because this verb has a
139/// second legitimate way to be complete — operating on every oversized memory —
140/// and a message naming only the name would read as though batch mode did not
141/// exist. GAP-SG-272 added the positional spelling, so the text names all three
142/// ways in rather than the one flag it used to hardcode in English only.
143pub fn split_body_needs_name_or_batch() -> String {
144    match current() {
145        Language::English => "name required: pass it as a positional argument or via --name, \
146             or use --batch to split every oversized memory"
147            .to_string(),
148        Language::Portuguese => {
149            "nome obrigatório: passe como argumento posicional ou via --name, ou use \
150             --batch para dividir toda memória acima do teto"
151                .to_string()
152        }
153    }
154}
155
156/// Neither a name nor `--id` was supplied to a verb that accepts both.
157///
158/// One function for two call sites — `read` and `rename-entity` — which each
159/// carried their own English literal, and had already drifted: one listed the
160/// positional spelling and the other did not, for the same choice.
161pub fn name_or_id_required() -> String {
162    match current() {
163        Language::English => "name or --id required: pass the name as a positional \
164             argument, via --name, or identify the row with --id"
165            .to_string(),
166        Language::Portuguese => {
167            "nome ou --id obrigatório: passe o nome como argumento posicional, via \
168             --name, ou identifique a linha com --id"
169                .to_string()
170        }
171    }
172}
173
174/// `reclassify` single mode: neither `--new-type` nor `--description` was given.
175///
176/// Previously an English literal inline in the handler, which made the refusal
177/// untranslatable in a product that ships every other message in two languages.
178pub fn reclassify_needs_type_or_description() -> String {
179    match current() {
180        Language::English => {
181            "at least one of --new-type or --description is required in single mode".to_string()
182        }
183        Language::Portuguese => {
184            "pelo menos um entre --new-type e --description é obrigatório no modo single"
185                .to_string()
186        }
187    }
188}
189
190/// Single-mode missing name.
191///
192/// GAP-SG-272: the text names the positional spelling too, because the verbs that
193/// read this message now accept both. A refusal that names only the flag sends the
194/// operator to `--help` to discover the form they could have used.
195pub fn name_required_single_mode() -> String {
196    match current() {
197        Language::English => {
198            "name required in single mode: pass it as a positional argument or via --name"
199                .to_string()
200        }
201        Language::Portuguese => {
202            "nome obrigatório no modo single: passe como argumento posicional ou via --name"
203                .to_string()
204        }
205    }
206}
207
208/// Single-mode missing --target.
209pub fn target_required_single_mode() -> String {
210    match current() {
211        Language::English => "--target is required in single mode".to_string(),
212        Language::Portuguese => "--target é obrigatório no modo single".to_string(),
213    }
214}
215
216/// CREATE path requires `--type` and `--description`.
217pub fn type_and_description_required() -> String {
218    match current() {
219        Language::English => {
220            "--type and --description are required when creating a new memory".to_string()
221        }
222        Language::Portuguese => {
223            "--type e --description são obrigatórios ao criar uma nova memória".to_string()
224        }
225    }
226}
227
228/// `--target` only applies to re-embed operation.
229pub fn reembed_target_only(target: &str) -> String {
230    match current() {
231        Language::English => {
232            format!("--target {target} only applies to --operation re-embed")
233        }
234        Language::Portuguese => {
235            format!("--target {target} só se aplica a --operation re-embed")
236        }
237    }
238}
239
240/// Refusing orphan entity delete without --yes.
241/// Counts rows, not entities: the caller sums orphan entities, dangling
242/// relationships and every other foreign key violation in the file, and a
243/// message that named only one of the three would understate what is about to
244/// be deleted.
245pub fn refuse_delete_orphans_without_yes(orphan_count: usize) -> String {
246    match current() {
247        Language::English => format!(
248            "refusing to delete {orphan_count} orphaned rows without --yes (use --dry-run to preview)"
249        ),
250        Language::Portuguese => format!(
251            "recusando excluir {orphan_count} linhas órfãs sem --yes (use --dry-run para pré-visualizar)"
252        ),
253    }
254}
255
256/// Refusing vec orphan delete without --yes.
257pub fn refuse_delete_vec_orphans_without_yes(
258    orphan_count: i64,
259    orphan_entities_count: i64,
260    orphan_chunks_count: i64,
261) -> String {
262    match current() {
263        Language::English => format!(
264            "refusing to delete {orphan_count} memory embedding + {orphan_entities_count} vec_entities + {orphan_chunks_count} vec_chunks orphan rows without --yes (use --dry-run to preview)"
265        ),
266        Language::Portuguese => format!(
267            "recusando excluir {orphan_count} embeddings de memória + {orphan_entities_count} vec_entities + {orphan_chunks_count} vec_chunks órfãos sem --yes (use --dry-run para pré-visualizar)"
268        ),
269    }
270}
271
272/// Refusing to release slot without --yes.
273pub fn refuse_release_slot_without_yes(slot_id: &str, path: &str) -> String {
274    match current() {
275        Language::English => {
276            format!("refusing to release slot {slot_id} without --yes (file: {path})")
277        }
278        Language::Portuguese => {
279            format!("recusando liberar slot {slot_id} sem --yes (arquivo: {path})")
280        }
281    }
282}
283
284/// Self-referential merge by id (generic).
285pub fn self_merge_id(id: i64, target_id: i64) -> String {
286    match current() {
287        Language::English => format!(
288            "source entity id={id} equals target id={target_id} — \
289             self-referential merge is not allowed"
290        ),
291        Language::Portuguese => format!(
292            "entidade fonte id={id} é igual ao alvo id={target_id} — \
293             merge auto-referencial não é permitido"
294        ),
295    }
296}
297
298/// Self-referential merge by id (pre-check with --ids hint).
299pub fn self_merge_id_in_ids(id: i64) -> String {
300    match current() {
301        Language::English => format!(
302            "source entity id={id} equals target id={id} — \
303             self-referential merge is not allowed (remove target from --ids)"
304        ),
305        Language::Portuguese => format!(
306            "entidade fonte id={id} é igual ao alvo id={id} — \
307             merge auto-referencial não é permitido (remova o alvo de --ids)"
308        ),
309    }
310}
311
312/// Self-referential merge by name (generic).
313pub fn self_merge_name(name: &str, target_name: &str) -> String {
314    match current() {
315        Language::English => format!(
316            "source entity '{name}' equals target '{target_name}' — \
317             self-referential merge is not allowed"
318        ),
319        Language::Portuguese => format!(
320            "entidade fonte '{name}' é igual ao alvo '{target_name}' — \
321             merge auto-referencial não é permitido"
322        ),
323    }
324}
325
326/// Self-referential merge by name (pre-check with --names hint).
327pub fn self_merge_name_in_names(name: &str) -> String {
328    match current() {
329        Language::English => format!(
330            "source entity '{name}' equals target '{name}' — \
331             self-referential merge is not allowed (remove target from --names)"
332        ),
333        Language::Portuguese => format!(
334            "entidade fonte '{name}' é igual ao alvo '{name}' — \
335             merge auto-referencial não é permitido (remova o alvo de --names)"
336        ),
337    }
338}
339
340/// Source name resolves to target id (self-merge).
341pub fn self_merge_name_resolves_to_target(name: &str, target_id: i64) -> String {
342    match current() {
343        Language::English => format!(
344            "source entity '{name}' resolves to the target (id={target_id}) — \
345             self-referential merge is not allowed"
346        ),
347        Language::Portuguese => format!(
348            "entidade fonte '{name}' resolve para o alvo (id={target_id}) — \
349             merge auto-referencial não é permitido"
350        ),
351    }
352}
353
354/// Source and target entity names identical.
355pub fn source_target_entity_names_identical() -> String {
356    match current() {
357        Language::English => "source and target entity names are identical".to_string(),
358        Language::Portuguese => "nomes de entidade de origem e destino são idênticos".to_string(),
359    }
360}
361
362/// Source and target names identical.
363pub fn source_target_names_identical() -> String {
364    match current() {
365        Language::English => "source and target names are identical".to_string(),
366        Language::Portuguese => "nomes de origem e destino são idênticos".to_string(),
367    }
368}
369
370/// GAP-SG-142: `--filter` expression has an empty key on the left-hand side.
371pub fn agent_surface_filter_empty_key(expr: &str) -> String {
372    match current() {
373        Language::English => {
374            format!("invalid --filter expression '{expr}': the key must not be empty")
375        }
376        Language::Portuguese => {
377            format!("expressão --filter inválida '{expr}': a chave não pode ser vazia")
378        }
379    }
380}
381
382/// GAP-SG-142: `--filter` expression carries no recognised operator.
383pub fn agent_surface_filter_invalid(expr: &str) -> String {
384    match current() {
385        Language::English => format!(
386            "invalid --filter expression '{expr}': expected key=value, key!=value or key~substring"
387        ),
388        Language::Portuguese => format!(
389            "expressão --filter inválida '{expr}': esperado chave=valor, chave!=valor ou chave~substring"
390        ),
391    }
392}
393
394/// Unknown pending_embeddings status string.
395pub fn unknown_pending_embeddings_status(other: &str) -> String {
396    match current() {
397        Language::English => format!("unknown pending_embeddings status: {other}"),
398        Language::Portuguese => {
399            format!("status de pending_embeddings desconhecido: {other}")
400        }
401    }
402}
403
404/// Generic "missing / required field" helper.
405pub fn missing_field(field: &str) -> String {
406    match current() {
407        Language::English => format!("missing required field: {field}"),
408        Language::Portuguese => format!("campo obrigatório ausente: {field}"),
409    }
410}
411
412/// Sub-queries file has no usable lines.
413pub fn sub_queries_file_empty(path: &str) -> String {
414    match current() {
415        Language::English => format!("sub-queries file '{path}' has no usable lines"),
416        Language::Portuguese => {
417            format!("arquivo de sub-consultas '{path}' não tem linhas utilizáveis")
418        }
419    }
420}
421
422/// Failed to read names file.
423pub fn failed_to_read_names_file(path: &str, err: &impl std::fmt::Display) -> String {
424    match current() {
425        Language::English => format!("failed to read names file {path}: {err}"),
426        Language::Portuguese => format!("falha ao ler arquivo de nomes {path}: {err}"),
427    }
428}
429
430/// Localized message for `sync_destination_equals_source`.
431pub fn sync_destination_equals_source() -> String {
432    match current() {
433        Language::English => {
434            "destination path must differ from the source database path".to_string()
435        }
436        Language::Portuguese => {
437            "caminho de destino deve ser diferente do caminho do banco de dados fonte".to_string()
438        }
439    }
440}
441
442/// `schema --name <ID>` was given an id that is not shipped in `docs/schemas/`.
443///
444/// `suggestion` carries the nearest known id when one is close enough; the
445/// message stays terse when nothing is, because an unrelated pointer is worse
446/// than none.
447pub fn unknown_schema_id(id: &str, suggestion: Option<&str>) -> String {
448    match (current(), suggestion) {
449        (Language::English, Some(near)) => {
450            format!("unknown schema id '{id}'; did you mean '{near}'? Run `sqlite-graphrag schema` to list every id")
451        }
452        (Language::English, None) => {
453            format!("unknown schema id '{id}'; run `sqlite-graphrag schema` to list every id")
454        }
455        (Language::Portuguese, Some(near)) => {
456            format!("id de schema desconhecido '{id}'; você quis dizer '{near}'? Execute `sqlite-graphrag schema` para listar todos os ids")
457        }
458        (Language::Portuguese, None) => {
459            format!("id de schema desconhecido '{id}'; execute `sqlite-graphrag schema` para listar todos os ids")
460        }
461    }
462}
463
464/// A stdin-reading path was requested while `--no-input` was in force.
465///
466/// Reported before the read is attempted, so the guarantee holds even when a
467/// pipe is present and would have supplied data.
468pub fn no_input_blocks_stdin() -> String {
469    match current() {
470        Language::English => "--no-input is in force: this invocation refuses to read stdin; \
471             supply the content through a file or inline flag \
472             (for example --body / --body-file), or drop --no-input"
473            .to_string(),
474        Language::Portuguese => "--no-input está em vigor: esta invocação recusa ler stdin; \
475             forneça o conteúdo por arquivo ou flag inline \
476             (por exemplo --body / --body-file), ou remova --no-input"
477            .to_string(),
478    }
479}