@intlayer/lsp 8.11.4 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/esm/_virtual/_rolldown/runtime.mjs +36 -0
  2. package/dist/esm/callers/index.mjs +12 -0
  3. package/dist/esm/callers/index.mjs.map +1 -0
  4. package/dist/esm/callers/patterns.mjs +37 -0
  5. package/dist/esm/callers/patterns.mjs.map +1 -0
  6. package/dist/esm/dictionaryUtils.mjs +84 -0
  7. package/dist/esm/dictionaryUtils.mjs.map +1 -0
  8. package/dist/esm/findFieldInFile.mjs +22 -125
  9. package/dist/esm/findFieldInFile.mjs.map +1 -1
  10. package/dist/esm/findKeyAtOffset.mjs +20 -24
  11. package/dist/esm/findKeyAtOffset.mjs.map +1 -1
  12. package/dist/esm/findKeyInContentFile.mjs +259 -40
  13. package/dist/esm/findKeyInContentFile.mjs.map +1 -1
  14. package/dist/esm/findTCallAtOffset.mjs +33 -0
  15. package/dist/esm/findTCallAtOffset.mjs.map +1 -0
  16. package/dist/esm/findUsageFieldAtOffset.mjs +12 -51
  17. package/dist/esm/findUsageFieldAtOffset.mjs.map +1 -1
  18. package/dist/esm/index.mjs +343 -172
  19. package/dist/esm/index.mjs.map +1 -1
  20. package/dist/esm/oxcUtils.mjs +99 -29
  21. package/dist/esm/oxcUtils.mjs.map +1 -1
  22. package/dist/esm/usageAnalyzer.mjs +695 -0
  23. package/dist/esm/usageAnalyzer.mjs.map +1 -0
  24. package/dist/esm/utils.mjs +12 -1
  25. package/dist/types/callers/index.d.ts +5 -0
  26. package/dist/types/callers/index.d.ts.map +1 -0
  27. package/dist/types/callers/patterns.d.ts +30 -0
  28. package/dist/types/callers/patterns.d.ts.map +1 -0
  29. package/dist/types/dictionaryUtils.d.ts +11 -0
  30. package/dist/types/dictionaryUtils.d.ts.map +1 -0
  31. package/dist/types/findFieldInFile.d.ts +19 -22
  32. package/dist/types/findFieldInFile.d.ts.map +1 -1
  33. package/dist/types/findKeyAtOffset.d.ts +15 -12
  34. package/dist/types/findKeyAtOffset.d.ts.map +1 -1
  35. package/dist/types/findKeyInContentFile.d.ts +23 -21
  36. package/dist/types/findKeyInContentFile.d.ts.map +1 -1
  37. package/dist/types/findTCallAtOffset.d.ts +24 -0
  38. package/dist/types/findTCallAtOffset.d.ts.map +1 -0
  39. package/dist/types/findUsageFieldAtOffset.d.ts +14 -13
  40. package/dist/types/findUsageFieldAtOffset.d.ts.map +1 -1
  41. package/dist/types/index.d.ts +1 -1
  42. package/dist/types/oxcUtils.d.ts +44 -15
  43. package/dist/types/oxcUtils.d.ts.map +1 -1
  44. package/dist/types/usageAnalyzer.d.ts +141 -0
  45. package/dist/types/usageAnalyzer.d.ts.map +1 -0
  46. package/dist/types/utils.d.ts +15 -1
  47. package/package.json +11 -24
@@ -1,11 +1,14 @@
1
1
  import { parseText, walkAst } from "./oxcUtils.mjs";
2
2
 
3
3
  //#region src/findKeyInContentFile.ts
