@menapo/vi18n 0.0.0-stage → 0.1.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 (100) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +110 -2
  3. package/dist/apply-Bre-lY9J.js +228 -0
  4. package/dist/captured-Bj8bO6vd.js +1034 -0
  5. package/dist/check-BIKEuu6B.js +201 -0
  6. package/dist/check-HsmmpJZg.js +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +41 -0
  9. package/dist/codec-DuvU2VQy.js +207 -0
  10. package/dist/collect-Cx4_SKOe.js +13 -0
  11. package/dist/collisions-CfpZU8e4.js +217 -0
  12. package/dist/compile-BaDh0cHG.js +245 -0
  13. package/dist/compile-Cc97JaW2.js +2502 -0
  14. package/dist/concurrency-C9UUWuDC.js +96 -0
  15. package/dist/concurrency-DONL7t4V.js +1 -0
  16. package/dist/config.d.ts +21 -0
  17. package/dist/config.js +39 -0
  18. package/dist/dir-zw6bGMKQ.js +290 -0
  19. package/dist/duplicates-yuRljQ6h.js +185 -0
  20. package/dist/engine-C2ENBRJe.js +531 -0
  21. package/dist/engine.d.ts +43 -0
  22. package/dist/engine.js +1241 -0
  23. package/dist/exclude-Du-Zd_qI.js +26 -0
  24. package/dist/export-C9Sy6UWv.js +103 -0
  25. package/dist/extract-C3S8btx0.js +298 -0
  26. package/dist/formats-9unqj_fZ.js +781 -0
  27. package/dist/i18next-9HAoxwUI.js +1 -0
  28. package/dist/i18next-BEJgv_S8.js +1596 -0
  29. package/dist/import-nANvQutw.js +225 -0
  30. package/dist/index.d.ts +156 -0
  31. package/dist/index.js +239 -0
  32. package/dist/init-C2ScuW6a.js +792 -0
  33. package/dist/library-D5CWvTQs.js +1882 -0
  34. package/dist/lingui-zUiCl9Um.js +833 -0
  35. package/dist/lint.d.ts +105 -0
  36. package/dist/lint.js +971 -0
  37. package/dist/load-CQRz4PpM.js +2950 -0
  38. package/dist/load-gW-oJt1Q.js +747 -0
  39. package/dist/locales-DX4MOBFE.js +827 -0
  40. package/dist/lsp-RMMMc88P.js +31 -0
  41. package/dist/match-BGr8EXB3.js +89 -0
  42. package/dist/mcp-CHVwBoRP.js +28 -0
  43. package/dist/merge-driver-BTX87aVm.js +422 -0
  44. package/dist/messages-BfjIExyB.js +166 -0
  45. package/dist/migrate-IpLDMXmq.js +1484 -0
  46. package/dist/migrate-rCQuOGsf.js +1 -0
  47. package/dist/openai-CTaMDcl-.js +346 -0
  48. package/dist/output-DWE3SIUA.js +164 -0
  49. package/dist/parse.d.ts +62 -0
  50. package/dist/parse.js +156 -0
  51. package/dist/pipeline-C9Dbotkh.js +2031 -0
  52. package/dist/pipeline-DbX6rlo5.js +1 -0
  53. package/dist/project-Bw9UPIdx.js +1046 -0
  54. package/dist/project-C4GpaN-n.js +381 -0
  55. package/dist/pseudo-DRv30iN5.js +145 -0
  56. package/dist/raw-C4Y6vyGY.js +4708 -0
  57. package/dist/react-intl-BP0u-D5m.js +519 -0
  58. package/dist/restore-CiYv-jxS.js +175 -0
  59. package/dist/rewrite-EsIw_lsq.js +803 -0
  60. package/dist/rich-DCUKl5HM.js +97 -0
  61. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  62. package/dist/run-BEcnBbu-.js +694 -0
  63. package/dist/runtime-DTMmliT1.js +64 -0
  64. package/dist/scan-worker.d.ts +62 -0
  65. package/dist/scan-worker.js +1 -0
  66. package/dist/scoped-BMYpSthj.js +44 -0
  67. package/dist/server-Da1Ot9IN.js +10485 -0
  68. package/dist/server-deBOq2vq.js +11823 -0
  69. package/dist/server.d.ts +169 -0
  70. package/dist/server.js +1234 -0
  71. package/dist/sitemap-DJXzKoza.js +159 -0
  72. package/dist/sources-D19czDSA.js +2242 -0
  73. package/dist/status-zr4jY59Y.js +149 -0
  74. package/dist/stdio-CVaJfxA7.js +29 -0
  75. package/dist/stdio-FPOKQAgu.js +95 -0
  76. package/dist/sync-B9V2KFwb.js +658 -0
  77. package/dist/t-DC-c9zE5.js +454 -0
  78. package/dist/test.d.ts +116 -0
  79. package/dist/test.js +319 -0
  80. package/dist/translate-CQnJfLjO.js +299 -0
  81. package/dist/ts-BaU5yOf0.js +4863 -0
  82. package/dist/types-CgUCkFU1.d.ts +707 -0
  83. package/dist/types-D3QGM8XO.js +146 -0
  84. package/dist/types-DsdWBG0b.d.ts +558 -0
  85. package/dist/types-DtvQ7UdD.js +1948 -0
  86. package/dist/types-wSh9fCBY.js +11 -0
  87. package/dist/units-CQ5Q99RC.js +150 -0
  88. package/dist/url-CRfCOhUI.js +172 -0
  89. package/dist/url-CsBkRaJy.js +225 -0
  90. package/dist/validate-BxpupdAf.js +2615 -0
  91. package/dist/validate-D6DJepZv.js +365 -0
  92. package/dist/version-Byqi2guX.js +28 -0
  93. package/dist/vite-scan-CT_vsp4g.js +242 -0
  94. package/dist/vite.d.ts +34 -0
  95. package/dist/vite.js +7558 -0
  96. package/dist/vue-i18n-g0iGy6CZ.js +651 -0
  97. package/dist/which-BOemZ5Q8.js +116 -0
  98. package/dist/worker-84U6uYB3.js +1656 -0
  99. package/package.json +97 -3
  100. package/schemas/catalog.json +57 -0
