@i18n-micro/cli 0.0.0-stage → 2.0.1

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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -2
  3. package/bin/i18n-micro.mjs +3 -0
  4. package/dist/chunks/check-CwSzATDI.mjs +175 -0
  5. package/dist/chunks/clean-Do9z6D5j.mjs +68 -0
  6. package/dist/chunks/clean-bQVjG_2M.mjs +1 -0
  7. package/dist/chunks/export-C2J-pagB.mjs +219 -0
  8. package/dist/chunks/export-usBaNIpp.mjs +1 -0
  9. package/dist/chunks/extract-CJ8UfrVJ.mjs +30 -0
  10. package/dist/chunks/format-6CtMgO-M.mjs +26 -0
  11. package/dist/chunks/http-whB7a5ye.mjs +100 -0
  12. package/dist/chunks/import-CnL8U6Za.mjs +99 -0
  13. package/dist/chunks/info-Lom_wHos.mjs +32 -0
  14. package/dist/chunks/json-BV_apCMu.mjs +439 -0
  15. package/dist/chunks/lint-DDGSQAXd.mjs +125 -0
  16. package/dist/chunks/nuxt-dKxG087V.mjs +366 -0
  17. package/dist/chunks/package-BejjFS6s.mjs +3 -0
  18. package/dist/chunks/project-DgWgr7pF.mjs +206 -0
  19. package/dist/chunks/protect-D1HIuxmJ.mjs +36 -0
  20. package/dist/chunks/pseudo-oXBaONgF.mjs +32 -0
  21. package/dist/chunks/rename-uJnBX86A.mjs +117 -0
  22. package/dist/chunks/runner-CsPPffV1.mjs +200 -0
  23. package/dist/chunks/search-Dj_Ynq_x.mjs +67 -0
  24. package/dist/chunks/source-Bx2lHSW7.mjs +744 -0
  25. package/dist/chunks/spec-DwveYr8X.mjs +507 -0
  26. package/dist/chunks/stats-BU-SmrVR.mjs +156 -0
  27. package/dist/chunks/sync-BY2EFEda.mjs +1 -0
  28. package/dist/chunks/sync-C0NVDabK.mjs +56 -0
  29. package/dist/chunks/sync-remote-y3ZATbF2.mjs +148 -0
  30. package/dist/chunks/text-to-i18n-DweavFLm.mjs +404 -0
  31. package/dist/chunks/translate-CmnmgAHX.mjs +543 -0
  32. package/dist/cli.mjs +4 -0
  33. package/dist/index.mjs +3 -0
  34. package/dist/src/cli.d.ts +1 -0
  35. package/dist/src/commands/check.d.ts +28 -0
  36. package/dist/src/commands/clean.d.ts +26 -0
  37. package/dist/src/commands/export.d.ts +10 -0
  38. package/dist/src/commands/extract.d.ts +6 -0
  39. package/dist/src/commands/format.d.ts +8 -0
  40. package/dist/src/commands/import.d.ts +8 -0
  41. package/dist/src/commands/info.d.ts +5 -0
  42. package/dist/src/commands/lint.d.ts +16 -0
  43. package/dist/src/commands/pseudo.d.ts +9 -0
  44. package/dist/src/commands/rename.d.ts +9 -0
  45. package/dist/src/commands/search.d.ts +20 -0
  46. package/dist/src/commands/stats.d.ts +20 -0
  47. package/dist/src/commands/sync-remote.d.ts +30 -0
  48. package/dist/src/commands/sync.d.ts +20 -0
  49. package/dist/src/commands/text-to-i18n.d.ts +10 -0
  50. package/dist/src/commands/translate.d.ts +14 -0
  51. package/dist/src/csv.d.ts +3 -0
  52. package/dist/src/errors.d.ts +22 -0
  53. package/dist/src/fs.d.ts +45 -0
  54. package/dist/src/glossary.d.ts +12 -0
  55. package/dist/src/help.d.ts +3 -0
  56. package/dist/src/http.d.ts +10 -0
  57. package/dist/src/index.d.ts +3 -0
  58. package/dist/src/json.d.ts +34 -0
  59. package/dist/src/lint-rules.d.ts +9 -0
  60. package/dist/src/log.d.ts +88 -0
  61. package/dist/src/nuxt.d.ts +66 -0
  62. package/dist/src/po.d.ts +14 -0
  63. package/dist/src/project.d.ts +64 -0
  64. package/dist/src/runner.d.ts +6 -0
  65. package/dist/src/source.d.ts +109 -0
  66. package/dist/src/spec.d.ts +57 -0
  67. package/dist/src/text-to-i18n.d.ts +46 -0
  68. package/dist/src/translate/ai.d.ts +2 -0
  69. package/dist/src/translate/drivers.d.ts +26 -0
  70. package/dist/src/translate/engine.d.ts +33 -0
  71. package/dist/src/translate/protect.d.ts +11 -0
  72. package/package.json +71 -3