4
- const KEY_DECLARATION_REGULAR_EXPRESSION = /\bkey\s*:\s*(?:(['"`])([^'"`\r\n]+)\1|([^\s'"`{},\r\n]+))/g;
4
+ const KEY_DECLARATION_REGULAR_EXPRESSION = /(?:^|[{,\s])"?key"?\s*:\s*(?:(['"`])([^'"`\r\n]+)\1|([^ \t'"`{},\r\n]+))/g;
5
5
  /**
6
6
  * If `offset` falls within a `key:` declaration in a content file,
7
7
  * return the declared key string; otherwise return null.
8
- * Handles quoted values (TS/JS/JSON) and bare values (YAML/Markdown frontmatter).
8
+ * Handles:
9
+ * - Quoted values: key: "foo" | key: 'foo' | key: `foo` (TS/JS)
10
+ * - JSON values: "key": "foo" (JSON/JSON5/JSONC)
11
+ * - Bare values: key: foo-bar (YAML / Markdown frontmatter)
9
12
  */
10
13
  const findKeyInContentFile = (text, offset) => {
11
14
  for (const match of text.matchAll(KEY_DECLARATION_REGULAR_EXPRESSION)) {
@@ -15,11 +18,66 @@ const findKeyInContentFile = (text, offset) => {
15
18
  }
16
19
  return null;
17
20
  };
18
- /** Returns true when `node` is a call to the intlayer `t()` translation function. */
19
- const isTCall = (node) => {
21
+ /**
22
+ * If `offset` falls on a content field property key inside an intlayer content
23
+ * file, return the dictionary key, the leaf field name, and the full field path.
24
+ *
25
+ * The `fileExtension` parameter controls which parser is used:
26
+ *
27
+ * - `.json` / `.json5` / `.jsonc` → dedicated JSON parser
28
+ * - `.yaml` / `.yml` → YAML regex heuristic
29
+ * - `.md` / `.mdx` → always null (body is the content value)
30
+ * - everything else → Oxc AST (TS/JS/TSX/JSX)
31
+ *
32
+ * When `fileExtension` is omitted the function falls back to Oxc, then to
33
+ * JSON/YAML heuristics based on text shape.
34
+ */
35
+ const findContentFieldAtOffset = (text, offset, fileExtension) => {
36
+ const ext = fileExtension?.toLowerCase() ?? "";
37
+ if (ext === ".md" || ext === ".mdx") return null;
38
+ if (ext === ".json" || ext === ".json5" || ext === ".jsonc") return findContentFieldAtOffsetJson(text, offset);
39
+ if (ext === ".yaml" || ext === ".yml") return findContentFieldAtOffsetYaml(text, offset);
40
+ const program = parseText(text);
41
+ if (program) return findContentFieldAtOffsetOxc(text, offset, program);
42
+ if (/^\s*\{/.test(text)) return findContentFieldAtOffsetJson(text, offset);
43
+ if (/^---\s*\r?\n/.test(text)) {
44
+ if (!/^content\s*:/m.test(text)) return null;
45
+ }
46
+ return findContentFieldAtOffsetYaml(text, offset);
47
+ };
48
+ /**
49
+ * All Intlayer helper function names whose arguments are locale-map or
50
+ * control-flow data — NOT navigable content field names. The walker must
51
+ * treat these calls as opaque leaf values and never descend into them.
52
+ *
53
+ * `t` – translation ({ en: "…", fr: "…" })
54
+ * `enu` – enumeration ({ "0": "…", ">5": "…" })
55
+ * `cond` – condition ({ true: "…", false: "…" })
56
+ * `plural` – plural rules ({ one: "…", other: "…" })
57
+ * `gender` – gender rules ({ male: "…", female: "…", other: "…" })
58
+ * `insert` – insertion ("Hello {{name}}!")
59
+ * `nest` – nested dict ("navbar")
60
+ * `md` – markdown ("# …")
61
+ * `file` – file reference ("./path/to/file.txt")
62
+ * `html` – html content ("<p>…</p>")
63
+ */
64
+ const INTLAYER_LEAF_CALL_NAMES = /* @__PURE__ */ new Set([
65
+ "t",
66
+ "enu",
67
+ "cond",
68
+ "plural",
69
+ "gender",
70
+ "insert",
71
+ "nest",
72
+ "md",
73
+ "file",
74
+ "html"
75
+ ]);
76
+ /** Returns true when `node` is a call to any intlayer leaf helper function. */
77
+ const isIntlayerLeafCall = (node) => {
20
78
  if (node["type"] !== "CallExpression") return false;
21
79
  const callee = node["callee"];
22
- return callee?.["type"] === "Identifier" && callee["name"] === "t";
80
+ return callee?.["type"] === "Identifier" && INTLAYER_LEAF_CALL_NAMES.has(callee["name"]);
23
81
  };
24
82
  /** Extract a string value from a Literal / StringLiteral / TemplateLiteral node. */
25
83
  const getStringValue = (node) => {
@@ -31,48 +89,209 @@ const getStringValue = (node) => {
31
89
  return null;
32
90
  };
33
91
  /**
34
- * Fallback for non-JS/TS files (YAML, Markdown, JSON) or when Oxc cannot parse
35
- * the file. Uses the original regex approach, returning a single-element path.
92
+ * Well-known meta-keys that appear inside Intlayer typed nodes in JSON files.
93
+ * e.g. { "nodeType": "translation", "translation": { "en": "…" } }
94
+ * None of these are navigable content fields.
36
95
  */
37
- const findContentFieldAtOffsetRegex = (text, offset) => {
38
- const keyMatch = /\bkey\s*:\s*(?:(['"`])([^'"`\r\n]+)\1|([^\s'"`{},\r\n]+))/.exec(text);
39
- if (!keyMatch) return null;
40
- const dictionaryKey = keyMatch[2] ?? keyMatch[3];
96
+ const JSON_INTLAYER_NODE_KEYS = /* @__PURE__ */ new Set([
97
+ "nodeType",
98
+ "translation",
99
+ "enumeration",
100
+ "condition",
101
+ "plural",
102
+ "insertion",
103
+ "nested",
104
+ "markdown",
105
+ "html",
106
+ "file",
107
+ "composite",
108
+ "dictionaryKey",
109
+ "$schema",
110
+ "id",
111
+ "projectIds",
112
+ "localId",
113
+ "localIds",
114
+ "filePath",
115
+ "versions",
116
+ "filled",
117
+ "key",
118
+ "title",
119
+ "description",
120
+ "tags",
121
+ "format",
122
+ "locale",
123
+ "schema",
124
+ "location",
125
+ "fill",
126
+ "priority",
127
+ "version",
128
+ "importMode"
129
+ ]);
130
+ /**
131
+ * Find the byte offset of `fieldName` (as a JSON property key) inside `text`,
132
+ * searching within the range [searchFrom, searchTo).
133
+ *
134
+ * Matches `"fieldName"` surrounded by typical JSON delimiters.
135
+ */
136
+ const findJsonKeyOffset = (text, fieldName, searchFrom, searchTo) => {
137
+ const pattern = new RegExp(`"${fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`, "g");
138
+ pattern.lastIndex = searchFrom;
139
+ let match;
140
+ while (true) {
141
+ match = pattern.exec(text);
142
+ if (match === null) break;
143
+ if (match.index >= searchTo) break;
144
+ return match.index;
145
+ }
146
+ return -1;
147
+ };
148
+ /**
149
+ * Strip JSON5/JSONC single-line (//) and multi-line (/* *\/\/) comments,
150
+ * and trailing commas, producing valid JSON that JSON.parse can handle.
151
+ */
152
+ const stripJsonComments = (text) => text.replace(/(?<!:)\/\/[^\r\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
153
+ /**
154
+ * Parse a JSON/JSON5/JSONC content file and, if `offset` falls on a content
155
+ * field key, return the dictionary key, leaf field name, and full field path.
156
+ *
157
+ * Returns null when:
158
+ * - The text is not valid JSON (even after comment stripping)
159
+ * - The cursor is not on a navigable content field key
160
+ * - The field is a meta/system key (nodeType, translation, locale codes, …)
161
+ */
162
+ const findContentFieldAtOffsetJson = (text, offset) => {
163
+ let parsed;
164
+ try {
165
+ parsed = JSON.parse(stripJsonComments(text));
166
+ } catch {
167
+ return null;
168
+ }
169
+ const dictionaryKey = typeof parsed["key"] === "string" ? parsed["key"] : null;
41
170
  if (!dictionaryKey) return null;
42
- let wordStart = offset;
43
- while (wordStart > 0 && /\w/.test(text[wordStart - 1])) wordStart--;
44
- let wordEnd = offset;
45
- while (wordEnd < text.length && /\w/.test(text[wordEnd])) wordEnd++;
46
- if (wordStart === wordEnd) return null;
47
- const word = text.slice(wordStart, wordEnd);
48
- if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(word)) return null;
49
- const after = text.slice(wordEnd);
50
- if (!/^\s*:(?!:)/.test(after)) return null;
51
- return {
52
- dictionaryKey,
53
- fieldName: word,
54
- fieldPath: [word]
171
+ const contentObj = parsed["content"];
172
+ if (!contentObj || typeof contentObj !== "object" || Array.isArray(contentObj)) return null;
173
+ const contentKeyIndex = text.indexOf("\"content\"");
174
+ if (contentKeyIndex === -1) return null;
175
+ /**
176
+ * Recursively walk the parsed content object.
177
+ * For each property key, check whether the raw text position of that key
178
+ * (within the remaining text slice) overlaps with `offset`.
179
+ *
180
+ * `textFrom` is where we start searching for each key in the raw text —
181
+ * we advance it as we consume properties to avoid false matches on
182
+ * identically-named sibling keys.
183
+ */
184
+ const walk = (obj, path, searchFrom, searchTo) => {
185
+ for (const [fieldName, fieldValue] of Object.entries(obj)) {
186
+ if (JSON_INTLAYER_NODE_KEYS.has(fieldName)) continue;
187
+ if (/^[a-z]{2}(-[A-Z]{2})?$/.test(fieldName)) continue;
188
+ const keyStart = findJsonKeyOffset(text, fieldName, searchFrom, searchTo);
189
+ if (keyStart === -1) continue;
190
+ const keyEnd = keyStart + fieldName.length + 2;
191
+ if (offset >= keyStart && offset <= keyEnd) return {
192
+ dictionaryKey,
193
+ fieldName,
194
+ fieldPath: [...path, fieldName]
195
+ };
196
+ if (fieldValue !== null && typeof fieldValue === "object" && !Array.isArray(fieldValue)) {
197
+ const nested = fieldValue;
198
+ if ("nodeType" in nested) continue;
199
+ const result = walk(nested, [...path, fieldName], keyEnd, searchTo);
200
+ if (result) return result;
201
+ }
202
+ }
203
+ return null;
55
204
  };
205
+ return walk(contentObj, [], contentKeyIndex, text.length);
56
206
  };
57
207
  /**
58
- * If `offset` falls on a content field property key inside an intlayer content
59
- * file, return the dictionary key, the leaf field name, and the full field path
60
- * (array of names from the content root to the clicked field).
61
- *
62
- * For example, in:
63
- * content: { searchInput: { text: t({...}) } }
64
- *
65
- * Clicking on `text` returns `{ dictionaryKey, fieldName: 'text', fieldPath: ['searchInput', 'text'] }`.
66
- * Clicking on `searchInput` returns `{ ..., fieldName: 'searchInput', fieldPath: ['searchInput'] }`.
208
+ * Locale-code pattern — keys matching this inside YAML content blocks are
209
+ * translation locale identifiers, not navigable content fields.
210
+ */
211
+ const LOCALE_CODE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;
212
+ /**
213
+ * YAML / Markdown meta-keys that appear at the top level or inside typed nodes.
214
+ * These should not be treated as navigable content fields.
215
+ */
216
+ const YAML_META_KEYS = /* @__PURE__ */ new Set([
217
+ "nodeType",
218
+ "translation",
219
+ "enumeration",
220
+ "condition",
221
+ "plural",
222
+ "insertion",
223
+ "nested",
224
+ "markdown",
225
+ "html",
226
+ "file",
227
+ "composite",
228
+ "dictionaryKey",
229
+ "key",
230
+ "title",
231
+ "description",
232
+ "tags",
233
+ "format",
234
+ "locale",
235
+ "schema",
236
+ "location",
237
+ "fill",
238
+ "priority",
239
+ "version",
240
+ "importMode",
241
+ "$schema"
242
+ ]);
243
+ /**
244
+ * Improved regex fallback for YAML and Markdown content files.
67
245
  *
68
- * Locale keys inside `t({en:…, fr:…})` calls are correctly skipped.
246
+ * Strategy:
247
+ * 1. Extract the dictionary key from the frontmatter / top-level `key:` line.
248
+ * 2. Locate the `content:` section (YAML files only; Markdown has no `content:` block).
249
+ * 3. Within the content section, find all `fieldName:` patterns at consistent
250
+ * indentation levels, skipping known meta-keys and locale codes.
251
+ * 4. Check whether `offset` falls within one of those key spans.
252
+ * 5. Return a single-element `fieldPath` (deep path tracking is impractical
253
+ * without a full YAML parser, but single-level is enough for most use cases).
69
254
  *
70
- * Uses Oxc AST for JS/TS/TSX/JSX files; falls back to a regex heuristic for
71
- * YAML and Markdown where Oxc cannot parse.
255
+ * For Markdown files, `findContentFieldAtOffset` always returns null because
256
+ * the body text is the content value — individual content "fields" are not
257
+ * declared as separate properties.
72
258
  */
73
- const findContentFieldAtOffset = (text, offset) => {
74
- const program = parseText(text);
75
- if (!program) return findContentFieldAtOffsetRegex(text, offset);
259
+ const findContentFieldAtOffsetYaml = (text, offset) => {
260
+ const keyMatch = /^key\s*:\s*(?:(['"`])([^'"`\r\n]+)\1|([^\s'"`{},\r\n]+))/m.exec(text);
261
+ if (!keyMatch) return null;
262
+ const dictionaryKey = keyMatch[2] ?? keyMatch[3];
263
+ if (!dictionaryKey) return null;
264
+ const contentSectionMatch = /^content\s*:/m.exec(text);
265
+ if (!contentSectionMatch) return null;
266
+ const contentStart = contentSectionMatch.index + contentSectionMatch[0].length;
267
+ const afterContent = text.slice(contentStart);
268
+ const nextTopLevelMatch = /\r?\n[a-zA-Z_$][a-zA-Z0-9_$]*\s*:/m.exec(afterContent);
269
+ const contentEnd = nextTopLevelMatch ? contentStart + nextTopLevelMatch.index : text.length;
270
+ if (offset < contentStart || offset > contentEnd) return null;
271
+ const contentText = text.slice(contentStart, contentEnd);
272
+ const fieldPattern = /^([ \t]+)([a-zA-Z_$][a-zA-Z0-9_$]*)[ \t]*:/gm;
273
+ let bestMatch = null;
274
+ let fieldMatch;
275
+ while (true) {
276
+ fieldMatch = fieldPattern.exec(contentText);
277
+ if (fieldMatch === null) break;
278
+ const fieldName = fieldMatch[2];
279
+ if (YAML_META_KEYS.has(fieldName)) continue;
280
+ if (LOCALE_CODE_PATTERN.test(fieldName)) continue;
281
+ const fieldKeyStart = contentStart + fieldMatch.index + fieldMatch[1].length;
282
+ const fieldKeyEnd = fieldKeyStart + fieldName.length;
283
+ if (offset >= fieldKeyStart && offset <= fieldKeyEnd) {
284
+ bestMatch = {
285
+ dictionaryKey,
286
+ fieldName,
287
+ fieldPath: [fieldName]
288
+ };
289
+ break;
290
+ }
291
+ }
292
+ return bestMatch;
293
+ };
294
+ const findContentFieldAtOffsetOxc = (_text, offset, program) => {
76
295
  let foundResult = null;
77
296
  walkAst(program, (node) => {
78
297
  if (foundResult) return true;
@@ -114,7 +333,7 @@ const findContentFieldAtOffset = (text, offset) => {
114
333
  return true;
115
334
  }
116
335
  if (!propValue) continue;
117
- if (isTCall(propValue)) continue;
336
+ if (isIntlayerLeafCall(propValue)) continue;
118
337
  if (propValue["type"] === "ObjectExpression") {
119
338
  if (walkContent(propValue, [...path, fieldName])) return true;
120
339
  }
@@ -1 +1 @@
1
- {"version":3,"file":"findKeyInContentFile.mjs","names":[],"sources":["../../src/findKeyInContentFile.ts"],"sourcesContent":["import { type OxcNode, parseText, walkAst } from './oxcUtils';\n\n// Group 2: quoted value — key: \"foo\" | key: 'foo' | key: `foo`\n// Group 3: unquoted value — key: foo-bar (YAML / markdown frontmatter)\nconst KEY_DECLARATION_REGULAR_EXPRESSION =\n /\\bkey\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^\\s'\"`{},\\r\\n]+))/g;\n\n/**\n * If `offset` falls within a `key:` declaration in a content file,\n * return the declared key string; otherwise return null.\n * Handles quoted values (TS/JS/JSON) and bare values (YAML/Markdown frontmatter).\n */\nexport const findKeyInContentFile = (\n text: string,\n offset: number\n): string | null => {\n for (const match of text.matchAll(KEY_DECLARATION_REGULAR_EXPRESSION)) {\n const start = match.index!;\n const end = start + match[0].length;\n\n if (offset >= start && offset <= end) {\n return match[2] ?? match[3] ?? null;\n }\n }\n\n return null;\n};\n\n/** Returns true when `node` is a call to the intlayer `t()` translation function. */\nconst isTCall = (node: OxcNode): boolean => {\n if (node['type'] !== 'CallExpression') return false;\n const callee = node['callee'] as OxcNode | undefined;\n return (\n callee?.['type'] === 'Identifier' && (callee['name'] as string) === 't'\n );\n};\n\n/** Extract a string value from a Literal / StringLiteral / TemplateLiteral node. */\nconst getStringValue = (node: OxcNode): string | null => {\n if (\n (node['type'] === 'Literal' || node['type'] === 'StringLiteral') &&\n typeof node['value'] === 'string'\n ) {\n return node['value'] as string;\n }\n if (node['type'] === 'TemplateLiteral') {\n const quasis = node['quasis'] as OxcNode[] | undefined;\n const valueNode = quasis?.[0]?.['value'] as OxcNode | undefined;\n return (\n (valueNode?.['cooked'] as string | undefined) ??\n (valueNode?.['raw'] as string | undefined) ??\n null\n );\n }\n return null;\n};\n\n/**\n * Fallback for non-JS/TS files (YAML, Markdown, JSON) or when Oxc cannot parse\n * the file. Uses the original regex approach, returning a single-element path.\n */\nconst findContentFieldAtOffsetRegex = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string; fieldPath: string[] } | null => {\n const keyMatch =\n /\\bkey\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^\\s'\"`{},\\r\\n]+))/.exec(text);\n if (!keyMatch) return null;\n\n const dictionaryKey = keyMatch[2] ?? keyMatch[3];\n if (!dictionaryKey) return null;\n\n let wordStart = offset;\n while (wordStart > 0 && /\\w/.test(text[wordStart - 1]!)) wordStart--;\n let wordEnd = offset;\n while (wordEnd < text.length && /\\w/.test(text[wordEnd]!)) wordEnd++;\n\n if (wordStart === wordEnd) return null;\n\n const word = text.slice(wordStart, wordEnd);\n if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(word)) return null;\n\n const after = text.slice(wordEnd);\n if (!/^\\s*:(?!:)/.test(after)) return null;\n\n return { dictionaryKey, fieldName: word, fieldPath: [word] };\n};\n\n/**\n * If `offset` falls on a content field property key inside an intlayer content\n * file, return the dictionary key, the leaf field name, and the full field path\n * (array of names from the content root to the clicked field).\n *\n * For example, in:\n * content: { searchInput: { text: t({...}) } }\n *\n * Clicking on `text` returns `{ dictionaryKey, fieldName: 'text', fieldPath: ['searchInput', 'text'] }`.\n * Clicking on `searchInput` returns `{ ..., fieldName: 'searchInput', fieldPath: ['searchInput'] }`.\n *\n * Locale keys inside `t({en:…, fr:…})` calls are correctly skipped.\n *\n * Uses Oxc AST for JS/TS/TSX/JSX files; falls back to a regex heuristic for\n * YAML and Markdown where Oxc cannot parse.\n */\nexport const findContentFieldAtOffset = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string; fieldPath: string[] } | null => {\n const program = parseText(text);\n if (!program) return findContentFieldAtOffsetRegex(text, offset);\n\n let foundResult: {\n dictionaryKey: string;\n fieldName: string;\n fieldPath: string[];\n } | null = null;\n\n walkAst(program, (node) => {\n if (foundResult) return true;\n if (node['type'] !== 'ObjectExpression') return;\n\n const properties = node['properties'] as OxcNode[] | undefined;\n if (!properties?.length) return;\n\n // Look for sibling `key:` and `content:` properties in the same object\n let dictionaryKey: string | null = null;\n let contentNode: OxcNode | null = null;\n\n for (const prop of properties) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey || !propValue) continue;\n\n const keyName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n\n if (keyName === 'key') dictionaryKey = getStringValue(propValue);\n if (keyName === 'content') contentNode = propValue;\n }\n\n if (!dictionaryKey || !contentNode) return;\n\n // Walk the `content:` object tree, tracking the path to each property key\n const walkContent = (obj: OxcNode, path: string[]): boolean => {\n if (obj['type'] !== 'ObjectExpression') return false;\n const props = obj['properties'] as OxcNode[] | undefined;\n if (!props) return false;\n\n for (const prop of props) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey) continue;\n\n const fieldName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n if (!fieldName) continue;\n\n const keyStart = propKey['start'] as number;\n const keyEnd = propKey['end'] as number;\n\n // Cursor is on this property key\n if (offset >= keyStart && offset <= keyEnd) {\n const fieldPath = [...path, fieldName];\n foundResult = { dictionaryKey: dictionaryKey!, fieldName, fieldPath };\n return true;\n }\n\n if (!propValue) continue;\n // Skip t({...}) — those contain locale codes, not navigable content fields\n if (isTCall(propValue)) continue;\n // Recurse into nested plain objects\n if (propValue['type'] === 'ObjectExpression') {\n if (walkContent(propValue, [...path, fieldName])) return true;\n }\n }\n\n return false;\n };\n\n if (walkContent(contentNode, [])) return true;\n });\n\n return foundResult;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"findKeyInContentFile.mjs","names":[],"sources":["../../src/findKeyInContentFile.ts"],"sourcesContent":["import { type OxcNode, parseText, walkAst } from './oxcUtils';\n\n// Group 2: quoted value — key: \"foo\" | key: 'foo' | key: `foo` (TS/JS/JSON)\n// Group 3: unquoted value — key: foo-bar (YAML / Markdown frontmatter)\nconst KEY_DECLARATION_REGULAR_EXPRESSION =\n /(?:^|[{,\\s])\"?key\"?\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^ \\t'\"`{},\\r\\n]+))/g;\n\n/**\n * If `offset` falls within a `key:` declaration in a content file,\n * return the declared key string; otherwise return null.\n * Handles:\n * - Quoted values: key: \"foo\" | key: 'foo' | key: `foo` (TS/JS)\n * - JSON values: \"key\": \"foo\" (JSON/JSON5/JSONC)\n * - Bare values: key: foo-bar (YAML / Markdown frontmatter)\n */\nexport const findKeyInContentFile = (\n text: string,\n offset: number\n): string | null => {\n for (const match of text.matchAll(KEY_DECLARATION_REGULAR_EXPRESSION)) {\n const start = match.index!;\n const end = start + match[0].length;\n\n if (offset >= start && offset <= end) {\n return match[2] ?? match[3] ?? null;\n }\n }\n\n return null;\n};\n\n/**\n * If `offset` falls on a content field property key inside an intlayer content\n * file, return the dictionary key, the leaf field name, and the full field path.\n *\n * The `fileExtension` parameter controls which parser is used:\n *\n * - `.json` / `.json5` / `.jsonc` → dedicated JSON parser\n * - `.yaml` / `.yml` → YAML regex heuristic\n * - `.md` / `.mdx` → always null (body is the content value)\n * - everything else → Oxc AST (TS/JS/TSX/JSX)\n *\n * When `fileExtension` is omitted the function falls back to Oxc, then to\n * JSON/YAML heuristics based on text shape.\n */\nexport const findContentFieldAtOffset = (\n text: string,\n offset: number,\n fileExtension?: string\n): FieldResult | null => {\n const ext = fileExtension?.toLowerCase() ?? '';\n\n // Markdown has no individual navigable content fields\n if (ext === '.md' || ext === '.mdx') return null;\n\n // JSON / JSON5 / JSONC\n if (ext === '.json' || ext === '.json5' || ext === '.jsonc') {\n return findContentFieldAtOffsetJson(text, offset);\n }\n\n // YAML / YML\n if (ext === '.yaml' || ext === '.yml') {\n return findContentFieldAtOffsetYaml(text, offset);\n }\n\n // TS / JS / TSX / JSX — use Oxc AST\n const program = parseText(text);\n if (program) {\n return findContentFieldAtOffsetOxc(text, offset, program);\n }\n\n // Oxc failed to parse — fall back on content-shape heuristics\n // JSON: starts with optional whitespace then `{`\n if (/^\\s*\\{/.test(text)) {\n return findContentFieldAtOffsetJson(text, offset);\n }\n\n // Markdown: YAML frontmatter fence with no `content:` block\n if (/^---\\s*\\r?\\n/.test(text)) {\n if (!/^content\\s*:/m.test(text)) return null;\n }\n\n // YAML (or anything else with a content: block)\n return findContentFieldAtOffsetYaml(text, offset);\n};\n\n// ---------------------------------------------------------------------------\n// Helpers shared by the Oxc and YAML/JSON fallback paths\n// ---------------------------------------------------------------------------\n\n/**\n * All Intlayer helper function names whose arguments are locale-map or\n * control-flow data — NOT navigable content field names. The walker must\n * treat these calls as opaque leaf values and never descend into them.\n *\n * `t` – translation ({ en: \"…\", fr: \"…\" })\n * `enu` – enumeration ({ \"0\": \"…\", \">5\": \"…\" })\n * `cond` – condition ({ true: \"…\", false: \"…\" })\n * `plural` – plural rules ({ one: \"…\", other: \"…\" })\n * `gender` – gender rules ({ male: \"…\", female: \"…\", other: \"…\" })\n * `insert` – insertion (\"Hello {{name}}!\")\n * `nest` – nested dict (\"navbar\")\n * `md` – markdown (\"# …\")\n * `file` – file reference (\"./path/to/file.txt\")\n * `html` – html content (\"<p>…</p>\")\n */\nconst INTLAYER_LEAF_CALL_NAMES: ReadonlySet<string> = new Set([\n 't',\n 'enu',\n 'cond',\n 'plural',\n 'gender',\n 'insert',\n 'nest',\n 'md',\n 'file',\n 'html',\n]);\n\n/** Returns true when `node` is a call to any intlayer leaf helper function. */\nconst isIntlayerLeafCall = (node: OxcNode): boolean => {\n if (node['type'] !== 'CallExpression') return false;\n const callee = node['callee'] as OxcNode | undefined;\n return (\n callee?.['type'] === 'Identifier' &&\n INTLAYER_LEAF_CALL_NAMES.has(callee['name'] as string)\n );\n};\n\n/** Extract a string value from a Literal / StringLiteral / TemplateLiteral node. */\nconst getStringValue = (node: OxcNode): string | null => {\n if (\n (node['type'] === 'Literal' || node['type'] === 'StringLiteral') &&\n typeof node['value'] === 'string'\n ) {\n return node['value'] as string;\n }\n if (node['type'] === 'TemplateLiteral') {\n const quasis = node['quasis'] as OxcNode[] | undefined;\n const valueNode = quasis?.[0]?.['value'] as OxcNode | undefined;\n return (\n (valueNode?.['cooked'] as string | undefined) ??\n (valueNode?.['raw'] as string | undefined) ??\n null\n );\n }\n return null;\n};\n\n// ---------------------------------------------------------------------------\n// JSON content-file support\n// ---------------------------------------------------------------------------\n\n/**\n * Well-known meta-keys that appear inside Intlayer typed nodes in JSON files.\n * e.g. { \"nodeType\": \"translation\", \"translation\": { \"en\": \"…\" } }\n * None of these are navigable content fields.\n */\nconst JSON_INTLAYER_NODE_KEYS: ReadonlySet<string> = new Set([\n 'nodeType',\n 'translation',\n 'enumeration',\n 'condition',\n 'plural',\n 'insertion',\n 'nested',\n 'markdown',\n 'html',\n 'file',\n 'composite',\n 'dictionaryKey',\n // JSON schema / system properties\n '$schema',\n 'id',\n 'projectIds',\n 'localId',\n 'localIds',\n 'filePath',\n 'versions',\n 'filled',\n // Top-level dictionary meta-properties (not inside content)\n 'key',\n 'title',\n 'description',\n 'tags',\n 'format',\n 'locale',\n 'schema',\n 'location',\n 'fill',\n 'priority',\n 'version',\n 'importMode',\n]);\n\n/**\n * Find the byte offset of `fieldName` (as a JSON property key) inside `text`,\n * searching within the range [searchFrom, searchTo).\n *\n * Matches `\"fieldName\"` surrounded by typical JSON delimiters.\n */\nconst findJsonKeyOffset = (\n text: string,\n fieldName: string,\n searchFrom: number,\n searchTo: number\n): number => {\n const pattern = new RegExp(\n `\"${fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\"`,\n 'g'\n );\n pattern.lastIndex = searchFrom;\n let match: RegExpExecArray | null;\n while (true) {\n match = pattern.exec(text);\n if (match === null) break;\n if (match.index >= searchTo) break;\n return match.index;\n }\n return -1;\n};\n\n/**\n * Strip JSON5/JSONC single-line (//) and multi-line (/* *\\/\\/) comments,\n * and trailing commas, producing valid JSON that JSON.parse can handle.\n */\nconst stripJsonComments = (text: string): string =>\n text\n // Remove single-line comments (// …) EXCEPT in URLs (e.g., https://)\n .replace(/(?<!:)\\/\\/[^\\r\\n]*/g, '')\n // Remove block comments (/* … */)\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n // Remove trailing commas before } or ]\n .replace(/,(\\s*[}\\]])/g, '$1');\n\ntype FieldResult = {\n dictionaryKey: string;\n fieldName: string;\n fieldPath: string[];\n};\n\n/**\n * Parse a JSON/JSON5/JSONC content file and, if `offset` falls on a content\n * field key, return the dictionary key, leaf field name, and full field path.\n *\n * Returns null when:\n * - The text is not valid JSON (even after comment stripping)\n * - The cursor is not on a navigable content field key\n * - The field is a meta/system key (nodeType, translation, locale codes, …)\n */\nconst findContentFieldAtOffsetJson = (\n text: string,\n offset: number\n): FieldResult | null => {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(stripJsonComments(text)) as Record<string, unknown>;\n } catch {\n return null;\n }\n\n const dictionaryKey =\n typeof parsed['key'] === 'string' ? parsed['key'] : null;\n if (!dictionaryKey) return null;\n\n const contentObj = parsed['content'];\n if (\n !contentObj ||\n typeof contentObj !== 'object' ||\n Array.isArray(contentObj)\n )\n return null;\n\n // Find the byte range of the \"content\" block in raw text so we can scope\n // offset comparisons to content fields only.\n const contentKeyIndex = text.indexOf('\"content\"');\n if (contentKeyIndex === -1) return null;\n\n /**\n * Recursively walk the parsed content object.\n * For each property key, check whether the raw text position of that key\n * (within the remaining text slice) overlaps with `offset`.\n *\n * `textFrom` is where we start searching for each key in the raw text —\n * we advance it as we consume properties to avoid false matches on\n * identically-named sibling keys.\n */\n const walk = (\n obj: Record<string, unknown>,\n path: string[],\n searchFrom: number,\n searchTo: number\n ): FieldResult | null => {\n for (const [fieldName, fieldValue] of Object.entries(obj)) {\n // Skip Intlayer-internal node keys — they are not navigable content fields\n if (JSON_INTLAYER_NODE_KEYS.has(fieldName)) continue;\n\n // Skip keys that look like locale codes (e.g. \"en\", \"fr\", \"en-GB\")\n if (/^[a-z]{2}(-[A-Z]{2})?$/.test(fieldName)) continue;\n\n const keyStart = findJsonKeyOffset(text, fieldName, searchFrom, searchTo);\n if (keyStart === -1) continue;\n const keyEnd = keyStart + fieldName.length + 2; // +2 for surrounding quotes\n\n if (offset >= keyStart && offset <= keyEnd) {\n return {\n dictionaryKey,\n fieldName,\n fieldPath: [...path, fieldName],\n };\n }\n\n // Recurse into plain nested objects (skip typed nodes — they have nodeType)\n if (\n fieldValue !== null &&\n typeof fieldValue === 'object' &&\n !Array.isArray(fieldValue)\n ) {\n const nested = fieldValue as Record<string, unknown>;\n // Skip Intlayer typed nodes — their children are not content fields\n if ('nodeType' in nested) continue;\n\n const result = walk(nested, [...path, fieldName], keyEnd, searchTo);\n if (result) return result;\n }\n }\n return null;\n };\n\n return walk(\n contentObj as Record<string, unknown>,\n [],\n contentKeyIndex,\n text.length\n );\n};\n\n// ---------------------------------------------------------------------------\n// YAML / Markdown frontmatter fallback\n// ---------------------------------------------------------------------------\n\n/**\n * Locale-code pattern — keys matching this inside YAML content blocks are\n * translation locale identifiers, not navigable content fields.\n */\nconst LOCALE_CODE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;\n\n/**\n * YAML / Markdown meta-keys that appear at the top level or inside typed nodes.\n * These should not be treated as navigable content fields.\n */\nconst YAML_META_KEYS: ReadonlySet<string> = new Set([\n 'nodeType',\n 'translation',\n 'enumeration',\n 'condition',\n 'plural',\n 'insertion',\n 'nested',\n 'markdown',\n 'html',\n 'file',\n 'composite',\n 'dictionaryKey',\n // Frontmatter / top-level keys\n 'key',\n 'title',\n 'description',\n 'tags',\n 'format',\n 'locale',\n 'schema',\n 'location',\n 'fill',\n 'priority',\n 'version',\n 'importMode',\n '$schema',\n]);\n\n/**\n * Improved regex fallback for YAML and Markdown content files.\n *\n * Strategy:\n * 1. Extract the dictionary key from the frontmatter / top-level `key:` line.\n * 2. Locate the `content:` section (YAML files only; Markdown has no `content:` block).\n * 3. Within the content section, find all `fieldName:` patterns at consistent\n * indentation levels, skipping known meta-keys and locale codes.\n * 4. Check whether `offset` falls within one of those key spans.\n * 5. Return a single-element `fieldPath` (deep path tracking is impractical\n * without a full YAML parser, but single-level is enough for most use cases).\n *\n * For Markdown files, `findContentFieldAtOffset` always returns null because\n * the body text is the content value — individual content \"fields\" are not\n * declared as separate properties.\n */\nconst findContentFieldAtOffsetYaml = (\n text: string,\n offset: number\n): FieldResult | null => {\n // Extract dictionary key\n const keyMatch =\n /^key\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^\\s'\"`{},\\r\\n]+))/m.exec(text);\n if (!keyMatch) return null;\n const dictionaryKey = keyMatch[2] ?? keyMatch[3];\n if (!dictionaryKey) return null;\n\n // Find the content: section start\n const contentSectionMatch = /^content\\s*:/m.exec(text);\n if (!contentSectionMatch) return null;\n const contentStart =\n contentSectionMatch.index + contentSectionMatch[0].length;\n\n // Find the end of the content section: next top-level key (no leading whitespace)\n // or end of file.\n const afterContent = text.slice(contentStart);\n const nextTopLevelMatch = /\\r?\\n[a-zA-Z_$][a-zA-Z0-9_$]*\\s*:/m.exec(\n afterContent\n );\n const contentEnd = nextTopLevelMatch\n ? contentStart + nextTopLevelMatch.index\n : text.length;\n\n // The cursor must be within the content block\n if (offset < contentStart || offset > contentEnd) return null;\n\n // Find all ` fieldName:` patterns within the content block.\n // We look for lines that have at least 2 spaces of indentation and a\n // word-character key followed by a colon.\n const contentText = text.slice(contentStart, contentEnd);\n const fieldPattern = /^([ \\t]+)([a-zA-Z_$][a-zA-Z0-9_$]*)[ \\t]*:/gm;\n\n let bestMatch: FieldResult | null = null;\n let fieldMatch: RegExpExecArray | null;\n\n while (true) {\n fieldMatch = fieldPattern.exec(contentText);\n if (fieldMatch === null) break;\n\n const fieldName = fieldMatch[2]!;\n\n // Skip meta-keys and locale codes\n if (YAML_META_KEYS.has(fieldName)) continue;\n if (LOCALE_CODE_PATTERN.test(fieldName)) continue;\n\n const fieldKeyStart =\n contentStart + fieldMatch.index + fieldMatch[1]!.length; // skip indentation\n const fieldKeyEnd = fieldKeyStart + fieldName.length;\n\n if (offset >= fieldKeyStart && offset <= fieldKeyEnd) {\n bestMatch = { dictionaryKey, fieldName, fieldPath: [fieldName] };\n break;\n }\n }\n\n return bestMatch;\n};\n\n// ---------------------------------------------------------------------------\n// Oxc (TS / JS) AST path\n// ---------------------------------------------------------------------------\n\nconst findContentFieldAtOffsetOxc = (\n _text: string,\n offset: number,\n program: OxcNode\n): FieldResult | null => {\n let foundResult: FieldResult | null = null;\n\n walkAst(program, (node) => {\n if (foundResult) return true;\n if (node['type'] !== 'ObjectExpression') return;\n\n const properties = node['properties'] as OxcNode[] | undefined;\n if (!properties?.length) return;\n\n // Look for sibling `key:` and `content:` properties in the same object\n let dictionaryKey: string | null = null;\n let contentNode: OxcNode | null = null;\n\n for (const prop of properties) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey || !propValue) continue;\n\n const keyName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n\n if (keyName === 'key') dictionaryKey = getStringValue(propValue);\n if (keyName === 'content') contentNode = propValue;\n }\n\n if (!dictionaryKey || !contentNode) return;\n\n // Walk the `content:` object tree, tracking the path to each property key\n const walkContent = (obj: OxcNode, path: string[]): boolean => {\n if (obj['type'] !== 'ObjectExpression') return false;\n const props = obj['properties'] as OxcNode[] | undefined;\n if (!props) return false;\n\n for (const prop of props) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey) continue;\n\n const fieldName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n if (!fieldName) continue;\n\n const keyStart = propKey['start'] as number;\n const keyEnd = propKey['end'] as number;\n\n // Cursor is on this property key\n if (offset >= keyStart && offset <= keyEnd) {\n const fieldPath = [...path, fieldName];\n foundResult = { dictionaryKey: dictionaryKey!, fieldName, fieldPath };\n return true;\n }\n\n if (!propValue) continue;\n // Skip all intlayer leaf calls — their arguments are not navigable content fields\n if (isIntlayerLeafCall(propValue)) continue;\n // Recurse into nested plain objects\n if (propValue['type'] === 'ObjectExpression') {\n if (walkContent(propValue, [...path, fieldName])) return true;\n }\n }\n\n return false;\n };\n\n if (walkContent(contentNode, [])) return true;\n });\n\n return foundResult;\n};\n"],"mappings":";;;AAIA,MAAM,qCACJ;;;;;;;;;AAUF,MAAa,wBACX,MACA,WACkB;CAClB,KAAK,MAAM,SAAS,KAAK,SAAS,kCAAkC,GAAG;EACrE,MAAM,QAAQ,MAAM;EACpB,MAAM,MAAM,QAAQ,MAAM,EAAE,CAAC;EAE7B,IAAI,UAAU,SAAS,UAAU,KAC/B,OAAO,MAAM,MAAM,MAAM,MAAM;CAEnC;CAEA,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,MAAa,4BACX,MACA,QACA,kBACuB;CACvB,MAAM,MAAM,eAAe,YAAY,KAAK;CAG5C,IAAI,QAAQ,SAAS,QAAQ,QAAQ,OAAO;CAG5C,IAAI,QAAQ,WAAW,QAAQ,YAAY,QAAQ,UACjD,OAAO,6BAA6B,MAAM,MAAM;CAIlD,IAAI,QAAQ,WAAW,QAAQ,QAC7B,OAAO,6BAA6B,MAAM,MAAM;CAIlD,MAAM,UAAU,UAAU,IAAI;CAC9B,IAAI,SACF,OAAO,4BAA4B,MAAM,QAAQ,OAAO;CAK1D,IAAI,SAAS,KAAK,IAAI,GACpB,OAAO,6BAA6B,MAAM,MAAM;CAIlD,IAAI,eAAe,KAAK,IAAI,GAC1B;MAAI,CAAC,gBAAgB,KAAK,IAAI,GAAG,OAAO;CAAI;CAI9C,OAAO,6BAA6B,MAAM,MAAM;AAClD;;;;;;;;;;;;;;;;;AAsBA,MAAM,2CAAgD,IAAI,IAAI;CAC5D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,MAAM,sBAAsB,SAA2B;CACrD,IAAI,KAAK,YAAY,kBAAkB,OAAO;CAC9C,MAAM,SAAS,KAAK;CACpB,OACE,SAAS,YAAY,gBACrB,yBAAyB,IAAI,OAAO,OAAiB;AAEzD;;AAGA,MAAM,kBAAkB,SAAiC;CACvD,KACG,KAAK,YAAY,aAAa,KAAK,YAAY,oBAChD,OAAO,KAAK,aAAa,UAEzB,OAAO,KAAK;CAEd,IAAI,KAAK,YAAY,mBAAmB;EAEtC,MAAM,YADS,KAAK,SACI,GAAG,EAAE,GAAG;EAChC,OACG,YAAY,aACZ,YAAY,UACb;CAEJ;CACA,OAAO;AACT;;;;;;AAWA,MAAM,0CAA+C,IAAI,IAAI;CAC3D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;AAQD,MAAM,qBACJ,MACA,WACA,YACA,aACW;CACX,MAAM,UAAU,IAAI,OAClB,IAAI,UAAU,QAAQ,uBAAuB,MAAM,EAAE,IACrD,GACF;CACA,QAAQ,YAAY;CACpB,IAAI;CACJ,OAAO,MAAM;EACX,QAAQ,QAAQ,KAAK,IAAI;EACzB,IAAI,UAAU,MAAM;EACpB,IAAI,MAAM,SAAS,UAAU;EAC7B,OAAO,MAAM;CACf;CACA,OAAO;AACT;;;;;AAMA,MAAM,qBAAqB,SACzB,KAEG,QAAQ,uBAAuB,EAAE,CAAC,CAElC,QAAQ,qBAAqB,EAAE,CAAC,CAEhC,QAAQ,gBAAgB,IAAI;;;;;;;;;;AAiBjC,MAAM,gCACJ,MACA,WACuB;CACvB,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,kBAAkB,IAAI,CAAC;CAC7C,QAAQ;EACN,OAAO;CACT;CAEA,MAAM,gBACJ,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;CACtD,IAAI,CAAC,eAAe,OAAO;CAE3B,MAAM,aAAa,OAAO;CAC1B,IACE,CAAC,cACD,OAAO,eAAe,YACtB,MAAM,QAAQ,UAAU,GAExB,OAAO;CAIT,MAAM,kBAAkB,KAAK,QAAQ,aAAW;CAChD,IAAI,oBAAoB,IAAI,OAAO;;;;;;;;;;CAWnC,MAAM,QACJ,KACA,MACA,YACA,aACuB;EACvB,KAAK,MAAM,CAAC,WAAW,eAAe,OAAO,QAAQ,GAAG,GAAG;GAEzD,IAAI,wBAAwB,IAAI,SAAS,GAAG;GAG5C,IAAI,yBAAyB,KAAK,SAAS,GAAG;GAE9C,MAAM,WAAW,kBAAkB,MAAM,WAAW,YAAY,QAAQ;GACxE,IAAI,aAAa,IAAI;GACrB,MAAM,SAAS,WAAW,UAAU,SAAS;GAE7C,IAAI,UAAU,YAAY,UAAU,QAClC,OAAO;IACL;IACA;IACA,WAAW,CAAC,GAAG,MAAM,SAAS;GAChC;GAIF,IACE,eAAe,QACf,OAAO,eAAe,YACtB,CAAC,MAAM,QAAQ,UAAU,GACzB;IACA,MAAM,SAAS;IAEf,IAAI,cAAc,QAAQ;IAE1B,MAAM,SAAS,KAAK,QAAQ,CAAC,GAAG,MAAM,SAAS,GAAG,QAAQ,QAAQ;IAClE,IAAI,QAAQ,OAAO;GACrB;EACF;EACA,OAAO;CACT;CAEA,OAAO,KACL,YACA,CAAC,GACD,iBACA,KAAK,MACP;AACF;;;;;AAUA,MAAM,sBAAsB;;;;;AAM5B,MAAM,iCAAsC,IAAI,IAAI;CAClD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;;;;AAkBD,MAAM,gCACJ,MACA,WACuB;CAEvB,MAAM,WACJ,4DAA4D,KAAK,IAAI;CACvE,IAAI,CAAC,UAAU,OAAO;CACtB,MAAM,gBAAgB,SAAS,MAAM,SAAS;CAC9C,IAAI,CAAC,eAAe,OAAO;CAG3B,MAAM,sBAAsB,gBAAgB,KAAK,IAAI;CACrD,IAAI,CAAC,qBAAqB,OAAO;CACjC,MAAM,eACJ,oBAAoB,QAAQ,oBAAoB,EAAE,CAAC;CAIrD,MAAM,eAAe,KAAK,MAAM,YAAY;CAC5C,MAAM,oBAAoB,qCAAqC,KAC7D,YACF;CACA,MAAM,aAAa,oBACf,eAAe,kBAAkB,QACjC,KAAK;CAGT,IAAI,SAAS,gBAAgB,SAAS,YAAY,OAAO;CAKzD,MAAM,cAAc,KAAK,MAAM,cAAc,UAAU;CACvD,MAAM,eAAe;CAErB,IAAI,YAAgC;CACpC,IAAI;CAEJ,OAAO,MAAM;EACX,aAAa,aAAa,KAAK,WAAW;EAC1C,IAAI,eAAe,MAAM;EAEzB,MAAM,YAAY,WAAW;EAG7B,IAAI,eAAe,IAAI,SAAS,GAAG;EACnC,IAAI,oBAAoB,KAAK,SAAS,GAAG;EAEzC,MAAM,gBACJ,eAAe,WAAW,QAAQ,WAAW,EAAE,CAAE;EACnD,MAAM,cAAc,gBAAgB,UAAU;EAE9C,IAAI,UAAU,iBAAiB,UAAU,aAAa;GACpD,YAAY;IAAE;IAAe;IAAW,WAAW,CAAC,SAAS;GAAE;GAC/D;EACF;CACF;CAEA,OAAO;AACT;AAMA,MAAM,+BACJ,OACA,QACA,YACuB;CACvB,IAAI,cAAkC;CAEtC,QAAQ,UAAU,SAAS;EACzB,IAAI,aAAa,OAAO;EACxB,IAAI,KAAK,YAAY,oBAAoB;EAEzC,MAAM,aAAa,KAAK;EACxB,IAAI,CAAC,YAAY,QAAQ;EAGzB,IAAI,gBAA+B;EACnC,IAAI,cAA8B;EAElC,KAAK,MAAM,QAAQ,YAAY;GAC7B,IAAI,KAAK,YAAY,YAAY;GACjC,MAAM,UAAU,KAAK;GACrB,MAAM,YAAY,KAAK;GACvB,IAAI,CAAC,WAAW,CAAC,WAAW;GAE5B,MAAM,UACH,QAAQ,WACR,QAAQ;GAEX,IAAI,YAAY,OAAO,gBAAgB,eAAe,SAAS;GAC/D,IAAI,YAAY,WAAW,cAAc;EAC3C;EAEA,IAAI,CAAC,iBAAiB,CAAC,aAAa;EAGpC,MAAM,eAAe,KAAc,SAA4B;GAC7D,IAAI,IAAI,YAAY,oBAAoB,OAAO;GAC/C,MAAM,QAAQ,IAAI;GAClB,IAAI,CAAC,OAAO,OAAO;GAEnB,KAAK,MAAM,QAAQ,OAAO;IACxB,IAAI,KAAK,YAAY,YAAY;IACjC,MAAM,UAAU,KAAK;IACrB,MAAM,YAAY,KAAK;IACvB,IAAI,CAAC,SAAS;IAEd,MAAM,YACH,QAAQ,WACR,QAAQ;IACX,IAAI,CAAC,WAAW;IAEhB,MAAM,WAAW,QAAQ;IACzB,MAAM,SAAS,QAAQ;IAGvB,IAAI,UAAU,YAAY,UAAU,QAAQ;KAC1C,MAAM,YAAY,CAAC,GAAG,MAAM,SAAS;KACrC,cAAc;MAAiB;MAAgB;MAAW;KAAU;KACpE,OAAO;IACT;IAEA,IAAI,CAAC,WAAW;IAEhB,IAAI,mBAAmB,SAAS,GAAG;IAEnC,IAAI,UAAU,YAAY,oBACxB;SAAI,YAAY,WAAW,CAAC,GAAG,MAAM,SAAS,CAAC,GAAG,OAAO;IAAI;GAEjE;GAEA,OAAO;EACT;EAEA,IAAI,YAAY,aAAa,CAAC,CAAC,GAAG,OAAO;CAC3C,CAAC;CAED,OAAO;AACT"}
@@ -0,0 +1,33 @@
1
+ import { findMessageUsageAtOffset } from "./usageAnalyzer.mjs";
2
+
3
+ //#region src/findTCallAtOffset.ts
4
+ /**
5
+ * Detect when the cursor is on a message usage carrying an explicit id: a
6
+ * `t('key')` call from any translator caller, a self call
7
+ * (`formatMessage({ id })`, `i18n._('id')`), a JSX message component or a
8
+ * lingui tagged template.
9
+ *
10
+ * Thin compatibility wrapper over `findMessageUsageAtOffset` — kept for the
11
+ * VS Code extension. `fieldName` is the dotted field path.
12
+ *
13
+ * Handles all assignment forms:
14
+ * const t = useTranslations('ns'); t('field')
15
+ * const { t } = useTranslation('ns'); t('field')
16
+ * const { t: translate } = useTranslation('ns'); translate('field')
17
+ * const t = await getFixedT('en', 'ns'); t('field')
18
+ * const { t } = useI18n({ namespace: 'ns' }); t('field')
19
+ */
20
+ const findTCallAtOffset = (text, offset) => {
21
+ const usage = findMessageUsageAtOffset(text, offset);
22
+ if (!usage) return null;
23
+ if (usage.kind !== "call" && usage.kind !== "jsx" && usage.kind !== "tagged-template") return null;
24
+ if (usage.fieldPath.length === 0) return null;
25
+ return {
26
+ dictionaryKey: usage.dictionaryKey,
27
+ fieldName: usage.fieldPath.join(".")
28
+ };
29
+ };
30
+
31
+ //#endregion
32
+ export { findTCallAtOffset };
33
+ //# sourceMappingURL=findTCallAtOffset.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findTCallAtOffset.mjs","names":[],"sources":["../../src/findTCallAtOffset.ts"],"sourcesContent":["import { findMessageUsageAtOffset } from './usageAnalyzer';\n\n/**\n * Detect when the cursor is on a message usage carrying an explicit id: a\n * `t('key')` call from any translator caller, a self call\n * (`formatMessage({ id })`, `i18n._('id')`), a JSX message component or a\n * lingui tagged template.\n *\n * Thin compatibility wrapper over `findMessageUsageAtOffset` — kept for the\n * VS Code extension. `fieldName` is the dotted field path.\n *\n * Handles all assignment forms:\n * const t = useTranslations('ns'); t('field')\n * const { t } = useTranslation('ns'); t('field')\n * const { t: translate } = useTranslation('ns'); translate('field')\n * const t = await getFixedT('en', 'ns'); t('field')\n * const { t } = useI18n({ namespace: 'ns' }); t('field')\n */\nexport const findTCallAtOffset = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string } | null => {\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (!usage) return null;\n\n if (\n usage.kind !== 'call' &&\n usage.kind !== 'jsx' &&\n usage.kind !== 'tagged-template'\n ) {\n return null;\n }\n\n if (usage.fieldPath.length === 0) return null;\n\n return {\n dictionaryKey: usage.dictionaryKey,\n fieldName: usage.fieldPath.join('.'),\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,MAAa,qBACX,MACA,WACwD;CACxD,MAAM,QAAQ,yBAAyB,MAAM,MAAM;CAEnD,IAAI,CAAC,OAAO,OAAO;CAEnB,IACE,MAAM,SAAS,UACf,MAAM,SAAS,SACf,MAAM,SAAS,mBAEf,OAAO;CAGT,IAAI,MAAM,UAAU,WAAW,GAAG,OAAO;CAEzC,OAAO;EACL,eAAe,MAAM;EACrB,WAAW,MAAM,UAAU,KAAK,GAAG;CACrC;AACF"}
@@ -1,9 +1,9 @@
1
- import { getFirstStringArg, isIntlayerCall, parseText, walkAst } from "./oxcUtils.mjs";
1
+ import { findMessageUsageAtOffset } from "./usageAnalyzer.mjs";
2
2
 
3
3
  //#region src/findUsageFieldAtOffset.ts
4
4
  /**
5
5
  * Detect when the cursor is on a field name that was obtained from a
6
- * useIntlayer / getIntlayer call in the same file. Two patterns:
6
+ * useIntlayer / getIntlayer call in the same file. Two patterns:
7
7
  *
8
8
  * Pattern 1 — destructuring:
9
9
  * const { localeSwitcherLabel } = useIntlayer("locale-switcher")
@@ -13,58 +13,19 @@ import { getFirstStringArg, isIntlayerCall, parseText, walkAst } from "./oxcUtil
13
13
  * const t = useIntlayer("locale-switcher")
14
14
  * t.localeSwitcherLabel ← cursor on the property
15
15
  *
16
+ * Thin compatibility wrapper over `findMessageUsageAtOffset`.
16
17
  * Returns { dictionaryKey, fieldName } when matched, null otherwise.
17
18
  */
18
19
  const findUsageFieldAtOffset = (text, offset) => {
19
- const program = parseText(text);
20
- if (!program) return null;
21
- const variableToDictionaryKeyMap = /* @__PURE__ */ new Map();
22
- let result = null;
23
- walkAst(program, (node) => {
24
- if (result) return true;
25
- if (node["type"] !== "VariableDeclarator") return;
26
- const initializer = node["init"];
27
- if (!initializer || !isIntlayerCall(initializer)) return;
28
- const dictionaryKey = getFirstStringArg(initializer);
29
- if (!dictionaryKey) return;
30
- const identifierNode = node["id"];
31
- if (!identifierNode) return;
32
- if (identifierNode["type"] === "ObjectPattern") for (const propertyNode of identifierNode["properties"] ?? []) {
33
- const keyNode = propertyNode["key"];
34
- if (!keyNode) continue;
35
- const keyStart = keyNode["start"];
36
- const keyEnd = keyNode["end"];
37
- if (offset >= keyStart && offset <= keyEnd) {
38
- result = {
39
- dictionaryKey,
40
- fieldName: keyNode["name"]
41
- };
42
- return true;
43
- }
44
- }
45
- if (identifierNode["type"] === "Identifier") variableToDictionaryKeyMap.set(identifierNode["name"], dictionaryKey);
46
- });
47
- if (result) return result;
48
- walkAst(program, (node) => {
49
- if (result) return true;
50
- if (node["type"] !== "MemberExpression") return;
51
- const propertyNode = node["property"];
52
- if (!propertyNode) return;
53
- const propertyStart = propertyNode["start"];
54
- const propertyEnd = propertyNode["end"];
55
- if (offset < propertyStart || offset > propertyEnd) return;
56
- const objectNode = node["object"];
57
- if (objectNode?.["type"] !== "Identifier") return;
58
- const variableName = objectNode["name"];
59
- const dictionaryKey = variableToDictionaryKeyMap.get(variableName);
60
- if (!dictionaryKey) return;
61
- result = {
62
- dictionaryKey,
63
- fieldName: propertyNode["name"]
64
- };
65
- return true;
66
- });
67
- return result;
20
+ const usage = findMessageUsageAtOffset(text, offset);
21
+ if (!usage) return null;
22
+ if (usage.kind !== "member" && usage.kind !== "destructure") return null;
23
+ const fieldName = usage.fieldPath[usage.fieldPath.length - 1];
24
+ if (!fieldName) return null;
25
+ return {
26
+ dictionaryKey: usage.dictionaryKey,
27
+ fieldName
28
+ };
68
29
  };
69
30
 
70
31
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"findUsageFieldAtOffset.mjs","names":[],"sources":["../../src/findUsageFieldAtOffset.ts"],"sourcesContent":["import {\n getFirstStringArg,\n isIntlayerCall,\n type OxcNode,\n parseText,\n walkAst,\n} from './oxcUtils';\n\n/**\n * Detect when the cursor is on a field name that was obtained from a\n * useIntlayer / getIntlayer call in the same file. Two patterns:\n *\n * Pattern 1 — destructuring:\n * const { localeSwitcherLabel } = useIntlayer(\"locale-switcher\")\n * ^^^^^^^^^^^^^^^^^^ ← cursor here\n *\n * Pattern 2 — member access:\n * const t = useIntlayer(\"locale-switcher\")\n * t.localeSwitcherLabel ← cursor on the property\n *\n * Returns { dictionaryKey, fieldName } when matched, null otherwise.\n */\nexport const findUsageFieldAtOffset = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string } | null => {\n const program = parseText(text);\n if (!program) return null;\n\n // Map: variable name → dictionary key (for member-access look-up)\n const variableToDictionaryKeyMap = new Map<string, string>();\n let result: { dictionaryKey: string; fieldName: string } | null = null;\n\n // Single pass: collect variable→key assignments AND check destructure spans\n walkAst(program, (node) => {\n if (result) return true;\n if (node['type'] !== 'VariableDeclarator') return;\n\n const initializer = node['init'] as OxcNode | undefined;\n if (!initializer || !isIntlayerCall(initializer)) return;\n\n const dictionaryKey = getFirstStringArg(initializer);\n if (!dictionaryKey) return;\n\n const identifierNode = node['id'] as OxcNode | undefined;\n if (!identifierNode) return;\n\n // Pattern 1: ObjectPattern destructure → { field } = useIntlayer(\"key\")\n if (identifierNode['type'] === 'ObjectPattern') {\n for (const propertyNode of (identifierNode['properties'] as OxcNode[]) ??\n []) {\n const keyNode = propertyNode['key'] as OxcNode | undefined;\n if (!keyNode) continue;\n const keyStart = keyNode['start'] as number;\n const keyEnd = keyNode['end'] as number;\n if (offset >= keyStart && offset <= keyEnd) {\n result = {\n dictionaryKey: dictionaryKey,\n fieldName: keyNode['name'] as string,\n };\n return true;\n }\n }\n }\n\n // Record simple Identifier assignments for Pattern 2\n if (identifierNode['type'] === 'Identifier') {\n variableToDictionaryKeyMap.set(\n identifierNode['name'] as string,\n dictionaryKey\n );\n }\n });\n\n if (result) return result;\n\n // Pattern 2: MemberExpression → variableName.field where variableName = useIntlayer(\"key\")\n walkAst(program, (node) => {\n if (result) return true;\n if (node['type'] !== 'MemberExpression') return;\n\n const propertyNode = node['property'] as OxcNode | undefined;\n if (!propertyNode) return;\n const propertyStart = propertyNode['start'] as number;\n const propertyEnd = propertyNode['end'] as number;\n if (offset < propertyStart || offset > propertyEnd) return;\n\n const objectNode = node['object'] as OxcNode | undefined;\n if (objectNode?.['type'] !== 'Identifier') return;\n const variableName = objectNode['name'] as string;\n const dictionaryKey = variableToDictionaryKeyMap.get(variableName);\n if (!dictionaryKey) return;\n\n result = {\n dictionaryKey: dictionaryKey,\n fieldName: propertyNode['name'] as string,\n };\n return true;\n });\n\n return result;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,MAAa,0BACX,MACA,WACwD;CACxD,MAAM,UAAU,UAAU,IAAI;CAC9B,IAAI,CAAC,SAAS,OAAO;CAGrB,MAAM,6CAA6B,IAAI,IAAoB;CAC3D,IAAI,SAA8D;CAGlE,QAAQ,UAAU,SAAS;EACzB,IAAI,QAAQ,OAAO;EACnB,IAAI,KAAK,YAAY,sBAAsB;EAE3C,MAAM,cAAc,KAAK;EACzB,IAAI,CAAC,eAAe,CAAC,eAAe,WAAW,GAAG;EAElD,MAAM,gBAAgB,kBAAkB,WAAW;EACnD,IAAI,CAAC,eAAe;EAEpB,MAAM,iBAAiB,KAAK;EAC5B,IAAI,CAAC,gBAAgB;EAGrB,IAAI,eAAe,YAAY,iBAC7B,KAAK,MAAM,gBAAiB,eAAe,iBACzC,CAAC,GAAG;GACJ,MAAM,UAAU,aAAa;GAC7B,IAAI,CAAC,SAAS;GACd,MAAM,WAAW,QAAQ;GACzB,MAAM,SAAS,QAAQ;GACvB,IAAI,UAAU,YAAY,UAAU,QAAQ;IAC1C,SAAS;KACQ;KACf,WAAW,QAAQ;IACrB;IACA,OAAO;GACT;EACF;EAIF,IAAI,eAAe,YAAY,cAC7B,2BAA2B,IACzB,eAAe,SACf,aACF;CAEJ,CAAC;CAED,IAAI,QAAQ,OAAO;CAGnB,QAAQ,UAAU,SAAS;EACzB,IAAI,QAAQ,OAAO;EACnB,IAAI,KAAK,YAAY,oBAAoB;EAEzC,MAAM,eAAe,KAAK;EAC1B,IAAI,CAAC,cAAc;EACnB,MAAM,gBAAgB,aAAa;EACnC,MAAM,cAAc,aAAa;EACjC,IAAI,SAAS,iBAAiB,SAAS,aAAa;EAEpD,MAAM,aAAa,KAAK;EACxB,IAAI,aAAa,YAAY,cAAc;EAC3C,MAAM,eAAe,WAAW;EAChC,MAAM,gBAAgB,2BAA2B,IAAI,YAAY;EACjE,IAAI,CAAC,eAAe;EAEpB,SAAS;GACQ;GACf,WAAW,aAAa;EAC1B;EACA,OAAO;CACT,CAAC;CAED,OAAO;AACT"}
1
+ {"version":3,"file":"findUsageFieldAtOffset.mjs","names":[],"sources":["../../src/findUsageFieldAtOffset.ts"],"sourcesContent":["import { findMessageUsageAtOffset } from './usageAnalyzer';\n\n/**\n * Detect when the cursor is on a field name that was obtained from a\n * useIntlayer / getIntlayer call in the same file. Two patterns:\n *\n * Pattern 1 — destructuring:\n * const { localeSwitcherLabel } = useIntlayer(\"locale-switcher\")\n * ^^^^^^^^^^^^^^^^^^ ← cursor here\n *\n * Pattern 2 — member access:\n * const t = useIntlayer(\"locale-switcher\")\n * t.localeSwitcherLabel ← cursor on the property\n *\n * Thin compatibility wrapper over `findMessageUsageAtOffset`.\n * Returns { dictionaryKey, fieldName } when matched, null otherwise.\n */\nexport const findUsageFieldAtOffset = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string } | null => {\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (!usage) return null;\n\n if (usage.kind !== 'member' && usage.kind !== 'destructure') return null;\n\n const fieldName = usage.fieldPath[usage.fieldPath.length - 1];\n\n if (!fieldName) return null;\n\n return { dictionaryKey: usage.dictionaryKey, fieldName };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,MAAa,0BACX,MACA,WACwD;CACxD,MAAM,QAAQ,yBAAyB,MAAM,MAAM;CAEnD,IAAI,CAAC,OAAO,OAAO;CAEnB,IAAI,MAAM,SAAS,YAAY,MAAM,SAAS,eAAe,OAAO;CAEpE,MAAM,YAAY,MAAM,UAAU,MAAM,UAAU,SAAS;CAE3D,IAAI,CAAC,WAAW,OAAO;CAEvB,OAAO;EAAE,eAAe,MAAM;EAAe;CAAU;AACzD"}