Skip to main content

sqlite_graphrag/i18n/validation/
messages_naming.rs

1//! Messages about NAMES, namespaces and filesystem paths (GAP-SG-146).
2//!
3//! The kebab-case contract for memory and entity names, namespace shape, and
4//! the path guards that keep writes inside their intended root.
5
6use crate::i18n::{current, Language};
7
8/// Entity type label that is empty once trimmed.
9///
10/// The four `entity_type_*` refusals below guard SHAPE only. v1.2.8 opened the
11/// vocabulary, so an unknown label is no longer an error — these fire on labels
12/// that could not be a word in any vocabulary, which is the only bound left
13/// after V017 dropped the SQL `CHECK`.
14pub fn entity_type_blank() -> String {
15    match current() {
16        Language::English => {
17            "entity type must not be empty; omit the field to accept the default `concept`"
18                .to_string()
19        }
20        Language::Portuguese => {
21            "tipo de entidade não pode ser vazio; omita o campo para aceitar o padrão `concept`"
22                .to_string()
23        }
24    }
25}
26
27/// Entity type label containing a line break.
28pub fn entity_type_has_newline(value: &str) -> String {
29    let shown = value.replace('\n', "\\n").replace('\r', "\\r");
30    match current() {
31        Language::English => {
32            format!("entity type must not contain a line break: '{shown}'")
33        }
34        Language::Portuguese => {
35            format!("tipo de entidade não pode conter quebra de linha: '{shown}'")
36        }
37    }
38}
39
40/// Entity type label made only of digits.
41pub fn entity_type_digits_only(value: &str) -> String {
42    match current() {
43        Language::English => {
44            format!("entity type must not be digits only: '{value}'")
45        }
46        Language::Portuguese => {
47            format!("tipo de entidade não pode ser apenas dígitos: '{value}'")
48        }
49    }
50}
51
52/// Entity type label longer than the allowed character count.
53pub fn entity_type_too_long(value: &str, max: usize) -> String {
54    let len = value.chars().count();
55    match current() {
56        Language::English => {
57            format!("entity type must be at most {max} characters, got {len}: '{value}'")
58        }
59        Language::Portuguese => {
60            format!(
61                "tipo de entidade deve ter no máximo {max} caracteres, recebeu {len}: '{value}'"
62            )
63        }
64    }
65}
66
67/// Localized message for `name_length`.
68pub fn name_length(max: usize) -> String {
69    match current() {
70        Language::English => format!("name must be 1-{max} chars"),
71        Language::Portuguese => format!("nome deve ter entre 1 e {max} caracteres"),
72    }
73}
74
75/// Localized message for `name_kebab`.
76pub fn name_kebab(name: &str) -> String {
77    match current() {
78        Language::English => {
79            format!("name must be kebab-case slug (lowercase letters, digits, hyphens): '{name}'")
80        }
81        Language::Portuguese => {
82            format!("nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{name}'")
83        }
84    }
85}
86
87/// Name normalized to empty (only hyphens/underscores/spaces).
88pub fn name_empty_after_normalization() -> String {
89    match current() {
90        Language::English => {
91            "name cannot be empty after normalization (input was blank or contained only hyphens/underscores/spaces)"
92                .to_string()
93        }
94        Language::Portuguese => {
95            "nome não pode ficar vazio após normalização (entrada em branco ou só com hífens/sublinhados/espaços)"
96                .to_string()
97        }
98    }
99}
100
101/// Empty name rejected.
102pub fn name_must_not_be_empty() -> String {
103    match current() {
104        Language::English => "name must not be empty".to_string(),
105        Language::Portuguese => "nome não pode ser vazio".to_string(),
106    }
107}
108
109/// `--name-prefix` must be kebab-case starting with a letter.
110pub fn name_prefix_kebab(prefix: &str) -> String {
111    match current() {
112        Language::English => format!(
113            "--name-prefix '{prefix}' must start with a lowercase letter and contain              only lowercase letters, digits and hyphens (kebab-case)"
114        ),
115        Language::Portuguese => format!(
116            "--name-prefix '{prefix}' deve começar com letra minúscula e conter              apenas letras minúsculas, dígitos e hífens (kebab-case)"
117        ),
118    }
119}
120
121/// Localized message for `reserved_name`.
122pub fn reserved_name() -> String {
123    match current() {
124        Language::English => {
125            "names and namespaces starting with __ are reserved for internal use".to_string()
126        }
127        Language::Portuguese => {
128            "nomes e namespaces iniciados com __ são reservados para uso interno".to_string()
129        }
130    }
131}
132
133/// Localized message for `new_name_kebab`.
134pub fn new_name_kebab(name: &str) -> String {
135    match current() {
136        Language::English => format!(
137            "new-name must be kebab-case slug (lowercase letters, digits, hyphens): '{name}'"
138        ),
139        Language::Portuguese => {
140            format!("novo nome deve estar em kebab-case (minúsculas, dígitos, hífens): '{name}'")
141        }
142    }
143}
144
145/// Localized message for `new_name_length`.
146pub fn new_name_length(max: usize) -> String {
147    match current() {
148        Language::English => format!("new-name must be 1-{max} chars"),
149        Language::Portuguese => format!("novo nome deve ter entre 1 e {max} caracteres"),
150    }
151}
152
153/// `--strict-name` refused auto-normalization.
154pub fn strict_name_not_canonical(original: &str, normalized: &str) -> String {
155    match current() {
156        Language::English => format!(
157            "--strict-name is set but '{original}' is not canonical kebab-case; \
158             re-run with --name '{normalized}' (or drop --strict-name to allow auto-normalization)"
159        ),
160        Language::Portuguese => format!(
161            "--strict-name está ativo mas '{original}' não é kebab-case canônico; \
162             reexecute com --name '{normalized}' (ou remova --strict-name para permitir auto-normalização)"
163        ),
164    }
165}
166
167/// GAP-SG-216: `--strict-entity-types` refused a silent taxonomy rewrite.
168///
169/// Placed beside [`strict_name_not_canonical`] because it is the same refusal
170/// about the sibling field: one guards the name the caller typed, the other the
171/// type they typed. Both name the offending value, the value it would become,
172/// and the way out — a refusal that only states a verdict leaves the caller
173/// guessing which of the two contracts they broke.
174///
175/// `folds` arrives pre-phrased by
176/// `crate::commands::remember::graph_input::collect_entity_type_folds`, which
177/// already names the declared label, the canonical kind and the entity. Listing
178/// them verbatim keeps ONE phrasing of the fold instead of two that drift.
179pub fn strict_entity_type_folded(folds: &[String]) -> String {
180    let list = folds.join("; ");
181    let allowed = crate::entity_type::CANONICAL_ENTITY_TYPES.join(", ");
182    match current() {
183        Language::English => format!(
184            "--strict-entity-types is set and {} declared type(s) are outside the \
185             recommended vocabulary: {list}. The thirteen canonical kinds are {allowed}. \
186             Re-declare each entity with one of them, or drop --strict-entity-types to \
187             store the label as written (it is reported in the response `warnings` \
188             either way)",
189            folds.len()
190        ),
191        Language::Portuguese => format!(
192            "--strict-entity-types está ativo e {} tipo(s) declarado(s) estão fora do \
193             vocabulário recomendado: {list}. Os treze tipos canônicos são {allowed}. \
194             Redeclare cada entidade com um deles, ou remova --strict-entity-types para \
195             gravar o rótulo como foi escrito (que é reportado em `warnings` de todo \
196             modo)",
197            folds.len()
198        ),
199    }
200}
201
202/// Child name derived from parent exceeds MAX_MEMORY_NAME_LEN.
203pub fn child_name_exceeds_max(child: &str, parent: &str, max: usize) -> String {
204    match current() {
205        Language::English => format!(
206            "child name '{child}' derived from '{parent}' exceeds MAX_MEMORY_NAME_LEN ({max})"
207        ),
208        Language::Portuguese => format!(
209            "nome filho '{child}' derivado de '{parent}' excede MAX_MEMORY_NAME_LEN ({max})"
210        ),
211    }
212}
213
214/// Child name is not kebab-case ASCII.
215pub fn child_name_not_kebab(child: &str) -> String {
216    match current() {
217        Language::English => {
218            format!("child name '{child}' is not kebab-case ASCII; rename the parent memory")
219        }
220        Language::Portuguese => {
221            format!("nome filho '{child}' não é kebab-case ASCII; renomeie a memória pai")
222        }
223    }
224}
225
226/// Localized message for `namespace_format`.
227pub fn namespace_format() -> String {
228    match current() {
229        Language::English => "namespace must be alphanumeric + hyphens/underscores".to_string(),
230        Language::Portuguese => {
231            "namespace deve ser alfanumérico com hífens/sublinhados".to_string()
232        }
233    }
234}
235
236/// Localized message for `namespace_length`.
237pub fn namespace_length() -> String {
238    match current() {
239        Language::English => "namespace must be 1-80 chars".to_string(),
240        Language::Portuguese => "namespace deve ter entre 1 e 80 caracteres".to_string(),
241    }
242}
243
244/// Short ALL_CAPS entity name rejected as NER noise.
245pub fn entity_name_all_caps_noise(name: &str) -> String {
246    match current() {
247        Language::English => format!(
248            "entity name '{name}' rejected: short ALL_CAPS names are typically NER noise"
249        ),
250        Language::Portuguese => format!(
251            "nome de entidade '{name}' rejeitado: nomes curtos em CAIXA ALTA são tipicamente ruído de NER"
252        ),
253    }
254}
255
256/// Entity rename target already exists.
257pub fn entity_name_already_exists(name: &str, namespace: &str) -> String {
258    match current() {
259        Language::English => {
260            format!("entity with name '{name}' already exists in namespace '{namespace}'")
261        }
262        Language::Portuguese => {
263            format!("entidade com nome '{name}' já existe no namespace '{namespace}'")
264        }
265    }
266}
267
268/// Entity name normalizes too short.
269pub fn entity_name_normalizes_too_short(original: &str, normalized: &str) -> String {
270    match current() {
271        Language::English => format!(
272            "entity name '{original}' normalizes to '{normalized}' which is too short (minimum 2 characters)"
273        ),
274        Language::Portuguese => format!(
275            "nome de entidade '{original}' normaliza para '{normalized}' que é curto demais (mínimo 2 caracteres)"
276        ),
277    }
278}
279
280/// Purely numeric entity name rejected.
281pub fn entity_name_purely_numeric(name: &str) -> String {
282    match current() {
283        Language::English => format!(
284            "entity name '{name}' rejected: purely numeric names look like entity IDs — \
285             use --from-id/--to-id for ID-based linking, or pass a non-numeric name"
286        ),
287        Language::Portuguese => format!(
288            "nome de entidade '{name}' rejeitado: nomes puramente numéricos parecem IDs — \
289             use --from-id/--to-id para link por ID, ou passe um nome não numérico"
290        ),
291    }
292}
293
294/// Entity name too short.
295pub fn entity_name_too_short(name: &str) -> String {
296    match current() {
297        Language::English => format!("entity name '{name}' must be at least 2 characters"),
298        Language::Portuguese => {
299            format!("nome de entidade '{name}' deve ter pelo menos 2 caracteres")
300        }
301    }
302}
303
304/// Too many name collisions while generating a unique memory name.
305pub fn too_many_name_collisions(base: &str, max: usize) -> String {
306    match current() {
307        Language::English => format!(
308            "too many name collisions for base '{base}' (>{max}); rename source files to disambiguate"
309        ),
310        Language::Portuguese => format!(
311            "muitas colisões de nome para a base '{base}' (>{max}); renomeie os arquivos de origem para desambiguar"
312        ),
313    }
314}
315
316/// Path has no valid parent component.
317pub fn path_no_valid_parent(path: &str) -> String {
318    match current() {
319        Language::English => format!("path '{path}' has no valid parent component"),
320        Language::Portuguese => format!("caminho '{path}' não tem componente pai válido"),
321    }
322}
323
324/// Localized message for `path_traversal`.
325pub fn path_traversal(p: &str) -> String {
326    match current() {
327        Language::English => format!("path traversal rejected: {p}"),
328        Language::Portuguese => format!("traversal de caminho rejeitado: {p}"),
329    }
330}
331
332/// Directory path does not exist.
333pub fn directory_not_found(path: &str) -> String {
334    match current() {
335        Language::English => format!("directory not found: {path}"),
336        Language::Portuguese => format!("diretório não encontrado: {path}"),
337    }
338}
339
340/// Path exists but is not a directory.
341pub fn not_a_directory(path: &str) -> String {
342    match current() {
343        Language::English => format!("path is not a directory: {path}"),
344        Language::Portuguese => format!("caminho não é um diretório: {path}"),
345    }
346}