@menapo/vi18n 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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-D34P-S--.js +1 -0
  6. package/dist/check-DyrAbv73.js +201 -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-B5Mz6WJm.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-B7p40M__.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-DGsMbcTZ.js +103 -0
  25. package/dist/extract-Dujlsgnk.js +298 -0
  26. package/dist/formats-KPIg-08V.js +781 -0
  27. package/dist/i18next-CrUnKFfh.js +1596 -0
  28. package/dist/i18next-Rgo-v9l0.js +1 -0
  29. package/dist/import-Cwdhcywt.js +225 -0
  30. package/dist/index.d.ts +156 -0
  31. package/dist/index.js +239 -0
  32. package/dist/init-BecCTvKi.js +792 -0
  33. package/dist/library-D5CWvTQs.js +1882 -0
  34. package/dist/lingui--PcOOYry.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-Cq1Nuu-Q.js +31 -0
  41. package/dist/match-BGr8EXB3.js +89 -0
  42. package/dist/mcp-Cf0T7qPW.js +28 -0
  43. package/dist/merge-driver-CpiiSVIc.js +422 -0
  44. package/dist/messages-Cxa-5tR0.js +166 -0
  45. package/dist/migrate-CntqvrW4.js +1 -0
  46. package/dist/migrate-dPgq9uzy.js +1484 -0
  47. package/dist/openai-CTaMDcl-.js +346 -0
  48. package/dist/output-DsaHKHEt.js +164 -0
  49. package/dist/parse.d.ts +62 -0
  50. package/dist/parse.js +156 -0
  51. package/dist/pipeline-C7g11THL.js +2031 -0
  52. package/dist/pipeline-wxS0SApG.js +1 -0
  53. package/dist/project-Dwx7iPm6.js +381 -0
  54. package/dist/project-pXipT8iG.js +1046 -0
  55. package/dist/pseudo-DRv30iN5.js +145 -0
  56. package/dist/raw-C4Y6vyGY.js +4708 -0
  57. package/dist/react-intl-CaBVwJ9u.js +519 -0
  58. package/dist/restore-Btq66Tid.js +175 -0
  59. package/dist/rewrite-CiTRAICl.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-DYD_VXKy.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-BDkykMjb.js +11823 -0
  68. package/dist/server-qv3mz70d.js +10485 -0
  69. package/dist/server.d.ts +169 -0
  70. package/dist/server.js +1234 -0
  71. package/dist/sitemap-DV3o8QD7.js +159 -0
  72. package/dist/sources-D19czDSA.js +2242 -0
  73. package/dist/status-DZArW5aE.js +149 -0
  74. package/dist/stdio-CVaJfxA7.js +29 -0
  75. package/dist/stdio-FPOKQAgu.js +95 -0
  76. package/dist/sync-CcVzger_.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-DZR3MkSA.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-BlyLGlOa.js +28 -0
  93. package/dist/vite-scan-B_YW4X_a.js +242 -0
  94. package/dist/vite.d.ts +34 -0
  95. package/dist/vite.js +7558 -0
  96. package/dist/vue-i18n-CEMnpv7k.js +651 -0
  97. package/dist/which-C6PFI1Dm.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,531 @@