@@ -0,0 +1,744 @@
1
+ import { m as walkFiles, n as globToRegExp, r as isUnder, s as readLoose, u as relativePath } from "./nuxt-dKxG087V.mjs";
2
+ import { forwardSlashes } from "@i18n-micro/utils/app-path";
3
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
4
+ import { basename, dirname, extname, relative } from "node:path";
5
+ import { babelParse, parse as parse$1, walk } from "@vue/compiler-sfc";
6
+ // src/source.ts
7
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
8
+ ".vue",
9
+ ".js",
10
+ ".ts",
11
+ ".jsx",
12
+ ".tsx",
13
+ ".mjs",
14
+ ".cjs",
15
+ ".mts",
16
+ ".cts"
17
+ ]);
18
+ /** Text no parser here reads. Keys in it still count as evidence of use. */
19
+ var TEMPLATE_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".pug"]);
20
+ var DATA_EXTENSIONS = /* @__PURE__ */ new Set([
21
+ ".json",
22
+ ".yaml",
23
+ ".yml",
24
+ ".md",
25
+ ".mdx"
26
+ ]);
27
+ var SKIPPED_DIRS = /* @__PURE__ */ new Set([
28
+ "node_modules",
29
+ ".git",
30
+ ".nuxt",
31
+ ".output",
32
+ ".data",
33
+ ".cache",
34
+ ".vercel",
35
+ ".netlify",
36
+ ".idea",
37
+ ".vscode"
38
+ ]);
39
+ /** Build output by these names only at a root: `pages/dist/` is a page. */
40
+ var OUTPUT_DIRS = /* @__PURE__ */ new Set(["dist", "coverage"]);
41
+ var SKIPPED_DATA = /^(package(-lock)?\.json|pnpm-lock\.yaml|yarn\.lock|tsconfig.*\.json)$/;
42
+ /** Test code by location, relative to the project root. `pages/tests/` is a route. */
43
+ var TEST_DIR = /^(tests?|__tests__|e2e|cypress|playwright)\//;
44
+ /** Built by a tool, wherever it sits. Here a bare `t(e)` is a minified name, not a guess worth stopping for. */
45
+ var GENERATED_PATH = /(^|\/)(dist|\.vitepress|storybook-static)\//;
46
+ /** Test code by name. Only code: `Latest.spec.vue` renders. */
47
+ var TEST_NAME = /\.(test|spec)\.[cm]?[jt]sx?$/;
48
+ var NODE_TYPES = {
49
+ ELEMENT: 1,
50
+ TEXT: 2,
51
+ COMMENT: 3,
52
+ INTERPOLATION: 5,
53
+ ATTRIBUTE: 6,
54
+ DIRECTIVE: 7
55
+ };
56
+ var isCodeFile = (path) => CODE_EXTENSIONS.has(extname(path).toLowerCase());
57
+ /** The translation API by name. `bound` returns a key function for a route: `$_t(route)('key')`. */
58
+ var DOLLAR_NAMES = [
59
+ ["$t", "key"],
60
+ ["$ts", "key"],
61
+ ["$tc", "key"],
62
+ ["$has", "guard"],
63
+ ["$_t", "bound"],
64
+ ["$_ts", "bound"]
65
+ ];
66
+ /** `useI18n()` also returns these. In a file without `useI18n` a plain `t()` is somebody else's function. */
67
+ var BARE_NAMES = [
68
+ ["t", "key"],
69
+ ["ts", "key"],
70
+ ["tc", "key"],
71
+ ["has", "guard"],
72
+ ["_t", "bound"],
73
+ ["_ts", "bound"]
74
+ ];
75
+ var API_NAMES = new Map([...DOLLAR_NAMES, ...BARE_NAMES]);
76
+ var I18N_SOURCES = /* @__PURE__ */ new Set(["useI18n", "useNuxtApp"]);
77
+ /** Resolves to the `t` function itself. */
78
+ var SERVER_T = "useTranslationServerMiddleware";
79
+ var KEY_LIKE = /^[^\s'"`<>{}()]{1,200}$/;
80
+ var asNode = (value) => value;
81
+ var lineOf = (content, offset) => content.slice(0, Math.max(0, offset)).split("\n").length;
82
+ function staticString(node) {
83
+ if (node?.type === "StringLiteral") return node.value;
84
+ const quasis = node?.quasis;
85
+ if (node?.type === "TemplateLiteral" && quasis?.length === 1) return quasis[0].value.cooked;
86
+ return null;
87
+ }
88
+ var propertyName = (callee) => {
89
+ return (callee?.type === "MemberExpression" || callee?.type === "OptionalMemberExpression") && !callee.computed ? asNode(callee.property)?.name ?? null : null;
90
+ };
91
+ /** `i18n`, `this.$i18n`, `useI18n()`: the name a member call hangs off. */
92
+ function objectName(callee) {
93
+ const object = asNode(callee?.object);
94
+ return String(object?.name ?? asNode(object?.property)?.name ?? asNode(object?.callee)?.name ?? "");
95
+ }
96
+ var isI18nObject = (name, names) => name.slice(-4).toLowerCase() === "i18n" || names.objects.has(name) || I18N_SOURCES.has(name);
97
+ function familyOf(callee, names) {
98
+ if (isServerT(callee)) return "key";
99
+ if (callee?.type === "Identifier") return names.families.get(callee.name) ?? null;
100
+ if (callee?.type === "CallExpression" || callee?.type === "OptionalCallExpression") return familyOf(asNode(callee.callee), names) === "bound" ? "key" : null;
101
+ const name = propertyName(callee);
102
+ if (!name) return null;
103
+ const family = API_NAMES.get(name) ?? null;
104
+ return family && (name.startsWith("$") || isI18nObject(objectName(callee), names)) ? family : null;
105
+ }
106
+ var unawaited = (node) => node?.type === "AwaitExpression" ? asNode(node.argument) : node;
107
+ var isServerT = (node) => {
108
+ const call = unawaited(node);
109
+ return call?.type === "CallExpression" && asNode(call.callee)?.name === SERVER_T;
110
+ };
111
+ function isI18nSource(node, names) {
112
+ if (node?.type === "AwaitExpression") return isI18nSource(asNode(node.argument), names);
113
+ if (node?.type === "Identifier") return isI18nObject(node.name, names);
114
+ return node?.type === "CallExpression" && I18N_SOURCES.has(asNode(node.callee)?.name);
115
+ }
116
+ /** `useTranslationServerMiddleware(event).then((tr) => ...)`. */
117
+ function readServerThen(node, names) {
118
+ const callee = asNode(node.callee);
119
+ if (propertyName(callee) !== "then" || !isServerT(asNode(callee?.object))) return;
120
+ const params = asNode(node.arguments[0])?.params;
121
+ if (params?.[0]) addAlias(params[0], "t", names);
122
+ }
123
+ function addAlias(local, original, names) {
124
+ const family = API_NAMES.get(original);
125
+ const target = local?.type === "AssignmentPattern" ? asNode(local.left) : local;
126
+ if (!family || target?.type !== "Identifier") return;
127
+ names.families.set(target.name, family);
128
+ names.values.add(target.name);
129
+ }
130
+ var keyName = (prop) => String(asNode(prop.key)?.name ?? asNode(prop.key)?.value ?? "");
131
+ function readObjectPattern(pattern, init, names) {
132
+ const fromI18n = isI18nSource(init, names);
133
+ for (const prop of pattern.properties) {
134
+ if (prop.type === "RestElement" && fromI18n) names.objects.add(asNode(prop.argument)?.name);
135
+ if (prop.type === "RestElement") continue;
136
+ const original = keyName(prop);
137
+ const local = asNode(prop.value);
138
+ if (fromI18n) addAlias(local, original, names);
139
+ else if (API_NAMES.get(original) === "key" && local?.type === "Identifier") names.unknown.add(local.name);
140
+ }
141
+ }
142
+ var FUNCTION_NODES = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression"]);
143
+ /** `const [tr] = await Promise.all([useTranslationServerMiddleware(event)])`. */
144
+ function holdsServerT(node) {
145
+ let found = false;
146
+ walk(node, { enter(raw) {
147
+ found ||= isServerT(raw);
148
+ } });
149
+ return found;
150
+ }
151
+ function readDeclarator(node, names) {
152
+ const id = asNode(node.id);
153
+ const init = asNode(node.init);
154
+ if (id?.type === "ObjectPattern") {
155
+ readObjectPattern(id, init, names);
156
+ return;
157
+ }
158
+ if (id?.type === "ArrayPattern" && init && holdsServerT(init)) {
159
+ for (const element of id.elements) addAlias(element ?? void 0, "t", names);
160
+ return;
161
+ }
162
+ if (id?.type !== "Identifier") return;
163
+ if (FUNCTION_NODES.has(init?.type ?? "")) {
164
+ names.locals.add(id.name);
165
+ return;
166
+ }
167
+ if (isServerT(init)) {
168
+ addAlias(id, "t", names);
169
+ return;
170
+ }
171
+ if (isI18nSource(init, names)) {
172
+ names.objects.add(id.name);
173
+ return;
174
+ }
175
+ const family = init?.type === "CallExpression" ? familyOf(asNode(init.callee), names) === "bound" ? "key" : null : familyOf(init, names);
176
+ if (!family) return;
177
+ names.families.set(id.name, family);
178
+ names.values.add(id.name);
179
+ }
180
+ /** Aliases first: a template or a later function uses what the script declares. */
181
+ function readNames(ast, names) {
182
+ walk(ast, { enter(raw) {
183
+ const node = raw;
184
+ if (node.type === "VariableDeclarator") readDeclarator(node, names);
185
+ if (node.type === "CallExpression") readServerThen(node, names);
186
+ if (node.type === "AssignmentExpression") readDeclarator({
187
+ type: node.type,
188
+ id: node.left,
189
+ init: node.right
190
+ }, names);
191
+ if (node.type === "FunctionDeclaration") names.locals.add(asNode(node.id)?.name);
192
+ } });
193
+ }
194
+ function newNames(content) {
195
+ const bare = /useI18n|useTranslationServerMiddleware|i18n-?group/i.test(content);
196
+ return {
197
+ families: new Map([...DOLLAR_NAMES, ...bare ? BARE_NAMES : []]),
198
+ locals: /* @__PURE__ */ new Set(),
199
+ unknown: /* @__PURE__ */ new Set(),
200
+ values: new Set(DOLLAR_NAMES.map(([name]) => name)),
201
+ objects: /* @__PURE__ */ new Set()
202
+ };
203
+ }
204
+ var TYPE_WRAPPERS = /* @__PURE__ */ new Set([
205
+ "TSAsExpression",
206
+ "TSTypeAssertion",
207
+ "TSNonNullExpression",
208
+ "TSSatisfiesExpression",
209
+ "ParenthesizedExpression"
210
+ ]);
211
+ /** `$t('a' as string)` is still the key `a`. */
212
+ var unwrap = (node) => node && TYPE_WRAPPERS.has(node.type) ? unwrap(asNode(node.expression)) : node;
213
+ /** `\`a.${x}\`` and `'a.' + x` both pin down `a.`. */
214
+ function staticPrefix(node) {
215
+ if (node?.type === "BinaryExpression") return staticPrefix(asNode(node.left));
216
+ return (node?.quasis)?.[0]?.value.cooked ?? staticString(node) ?? "";
217
+ }
218
+ function addRef(literal, node, context, extra, inGroup) {
219
+ const { base, content, file, group, keyNodes, scan } = context;
220
+ keyNodes.add(node);
221
+ const start = base + node.start + 1;
222
+ const keyPrefix = inGroup ? `${group.prefix}.` : void 0;
223
+ scan.refs.push({
224
+ key: `${keyPrefix ?? ""}${literal}`,
225
+ file,
226
+ line: lineOf(content, start),
227
+ start,
228
+ end: start + literal.length,
229
+ keyPrefix,
230
+ ...extra
231
+ });
232
+ }
233
+ function addArgument(wrapped, context, extra, inGroup) {
234
+ const argument = unwrap(wrapped);
235
+ if (!argument) return;
236
+ const literal = staticString(argument);
237
+ if (literal !== null) {
238
+ addRef(literal, argument, context, extra, inGroup);
239
+ return;
240
+ }
241
+ if (argument.type === "ConditionalExpression") {
242
+ addArgument(asNode(argument.consequent), context, extra, inGroup);
243
+ addArgument(asNode(argument.alternate), context, extra, inGroup);
244
+ return;
245
+ }
246
+ if (argument.type === "LogicalExpression") {
247
+ addArgument(asNode(argument.left), context, extra, inGroup);
248
+ addArgument(asNode(argument.right), context, extra, inGroup);
249
+ return;
250
+ }
251
+ const { base, content, file, scan, group } = context;
252
+ const start = base + argument.start;
253
+ const prefix = `${inGroup ? `${group.prefix}.` : ""}${staticPrefix(argument)}`;
254
+ scan.dynamic.push({
255
+ file,
256
+ line: lineOf(content, start),
257
+ expression: content.slice(start, base + argument.end),
258
+ prefix
259
+ });
260
+ }
261
+ function addLiteral(text, scan, location) {
262
+ if (!KEY_LIKE.test(text)) return;
263
+ if (text.endsWith(".")) scan.prefixes.add(text);
264
+ else scan.literals.set(text, [...scan.literals.get(text) ?? [], location]);
265
+ }
266
+ var NOT_A_GUARD = { guard: false };
267
+ function addOpaque(expression, node, context, guess = false) {
268
+ const { base, content, file, scan } = context;
269
+ scan.dynamic.push({
270
+ file,
271
+ line: lineOf(content, base + node.start),
272
+ expression,
273
+ prefix: "",
274
+ guess
275
+ });
276
+ }
277
+ /** `I18nT`, `i18n-t`, and the `Lazy` form Nuxt registers for every component. */
278
+ var TAG_HEADS = [
279
+ "i18n",
280
+ "i18n-",
281
+ "lazyi18n",
282
+ "lazyi18n-",
283
+ "lazy-i18n",
284
+ "lazy-i18n-"
285
+ ];
286
+ var KEY_TAGS = new Set(TAG_HEADS.map((head) => `${head}t`));
287
+ var GROUP_TAGS = new Set(TAG_HEADS.map((head) => `${head}group`));
288
+ var isKeyTag = (tag) => KEY_TAGS.has(tag.toLowerCase());
289
+ var isGroupTag = (tag) => GROUP_TAGS.has(tag.toLowerCase());
290
+ var isTranslationTag = (tag) => isKeyTag(tag) || isGroupTag(tag);
291
+ var RENDER_FUNCTIONS = /* @__PURE__ */ new Set(["h", "createVNode"]);
292
+ function componentName(node) {
293
+ if (node?.type === "CallExpression") return staticString(node.arguments[0]) ?? "";
294
+ return String(node?.name ?? staticString(node) ?? "");
295
+ }
296
+ /** `h(I18nT, props)`: the key is in an object this code does not spell out. */
297
+ function visitRender(node, context) {
298
+ if (!RENDER_FUNCTIONS.has(asNode(node.callee)?.name)) return;
299
+ const [component, props] = node.arguments;
300
+ const name = componentName(component);
301
+ if (!isTranslationTag(name) || !props) return;
302
+ if (!(props.type === "ObjectExpression" && !props.properties.some((prop) => prop.type === "SpreadElement"))) addOpaque(`h(${name}, ...)`, props, context);
303
+ }
304
+ /** `<i18n-t keypath={x} />` and `<i18n-t {...x} />` in JSX. */
305
+ function visitJsx(node, context) {
306
+ const tag = String(asNode(node.name)?.name ?? "");
307
+ if (!isTranslationTag(tag)) return;
308
+ const attributes = node.attributes;
309
+ for (const attribute of attributes) {
310
+ if (attribute.type === "JSXSpreadAttribute") addOpaque(`<${tag} {...}>`, attribute, context);
311
+ if (attribute.type !== "JSXAttribute") continue;
312
+ const name = asNode(attribute.name)?.name;
313
+ const value = asNode(attribute.value);
314
+ if (name === "keypath" || name === "prefix") addArgument(value?.type === "JSXExpressionContainer" ? asNode(value.expression) : value, context, NOT_A_GUARD, false);
315
+ }
316
+ }
317
+ /** A `keypath` in an object is a key on its way to `<i18n-t>`: `h(I18nT, { keypath })`, `v-bind="{ keypath }"`. */
318
+ function visitProperty(node, context) {
319
+ if (keyName(node) === "keypath" && !node.computed) addArgument(asNode(node.value), context, NOT_A_GUARD, false);
320
+ }
321
+ /** `t(name)` with `t` from a parameter, an import or a call: a wrapper composable, most likely. It may be a translation. */
322
+ var BARE_KEY_CALL = new Set(BARE_NAMES.filter(([, family]) => family === "key").map(([name]) => name));
323
+ function visitUnknownCall(callee, argument, context) {
324
+ const target = unwrap(argument);
325
+ if (callee?.type !== "Identifier" || !target || staticString(target) !== null) return;
326
+ const name = callee.name;
327
+ if ((BARE_KEY_CALL.has(name) || context.names.unknown.has(name)) && !context.names.locals.has(name)) addOpaque(`${name}(...) of unknown origin`, target, context, true);
328
+ }
329
+ function visitCall(node, context) {
330
+ const callee = asNode(node.callee);
331
+ const inGroup = callee?.type === "Identifier" && Boolean(context.group?.names.has(callee.name));
332
+ const family = inGroup ? "key" : familyOf(callee, context.names);
333
+ if (family === null) visitUnknownCall(callee, node.arguments[0], context);
334
+ if (family !== "key" && family !== "guard") return;
335
+ const [key, , fallback] = node.arguments;
336
+ addArgument(key, context, {
337
+ guard: family === "guard",
338
+ defaultText: staticString(fallback) ?? void 0
339
+ }, inGroup);
340
+ }
341
+ /** `$t.call(null, k)`: the function itself, handed to something else. */
342
+ var FUNCTION_METHODS = /* @__PURE__ */ new Set([
343
+ "call",
344
+ "apply",
345
+ "bind"
346
+ ]);
347
+ /** Where a name only stands for itself: a declaration, a parameter, a property, a type. */
348
+ function isValueUse(parent, key, declared, node) {
349
+ if (key === "object" && parent) return FUNCTION_METHODS.has(propertyName(parent) ?? "");
350
+ if (!parent || declared.has(node) || key === "callee" || key === "params" || key === "id") return false;
351
+ if ((key === "key" || key === "property") && !parent.computed) return false;
352
+ if (parent.type.startsWith("Import") || parent.type.startsWith("Export")) return false;
353
+ if (parent.type === "UnaryExpression" && parent.operator === "typeof") return false;
354
+ return !parent.type.startsWith("TS") || key === "expression";
355
+ }
356
+ function declare(node, declared) {
357
+ if (!node) return;
358
+ declared.add(node);
359
+ if (node.type === "ObjectPattern") for (const prop of node.properties) declare(asNode(prop.value) ?? prop, declared);
360
+ if (node.type === "ArrayPattern") for (const element of node.elements) declare(element ?? void 0, declared);
361
+ if (node.type === "AssignmentPattern") declare(asNode(node.left), declared);
362
+ if (node.type === "RestElement") declare(asNode(node.argument), declared);
363
+ }
364
+ function visitNode(node, parent, key, context, declared) {
365
+ const { base, content, file, names, scan } = context;
366
+ if (node.type === "VariableDeclarator") declare(asNode(node.id), declared);
367
+ if (Array.isArray(node.params)) for (const param of node.params) declare(param, declared);
368
+ if (node.type === "CallExpression" || node.type === "OptionalCallExpression") visitCall(node, context);
369
+ if (node.type === "CallExpression") visitRender(node, context);
370
+ if (node.type === "JSXOpeningElement") visitJsx(node, context);
371
+ if (node.type === "ObjectProperty") visitProperty(node, context);
372
+ const literal = staticString(node);
373
+ if (literal !== null && !context.keyNodes.has(node)) addLiteral(literal, scan, `${file}:${lineOf(content, base + node.start)}`);
374
+ if (node.type === "TemplateLiteral" && literal === null) addLiteral(staticPrefix(node), scan, file);
375
+ if (node.type !== "Identifier" || !names.values.has(node.name) || !isValueUse(parent, key, declared, node)) return;
376
+ const start = base + node.start;
377
+ scan.dynamic.push({
378
+ file,
379
+ line: lineOf(content, start),
380
+ expression: `${node.name} passed as a value`,
381
+ prefix: ""
382
+ });
383
+ }
384
+ function visitCode(ast, context) {
385
+ const declared = /* @__PURE__ */ new WeakSet();
386
+ walk(ast, { enter(raw, parent, key) {
387
+ visitNode(raw, parent, key, context, declared);
388
+ } });
389
+ }
390
+ /** JSX only where the file allows it: in plain TypeScript `<string>x` is a cast. */
391
+ function tryParse(code, plugins) {
392
+ try {
393
+ return babelParse(code, {
394
+ sourceType: "module",
395
+ errorRecovery: true,
396
+ plugins
397
+ });
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+ var isClean = (ast) => Boolean(ast) && !ast.errors?.length;
403
+ /**
404
+ * The syntax tree, or `null` when the code does not parse. A file with a recoverable error
405
+ * (a duplicate declaration, say) still gives a whole tree; `strict` refuses it, for telling
406
+ * an expression from a statement.
407
+ */
408
+ function parseCode(code, lang = "ts", strict = false) {
409
+ const first = tryParse(code, lang.endsWith("x") ? [
410
+ "typescript",
411
+ "jsx",
412
+ "decorators"
413
+ ] : ["typescript", "decorators"]);
414
+ if (isClean(first)) return first;
415
+ const retry = lang === "js" || lang === "cjs" || lang === "mjs" ? tryParse(code, ["jsx", "decorators"]) : null;
416
+ if (isClean(retry)) return retry;
417
+ return strict ? null : first ?? retry;
418
+ }
419
+ var TEXT_CALL = /(?:\$tc?s?|\$has|\btc?s?)\s*\(\s*(?:['"`]([^'"`$]+)['"`]|([^'"`)\s][^)]*))/g;
420
+ var QUOTED = /['"`]([^'"`\n]+)['"`]/g;
421
+ /** For what no parser here reads, such as Pug templates: finding a key too many is the safe mistake. */
422
+ function collectFromText(text, context, base, bareCalls = true) {
423
+ const { content, file, scan } = context;
424
+ for (const match of text.matchAll(TEXT_CALL)) {
425
+ const line = lineOf(content, base + match.index);
426
+ if (match[1]) scan.refs.push({
427
+ key: match[1],
428
+ file,
429
+ line,
430
+ start: -1,
431
+ end: -1,
432
+ guard: false
433
+ });
434
+ else if (bareCalls || match[0][0] === "$") scan.dynamic.push({
435
+ file,
436
+ line,
437
+ expression: match[2].trim(),
438
+ prefix: ""
439
+ });
440
+ }
441
+ for (const match of text.matchAll(QUOTED)) addLiteral(match[1], scan, `${file}:${lineOf(content, base + match.index)}`);
442
+ }
443
+ function unproven(context, text, base, reason) {
444
+ context.scan.unproven.push(reason);
445
+ collectFromText(text, context, base);
446
+ }
447
+ function scanExpression(expression, offset, context) {
448
+ const wrapped = parseCode(`(${expression}\n)`, "ts", true);
449
+ const ast = wrapped ?? parseCode(expression);
450
+ if (!ast) {
451
+ unproven(context, expression, offset, `unparsed template expression at line ${lineOf(context.content, offset)}`);
452
+ return;
453
+ }
454
+ visitCode(ast, {
455
+ ...context,
456
+ base: wrapped ? offset - 1 : offset
457
+ });
458
+ }
459
+ function addKeypathAttribute(prop, context) {
460
+ const { file, content, scan } = context;
461
+ const { value } = prop;
462
+ const quote = value.loc.source[0] === "'" || value.loc.source[0] === "\"" ? 1 : 0;
463
+ const start = value.loc.start.offset + quote;
464
+ scan.refs.push({
465
+ key: value.content,
466
+ file,
467
+ line: lineOf(content, start),
468
+ start,
469
+ end: start + value.content.length,
470
+ guard: false
471
+ });
472
+ }
473
+ function addGroupPrefix(prop, tag, context) {
474
+ const { file, content, scan } = context;
475
+ const line = lineOf(content, prop.value.loc.start.offset);
476
+ scan.dynamic.push({
477
+ file,
478
+ line,
479
+ expression: `<${tag} prefix="${prop.value.content}">`,
480
+ prefix: prop.value.content,
481
+ group: true
482
+ });
483
+ }
484
+ function readAttribute(prop, tag, context) {
485
+ if (!prop.value) return;
486
+ if (prop.name === "keypath" && isKeyTag(tag)) addKeypathAttribute(prop, context);
487
+ else if (prop.name === "prefix" && isGroupTag(tag)) addGroupPrefix(prop, tag, context);
488
+ else addLiteral(prop.value.content, context.scan, `${context.file}:${lineOf(context.content, prop.value.loc.start.offset)}`);
489
+ }
490
+ /** `v-slot="{ t: tr }"` of an `<i18n-group>`: what the block calls the scoped `t`. */
491
+ function readSlotNames(expression, group) {
492
+ const ast = parseCode(`(${expression}\n)=>0`);
493
+ if (!ast) return;
494
+ walk(ast, { enter(raw) {
495
+ const node = raw;
496
+ if (node.type !== "ObjectProperty" || asNode(node.key)?.name !== "t") return;
497
+ const local = asNode(node.value);
498
+ if (local?.type === "Identifier") group.names.add(local.name);
499
+ } });
500
+ }
501
+ /** `<i18n-t :keypath />` and `<i18n-t v-bind="p" />`: the key is somewhere the template does not show. */
502
+ function readHiddenKey(prop, tag, context) {
503
+ const translates = isTranslationTag(tag);
504
+ const target = prop.arg?.content;
505
+ const hidden = prop.name === "bind" && (target === void 0 || !prop.exp?.content && (target === "keypath" || target === "prefix"));
506
+ if (!translates || !hidden) return;
507
+ const { content, file, scan } = context;
508
+ const line = lineOf(content, prop.loc.start.offset);
509
+ scan.dynamic.push({
510
+ file,
511
+ line,
512
+ expression: `<${tag} ${prop.loc.source}>`,
513
+ prefix: ""
514
+ });
515
+ }
516
+ function readDirective(prop, tag, context) {
517
+ readHiddenKey(prop, tag, context);
518
+ const expression = prop.forParseResult?.source ?? prop.exp;
519
+ if (!expression?.content) return;
520
+ const start = expression.loc.start.offset;
521
+ if (prop.name === "slot") {
522
+ if (context.group) readSlotNames(expression.content, context.group);
523
+ return;
524
+ }
525
+ const argument = prop.arg?.content;
526
+ if (argument === "keypath" && isKeyTag(tag)) {
527
+ const ast = parseCode(`$t(${expression.content}\n)`);
528
+ if (ast) visitCode(ast, {
529
+ ...context,
530
+ base: start - 3
531
+ });
532
+ else unproven(context, expression.content, start, `unparsed :keypath at line ${lineOf(context.content, start)}`);
533
+ return;
534
+ }
535
+ if (argument === "prefix" && isGroupTag(tag)) {
536
+ const line = lineOf(context.content, start);
537
+ context.scan.dynamic.push({
538
+ file: context.file,
539
+ line,
540
+ expression: expression.content,
541
+ prefix: ""
542
+ });
543
+ }
544
+ scanExpression(expression.content, start, context);
545
+ }
546
+ /** `<component :is="'i18n-t'">` is an `<i18n-t>`, and so is a dynamic `is` that gets a `keypath`. */
547
+ function tagOf(node) {
548
+ if (node.tag !== "component") return node.tag ?? "";
549
+ const is = node.props?.find((prop) => prop.name === "is" || prop.arg?.content === "is");
550
+ const named = (is?.value?.content ?? is?.exp?.content ?? "").replace(/^['"`]|['"`]$/g, "");
551
+ return node.props?.some((prop) => prop.name === "keypath" || prop.arg?.content === "keypath") ? "i18n-t" : named;
552
+ }
553
+ function groupOf(node, context) {
554
+ if (!isGroupTag(tagOf(node))) return context;
555
+ const prefix = node.props?.find((prop) => prop.type === NODE_TYPES.ATTRIBUTE && prop.name === "prefix")?.value?.content;
556
+ return prefix ? {
557
+ ...context,
558
+ group: {
559
+ prefix,
560
+ names: /* @__PURE__ */ new Set()
561
+ }
562
+ } : context;
563
+ }
564
+ function visitTemplate(node, outer) {
565
+ if (node.type === NODE_TYPES.INTERPOLATION && node.content?.content) scanExpression(node.content.content, node.content.loc.start.offset, outer);
566
+ const context = node.type === NODE_TYPES.ELEMENT ? groupOf(node, outer) : outer;
567
+ if (node.type === NODE_TYPES.ELEMENT) for (const prop of node.props ?? []) {
568
+ if (prop.type === NODE_TYPES.ATTRIBUTE) readAttribute(prop, tagOf(node), context);
569
+ if (prop.type === NODE_TYPES.DIRECTIVE) readDirective(prop, tagOf(node), context);
570
+ }
571
+ for (const child of node.children ?? []) visitTemplate(child, context);
572
+ }
573
+ function scanVue(content, context) {
574
+ const { descriptor, errors } = parse$1(content, { filename: context.file });
575
+ const { template, script, scriptSetup } = descriptor;
576
+ if (errors.length > 0) context.scan.unproven.push(`SFC parse error: ${String(errors[0].message)}`);
577
+ const blocks = [script, scriptSetup].flatMap((block) => block ? [{
578
+ block,
579
+ ast: parseCode(block.content, block.lang ?? "ts")
580
+ }] : []);
581
+ for (const { ast } of blocks) if (ast) readNames(ast, context.names);
582
+ for (const { block, ast } of blocks) if (ast) visitCode(ast, {
583
+ ...context,
584
+ base: block.loc.start.offset
585
+ });
586
+ else unproven(context, block.content, block.loc.start.offset, `unparsed <script> in ${basename(context.file)}`);
587
+ if (template?.ast && (!template.lang || template.lang === "html")) visitTemplate(template.ast, context);
588
+ else if (template) collectFromText(template.content, context, template.loc.start.offset);
589
+ }
590
+ function scanCode(content, context) {
591
+ const ast = parseCode(content, extname(context.file).slice(1).toLowerCase());
592
+ if (!ast) {
593
+ unproven(context, content, 0, `parse error in ${basename(context.file)}`);
594
+ return;
595
+ }
596
+ readNames(ast, context.names);
597
+ visitCode(ast, context);
598
+ }
599
+ /** Every use of a translation key in one file, with positions. Comments do not count. */
600
+ function scanFile(content, file) {
601
+ const scan = {
602
+ refs: [],
603
+ dynamic: [],
604
+ literals: /* @__PURE__ */ new Map(),
605
+ prefixes: /* @__PURE__ */ new Set(),
606
+ unproven: []
607
+ };
608
+ const context = {
609
+ content,
610
+ file,
611
+ scan,
612
+ names: newNames(content),
613
+ base: 0,
614
+ group: null,
615
+ keyNodes: /* @__PURE__ */ new WeakSet()
616
+ };
617
+ const extension = extname(file).toLowerCase();
618
+ if (content.trim() === "") return scan;
619
+ if (extension === ".vue") scanVue(content, context);
620
+ else if (CODE_EXTENSIONS.has(extension)) scanCode(content, context);
621
+ else collectFromText(content, context, 0, false);
622
+ return scan;
623
+ }
624
+ var SEGMENT_TOKEN = /\[{1,2}(?:\.\.\.)?([^\]]+)\]{1,2}/g;
625
+ /** Route name Nuxt gives a page file: the name of its `locales/pages/<name>/` directory. */
626
+ function pageNameOf(pagePath) {
627
+ const segments = forwardSlashes(pagePath).replace(/\.[^./]+$/, "").replace(/\.(client|server)$/, "").split("/").filter((segment) => !(segment.length > 1 && segment[0] === "(" && segment.endsWith(")"))).map((segment) => segment.replace(SEGMENT_TOKEN, "$1"));
628
+ if (segments.at(-1) === "index" && segments.length > 1) segments.pop();
629
+ return segments.join("-") || "index";
630
+ }
631
+ var PAGE_META_NAME = /definePageMeta\(\s*\{[^}]*?\bname:\s*['"`]([^'"`]+)['"`]/;
632
+ function pageScopeOf(file, pagesRoot, content) {
633
+ return content.match(PAGE_META_NAME)?.[1] ?? pageNameOf(relative(pagesRoot, file));
634
+ }
635
+ var DATA_TOKEN = /[\p{L}\p{N}_$-]+(?:\.[\p{L}\p{N}_$-]+)*/gu;
636
+ /** Evidence only, never saved: a stray byte in a notes file must not stop anything. */
637
+ var dataTokensOf = (path) => Array.from(readLoose(path).matchAll(DATA_TOKEN), (match) => match[0]);
638
+ function collectData(path, scan) {
639
+ scan.dataFiles.push(path);
640
+ for (const token of dataTokensOf(path)) scan.dataTokens.add(token);
641
+ }
642
+ /** Everything of the project a key can hide in, once each, whatever folder it lives in. */
643
+ function projectFiles(config, options = {}) {
644
+ const excluded = new Set(config.translationDirs);
645
+ const ignored = (options.ignore ?? []).map(globToRegExp);
646
+ const isIgnored = (path) => {
647
+ const rel = relativePath(config.rootDir, path);
648
+ return ignored.some((pattern) => pattern.test(rel));
649
+ };
650
+ const accept = (path) => {
651
+ const extension = extname(path).toLowerCase();
652
+ return (CODE_EXTENSIONS.has(extension) || TEMPLATE_EXTENSIONS.has(extension) || DATA_EXTENSIONS.has(extension)) && !path.endsWith(".d.ts") && !SKIPPED_DATA.test(basename(path)) && !isIgnored(path);
653
+ };
654
+ const roots = [.../* @__PURE__ */ new Set([
655
+ config.rootDir,
656
+ config.srcDir,
657
+ ...config.layerRoots,
658
+ ...config.extraSourceDirs
659
+ ])];
660
+ const skipDir = (path) => SKIPPED_DIRS.has(basename(path)) || OUTPUT_DIRS.has(basename(path)) && roots.includes(dirname(path)) || excluded.has(path) || isIgnored(path);
661
+ const seen = new Set(config.translationDirs.flatMap((dir) => walkFiles(dir, (path) => path.endsWith(".json")).map((file) => realpathSync(file))));
662
+ return roots.flatMap((root) => walkFiles(root, accept, skipDir)).filter((file) => {
663
+ const real = realpathSync(file);
664
+ return seen.has(real) ? false : Boolean(seen.add(real));
665
+ });
666
+ }
667
+ var inTestDir = (config, file) => TEST_DIR.test(relativePath(config.rootDir, file));
668
+ var isTestFile = (config, file) => inTestDir(config, file) || TEST_NAME.test(file);
669
+ var AVERAGE_LINE_OF_A_BUNDLE = 1e3;
670
+ var isMinified = (file, content) => file.endsWith(".min.js") || file.endsWith(".min.mjs") || file.endsWith(".min.cjs") || content.length / content.split("\n").length > AVERAGE_LINE_OF_A_BUNDLE;
671
+ var isServedAsIs = (config, file, content) => [config.rootDir, config.srcDir].some((root) => relativePath(root, file).startsWith("public/")) || isMinified(file, content);
672
+ function addFile(file, config, out) {
673
+ const content = readFileSync(file, "utf8");
674
+ const found = scanFile(content, file);
675
+ const pagesRoot = config.pagesDirs.find((dir) => isUnder(dir, file));
676
+ const page = pagesRoot && isCodeFile(file) ? pageScopeOf(file, pagesRoot, content) : null;
677
+ const test = !page && isTestFile(config, file);
678
+ for (const ref of found.refs) out.refs.push({
679
+ ...ref,
680
+ page,
681
+ test
682
+ });
683
+ for (const [text, locations] of found.literals) out.literals.set(text, [...out.literals.get(text) ?? [], ...locations]);
684
+ for (const prefix of found.prefixes) out.prefixes.add(prefix);
685
+ out.files += 1;
686
+ if (!page && (inTestDir(config, file) || isServedAsIs(config, file, content))) return;
687
+ const generated = !page && GENERATED_PATH.test(relativePath(config.rootDir, file));
688
+ out.dynamic.push(...found.dynamic.filter((entry) => !(generated && entry.guess)));
689
+ for (const reason of found.unproven) out.unproven.push({
690
+ file,
691
+ reason
692
+ });
693
+ }
694
+ /**
695
+ * Whether a write may touch this source. The scan reads layers and alias targets to prove use;
696
+ * they belong to whoever else builds on them, and their locale files stay as they are.
697
+ */
698
+ function isOwnSource(config, file) {
699
+ const real = realpathSync(file);
700
+ const project = realpathSync(config.rootDir);
701
+ const nested = config.layerRoots.filter((root) => existsSync(root)).map((root) => realpathSync(root)).filter((root) => root !== project && isUnder(project, root));
702
+ return isUnder(project, real) && !forwardSlashes(real.slice(project.length)).includes("/node_modules/") && !nested.some((root) => isUnder(root, real));
703
+ }
704
+ function scanProject(config, options = {}) {
705
+ const out = {
706
+ refs: [],
707
+ dynamic: [],
708
+ literals: /* @__PURE__ */ new Map(),
709
+ prefixes: /* @__PURE__ */ new Set(),
710
+ dataTokens: /* @__PURE__ */ new Set(),
711
+ dataFiles: [],
712
+ unproven: [],
713
+ files: 0
714
+ };
715
+ for (const file of projectFiles(config, options)) if (DATA_EXTENSIONS.has(extname(file).toLowerCase())) collectData(file, out);
716
+ else addFile(file, config, out);
717
+ return out;
718
+ }
719
+ var parentsOf = (key) => {
720
+ const parts = key.split(".");
721
+ return parts.slice(1).map((_part, index) => parts.slice(0, index + 1).join("."));
722
+ };
723
+ /**
724
+ * Whether a key may be read. One answer for `clean`, `check` and `search`.
725
+ *
726
+ * Any trace counts, not only a call: a string equal to the key can reach `$t` through a
727
+ * variable, a prop or a data file. A dead key that stays is a line of noise; a live key that
728
+ * goes is a broken page.
729
+ */
730
+ function usageIndex(scan) {
731
+ const called = new Set(scan.refs.map((ref) => ref.key));
732
+ const passedThrough = new Set(scan.refs.flatMap((ref) => parentsOf(ref.key)));
733
+ const prefixes = [...scan.prefixes, ...scan.dynamic.map((entry) => entry.prefix).filter(Boolean)];
734
+ return (key) => called.has(key) || passedThrough.has(key) || scan.literals.has(key) || scan.dataTokens.has(key) || parentsOf(key).some((parent) => called.has(parent)) || prefixes.some((prefix) => key.startsWith(prefix));
735
+ }
736
+ /** Why "no source file uses this key" cannot be trusted in this project. Empty when it can. */
737
+ function usageDoubts(scan) {
738
+ return [
739
+ ...scan.dynamic.filter((entry) => !entry.prefix).map(({ file, line, expression }) => `Key computed at runtime in ${file}:${line}: ${expression}`),
740
+ ...scan.unproven.map(({ file, reason }) => `Could not read ${file}: ${reason}`),
741
+ ...scan.refs.length === 0 ? ["No source file uses any key: the scan is probably looking in the wrong place."] : []
742
+ ];
743
+ }
744
+ export { isTestFile as a, projectFiles as c, staticString as d, usageDoubts as f, isOwnSource as i, scanFile as l, dataTokensOf as n, lineOf as o, usageIndex as p, isCodeFile as r, parseCode as s, NODE_TYPES as t, scanProject as u };