@x18n/builtins 0.0.2 → 0.0.5

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.
@@ -0,0 +1,727 @@
1
+ //#region src/scanners/js.ts
2
+ const hardStringIgnoredAttributes = /* @__PURE__ */ new Set([
3
+ "class",
4
+ "className",
5
+ "style",
6
+ "key",
7
+ "ref",
8
+ "id",
9
+ "href",
10
+ "src",
11
+ "type",
12
+ "name",
13
+ "for",
14
+ "htmlFor",
15
+ "role",
16
+ "slot",
17
+ "as",
18
+ "to",
19
+ "target",
20
+ "rel",
21
+ "lang",
22
+ "xmlns",
23
+ "viewBox",
24
+ "d",
25
+ "fill",
26
+ "stroke"
27
+ ]);
28
+ function isNode(value) {
29
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string" && "start" in value;
30
+ }
31
+ function isStringLiteral(node) {
32
+ return node?.type === "Literal" && typeof node.value === "string";
33
+ }
34
+ /** Inner range of a quoted literal. */
35
+ function innerRange(node) {
36
+ return {
37
+ start: node.start + 1,
38
+ end: node.end - 1
39
+ };
40
+ }
41
+ function unwrap(node) {
42
+ let current = node;
43
+ while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression" || current.type === "ParenthesizedExpression" || current.type === "AwaitExpression" || current.type === "ChainExpression") current = current.type === "AwaitExpression" ? current.argument : current.expression;
44
+ return current;
45
+ }
46
+ /** `a.b.c` for identifier and member chains, `this.` dropped; undefined for anything else. */
47
+ function calleeName(node) {
48
+ const target = unwrap(node);
49
+ if (target.type === "Identifier") return target.name;
50
+ if (target.type === "ThisExpression") return "this";
51
+ if (target.type === "MemberExpression" && !target.computed && target.property.type === "Identifier") {
52
+ const object = calleeName(target.object);
53
+ if (object === void 0) return void 0;
54
+ return object === "this" ? target.property.name : `${object}.${target.property.name}`;
55
+ }
56
+ }
57
+ function jsxName(node) {
58
+ if (node.type === "JSXIdentifier") return node.name;
59
+ if (node.type === "JSXMemberExpression") {
60
+ const object = node.object.type === "JSXIdentifier" ? node.object.name : node.object.type === "JSXMemberExpression" ? jsxName(node.object) : void 0;
61
+ return object ? `${object}.${node.property.name}` : void 0;
62
+ }
63
+ }
64
+ function propertyName(node) {
65
+ if (node.type === "Identifier") return node.name;
66
+ if (isStringLiteral(node)) return node.value;
67
+ }
68
+ function objectProperty(object, name) {
69
+ for (const property of object.properties) if (property.type === "Property" && !property.computed && propertyName(property.key) === name) return property.value;
70
+ }
71
+ var FileScan = class {
72
+ text;
73
+ offset;
74
+ collectHardStrings;
75
+ ignoredCallees;
76
+ usages = [];
77
+ keySlots = [];
78
+ hardStrings = [];
79
+ frames = [/* @__PURE__ */ new Map()];
80
+ constants = /* @__PURE__ */ new Map();
81
+ /** Several specs may share a callee, for example lingui `t('id')` and `t({ id })`. */
82
+ declared = /* @__PURE__ */ new Map();
83
+ factories = /* @__PURE__ */ new Map();
84
+ typed = /* @__PURE__ */ new Map();
85
+ consumed = /* @__PURE__ */ new Set();
86
+ constructor(text, offset, calls, collectHardStrings, ignoredCallees = /* @__PURE__ */ new Set()) {
87
+ this.text = text;
88
+ this.offset = offset;
89
+ this.collectHardStrings = collectHardStrings;
90
+ this.ignoredCallees = ignoredCallees;
91
+ for (const spec of calls) for (const name of [spec.callee].flat()) if (spec.factory) {
92
+ const list = this.factories.get(name) ?? [];
93
+ this.factories.set(name, list);
94
+ list.push(spec);
95
+ } else if (spec.types?.length) {
96
+ for (const type of spec.types) this.typed.set(type, spec);
97
+ this.declare(name, spec);
98
+ } else this.declare(name, spec);
99
+ }
100
+ range(range) {
101
+ return {
102
+ start: range.start + this.offset,
103
+ end: range.end + this.offset
104
+ };
105
+ }
106
+ declare(name, spec) {
107
+ const list = this.declared.get(name) ?? [];
108
+ this.declared.set(name, list);
109
+ list.push(spec);
110
+ }
111
+ lookup(name) {
112
+ return this.lookupAll(name)[0];
113
+ }
114
+ lookupAll(name) {
115
+ for (let i = this.frames.length - 1; i >= 0; i--) {
116
+ const binding = this.frames[i].get(name);
117
+ if (binding) return [binding];
118
+ }
119
+ const specs = this.declared.get(name);
120
+ if (specs) return specs.map((spec) => ({ spec }));
121
+ const dot = name.indexOf(".");
122
+ if (dot > 0) {
123
+ const root = this.lookup(name.slice(0, dot));
124
+ if (root?.spec.returns?.some((ret) => ret.endsWith(name.slice(dot)))) return [root];
125
+ }
126
+ return [];
127
+ }
128
+ bind(name, binding) {
129
+ this.frames.at(-1).set(name, binding);
130
+ }
131
+ /** ---- Reading arguments ---- */
132
+ stringArg(value) {
133
+ if (!value) return void 0;
134
+ if ("props" in value) return void 0;
135
+ const node = unwrap(value);
136
+ if (isStringLiteral(node)) return node.value;
137
+ if (node.type === "ArrayExpression" && node.elements[0] && isStringLiteral(node.elements[0])) return node.elements[0].value;
138
+ if (node.type === "JSXExpressionContainer" && node.expression.type !== "JSXEmptyExpression") return this.stringArg(node.expression);
139
+ if (node.type === "Identifier") {
140
+ const constant = this.constants.get(node.name);
141
+ return constant && isStringLiteral(constant) ? constant.value : void 0;
142
+ }
143
+ }
144
+ argValues(args, source, fallback) {
145
+ const arg = args[typeof source === "number" ? source : source?.arg ?? fallback];
146
+ const property = typeof source === "object" ? source.property : void 0;
147
+ if (!arg || property === void 0) return arg ? [arg] : [];
148
+ return property.split(".").reduce((values, segment) => values.flatMap((value) => {
149
+ if ("props" in value) {
150
+ const prop = value.props.get(segment);
151
+ return prop ? [prop] : [];
152
+ }
153
+ const node = unwrap(value);
154
+ if (node.type === "JSXExpressionContainer" && node.expression.type === "ObjectExpression") return segment === "*" ? this.allValues(node.expression) : [objectProperty(node.expression, segment)].filter(isNode);
155
+ if (node.type !== "ObjectExpression") return [];
156
+ return segment === "*" ? this.allValues(node) : [objectProperty(node, segment)].filter(isNode);
157
+ }), [arg]);
158
+ }
159
+ allValues(object) {
160
+ return object.properties.flatMap((p) => p.type === "Property" ? [p.value] : []);
161
+ }
162
+ /** ---- Usages ---- */
163
+ addKey(value, binding, namespace) {
164
+ if ("props" in value) return;
165
+ const node = unwrap(value);
166
+ const base = {
167
+ namespace: namespace ?? binding.namespace,
168
+ keyPrefix: binding.keyPrefix
169
+ };
170
+ if (isStringLiteral(node)) {
171
+ this.consumed.add(node.start);
172
+ this.keySlots.push(this.range(innerRange(node)));
173
+ this.usages.push({
174
+ ...base,
175
+ raw: node.value,
176
+ range: this.range(innerRange(node)),
177
+ kind: "call",
178
+ dynamic: false
179
+ });
180
+ return;
181
+ }
182
+ if (node.type === "JSXExpressionContainer") {
183
+ if (node.expression.type !== "JSXEmptyExpression") this.addKey(node.expression, binding, namespace);
184
+ return;
185
+ }
186
+ if (node.type === "TemplateLiteral") {
187
+ const first = node.quasis[0];
188
+ const raw = first.value.cooked ?? first.value.raw;
189
+ this.consumed.add(node.start);
190
+ const range = this.range({
191
+ start: node.start + 1,
192
+ end: node.start + 1 + raw.length
193
+ });
194
+ this.usages.push({
195
+ ...base,
196
+ raw,
197
+ range,
198
+ kind: "call",
199
+ dynamic: node.expressions.length === 0 ? false : raw ? "prefix" : "unknown"
200
+ });
201
+ return;
202
+ }
203
+ if (node.type === "ConditionalExpression") {
204
+ this.addKey(node.consequent, binding, namespace);
205
+ this.addKey(node.alternate, binding, namespace);
206
+ return;
207
+ }
208
+ if (node.type === "ArrowFunctionExpression" && node.params[0]?.type === "Identifier") return this.addSelector(node.params[0].name, node.body, base);
209
+ const constant = node.type === "Identifier" ? this.constants.get(node.name) : node.type === "MemberExpression" ? this.memberConstant(node) : void 0;
210
+ if (constant && isStringLiteral(constant)) {
211
+ this.usages.push({
212
+ ...base,
213
+ raw: constant.value,
214
+ range: this.range(innerRange(constant)),
215
+ kind: "constant",
216
+ dynamic: false
217
+ });
218
+ return;
219
+ }
220
+ this.usages.push({
221
+ ...base,
222
+ raw: this.text.slice(node.start, node.end),
223
+ range: this.range({
224
+ start: node.start,
225
+ end: node.end
226
+ }),
227
+ kind: "call",
228
+ dynamic: "unknown"
229
+ });
230
+ }
231
+ /** i18next `t($ => $.a.b)`. */
232
+ addSelector(param, body, base) {
233
+ const segments = [];
234
+ let node = unwrap(body);
235
+ let end = node.end;
236
+ while (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") {
237
+ segments.unshift(node.property.name);
238
+ node = unwrap(node.object);
239
+ }
240
+ if (node.type !== "Identifier" || node.name !== param || segments.length === 0) return;
241
+ const start = node.end + 1;
242
+ end = Math.max(end, start);
243
+ this.usages.push({
244
+ ...base,
245
+ raw: segments.join("."),
246
+ range: this.range({
247
+ start,
248
+ end
249
+ }),
250
+ kind: "selector",
251
+ dynamic: false
252
+ });
253
+ }
254
+ memberConstant(node) {
255
+ if (node.computed || node.property.type !== "Identifier" || node.object.type !== "Identifier") return void 0;
256
+ const object = this.constants.get(node.object.name);
257
+ return object && object.type === "ObjectExpression" ? objectProperty(object, node.property.name) : void 0;
258
+ }
259
+ handleCall(name, args) {
260
+ const bindings = this.lookupAll(name).filter((binding) => !binding.spec.factory);
261
+ for (const binding of bindings) {
262
+ const namespace = binding.spec.namespace ? this.stringArg(this.argValues(args, binding.spec.namespace, 1)[0]) : void 0;
263
+ for (const value of this.argValues(args, binding.spec.key, 0)) {
264
+ if (bindings.length > 1 && !("props" in value) && unwrap(value).type === "ObjectExpression") continue;
265
+ this.addKey(value, binding, namespace);
266
+ }
267
+ }
268
+ return bindings.length > 0;
269
+ }
270
+ call(node) {
271
+ const name = calleeName(node.callee);
272
+ if (name !== void 0) this.handleCall(name, node.arguments);
273
+ }
274
+ jsx(node) {
275
+ const name = jsxName(node.name);
276
+ if (!name || !this.lookup(name)) return;
277
+ const props = /* @__PURE__ */ new Map();
278
+ for (const attribute of node.attributes) if (attribute.type === "JSXAttribute" && attribute.value && attribute.name.type === "JSXIdentifier") {
279
+ props.set(attribute.name.name, attribute.value);
280
+ if (isStringLiteral(attribute.value)) this.consumed.add(attribute.value.start);
281
+ }
282
+ this.handleCall(name, [{ props }]);
283
+ }
284
+ /** ---- Bindings ---- */
285
+ declarator(id, init) {
286
+ if (!init) return;
287
+ const value = unwrap(init);
288
+ if (id.type === "Identifier" && (isStringLiteral(value) || value.type === "ObjectExpression")) this.constants.set(id.name, value);
289
+ if (value.type === "CallExpression") {
290
+ const factory = calleeName(value.callee);
291
+ const specs = factory ? this.factories.get(factory) : void 0;
292
+ if (specs) this.bindFactory(id, value, specs);
293
+ return;
294
+ }
295
+ if (id.type === "Identifier" && (value.type === "Identifier" || value.type === "MemberExpression")) {
296
+ const name = calleeName(value);
297
+ const binding = name ? this.lookup(name) : void 0;
298
+ if (binding) this.bind(id.name, binding);
299
+ }
300
+ }
301
+ /** The first factory spec whose namespace and prefix sources all resolve, else the first spec. */
302
+ pickFactory(call, specs) {
303
+ const resolved = specs.map((spec) => {
304
+ const namespace = spec.namespace ? this.stringArg(this.argValues(call.arguments, spec.namespace, 0)[0]) : void 0;
305
+ const keyPrefix = spec.keyPrefix ? this.stringArg(this.argValues(call.arguments, spec.keyPrefix, 0)[0]) : void 0;
306
+ const complete = (!spec.namespace || namespace !== void 0) && (!spec.keyPrefix || keyPrefix !== void 0);
307
+ return {
308
+ binding: {
309
+ spec: {
310
+ callee: spec.callee,
311
+ returns: spec.returns
312
+ },
313
+ namespace,
314
+ keyPrefix
315
+ },
316
+ complete
317
+ };
318
+ });
319
+ return (resolved.find((r) => r.complete) ?? resolved[0]).binding;
320
+ }
321
+ bindFactory(id, call, specs) {
322
+ const binding = this.pickFactory(call, specs);
323
+ const returns = (binding.spec.returns ?? ["t"]).filter((name) => !name.includes("."));
324
+ if (id.type === "Identifier") {
325
+ this.bind(id.name, binding);
326
+ return;
327
+ }
328
+ if (id.type !== "ObjectPattern") return;
329
+ for (const property of id.properties) {
330
+ if (property.type !== "Property") continue;
331
+ const key = propertyName(property.key);
332
+ const local = unwrap(property.value);
333
+ if (key && returns.includes(key) && local.type === "Identifier") this.bind(local.name, binding);
334
+ if (key && local.type === "Identifier" && this.declared.has(`${key}.t`)) this.bind(`${local.name}.t`, binding);
335
+ }
336
+ }
337
+ params(params) {
338
+ for (const param of params) {
339
+ const target = param.type === "TSParameterProperty" ? param.parameter : param;
340
+ if (target.type !== "Identifier" || !target.typeAnnotation) continue;
341
+ const annotation = target.typeAnnotation.typeAnnotation;
342
+ const typeName = annotation.type === "TSTypeReference" && annotation.typeName.type === "Identifier" ? annotation.typeName.name : void 0;
343
+ const spec = typeName ? this.typed.get(typeName) : void 0;
344
+ if (spec) this.bind(target.name, { spec: {
345
+ ...spec,
346
+ types: void 0
347
+ } });
348
+ }
349
+ }
350
+ importSpecifier(node) {
351
+ const imported = node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
352
+ const spec = this.declared.get(imported)?.[0];
353
+ if (spec && imported !== node.local.name) this.bind(node.local.name, { spec });
354
+ }
355
+ /** ---- Hard strings ---- */
356
+ hard(text, range, kind, attribute) {
357
+ if (!this.collectHardStrings || !/\p{L}/u.test(text)) return;
358
+ this.hardStrings.push({
359
+ text,
360
+ range: this.range(range),
361
+ kind,
362
+ ...attribute ? { attribute } : {}
363
+ });
364
+ }
365
+ ignoredCall(name) {
366
+ return name !== void 0 && (name.startsWith("console.") || name === "require" || this.ignoredCallees.has(name));
367
+ }
368
+ /** ---- Walk ---- */
369
+ walk(node, parent, parentKey) {
370
+ const scoped = node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "BlockStatement";
371
+ if (scoped) this.frames.push(/* @__PURE__ */ new Map());
372
+ this.visit(node, parent, parentKey);
373
+ for (const [key, child] of Object.entries(node)) {
374
+ if (key === "parent") continue;
375
+ if (Array.isArray(child)) {
376
+ for (const item of child) if (isNode(item)) this.walk(item, node, key);
377
+ } else if (isNode(child)) this.walk(child, node, key);
378
+ }
379
+ if (scoped) this.frames.pop();
380
+ }
381
+ visit(node, parent, parentKey) {
382
+ switch (node.type) {
383
+ case "CallExpression":
384
+ this.call(node);
385
+ break;
386
+ case "JSXOpeningElement":
387
+ this.jsx(node);
388
+ break;
389
+ case "VariableDeclarator":
390
+ this.declarator(node.id, node.init);
391
+ break;
392
+ case "FunctionDeclaration":
393
+ case "FunctionExpression":
394
+ case "ArrowFunctionExpression":
395
+ this.params(node.params);
396
+ break;
397
+ case "ImportSpecifier":
398
+ this.importSpecifier(node);
399
+ break;
400
+ case "JSXText": {
401
+ const text = node.value.trim();
402
+ if (text) this.hard(text, {
403
+ start: node.start + node.value.indexOf(text),
404
+ end: node.start + node.value.indexOf(text) + text.length
405
+ }, "jsx-text");
406
+ break;
407
+ }
408
+ case "Literal":
409
+ case "TemplateLiteral": this.literal(node, parent, parentKey);
410
+ }
411
+ }
412
+ literal(node, parent, parentKey) {
413
+ if (this.consumed.has(node.start) || !parent) return;
414
+ if (parent.type === "JSXAttribute") {
415
+ const name = parent.name.type === "JSXIdentifier" ? parent.name.name : "";
416
+ if (isStringLiteral(node) && !hardStringIgnoredAttributes.has(name) && !name.startsWith("data-") && !name.startsWith("aria-")) this.hard(node.value, {
417
+ start: parent.start,
418
+ end: parent.end
419
+ }, "jsx-attribute", name);
420
+ return;
421
+ }
422
+ if (parent.type === "ImportDeclaration" || parent.type === "ExportNamedDeclaration" || parent.type === "ExportAllDeclaration" || parent.type === "TSLiteralType" || parent.type === "ImportExpression" || parent.type === "TSExternalModuleReference" || parent.type === "TSModuleDeclaration" || parent.type === "Property" && parentKey === "key" || parent.type === "MemberExpression" && parentKey === "property" || parent.type === "ExpressionStatement" || parent.type === "BinaryExpression" || parent.type === "SwitchCase" || parent.type === "CallExpression" && this.ignoredCall(calleeName(parent.callee))) return;
423
+ if (isStringLiteral(node)) this.hard(node.value, {
424
+ start: node.start,
425
+ end: node.end
426
+ }, "js-string");
427
+ else if (node.type === "TemplateLiteral") this.template(node);
428
+ }
429
+ /** `Hello ${user.name}` becomes `Hello {0}` with the argument `name: user.name`. */
430
+ template(node) {
431
+ if (!this.collectHardStrings) return;
432
+ const args = node.expressions.map((expression, i) => {
433
+ const source = this.text.slice(expression.start, expression.end);
434
+ return {
435
+ name: expression.type === "Identifier" ? expression.name : expression.type === "MemberExpression" && expression.property.type === "Identifier" ? expression.property.name : `arg${i}`,
436
+ expression: source
437
+ };
438
+ });
439
+ const text = node.quasis.map((q, i) => `${q.value.cooked ?? q.value.raw}${i < args.length ? `{${i}}` : ""}`).join("");
440
+ if (/\p{L}/u.test(text)) this.hardStrings.push({
441
+ text,
442
+ range: this.range({
443
+ start: node.start,
444
+ end: node.end
445
+ }),
446
+ kind: "js-template",
447
+ args
448
+ });
449
+ }
450
+ markers(program, comments) {
451
+ for (const comment of comments) {
452
+ const marker = /@x18n-keys?\b/.exec(comment.value)?.[0];
453
+ if (!marker) continue;
454
+ const next = this.firstAfter(program, comment.end);
455
+ if (!next) continue;
456
+ const literals = marker === "@x18n-keys" ? this.literalsIn(next) : isStringLiteral(next) ? [next] : [];
457
+ for (const literal of literals) if (isStringLiteral(literal)) this.usages.push({
458
+ raw: literal.value,
459
+ range: this.range(innerRange(literal)),
460
+ kind: "marker",
461
+ dynamic: false
462
+ });
463
+ }
464
+ }
465
+ firstAfter(root, offset) {
466
+ let best;
467
+ const visit = (node) => {
468
+ if (node.end <= offset || best && node.start > best.start) return;
469
+ if (node.start >= offset && (node.type === "Literal" || node.type === "ArrayExpression" || node.type === "ObjectExpression")) {
470
+ if (!best || node.start < best.start) best = node;
471
+ return;
472
+ }
473
+ for (const [key, child] of Object.entries(node)) {
474
+ if (key === "parent") continue;
475
+ for (const item of [child].flat()) if (isNode(item)) visit(item);
476
+ }
477
+ };
478
+ visit(root);
479
+ return best;
480
+ }
481
+ literalsIn(node) {
482
+ if (isStringLiteral(node)) return [node];
483
+ if (node.type === "ArrayExpression") return node.elements.flatMap((element) => element ? this.literalsIn(element) : []);
484
+ if (node.type === "ObjectExpression") return this.allValues(node).flatMap((value) => this.literalsIn(value));
485
+ return [];
486
+ }
487
+ result() {
488
+ return {
489
+ usages: this.usages,
490
+ keySlots: this.keySlots,
491
+ hardStrings: this.hardStrings
492
+ };
493
+ }
494
+ };
495
+ let parser;
496
+ /** Loads oxc lazily: native in Node, WASM through the package's browser export. */
497
+ function loadParser() {
498
+ parser ??= import("oxc-parser").then((mod) => mod.parseSync);
499
+ return parser;
500
+ }
501
+ function scanSource(parse, text, options) {
502
+ const source = options.expression ? `(${text})` : text;
503
+ const shift = options.expression ? -1 : 0;
504
+ const result = parse(options.filename, source, {
505
+ sourceType: "module",
506
+ range: false
507
+ });
508
+ const scan = new FileScan(source, (options.offset ?? 0) + shift, options.calls, options.hardStrings !== false, new Set(options.ignoreCallees));
509
+ scan.walk(result.program, void 0, "");
510
+ scan.markers(result.program, result.comments);
511
+ return scan.result();
512
+ }
513
+ /** The AST scanner for JS and TS, driven by call specs (`usage.calls`). */
514
+ function js() {
515
+ return {
516
+ name: "x18n:scanner-js",
517
+ scanners: [{
518
+ id: "js",
519
+ extensions: [
520
+ "js",
521
+ "jsx",
522
+ "mjs",
523
+ "cjs",
524
+ "ts",
525
+ "tsx",
526
+ "mts",
527
+ "cts"
528
+ ],
529
+ async scan(file, ctx) {
530
+ return scanSource(await loadParser(), file.text, {
531
+ calls: ctx.calls,
532
+ filename: file.path,
533
+ hardStrings: true,
534
+ ignoreCallees: ctx.config.extract.ignoreCallees
535
+ });
536
+ }
537
+ }]
538
+ };
539
+ }
540
+ //#endregion
541
+ //#region src/scanners/vue.ts
542
+ let compiler;
543
+ function loadVueCompiler() {
544
+ compiler ??= import("@vue/compiler-sfc");
545
+ return compiler;
546
+ }
547
+ const ELEMENT = 1;
548
+ const TEXT = 2;
549
+ const SIMPLE_EXPRESSION = 4;
550
+ const INTERPOLATION = 5;
551
+ const ATTRIBUTE = 6;
552
+ /** `keypath` is the vue-i18n prop for a key, on `<i18n-t>` and on the app's own wrappers of it; `path` on the legacy components. */
553
+ function isKeyAttribute(tag, name) {
554
+ return name === "keypath" || name === "path" && (tag === "i18n" || tag === "i18n-d");
555
+ }
556
+ const ignoredAttributes = /* @__PURE__ */ new Set([
557
+ "class",
558
+ "style",
559
+ "key",
560
+ "ref",
561
+ "id",
562
+ "name",
563
+ "type",
564
+ "href",
565
+ "src",
566
+ "to",
567
+ "for",
568
+ "role",
569
+ "lang",
570
+ "target",
571
+ "rel",
572
+ "is",
573
+ "slot"
574
+ ]);
575
+ function merge(into, result) {
576
+ into.usages.push(...result.usages);
577
+ into.keySlots.push(...result.keySlots ?? []);
578
+ into.hardStrings.push(...result.hardStrings ?? []);
579
+ }
580
+ var TemplateScan = class {
581
+ parse;
582
+ calls;
583
+ out = {
584
+ usages: [],
585
+ keySlots: [],
586
+ hardStrings: []
587
+ };
588
+ constructor(parse, calls) {
589
+ this.parse = parse;
590
+ this.calls = calls;
591
+ }
592
+ expression(content, offset) {
593
+ if (!content.trim()) return;
594
+ try {
595
+ merge(this.out, scanSource(this.parse, content, {
596
+ calls: this.calls,
597
+ offset,
598
+ filename: "template.ts",
599
+ expression: true,
600
+ hardStrings: false
601
+ }));
602
+ } catch {}
603
+ }
604
+ /** `:keypath="cond ? 'a' : 'b'"`: the expression is read as the key argument of a translate call. */
605
+ keyExpression(content, offset) {
606
+ const callee = "__x18nKey(";
607
+ try {
608
+ merge(this.out, scanSource(this.parse, `${callee}${content})`, {
609
+ calls: [{ callee: "__x18nKey" }],
610
+ offset: offset - 10,
611
+ filename: "template.ts",
612
+ hardStrings: false
613
+ }));
614
+ } catch {}
615
+ }
616
+ /** `v-t="'key'"` and `v-t="{ path: 'key' }"`. */
617
+ vt(dir) {
618
+ const exp = dir.exp;
619
+ if (!exp || !("content" in exp)) return;
620
+ const offset = exp.loc.start.offset;
621
+ const literal = /^\s*(['"`])(.*)\1\s*$/.exec(exp.content) ?? /path\s*:\s*(['"`])(.*?)\1/.exec(exp.content);
622
+ if (!literal) return;
623
+ const start = offset + exp.content.indexOf(literal[2], literal.index);
624
+ const range = {
625
+ start,
626
+ end: start + literal[2].length
627
+ };
628
+ this.out.usages.push({
629
+ raw: literal[2],
630
+ range,
631
+ kind: "call",
632
+ dynamic: false
633
+ });
634
+ this.out.keySlots.push(range);
635
+ }
636
+ attribute(element, prop) {
637
+ if (prop.type === ATTRIBUTE) {
638
+ const value = prop.value;
639
+ if (!value) return;
640
+ const range = {
641
+ start: value.loc.start.offset + 1,
642
+ end: value.loc.end.offset - 1
643
+ };
644
+ if (isKeyAttribute(element.tag, prop.name)) {
645
+ this.out.usages.push({
646
+ raw: value.content,
647
+ range,
648
+ kind: "call",
649
+ dynamic: false
650
+ });
651
+ this.out.keySlots.push(range);
652
+ } else if (!ignoredAttributes.has(prop.name) && !prop.name.startsWith("data-") && /\p{L}/u.test(value.content)) this.out.hardStrings.push({
653
+ text: value.content,
654
+ range: {
655
+ start: prop.loc.start.offset,
656
+ end: prop.loc.end.offset
657
+ },
658
+ kind: "vue-attribute",
659
+ attribute: prop.name
660
+ });
661
+ return;
662
+ }
663
+ if (prop.name === "t") {
664
+ this.vt(prop);
665
+ return;
666
+ }
667
+ const exp = prop.exp;
668
+ if (exp && "content" in exp) {
669
+ if (prop.name === "bind" && prop.arg && "content" in prop.arg && isKeyAttribute(element.tag, prop.arg.content)) this.keyExpression(exp.content, exp.loc.start.offset);
670
+ else this.expression(exp.content, exp.loc.start.offset);
671
+ }
672
+ }
673
+ node(node) {
674
+ if (node.type === INTERPOLATION && "content" in node.content) this.expression(node.content.content, node.content.loc.start.offset);
675
+ if (node.type === TEXT) {
676
+ const text = node.content.trim();
677
+ if (text && /\p{L}/u.test(text)) {
678
+ const start = node.loc.start.offset + node.content.indexOf(text);
679
+ this.out.hardStrings.push({
680
+ text,
681
+ range: {
682
+ start,
683
+ end: start + text.length
684
+ },
685
+ kind: "vue-template-text"
686
+ });
687
+ }
688
+ }
689
+ if (node.type === ELEMENT) for (const prop of node.props) this.attribute(node, prop);
690
+ if ("children" in node) {
691
+ for (const child of node.children) if (typeof child === "object" && child.type !== SIMPLE_EXPRESSION) this.node(child);
692
+ }
693
+ }
694
+ };
695
+ /** Vue SFCs: `<script>` through the `js` scanner, template expressions, `v-t`, `<i18n-t keypath>`. */
696
+ function vue() {
697
+ return {
698
+ name: "x18n:scanner-vue",
699
+ scanners: [{
700
+ id: "vue",
701
+ extensions: ["vue"],
702
+ async scan(file, ctx) {
703
+ const [{ parse }, parseJs] = await Promise.all([loadVueCompiler(), loadParser()]);
704
+ const { descriptor } = parse(file.text, {
705
+ filename: file.path,
706
+ sourceMap: false,
707
+ ignoreEmpty: false
708
+ });
709
+ const template = new TemplateScan(parseJs, ctx.calls);
710
+ for (const script of [descriptor.script, descriptor.scriptSetup]) {
711
+ if (!script) continue;
712
+ const lang = script.lang ?? "js";
713
+ merge(template.out, scanSource(parseJs, script.content, {
714
+ calls: ctx.calls,
715
+ ignoreCallees: ctx.config.extract.ignoreCallees,
716
+ offset: script.loc.start.offset,
717
+ filename: `script.${lang === "tsx" || lang === "jsx" ? lang : lang === "ts" ? "ts" : "js"}`
718
+ }));
719
+ }
720
+ if (descriptor.template?.ast) template.node(descriptor.template.ast);
721
+ return template.out;
722
+ }
723
+ }]
724
+ };
725
+ }
726
+ //#endregion
727
+ export { loadParser as a, js as i, vue as n, scanSource as o, calleeName as r, loadVueCompiler as t };