@@ -0,0 +1,1484 @@
1
+ import { o as isDiagnosticError } from "./types-D3QGM8XO.js";
2
+ import { o as isValidManualId, r as validateTranslation } from "./validate-BxpupdAf.js";
3
+ import { n as printICU } from "./codec-DuvU2VQy.js";
4
+ import { S as entryKey, m as updateCatalogFile, p as serializeCatalog, s as catalogPath, w as sourceMatches } from "./library-D5CWvTQs.js";
5
+ import { i as listProjectFiles, r as isIncluded } from "./project-Bw9UPIdx.js";
6
+ import { t as VERSION } from "./version-Byqi2guX.js";
7
+ import { l as langFromId, u as parseModule } from "./worker-84U6uYB3.js";
8
+ import { t as syncCatalog } from "./sync-B9V2KFwb.js";
9
+ import { i as rootRelative, r as plural } from "./output-DWE3SIUA.js";
10
+ import { t as EXIT } from "./types-wSh9fCBY.js";
11
+ import { a as loadCatalogs, i as hasErrors, n as compareText, o as loadProject, t as CliExit } from "./project-C4GpaN-n.js";
12
+ import { basename, dirname, extname, join, resolve } from "node:path";
13
+ import { randomBytes } from "node:crypto";
14
+ import { readFile, rename, rm, writeFile } from "node:fs/promises";
15
+ //#region src/cli/commands/migrate.ts
16
+ /**
17
+ * `vi18n migrate --from saas-i18n --catalog <file> [--catalog <l>=<file>]…
18
+ * [--write]` (INTERNALS §2.14, PLAN §11): moves a project from `@saas/i18n`'s
19
+ * key catalogue to vi18n manual ids.
20
+ *
21
+ * - `<x>.t("key"[, values])` with a catalogue key becomes
22
+ * `t({ id: "key", message: "<text>" }[, values])`, importing `t` once; so
23
+ * does a call of a local bound to `<x>.t` (`const t = i18nFor(l).t`,
24
+ * `const { t } = i18n`);
25
+ * - computed keys are not rewritten: every catalogue key the expression can
26
+ * produce becomes a `msg()` descriptor in `messages.vi18n.ts` beside the
27
+ * catalogue, and the call site is reported (`migrate-dynamic-key`); so
28
+ * does a key spelled as a string anywhere else (a route table, a helper's
29
+ * argument), so no key the code names is dropped as unused;
30
+ * - other locales' texts become `translated` manual entries.
31
+ *
32
+ * Catalogues are read with the parser, never executed. Without `--write`
33
+ * nothing is written and the plan is printed; any error writes nothing.
34
+ */
35
+ const RUNTIME = "@menapo/vi18n";
36
+ const MESSAGES_MODULE = "messages.vi18n.ts";
37
+ /** Extensions parsed as modules; everything else (`.astro`, `.vue`, `.svelte`, `.mdx`) is scanned lexically. */
38
+ const MODULE_EXTENSIONS = /* @__PURE__ */ new Set([
39
+ ".js",
40
+ ".jsx",
41
+ ".mjs",
42
+ ".cjs",
43
+ ".ts",
44
+ ".tsx",
45
+ ".mts",
46
+ ".cts"
47
+ ]);
48
+ /** `@saas/i18n` placeholders: `{name}`, `{name:date}`, `{name:number}`, `{name:currency}`. */
49
+ const PLACEHOLDER = /\{(\w+)(?::(date|number|currency))?\}/g;
50
+ function isNode(value) {
51
+ return typeof value === "object" && value !== null && typeof value.type === "string";
52
+ }
53
+ /** Children in source order (ESTree field order is source order for every node vi18n meets). */
54
+ function* children(node) {
55
+ for (const key in node) {
56
+ if (key === "type" || key === "start" || key === "end") continue;
57
+ const value = node[key];
58
+ if (Array.isArray(value)) {
59
+ for (const item of value) if (isNode(item)) yield item;
60
+ } else if (isNode(value)) yield value;
61
+ }
62
+ }
63
+ function unwrapExpression(node) {
64
+ let current = node;
65
+ for (;;) if (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression" || current.type === "TSTypeAssertion" || current.type === "ParenthesizedExpression") current = current.expression;
66
+ else return current;
67
+ }
68
+ /**
69
+ * The value of a string-like expression: a string literal, a template
70
+ * without substitutions, or a `+` concatenation of those; null otherwise.
71
+ */
72
+ function staticString(node) {
73
+ const n = unwrapExpression(node);
74
+ if (n.type === "Literal" && typeof n.value === "string") return n.value;
75
+ if (n.type === "TemplateLiteral" && n.expressions.length === 0) return n.quasis[0].value.cooked;
76
+ if (n.type === "BinaryExpression" && n.operator === "+") {
77
+ const left = staticString(n.left);
78
+ const right = staticString(n.right);
79
+ return left === null || right === null ? null : left + right;
80
+ }
81
+ return null;
82
+ }
83
+ function propertyKey(property) {
84
+ const key = property.key;
85
+ if (property.computed === true) return staticString(key);
86
+ if (key.type === "Identifier") return key.name;
87
+ if (key.type === "Literal") return String(key.value);
88
+ return null;
89
+ }
90
+ /**
91
+ * The exported object literal of a catalogue module (`export const en = {…}`,
92
+ * with or without a type annotation or `satisfies`, or `export default {…}`),
93
+ * flattened: nested objects join their keys with `.`.
94
+ */
95
+ function readCatalogue(text, file, locale, display) {
96
+ const diagnostics = [];
97
+ const catalogue = {
98
+ file,
99
+ locale,
100
+ texts: /* @__PURE__ */ new Map()
101
+ };
102
+ const fail = (message, offset = 0) => {
103
+ const diagnostic = {
104
+ code: "catalog-parse",
105
+ severity: "error",
106
+ message,
107
+ file: display
108
+ };
109
+ if (offset > 0) Object.assign(diagnostic, positionOf(text, offset));
110
+ diagnostics.push(diagnostic);
111
+ };
112
+ let program;
113
+ try {
114
+ program = parseModule(text, langFromId(file), file).program;
115
+ } catch (error) {
116
+ if (!isDiagnosticError(error)) throw error;
117
+ diagnostics.push({
118
+ ...error.diagnostic,
119
+ file: display
120
+ });
121
+ return {
122
+ catalogue,
123
+ diagnostics
124
+ };
125
+ }
126
+ const objects = [];
127
+ for (const statement of program.body) if (statement.type === "ExportDefaultDeclaration") {
128
+ const value = unwrapExpression(statement.declaration);
129
+ if (value.type === "ObjectExpression") objects.push(value);
130
+ } else if (statement.type === "ExportNamedDeclaration" && isNode(statement.declaration)) {
131
+ const declaration = statement.declaration;
132
+ if (declaration.type !== "VariableDeclaration") continue;
133
+ for (const declarator of declaration.declarations) {
134
+ if (!isNode(declarator.init)) continue;
135
+ const value = unwrapExpression(declarator.init);
136
+ if (value.type === "ObjectExpression") objects.push(value);
137
+ }
138
+ }
139
+ if (objects.length !== 1) {
140
+ fail(objects.length === 0 ? `${basename(file)} exports no object literal; a saas-i18n catalogue is a module exporting one object of strings` : `${basename(file)} exports ${objects.length} object literals; a saas-i18n catalogue exports exactly one`);
141
+ return {
142
+ catalogue,
143
+ diagnostics
144
+ };
145
+ }
146
+ const walk = (object, prefix) => {
147
+ for (const property of object.properties) {
148
+ if (property.type !== "Property") {
149
+ fail("spread properties cannot be read without executing the catalogue; inline them", property.start);
150
+ continue;
151
+ }
152
+ const key = propertyKey(property);
153
+ if (key === null) {
154
+ fail("a computed key cannot be read without executing the catalogue", property.start);
155
+ continue;
156
+ }
157
+ const value = unwrapExpression(property.value);
158
+ if (value.type === "ObjectExpression") {
159
+ walk(value, prefix + key + ".");
160
+ continue;
161
+ }
162
+ const textValue = staticString(value);
163
+ if (textValue === null) {
164
+ fail(`the value of "${prefix + key}" is not a string literal`, value.start);
165
+ continue;
166
+ }
167
+ catalogue.texts.set(prefix + key, textValue);
168
+ }
169
+ };
170
+ walk(objects[0], "");
171
+ return {
172
+ catalogue,
173
+ diagnostics
174
+ };
175
+ }
176
+ /**
177
+ * A saas-i18n message as ICU text. Placeholders become arguments
178
+ * (`{n:number}` → `{n, number}`, `{d:date}` → `{d, date, medium}`, the ICU
179
+ * style nearest saas's `dateStyle: "medium"`, the same text in English only);
180
+ * everything else is literal text, quoted by the
181
+ * canonical printer where ICU would read it as syntax (`{`, `<b`, `'{`).
182
+ */
183
+ function saasToIcu(text) {
184
+ const nodes = [];
185
+ const currency = [];
186
+ const literal = (value) => {
187
+ if (value === "") return;
188
+ const last = nodes[nodes.length - 1];
189
+ if (last?.type === "literal") last.value += value;
190
+ else nodes.push({
191
+ type: "literal",
192
+ value
193
+ });
194
+ };
195
+ let at = 0;
196
+ for (const match of text.matchAll(PLACEHOLDER)) {
197
+ literal(text.slice(at, match.index));
198
+ at = match.index + match[0].length;
199
+ const name = match[1];
200
+ const format = match[2];
201
+ let node;
202
+ if (format === "number") node = {
203
+ type: "number",
204
+ name,
205
+ style: null,
206
+ options: null
207
+ };
208
+ else if (format === "date") node = {
209
+ type: "date",
210
+ name,
211
+ style: {
212
+ kind: "keyword",
213
+ value: "medium"
214
+ },
215
+ options: null
216
+ };
217
+ else {
218
+ if (format === "currency") currency.push(name);
219
+ node = {
220
+ type: "argument",
221
+ name
222
+ };
223
+ }
224
+ nodes.push(node);
225
+ }
226
+ literal(text.slice(at));
227
+ return {
228
+ icu: printICU(nodes),
229
+ currency
230
+ };
231
+ }
232
+ /** Names of plain `{name}` placeholders (no `:date`, `:number`, `:currency`) in a saas text, deduplicated. */
233
+ function plainPlaceholders(text) {
234
+ const names = /* @__PURE__ */ new Set();
235
+ for (const match of text.matchAll(PLACEHOLDER)) if (match[2] === void 0) names.add(match[1]);
236
+ return [...names];
237
+ }
238
+ /**
239
+ * Whether a name usually holds a date: `date`, `updated`, `expires`, or a
240
+ * camel-case or snake-case suffix (`publishedAt`, `startDate`, `created_on`).
241
+ * Number-like names (`days`, `readingTime`) are left out: saas formatted
242
+ * only `Date` values, so they render the same in vi18n.
243
+ */
244
+ function isDateName(name) {
245
+ return /^(?:date|datetime|timestamp|when|since|until|deadline|expires|expiry|expiration|created|updated|modified|published|posted|due)$/i.test(name) || /[a-z0-9](?:Date|At|On)$/.test(name) || /^date[A-Z_]/.test(name) || /_(?:date|at|on)$/i.test(name);
246
+ }
247
+ /**
248
+ * The value written for `name` in a call's values object (`{ date: new
249
+ * Date() }`, or `name` itself for `{ date }`); null when the call names no
250
+ * such property. Lexical, so it reads calls in components alike.
251
+ */
252
+ function valueExpression(call, name) {
253
+ const property = new RegExp(`[{,]\\s*(?:${name}|"${name}"|'${name}')\\s*:\\s*`).exec(call);
254
+ if (property === null) return new RegExp(`[{,]\\s*${name}\\s*[,}]`).test(call) ? name : null;
255
+ const start = property.index + property[0].length;
256
+ for (let i = start; i < call.length; i++) {
257
+ const c = call[i];
258
+ if (c === "," || c === "}" || c === ")") return call.slice(start, i).trim();
259
+ let end = i + 1;
260
+ if (c === "(" || c === "[" || c === "{") end = skipBalanced(call, i);
261
+ else if (c === "\"" || c === "'") end = readString(call, i)?.end ?? null;
262
+ else if (c === "`") end = readTemplate(call, i)?.end ?? null;
263
+ if (end === null) return null;
264
+ i = end - 1;
265
+ }
266
+ return null;
267
+ }
268
+ /** Whether a value expression passes a `Date`: `new Date(…)`, or ends in a date-like name (`post.updatedAt`). */
269
+ function looksLikeDate(expression) {
270
+ const constructed = /^new\s+Date\b\s*/.exec(expression);
271
+ if (constructed !== null) {
272
+ const at = constructed[0].length;
273
+ return at === expression.length || skipBalanced(expression, at) === expression.length;
274
+ }
275
+ const last = /([A-Za-z_$][\w$]*)\s*(\(\s*\))?\s*$/.exec(expression.replace(/[!?]+$/, ""));
276
+ if (last === null) return false;
277
+ const name = last[1];
278
+ if (last[2] !== void 0 && DATE_NUMBER_GETTER.test(name)) return false;
279
+ return isDateName(name);
280
+ }
281
+ /** `Date` methods that return numbers, not dates. */
282
+ const DATE_NUMBER_GETTER = /^(?:get(?:UTC)?(?:Date|Day|FullYear|Month|Hours|Minutes|Seconds|Milliseconds)|getTime|getTimezoneOffset|valueOf)$/;
283
+ /** A key as a valid manual id (INTERNALS §2.2.3): invalid characters → `_`, a key without `.` gets `msg.`. */
284
+ function toManualId(key) {
285
+ if (isValidManualId(key)) return key;
286
+ const segments = key.split(".").map((segment) => segment.replace(/[^A-Za-z0-9_:-]/g, "_") || "_");
287
+ if (segments.length < 2) segments.unshift("msg");
288
+ return segments.join(".");
289
+ }
290
+ /** Matches catalogue keys against a template's static parts. */
291
+ function templateKeys(quasis, keys) {
292
+ const found = /* @__PURE__ */ new Set();
293
+ const literal = quasis.join("");
294
+ if (!/[A-Za-z]/.test(literal)) return found;
295
+ const pattern = new RegExp("^" + quasis.map((q) => q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^]+?") + "$");
296
+ for (const key of keys) if (pattern.test(key)) found.add(key);
297
+ return found;
298
+ }
299
+ /**
300
+ * Keys searched for as plain strings outside calls: only nested ones
301
+ * (`nav.home`). A one-word key (`quote`) is as likely an ordinary string of
302
+ * the code, and claiming it would only add noise.
303
+ */
304
+ function isNamedKey(value, keys) {
305
+ return value.includes(".") && keys.has(value);
306
+ }
307
+ function planModule(file, text, keys, keyList, display) {
308
+ const plan = {
309
+ file,
310
+ text,
311
+ rewrites: [],
312
+ dynamic: [],
313
+ importedT: null,
314
+ program: null,
315
+ diagnostics: []
316
+ };
317
+ let program;
318
+ try {
319
+ program = parseModule(text, langFromId(file), file).program;
320
+ } catch (error) {
321
+ if (!isDiagnosticError(error)) throw error;
322
+ plan.diagnostics.push({
323
+ ...error.diagnostic,
324
+ file: display
325
+ });
326
+ return plan;
327
+ }
328
+ plan.program = program;
329
+ const runtimeNamespaces = /* @__PURE__ */ new Set();
330
+ for (const statement of program.body) {
331
+ if (statement.type !== "ImportDeclaration") continue;
332
+ if (statement.source.value !== RUNTIME) continue;
333
+ for (const specifier of statement.specifiers) {
334
+ const local = specifier.local.name;
335
+ if (specifier.type === "ImportNamespaceSpecifier") runtimeNamespaces.add(local);
336
+ else if (specifier.type === "ImportSpecifier") {
337
+ const imported = specifier.imported;
338
+ if ((imported.name ?? imported.value) === "t" && statement.importKind !== "type") plan.importedT = local;
339
+ }
340
+ }
341
+ }
342
+ const bindings = moduleBindings(program, plan.importedT);
343
+ const handled = /* @__PURE__ */ new Set();
344
+ const visit = (node, parent) => {
345
+ if (node.type === "CallExpression") {
346
+ const callee = unwrapExpression(node.callee);
347
+ const args = node.arguments;
348
+ let receiver = null;
349
+ if (callee.type === "MemberExpression" && callee.computed !== true && callee.property.type === "Identifier" && callee.property.name === "t") {
350
+ const object = callee.object;
351
+ if (!(object.type === "Identifier" && runtimeNamespaces.has(object.name))) receiver = text.slice(object.start, object.end).replace(/\s+/g, "");
352
+ } else if (callee.type === "Identifier" && bindings.has(callee.name)) receiver = callee.name;
353
+ const first = args[0];
354
+ if (receiver !== null && first !== void 0 && first.type !== "SpreadElement") {
355
+ const key = staticString(first);
356
+ if (key !== null) {
357
+ if (keys.has(key)) {
358
+ const literal = unwrapExpression(first);
359
+ handled.add(literal.start);
360
+ const quote = text[literal.start] === "'" ? "'" : "\"";
361
+ plan.rewrites.push({
362
+ key,
363
+ receiver,
364
+ callee: {
365
+ start: callee.start,
366
+ end: callee.end,
367
+ text: ""
368
+ },
369
+ argument: {
370
+ start: first.start,
371
+ end: first.end,
372
+ text: quote
373
+ },
374
+ original: text.slice(node.start, node.end),
375
+ markup: null
376
+ });
377
+ }
378
+ } else plan.dynamic.push({
379
+ offset: first.start,
380
+ expression: text.slice(first.start, first.end),
381
+ keys: expressionKeys(first, keys, keyList, handled),
382
+ receiver
383
+ });
384
+ }
385
+ }
386
+ if (node.type === "ObjectExpression") {
387
+ const properties = node.properties.filter((p) => p.type === "Property");
388
+ if (properties.some((p) => propertyKey(p) === "message")) {
389
+ for (const p of properties) if (propertyKey(p) === "id") handled.add(p.value.start);
390
+ }
391
+ }
392
+ if (node.type === "Literal" && typeof node.value === "string" && isNamedKey(node.value, keys) && !handled.has(node.start) && isKeyPosition(node, parent)) plan.dynamic.push({
393
+ offset: node.start,
394
+ expression: text.slice(node.start, node.end),
395
+ keys: /* @__PURE__ */ new Set([node.value]),
396
+ receiver: null,
397
+ literal: true
398
+ });
399
+ if (node.type === "TemplateLiteral" && !handled.has(node.start)) {
400
+ if (node.expressions.length > 0) {
401
+ const found = templateKeys(node.quasis.map((q) => q.value.cooked ?? ""), keyList);
402
+ if (found.size > 0) plan.dynamic.push({
403
+ offset: node.start,
404
+ expression: text.slice(node.start, node.end),
405
+ keys: found,
406
+ receiver: null
407
+ });
408
+ }
409
+ }
410
+ for (const child of children(node)) visit(child, node);
411
+ };
412
+ visit(program, null);
413
+ return plan;
414
+ }
415
+ /**
416
+ * Locals bound to a saas `t` (`const t = i18nFor(l).t`, `const { t } =
417
+ * i18n`), whose calls are call sites like `<x>.t(…)`. Scopes are ignored:
418
+ * a call of an unrelated local of the same name only matters when its
419
+ * first argument is a catalogue key.
420
+ */
421
+ function moduleBindings(program, importedT) {
422
+ const names = /* @__PURE__ */ new Set();
423
+ const visit = (node) => {
424
+ if (node.type === "VariableDeclarator") {
425
+ const id = node.id;
426
+ const init = isNode(node.init) ? unwrapExpression(node.init) : null;
427
+ if (id.type === "Identifier" && init !== null && init.type === "MemberExpression" && init.computed !== true && init.property.name === "t") names.add(id.name);
428
+ else if (id.type === "ObjectPattern") for (const property of id.properties) {
429
+ if (property.type !== "Property" || propertyKey(property) !== "t") continue;
430
+ let value = property.value;
431
+ if (value.type === "AssignmentPattern") value = value.left;
432
+ if (value.type === "Identifier") names.add(value.name);
433
+ }
434
+ }
435
+ for (const child of children(node)) visit(child);
436
+ };
437
+ visit(program);
438
+ if (importedT !== null) names.delete(importedT);
439
+ return names;
440
+ }
441
+ /**
442
+ * Whether a string literal equal to a key is a use of that key: not an
443
+ * object key, a module specifier or a type.
444
+ */
445
+ function isKeyPosition(node, parent) {
446
+ if (parent === null) return true;
447
+ switch (parent.type) {
448
+ case "ImportDeclaration":
449
+ case "ExportNamedDeclaration":
450
+ case "ExportAllDeclaration":
451
+ case "ImportExpression":
452
+ case "TSLiteralType":
453
+ case "TSExternalModuleReference": return false;
454
+ case "Property": return parent.key !== node || parent.computed === true;
455
+ default: return true;
456
+ }
457
+ }
458
+ /** Catalogue keys an expression can evaluate to: its string literals and templates that match keys. */
459
+ function expressionKeys(node, keys, keyList, handled) {
460
+ const found = /* @__PURE__ */ new Set();
461
+ const visit = (n) => {
462
+ if (n.type === "Literal" && typeof n.value === "string" && keys.has(n.value)) {
463
+ found.add(n.value);
464
+ handled.add(n.start);
465
+ }
466
+ if (n.type === "TemplateLiteral") {
467
+ handled.add(n.start);
468
+ const quasis = n.quasis.map((q) => q.value.cooked ?? "");
469
+ for (const key of templateKeys(quasis, keyList)) found.add(key);
470
+ }
471
+ for (const child of children(n)) visit(child);
472
+ };
473
+ visit(node);
474
+ return found;
475
+ }
476
+ /** `.t(` or `?.t(`; the receiver before it is found by walking back. */
477
+ const MEMBER_T = /\??\.\s*t\s*\(/g;
478
+ /** Words after which `(` opens an expression, not a call: `return (a).t(…)`. */
479
+ const KEYWORDS = /* @__PURE__ */ new Set([
480
+ "return",
481
+ "typeof",
482
+ "await",
483
+ "case",
484
+ "in",
485
+ "of",
486
+ "new",
487
+ "void",
488
+ "delete",
489
+ "yield",
490
+ "else",
491
+ "do"
492
+ ]);
493
+ const ESCAPES = {
494
+ n: "\n",
495
+ t: " ",
496
+ r: "\r",
497
+ b: "\b",
498
+ f: "\f",
499
+ v: "\v",
500
+ 0: "\0"
501
+ };
502
+ /** A JS string literal starting at `at` (a quote): its value and end, or null when unterminated. */
503
+ function readString(text, at) {
504
+ const quote = text[at];
505
+ let value = "";
506
+ for (let i = at + 1; i < text.length; i++) {
507
+ const c = text[i];
508
+ if (c === quote) return {
509
+ value,
510
+ end: i + 1
511
+ };
512
+ if (c === "\n") return null;
513
+ if (c === "\\") {
514
+ const next = text[++i];
515
+ if (next === void 0) return null;
516
+ if (next === "u" || next === "x") {
517
+ const hex = next === "x" ? text.slice(i + 1, i + 3) : text.slice(i + 1, i + 5);
518
+ value += String.fromCharCode(Number.parseInt(hex, 16));
519
+ i += hex.length;
520
+ } else if (next === "\n") {} else value += ESCAPES[next] ?? next;
521
+ continue;
522
+ }
523
+ value += c;
524
+ }
525
+ return null;
526
+ }
527
+ /** A template literal starting at `at`: cooked static parts, and its end. */
528
+ function readTemplate(text, at) {
529
+ const quasis = [""];
530
+ for (let i = at + 1; i < text.length; i++) {
531
+ const c = text[i];
532
+ if (c === "`") return {
533
+ quasis,
534
+ end: i + 1
535
+ };
536
+ if (c === "\\") {
537
+ quasis[quasis.length - 1] += text[++i] ?? "";
538
+ continue;
539
+ }
540
+ if (c === "$" && text[i + 1] === "{") {
541
+ const end = skipBalanced(text, i + 1);
542
+ if (end === null) return null;
543
+ i = end - 1;
544
+ quasis.push("");
545
+ continue;
546
+ }
547
+ quasis[quasis.length - 1] += c;
548
+ }
549
+ return null;
550
+ }
551
+ /** From an opening bracket at `at`, the offset after its matching close (strings and templates skipped). */
552
+ function skipBalanced(text, at) {
553
+ let depth = 0;
554
+ for (let i = at; i < text.length; i++) {
555
+ const c = text[i];
556
+ if (c === "(" || c === "[" || c === "{") depth++;
557
+ else if (c === ")" || c === "]" || c === "}") {
558
+ depth--;
559
+ if (depth === 0) return i + 1;
560
+ } else if (c === "\"" || c === "'") {
561
+ const s = readString(text, i);
562
+ if (s === null) return null;
563
+ i = s.end - 1;
564
+ } else if (c === "`") {
565
+ const s = readTemplate(text, i);
566
+ if (s === null) return null;
567
+ i = s.end - 1;
568
+ }
569
+ }
570
+ return null;
571
+ }
572
+ /** The end of a call argument starting at `at`: the next `,` or `)` at depth 0. */
573
+ function argumentEnd(text, at) {
574
+ for (let i = at; i < text.length; i++) {
575
+ const c = text[i];
576
+ if (c === "," || c === ")") return i;
577
+ if (c === "(" || c === "[" || c === "{") {
578
+ const end = skipBalanced(text, i);
579
+ if (end === null) return null;
580
+ i = end - 1;
581
+ } else if (c === "\"" || c === "'") {
582
+ const s = readString(text, i);
583
+ if (s === null) return null;
584
+ i = s.end - 1;
585
+ } else if (c === "`") {
586
+ const s = readTemplate(text, i);
587
+ if (s === null) return null;
588
+ i = s.end - 1;
589
+ }
590
+ }
591
+ return null;
592
+ }
593
+ function skipSpace(text, at) {
594
+ let i = at;
595
+ while (i < text.length && /\s/.test(text[i])) i++;
596
+ return i;
597
+ }
598
+ /** From a closing bracket at `at`, the offset of its opening one (strings skipped), or null. */
599
+ function matchBack(text, at) {
600
+ let depth = 0;
601
+ for (let i = at; i >= 0; i--) {
602
+ const c = text[i];
603
+ if (c === ")" || c === "]" || c === "}") depth++;
604
+ else if (c === "(" || c === "[" || c === "{") {
605
+ depth--;
606
+ if (depth === 0) return i;
607
+ } else if (c === "\"" || c === "'" || c === "`") {
608
+ let j = i - 1;
609
+ while (j >= 0 && !(text[j] === c && text[j - 1] !== "\\")) j--;
610
+ if (j < 0) return null;
611
+ i = j;
612
+ }
613
+ }
614
+ return null;
615
+ }
616
+ /**
617
+ * The start of the receiver ending at `end` (exclusive): identifiers joined
618
+ * by `.`/`?.`, with calls and indexes (`i18nFor(locale).t`, `a[0].t`).
619
+ * Null when there is none (`).t(` after an operator, a number).
620
+ */
621
+ function receiverStart(text, end) {
622
+ const back = (j) => {
623
+ let k = j;
624
+ while (k > 0 && /\s/.test(text[k - 1])) k--;
625
+ return k;
626
+ };
627
+ let j = back(end);
628
+ let start = null;
629
+ for (;;) {
630
+ const c = text[j - 1];
631
+ if (c === ")" || c === "]") {
632
+ const open = matchBack(text, j - 1);
633
+ if (open === null) return null;
634
+ j = open;
635
+ start = j;
636
+ const before = text[j - 1];
637
+ if (before !== void 0 && /[\w$)\]]/.test(before)) {
638
+ let k = j;
639
+ while (k > 0 && /[\w$]/.test(text[k - 1])) k--;
640
+ if (KEYWORDS.has(text.slice(k, j))) return start;
641
+ continue;
642
+ }
643
+ if (before === "." && text[j - 2] === "?") {
644
+ j -= 2;
645
+ continue;
646
+ }
647
+ return start;
648
+ }
649
+ if (c !== void 0 && /[\w$]/.test(c)) {
650
+ let k = j;
651
+ while (k > 0 && /[\w$]/.test(text[k - 1])) k--;
652
+ if (/[0-9]/.test(text[k])) return null;
653
+ start = k;
654
+ const dot = back(k);
655
+ if (text[dot - 1] !== ".") return start;
656
+ j = text[dot - 2] === "?" ? dot - 2 : dot - 1;
657
+ j = back(j);
658
+ continue;
659
+ }
660
+ return start;
661
+ }
662
+ }
663
+ /**
664
+ * Locals bound to a saas `t` in a component's script or front matter
665
+ * (`const t = i18nFor(locale).t`, `const { t } = i18n`).
666
+ */
667
+ function componentBindings(text, importedT) {
668
+ const names = /* @__PURE__ */ new Set();
669
+ const declaration = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*/g;
670
+ for (let match = declaration.exec(text); match !== null; match = declaration.exec(text)) {
671
+ const at = match.index + match[0].length;
672
+ let end = at;
673
+ while (end < text.length && !/[;\n,]/.test(text[end])) {
674
+ const c = text[end];
675
+ if (c === "(" || c === "[" || c === "{") {
676
+ const close = skipBalanced(text, end);
677
+ if (close === null) break;
678
+ end = close;
679
+ } else if (c === "\"" || c === "'") {
680
+ const literal = readString(text, end);
681
+ if (literal === null) break;
682
+ end = literal.end;
683
+ } else end++;
684
+ }
685
+ if (/\??\.\s*t\s*$/.test(text.slice(at, end).trimEnd())) names.add(match[1]);
686
+ }
687
+ const destructure = /\b(?:const|let|var)\s*\{([^}]*)\}\s*=/g;
688
+ for (let match = destructure.exec(text); match !== null; match = destructure.exec(text)) for (const part of match[1].split(",")) {
689
+ const m = /^\s*t\s*(?::\s*([A-Za-z_$][\w$]*))?\s*(?:=.*)?$/.exec(part);
690
+ if (m !== null) names.add(m[1] ?? "t");
691
+ }
692
+ if (importedT !== null) names.delete(importedT);
693
+ return names;
694
+ }
695
+ /**
696
+ * A Vue SFC's quoted attribute values, outside `<script>` and `<style>`
697
+ * blocks, and the contents of those blocks (`code`): what lies in neither is
698
+ * template text.
699
+ */
700
+ function vueRegions(text) {
701
+ const values = [];
702
+ const code = [];
703
+ let i = 0;
704
+ while (i < text.length) {
705
+ if (text.startsWith("<!--", i)) {
706
+ const end = text.indexOf("-->", i + 4);
707
+ i = end === -1 ? text.length : end + 3;
708
+ continue;
709
+ }
710
+ if (text[i] !== "<" || !/[A-Za-z]/.test(text[i + 1] ?? "")) {
711
+ i++;
712
+ continue;
713
+ }
714
+ const name = /^<([A-Za-z][\w-]*)/.exec(text.slice(i, i + 64))?.[1]?.toLowerCase() ?? "";
715
+ i += name.length + 1;
716
+ while (i < text.length && text[i] !== ">") {
717
+ const c = text[i];
718
+ if (c === "\"" || c === "'") {
719
+ const end = text.indexOf(c, i + 1);
720
+ if (end === -1) return {
721
+ attributes: values,
722
+ code
723
+ };
724
+ if (/=\s*$/.test(text.slice(Math.max(0, i - 16), i))) values.push({
725
+ start: i + 1,
726
+ end,
727
+ quote: c
728
+ });
729
+ i = end + 1;
730
+ } else i++;
731
+ }
732
+ i++;
733
+ if (name === "script" || name === "style") {
734
+ const close = text.indexOf(`</${name}`, i);
735
+ const end = close === -1 ? text.length : close;
736
+ code.push({
737
+ start: i,
738
+ end
739
+ });
740
+ i = end;
741
+ }
742
+ }
743
+ return {
744
+ attributes: values,
745
+ code
746
+ };
747
+ }
748
+ function planComponent(file, text, keys, keyList) {
749
+ const plan = {
750
+ file,
751
+ text,
752
+ rewrites: [],
753
+ dynamic: [],
754
+ importedT: null,
755
+ program: null,
756
+ diagnostics: []
757
+ };
758
+ const existing = new RegExp(String.raw`import\s*\{([^}]*)\}\s*from\s*["']${RUNTIME.replace("/", "\\/")}["']`).exec(text);
759
+ if (existing !== null) for (const specifier of existing[1].split(",")) {
760
+ const m = /^\s*t(?:\s+as\s+([A-Za-z_$][\w$]*))?\s*$/.exec(specifier);
761
+ if (m !== null) plan.importedT = m[1] ?? "t";
762
+ }
763
+ const regions = extname(file) === ".vue" ? vueRegions(text) : null;
764
+ const markupAt = (offset) => {
765
+ if (regions === null) return null;
766
+ if (regions.code.some((r) => r.start <= offset && offset < r.end)) return null;
767
+ return { quote: regions.attributes.find((a) => a.start <= offset && offset < a.end)?.quote ?? null };
768
+ };
769
+ const templatesSeen = /* @__PURE__ */ new Set();
770
+ /** Ranges already accounted for: rewritten arguments and computed expressions. */
771
+ const covered = [];
772
+ const call = (calleeStart, calleeEnd, open, receiver) => {
773
+ const at = skipSpace(text, open);
774
+ const c = text[at];
775
+ if (c === ")") return;
776
+ const rewrite = (key, end, quote) => {
777
+ const close = skipBalanced(text, open - 1) ?? end + 1;
778
+ covered.push([at, end]);
779
+ plan.rewrites.push({
780
+ key,
781
+ receiver,
782
+ callee: {
783
+ start: calleeStart,
784
+ end: calleeEnd,
785
+ text: ""
786
+ },
787
+ argument: {
788
+ start: at,
789
+ end,
790
+ text: quote
791
+ },
792
+ original: text.slice(calleeStart, close),
793
+ markup: markupAt(at)
794
+ });
795
+ };
796
+ if (c === "\"" || c === "'") {
797
+ const literal = readString(text, at);
798
+ if (literal === null) return;
799
+ const after = skipSpace(text, literal.end);
800
+ if (text[after] !== "," && text[after] !== ")") {
801
+ const end = argumentEnd(text, at);
802
+ if (end !== null) pushDynamic(plan, text, at, end, receiver, keys, keyList, templatesSeen, covered);
803
+ return;
804
+ }
805
+ if (keys.has(literal.value)) rewrite(literal.value, literal.end, c);
806
+ return;
807
+ }
808
+ const end = argumentEnd(text, at);
809
+ if (end === null) return;
810
+ if (c === "`") {
811
+ const template = readTemplate(text, at);
812
+ if (template !== null && template.quasis.length === 1 && skipSpace(text, template.end) === end) {
813
+ const key = template.quasis[0];
814
+ templatesSeen.add(at);
815
+ if (keys.has(key)) rewrite(key, template.end, "\"");
816
+ return;
817
+ }
818
+ }
819
+ pushDynamic(plan, text, at, end, receiver, keys, keyList, templatesSeen, covered);
820
+ };
821
+ MEMBER_T.lastIndex = 0;
822
+ for (let match = MEMBER_T.exec(text); match !== null; match = MEMBER_T.exec(text)) {
823
+ const start = receiverStart(text, match.index);
824
+ if (start === null) continue;
825
+ const receiver = text.slice(start, match.index).replace(/\s+/g, "");
826
+ const open = match.index + match[0].length;
827
+ call(start, text.lastIndexOf("t", open - 1) + 1, open, receiver);
828
+ }
829
+ for (const name of componentBindings(text, plan.importedT)) {
830
+ const pattern = new RegExp(String.raw`(?<![\w$.])${name.replace(/\$/g, "\\$")}\s*\(`, "g");
831
+ for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
832
+ if (/\bfunction\s*$/.test(text.slice(Math.max(0, match.index - 12), match.index))) continue;
833
+ call(match.index, match.index + name.length, match.index + match[0].length, name);
834
+ }
835
+ }
836
+ for (let at = text.indexOf("`"); at !== -1; at = text.indexOf("`", at + 1)) {
837
+ if (templatesSeen.has(at)) continue;
838
+ const template = readTemplate(text, at);
839
+ if (template === null) continue;
840
+ if (template.quasis.length > 1) {
841
+ const found = templateKeys(template.quasis, keyList);
842
+ if (found.size > 0) plan.dynamic.push({
843
+ offset: at,
844
+ expression: text.slice(at, template.end),
845
+ keys: found,
846
+ receiver: null
847
+ });
848
+ }
849
+ at = template.end - 1;
850
+ }
851
+ const inCovered = (offset) => covered.some(([a, b]) => a <= offset && offset < b);
852
+ for (const key of keyList) {
853
+ if (!isNamedKey(key, keys)) continue;
854
+ for (const quote of [
855
+ "\"",
856
+ "'",
857
+ "`"
858
+ ]) {
859
+ const needle = quote + key + quote;
860
+ for (let at = text.indexOf(needle); at !== -1; at = text.indexOf(needle, at + 1)) {
861
+ if (inCovered(at)) continue;
862
+ if (/\bid\s*:\s*$/.test(text.slice(Math.max(0, at - 16), at))) continue;
863
+ plan.dynamic.push({
864
+ offset: at,
865
+ expression: needle,
866
+ keys: /* @__PURE__ */ new Set([key]),
867
+ receiver: null,
868
+ literal: true
869
+ });
870
+ }
871
+ }
872
+ }
873
+ plan.dynamic.sort((a, b) => a.offset - b.offset);
874
+ plan.rewrites.sort((a, b) => a.callee.start - b.callee.start);
875
+ return plan;
876
+ }
877
+ function pushDynamic(plan, text, start, end, receiver, keys, keyList, templatesSeen, covered) {
878
+ covered.push([start, end]);
879
+ const expression = text.slice(start, end).trim();
880
+ const found = /* @__PURE__ */ new Set();
881
+ for (let i = start; i < end; i++) {
882
+ const c = text[i];
883
+ if (c === "\"" || c === "'") {
884
+ const s = readString(text, i);
885
+ if (s === null) break;
886
+ if (keys.has(s.value)) found.add(s.value);
887
+ i = s.end - 1;
888
+ } else if (c === "`") {
889
+ const s = readTemplate(text, i);
890
+ if (s === null) break;
891
+ templatesSeen.add(i);
892
+ for (const key of templateKeys(s.quasis, keyList)) found.add(key);
893
+ i = s.end - 1;
894
+ }
895
+ }
896
+ plan.dynamic.push({
897
+ offset: start,
898
+ expression,
899
+ keys: found,
900
+ receiver
901
+ });
902
+ }
903
+ /** A JS string literal in the given quote style (a Vue attribute may need single quotes). */
904
+ function quoteJs(value, quote) {
905
+ if (quote === "\"") return JSON.stringify(value);
906
+ return "'" + JSON.stringify(value).slice(1, -1).replace(/\\"/g, "\"").replace(/'/g, "\\'") + "'";
907
+ }
908
+ /**
909
+ * `{ id, message }` in the quote style of the key it replaces, encoded for
910
+ * where it lands. A Vue template decodes entities in attribute values and in
911
+ * `{{ }}` before compiling the expression, so catalogue text there is written
912
+ * as markup: `&` as `&amp;` (else `&#39;` in a message would end its string
913
+ * literal and run the rest as code), an attribute value's own quote as an
914
+ * entity, and in text the `}` of a `}}` as `&#125;`, since `}}` would end the
915
+ * interpolation inside the message (an ICU plural ends with one).
916
+ */
917
+ function descriptor(info, quote, markup = null) {
918
+ return encodeMarkup(`{ id: ${quoteJs(info.id, quote)}, message: ${quoteJs(info.canonical, quote)} }`, markup);
919
+ }
920
+ /** Code for where it lands in a Vue template (see {@link descriptor}); unchanged in code. */
921
+ function encodeMarkup(code, markup) {
922
+ if (markup === null) return code;
923
+ const html = code.replaceAll("&", "&amp;");
924
+ if (markup.quote === null) return html.replaceAll("}}", "}&#125;");
925
+ return html.replaceAll(markup.quote, markup.quote === "\"" ? "&quot;" : "&#39;");
926
+ }
927
+ /** A local name for `t` the file does not already use. */
928
+ function localName(text) {
929
+ for (const name of [
930
+ "t",
931
+ "vt",
932
+ "vi18nT"
933
+ ]) if (!new RegExp(String.raw`(?<![.\w$])${name}(?![\w$])`).test(text)) return name;
934
+ return "vi18nT_";
935
+ }
936
+ function importLine(local) {
937
+ return local === "t" ? `import { t } from "${RUNTIME}";` : `import { t as ${local} } from "${RUNTIME}";`;
938
+ }
939
+ /** Where the import goes, per file kind, creating the script block or front matter a component lacks. */
940
+ function importEdit(file, text, local, program) {
941
+ return importLineEdit(file, text, importLine(local), program);
942
+ }
943
+ /** {@link importEdit} for any import line (several names, a type import). */
944
+ function importLineEdit(file, text, line, program) {
945
+ const ext = extname(file);
946
+ if (program !== null) {
947
+ let end = -1;
948
+ for (const statement of program.body) if (statement.type === "ImportDeclaration") end = statement.end;
949
+ if (end !== -1) return {
950
+ start: end,
951
+ end,
952
+ text: "\n" + line
953
+ };
954
+ let at = 0;
955
+ if (text.startsWith("#!")) at = text.indexOf("\n") + 1;
956
+ for (const statement of program.body) if (statement.type === "ExpressionStatement" && typeof statement.directive === "string") at = text.indexOf("\n", statement.end) + 1 || statement.end;
957
+ else break;
958
+ return {
959
+ start: at,
960
+ end: at,
961
+ text: line + "\n"
962
+ };
963
+ }
964
+ if (ext === ".astro") {
965
+ const fence = /^\s*---[ \t]*\r?\n/.exec(text);
966
+ if (fence !== null) return {
967
+ start: fence[0].length,
968
+ end: fence[0].length,
969
+ text: line + "\n"
970
+ };
971
+ return {
972
+ start: 0,
973
+ end: 0,
974
+ text: `---\n${line}\n---\n`
975
+ };
976
+ }
977
+ if (ext === ".vue" || ext === ".svelte") {
978
+ const usable = [...text.matchAll(/<script\b([^>]*)>/g)].filter((m) => {
979
+ const attrs = m[1];
980
+ if (/\bsrc\s*=/.test(attrs)) return false;
981
+ if (ext === ".svelte") return !/\bcontext\s*=\s*["']module["']|\bmodule\b/.test(attrs);
982
+ return true;
983
+ });
984
+ const tag = ext === ".vue" ? usable.find((m) => /\bsetup\b/.test(m[1])) : usable[0];
985
+ if (tag !== void 0) {
986
+ const at = tag.index + tag[0].length;
987
+ return {
988
+ start: at,
989
+ end: at,
990
+ text: "\n" + line
991
+ };
992
+ }
993
+ const options = ext === ".vue" ? usable[0] : void 0;
994
+ if (options !== void 0) {
995
+ const lang = /\blang\s*=\s*["']([^"']+)["']/.exec(options[1])?.[1];
996
+ const close = text.indexOf("<\/script>", options.index);
997
+ if (close !== -1) {
998
+ const at = close + 9;
999
+ return {
1000
+ start: at,
1001
+ end: at,
1002
+ text: `\n${lang === void 0 ? "<script setup>" : `<script setup lang="${lang}">`}\n${line}\n<\/script>`
1003
+ };
1004
+ }
1005
+ }
1006
+ return {
1007
+ start: 0,
1008
+ end: 0,
1009
+ text: `${ext === ".vue" ? "<script setup>" : "<script>"}\n${line}\n<\/script>\n\n`
1010
+ };
1011
+ }
1012
+ const front = /^---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n/.exec(text);
1013
+ const at = front === null ? 0 : front[0].length;
1014
+ return {
1015
+ start: at,
1016
+ end: at,
1017
+ text: line + "\n\n"
1018
+ };
1019
+ }
1020
+ function applyEdits(text, edits) {
1021
+ const sorted = [...edits].sort((a, b) => b.start - a.start || b.end - a.end);
1022
+ let out = text;
1023
+ for (const edit of sorted) out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
1024
+ return out;
1025
+ }
1026
+ function positionOf(text, offset) {
1027
+ let line = 1;
1028
+ let lineStart = 0;
1029
+ for (let i = text.indexOf("\n"); i !== -1 && i < offset; i = text.indexOf("\n", i + 1)) {
1030
+ line++;
1031
+ lineStart = i + 1;
1032
+ }
1033
+ return {
1034
+ line,
1035
+ column: offset - lineStart + 1
1036
+ };
1037
+ }
1038
+ async function migrate(ctx, flags) {
1039
+ if (flags.from !== "saas-i18n") {
1040
+ const { migrateLibrary } = await import("./engine-C2ENBRJe.js");
1041
+ return migrateLibrary(ctx, flags);
1042
+ }
1043
+ const project = await loadProject(ctx);
1044
+ const { config, codec } = project;
1045
+ const rel = (file) => rootRelative(config.root, file);
1046
+ const data = {
1047
+ from: flags.from,
1048
+ write: flags.write,
1049
+ files: [],
1050
+ rewritten: 0,
1051
+ renamed: [],
1052
+ manualIds: [],
1053
+ messagesModule: null,
1054
+ entries: {},
1055
+ edits: [],
1056
+ unused: []
1057
+ };
1058
+ const diagnostics = [];
1059
+ const [first, ...others] = flags.catalogs;
1060
+ if (first.locale !== null && first.locale !== config.sourceLocale) throw usage(`the first --catalog is the source locale's (${config.sourceLocale}), not ${first.locale}`);
1061
+ for (const other of others) if (other.locale === config.sourceLocale || !config.locales.includes(other.locale)) throw usage(`--catalog ${other.locale}=${other.file}: ${other.locale} must be a configured locale other than the source (${config.locales.join(", ")})`);
1062
+ const catalogues = [];
1063
+ for (const entry of flags.catalogs) {
1064
+ const file = resolve(ctx.global.cwd, entry.file);
1065
+ let text;
1066
+ try {
1067
+ text = await readFile(file, "utf8");
1068
+ } catch (error) {
1069
+ throw usage(`--catalog ${entry.file}: ${error instanceof Error ? error.message : String(error)}`);
1070
+ }
1071
+ const read = readCatalogue(text, file, entry.locale ?? config.sourceLocale, rel(file));
1072
+ diagnostics.push(...read.diagnostics);
1073
+ catalogues.push(read.catalogue);
1074
+ }
1075
+ const source = catalogues[0];
1076
+ const keys = /* @__PURE__ */ new Map();
1077
+ const takenIds = /* @__PURE__ */ new Map();
1078
+ for (const key of source.texts.keys()) if (isValidManualId(key)) takenIds.set(key, key);
1079
+ for (const [key, text] of source.texts) {
1080
+ const converted = saasToIcu(text);
1081
+ for (const name of converted.currency) diagnostics.push({
1082
+ code: "icu-unknown-currency",
1083
+ severity: "warning",
1084
+ message: `${key}: {${name}:currency} became {${name}}; format the amount with fmt.currencyMinor and pass the string`,
1085
+ file: rel(source.file),
1086
+ id: key
1087
+ });
1088
+ let canonical;
1089
+ try {
1090
+ canonical = codec.canonicalize(converted.icu);
1091
+ } catch (error) {
1092
+ if (!isDiagnosticError(error)) throw error;
1093
+ diagnostics.push({
1094
+ ...error.diagnostic,
1095
+ file: rel(source.file),
1096
+ id: key
1097
+ });
1098
+ continue;
1099
+ }
1100
+ let id = toManualId(key);
1101
+ if (id !== key) {
1102
+ if (takenIds.has(id)) {
1103
+ let n = 2;
1104
+ while (takenIds.has(`${id}_${n}`)) n++;
1105
+ id = `${id}_${n}`;
1106
+ }
1107
+ takenIds.set(id, key);
1108
+ }
1109
+ if (id !== key) data.renamed.push({
1110
+ from: key,
1111
+ to: id
1112
+ });
1113
+ keys.set(key, {
1114
+ key,
1115
+ id,
1116
+ canonical
1117
+ });
1118
+ }
1119
+ const keyList = [...keys.keys()];
1120
+ const messagesPath = join(dirname(source.file), MESSAGES_MODULE);
1121
+ const skip = /* @__PURE__ */ new Set([messagesPath, ...catalogues.map((c) => c.file)]);
1122
+ const listed = await listProjectFiles(config);
1123
+ const plans = [];
1124
+ for (const file of listed.files.slice().sort(compareText)) {
1125
+ if (skip.has(file)) continue;
1126
+ const text = await readFile(file, "utf8");
1127
+ if (!/\.\s*t\b|\{[^}]*\bt\b[^}]*\}\s*=/.test(text) && !text.includes("`") && !keyList.some((key) => key.includes(".") && text.includes(key))) continue;
1128
+ const plan = MODULE_EXTENSIONS.has(extname(file)) ? planModule(file, text, keys, keyList, rel(file)) : planComponent(file, text, keys, keyList);
1129
+ diagnostics.push(...plan.diagnostics);
1130
+ if (plan.rewrites.length > 0 || plan.dynamic.length > 0) plans.push(plan);
1131
+ }
1132
+ const receivers = /* @__PURE__ */ new Set();
1133
+ for (const plan of plans) for (const r of plan.rewrites) receivers.add(r.receiver);
1134
+ const dynamicKeys = /* @__PURE__ */ new Set();
1135
+ const used = /* @__PURE__ */ new Map();
1136
+ const use = (key, file) => {
1137
+ let files = used.get(key);
1138
+ if (files === void 0) used.set(key, files = /* @__PURE__ */ new Set());
1139
+ files.add(rel(file));
1140
+ };
1141
+ for (const plan of plans) {
1142
+ for (const r of plan.rewrites) use(r.key, plan.file);
1143
+ for (const site of plan.dynamic) {
1144
+ if (site.keys.size === 0 && (site.receiver === null || !receivers.has(site.receiver))) continue;
1145
+ for (const key of site.keys) {
1146
+ dynamicKeys.add(key);
1147
+ use(key, messagesPath);
1148
+ }
1149
+ const where = positionOf(plan.text, site.offset);
1150
+ const listedKeys = [...site.keys].sort(compareText);
1151
+ const example = listedKeys.length > 0 ? listedKeys.slice(0, 3).join(", ") + (listedKeys.length > 3 ? ", …" : "") : "";
1152
+ diagnostics.push({
1153
+ code: "migrate-dynamic-key",
1154
+ severity: "warning",
1155
+ message: site.literal ? `${site.expression} names a catalogue key outside a call vi18n migrate rewrites; it is a msg() descriptor in ${rel(messagesPath)}: import { messages } from it and call t(messages[key])` : site.keys.size > 0 ? `${site.expression} is a computed key and was not rewritten; its ${plural(site.keys.size, "key")} (${example}) are msg() descriptors in ${rel(messagesPath)}: import { messages } from it and call t(messages[key])` : `${site.receiver}.t(${site.expression}) uses a computed key that matches no catalogue key and was not rewritten; pass a msg() descriptor (t(messages[key])) instead`,
1156
+ file: rel(plan.file),
1157
+ line: where.line,
1158
+ column: where.column
1159
+ });
1160
+ }
1161
+ }
1162
+ for (const key of keyList) if (!used.has(key)) data.unused.push(key);
1163
+ diagnostics.push(...datePlaceholders(source, keys, used, plans, rel));
1164
+ const outputs = [];
1165
+ for (const plan of plans) {
1166
+ if (plan.rewrites.length === 0) continue;
1167
+ const local = plan.importedT ?? localName(plan.text);
1168
+ const edits = [];
1169
+ for (const r of plan.rewrites) {
1170
+ const info = keys.get(r.key);
1171
+ edits.push({
1172
+ ...r.callee,
1173
+ text: local
1174
+ });
1175
+ const replacement = descriptor(info, r.argument.text, r.markup);
1176
+ edits.push({
1177
+ ...r.argument,
1178
+ text: replacement
1179
+ });
1180
+ const where = positionOf(plan.text, r.callee.start);
1181
+ const to = applyEdits(r.original, [{
1182
+ start: 0,
1183
+ end: r.callee.end - r.callee.start,
1184
+ text: local
1185
+ }, {
1186
+ start: r.argument.start - r.callee.start,
1187
+ end: r.argument.end - r.callee.start,
1188
+ text: replacement
1189
+ }]);
1190
+ data.edits.push({
1191
+ file: rel(plan.file),
1192
+ line: where.line,
1193
+ column: where.column,
1194
+ from: r.original,
1195
+ to
1196
+ });
1197
+ }
1198
+ if (plan.importedT === null) edits.push(importEdit(plan.file, plan.text, local, plan.program));
1199
+ data.rewritten += plan.rewrites.length;
1200
+ outputs.push({
1201
+ file: plan.file,
1202
+ text: applyEdits(plan.text, edits)
1203
+ });
1204
+ }
1205
+ if (dynamicKeys.size > 0) {
1206
+ outputs.push({
1207
+ file: messagesPath,
1208
+ text: messagesModule(dynamicKeys, keys)
1209
+ });
1210
+ data.messagesModule = rel(messagesPath);
1211
+ if (!isIncluded(messagesPath, config)) diagnostics.push({
1212
+ code: "extract-outside-include",
1213
+ severity: "warning",
1214
+ message: `${rel(messagesPath)} is outside project.include, so vi18n extract will not see its msg() descriptors; add its directory to project.include`,
1215
+ file: rel(messagesPath)
1216
+ });
1217
+ }
1218
+ const messages = /* @__PURE__ */ new Map();
1219
+ for (const [key, files] of used) {
1220
+ const info = keys.get(key);
1221
+ messages.set(info.id, {
1222
+ id: info.id,
1223
+ manual: true,
1224
+ canonical: info.canonical,
1225
+ sourceHash: codec.sourceHash(info.canonical),
1226
+ label: codec.label(info.canonical),
1227
+ contexts: [],
1228
+ refs: [...files].sort(compareText),
1229
+ sources: [info.canonical],
1230
+ locations: []
1231
+ });
1232
+ }
1233
+ const translations = /* @__PURE__ */ new Map();
1234
+ for (const catalogue of others.length > 0 ? catalogues.slice(1) : []) {
1235
+ const map = /* @__PURE__ */ new Map();
1236
+ for (const [key, text] of catalogue.texts) {
1237
+ const info = keys.get(key);
1238
+ if (info === void 0 || !used.has(key)) continue;
1239
+ const converted = saasToIcu(text).icu;
1240
+ map.set(info.id, converted);
1241
+ diagnostics.push(...translationProblems(project, catalogue, info, converted, rel(catalogue.file)));
1242
+ }
1243
+ translations.set(catalogue.locale, map);
1244
+ }
1245
+ const loaded = await loadCatalogs(project);
1246
+ const readErrors = loaded.diagnostics.filter((d) => d.code === "catalog-parse" && d.severity === "error");
1247
+ diagnostics.push(...readErrors);
1248
+ const catalogPlans = planCatalogs(project, loaded.set, messages, translations);
1249
+ diagnostics.push(...catalogPlans.diagnostics);
1250
+ for (const [locale, count] of catalogPlans.counts) if (locale !== config.sourceLocale) data.entries[locale] = count;
1251
+ ctx.out.report(diagnostics);
1252
+ const failed = hasErrors(diagnostics);
1253
+ data.files = [...outputs.map((o) => rel(o.file)), ...catalogPlans.changed.map((locale) => rel(catalogPath(config, locale)))];
1254
+ if (flags.write && !failed) {
1255
+ for (const output of outputs) await atomicWrite(output.file, output.text);
1256
+ for (const locale of catalogPlans.changed) {
1257
+ const loadedCatalog = loaded.set.get(locale);
1258
+ await updateCatalogFile(catalogPath(config, locale), loadedCatalog?.hash ?? null, (current) => plannedCatalog(project, locale, current, catalogPlans.sourceCatalog, messages, translations.get(locale)).catalog, {
1259
+ locale,
1260
+ layout: project.layout,
1261
+ isSourceLocale: locale === config.sourceLocale,
1262
+ version: VERSION
1263
+ });
1264
+ }
1265
+ }
1266
+ printPlan(ctx, data, failed);
1267
+ return {
1268
+ exitCode: failed ? EXIT.failure : EXIT.ok,
1269
+ data
1270
+ };
1271
+ }
1272
+ /**
1273
+ * T defect C: saas formatted a `Date` in any placeholder with `dateStyle:
1274
+ * "medium"`;
1275
+ * `migrate` keeps a plain `{x}` as it is, where a `Date` still renders that
1276
+ * way but a date string never formats, and the catalogue does not say a date
1277
+ * is expected. The value's type is not visible here, so a placeholder is
1278
+ * reported when a call passes it something that looks like a `Date`, or when
1279
+ * its name says it holds one.
1280
+ */
1281
+ function datePlaceholders(source, keys, used, plans, rel) {
1282
+ const seen = /* @__PURE__ */ new Map();
1283
+ for (const plan of plans) for (const r of plan.rewrites) for (const name of plainPlaceholders(source.texts.get(r.key) ?? "")) {
1284
+ const at = `${r.key}\0${name}`;
1285
+ if (seen.has(at)) continue;
1286
+ const value = valueExpression(r.original.slice(r.argument.end - r.callee.start), name);
1287
+ if (value === null || !looksLikeDate(value)) continue;
1288
+ const { line } = positionOf(plan.text, r.callee.start);
1289
+ seen.set(at, {
1290
+ expression: value,
1291
+ file: rel(plan.file),
1292
+ line
1293
+ });
1294
+ }
1295
+ const out = [];
1296
+ for (const [key, text] of source.texts) {
1297
+ if (!keys.has(key) || !used.has(key)) continue;
1298
+ for (const name of plainPlaceholders(text)) {
1299
+ const passed = seen.get(`${key}\0${name}`);
1300
+ if (passed === void 0 && !isDateName(name)) continue;
1301
+ const why = passed === void 0 ? "its name says it holds a date" : `${passed.file}:${passed.line} passes ${passed.expression}`;
1302
+ out.push({
1303
+ code: "migrate-date-placeholder",
1304
+ severity: "warning",
1305
+ message: `${key}: {${name}} may receive a Date (${why}); saas formatted it with dateStyle "medium": write {${name}, date, medium} (the same text in English, not in every locale) and pass a Date or epoch milliseconds (new Date(value) for a string), or pass fmt.date(value, { dateStyle: "medium" }) for saas's exact text`,
1306
+ file: rel(source.file),
1307
+ id: key
1308
+ });
1309
+ }
1310
+ }
1311
+ return out;
1312
+ }
1313
+ function usage(message) {
1314
+ return new CliExit(EXIT.usage, [{
1315
+ code: "config-invalid",
1316
+ severity: "error",
1317
+ message
1318
+ }]);
1319
+ }
1320
+ /** A translation that does not fit its source is still written (people fix it), but reported now. */
1321
+ function translationProblems(project, catalogue, info, translation, file) {
1322
+ const { codec } = project;
1323
+ try {
1324
+ return validateTranslation(codec.parse(translation), codec.parse(info.canonical), {
1325
+ locale: catalogue.locale,
1326
+ text: translation
1327
+ }).filter((d) => d.severity === "error").map((d) => ({
1328
+ ...d,
1329
+ code: "catalog-invalid-translation",
1330
+ severity: "warning",
1331
+ message: `${info.key} (${catalogue.locale}): ${d.message}`,
1332
+ file,
1333
+ locale: catalogue.locale,
1334
+ id: info.id
1335
+ }));
1336
+ } catch (error) {
1337
+ if (!isDiagnosticError(error)) throw error;
1338
+ return [{
1339
+ ...error.diagnostic,
1340
+ severity: "warning",
1341
+ message: `${info.key} (${catalogue.locale}): ${error.diagnostic.message}`,
1342
+ file,
1343
+ locale: catalogue.locale,
1344
+ id: info.id
1345
+ }];
1346
+ }
1347
+ }
1348
+ /**
1349
+ * A locale's catalog after migration: the migrated messages added the way
1350
+ * the dev writer adds them (`add-only`, so nothing else changes), then the
1351
+ * catalogue's texts set on entries that have no translation yet — a
1352
+ * translation someone already made in vi18n is never replaced. `notes` are
1353
+ * translator comments for entries left empty (a text the codemod could not
1354
+ * convert, kept for a person to finish).
1355
+ */
1356
+ function plannedCatalog(project, locale, current, sourceCatalog, messages, translations, notes) {
1357
+ const { config, codec, layout } = project;
1358
+ const synced = syncCatalog({
1359
+ config,
1360
+ codec,
1361
+ messages,
1362
+ locale,
1363
+ catalog: current,
1364
+ sourceCatalog,
1365
+ options: {
1366
+ mode: "add-only",
1367
+ yes: false
1368
+ }
1369
+ });
1370
+ let translated = 0;
1371
+ let catalog = synced.catalog;
1372
+ if (translations !== void 0 && translations.size > 0 || notes !== void 0 && notes.size > 0) {
1373
+ let changed = false;
1374
+ const entries = catalog.entries.map((entry) => {
1375
+ const key = entryKey(entry, layout, codec);
1376
+ if (key === null) return entry;
1377
+ const message = messages.get(key);
1378
+ if (message === void 0 || entry.manualId !== (message.manual ? key : null)) return entry;
1379
+ if (entry.translation !== "" || !sourceMatches(entry, layout, message, codec)) return entry;
1380
+ const text = translations?.get(key);
1381
+ if (text !== void 0) {
1382
+ translated++;
1383
+ changed = true;
1384
+ return {
1385
+ ...entry,
1386
+ translation: text,
1387
+ state: "translated",
1388
+ fuzzy: false
1389
+ };
1390
+ }
1391
+ const added = (notes?.get(key) ?? []).filter((note) => !entry.comments.includes(note));
1392
+ if (added.length === 0) return entry;
1393
+ changed = true;
1394
+ return {
1395
+ ...entry,
1396
+ comments: [...entry.comments, ...added]
1397
+ };
1398
+ });
1399
+ if (changed) catalog = {
1400
+ ...catalog,
1401
+ entries
1402
+ };
1403
+ }
1404
+ return {
1405
+ catalog,
1406
+ added: synced.stats.added,
1407
+ translated,
1408
+ diagnostics: synced.diagnostics
1409
+ };
1410
+ }
1411
+ function planCatalogs(project, set, messages, translations, notes) {
1412
+ const { config } = project;
1413
+ const sourceCatalog = set.get(config.sourceLocale)?.catalog ?? null;
1414
+ const changed = [];
1415
+ const counts = /* @__PURE__ */ new Map();
1416
+ const diagnostics = [];
1417
+ if (messages.size === 0) return {
1418
+ changed,
1419
+ counts,
1420
+ sourceCatalog,
1421
+ diagnostics
1422
+ };
1423
+ for (const locale of config.locales) {
1424
+ const current = set.get(locale)?.catalog ?? null;
1425
+ const planned = plannedCatalog(project, locale, current, sourceCatalog, messages, translations.get(locale), notes?.get(locale));
1426
+ for (const d of planned.diagnostics) diagnostics.push({
1427
+ ...d,
1428
+ locale: d.locale ?? locale,
1429
+ file: d.file ?? rootRelative(config.root, catalogPath(config, locale))
1430
+ });
1431
+ counts.set(locale, planned.translated);
1432
+ if ((current === null ? null : serializeCatalog(current, {
1433
+ isSourceLocale: locale === config.sourceLocale,
1434
+ version: VERSION
1435
+ })) !== serializeCatalog(planned.catalog, {
1436
+ isSourceLocale: locale === config.sourceLocale,
1437
+ version: VERSION
1438
+ })) changed.push(locale);
1439
+ }
1440
+ return {
1441
+ changed,
1442
+ counts,
1443
+ sourceCatalog,
1444
+ diagnostics
1445
+ };
1446
+ }
1447
+ /** `export const messages = { "<key>": msg({ id, message }) }`, keyed by the catalogue key the code computes. */
1448
+ function messagesModule(dynamicKeys, keys) {
1449
+ const lines = [
1450
+ "// Generated by vi18n migrate --from saas-i18n: catalogue keys the code computes at",
1451
+ "// runtime, as msg() descriptors so vi18n extracts them. Look one up with",
1452
+ "// t(messages[key]); keys stay the catalogue's, ids are vi18n manual ids.",
1453
+ `import { msg } from "${RUNTIME}";`,
1454
+ "",
1455
+ "export const messages = {"
1456
+ ];
1457
+ for (const key of [...dynamicKeys].sort(compareText)) {
1458
+ const info = keys.get(key);
1459
+ lines.push(` ${JSON.stringify(key)}: msg(${descriptor(info, "\"")}),`);
1460
+ }
1461
+ lines.push("} as const;", "");
1462
+ return lines.join("\n");
1463
+ }
1464
+ function printPlan(ctx, data, failed) {
1465
+ for (const edit of data.edits) ctx.out.result(`${edit.file}:${edit.line}:${edit.column}\n - ${edit.from}\n + ${edit.to}`);
1466
+ if (data.renamed.length > 0) ctx.out.result(`renamed to valid manual ids: ${data.renamed.map((r) => `${r.from} → ${r.to}`).join(", ")}`);
1467
+ if (data.messagesModule !== null) ctx.out.result(`msg() table for computed keys: ${data.messagesModule}`);
1468
+ for (const [locale, count] of Object.entries(data.entries)) ctx.out.result(`${locale}: ${plural(count, "translation")} for the vi18n catalog`);
1469
+ if (data.unused.length > 0) ctx.out.result(`${plural(data.unused.length, "catalogue key")} no code uses: ${data.unused.join(", ")}`);
1470
+ const verb = data.write && !failed ? "rewrote" : "would rewrite";
1471
+ ctx.out.result(`${verb} ${plural(data.rewritten, "call")} in ${plural(data.files.length, "file")}` + (data.write ? failed ? " (nothing written: fix the errors first)" : "" : "; run with --write to apply"));
1472
+ }
1473
+ async function atomicWrite(path, content) {
1474
+ const temp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
1475
+ try {
1476
+ await writeFile(temp, content);
1477
+ await rename(temp, path);
1478
+ } catch (error) {
1479
+ await rm(temp, { force: true });
1480
+ throw error;
1481
+ }
1482
+ }
1483
+ //#endregion
1484
+ export { toManualId as _, encodeMarkup as a, migrate as c, positionOf as d, readString as f, skipSpace as g, skipBalanced as h, atomicWrite as i, planCatalogs as l, saasToIcu as m, applyEdits as n, importLineEdit as o, readTemplate as p, argumentEnd as r, isDateName as s, MESSAGES_MODULE as t, plannedCatalog as u, usage as v, vueRegions as y };