@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,827 @@
1
+ import { o as isDiagnosticError } from "./types-D3QGM8XO.js";
2
+ import { a as isManualId, s as parseIdScheme } from "./validate-BxpupdAf.js";
3
+ import { C as usableTranslation, S as entryKey, o as catalogLayout } from "./library-D5CWvTQs.js";
4
+ import { r as resolveDuplicates, t as duplicateDiagnostics } from "./duplicates-yuRljQ6h.js";
5
+ import { isAbsolute, relative, sep } from "node:path";
6
+ import { hash } from "node:crypto";
7
+ //#region src/core/compile/hash8.ts
8
+ /**
9
+ * First 8 characters of `base64url(sha256(utf8(text)))` (INTERNALS §2.9.1,
10
+ * §6 D36): the same family as message ids, and safe in URLs, file names and
11
+ * bundle keys without escaping.
12
+ */
13
+ function hash8(text) {
14
+ return hash("sha256", text, "base64url").slice(0, 8);
15
+ }
16
+ //#endregion
17
+ //#region src/core/compile/assets.ts
18
+ /**
19
+ * Server catalogs read from static assets (`ssr.catalogs: "assets"`, PLAN
20
+ * §5.11, §5.13; spike S12): the versioned layout that the client build and
21
+ * `vi18n compile --assets` write, the version that namespaces it, and the
22
+ * module that reads it on Workers. Shared by the Vite plugin and the CLI.
23
+ *
24
+ * The reader is generated code rather than part of `@menapo/vi18n/server`:
25
+ * that entry is at its PLAN §6 budget, and only Workers servers need it.
26
+ * `/server` keeps what every registration shares: one load per locale for
27
+ * concurrent scopes, and the least-recently-used bound the runtime config
28
+ * carries (`ssr.maxLoadedLocales`).
29
+ */
30
+ /** File names inside `vi18n/<version>/<locale>/`. */
31
+ const SERVER_FILES = {
32
+ /** The locale's complete server catalog (`BundleData`, fallbacks applied). */
33
+ catalog: "catalog.json",
34
+ /** A copy of the locale's manifest slice: the bundle hashes payloads name. */
35
+ slice: "manifest.json"
36
+ };
37
+ /** The directory of one version's server catalogs, relative to the static-assets root. */
38
+ function serverCatalogsDir(version) {
39
+ return `vi18n/${version}/`;
40
+ }
41
+ /**
42
+ * The version that namespaces the server catalogs (`vi18n/<version>/`).
43
+ * Code-only, like the message-set version `v` (INTERNALS §2.9.3), so a
44
+ * translation-only deploy keeps both the path and the server JS. It covers
45
+ * every id the server catalogs hold and the client's `v`: `v` alone covers
46
+ * client bundles only, and a server-only message whose id changed would
47
+ * otherwise read a catalog without it during a rolling deploy and render
48
+ * the id; the client's `v` covers the bundle keys the manifest slices map.
49
+ */
50
+ function serverCatalogVersion(input) {
51
+ const lines = [input.idScheme, input.v ?? "-"];
52
+ for (const id of [...new Set(input.ids)].sort()) lines.push(id + " " + input.sourceHashOf(id));
53
+ return hash8(lines.join("\n"));
54
+ }
55
+ /**
56
+ * The module whose default export maps each locale to a loader reading
57
+ * `vi18n/<version>/<locale>/catalog.json` through the Workers static-assets
58
+ * binding `env.ASSETS` (`import { env } from "cloudflare:workers"`, which
59
+ * reaches the binding from any handler without the app passing it: queue
60
+ * consumers and cron jobs that call `withLocale` included). The import is
61
+ * dynamic, so a runtime without it gets vi18n's message at the first read
62
+ * instead of a module-resolution error at startup. Absolute URLs: the
63
+ * binding rejects relative ones and ignores the host.
64
+ */
65
+ function assetCatalogsModule(options) {
66
+ const workers = JSON.stringify(options.workers ?? "cloudflare:workers");
67
+ const deploy = JSON.stringify(options.deploy);
68
+ const modules = JSON.stringify(options.modules);
69
+ const loaders = options.locales.map((locale) => ` ${JSON.stringify(locale)}: () => load(${JSON.stringify(locale)}),`);
70
+ return `${`// Generated by vi18n: server catalogs read from the Workers static-assets
71
+ // binding (ssr.catalogs "assets"). Only the locales requests use are read,
72
+ // and @menapo/vi18n/server keeps at most ssr.maxLoadedLocales of them.
73
+ const fail = (text) => new Error("vi18n: " + text);
74
+ const why = (error) => (error instanceof Error ? error.message : String(error));
75
+ let workers;
76
+
77
+ async function read(path) {
78
+ let assets;
79
+ try {
80
+ assets = (await (workers ??= import(${workers}))).env?.ASSETS;\n } catch (error) {\n workers = undefined;\n throw fail("catalogs are read from static assets through cloudflare:workers, which this runtime cannot import (" + why(error) + "): run this server on workerd, or " + ${modules});\n }\n if (typeof assets?.fetch !== "function") {\n throw fail('catalogs are read through the static-assets binding env.ASSETS, which this Worker does not have: add "assets": { "binding": "ASSETS" } to its wrangler config');\n }\n let response;\n try {\n response = await assets.fetch("https://assets.invalid/" + path);\n } catch (error) {\n throw fail("reading /" + path + " through env.ASSETS failed: " + why(error));\n }\n // A missing file is a 404, or an HTML page under not_found_handling.\n const type = response.headers.get("content-type") ?? "";\n if (!response.ok || /html/i.test(type)) {\n throw fail("/" + path + " is not in the static assets (" + response.status + (response.ok ? " " + type : "") + "): " + ${deploy});\n }\n try {\n return await response.json();\n } catch (error) {\n throw fail("/" + path + " is not JSON: " + why(error));\n }\n}\n`}${options.dir !== null ? `\nconst load = (locale) => read(${JSON.stringify(options.dir)} + locale + "/${SERVER_FILES.catalog}");\n` : `\nconst plan = () => import(${JSON.stringify(options.plan ?? "virtual:vi18n/client-plan")});\n\nasync function load(locale) {\n const p = (await plan()).default;\n if (typeof p !== "object" || p === null || typeof p.serverCatalogs !== "string") {\n throw fail("this server reads its catalogs from static assets, but the client build that writes them did not run (the client plan was never filled in): build the client environment with it, or " + ${modules});\n }\n const dir = p.serverCatalogs + locale + "/";\n // The slice names this deploy's bundle hashes for payloads; server JS\n // carries none, so it stays the same across translation-only deploys.\n const [catalog, slice] = await Promise.all([\n read(dir + "${SERVER_FILES.catalog}"),\n Object.hasOwn(p.hashes, locale) ? null : read(dir + "${SERVER_FILES.slice}"),\n ]);\n if (slice !== null) {\n if (slice?.v !== p.v) {\n throw fail("/" + dir + "${SERVER_FILES.slice} is from message set " + slice?.v + ", this server's is " + p.v + ": " + ${deploy});\n }\n p.hashes[locale] = slice.bundles;\n }\n return catalog;\n}\n`}\nexport default {\n${loaders.join("\n")}\n};\n`;
81
+ }
82
+ //#endregion
83
+ //#region src/runtime/types.ts
84
+ /**
85
+ * Opcodes of the IR. The runtime may inline the literal numbers to save
86
+ * bytes; 9 (`list`) and 10 (MF2 `.match`) are reserved and never emitted in 1.0.
87
+ */
88
+ const OP = {
89
+ select: 1,
90
+ plural: 2,
91
+ selectordinal: 3,
92
+ number: 4,
93
+ date: 5,
94
+ time: 6,
95
+ tag: 7,
96
+ tagStandalone: 8,
97
+ list: 9,
98
+ match: 10
99
+ };
100
+ /** The `globalThis[Symbol.for("menapo.vi18n")]` key. */
101
+ const STATE_KEY = "menapo.vi18n";
102
+ //#endregion
103
+ //#region src/core/ir/compile.ts
104
+ /**
105
+ * Compiles a parsed ICU message to the JSON IR the runtime interprets
106
+ * (INTERNALS §2.3, PLAN §5.9).
107
+ *
108
+ * A message of plain text stays a string, which is what most messages are and
109
+ * what the runtime returns without interpretation. Everything else becomes a
110
+ * flat `Part[]` with adjacent text merged, so the runtime never concatenates
111
+ * two literals it could have received as one.
112
+ */
113
+ function compileMessage(message) {
114
+ const text = literalText(message);
115
+ if (text !== null) return text;
116
+ const parts = [];
117
+ appendParts(message, parts);
118
+ return parts;
119
+ }
120
+ /** The concatenated text when every node is a literal, otherwise null. */
121
+ function literalText(nodes) {
122
+ if (nodes.length === 0) return "";
123
+ if (nodes.length === 1) {
124
+ const node = nodes[0];
125
+ return node.type === "literal" ? node.value : null;
126
+ }
127
+ let text = "";
128
+ for (const node of nodes) {
129
+ if (node.type !== "literal") return null;
130
+ text += node.value;
131
+ }
132
+ return text;
133
+ }
134
+ function appendParts(nodes, out) {
135
+ for (const node of nodes) {
136
+ if (node.type !== "literal") {
137
+ out.push(compileNode(node));
138
+ continue;
139
+ }
140
+ const last = out.length - 1;
141
+ if (last >= 0 && typeof out[last] === "string") out[last] = out[last] + node.value;
142
+ else if (node.value !== "") out.push(node.value);
143
+ }
144
+ }
145
+ function compileNode(node) {
146
+ switch (node.type) {
147
+ case "argument": return [node.name];
148
+ case "pound": return 0;
149
+ case "number":
150
+ case "date":
151
+ case "time": {
152
+ const op = node.type === "number" ? OP.number : node.type === "date" ? OP.date : OP.time;
153
+ const options = copyOptions(node.options);
154
+ return options === null ? [node.name, op] : [
155
+ node.name,
156
+ op,
157
+ options
158
+ ];
159
+ }
160
+ case "select": return [
161
+ node.name,
162
+ OP.select,
163
+ compileCases(node.options)
164
+ ];
165
+ case "plural": {
166
+ const op = node.pluralType === "ordinal" ? OP.selectordinal : OP.plural;
167
+ const cases = compileCases(node.options);
168
+ return node.offset === 0 ? [
169
+ node.name,
170
+ op,
171
+ cases
172
+ ] : [
173
+ node.name,
174
+ op,
175
+ cases,
176
+ node.offset
177
+ ];
178
+ }
179
+ case "tag": {
180
+ if (node.children === null) return [node.name, OP.tagStandalone];
181
+ const part = [node.name, OP.tag];
182
+ appendTagChildren(node.children, part);
183
+ return part;
184
+ }
185
+ }
186
+ }
187
+ /** Tag children are spread into the tag part after its name and opcode. */
188
+ function appendTagChildren(children, part) {
189
+ for (const child of children) {
190
+ if (child.type !== "literal") {
191
+ part.push(compileNode(child));
192
+ continue;
193
+ }
194
+ const last = part.length - 1;
195
+ if (last >= 2 && typeof part[last] === "string") part[last] = part[last] + child.value;
196
+ else if (child.value !== "") part.push(child.value);
197
+ }
198
+ }
199
+ /**
200
+ * Keys are added in option order, but a plain object lists integer-like keys
201
+ * (`"1"`, not `"01"`) first in ascending order (INTERNALS §2.3). Lookup goes
202
+ * through `Object.hasOwn`, so only the serialized order is affected.
203
+ */
204
+ function compileCases(options) {
205
+ const cases = {};
206
+ for (const option of options) {
207
+ const value = compileMessage(option.value);
208
+ if (option.key === "__proto__") Object.defineProperty(cases, option.key, {
209
+ value,
210
+ enumerable: true,
211
+ writable: true,
212
+ configurable: true
213
+ });
214
+ else cases[option.key] = value;
215
+ }
216
+ return cases;
217
+ }
218
+ /**
219
+ * The IR gets its own options object: the codec memoises ASTs, and the runtime
220
+ * keys formatter caches on the options object's identity per message.
221
+ */
222
+ function copyOptions(options) {
223
+ if (options === null) return null;
224
+ let copy = null;
225
+ for (const key in options) {
226
+ if (!Object.hasOwn(options, key)) continue;
227
+ copy ??= {};
228
+ copy[key] = options[key];
229
+ }
230
+ return copy;
231
+ }
232
+ //#endregion
233
+ //#region src/core/ir/pseudo.ts
234
+ /**
235
+ * Accented look-alikes of `A–Z` then `a–z` (one precomposed code point each),
236
+ * readable at a glance yet impossible to mistake for untranslated text.
237
+ */
238
+ const ACCENTED = "ÁƁÇÐÉƑĜĤÍĴĶĻṀÑÓÞǪŔŠŢÚṼŴẊÝŽáƀçðéƒĝĥíĵķļɱñóþǫŕšţúṽŵẋýž";
239
+ const ACCENTED_BY_CODE = (() => {
240
+ const table = [];
241
+ for (let i = 0; i < 26; i++) {
242
+ table[65 + i] = ACCENTED.charAt(i);
243
+ table[97 + i] = ACCENTED.charAt(26 + i);
244
+ }
245
+ return table;
246
+ })();
247
+ const IS_VOWEL = (() => {
248
+ const table = [];
249
+ for (const vowel of "AEIOUaeiou") table[vowel.charCodeAt(0)] = true;
250
+ return table;
251
+ })();
252
+ /**
253
+ * A pseudo-localised copy of a source message (INTERNALS §2.3).
254
+ *
255
+ * Only literal text changes: arguments, option keys, tags and `#` are kept, so
256
+ * the result has the source's structure and renders with the same values.
257
+ * `en-XA` accents ASCII letters and lengthens each literal that has letters by
258
+ * ⌈35 %⌉ of its length (PLAN §4.7), the typical growth from English to German
259
+ * or French, so layouts meet the overflow real translations cause; `ar-XB` keeps
260
+ * the letters and forces right-to-left display. Both bracket the whole
261
+ * message, so text on screen without brackets was not translated and brackets
262
+ * inside a sentence reveal concatenation (PLAN §5.21).
263
+ */
264
+ function pseudoLocalize(message, locale) {
265
+ return locale === "en-XA" ? wrap(mapNodes(message, accent), "[", "]") : wrap(mapNodes(message, identity), "[‮", "‬]");
266
+ }
267
+ /**
268
+ * Rebuilds a node with `text` applied to its literal text. Output nodes carry
269
+ * no `loc`: offsets into the source mean nothing for generated text.
270
+ */
271
+ function mapNode(node, text) {
272
+ switch (node.type) {
273
+ case "literal": return {
274
+ type: "literal",
275
+ value: text(node.value)
276
+ };
277
+ case "argument": return {
278
+ type: "argument",
279
+ name: node.name
280
+ };
281
+ case "pound": return { type: "pound" };
282
+ case "number":
283
+ case "date":
284
+ case "time": return {
285
+ type: node.type,
286
+ name: node.name,
287
+ style: node.style === null ? null : { ...node.style },
288
+ options: node.options === null ? null : { ...node.options }
289
+ };
290
+ case "select": return {
291
+ type: "select",
292
+ name: node.name,
293
+ options: mapOptions(node.options, text)
294
+ };
295
+ case "plural": return {
296
+ type: "plural",
297
+ pluralType: node.pluralType,
298
+ name: node.name,
299
+ offset: node.offset,
300
+ options: mapOptions(node.options, text)
301
+ };
302
+ case "tag": return {
303
+ type: "tag",
304
+ name: node.name,
305
+ children: node.children === null ? null : mapNodes(node.children, text)
306
+ };
307
+ }
308
+ }
309
+ function mapNodes(nodes, text) {
310
+ const out = [];
311
+ for (const node of nodes) out.push(mapNode(node, text));
312
+ return out;
313
+ }
314
+ function mapOptions(options, text) {
315
+ const out = [];
316
+ for (const option of options) out.push({
317
+ key: option.key,
318
+ value: mapNodes(option.value, text)
319
+ });
320
+ return out;
321
+ }
322
+ function identity(value) {
323
+ return value;
324
+ }
325
+ /** Extra characters per literal, in percent of its length (PLAN §4.7). */
326
+ const EXPANSION_PERCENT = 35;
327
+ function accent(value) {
328
+ const chars = Array.from(value);
329
+ let letters = 0;
330
+ let vowels = 0;
331
+ for (const char of chars) {
332
+ const code = char.charCodeAt(0);
333
+ if (char.length !== 1 || code >= 128 || ACCENTED_BY_CODE[code] === void 0) continue;
334
+ letters++;
335
+ if (IS_VOWEL[code] === true) vowels++;
336
+ }
337
+ const extra = letters === 0 ? 0 : Math.ceil(chars.length * EXPANSION_PERCENT / 100);
338
+ const byVowel = vowels > 0 && extra <= 2 * vowels;
339
+ const carriers = byVowel ? vowels : letters;
340
+ let out = "";
341
+ let seen = 0;
342
+ for (const char of chars) {
343
+ const code = char.charCodeAt(0);
344
+ const mapped = char.length === 1 && code < 128 ? ACCENTED_BY_CODE[code] : void 0;
345
+ if (mapped === void 0) {
346
+ out += char;
347
+ continue;
348
+ }
349
+ out += mapped;
350
+ if (byVowel && IS_VOWEL[code] !== true) continue;
351
+ const copies = Math.floor((seen + 1) * extra / carriers) - Math.floor(seen * extra / carriers);
352
+ seen++;
353
+ if (copies > 0) out += mapped.repeat(copies);
354
+ }
355
+ return out;
356
+ }
357
+ /**
358
+ * Adds the brackets as literal text, merged into a literal at either end so
359
+ * the result keeps the AST invariant that adjacent literals are one node.
360
+ */
361
+ function wrap(nodes, open, close) {
362
+ const first = nodes[0];
363
+ if (first?.type === "literal") first.value = open + first.value;
364
+ else nodes.unshift({
365
+ type: "literal",
366
+ value: open
367
+ });
368
+ const last = nodes[nodes.length - 1];
369
+ if (last.type === "literal") last.value += close;
370
+ else nodes.push({
371
+ type: "literal",
372
+ value: close
373
+ });
374
+ return nodes;
375
+ }
376
+ //#endregion
377
+ //#region src/core/compile/fallback.ts
378
+ const PSEUDO_LOCALES$1 = /* @__PURE__ */ new Set(["en-XA", "ar-XB"]);
379
+ /**
380
+ * The locales whose catalogs supply `locale`'s messages, most specific first
381
+ * and always ending with `sourceLocale` (INTERNALS §2.8.1).
382
+ *
383
+ * Order: `locale`; its truncations at `-` boundaries that are configured
384
+ * (`zh-Hant-TW` → `zh-Hant` → `zh`); then, for each of those in order, its
385
+ * configured `fallbacks`; then `sourceLocale`. Duplicates keep their first
386
+ * position, except `sourceLocale`, which only ever comes last: its messages
387
+ * compile from code and are always complete, so a locale written after it in
388
+ * `fallbacks` (`de: ["en", "fr"]`) would be unreachable for any consumer that
389
+ * stops at the source, while `compileLocales` would still consult it. The
390
+ * source locale's own chain is itself alone. Configured fallbacks are not
391
+ * followed transitively: the chain is what the user wrote for the locale and
392
+ * its ancestors, nothing they have to trace through other entries.
393
+ */
394
+ function fallbackChain(locale, config) {
395
+ const source = config.sourceLocale;
396
+ if (locale === source) return [locale];
397
+ if (PSEUDO_LOCALES$1.has(locale)) return [locale, source];
398
+ const chain = [locale];
399
+ for (let end = locale.lastIndexOf("-"); end > 0; end = locale.lastIndexOf("-", end - 1)) {
400
+ const ancestor = locale.slice(0, end);
401
+ if (ancestor !== source && config.locales.includes(ancestor) && !chain.includes(ancestor)) chain.push(ancestor);
402
+ }
403
+ const derived = chain.length;
404
+ for (let i = 0; i < derived; i++) {
405
+ const member = chain[i];
406
+ if (!Object.hasOwn(config.fallbacks, member)) continue;
407
+ for (const next of config.fallbacks[member]) if (next !== source && !chain.includes(next)) chain.push(next);
408
+ }
409
+ chain.push(source);
410
+ return chain;
411
+ }
412
+ //#endregion
413
+ //#region src/core/compile/locales.ts
414
+ /**
415
+ * Locale compilation (INTERNALS §2.8.2, PLAN §5.5 D10, §5.9): catalogs and
416
+ * code messages become one IR map per locale, fallbacks applied, with the
417
+ * reason every untranslated id fell back. The build, `vi18n compile`, dev
418
+ * catalog modules, `check` and `status` all read translations through here,
419
+ * so "what ships" has one definition.
420
+ */
421
+ const PSEUDO_LOCALES = /* @__PURE__ */ new Set(["en-XA", "ar-XB"]);
422
+ /** Ids listed in `catalog-missing-translation`; the rest are only counted. */
423
+ const MISSING_LISTED = 20;
424
+ const NO_ENTRY = {
425
+ ok: false,
426
+ reason: "no-entry"
427
+ };
428
+ const SCHEME_MISMATCH = {
429
+ ok: false,
430
+ reason: "scheme-mismatch"
431
+ };
432
+ function compileLocales(input) {
433
+ const { config, codec, messages, catalogs } = input;
434
+ const sourceFallback = input.sourceFallback ?? true;
435
+ const layout = catalogLayout(config);
436
+ const sourceLocale = config.sourceLocale;
437
+ const invalidSeverity = severityOf(config.strict.invalid);
438
+ const missingSeverity = severityOf(config.strict.missing);
439
+ const prepared = /* @__PURE__ */ new Map();
440
+ const prepare = (locale) => {
441
+ let found = prepared.get(locale);
442
+ if (found === void 0) {
443
+ found = prepareCatalog(catalogs, locale, layout, codec, config.root);
444
+ prepared.set(locale, found);
445
+ }
446
+ return found;
447
+ };
448
+ let sink = [];
449
+ const report = (catalog) => {
450
+ if (catalog !== null && catalog.pending !== null) {
451
+ for (const diagnostic of catalog.pending) sink.push(diagnostic);
452
+ catalog.pending = null;
453
+ }
454
+ return catalog;
455
+ };
456
+ const lookup = (catalog, id, message) => {
457
+ if (catalog === null) return NO_ENTRY;
458
+ const cached = catalog.lookups.get(id);
459
+ if (cached !== void 0) return cached;
460
+ const result = evaluate(catalog, id, message);
461
+ catalog.lookups.set(id, result);
462
+ return result;
463
+ };
464
+ const libraries = /* @__PURE__ */ new Map();
465
+ /** A package's language, for its catalogs that differ from the app's (§2.6.11). */
466
+ const librarySource = /* @__PURE__ */ new Map();
467
+ const librariesOf = (locale) => {
468
+ let found = libraries.get(locale);
469
+ if (found === void 0) {
470
+ found = [];
471
+ for (const loaded of catalogs.get(locale)?.libraries ?? []) {
472
+ const one = prepareCatalog(/* @__PURE__ */ new Map([[locale, loaded]]), locale, layout, codec, config.root);
473
+ if (one === null) continue;
474
+ const prepared = {
475
+ ...one,
476
+ pending: null,
477
+ library: true
478
+ };
479
+ if (loaded.librarySource !== void 0) librarySource.set(prepared, loaded.librarySource);
480
+ found.push(prepared);
481
+ }
482
+ libraries.set(locale, found);
483
+ }
484
+ return found;
485
+ };
486
+ /** A chain step: the app's catalog, then each library's for the same locale. */
487
+ const step = (catalog, locale, id, message) => {
488
+ const own = lookup(catalog, id, message);
489
+ if (own.ok) return own;
490
+ for (const library of librariesOf(locale)) {
491
+ const found = lookup(library, id, message);
492
+ if (found.ok) return found;
493
+ }
494
+ return own;
495
+ };
496
+ const evaluate = (catalog, id, message) => {
497
+ if (catalog.schemeMismatch && (layout === "id" || !message.manual)) return SCHEME_MISMATCH;
498
+ const entry = catalog.index.get(id);
499
+ if (entry === void 0) return NO_ENTRY;
500
+ const usable = usableTranslation(entry, layout, message, codec, catalog.locale);
501
+ if (usable.ok) return {
502
+ ok: true,
503
+ ir: compileMessage(usable.parsed),
504
+ state: entry.state === "ai" ? "ai" : "translated"
505
+ };
506
+ if (catalog.library) return {
507
+ ok: false,
508
+ reason: usable.reason
509
+ };
510
+ if (usable.reason === "invalid" && invalidSeverity !== null) sink.push(invalidDiagnostic(catalog, entry, id, usable.diagnostics, invalidSeverity));
511
+ else if (usable.reason === "source-mismatch" && entry.manualId !== null) sink.push(sourceMismatchDiagnostic(catalog, entry, id, message, layout));
512
+ return {
513
+ ok: false,
514
+ reason: usable.reason
515
+ };
516
+ };
517
+ const sourceIRs = /* @__PURE__ */ new Map();
518
+ const sourceIR = (id, message) => {
519
+ let ir = sourceIRs.get(id);
520
+ if (ir === void 0) {
521
+ ir = parseOrReport(codec, message, sink, (ast) => compileMessage(ast));
522
+ sourceIRs.set(id, ir);
523
+ }
524
+ return ir;
525
+ };
526
+ const all = withKeep(messages, input.keep ?? config.extraction.keep, {
527
+ config,
528
+ codec,
529
+ layout,
530
+ catalogs,
531
+ prepare,
532
+ sink: (diagnostic) => sink.push(diagnostic)
533
+ });
534
+ const keepSet = new Set(input.keep ?? config.extraction.keep);
535
+ const preamble = sink;
536
+ const result = /* @__PURE__ */ new Map();
537
+ for (const locale of input.locales) {
538
+ const chain = fallbackChain(locale, config);
539
+ const compiled = {
540
+ locale,
541
+ chain,
542
+ messages: /* @__PURE__ */ new Map(),
543
+ status: /* @__PURE__ */ new Map(),
544
+ labels: /* @__PURE__ */ new Map(),
545
+ diagnostics: result.size === 0 ? preamble : []
546
+ };
547
+ sink = compiled.diagnostics;
548
+ result.set(locale, compiled);
549
+ if (PSEUDO_LOCALES.has(locale)) {
550
+ compilePseudo(compiled, locale, all, codec);
551
+ continue;
552
+ }
553
+ if (locale === sourceLocale) {
554
+ report(prepare(locale));
555
+ const status = {
556
+ state: "translated",
557
+ from: sourceLocale
558
+ };
559
+ const foreign = librariesOf(locale);
560
+ for (const [id, message] of all) {
561
+ let ir = null;
562
+ for (const library of foreign) {
563
+ const found = lookup(library, id, message);
564
+ if (found.ok) {
565
+ ir = found.ir;
566
+ break;
567
+ }
568
+ }
569
+ ir ??= sourceIR(id, message);
570
+ if (ir === null) continue;
571
+ compiled.messages.set(id, ir);
572
+ compiled.status.set(id, status);
573
+ if (message.manual) compiled.labels.set(id, message.label);
574
+ }
575
+ continue;
576
+ }
577
+ const own = report(prepare(locale));
578
+ const walk = [];
579
+ for (let i = 1; i < chain.length; i++) {
580
+ const member = chain[i];
581
+ if (member !== sourceLocale) walk.push(report(prepare(member)));
582
+ }
583
+ const walkLocales = chain.slice(1).filter((member) => member !== sourceLocale);
584
+ const translated = {
585
+ state: "translated",
586
+ from: locale
587
+ };
588
+ const ai = {
589
+ state: "ai",
590
+ from: locale
591
+ };
592
+ const missing = /* @__PURE__ */ new Map();
593
+ let missingCount = 0;
594
+ const missingIds = [];
595
+ for (const [id, message] of all) {
596
+ const mine = step(own, locale, id, message);
597
+ if (mine.ok) {
598
+ compiled.messages.set(id, mine.ir);
599
+ compiled.status.set(id, mine.state === "ai" ? ai : translated);
600
+ if (message.manual) compiled.labels.set(id, message.label);
601
+ continue;
602
+ }
603
+ missingCount++;
604
+ if (missingIds.length < MISSING_LISTED) missingIds.push(id);
605
+ let from = sourceLocale;
606
+ let ir = null;
607
+ for (let i = 0; i < walk.length; i++) {
608
+ const found = step(walk[i], walkLocales[i], id, message);
609
+ if (found.ok) {
610
+ ir = found.ir;
611
+ from = walkLocales[i];
612
+ break;
613
+ }
614
+ }
615
+ if (ir === null) for (const library of librariesOf(sourceLocale)) {
616
+ if (librarySource.get(library) === locale) continue;
617
+ const found = lookup(library, id, message);
618
+ if (found.ok) {
619
+ ir = found.ir;
620
+ break;
621
+ }
622
+ }
623
+ const suppliedByEntry = ir !== null;
624
+ if (ir === null && (sourceFallback || keepSet.has(id))) ir = sourceIR(id, message);
625
+ if (ir !== null) compiled.messages.set(id, ir);
626
+ const statusKey = `${from}\0${mine.reason}`;
627
+ let status = missing.get(statusKey);
628
+ if (status === void 0) {
629
+ status = {
630
+ state: "missing",
631
+ from,
632
+ reason: mine.reason
633
+ };
634
+ missing.set(statusKey, status);
635
+ }
636
+ compiled.status.set(id, status);
637
+ if (message.manual) {
638
+ const label = suppliedByEntry ? message.label : ownLabel(own, id, layout, codec);
639
+ if (label !== null) compiled.labels.set(id, label);
640
+ else if (ir !== null) compiled.labels.set(id, message.label);
641
+ }
642
+ }
643
+ if (missingCount > 0 && missingSeverity !== null) {
644
+ const more = missingCount > missingIds.length ? ", …" : "";
645
+ compiled.diagnostics.push({
646
+ code: "catalog-missing-translation",
647
+ severity: missingSeverity,
648
+ message: `${missingCount} ${missingCount === 1 ? "message has" : "messages have"} no usable ${locale} translation (${missingIds.join(", ")}${more}); ${missingCount === 1 ? "it falls" : "they fall"} back along ${chain.join(" → ")}`,
649
+ locale,
650
+ ...own === null ? {} : { file: own.file }
651
+ });
652
+ }
653
+ }
654
+ return result;
655
+ }
656
+ function compilePseudo(compiled, locale, messages, codec) {
657
+ const status = {
658
+ state: "translated",
659
+ from: locale
660
+ };
661
+ for (const [id, message] of messages) {
662
+ const ir = parseOrReport(codec, message, compiled.diagnostics, (ast) => compileMessage(pseudoLocalize(ast, locale)));
663
+ if (ir === null) continue;
664
+ compiled.messages.set(id, ir);
665
+ compiled.status.set(id, status);
666
+ if (message.manual) compiled.labels.set(id, message.label);
667
+ }
668
+ }
669
+ function parseOrReport(codec, message, sink, build) {
670
+ try {
671
+ return build(codec.parse(message.canonical));
672
+ } catch (error) {
673
+ if (!isDiagnosticError(error)) throw error;
674
+ sink.push({
675
+ ...error.diagnostic,
676
+ id: error.diagnostic.id ?? message.id
677
+ });
678
+ return null;
679
+ }
680
+ }
681
+ function severityOf(level) {
682
+ return level === "off" ? null : level === "warn" ? "warning" : "error";
683
+ }
684
+ function displayPath(root, file) {
685
+ const rel = relative(root, file);
686
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return file.split(sep).join("/");
687
+ return rel.split(sep).join("/");
688
+ }
689
+ function prepareCatalog(catalogs, locale, layout, codec, root) {
690
+ const loaded = catalogs.get(locale);
691
+ if (loaded === void 0 || loaded.catalog === null) return null;
692
+ const file = displayPath(root, loaded.path);
693
+ const { catalog, duplicates } = resolveDuplicates(loaded.catalog, codec);
694
+ const pending = duplicateDiagnostics(duplicates, file);
695
+ const index = /* @__PURE__ */ new Map();
696
+ for (const entry of catalog.entries) {
697
+ const key = entryKey(entry, layout, codec);
698
+ if (key !== null) index.set(key, entry);
699
+ }
700
+ const recorded = catalog.header.idScheme;
701
+ const schemeMismatch = layout !== "source" && recorded !== null && recorded !== codec.idScheme;
702
+ if (schemeMismatch) pending.push(schemeDiagnostic(recorded, codec, layout, locale, file));
703
+ return {
704
+ locale,
705
+ file,
706
+ index,
707
+ schemeMismatch,
708
+ pending: pending.length > 0 ? pending : null,
709
+ lookups: /* @__PURE__ */ new Map(),
710
+ library: false
711
+ };
712
+ }
713
+ function schemeDiagnostic(recorded, codec, layout, locale, file) {
714
+ const old = parseIdScheme(recorded);
715
+ return {
716
+ code: "id-scheme-mismatch",
717
+ severity: "error",
718
+ message: old !== null ? `ids.length changed from ${old.length} to ${codec.idLength}: run vi18n extract to re-key` : `${layout === "json" ? "idScheme" : "X-Vi18n-Id"} is ${JSON.stringify(recorded)}, not ${codec.idScheme}: run vi18n extract to re-key`,
719
+ locale,
720
+ file
721
+ };
722
+ }
723
+ function invalidDiagnostic(catalog, entry, id, found, severity) {
724
+ const first = found[0];
725
+ const reason = first === void 0 ? "it does not validate" : found.length === 1 ? first.message : `${first.message}, and ${found.length - 1} more`;
726
+ const diagnostic = {
727
+ code: "catalog-invalid-translation",
728
+ severity,
729
+ message: `${id}: the ${catalog.locale} translation is not used (${reason}); fix it or clear it`,
730
+ file: catalog.file,
731
+ locale: catalog.locale,
732
+ id
733
+ };
734
+ if (entry.line > 0) diagnostic.line = entry.line;
735
+ if (found.length > 0) diagnostic.related = found.map((inner) => ({
736
+ file: catalog.file,
737
+ ...inner.line === void 0 ? {} : { line: inner.line },
738
+ message: `${inner.code}: ${inner.message}`
739
+ }));
740
+ return diagnostic;
741
+ }
742
+ function sourceMismatchDiagnostic(catalog, entry, id, message, layout) {
743
+ const old = entry.source ?? (layout === "id" ? entry.label : null);
744
+ const was = old === null ? "" : ` is for ${JSON.stringify(old)}`;
745
+ const diagnostic = {
746
+ code: "catalog-source-mismatch",
747
+ severity: "info",
748
+ message: `${id}: the ${catalog.locale} translation${was}, the code says ${JSON.stringify(message.canonical)}; run vi18n extract`,
749
+ file: catalog.file,
750
+ locale: catalog.locale,
751
+ id
752
+ };
753
+ if (entry.line > 0) diagnostic.line = entry.line;
754
+ return diagnostic;
755
+ }
756
+ /** The label of the source a locale's own manual entry was made from, valid or not. */
757
+ function ownLabel(own, id, layout, codec) {
758
+ const entry = own?.index.get(id);
759
+ if (entry === void 0) return null;
760
+ if (layout === "id") return entry.label;
761
+ if (entry.source === null) return null;
762
+ try {
763
+ return codec.label(codec.canonicalize(entry.source));
764
+ } catch (error) {
765
+ if (isDiagnosticError(error)) return null;
766
+ throw error;
767
+ }
768
+ }
769
+ /**
770
+ * The messages plus `keep` ids no message declares, whose identity comes from
771
+ * their catalog entries (INTERNALS §2.8.2): the source locale's entry first
772
+ * (in the `"id"` layout its `msgstr` is the only real source), then the other
773
+ * locales in config order. A keep id without any entry is `keep-missing-entry`.
774
+ */
775
+ function withKeep(messages, keep, context) {
776
+ let undeclared = null;
777
+ for (const id of keep) if (!messages.has(id) && (undeclared === null || !undeclared.includes(id))) (undeclared ??= []).push(id);
778
+ if (undeclared === null) return messages;
779
+ const { config, codec, catalogs } = context;
780
+ const sourceCatalog = catalogs.get(config.sourceLocale);
781
+ const sourceFile = sourceCatalog === void 0 ? void 0 : displayPath(config.root, sourceCatalog.path);
782
+ const all = new Map(messages);
783
+ const order = [config.sourceLocale, ...config.locales.filter((l) => l !== config.sourceLocale)];
784
+ for (const id of undeclared) {
785
+ let found = false;
786
+ let identity = null;
787
+ for (const locale of order) {
788
+ const entry = context.prepare(locale)?.index.get(id);
789
+ if (entry === void 0) continue;
790
+ found = true;
791
+ if (entry.source === null) continue;
792
+ identity = keepIdentity(id, entry.source, codec);
793
+ if (identity !== null) break;
794
+ }
795
+ if (identity !== null) {
796
+ all.set(id, identity);
797
+ continue;
798
+ }
799
+ context.sink({
800
+ code: "keep-missing-entry",
801
+ severity: "error",
802
+ message: found ? `extraction.keep lists ${id}, but no catalog entry for it has valid source text; run vi18n extract or fix the entry` : `extraction.keep lists ${id}, but no catalog has an entry for it; declare it with msg() in code or add it to the ${config.sourceLocale} catalog`,
803
+ id,
804
+ ...sourceFile === void 0 ? {} : { file: sourceFile }
805
+ });
806
+ }
807
+ return all;
808
+ }
809
+ function keepIdentity(id, source, codec) {
810
+ let canonical;
811
+ try {
812
+ canonical = codec.canonicalize(source);
813
+ } catch (error) {
814
+ if (isDiagnosticError(error)) return null;
815
+ throw error;
816
+ }
817
+ const sourceHash = codec.sourceHash(canonical);
818
+ return {
819
+ id,
820
+ manual: isManualId(id),
821
+ canonical,
822
+ sourceHash,
823
+ label: sourceHash.slice(0, codec.idLength)
824
+ };
825
+ }
826
+ //#endregion
827
+ export { assetCatalogsModule as a, hash8 as c, SERVER_FILES as i, compileMessage as n, serverCatalogVersion as o, STATE_KEY as r, serverCatalogsDir as s, compileLocales as t };