1
+ import { o as isDiagnosticError } from "./types-D3QGM8XO.js";
2
+ import { r as validateTranslation } from "./validate-BxpupdAf.js";
3
+ import { m as updateCatalogFile, s as catalogPath } from "./library-D5CWvTQs.js";
4
+ import { i as listProjectFiles, r as isIncluded } from "./project-pXipT8iG.js";
5
+ import { t as VERSION } from "./version-BlyLGlOa.js";
6
+ import { i as rootRelative, r as plural } from "./output-DsaHKHEt.js";
7
+ import { t as EXIT } from "./types-wSh9fCBY.js";
8
+ import { a as loadCatalogs, i as hasErrors, n as compareText, o as loadProject } from "./project-Dwx7iPm6.js";
9
+ import { _ as toManualId, a as encodeMarkup, d as positionOf, i as atomicWrite, l as planCatalogs, t as MESSAGES_MODULE, u as plannedCatalog, v as usage } from "./migrate-dPgq9uzy.js";
10
+ import { f as jsString, n as RUNTIME_MODULE, r as renderPlan } from "./rewrite-CiTRAICl.js";
11
+ import { basename, dirname, extname, join, relative, resolve } from "node:path";
12
+ import { readFile, readdir, stat } from "node:fs/promises";
13
+ //#region src/cli/commands/migrate/engine.ts
14
+ /**
15
+ * `vi18n migrate --from i18next | react-intl | lingui | vue-i18n` (INTERNALS
16
+ * §2.14, PLAN §5.19): the driver every library adapter shares.
17
+ *
18
+ * 1. `--catalog` files and directories become locale catalogs (the locale
19
+ * read from the path where no `<locale>=` names it); the adapter reads
20
+ * them and converts each text to ICU, keeping what it cannot convert;
21
+ * 2. the adapter plans each file under `project.include`: call sites become
22
+ * edits rendered later, and what it cannot rewrite is reported (keys a
23
+ * reported site names go to a `msg()` table, so no translation is lost);
24
+ * 3. every used message gets an id: a content id (the text is the message),
25
+ * unless two messages share a text but not their translations, or
26
+ * `--keep-ids` keeps a developer-chosen key: then a manual id;
27
+ * 4. edits are rendered, imports added, and the vi18n catalogs planned the
28
+ * way `--from saas-i18n` does (add-only; existing translations win).
29
+ *
30
+ * Without `--write` nothing is written; any error writes nothing.
31
+ */
32
+ const LOCALE_LIKE = /^[a-z]{2,3}(?:[-_][A-Za-z0-9]{2,8})*$/i;
33
+ function normalizeLocale(name) {
34
+ return name.replace(/_/g, "-").toLowerCase();
35
+ }
36
+ /** The configured locale a path names (basename first, then directories, nearest first). */
37
+ function localeOfPath(path, locales) {
38
+ const byName = new Map(locales.map((l) => [normalizeLocale(l), l]));
39
+ const parts = path.split(/[\\/]/).filter((p) => p !== "");
40
+ const base = (parts.pop() ?? "").replace(/\.[^.]+$/, "");
41
+ const baseLocale = byName.get(normalizeLocale(base));
42
+ if (baseLocale !== void 0) return {
43
+ locale: baseLocale,
44
+ fromDirectory: false
45
+ };
46
+ let unknown = LOCALE_LIKE.test(base) && base.length <= 3 ? base : null;
47
+ for (let i = parts.length - 1; i >= 0; i--) {
48
+ const part = parts[i];
49
+ const locale = byName.get(normalizeLocale(part));
50
+ if (locale !== void 0) return {
51
+ locale,
52
+ fromDirectory: true
53
+ };
54
+ if (unknown === null && LOCALE_LIKE.test(part) && part.length <= 5 && /^[a-z]/.test(part)) unknown = part;
55
+ }
56
+ return unknown === null ? null : { unknown };
57
+ }
58
+ async function listFiles(dir, extensions) {
59
+ const out = [];
60
+ const walk = async (current, depth) => {
61
+ if (depth > 6) return;
62
+ const entries = await readdir(current, { withFileTypes: true });
63
+ for (const entry of entries.sort((a, b) => compareText(a.name, b.name))) {
64
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
65
+ const path = join(current, entry.name);
66
+ if (entry.isDirectory()) await walk(path, depth + 1);
67
+ else if (extensions.includes(extname(entry.name).toLowerCase())) out.push(path);
68
+ }
69
+ };
70
+ await walk(dir, 0);
71
+ return out;
72
+ }
73
+ /** `--catalog` entries as files with locales (and i18next namespaces). */
74
+ async function resolveCatalogFiles(cwd, config, entries, extensions, env) {
75
+ const out = [];
76
+ const seen = /* @__PURE__ */ new Set();
77
+ const add = (file) => {
78
+ if (seen.has(file.file)) return;
79
+ seen.add(file.file);
80
+ out.push(file);
81
+ };
82
+ const configured = (locale) => {
83
+ const match = config.locales.find((l) => normalizeLocale(l) === normalizeLocale(locale));
84
+ if (match === void 0) throw usage(`--catalog ${locale}=…: ${locale} is not a configured locale (${config.locales.join(", ")})`);
85
+ return match;
86
+ };
87
+ for (const [index, entry] of entries.entries()) {
88
+ const path = resolve(cwd, entry.file);
89
+ let info;
90
+ try {
91
+ info = await stat(path);
92
+ } catch (error) {
93
+ throw usage(`--catalog ${entry.file}: ${error instanceof Error ? error.message : String(error)}`);
94
+ }
95
+ if (info.isDirectory()) {
96
+ const files = await listFiles(path, extensions);
97
+ if (files.length === 0) throw usage(`--catalog ${entry.file}: no ${extensions.join(", ")} files in it`);
98
+ for (const file of files) {
99
+ if (entry.locale !== null) {
100
+ add({
101
+ locale: configured(entry.locale),
102
+ file,
103
+ namespace: basename(file, extname(file))
104
+ });
105
+ continue;
106
+ }
107
+ const found = localeOfPath(relative(path, file), config.locales);
108
+ if (found !== null && "locale" in found) add({
109
+ locale: found.locale,
110
+ file,
111
+ namespace: found.fromDirectory ? basename(file, extname(file)) : null
112
+ });
113
+ else env.report("migrate-manual", found === null ? `${env.rel(file)} names no configured locale and was not read; pass it as --catalog <locale>=<file>` : `${env.rel(file)} is a ${found.unknown} catalog, which is not a configured locale: its translations were not migrated (add ${found.unknown} to locales and run again)`, { file });
114
+ }
115
+ continue;
116
+ }
117
+ if (entry.locale !== null) {
118
+ add({
119
+ locale: configured(entry.locale),
120
+ file: path,
121
+ namespace: null
122
+ });
123
+ continue;
124
+ }
125
+ const found = localeOfPath(relative(cwd, path), config.locales);
126
+ if (found !== null && "locale" in found) add({
127
+ locale: found.locale,
128
+ file: path,
129
+ namespace: found.fromDirectory ? basename(path, extname(path)) : null
130
+ });
131
+ else if (index === 0) add({
132
+ locale: config.sourceLocale,
133
+ file: path,
134
+ namespace: null
135
+ });
136
+ else throw usage(`--catalog ${entry.file}: the path names no configured locale; write <locale>=${entry.file}`);
137
+ }
138
+ return out;
139
+ }
140
+ /** Lowercase words of a text, for a readable manual id segment. */
141
+ function slug(text, max = 32) {
142
+ const words = text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().match(/[a-z0-9]+/g);
143
+ if (words === null) return "";
144
+ let out = "";
145
+ for (const word of words) {
146
+ const next = out === "" ? word : `${out}_${word}`;
147
+ if (next.length > max) break;
148
+ out = next;
149
+ }
150
+ return out || words[0].slice(0, max);
151
+ }
152
+ /** Whether merging these messages into one content id would drop a translation. */
153
+ function translationsDiffer(group) {
154
+ const locales = new Set(group.flatMap((d) => [...d.translations.keys()]));
155
+ for (const locale of locales) {
156
+ const texts = /* @__PURE__ */ new Set();
157
+ for (const def of group) {
158
+ const translated = def.translations.get(locale);
159
+ if (translated === void 0) continue;
160
+ texts.add("icu" in translated ? translated.icu : `\0${translated.unconverted}`);
161
+ }
162
+ if (texts.size > 1) return true;
163
+ }
164
+ return false;
165
+ }
166
+ function assignIds(keys, defs, keepIds, codec) {
167
+ const groups = /* @__PURE__ */ new Map();
168
+ for (const key of keys) {
169
+ const def = defs.get(key);
170
+ if (def === void 0) continue;
171
+ let group = groups.get(def.canonical);
172
+ if (group === void 0) groups.set(def.canonical, group = []);
173
+ group.push(def);
174
+ }
175
+ const ids = /* @__PURE__ */ new Map();
176
+ const reasons = /* @__PURE__ */ new Map();
177
+ const taken = /* @__PURE__ */ new Set();
178
+ const pending = [];
179
+ for (const group of groups.values()) {
180
+ const conflict = group.length > 1 && translationsDiffer(group);
181
+ for (const def of group) if (conflict) {
182
+ reasons.set(def.key, "same text as another message, other translations");
183
+ pending.push(def);
184
+ } else if (keepIds && def.named) {
185
+ reasons.set(def.key, "--keep-ids");
186
+ pending.push(def);
187
+ } else ids.set(def.key, {
188
+ id: codec.label(def.canonical),
189
+ manual: false
190
+ });
191
+ }
192
+ const base = (def) => def.named ? toManualId(def.key.replace(/:/g, ".")) : toManualId(`${slug(def.hint ?? def.context ?? "") || "msg"}.${slug(def.source) || "message"}`);
193
+ for (const def of pending) {
194
+ const id = base(def);
195
+ if (def.named && id === def.key) taken.add(id);
196
+ }
197
+ for (const def of pending.sort((a, b) => compareText(a.key, b.key))) {
198
+ let id = base(def);
199
+ if (!(def.named && id === def.key)) {
200
+ if (taken.has(id)) {
201
+ let n = 2;
202
+ while (taken.has(`${id}_${n}`)) n++;
203
+ id = `${id}_${n}`;
204
+ }
205
+ taken.add(id);
206
+ }
207
+ ids.set(def.key, {
208
+ id,
209
+ manual: true
210
+ });
211
+ }
212
+ return {
213
+ ids,
214
+ reasons
215
+ };
216
+ }
217
+ async function createAdapter(library) {
218
+ switch (library) {
219
+ case "i18next": return (await import("./i18next-Rgo-v9l0.js")).createI18nextAdapter();
220
+ case "react-intl": return (await import("./react-intl-CaBVwJ9u.js")).createReactIntlAdapter();
221
+ case "lingui": return (await import("./lingui--PcOOYry.js")).createLinguiAdapter();
222
+ case "vue-i18n": return (await import("./vue-i18n-CEMnpv7k.js")).createVueI18nAdapter();
223
+ }
224
+ }
225
+ /** Where the `msg()` table goes: beside the source catalog when that is extracted, else in the first include root. */
226
+ function messagesPath(config, files) {
227
+ const source = files.find((f) => f.locale === config.sourceLocale);
228
+ if (source !== void 0) {
229
+ const beside = join(dirname(source.file), MESSAGES_MODULE);
230
+ if (isIncluded(beside, config)) return beside;
231
+ }
232
+ return join(config.project.include[0] ?? config.root, MESSAGES_MODULE);
233
+ }
234
+ async function migrateLibrary(ctx, flags) {
235
+ const library = flags.from;
236
+ const project = await loadProject(ctx);
237
+ const { config, codec } = project;
238
+ const rel = (file) => rootRelative(config.root, file);
239
+ const diagnostics = [];
240
+ const defs = /* @__PURE__ */ new Map();
241
+ const failed = /* @__PURE__ */ new Set();
242
+ const env = {
243
+ config,
244
+ codec,
245
+ library,
246
+ rel,
247
+ diagnostics,
248
+ define(def) {
249
+ if (defs.has(def.key)) return true;
250
+ if (failed.has(def.key)) return false;
251
+ try {
252
+ defs.set(def.key, {
253
+ ...def,
254
+ canonical: codec.canonicalize(def.source)
255
+ });
256
+ return true;
257
+ } catch (error) {
258
+ if (!isDiagnosticError(error)) throw error;
259
+ failed.add(def.key);
260
+ diagnostics.push({
261
+ code: "migrate-unconverted",
262
+ severity: "warning",
263
+ message: `${def.key}: the source text is not valid ICU after conversion (${error.diagnostic.message}); its call sites were not rewritten: ${JSON.stringify(def.source)}`,
264
+ file: rel(def.file),
265
+ ...def.line > 0 ? { line: def.line } : {},
266
+ id: def.key
267
+ });
268
+ return false;
269
+ }
270
+ },
271
+ message: (key) => defs.get(key),
272
+ report(code, message, at, severity = "warning") {
273
+ const diagnostic = {
274
+ code,
275
+ severity,
276
+ message,
277
+ file: rel(at.file)
278
+ };
279
+ if (at.text !== void 0 && at.offset !== void 0) Object.assign(diagnostic, positionOf(at.text, at.offset));
280
+ else if (at.line !== void 0 && at.line > 0) diagnostic.line = at.line;
281
+ diagnostics.push(diagnostic);
282
+ }
283
+ };
284
+ const adapter = await createAdapter(library);
285
+ const data = {
286
+ from: library,
287
+ write: flags.write,
288
+ files: [],
289
+ rewritten: 0,
290
+ renamed: [],
291
+ manualIds: [],
292
+ messagesModule: null,
293
+ entries: {},
294
+ edits: [],
295
+ unused: []
296
+ };
297
+ const catalogFiles = await resolveCatalogFiles(ctx.global.cwd, config, flags.catalogs, adapter.catalogExtensions, env);
298
+ if (adapter.catalogsRequired && !catalogFiles.some((f) => f.locale === config.sourceLocale)) throw usage(`--catalog: no ${config.sourceLocale} catalog found; ${library} keeps the source text in the catalog of the source locale (${config.sourceLocale})`);
299
+ adapter.load(catalogFiles, env);
300
+ const table = messagesPath(config, catalogFiles);
301
+ const skip = /* @__PURE__ */ new Set([table, ...catalogFiles.map((f) => f.file)]);
302
+ const listed = await listProjectFiles(config);
303
+ const plans = [];
304
+ for (const file of listed.files.slice().sort(compareText)) {
305
+ if (skip.has(file)) continue;
306
+ const text = await readFile(file, "utf8");
307
+ if (!adapter.mentions(text)) continue;
308
+ if (!adapter.extensions.has(extname(file))) {
309
+ env.report("migrate-manual", `uses ${library}, but ${extname(file)} files are not rewritten by vi18n migrate --from ${library}: convert it by hand`, { file });
310
+ continue;
311
+ }
312
+ const plan = adapter.planFile(file, text, env);
313
+ if (plan !== null && (plan.edits.length > 0 || plan.manual.length > 0)) plans.push(plan);
314
+ }
315
+ const used = /* @__PURE__ */ new Map();
316
+ const use = (key, file) => {
317
+ let files = used.get(key);
318
+ if (files === void 0) used.set(key, files = /* @__PURE__ */ new Set());
319
+ files.add(rel(file));
320
+ };
321
+ const dynamicKeys = /* @__PURE__ */ new Set();
322
+ for (const plan of plans) {
323
+ for (const key of renderPlan(plan, () => "").used) use(key, plan.file);
324
+ for (const site of plan.manual) {
325
+ const where = positionOf(plan.text, site.offset);
326
+ const keys = site.keys.filter((key) => defs.has(key));
327
+ for (const key of keys) {
328
+ dynamicKeys.add(key);
329
+ use(key, table);
330
+ }
331
+ const sorted = [...new Set(keys)].sort(compareText);
332
+ const example = sorted.slice(0, 3).join(", ") + (sorted.length > 3 ? ", …" : "");
333
+ diagnostics.push({
334
+ code: keys.length > 0 ? "migrate-dynamic-key" : "migrate-manual",
335
+ severity: "warning",
336
+ message: keys.length > 0 ? `${site.message}; ${plural(sorted.length, "message")} (${example}) ${sorted.length === 1 ? "is a msg() descriptor" : "are msg() descriptors"} in ${rel(table)}: import { messages } from it and call t(messages[key])` : site.message,
337
+ file: rel(plan.file),
338
+ line: where.line,
339
+ column: where.column
340
+ });
341
+ }
342
+ }
343
+ const usedKeys = [...used.keys()].sort(compareText);
344
+ const { ids, reasons } = assignIds(usedKeys, defs, flags.keepIds, codec);
345
+ data.unused.push(...adapter.unusedKeys());
346
+ data.unused.sort(compareText);
347
+ for (const key of usedKeys) {
348
+ const assigned = ids.get(key);
349
+ if (assigned?.manual === true) data.manualIds.push({
350
+ key,
351
+ id: assigned.id,
352
+ reason: reasons.get(key) ?? ""
353
+ });
354
+ }
355
+ const describe = (key, quote, markup) => {
356
+ const def = defs.get(key);
357
+ const assigned = ids.get(key);
358
+ const parts = [];
359
+ if (assigned.manual) parts.push(`id: ${jsString(assigned.id, quote)}`);
360
+ parts.push(`message: ${jsString(def.canonical, quote)}`);
361
+ if (def.context !== null) parts.push(`context: ${jsString(def.context, quote)}`);
362
+ const code = parts.length === 1 ? jsString(def.canonical, quote) : `{ ${parts.join(", ")} }`;
363
+ return encodeMarkup(code, markup);
364
+ };
365
+ const outputs = [];
366
+ for (const plan of plans) {
367
+ if (plan.edits.length === 0) continue;
368
+ const rendered = renderPlan(plan, describe);
369
+ for (const edit of rendered.listed) {
370
+ const where = positionOf(plan.text, edit.start);
371
+ data.edits.push({
372
+ file: rel(plan.file),
373
+ line: where.line,
374
+ column: where.column,
375
+ from: edit.from,
376
+ to: edit.to
377
+ });
378
+ }
379
+ data.rewritten += rendered.rewritten;
380
+ if (rendered.text !== plan.text) outputs.push({
381
+ file: plan.file,
382
+ text: rendered.text
383
+ });
384
+ }
385
+ if (dynamicKeys.size > 0) {
386
+ outputs.push({
387
+ file: table,
388
+ text: messagesModule(library, [...dynamicKeys], defs, describe)
389
+ });
390
+ data.messagesModule = rel(table);
391
+ if (!isIncluded(table, config)) diagnostics.push({
392
+ code: "extract-outside-include",
393
+ severity: "warning",
394
+ message: `${rel(table)} is outside project.include, so vi18n extract will not see its msg() descriptors; add its directory to project.include`,
395
+ file: rel(table)
396
+ });
397
+ }
398
+ const messages = /* @__PURE__ */ new Map();
399
+ const translations = /* @__PURE__ */ new Map();
400
+ const notes = /* @__PURE__ */ new Map();
401
+ for (const key of usedKeys) {
402
+ const def = defs.get(key);
403
+ const assigned = ids.get(key);
404
+ const refs = [...used.get(key) ?? []];
405
+ const existing = messages.get(assigned.id);
406
+ if (existing === void 0) messages.set(assigned.id, {
407
+ id: assigned.id,
408
+ manual: assigned.manual,
409
+ canonical: def.canonical,
410
+ sourceHash: codec.sourceHash(def.canonical),
411
+ label: codec.label(def.canonical),
412
+ contexts: def.context === null ? [] : [def.context],
413
+ refs: refs.sort(compareText),
414
+ sources: [def.canonical],
415
+ locations: []
416
+ });
417
+ else {
418
+ existing.refs = [.../* @__PURE__ */ new Set([...existing.refs, ...refs])].sort(compareText);
419
+ if (def.context !== null && !existing.contexts.includes(def.context)) existing.contexts = [...existing.contexts, def.context].sort(compareText);
420
+ }
421
+ for (const [locale, translated] of def.translations) {
422
+ if (locale === config.sourceLocale || !config.locales.includes(locale)) continue;
423
+ if ("icu" in translated) {
424
+ let map = translations.get(locale);
425
+ if (map === void 0) translations.set(locale, map = /* @__PURE__ */ new Map());
426
+ if (map.has(assigned.id)) continue;
427
+ map.set(assigned.id, translated.icu);
428
+ diagnostics.push(...translationProblems(codec, def, locale, translated.icu, assigned.id, rel));
429
+ } else {
430
+ let map = notes.get(locale);
431
+ if (map === void 0) notes.set(locale, map = /* @__PURE__ */ new Map());
432
+ const note = `vi18n migrate: ${library} text not converted (${translated.reason}): ${translated.unconverted.replace(/\r?\n/g, "\\n")}`;
433
+ map.set(assigned.id, [...map.get(assigned.id) ?? [], note]);
434
+ diagnostics.push({
435
+ code: "migrate-unconverted",
436
+ severity: "warning",
437
+ message: `${def.key} (${locale}): ${translated.reason}; the entry stays untranslated with the original as a translator comment: ${JSON.stringify(translated.unconverted)}`,
438
+ file: rel(translated.file ?? def.file),
439
+ ...translated.file !== void 0 && (translated.line ?? 0) > 0 ? { line: translated.line } : {},
440
+ locale,
441
+ id: assigned.id
442
+ });
443
+ }
444
+ }
445
+ }
446
+ const loaded = await loadCatalogs(project);
447
+ diagnostics.push(...loaded.diagnostics.filter((d) => d.code === "catalog-parse" && d.severity === "error"));
448
+ const catalogPlans = planCatalogs(project, loaded.set, messages, translations, notes);
449
+ diagnostics.push(...catalogPlans.diagnostics);
450
+ for (const [locale, count] of catalogPlans.counts) if (locale !== config.sourceLocale) data.entries[locale] = count;
451
+ ctx.out.report(diagnostics);
452
+ const failedRun = hasErrors(diagnostics);
453
+ data.files = [...outputs.map((o) => rel(o.file)), ...catalogPlans.changed.map((locale) => rel(catalogPath(config, locale)))];
454
+ if (flags.write && !failedRun) {
455
+ for (const output of outputs) await atomicWrite(output.file, output.text);
456
+ for (const locale of catalogPlans.changed) await updateCatalogFile(catalogPath(config, locale), loaded.set.get(locale)?.hash ?? null, (current) => plannedCatalog(project, locale, current, catalogPlans.sourceCatalog, messages, translations.get(locale), notes.get(locale)).catalog, {
457
+ locale,
458
+ layout: project.layout,
459
+ isSourceLocale: locale === config.sourceLocale,
460
+ version: VERSION
461
+ });
462
+ }
463
+ printPlan(ctx, data, failedRun);
464
+ return {
465
+ exitCode: failedRun ? EXIT.failure : EXIT.ok,
466
+ data
467
+ };
468
+ }
469
+ /** A translation that does not fit its source is still written (people fix it), but reported now. */
470
+ function translationProblems(codec, def, locale, translation, id, rel) {
471
+ const at = {
472
+ file: rel(def.file),
473
+ locale,
474
+ id
475
+ };
476
+ try {
477
+ return validateTranslation(codec.parse(translation), codec.parse(def.canonical), {
478
+ locale,
479
+ text: translation
480
+ }).filter((d) => d.severity === "error").map((d) => ({
481
+ ...d,
482
+ ...at,
483
+ code: "catalog-invalid-translation",
484
+ severity: "warning",
485
+ message: `${def.key} (${locale}): ${d.message}`
486
+ }));
487
+ } catch (error) {
488
+ if (!isDiagnosticError(error)) throw error;
489
+ return [{
490
+ ...error.diagnostic,
491
+ ...at,
492
+ severity: "warning",
493
+ message: `${def.key} (${locale}): ${error.diagnostic.message}`
494
+ }];
495
+ }
496
+ }
497
+ /** `export const messages = { "<key>": msg(…) }`, keyed by the old library's key the code computes. */
498
+ function messagesModule(library, keys, defs, describe) {
499
+ const lines = [
500
+ `// Generated by vi18n migrate --from ${library}: messages the code looks up by a`,
501
+ "// computed key, or at call sites vi18n migrate left for a person, as msg()",
502
+ "// descriptors so vi18n extract keeps them. Look one up with t(messages[key]).",
503
+ `import { msg } from "${RUNTIME_MODULE}";`,
504
+ "",
505
+ "export const messages = {"
506
+ ];
507
+ for (const key of [...new Set(keys)].sort(compareText)) {
508
+ if (!defs.has(key)) continue;
509
+ lines.push(` ${JSON.stringify(key)}: msg(${describe(key, "\"", null)}),`);
510
+ }
511
+ lines.push("} as const;", "");
512
+ return lines.join("\n");
513
+ }
514
+ function printPlan(ctx, data, failed) {
515
+ const indent = (text) => text.replace(/\n/g, "\n ");
516
+ for (const edit of data.edits) {
517
+ const lines = [`${edit.file}:${edit.line}:${edit.column}`];
518
+ const show = (text) => indent(text.replace(/^[ \t]+/, "").replace(/\n$/, ""));
519
+ if (edit.from !== "") lines.push(` - ${show(edit.from)}`);
520
+ if (edit.to !== "") lines.push(` + ${show(edit.to)}`);
521
+ ctx.out.result(lines.join("\n"));
522
+ }
523
+ if (data.manualIds.length > 0) ctx.out.result(`manual ids: ${data.manualIds.map((m) => `${m.key} → ${m.id} (${m.reason})`).join(", ")}`);
524
+ if (data.messagesModule !== null) ctx.out.result(`msg() table: ${data.messagesModule}`);
525
+ for (const [locale, count] of Object.entries(data.entries)) ctx.out.result(`${locale}: ${plural(count, "translation")} for the vi18n catalog`);
526
+ if (data.unused.length > 0) ctx.out.result(`${plural(data.unused.length, "catalog key")} no code uses: ${data.unused.join(", ")}`);
527
+ const verb = data.write && !failed ? "rewrote" : "would rewrite";
528
+ 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"));
529
+ }
530
+ //#endregion
531
+ export { migrateLibrary };
@@ -0,0 +1,43 @@
1
+ import "./types-DsdWBG0b.js";
2
+ //#region src/runtime/devtools.d.ts
3
+ /** The part of `import.meta.hot` the editor uses. */
4
+ interface DevHot {
5
+ on(event: string, listener: (data: never) => void): void;
6
+ send(event: string, data?: unknown): void;
7
+ }
8
+ /**
9
+ * Installs the editor on the page: the `seen` recorder on the page scope,
10
+ * the Alt+click listener, the HMR reply listener and the dock's bridge.
11
+ * Called by the plugin's dev client boot (`devtools: true`) with its
12
+ * `import.meta.hot`; a no-op outside a document or without HMR.
13
+ */
14
+ export declare function inContext(hot: DevHot | undefined, endpoint: string): void;
15
+ //#endregion
16
+ //#region src/runtime/loader.d.ts
17
+ /**
18
+ * The client engine behind the global state (INTERNALS §2.11): registration,
19
+ * bundle loading, skew handling, locale switching, the navigation watcher,
20
+ * the `fetch` wrapper and persistence. Only `@menapo/vi18n/engine` imports
21
+ * it, and the generated client boot module imports that entry first; it
22
+ * hooks into the core's `register` and replaces the inert client methods
23
+ * `state.ts` installs, so neither the `.` entry nor `/server` carries it.
24
+ *
25
+ * Its data is private to the runtime copy that created the state; every
26
+ * public export reaches it through the state's methods, so a second copy never
27
+ * reads its own empty variables. Module-level variables rather than one
28
+ * object, and one-letter fields on `LocaleData`: the minifier renames
29
+ * variables but never properties, and this file is most of the engine's
30
+ * 3.5 KB budget (PLAN §6).
31
+ */
32
+ /**
33
+ * The creating side of the bridge: `virtual:vi18n/bridge` calls it in every
34
+ * realm whose code constructs a worker (a page, or a worker that starts its
35
+ * own). Each `new Worker(url)` gets the realm's locale as a URL fragment,
36
+ * which leaves the request alone; a worker that answers with a hello is told
37
+ * the version and map, then every locale this realm flips to. Workers that
38
+ * never say hello (no vi18n in them, or `type: "classic"` scripts the
39
+ * fragment does not reach) never hear from it.
40
+ */
41
+ export declare function bridge(): void;
42
+ //#endregion
43
+ export {};