@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,2502 @@
1
+ import { m as OP } from "./dir-zw6bGMKQ.js";
2
+ //#region src/core/diagnostics.ts
3
+ /**
4
+ * The only way core code creates a throwable diagnostic, so every error that
5
+ * crosses a module boundary can be reported without knowing who threw it.
6
+ */
7
+ function diagnosticError(diagnostic) {
8
+ const error = new Error(diagnostic.message);
9
+ error.name = "DiagnosticError";
10
+ error.diagnostic = diagnostic;
11
+ return error;
12
+ }
13
+ /**
14
+ * Recognises a {@link DiagnosticError} by shape rather than by class: errors
15
+ * thrown in a `vm` context or another bundled copy of vi18n fail `instanceof`,
16
+ * while `Object.prototype.toString` sees the internal error slot in any realm.
17
+ */
18
+ function isDiagnosticError(error) {
19
+ if (typeof error !== "object" || error === null) return false;
20
+ if (!(error instanceof Error) && Object.prototype.toString.call(error) !== "[object Error]") return false;
21
+ const diagnostic = error.diagnostic;
22
+ return typeof diagnostic === "object" && diagnostic !== null && typeof diagnostic.code === "string";
23
+ }
24
+ //#endregion
25
+ //#region src/core/icu/skeleton.ts
26
+ const fail = (code, message) => ({
27
+ ok: false,
28
+ code,
29
+ message
30
+ });
31
+ /**
32
+ * Resolves the style text of `{name, type, style}` as FormatJS leaves it
33
+ * (`trimEnd`ed; `null` when absent).
34
+ */
35
+ function resolveStyle(type, style) {
36
+ if (style === null) return {
37
+ ok: true,
38
+ style: null,
39
+ options: keywordOptions(type, null)
40
+ };
41
+ if (style.startsWith("::")) {
42
+ const skeleton = style.slice(2).trimStart();
43
+ return type === "number" ? numberSkeleton(skeleton) : dateSkeleton(skeleton);
44
+ }
45
+ if (type === "number") {
46
+ if (style === "integer" || style === "percent") return {
47
+ ok: true,
48
+ style: {
49
+ kind: "keyword",
50
+ value: style
51
+ },
52
+ options: keywordOptions(type, style)
53
+ };
54
+ if (style === "currency") return fail("icu-unknown-currency", "{…, number, currency} needs a currency code: write ::currency/EUR (with the code you need)");
55
+ } else if (style === "short" || style === "medium" || style === "long" || style === "full") return {
56
+ ok: true,
57
+ style: {
58
+ kind: "keyword",
59
+ value: style
60
+ },
61
+ options: keywordOptions(type, style)
62
+ };
63
+ return fail("icu-pattern-style", `"${style}" is not a ${type} style: use ${type === "number" ? "integer, percent" : "short, medium, long, full"} or a ::skeleton`);
64
+ }
65
+ /** FormatJS's default `formats` (intl-messageformat), copied fresh per message. */
66
+ function keywordOptions(type, keyword) {
67
+ switch (type) {
68
+ case "number":
69
+ if (keyword === "integer") return { maximumFractionDigits: 0 };
70
+ if (keyword === "percent") return { style: "percent" };
71
+ return null;
72
+ case "date": switch (keyword) {
73
+ case "short": return {
74
+ month: "numeric",
75
+ day: "numeric",
76
+ year: "2-digit"
77
+ };
78
+ case "medium": return {
79
+ month: "short",
80
+ day: "numeric",
81
+ year: "numeric"
82
+ };
83
+ case "long": return {
84
+ month: "long",
85
+ day: "numeric",
86
+ year: "numeric"
87
+ };
88
+ case "full": return {
89
+ weekday: "long",
90
+ month: "long",
91
+ day: "numeric",
92
+ year: "numeric"
93
+ };
94
+ default: return null;
95
+ }
96
+ case "time": switch (keyword) {
97
+ case "short": return {
98
+ hour: "numeric",
99
+ minute: "numeric"
100
+ };
101
+ case "long":
102
+ case "full": return {
103
+ hour: "numeric",
104
+ minute: "numeric",
105
+ second: "numeric",
106
+ timeZoneName: "short"
107
+ };
108
+ default: return {
109
+ hour: "numeric",
110
+ minute: "numeric",
111
+ second: "numeric"
112
+ };
113
+ }
114
+ }
115
+ }
116
+ /** `@formatjs/icu-skeleton-parser`'s token separator (Pattern_White_Space). */
117
+ const SKELETON_SPACE = /[\t-\r \x85\u200E\u200F\u2028\u2029]/;
118
+ const FRACTION_PRECISION = /^\.(?:(0+)(\*)?|(#+)|(0+)(#+))$/;
119
+ const SIGNIFICANT_PRECISION = /^(@+)(\+|#+)?([rs])?$/;
120
+ const CONCISE_INTEGER_WIDTH = /^0+$/;
121
+ const CONCISE_SCIENTIFIC = /^(EE?)(\+[!?])?(0+)$/;
122
+ const SIGN$1 = {
123
+ "sign-auto": { signDisplay: "auto" },
124
+ "sign-accounting": { currencySign: "accounting" },
125
+ "()": { currencySign: "accounting" },
126
+ "sign-always": { signDisplay: "always" },
127
+ "+!": { signDisplay: "always" },
128
+ "sign-accounting-always": {
129
+ signDisplay: "always",
130
+ currencySign: "accounting"
131
+ },
132
+ "()!": {
133
+ signDisplay: "always",
134
+ currencySign: "accounting"
135
+ },
136
+ "sign-except-zero": { signDisplay: "exceptZero" },
137
+ "+?": { signDisplay: "exceptZero" },
138
+ "sign-accounting-except-zero": {
139
+ signDisplay: "exceptZero",
140
+ currencySign: "accounting"
141
+ },
142
+ "()?": {
143
+ signDisplay: "exceptZero",
144
+ currencySign: "accounting"
145
+ },
146
+ "sign-never": { signDisplay: "never" },
147
+ "+_": { signDisplay: "never" }
148
+ };
149
+ /** Option-less stems with fixed options (FormatJS `parseNumberSkeleton`). */
150
+ const SIMPLE_STEMS = {
151
+ percent: { style: "percent" },
152
+ "%": { style: "percent" },
153
+ "group-off": { useGrouping: false },
154
+ ",_": { useGrouping: false },
155
+ "precision-integer": { maximumFractionDigits: 0 },
156
+ ".": { maximumFractionDigits: 0 },
157
+ "compact-short": {
158
+ notation: "compact",
159
+ compactDisplay: "short"
160
+ },
161
+ K: {
162
+ notation: "compact",
163
+ compactDisplay: "short"
164
+ },
165
+ "compact-long": {
166
+ notation: "compact",
167
+ compactDisplay: "long"
168
+ },
169
+ KK: {
170
+ notation: "compact",
171
+ compactDisplay: "long"
172
+ },
173
+ "notation-simple": { notation: "standard" },
174
+ "unit-width-narrow": {
175
+ currencyDisplay: "narrowSymbol",
176
+ unitDisplay: "narrow"
177
+ },
178
+ "unit-width-short": {
179
+ currencyDisplay: "code",
180
+ unitDisplay: "short"
181
+ },
182
+ "unit-width-full-name": {
183
+ currencyDisplay: "name",
184
+ unitDisplay: "long"
185
+ },
186
+ "unit-width-iso-code": { currencyDisplay: "symbol" },
187
+ "rounding-mode-floor": { roundingMode: "floor" },
188
+ "rounding-mode-ceiling": { roundingMode: "ceil" },
189
+ "rounding-mode-down": { roundingMode: "trunc" },
190
+ "rounding-mode-up": { roundingMode: "expand" },
191
+ "rounding-mode-half-even": { roundingMode: "halfEven" },
192
+ "rounding-mode-half-down": { roundingMode: "halfTrunc" },
193
+ "rounding-mode-half-up": { roundingMode: "halfExpand" }
194
+ };
195
+ /**
196
+ * Converts a number skeleton (without `::`). The canonical style joins the
197
+ * tokens with one space; options come from FormatJS's conversion.
198
+ */
199
+ function numberSkeleton(skeleton) {
200
+ const tokens = skeleton.split(SKELETON_SPACE).filter((token) => token.length > 0);
201
+ if (tokens.length === 0) return fail("icu-syntax", "Expected a number skeleton after ::");
202
+ const options = {};
203
+ for (const token of tokens) {
204
+ const parts = token.split("/");
205
+ const stem = parts[0];
206
+ for (let i = 1; i < parts.length; i++) if (parts[i] === "") return fail("icu-syntax", `Number skeleton token "${token}" has an empty option`);
207
+ const problem = applyNumberStem(stem, parts, token, options);
208
+ if (problem !== null) return problem;
209
+ }
210
+ try {
211
+ new Intl.NumberFormat("en", options);
212
+ } catch (error) {
213
+ return fail("icu-unknown-skeleton", `Number skeleton "${tokens.join(" ")}" cannot be formatted: ${error.message}`);
214
+ }
215
+ return {
216
+ ok: true,
217
+ style: {
218
+ kind: "skeleton",
219
+ value: tokens.join(" ")
220
+ },
221
+ options: nonEmpty(options)
222
+ };
223
+ }
224
+ function applyNumberStem(stem, parts, token, options) {
225
+ const optionCount = parts.length - 1;
226
+ const simple = Object.hasOwn(SIMPLE_STEMS, stem) ? SIMPLE_STEMS[stem] : void 0;
227
+ if (simple !== void 0) {
228
+ if (optionCount > 0) return unknownStem(token, `${stem} takes no option`);
229
+ Object.assign(options, simple);
230
+ return null;
231
+ }
232
+ switch (stem) {
233
+ case "currency": {
234
+ const code = parts[1];
235
+ if (optionCount !== 1 || code === void 0) return fail("icu-unknown-currency", `${token} needs one currency code: write ::currency/EUR (with the code you need)`);
236
+ if (!supportedCurrencies().has(code)) return fail("icu-unknown-currency", `Unknown currency code "${code}": use an ISO 4217 code such as EUR`);
237
+ options.style = "currency";
238
+ options.currency = code;
239
+ return null;
240
+ }
241
+ case "measure-unit":
242
+ case "unit": {
243
+ const written = parts[1];
244
+ if (optionCount !== 1 || written === void 0) return fail("icu-unknown-unit", `${token} needs one unit: write ::unit/kilometer or ::measure-unit/length-meter`);
245
+ const unit = written.replace(/^(.*?)-/, "");
246
+ if (stem === "unit" && unit !== written) return fail("icu-unknown-unit", isSupportedUnit(unit) && !isSupportedUnit(written) ? `${token} has a unit type: write ::unit/${unit} or ::measure-unit/${written}` : `${token} names a hyphenated unit, which ::unit/ cannot express: write ::measure-unit/<type>-${written}, as in ::measure-unit/volume-fluid-ounce or ::measure-unit/speed-kilometer-per-hour`);
247
+ if (!isSupportedUnit(unit)) return fail("icu-unknown-unit", `Unknown unit "${written}": use a unit Intl.NumberFormat supports, such as kilometer`);
248
+ options.style = "unit";
249
+ options.unit = unit;
250
+ return null;
251
+ }
252
+ case "scientific":
253
+ case "engineering":
254
+ options.notation = stem;
255
+ for (let i = 1; i < parts.length; i++) {
256
+ const option = parts[i];
257
+ if (!Object.hasOwn(SIGN$1, option)) return unknownStem(token, `"${option}" is not a sign option`);
258
+ Object.assign(options, SIGN$1[option]);
259
+ }
260
+ return null;
261
+ case "integer-width": {
262
+ const match = optionCount === 1 ? /^\*(0+)$/.exec(parts[1]) : null;
263
+ if (match === null) return unknownStem(token, "vi18n supports integer-width/*0… (a minimum) only");
264
+ options.minimumIntegerDigits = match[1].length;
265
+ return null;
266
+ }
267
+ case "scale":
268
+ case "%x100": return unknownStem(token, "scaling is not supported: scale the value before formatting it");
269
+ }
270
+ if (CONCISE_INTEGER_WIDTH.test(stem)) {
271
+ if (optionCount > 0) return unknownStem(token, `${stem} takes no option`);
272
+ options.minimumIntegerDigits = stem.length;
273
+ return null;
274
+ }
275
+ const fraction = FRACTION_PRECISION.exec(stem);
276
+ if (fraction !== null) {
277
+ if (optionCount > 1) return unknownStem(token, "fraction precision takes at most one option");
278
+ const [, zeros, star, hashes, minZeros, maxHashes] = fraction;
279
+ if (star === "*") options.minimumFractionDigits = zeros.length;
280
+ else if (hashes !== void 0) options.maximumFractionDigits = hashes.length;
281
+ else if (minZeros !== void 0 && maxHashes !== void 0) {
282
+ options.minimumFractionDigits = minZeros.length;
283
+ options.maximumFractionDigits = minZeros.length + maxHashes.length;
284
+ } else {
285
+ options.minimumFractionDigits = zeros.length;
286
+ options.maximumFractionDigits = zeros.length;
287
+ }
288
+ const option = parts[1];
289
+ if (option === "w") options.trailingZeroDisplay = "stripIfInteger";
290
+ else if (option !== void 0 && !applySignificant(option, options)) return unknownStem(token, `"${option}" is neither w nor a significant-digits option such as @@#`);
291
+ return null;
292
+ }
293
+ if (stem.startsWith("@")) {
294
+ if (optionCount > 0) return unknownStem(token, `${stem} takes no option`);
295
+ if (!applySignificant(stem, options)) return unknownStem(token, "malformed significant digits");
296
+ return null;
297
+ }
298
+ if (Object.hasOwn(SIGN$1, stem)) {
299
+ if (optionCount > 0) return unknownStem(token, `${stem} takes no option`);
300
+ Object.assign(options, SIGN$1[stem]);
301
+ return null;
302
+ }
303
+ const concise = CONCISE_SCIENTIFIC.exec(stem);
304
+ if (concise !== null) {
305
+ if (optionCount > 0) return unknownStem(token, `${stem} takes no option`);
306
+ options.notation = concise[1] === "EE" ? "engineering" : "scientific";
307
+ if (concise[2] === "+!") options.signDisplay = "always";
308
+ else if (concise[2] === "+?") options.signDisplay = "exceptZero";
309
+ options.minimumIntegerDigits = concise[3].length;
310
+ return null;
311
+ }
312
+ return unknownStem(token, "not a number skeleton stem vi18n supports");
313
+ }
314
+ /** FormatJS `parseSignificantPrecision`, restricted to inputs it does not crash on. */
315
+ function applySignificant(text, options) {
316
+ const match = SIGNIFICANT_PRECISION.exec(text);
317
+ if (match === null) return false;
318
+ const [, ats, extra, priority] = match;
319
+ if (priority === "r") options.roundingPriority = "morePrecision";
320
+ else if (priority === "s") options.roundingPriority = "lessPrecision";
321
+ if (extra === void 0) {
322
+ options.minimumSignificantDigits = ats.length;
323
+ options.maximumSignificantDigits = ats.length;
324
+ } else if (extra === "+") options.minimumSignificantDigits = ats.length;
325
+ else {
326
+ options.minimumSignificantDigits = ats.length;
327
+ options.maximumSignificantDigits = ats.length + extra.length;
328
+ }
329
+ return true;
330
+ }
331
+ function unknownStem(token, why) {
332
+ return fail("icu-unknown-skeleton", `Unsupported skeleton token "${token}": ${why}`);
333
+ }
334
+ /**
335
+ * FormatJS's field regex (runs of one symbol), plus `j` and `J`, which
336
+ * FormatJS resolves only when given a locale. FormatJS skips quoted text with
337
+ * a lookahead that rescans the rest of the skeleton at every letter, which is
338
+ * quadratic; `unquotedFields` masks quoted text first instead.
339
+ */
340
+ const DATE_FIELD = /(?:[Eec]{1,6}|G{1,5}|[Qq]{1,5}|(?:[yYur]+|U{1,5})|[ML]{1,5}|d{1,2}|D{1,3}|F{1}|[abB]{1,5}|[hkHK]{1,2}|[jJ]{1,2}|w{1,2}|W{1}|m{1,2}|s{1,2}|[zZOvVxX]{1,4})/g;
341
+ /**
342
+ * The skeleton with every character FormatJS's lookahead
343
+ * `(?=([^']*'[^']*')*[^']*$)` treats as quoted (an odd number of apostrophes
344
+ * follows it) replaced by a space, in linear time.
345
+ */
346
+ function unquotedFields(value) {
347
+ let following = 0;
348
+ for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) === 39) following++;
349
+ if (following === 0) return value;
350
+ let out = "";
351
+ let start = 0;
352
+ for (let i = 0; i <= value.length; i++) {
353
+ if (i < value.length && value.charCodeAt(i) !== 39) continue;
354
+ out += following % 2 === 0 ? value.slice(start, i) : " ".repeat(i - start);
355
+ if (i < value.length) {
356
+ out += "'";
357
+ following--;
358
+ }
359
+ start = i + 1;
360
+ }
361
+ return out;
362
+ }
363
+ const NUMERIC_2DIGIT = ["numeric", "2-digit"];
364
+ const MONTH_WIDTHS = [
365
+ "numeric",
366
+ "2-digit",
367
+ "short",
368
+ "long",
369
+ "narrow"
370
+ ];
371
+ const LOCAL_WEEKDAY = [
372
+ "short",
373
+ "long",
374
+ "narrow",
375
+ "short"
376
+ ];
377
+ const HOUR_CYCLES = {
378
+ h: "h12",
379
+ H: "h23",
380
+ K: "h11",
381
+ k: "h24"
382
+ };
383
+ /**
384
+ * Converts a date/time skeleton (without `::`). Like FormatJS, characters that
385
+ * are not ASCII letters, and quoted text, are ignored; unlike FormatJS, an
386
+ * unknown or unsupported letter, a mixed run (`yY`) or a field given twice is
387
+ * rejected.
388
+ */
389
+ function dateSkeleton(skeleton) {
390
+ const tokens = skeleton.split(SKELETON_SPACE).filter((token) => token.length > 0);
391
+ if (tokens.length === 0) return fail("icu-syntax", "Expected a date skeleton after ::");
392
+ const value = tokens.join(" ");
393
+ const options = {};
394
+ let covered = 0;
395
+ let letters = 0;
396
+ let quoted = false;
397
+ for (let i = 0; i < value.length; i++) {
398
+ const c = value.charCodeAt(i);
399
+ if (c === 39) quoted = !quoted;
400
+ else if (!quoted && isAsciiLetter$1(c)) letters++;
401
+ }
402
+ const fields = unquotedFields(value);
403
+ DATE_FIELD.lastIndex = 0;
404
+ for (let match = DATE_FIELD.exec(fields); match !== null; match = DATE_FIELD.exec(fields)) {
405
+ const run = match[0];
406
+ covered += run.length;
407
+ const problem = applyDateField(run, options);
408
+ if (problem !== null) return problem;
409
+ }
410
+ if (covered !== letters) {
411
+ quoted = false;
412
+ for (let i = 0; i < value.length; i++) {
413
+ const c = value.charCodeAt(i);
414
+ if (c === 39) quoted = !quoted;
415
+ else if (!quoted && isAsciiLetter$1(c) && !/[EecGQqyYurUMLdDFabBhkHKjJwWmszZOvVxX]/.test(value[i])) return unknownDate(value[i], "not a date field symbol");
416
+ }
417
+ return unknownDate(value, "a field is longer than its symbol allows");
418
+ }
419
+ return {
420
+ ok: true,
421
+ style: {
422
+ kind: "skeleton",
423
+ value
424
+ },
425
+ options: nonEmpty(options)
426
+ };
427
+ }
428
+ function applyDateField(run, options) {
429
+ const symbol = run[0];
430
+ const length = run.length;
431
+ for (let i = 1; i < length; i++) if (run[i] !== symbol) return unknownDate(run, "mixes field symbols");
432
+ const set = (option, value) => {
433
+ if (Object.hasOwn(options, option)) return unknownDate(run, `repeats the ${option} field`);
434
+ options[option] = value;
435
+ return null;
436
+ };
437
+ switch (symbol) {
438
+ case "G": return set("era", length === 4 ? "long" : length === 5 ? "narrow" : "short");
439
+ case "y": return set("year", length === 2 ? "2-digit" : "numeric");
440
+ case "M":
441
+ case "L": return set("month", MONTH_WIDTHS[length - 1]);
442
+ case "d": return set("day", NUMERIC_2DIGIT[length - 1]);
443
+ case "E": return set("weekday", length === 4 ? "long" : length === 5 ? "narrow" : "short");
444
+ case "e":
445
+ case "c":
446
+ if (length < 4) return unknownDate(run, "numeric weekdays are not supported; use EEE or EEEE");
447
+ return set("weekday", LOCAL_WEEKDAY[length - 3]);
448
+ case "a": return set("hour12", true);
449
+ case "h":
450
+ case "H":
451
+ case "K":
452
+ case "k": return set("hourCycle", HOUR_CYCLES[symbol]) ?? set("hour", NUMERIC_2DIGIT[length - 1]);
453
+ case "j": return set("hour", NUMERIC_2DIGIT[length - 1]);
454
+ case "J": return set("hourCycle", "h23") ?? set("hour", NUMERIC_2DIGIT[length - 1]);
455
+ case "m": return set("minute", NUMERIC_2DIGIT[length - 1]);
456
+ case "s": return set("second", NUMERIC_2DIGIT[length - 1]);
457
+ case "z": return set("timeZoneName", length < 4 ? "short" : "long");
458
+ case "Y":
459
+ case "u":
460
+ case "U":
461
+ case "r": return unknownDate(run, "use y for the year");
462
+ case "D":
463
+ case "F": return unknownDate(run, "use d for the day");
464
+ case "b":
465
+ case "B": return unknownDate(run, "use a for the day period, or j for the locale's preferred hour");
466
+ case "Z":
467
+ case "O":
468
+ case "v":
469
+ case "V":
470
+ case "X":
471
+ case "x": return unknownDate(run, "use z for the time zone");
472
+ default: return unknownDate(run, "Intl.DateTimeFormat cannot format this field");
473
+ }
474
+ }
475
+ function unknownDate(field, why) {
476
+ return fail("icu-unknown-skeleton", `Unsupported date skeleton field "${field}": ${why}`);
477
+ }
478
+ function isAsciiLetter$1(c) {
479
+ return c >= 65 && c <= 90 || c >= 97 && c <= 122;
480
+ }
481
+ function nonEmpty(options) {
482
+ for (const _ in options) return options;
483
+ return null;
484
+ }
485
+ let currencies;
486
+ let units;
487
+ function supportedCurrencies() {
488
+ return currencies ??= new Set(Intl.supportedValuesOf("currency"));
489
+ }
490
+ /** A sanctioned unit, or `<unit>-per-<unit>` of two (what Intl.NumberFormat accepts). */
491
+ function isSupportedUnit(unit) {
492
+ units ??= new Set(Intl.supportedValuesOf("unit"));
493
+ if (units.has(unit)) return true;
494
+ const per = unit.indexOf("-per-");
495
+ return per > 0 && units.has(unit.slice(0, per)) && units.has(unit.slice(per + 5));
496
+ }
497
+ //#endregion
498
+ //#region src/core/icu/parse.ts
499
+ /**
500
+ * ICU MessageFormat 1 in FormatJS's dialect (INTERNALS §2.1), written as one
501
+ * recursive-descent pass over UTF-16 code units: every syntax character is
502
+ * ASCII, so literal text is copied by slices and never walked by code point.
503
+ *
504
+ * Deviations from `@formatjs/icu-messageformat-parser` 3.5.21, each tested:
505
+ * standalone tags (`<br/>`) are tag nodes; RBNF types, `choice`, pattern
506
+ * styles, unknown skeletons, currencies and units are rejected; `=N` keys are
507
+ * normalised (so `=1` and `=01` are duplicates); and `#` follows the
508
+ * pound-sensitivity rule below instead of looking at the parent argument only.
509
+ */
510
+ const TAB$1 = 9;
511
+ const CR$2 = 13;
512
+ const SPACE$1 = 32;
513
+ const HASH$2 = 35;
514
+ const APOS$1 = 39;
515
+ const PLUS$1 = 43;
516
+ const COMMA = 44;
517
+ const MINUS$1 = 45;
518
+ const SLASH$2 = 47;
519
+ const COLON$1 = 58;
520
+ const LT$1 = 60;
521
+ const EQUALS$1 = 61;
522
+ const GT$1 = 62;
523
+ const LBRACE$2 = 123;
524
+ const RBRACE$2 = 125;
525
+ /** `[^\p{White_Space}\p{Pattern_Syntax}]*`: FormatJS's argument and selector names. */
526
+ const IDENTIFIER = /[^\p{White_Space}\p{Pattern_Syntax}]*/uy;
527
+ const UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
528
+ "spellout",
529
+ "ordinal",
530
+ "duration",
531
+ "choice"
532
+ ]);
533
+ /**
534
+ * Options and tags nested deeper than this are rejected. The parser, printer
535
+ * and validator recurse, so without a limit one hostile catalog entry of a
536
+ * few kilobytes overflows the stack with a RangeError instead of a diagnostic.
537
+ */
538
+ const MAX_DEPTH = 200;
539
+ /**
540
+ * Parses a message already passed through `normalizeSource` (NFC + LF), so
541
+ * offsets in `loc` and in errors refer to that string.
542
+ *
543
+ * @throws {DiagnosticError} `icu-*` with 1-based `line`/`column` in the message.
544
+ */
545
+ function parseICU(source, options) {
546
+ return new Parser$1(source, options?.locations === true, null).parseRoot();
547
+ }
548
+ /**
549
+ * Offsets of apostrophes that open quoted literal text (`'{`, `'<`, `'}`,
550
+ * `'>`, `'#` where pound-sensitive). The apostrophe lint reports them: most
551
+ * are a typographic apostrophe that silently swallowed a placeholder. Returns
552
+ * what was found before a syntax error, if any.
553
+ */
554
+ function quoteStarts(source) {
555
+ const found = [];
556
+ try {
557
+ new Parser$1(source, false, found).parseRoot();
558
+ } catch {}
559
+ return found;
560
+ }
561
+ /** 1-based line and column (UTF-16 code units) of an offset. */
562
+ function positionAt(source, offset) {
563
+ let line = 1;
564
+ let lineStart = 0;
565
+ const end = Math.min(offset, source.length);
566
+ for (let i = source.indexOf("\n"); i !== -1 && i < end; i = source.indexOf("\n", i + 1)) {
567
+ line++;
568
+ lineStart = i + 1;
569
+ }
570
+ return {
571
+ line,
572
+ column: end - lineStart + 1
573
+ };
574
+ }
575
+ var Parser$1 = class {
576
+ src;
577
+ locations;
578
+ quotes;
579
+ pos = 0;
580
+ constructor(src, locations, quotes) {
581
+ this.src = src;
582
+ this.locations = locations;
583
+ this.quotes = quotes;
584
+ }
585
+ parseRoot() {
586
+ return this.parseMessage(0, false, false, false);
587
+ }
588
+ /**
589
+ * @param depth > 0 inside an option or a tag, where `}` ends the message
590
+ * @param pluralParent the nearest enclosing argument is a plural or selectordinal
591
+ * @param poundSensitive any enclosing argument is a plural or selectordinal
592
+ * @param inTag a closing tag ends the message instead of being an error
593
+ */
594
+ parseMessage(depth, pluralParent, poundSensitive, inTag) {
595
+ if (depth > MAX_DEPTH) throw this.error("icu-syntax", `Message is nested too deeply (more than ${MAX_DEPTH} levels of options and tags): split it`, this.pos);
596
+ const nodes = [];
597
+ const src = this.src;
598
+ while (this.pos < src.length) {
599
+ const c = src.charCodeAt(this.pos);
600
+ if (c === LBRACE$2) nodes.push(this.parseArgument(depth, poundSensitive, inTag));
601
+ else if (c === RBRACE$2 && depth > 0) break;
602
+ else if (c === HASH$2 && poundSensitive) {
603
+ if (!pluralParent) throw this.error("icu-pound-nested", "# is ambiguous inside a select nested in a plural: write the plural's argument (as in {count}) for the number, or '#' for a literal #", this.pos);
604
+ const start = this.pos++;
605
+ nodes.push(this.locations ? {
606
+ type: "pound",
607
+ loc: {
608
+ start,
609
+ end: this.pos
610
+ }
611
+ } : { type: "pound" });
612
+ } else if (c === LT$1 && src.charCodeAt(this.pos + 1) === SLASH$2) {
613
+ if (inTag) break;
614
+ throw this.error("icu-syntax", "Closing tag without a matching opening tag", this.pos);
615
+ } else if (c === LT$1 && isAsciiLetter(src.charCodeAt(this.pos + 1))) nodes.push(this.parseTag(depth, pluralParent, poundSensitive));
616
+ else {
617
+ const start = this.pos;
618
+ const value = this.parseLiteral(depth, poundSensitive);
619
+ nodes.push(this.locations ? {
620
+ type: "literal",
621
+ value,
622
+ loc: {
623
+ start,
624
+ end: this.pos
625
+ }
626
+ } : {
627
+ type: "literal",
628
+ value
629
+ });
630
+ }
631
+ }
632
+ return nodes;
633
+ }
634
+ /** Reads literal text up to the next syntax character, removing ICU quoting. */
635
+ parseLiteral(depth, poundSensitive) {
636
+ const src = this.src;
637
+ const length = src.length;
638
+ let value = "";
639
+ let pos = this.pos;
640
+ let run = pos;
641
+ while (pos < length) {
642
+ const c = src.charCodeAt(pos);
643
+ if (c === APOS$1) {
644
+ const next = src.charCodeAt(pos + 1);
645
+ if (next === APOS$1) {
646
+ value += src.slice(run, pos + 1);
647
+ pos += 2;
648
+ run = pos;
649
+ continue;
650
+ }
651
+ if (next === LBRACE$2 || next === RBRACE$2 || next === LT$1 || next === GT$1 || next === HASH$2 && poundSensitive) {
652
+ this.quotes?.push(pos);
653
+ value += src.slice(run, pos);
654
+ let quoted = pos + 1;
655
+ pos += 2;
656
+ for (;;) {
657
+ if (pos >= length) {
658
+ value += src.slice(quoted, pos);
659
+ break;
660
+ }
661
+ if (src.charCodeAt(pos) === APOS$1) {
662
+ if (src.charCodeAt(pos + 1) === APOS$1) {
663
+ value += src.slice(quoted, pos + 1);
664
+ pos += 2;
665
+ quoted = pos;
666
+ continue;
667
+ }
668
+ value += src.slice(quoted, pos);
669
+ pos++;
670
+ break;
671
+ }
672
+ pos++;
673
+ }
674
+ run = pos;
675
+ continue;
676
+ }
677
+ pos++;
678
+ continue;
679
+ }
680
+ if (c === LBRACE$2) break;
681
+ if (c === LT$1) {
682
+ const next = src.charCodeAt(pos + 1);
683
+ if (next === SLASH$2 || isAsciiLetter(next)) break;
684
+ } else if (c === HASH$2 && poundSensitive || c === RBRACE$2 && depth > 0) break;
685
+ pos++;
686
+ }
687
+ this.pos = pos;
688
+ if (value === "") return src.slice(run, pos);
689
+ return (run === pos ? value : value + src.slice(run, pos)).normalize("NFC");
690
+ }
691
+ parseTag(depth, pluralParent, poundSensitive) {
692
+ const start = this.pos++;
693
+ const name = this.parseTagName();
694
+ this.skipSpace();
695
+ if (this.src.startsWith("/>", this.pos)) {
696
+ this.pos += 2;
697
+ return this.locations ? {
698
+ type: "tag",
699
+ name,
700
+ children: null,
701
+ loc: {
702
+ start,
703
+ end: this.pos
704
+ }
705
+ } : {
706
+ type: "tag",
707
+ name,
708
+ children: null
709
+ };
710
+ }
711
+ if (this.src.charCodeAt(this.pos) !== GT$1) throw this.error("icu-syntax", `Malformed tag <${name}: tags have no attributes`, start);
712
+ this.pos++;
713
+ const children = this.parseMessage(depth + 1, pluralParent, poundSensitive, true);
714
+ const closeStart = this.pos;
715
+ if (!this.src.startsWith("</", this.pos)) throw this.error("icu-syntax", `Tag <${name}> is not closed: add </${name}>`, start);
716
+ this.pos += 2;
717
+ if (!isAsciiLetter(this.src.charCodeAt(this.pos))) throw this.error("icu-syntax", `Malformed closing tag for <${name}>`, closeStart);
718
+ const closeName = this.parseTagName();
719
+ if (closeName !== name) throw this.error("icu-syntax", `Closing tag </${closeName}> does not match <${name}>`, closeStart);
720
+ this.skipSpace();
721
+ if (this.src.charCodeAt(this.pos) !== GT$1) throw this.error("icu-syntax", `Malformed closing tag for <${name}>`, closeStart);
722
+ this.pos++;
723
+ return this.locations ? {
724
+ type: "tag",
725
+ name,
726
+ children,
727
+ loc: {
728
+ start,
729
+ end: this.pos
730
+ }
731
+ } : {
732
+ type: "tag",
733
+ name,
734
+ children
735
+ };
736
+ }
737
+ /** Assumes the current character is an ASCII letter (checked by the caller). */
738
+ parseTagName() {
739
+ const src = this.src;
740
+ const start = this.pos++;
741
+ while (this.pos < src.length) {
742
+ const c = src.codePointAt(this.pos);
743
+ if (!isTagNameChar(c)) break;
744
+ this.pos += c > 65535 ? 2 : 1;
745
+ }
746
+ return src.slice(start, this.pos);
747
+ }
748
+ parseArgument(depth, poundSensitive, inTag) {
749
+ const src = this.src;
750
+ const open = this.pos++;
751
+ this.skipSpace();
752
+ if (this.pos >= src.length) throw this.unclosed(open);
753
+ if (src.charCodeAt(this.pos) === RBRACE$2) throw this.error("icu-syntax", "Empty argument {}: name it, as in {name}", open);
754
+ const name = this.identifier();
755
+ if (name === "") throw this.malformed(open);
756
+ this.skipSpace();
757
+ if (this.pos >= src.length) throw this.unclosed(open);
758
+ const c = src.charCodeAt(this.pos);
759
+ if (c === RBRACE$2) {
760
+ this.pos++;
761
+ return this.locations ? {
762
+ type: "argument",
763
+ name,
764
+ loc: {
765
+ start: open,
766
+ end: this.pos
767
+ }
768
+ } : {
769
+ type: "argument",
770
+ name
771
+ };
772
+ }
773
+ if (c !== COMMA) throw this.malformed(open);
774
+ this.pos++;
775
+ this.skipSpace();
776
+ if (this.pos >= src.length) throw this.unclosed(open);
777
+ const typeStart = this.pos;
778
+ const type = this.identifier();
779
+ switch (type) {
780
+ case "number":
781
+ case "date":
782
+ case "time": return this.parseFormatted(type, name, open);
783
+ case "select":
784
+ case "plural":
785
+ case "selectordinal": return this.parseChoice(type, name, open, depth, poundSensitive, inTag);
786
+ case "": throw this.error("icu-syntax", `Expected an argument type after {${name},`, typeStart);
787
+ default:
788
+ if (UNSUPPORTED_TYPES.has(type)) throw this.error("icu-unsupported-type", type === "choice" ? "{…, choice} is not supported: use {…, plural} or {…, select}" : `{…, ${type}} is not supported because Intl cannot render it: use {…, number}, {…, plural} or {…, selectordinal}`, typeStart);
789
+ throw this.error("icu-syntax", `Unknown argument type "${type}": use number, date, time, select, plural or selectordinal`, typeStart);
790
+ }
791
+ }
792
+ parseFormatted(type, name, open) {
793
+ const src = this.src;
794
+ this.skipSpace();
795
+ let style = null;
796
+ let styleStart = this.pos;
797
+ if (src.charCodeAt(this.pos) === COMMA) {
798
+ this.pos++;
799
+ this.skipSpace();
800
+ styleStart = this.pos;
801
+ style = this.simpleStyle().trimEnd();
802
+ if (style.length === 0) throw this.error("icu-syntax", `Expected a style after {${name}, ${type},`, this.pos);
803
+ }
804
+ if (src.charCodeAt(this.pos) !== RBRACE$2) throw this.unclosed(open);
805
+ this.pos++;
806
+ const resolved = resolveStyle(type, style);
807
+ if (!resolved.ok) throw this.error(resolved.code, resolved.message, styleStart);
808
+ const node = {
809
+ type,
810
+ name,
811
+ style: resolved.style,
812
+ options: resolved.options
813
+ };
814
+ if (this.locations) node.loc = {
815
+ start: open,
816
+ end: this.pos
817
+ };
818
+ return node;
819
+ }
820
+ /** ICU `MessagePattern` simple style: up to the unbalanced `}`, quotes kept. */
821
+ simpleStyle() {
822
+ const src = this.src;
823
+ const start = this.pos;
824
+ let nested = 0;
825
+ while (this.pos < src.length) {
826
+ const c = src.charCodeAt(this.pos);
827
+ if (c === APOS$1) {
828
+ const close = src.indexOf("'", this.pos + 1);
829
+ if (close === -1) throw this.error("icu-syntax", "Unclosed quote in the argument style", this.pos);
830
+ this.pos = close + 1;
831
+ } else if (c === LBRACE$2) {
832
+ nested++;
833
+ this.pos++;
834
+ } else if (c === RBRACE$2) {
835
+ if (nested === 0) break;
836
+ nested--;
837
+ this.pos++;
838
+ } else this.pos++;
839
+ }
840
+ return src.slice(start, this.pos);
841
+ }
842
+ parseChoice(type, name, open, depth, poundSensitive, inTag) {
843
+ const src = this.src;
844
+ this.skipSpace();
845
+ if (src.charCodeAt(this.pos) !== COMMA) throw this.error("icu-syntax", `Expected options after {${name}, ${type}`, this.pos);
846
+ this.pos++;
847
+ this.skipSpace();
848
+ let keyStart = this.pos;
849
+ let selector = this.identifier();
850
+ let offset = 0;
851
+ const plural = type !== "select";
852
+ if (plural && selector === "offset") {
853
+ if (src.charCodeAt(this.pos) !== COLON$1) throw this.error("icu-syntax", "Expected offset:<integer>", this.pos);
854
+ this.pos++;
855
+ this.skipSpace();
856
+ offset = this.decimal("Expected an integer after offset:");
857
+ this.skipSpace();
858
+ keyStart = this.pos;
859
+ selector = this.identifier();
860
+ }
861
+ const options = [];
862
+ const keys = /* @__PURE__ */ new Set();
863
+ const childPoundSensitive = poundSensitive || plural;
864
+ for (;;) {
865
+ let key = selector;
866
+ if (key === "") {
867
+ if (!plural || src.charCodeAt(this.pos) !== EQUALS$1) break;
868
+ this.pos++;
869
+ key = "=" + String(this.decimal(`Expected a number after = in {${name}, ${type}}`));
870
+ }
871
+ if (keys.has(key)) throw this.error("icu-syntax", `Duplicate option "${key}" in {${name}, ${type}}`, keyStart);
872
+ keys.add(key);
873
+ this.skipSpace();
874
+ if (src.charCodeAt(this.pos) !== LBRACE$2) throw this.error("icu-syntax", `Expected {…} after option "${key}" in {${name}, ${type}}`, this.pos);
875
+ const valueOpen = this.pos++;
876
+ const value = this.parseMessage(depth + 1, plural, childPoundSensitive, inTag);
877
+ if (src.charCodeAt(this.pos) !== RBRACE$2) throw this.unclosed(valueOpen);
878
+ this.pos++;
879
+ options.push(this.locations ? {
880
+ key,
881
+ value,
882
+ loc: {
883
+ start: keyStart,
884
+ end: this.pos
885
+ }
886
+ } : {
887
+ key,
888
+ value
889
+ });
890
+ this.skipSpace();
891
+ keyStart = this.pos;
892
+ selector = this.identifier();
893
+ }
894
+ if (options.length === 0) throw this.error("icu-syntax", `Expected at least one option in {${name}, ${type}}`, this.pos);
895
+ if (!keys.has("other")) throw this.error("icu-missing-other", `{${name}, ${type}} needs an other {…} option`, open);
896
+ if (src.charCodeAt(this.pos) !== RBRACE$2) throw this.unclosed(open);
897
+ this.pos++;
898
+ if (type === "select") return this.locations ? {
899
+ type: "select",
900
+ name,
901
+ options,
902
+ loc: {
903
+ start: open,
904
+ end: this.pos
905
+ }
906
+ } : {
907
+ type: "select",
908
+ name,
909
+ options
910
+ };
911
+ const node = {
912
+ type: "plural",
913
+ pluralType: type === "plural" ? "cardinal" : "ordinal",
914
+ name,
915
+ offset,
916
+ options
917
+ };
918
+ if (this.locations) node.loc = {
919
+ start: open,
920
+ end: this.pos
921
+ };
922
+ return node;
923
+ }
924
+ /** FormatJS `tryParseDecimalInteger`: optional sign, ASCII digits, a safe integer. */
925
+ decimal(expected) {
926
+ const src = this.src;
927
+ const start = this.pos;
928
+ let sign = 1;
929
+ const c = src.charCodeAt(this.pos);
930
+ if (c === PLUS$1) this.pos++;
931
+ else if (c === MINUS$1) {
932
+ sign = -1;
933
+ this.pos++;
934
+ }
935
+ const digits = this.pos;
936
+ let value = 0;
937
+ while (this.pos < src.length) {
938
+ const d = src.charCodeAt(this.pos) - 48;
939
+ if (d < 0 || d > 9) break;
940
+ value = value * 10 + d;
941
+ this.pos++;
942
+ }
943
+ if (this.pos === digits) throw this.error("icu-syntax", expected, start);
944
+ if (!Number.isSafeInteger(value)) throw this.error("icu-syntax", "Number is too large", start);
945
+ return sign * value + 0;
946
+ }
947
+ identifier() {
948
+ IDENTIFIER.lastIndex = this.pos;
949
+ const match = IDENTIFIER.exec(this.src);
950
+ const name = match === null ? "" : match[0];
951
+ this.pos += name.length;
952
+ return name;
953
+ }
954
+ /** FormatJS `bumpSpace`: Pattern_White_Space (note: LRM/RLM yes, NBSP no). */
955
+ skipSpace() {
956
+ const src = this.src;
957
+ while (this.pos < src.length) {
958
+ const c = src.charCodeAt(this.pos);
959
+ if (c >= TAB$1 && c <= CR$2 || c === SPACE$1 || c === 133 || c === 8206 || c === 8207 || c === 8232 || c === 8233) this.pos++;
960
+ else break;
961
+ }
962
+ }
963
+ unclosed(open) {
964
+ return this.error("icu-syntax", "Argument is not closed: expected }", open);
965
+ }
966
+ malformed(open) {
967
+ return this.error("icu-syntax", "Malformed argument: names cannot contain spaces or ICU syntax characters", open);
968
+ }
969
+ error(code, message, offset) {
970
+ const { line, column } = positionAt(this.src, offset);
971
+ return diagnosticError({
972
+ code,
973
+ severity: "error",
974
+ message,
975
+ line,
976
+ column
977
+ });
978
+ }
979
+ };
980
+ function isAsciiLetter(c) {
981
+ return c >= 65 && c <= 90 || c >= 97 && c <= 122;
982
+ }
983
+ /** FormatJS `_isPotentialElementNameChar` (custom-element name characters); MF2 markup names are checked with it too. */
984
+ function isTagNameChar(c) {
985
+ return c === 45 || c === 46 || c >= 48 && c <= 57 || c === 95 || c >= 97 && c <= 122 || c >= 65 && c <= 90 || c === 183 || c >= 192 && c <= 214 || c >= 216 && c <= 246 || c >= 248 && c <= 893 || c >= 895 && c <= 8191 || c >= 8204 && c <= 8205 || c >= 8255 && c <= 8256 || c >= 8304 && c <= 8591 || c >= 11264 && c <= 12271 || c >= 12289 && c <= 55295 || c >= 63744 && c <= 64975 || c >= 65008 && c <= 65533 || c >= 65536 && c <= 983039;
986
+ }
987
+ //#endregion
988
+ //#region src/core/mf2/syntax.ts
989
+ /**
990
+ * MessageFormat 2 syntax (Unicode LDML part 9, `message.abnf`) read into its
991
+ * data model, with the spec's data-model errors (PLAN §4.2, INTERNALS §2.1.4).
992
+ * This is the second front end: `lower.ts` turns the result into the ICU AST
993
+ * that validation, ids, catalogs and the IR compiler already read, so nothing
994
+ * after the parser knows MF2 exists.
995
+ *
996
+ * One pass over UTF-16 code units; names are read by code point. Every error
997
+ * is `icu-syntax` with an "MF2:" message and a 1-based position.
998
+ */
999
+ const NUL = 0;
1000
+ const TAB = 9;
1001
+ const LF = 10;
1002
+ const CR$1 = 13;
1003
+ const SPACE = 32;
1004
+ const HASH$1 = 35;
1005
+ const DOLLAR = 36;
1006
+ const PLUS = 43;
1007
+ const MINUS = 45;
1008
+ const DOT = 46;
1009
+ const SLASH$1 = 47;
1010
+ const COLON = 58;
1011
+ const EQUALS = 61;
1012
+ const AT = 64;
1013
+ const BACKSLASH = 92;
1014
+ const UNDERSCORE = 95;
1015
+ const LBRACE$1 = 123;
1016
+ const PIPE = 124;
1017
+ const RBRACE$1 = 125;
1018
+ const IDEOGRAPHIC_SPACE = 12288;
1019
+ /** `ws`: SP, HTAB, CR, LF, U+3000. */
1020
+ const isWs = (c) => c === SPACE || c === TAB || c === LF || c === CR$1 || c === IDEOGRAPHIC_SPACE;
1021
+ /** `bidi`: ALM, LRM, RLM and the four isolates. */
1022
+ const isBidi = (c) => c === 1564 || c === 8206 || c === 8207 || c >= 8294 && c <= 8297;
1023
+ /** `name-start`, by code point. */
1024
+ function isNameStart(c) {
1025
+ if (c < 161) return c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === PLUS || c === UNDERSCORE;
1026
+ return (c <= 1563 || c >= 1565 && c <= 5759 || c >= 5761 && c <= 8191 || c >= 8203 && c <= 8205 || c >= 8208 && c <= 8231 || c >= 8240 && c <= 8286 || c >= 8288 && c <= 8293 || c >= 8298 && c <= 12287 || c >= 12289 && c <= 55295 || c >= 57344 && c <= 64975 || c >= 65008 && c <= 65533 || c >= 65536 && c <= 1114109) && (c & 65534) !== 65534;
1027
+ }
1028
+ const isNameChar = (c) => isNameStart(c) || c >= 48 && c <= 57 || c === MINUS || c === DOT;
1029
+ /**
1030
+ * Every MF2 diagnostic: syntax and data-model errors are `icu-syntax`, and
1031
+ * what vi18n cannot express is `icu-unsupported-type` (`lower.ts`), so the
1032
+ * CLI, catalogs and `check` report them like any invalid message.
1033
+ */
1034
+ function mf2Error(source, message, offset, code = "icu-syntax") {
1035
+ const { line, column } = positionAt(source, offset);
1036
+ return diagnosticError({
1037
+ code,
1038
+ severity: "error",
1039
+ message: `MF2: ${message}`,
1040
+ line,
1041
+ column
1042
+ });
1043
+ }
1044
+ /**
1045
+ * Parses a message already passed through `normalizeSource` (NFC + LF).
1046
+ *
1047
+ * @throws {DiagnosticError} `icu-syntax` for syntax and data-model errors.
1048
+ */
1049
+ function parseMf2Syntax(source) {
1050
+ const message = new Parser(source).message();
1051
+ checkDataModel(source, message);
1052
+ return message;
1053
+ }
1054
+ var Parser = class {
1055
+ src;
1056
+ pos = 0;
1057
+ constructor(src) {
1058
+ this.src = src;
1059
+ }
1060
+ message() {
1061
+ this.optional();
1062
+ if (this.at(DOT) || this.src.startsWith("{{", this.pos)) return this.complex();
1063
+ this.pos = 0;
1064
+ return {
1065
+ declarations: [],
1066
+ selectors: [],
1067
+ variants: [],
1068
+ pattern: this.pattern(false),
1069
+ matchAt: 0
1070
+ };
1071
+ }
1072
+ complex() {
1073
+ const declarations = [];
1074
+ for (;;) {
1075
+ this.optional();
1076
+ const at = this.pos;
1077
+ if (this.keyword(".input")) {
1078
+ this.optional();
1079
+ if (!this.at(LBRACE$1)) throw this.fail("expected {$variable …} after .input");
1080
+ const value = this.expression();
1081
+ if (value.operand?.type !== "variable") throw this.fail(".input declares a variable: write .input {$name …}", value.at);
1082
+ declarations.push({
1083
+ kind: "input",
1084
+ name: value.operand.name,
1085
+ value,
1086
+ at
1087
+ });
1088
+ } else if (this.keyword(".local")) {
1089
+ this.space("expected a space after .local");
1090
+ if (!this.at(DOLLAR)) throw this.fail("expected $variable after .local");
1091
+ const name = this.variable();
1092
+ this.optional();
1093
+ if (!this.at(EQUALS)) throw this.fail(`expected = after .local $${name}`);
1094
+ this.pos++;
1095
+ this.optional();
1096
+ if (!this.at(LBRACE$1)) throw this.fail(`expected {…} after .local $${name} =`);
1097
+ declarations.push({
1098
+ kind: "local",
1099
+ name,
1100
+ value: this.expression(),
1101
+ at
1102
+ });
1103
+ } else if (this.keyword(".match")) return this.matcher(declarations, at);
1104
+ else if (this.src.startsWith("{{", this.pos)) {
1105
+ this.pos += 2;
1106
+ const pattern = this.pattern(true);
1107
+ this.end();
1108
+ return {
1109
+ declarations,
1110
+ selectors: [],
1111
+ variants: [],
1112
+ pattern,
1113
+ matchAt: 0
1114
+ };
1115
+ } else if (this.at(DOT)) throw this.fail("unknown keyword: use .input, .local or .match");
1116
+ else throw this.fail("expected a declaration, .match or a {{…}} body");
1117
+ }
1118
+ }
1119
+ matcher(declarations, matchAt) {
1120
+ const selectors = [];
1121
+ for (;;) {
1122
+ const save = this.pos;
1123
+ if (!this.optional() || !this.at(DOLLAR)) {
1124
+ this.pos = save;
1125
+ break;
1126
+ }
1127
+ selectors.push(this.variable());
1128
+ }
1129
+ if (selectors.length === 0) throw this.fail("expected $variable after .match");
1130
+ this.space("expected a space before the first variant");
1131
+ const variants = [];
1132
+ for (;;) {
1133
+ const at = this.pos;
1134
+ const keys = [this.key()];
1135
+ for (;;) {
1136
+ const spaced = this.optional();
1137
+ if (this.src.startsWith("{{", this.pos)) break;
1138
+ if (!spaced) throw this.fail("expected a space between keys, or {{");
1139
+ keys.push(this.key());
1140
+ }
1141
+ this.pos += 2;
1142
+ variants.push({
1143
+ keys,
1144
+ pattern: this.pattern(true),
1145
+ at
1146
+ });
1147
+ this.optional();
1148
+ if (this.pos >= this.src.length) break;
1149
+ }
1150
+ return {
1151
+ declarations,
1152
+ selectors,
1153
+ variants,
1154
+ pattern: null,
1155
+ matchAt
1156
+ };
1157
+ }
1158
+ key() {
1159
+ const c = this.src.charCodeAt(this.pos);
1160
+ if (c === 42) {
1161
+ this.pos++;
1162
+ return null;
1163
+ }
1164
+ if (c === PIPE || isNameChar(this.codePoint())) return this.literal().value;
1165
+ throw this.fail("expected a key: a literal or *");
1166
+ }
1167
+ /**
1168
+ * A pattern up to the end of the message (simple) or up to `}}` (quoted).
1169
+ * Text keeps every character but the escapes.
1170
+ */
1171
+ pattern(quoted) {
1172
+ const src = this.src;
1173
+ const parts = [];
1174
+ let text = "";
1175
+ let run = this.pos;
1176
+ for (;;) {
1177
+ if (this.pos >= src.length) {
1178
+ if (quoted) throw this.fail("the pattern is not closed: add }}");
1179
+ text += src.slice(run, this.pos);
1180
+ break;
1181
+ }
1182
+ const c = src.charCodeAt(this.pos);
1183
+ if (c === BACKSLASH) {
1184
+ text += src.slice(run, this.pos) + this.escape();
1185
+ run = this.pos;
1186
+ } else if (c === LBRACE$1) {
1187
+ text += src.slice(run, this.pos);
1188
+ if (text !== "") parts.push(text);
1189
+ text = "";
1190
+ parts.push(this.placeholder());
1191
+ run = this.pos;
1192
+ } else if (c === RBRACE$1) {
1193
+ if (!quoted || src.charCodeAt(this.pos + 1) !== RBRACE$1) throw this.fail("} must be escaped as \\} in text");
1194
+ text += src.slice(run, this.pos);
1195
+ this.pos += 2;
1196
+ break;
1197
+ } else this.textChar(c);
1198
+ }
1199
+ if (text !== "") parts.push(text);
1200
+ return parts;
1201
+ }
1202
+ /** Moves past one text code point, rejecting NUL and lone surrogates. */
1203
+ textChar(c) {
1204
+ if (c === NUL) throw this.fail("NUL is not allowed in a message");
1205
+ if (c >= 55296 && c <= 57343) {
1206
+ const next = this.src.charCodeAt(this.pos + 1);
1207
+ if (c > 56319 || !(next >= 56320 && next <= 57343)) throw this.fail("lone surrogate code unit");
1208
+ this.pos += 2;
1209
+ return;
1210
+ }
1211
+ this.pos++;
1212
+ }
1213
+ escape() {
1214
+ const next = this.src.charCodeAt(this.pos + 1);
1215
+ if (next === BACKSLASH || next === LBRACE$1 || next === PIPE || next === RBRACE$1) {
1216
+ this.pos += 2;
1217
+ return String.fromCharCode(next);
1218
+ }
1219
+ throw this.fail("\\ escapes only \\, {, | and }");
1220
+ }
1221
+ placeholder() {
1222
+ const at = this.pos++;
1223
+ this.optional();
1224
+ const c = this.src.charCodeAt(this.pos);
1225
+ if (c === HASH$1 || c === SLASH$1) return this.markup(at, c === HASH$1);
1226
+ this.pos = at;
1227
+ return this.expression();
1228
+ }
1229
+ /** `{` o (literal | variable | function) … `}`; markup is not an expression. */
1230
+ expression() {
1231
+ const at = this.pos++;
1232
+ this.optional();
1233
+ let operand = null;
1234
+ let fn = null;
1235
+ const c = this.src.charCodeAt(this.pos);
1236
+ if (c === DOLLAR) operand = {
1237
+ type: "variable",
1238
+ name: this.variable()
1239
+ };
1240
+ else if (c === COLON) fn = this.fn();
1241
+ else if (c === PIPE || isNameChar(this.codePoint())) operand = this.literal();
1242
+ else if (c === HASH$1 || c === SLASH$1) throw this.fail("markup cannot be used here");
1243
+ else throw this.fail("expected a value, $variable or :function in {…}");
1244
+ let attributes = false;
1245
+ for (;;) {
1246
+ const spaced = this.optional();
1247
+ if (this.at(RBRACE$1)) {
1248
+ this.pos++;
1249
+ return {
1250
+ type: "expression",
1251
+ operand,
1252
+ fn,
1253
+ at
1254
+ };
1255
+ }
1256
+ if (!spaced) throw this.fail("expected a space or }");
1257
+ if (this.at(COLON) && fn === null && !attributes) fn = this.fn();
1258
+ else if (this.at(AT)) {
1259
+ this.attribute();
1260
+ attributes = true;
1261
+ } else throw this.fail("expected :function, @attribute or }");
1262
+ }
1263
+ }
1264
+ markup(at, open) {
1265
+ this.pos++;
1266
+ const name = this.identifier();
1267
+ const options = this.options();
1268
+ let kind = open ? "open" : "close";
1269
+ for (;;) {
1270
+ const spaced = this.optional();
1271
+ if (this.at(RBRACE$1)) break;
1272
+ if (open && this.at(SLASH$1)) {
1273
+ this.pos++;
1274
+ if (!this.at(RBRACE$1)) throw this.fail("expected } after /");
1275
+ kind = "standalone";
1276
+ break;
1277
+ }
1278
+ if (!spaced || !this.at(AT)) throw this.fail("expected @attribute or }");
1279
+ this.attribute();
1280
+ }
1281
+ this.pos++;
1282
+ return {
1283
+ type: "markup",
1284
+ kind,
1285
+ name,
1286
+ options,
1287
+ at
1288
+ };
1289
+ }
1290
+ /** `:` identifier *(s option). */
1291
+ fn() {
1292
+ this.pos++;
1293
+ return {
1294
+ name: this.identifier(),
1295
+ options: this.options()
1296
+ };
1297
+ }
1298
+ options() {
1299
+ const options = [];
1300
+ const seen = /* @__PURE__ */ new Set();
1301
+ for (;;) {
1302
+ const save = this.pos;
1303
+ if (!this.optional() || !this.nameStartsHere()) {
1304
+ this.pos = save;
1305
+ return options;
1306
+ }
1307
+ const at = this.pos;
1308
+ const name = this.identifier();
1309
+ this.optional();
1310
+ if (!this.at(EQUALS)) throw this.fail(`expected = after the option ${name}`);
1311
+ this.pos++;
1312
+ this.optional();
1313
+ const value = this.at(DOLLAR) ? {
1314
+ type: "variable",
1315
+ name: this.variable()
1316
+ } : this.literal();
1317
+ if (seen.has(name)) throw this.fail(`the option ${name} is given twice`, at);
1318
+ seen.add(name);
1319
+ options.push({
1320
+ name,
1321
+ value
1322
+ });
1323
+ }
1324
+ }
1325
+ /** `@` identifier [o `=` o literal]. Attributes never change the output. */
1326
+ attribute() {
1327
+ this.pos++;
1328
+ this.identifier();
1329
+ const save = this.pos;
1330
+ this.optional();
1331
+ if (!this.at(EQUALS)) {
1332
+ this.pos = save;
1333
+ return;
1334
+ }
1335
+ this.pos++;
1336
+ this.optional();
1337
+ this.literal();
1338
+ }
1339
+ literal() {
1340
+ const src = this.src;
1341
+ if (this.at(PIPE)) {
1342
+ this.pos++;
1343
+ let value = "";
1344
+ let run = this.pos;
1345
+ for (;;) {
1346
+ if (this.pos >= src.length) throw this.fail("the literal is not closed: add |");
1347
+ const c = src.charCodeAt(this.pos);
1348
+ if (c === PIPE) break;
1349
+ if (c === BACKSLASH) {
1350
+ value += src.slice(run, this.pos) + this.escape();
1351
+ run = this.pos;
1352
+ } else this.textChar(c);
1353
+ }
1354
+ value += src.slice(run, this.pos);
1355
+ this.pos++;
1356
+ return {
1357
+ type: "literal",
1358
+ value
1359
+ };
1360
+ }
1361
+ const start = this.pos;
1362
+ while (this.pos < src.length && isNameChar(this.codePoint())) this.advance();
1363
+ if (this.pos === start) throw this.fail("expected a literal");
1364
+ return {
1365
+ type: "literal",
1366
+ value: src.slice(start, this.pos)
1367
+ };
1368
+ }
1369
+ variable() {
1370
+ this.pos++;
1371
+ return this.name();
1372
+ }
1373
+ /** [namespace `:`] name. */
1374
+ identifier() {
1375
+ const first = this.name();
1376
+ if (!this.at(COLON)) return first;
1377
+ this.pos++;
1378
+ return first + ":" + this.name();
1379
+ }
1380
+ /** [bidi] name-start *name-char [bidi]; the marks are not part of the name. */
1381
+ name() {
1382
+ if (isBidi(this.src.charCodeAt(this.pos))) this.pos++;
1383
+ if (!isNameStart(this.codePoint())) throw this.fail("expected a name");
1384
+ const start = this.pos;
1385
+ while (this.pos < this.src.length && isNameChar(this.codePoint())) this.advance();
1386
+ const name = this.src.slice(start, this.pos);
1387
+ if (isBidi(this.src.charCodeAt(this.pos))) this.pos++;
1388
+ return name;
1389
+ }
1390
+ nameStartsHere() {
1391
+ const c = this.src.charCodeAt(this.pos);
1392
+ return isBidi(c) || isNameStart(this.codePoint());
1393
+ }
1394
+ /** `s`: whitespace (with bidi marks) that must contain a ws character. */
1395
+ space(expected) {
1396
+ if (!this.optional()) throw this.fail(expected);
1397
+ }
1398
+ /** `o`: skips whitespace and bidi marks; true when a ws character was among them. */
1399
+ optional() {
1400
+ let ws = false;
1401
+ for (;;) {
1402
+ const c = this.src.charCodeAt(this.pos);
1403
+ if (isWs(c)) ws = true;
1404
+ else if (!isBidi(c)) return ws;
1405
+ this.pos++;
1406
+ }
1407
+ }
1408
+ /**
1409
+ * Keywords are case-sensitive. What may follow one (`{`, whitespace, `$`)
1410
+ * is checked by the caller, so `.inputs` fails there.
1411
+ */
1412
+ keyword(word) {
1413
+ if (!this.src.startsWith(word, this.pos)) return false;
1414
+ this.pos += word.length;
1415
+ return true;
1416
+ }
1417
+ end() {
1418
+ this.optional();
1419
+ if (this.pos < this.src.length) throw this.fail("unexpected text after the message body");
1420
+ }
1421
+ at(c) {
1422
+ return this.src.charCodeAt(this.pos) === c;
1423
+ }
1424
+ codePoint() {
1425
+ return this.src.codePointAt(this.pos) ?? -1;
1426
+ }
1427
+ advance() {
1428
+ this.pos += this.src.codePointAt(this.pos) > 65535 ? 2 : 1;
1429
+ }
1430
+ fail(message, offset = this.pos) {
1431
+ return mf2Error(this.src, message, offset);
1432
+ }
1433
+ };
1434
+ /**
1435
+ * The spec's data-model errors: duplicate declarations, selectors without
1436
+ * an annotation, variants with the wrong number of keys, a missing `* …`
1437
+ * fallback and duplicate variants.
1438
+ */
1439
+ function checkDataModel(source, message) {
1440
+ const mentioned = /* @__PURE__ */ new Set();
1441
+ const declared = /* @__PURE__ */ new Map();
1442
+ for (const declaration of message.declarations) {
1443
+ const own = variablesOf(declaration.value);
1444
+ if (declaration.kind === "local" && own.includes(declaration.name) || declared.has(declaration.name) || mentioned.has(declaration.name)) throw mf2Error(source, `$${declaration.name} is declared after it was used or declared: declare each variable once, before its first use`, declaration.at);
1445
+ declared.set(declaration.name, declaration);
1446
+ for (const name of own) mentioned.add(name);
1447
+ }
1448
+ if (message.pattern !== null) return;
1449
+ for (const selector of message.selectors) if (!annotated(selector, declared, /* @__PURE__ */ new Set())) throw mf2Error(source, `.match $${selector} needs a selector function: declare it as .input {$${selector} :number} or {$${selector} :string}`, message.matchAt);
1450
+ const count = message.selectors.length;
1451
+ let fallback = false;
1452
+ const seen = /* @__PURE__ */ new Set();
1453
+ for (const variant of message.variants) {
1454
+ if (variant.keys.length !== count) throw mf2Error(source, `this variant has ${variant.keys.length} key${variant.keys.length === 1 ? "" : "s"} for ${count} selector${count === 1 ? "" : "s"}`, variant.at);
1455
+ if (variant.keys.every((key) => key === null)) fallback = true;
1456
+ const id = JSON.stringify(variant.keys);
1457
+ if (seen.has(id)) throw mf2Error(source, "two variants have the same keys", variant.at);
1458
+ seen.add(id);
1459
+ }
1460
+ if (!fallback) throw mf2Error(source, `.match needs a fallback variant whose keys are all * (${Array(count).fill("*").join(" ")} {{…}})`, message.matchAt);
1461
+ }
1462
+ function variablesOf(expression) {
1463
+ const names = [];
1464
+ if (expression.operand?.type === "variable") names.push(expression.operand.name);
1465
+ for (const option of expression.fn?.options ?? []) if (option.value.type === "variable") names.push(option.value.name);
1466
+ return names;
1467
+ }
1468
+ /** A selector needs a function on its declaration or, through `.local` aliases, on one it refers to. */
1469
+ function annotated(name, declared, visiting) {
1470
+ const declaration = declared.get(name);
1471
+ if (declaration === void 0 || visiting.has(name)) return false;
1472
+ if (declaration.value.fn !== null) return true;
1473
+ const operand = declaration.value.operand;
1474
+ visiting.add(name);
1475
+ return operand?.type === "variable" && declaration.kind === "local" ? annotated(operand.name, declared, visiting) : false;
1476
+ }
1477
+ //#endregion
1478
+ //#region src/core/mf2/lower.ts
1479
+ /**
1480
+ * MF2 data model → the ICU AST (INTERNALS §2.1.4): the IR, ids, catalogs,
1481
+ * validation and pseudo-locales read MF2 messages exactly as they read ICU
1482
+ * ones, and the runtime gains no opcode (the reserved `.match` opcode 10
1483
+ * stays unused).
1484
+ *
1485
+ * `.match` with several selectors becomes nested select and plural
1486
+ * arguments built so that every value picks the variant MF2's pattern
1487
+ * selection picks: variants are filtered per selector and ranked by the
1488
+ * first selector, then the second, with `*` last. An exact number key is
1489
+ * preferred to a plural category, which only the runtime knows, so the case
1490
+ * of an `=N` key holds a second plural on the same argument for the
1491
+ * categories. Constant selectors (`.local $x = {a :string}`) are decided here.
1492
+ *
1493
+ * What the IR cannot express is `icu-unsupported-type`: formatting a literal
1494
+ * number, options set from variables, `:percent` as a selector, names that
1495
+ * are no ICU argument, select keys that are no ICU identifier, markup with
1496
+ * options or left open.
1497
+ */
1498
+ const KNOWN = /* @__PURE__ */ new Set([
1499
+ "string",
1500
+ "number",
1501
+ "integer",
1502
+ "percent",
1503
+ "currency",
1504
+ "date",
1505
+ "time",
1506
+ "datetime"
1507
+ ]);
1508
+ const NUMERIC = /* @__PURE__ */ new Set([
1509
+ "number",
1510
+ "integer",
1511
+ "percent",
1512
+ "currency"
1513
+ ]);
1514
+ /** `[^\p{White_Space}\p{Pattern_Syntax}]+`: ICU argument names and select keys. */
1515
+ const ICU_IDENTIFIER = /^[^\p{White_Space}\p{Pattern_Syntax}]+$/u;
1516
+ /** MF2's `number-literal`. */
1517
+ const NUMBER_LITERAL = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][-+]?\d+)?$/;
1518
+ /** Exact keys vi18n can match: `=N` takes integers. */
1519
+ const INTEGER_KEY = /^(?:0|-?[1-9]\d*)$/;
1520
+ /** MF2's `digit-size-option`. */
1521
+ const DIGIT_SIZE = /^(?:0|[1-9]\d?)$/;
1522
+ const CATEGORIES = [
1523
+ "zero",
1524
+ "one",
1525
+ "two",
1526
+ "few",
1527
+ "many",
1528
+ "other"
1529
+ ];
1530
+ /**
1531
+ * Steps of the `.match` expansion one message may take. Keys multiply across
1532
+ * selectors, so a hostile or generated message could otherwise take
1533
+ * exponential time; real messages stay far below.
1534
+ */
1535
+ const MAX_STEPS = 2e4;
1536
+ /**
1537
+ * An option problem, thrown by the style builders and turned into a
1538
+ * diagnostic where the expression's offset is known: an unknown option or
1539
+ * value is an error (`icu-syntax`), a valid one the IR cannot express is
1540
+ * `icu-unsupported-type`.
1541
+ */
1542
+ var Unsupported = class {
1543
+ message;
1544
+ code;
1545
+ constructor(message, code = "icu-unsupported-type") {
1546
+ this.message = message;
1547
+ this.code = code;
1548
+ }
1549
+ };
1550
+ const invalid = (message) => new Unsupported(message, "icu-syntax");
1551
+ /**
1552
+ * Lowers a parsed MF2 message (`parseMf2Syntax`) to the ICU AST.
1553
+ *
1554
+ * @throws {DiagnosticError} `icu-syntax` (unknown function, bad operand or
1555
+ * option) or `icu-unsupported-type` (valid MF2 that the IR cannot express).
1556
+ */
1557
+ function lowerMf2(source, message) {
1558
+ return new Lowering(source, message).run();
1559
+ }
1560
+ var Lowering = class {
1561
+ source;
1562
+ message;
1563
+ env = /* @__PURE__ */ new Map();
1564
+ lowered = /* @__PURE__ */ new Map();
1565
+ steps = 0;
1566
+ constructor(source, message) {
1567
+ this.source = source;
1568
+ this.message = message;
1569
+ }
1570
+ run() {
1571
+ for (const declaration of this.message.declarations) this.env.set(declaration.name, this.resolve(declaration.value));
1572
+ if (this.message.pattern !== null) return this.pattern(this.message.pattern);
1573
+ const selectors = this.message.selectors.map((name) => this.selector(name));
1574
+ return this.expand(selectors, 0, [this.message.variants]);
1575
+ }
1576
+ resolve(expression) {
1577
+ const { operand, fn, at } = expression;
1578
+ if (fn !== null && !KNOWN.has(fn.name)) throw this.fail(fn.name === "offset" || fn.name === "math" ? `:${fn.name} is not supported: pass the adjusted number as a value` : `unknown function :${fn.name}: use :string, :number, :integer, :percent, :currency, :date, :time or :datetime`, at, fn.name === "offset" || fn.name === "math" ? "icu-unsupported-type" : "icu-syntax");
1579
+ let base;
1580
+ if (operand === null) throw this.fail(`{:${fn?.name}} has no operand: write {$name :${fn?.name}}`, at);
1581
+ else if (operand.type === "literal") base = {
1582
+ kind: "text",
1583
+ text: operand.value
1584
+ };
1585
+ else base = this.env.get(operand.name) ?? {
1586
+ kind: "arg",
1587
+ name: operand.name,
1588
+ fn: null
1589
+ };
1590
+ if (fn === null) return base;
1591
+ const name = fn.name;
1592
+ const own = this.options(name, fn.options, at);
1593
+ if (name === "string") {
1594
+ if (base.kind === "arg") return {
1595
+ kind: "arg",
1596
+ name: base.name,
1597
+ fn: annotation(name, own)
1598
+ };
1599
+ return {
1600
+ kind: "text",
1601
+ text: base.text
1602
+ };
1603
+ }
1604
+ if (NUMERIC.has(name)) {
1605
+ const merged = inherit(name, base.kind === "text" ? null : base.fn, own);
1606
+ if (name === "currency" && !merged.options.has("currency")) throw this.fail(":currency needs currency=EUR (the ISO 4217 code)", at);
1607
+ this.style(merged, at);
1608
+ if (base.kind === "text") {
1609
+ if (!NUMBER_LITERAL.test(base.text)) throw this.fail(`${JSON.stringify(base.text)} is not a number for :${name}`, at);
1610
+ return {
1611
+ kind: "number",
1612
+ text: base.text,
1613
+ fn: merged
1614
+ };
1615
+ }
1616
+ if (base.kind === "number") return {
1617
+ kind: "number",
1618
+ text: base.text,
1619
+ fn: merged
1620
+ };
1621
+ return {
1622
+ kind: "arg",
1623
+ name: base.name,
1624
+ fn: merged
1625
+ };
1626
+ }
1627
+ if (base.kind !== "arg") throw this.fail(`formatting a literal date depends on the time zone: pass the date as a value`, at, "icu-unsupported-type");
1628
+ const fnDate = annotation(name, own);
1629
+ this.style(fnDate, at);
1630
+ return {
1631
+ kind: "arg",
1632
+ name: base.name,
1633
+ fn: fnDate
1634
+ };
1635
+ }
1636
+ /** Literal option values; a variable must name a constant declared earlier. */
1637
+ options(fn, options, at) {
1638
+ const out = /* @__PURE__ */ new Map();
1639
+ for (const option of options) {
1640
+ if (option.name === "u:id") continue;
1641
+ if (option.name.startsWith("u:")) throw this.fail(option.name === "u:dir" ? "u:dir is not supported: vi18n isolates string values itself (FSI … PDI)" : `the option ${option.name} is not supported`, at, "icu-unsupported-type");
1642
+ let value;
1643
+ if (option.value.type === "literal") value = option.value.value;
1644
+ else {
1645
+ const found = this.env.get(option.value.name);
1646
+ if (found?.kind !== "text" || option.name === "select") throw this.fail(`the option ${option.name} is set from $${option.value.name}: options must be literals`, at, "icu-unsupported-type");
1647
+ value = found.text;
1648
+ }
1649
+ out.set(option.name, value);
1650
+ }
1651
+ if (fn === "string" && out.size > 0) throw this.fail(`:string takes no options`, at);
1652
+ return out;
1653
+ }
1654
+ pattern(pattern) {
1655
+ const root = [];
1656
+ const stack = [];
1657
+ let nodes = root;
1658
+ for (const part of pattern) if (typeof part === "string") push(nodes, {
1659
+ type: "literal",
1660
+ value: part
1661
+ });
1662
+ else if (part.type === "expression") push(nodes, this.placeholder(this.resolve(part), part.at));
1663
+ else {
1664
+ if (!isTagName(part.name)) throw this.fail(`markup {#${part.name}} has no ICU tag equivalent: tag names start with an ASCII letter and have no namespace`, part.at, "icu-unsupported-type");
1665
+ if (part.options.some((option) => option.name !== "u:id")) throw this.fail(`markup options are not supported: pass what {#${part.name}} needs to its tag handler`, part.at, "icu-unsupported-type");
1666
+ if (part.kind === "standalone") push(nodes, {
1667
+ type: "tag",
1668
+ name: part.name,
1669
+ children: null
1670
+ });
1671
+ else if (part.kind === "open") {
1672
+ stack.push({
1673
+ name: part.name,
1674
+ nodes,
1675
+ at: part.at
1676
+ });
1677
+ nodes = [];
1678
+ } else {
1679
+ const open = stack.pop();
1680
+ if (open === void 0 || open.name !== part.name) throw this.fail(open === void 0 ? `{/${part.name}} closes markup that was never opened` : `{/${part.name}} closes {#${open.name}}: close markup in the order it was opened`, part.at, "icu-unsupported-type");
1681
+ push(open.nodes, {
1682
+ type: "tag",
1683
+ name: part.name,
1684
+ children: nodes
1685
+ });
1686
+ nodes = open.nodes;
1687
+ }
1688
+ }
1689
+ const unclosed = stack.pop();
1690
+ if (unclosed !== void 0) throw this.fail(`{#${unclosed.name}} is never closed: add {/${unclosed.name}} (or write {#${unclosed.name}/})`, unclosed.at, "icu-unsupported-type");
1691
+ return root;
1692
+ }
1693
+ placeholder(value, at) {
1694
+ if (value.kind === "text") return {
1695
+ type: "literal",
1696
+ value: value.text
1697
+ };
1698
+ if (value.kind === "number") throw this.fail(`formatting the literal ${value.text} depends on the locale: pass it as a value, or write it as text`, at, "icu-unsupported-type");
1699
+ const name = this.argumentName(value.name, at);
1700
+ const fn = value.fn;
1701
+ if (fn === null || fn.name === "string") return {
1702
+ type: "argument",
1703
+ name
1704
+ };
1705
+ return {
1706
+ name,
1707
+ ...this.style(fn, at)
1708
+ };
1709
+ }
1710
+ /** A formatting annotation as the ICU node's type, style and options. */
1711
+ style(fn, at) {
1712
+ let type = "number";
1713
+ let style;
1714
+ try {
1715
+ if (NUMERIC.has(fn.name)) style = numberStyle(fn);
1716
+ else [type, style] = dateStyle(fn);
1717
+ } catch (error) {
1718
+ if (error instanceof Unsupported) throw this.fail(error.message, at, error.code);
1719
+ throw error;
1720
+ }
1721
+ const resolved = resolveStyle(type, style);
1722
+ if (!resolved.ok) throw this.fail(resolved.message, at, resolved.code);
1723
+ return {
1724
+ type,
1725
+ style: resolved.style,
1726
+ options: resolved.options
1727
+ };
1728
+ }
1729
+ argumentName(name, at) {
1730
+ if (!ICU_IDENTIFIER.test(name)) throw this.fail(`$${name} cannot be an ICU argument name (no punctuation such as - . +): rename it`, at, "icu-unsupported-type");
1731
+ return name;
1732
+ }
1733
+ selector(name) {
1734
+ const at = this.message.matchAt;
1735
+ const value = this.env.get(name);
1736
+ if (value.kind === "text") return {
1737
+ kind: "constant",
1738
+ text: value.text
1739
+ };
1740
+ const fn = value.fn;
1741
+ if (fn.name === "string") return value.kind === "arg" ? {
1742
+ kind: "string",
1743
+ name: this.argumentName(value.name, at)
1744
+ } : {
1745
+ kind: "constant",
1746
+ text: value.text
1747
+ };
1748
+ if (fn.name === "number" || fn.name === "integer") {
1749
+ if (fn.inheritedSelect) throw this.fail(`$${name} takes select from the value it is declared from: set select on its own annotation`, at);
1750
+ const select = fn.options.get("select") ?? "plural";
1751
+ if (select !== "plural" && select !== "ordinal" && select !== "exact") throw this.fail(`select=${select}: use plural, ordinal or exact`, at);
1752
+ if (value.kind === "number") return {
1753
+ kind: "constant-number",
1754
+ text: value.text,
1755
+ exact: select === "exact"
1756
+ };
1757
+ return {
1758
+ kind: select,
1759
+ name: this.argumentName(value.name, at)
1760
+ };
1761
+ }
1762
+ throw this.fail(fn.name === "percent" ? "selecting on :percent multiplies the value by 100, which vi18n's plural rules cannot: select on the number itself" : `:${fn.name} cannot select: use :number, :integer or :string`, at, fn.name === "percent" ? "icu-unsupported-type" : "icu-syntax");
1763
+ }
1764
+ /**
1765
+ * The message for selectors `i…` when `groups` holds the variants still
1766
+ * possible, best first: each group's variants agree on the keys of the
1767
+ * selectors before `i`, and a group is only reached when the groups before
1768
+ * it have no variant left that matches.
1769
+ */
1770
+ expand(selectors, i, groups) {
1771
+ if (++this.steps > MAX_STEPS) throw this.fail("this .match has too many combinations of keys: split it into several messages", this.message.matchAt, "icu-unsupported-type");
1772
+ const live = groups.filter((group) => group.length > 0);
1773
+ if (i === selectors.length) {
1774
+ const variant = live[0][0];
1775
+ let found = this.lowered.get(variant);
1776
+ if (found === void 0) this.lowered.set(variant, found = this.pattern(variant.pattern));
1777
+ return found;
1778
+ }
1779
+ const selector = selectors[i];
1780
+ const keys = [];
1781
+ for (const group of live) for (const variant of group) {
1782
+ const key = variant.keys[i];
1783
+ if (key !== null && !keys.includes(key)) keys.push(key);
1784
+ }
1785
+ const only = (key) => (group) => group.filter((variant) => variant.keys[i] === key);
1786
+ const next = (...picks) => this.expand(selectors, i + 1, live.flatMap((group) => picks.map((pick) => pick(group))));
1787
+ if (selector.kind === "constant") return next(only(selector.text), only(null));
1788
+ if (selector.kind === "string") {
1789
+ for (const key of keys) if (!ICU_IDENTIFIER.test(key)) throw this.fail(`the key ${JSON.stringify(key)} cannot be an ICU select key (no spaces or punctuation)`, this.message.matchAt, "icu-unsupported-type");
1790
+ const options = keys.map((key) => ({
1791
+ key,
1792
+ value: next(only(key), only(null))
1793
+ }));
1794
+ const fallback = next(only(null));
1795
+ const other = options.find((option) => option.key === "other");
1796
+ if (other === void 0) options.push({
1797
+ key: "other",
1798
+ value: fallback
1799
+ });
1800
+ else if (!same(other.value, fallback)) throw this.fail(`a :string key "other" cannot be told apart from * in vi18n's IR: rename the value or merge the two variants`, this.message.matchAt, "icu-unsupported-type");
1801
+ return this.choice({
1802
+ type: "select",
1803
+ name: selector.name,
1804
+ options
1805
+ });
1806
+ }
1807
+ const exact = [];
1808
+ const categories = [];
1809
+ for (const key of keys) if (NUMBER_LITERAL.test(key)) {
1810
+ if (selector.kind === "constant-number") continue;
1811
+ if (!INTEGER_KEY.test(key) || !Number.isSafeInteger(Number(key))) throw this.fail(`the key ${key} is not an integer: vi18n matches exact keys on integers`, this.message.matchAt, "icu-unsupported-type");
1812
+ exact.push(key);
1813
+ } else if (CATEGORIES.includes(key)) {
1814
+ if (selector.kind !== "exact") categories.push(key);
1815
+ } else throw this.fail(`the key ${JSON.stringify(key)} is neither a number nor a plural category (zero, one, two, few, many, other)`, this.message.matchAt);
1816
+ if (selector.kind === "constant-number") {
1817
+ if (categories.length > 0 && !selector.exact) throw this.fail(`the plural category of the literal ${selector.text} depends on the locale: select on a value`, this.message.matchAt, "icu-unsupported-type");
1818
+ return next(only(String(Number(selector.text))), only(null));
1819
+ }
1820
+ const byCategory = (prefix) => {
1821
+ const first = prefix === null ? [] : [only(prefix)];
1822
+ const options = categories.map((category) => ({
1823
+ key: category,
1824
+ value: next(...first, only(category), only(null))
1825
+ }));
1826
+ const fallback = next(...first, only(null));
1827
+ const other = options.find((option) => option.key === "other");
1828
+ if (other === void 0) options.push({
1829
+ key: "other",
1830
+ value: fallback
1831
+ });
1832
+ else if (!same(other.value, fallback)) {
1833
+ for (const category of CATEGORIES) if (!categories.includes(category)) options.push({
1834
+ key: category,
1835
+ value: fallback
1836
+ });
1837
+ }
1838
+ return options;
1839
+ };
1840
+ const plural = (options) => this.choice({
1841
+ type: "plural",
1842
+ pluralType: selector.kind === "ordinal" ? "ordinal" : "cardinal",
1843
+ name: selector.name,
1844
+ offset: 0,
1845
+ options
1846
+ });
1847
+ const options = exact.map((key) => ({
1848
+ key: "=" + String(Number(key)),
1849
+ value: plural(byCategory(key))
1850
+ }));
1851
+ options.push(...byCategory(null));
1852
+ return plural(options);
1853
+ }
1854
+ /** A select or plural, or its one value when every case renders the same. */
1855
+ choice(node) {
1856
+ const [first, ...rest] = node.options;
1857
+ if (first !== void 0 && rest.every((option) => same(option.value, first.value))) return first.value;
1858
+ return [node];
1859
+ }
1860
+ fail(message, offset, code) {
1861
+ return mf2Error(this.source, message, offset, code);
1862
+ }
1863
+ };
1864
+ function annotation(name, options) {
1865
+ return {
1866
+ name,
1867
+ options,
1868
+ inheritedSelect: false
1869
+ };
1870
+ }
1871
+ /**
1872
+ * MF2 option inheritance: a numeric annotation on a numeric value keeps the
1873
+ * value's options it has itself, its own winning; `:integer` drops fraction
1874
+ * and minimum significant digits, `:percent` integer digits, increment and
1875
+ * `select`.
1876
+ */
1877
+ function inherit(name, prev, own) {
1878
+ if (prev === null || !NUMERIC.has(prev.name)) return annotation(name, own);
1879
+ const known = OPTIONS[name];
1880
+ const dropped = name === "integer" ? [
1881
+ "minimumFractionDigits",
1882
+ "maximumFractionDigits",
1883
+ "minimumSignificantDigits"
1884
+ ] : name === "percent" ? [
1885
+ "minimumIntegerDigits",
1886
+ "roundingIncrement",
1887
+ "select"
1888
+ ] : [];
1889
+ const options = /* @__PURE__ */ new Map();
1890
+ for (const [key, value] of prev.options) if (known.includes(key) && !dropped.includes(key)) options.set(key, value);
1891
+ const inheritedSelect = options.has("select") && !own.has("select");
1892
+ for (const [key, value] of own) options.set(key, value);
1893
+ return {
1894
+ name,
1895
+ options,
1896
+ inheritedSelect
1897
+ };
1898
+ }
1899
+ function push(nodes, node) {
1900
+ if (node.type !== "literal") {
1901
+ nodes.push(node);
1902
+ return;
1903
+ }
1904
+ if (node.value === "") return;
1905
+ const last = nodes[nodes.length - 1];
1906
+ if (last?.type === "literal") nodes[nodes.length - 1] = {
1907
+ type: "literal",
1908
+ value: last.value + node.value
1909
+ };
1910
+ else nodes.push(node);
1911
+ }
1912
+ /** Lowered ASTs are built from plain objects in a fixed key order. */
1913
+ const same = (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b);
1914
+ /** An ASCII letter, then ICU's tag name characters: no namespace. */
1915
+ function isTagName(name) {
1916
+ if (!/^[A-Za-z]/.test(name)) return false;
1917
+ for (const char of name.slice(1)) if (!isTagNameChar(char.codePointAt(0))) return false;
1918
+ return true;
1919
+ }
1920
+ const SIGN = {
1921
+ auto: "sign-auto",
1922
+ always: "sign-always",
1923
+ exceptZero: "sign-except-zero",
1924
+ never: "sign-never"
1925
+ };
1926
+ const ROUNDING = {
1927
+ ceil: "rounding-mode-ceiling",
1928
+ floor: "rounding-mode-floor",
1929
+ expand: "rounding-mode-up",
1930
+ trunc: "rounding-mode-down",
1931
+ halfExpand: "rounding-mode-half-up",
1932
+ halfTrunc: "rounding-mode-half-down",
1933
+ halfEven: "rounding-mode-half-even"
1934
+ };
1935
+ /** FormatJS's `unit-width-*` stems, read back to the `currencyDisplay` they give. */
1936
+ const CURRENCY_DISPLAY = {
1937
+ narrowSymbol: "unit-width-narrow",
1938
+ code: "unit-width-short",
1939
+ name: "unit-width-full-name"
1940
+ };
1941
+ const OPTIONS = {
1942
+ number: [
1943
+ "select",
1944
+ "signDisplay",
1945
+ "useGrouping",
1946
+ "minimumIntegerDigits",
1947
+ "minimumFractionDigits",
1948
+ "maximumFractionDigits",
1949
+ "minimumSignificantDigits",
1950
+ "maximumSignificantDigits",
1951
+ "trailingZeroDisplay",
1952
+ "roundingPriority",
1953
+ "roundingIncrement",
1954
+ "roundingMode"
1955
+ ],
1956
+ integer: [
1957
+ "select",
1958
+ "signDisplay",
1959
+ "useGrouping",
1960
+ "minimumIntegerDigits",
1961
+ "maximumSignificantDigits"
1962
+ ],
1963
+ percent: [
1964
+ "signDisplay",
1965
+ "useGrouping",
1966
+ "minimumFractionDigits",
1967
+ "maximumFractionDigits",
1968
+ "minimumSignificantDigits",
1969
+ "maximumSignificantDigits",
1970
+ "trailingZeroDisplay",
1971
+ "roundingPriority",
1972
+ "roundingMode"
1973
+ ],
1974
+ currency: [
1975
+ "currency",
1976
+ "currencySign",
1977
+ "currencyDisplay",
1978
+ "useGrouping",
1979
+ "minimumIntegerDigits",
1980
+ "fractionDigits",
1981
+ "minimumSignificantDigits",
1982
+ "maximumSignificantDigits",
1983
+ "trailingZeroDisplay",
1984
+ "roundingPriority",
1985
+ "roundingIncrement",
1986
+ "roundingMode"
1987
+ ]
1988
+ };
1989
+ /**
1990
+ * A numeric annotation as an ICU number skeleton (FormatJS semantics, so the
1991
+ * options match what MF2's `Intl.NumberFormat`-named options mean). Keyword
1992
+ * styles where one says the same: `integer`, `percent`.
1993
+ */
1994
+ function numberStyle(fn) {
1995
+ const o = fn.options;
1996
+ for (const key of o.keys()) if (!OPTIONS[fn.name].includes(key)) throw invalid(`:${fn.name} has no option ${key}`);
1997
+ const digits = (key) => {
1998
+ const value = o.get(key);
1999
+ if (value === void 0) return null;
2000
+ if (!DIGIT_SIZE.test(value)) throw invalid(`${key}=${value} is not a digit count`);
2001
+ return Number(value);
2002
+ };
2003
+ const tokens = [];
2004
+ if (fn.name === "integer") tokens.push(".");
2005
+ if (fn.name === "percent") tokens.push("percent");
2006
+ if (fn.name === "currency") tokens.push("currency/" + o.get("currency").toUpperCase());
2007
+ const minInt = digits("minimumIntegerDigits");
2008
+ const minFrac = digits("minimumFractionDigits");
2009
+ const maxFrac = digits("maximumFractionDigits");
2010
+ const minSig = digits("minimumSignificantDigits");
2011
+ const maxSig = digits("maximumSignificantDigits");
2012
+ if (minInt !== null) tokens.push("integer-width/*" + "0".repeat(minInt));
2013
+ let fraction = null;
2014
+ if (fn.name === "currency") {
2015
+ const value = o.get("fractionDigits");
2016
+ if (value !== void 0 && value !== "auto") {
2017
+ if (!DIGIT_SIZE.test(value)) throw invalid(`fractionDigits=${value} is not a digit count`);
2018
+ fraction = "." + "0".repeat(Number(value));
2019
+ }
2020
+ } else if (minFrac !== null || maxFrac !== null) {
2021
+ const min = minFrac ?? 0;
2022
+ if (maxFrac === null) fraction = min === 0 ? null : "." + "0".repeat(min) + "*";
2023
+ else if (maxFrac < min) throw invalid("maximumFractionDigits is below minimumFractionDigits");
2024
+ else fraction = "." + "0".repeat(min) + "#".repeat(maxFrac - min);
2025
+ }
2026
+ const strip = o.get("trailingZeroDisplay");
2027
+ if (strip === "stripIfInteger") {
2028
+ if (fraction === null || fraction.endsWith("*")) throw new Unsupported("trailingZeroDisplay=stripIfInteger needs both fraction digit counts");
2029
+ if (fraction !== ".") fraction += "/w";
2030
+ } else if (strip !== void 0 && strip !== "auto") throw new Unsupported(`trailingZeroDisplay=${strip} is not supported`);
2031
+ if (fraction !== null) tokens.push(fraction);
2032
+ let significant = null;
2033
+ if (minSig !== null || maxSig !== null) {
2034
+ const min = minSig ?? 1;
2035
+ if (min < 1) throw invalid("minimumSignificantDigits must be at least 1");
2036
+ if (maxSig === null) significant = "@".repeat(min) + "+";
2037
+ else if (maxSig < min) throw invalid("maximumSignificantDigits is below minimumSignificantDigits");
2038
+ else significant = "@".repeat(min) + "#".repeat(maxSig - min);
2039
+ }
2040
+ const priority = o.get("roundingPriority");
2041
+ if (priority !== void 0 && priority !== "auto") {
2042
+ if (significant === null || priority !== "morePrecision" && priority !== "lessPrecision") throw new Unsupported(`roundingPriority=${priority} needs significant digits set here`);
2043
+ significant += priority === "morePrecision" ? "r" : "s";
2044
+ }
2045
+ if (significant !== null) tokens.push(significant);
2046
+ const currencySign = o.get("currencySign");
2047
+ if (currencySign === "accounting") tokens.push("sign-accounting");
2048
+ else if (currencySign !== void 0 && currencySign !== "standard") throw new Unsupported(`currencySign=${currencySign} is not supported`);
2049
+ const sign = o.get("signDisplay");
2050
+ if (sign !== void 0 && sign !== "auto") {
2051
+ const stem = Object.hasOwn(SIGN, sign) ? SIGN[sign] : void 0;
2052
+ if (stem === void 0) throw new Unsupported(`signDisplay=${sign} is not supported`);
2053
+ tokens.push(stem);
2054
+ }
2055
+ const grouping = o.get("useGrouping");
2056
+ if (grouping === "never" || grouping === "false") tokens.push("group-off");
2057
+ else if (grouping !== void 0 && grouping !== "auto") throw new Unsupported(`useGrouping=${grouping} is not supported`);
2058
+ const display = o.get("currencyDisplay");
2059
+ if (display !== void 0 && display !== "symbol") {
2060
+ const stem = Object.hasOwn(CURRENCY_DISPLAY, display) ? CURRENCY_DISPLAY[display] : void 0;
2061
+ if (stem === void 0) throw new Unsupported(`currencyDisplay=${display} is not supported`);
2062
+ tokens.push(stem);
2063
+ }
2064
+ const increment = o.get("roundingIncrement");
2065
+ if (increment !== void 0 && increment !== "1") throw new Unsupported(`roundingIncrement=${increment} is not supported`);
2066
+ const mode = o.get("roundingMode");
2067
+ if (mode !== void 0 && mode !== "halfExpand") {
2068
+ const stem = Object.hasOwn(ROUNDING, mode) ? ROUNDING[mode] : void 0;
2069
+ if (stem === void 0) throw new Unsupported(`roundingMode=${mode} is not supported`);
2070
+ tokens.push(stem);
2071
+ }
2072
+ const select = o.get("select");
2073
+ if (select !== void 0 && select !== "plural" && select !== "ordinal" && select !== "exact") throw invalid(`select=${select}: use plural, ordinal or exact`);
2074
+ if (tokens.length === 0) return null;
2075
+ if (tokens.length === 1 && (tokens[0] === "." || tokens[0] === "percent")) return tokens[0] === "." ? "integer" : "percent";
2076
+ return "::" + tokens.join(" ");
2077
+ }
2078
+ const DATE_FIELDS = {
2079
+ weekday: ["E"],
2080
+ "day-weekday": ["d", "E"],
2081
+ "month-day": ["M", "d"],
2082
+ "month-day-weekday": [
2083
+ "M",
2084
+ "d",
2085
+ "E"
2086
+ ],
2087
+ "year-month-day": [
2088
+ "y",
2089
+ "M",
2090
+ "d"
2091
+ ],
2092
+ "year-month-day-weekday": [
2093
+ "y",
2094
+ "M",
2095
+ "d",
2096
+ "E"
2097
+ ]
2098
+ };
2099
+ const MONTH = {
2100
+ short: "M",
2101
+ medium: "MMM",
2102
+ long: "MMMM"
2103
+ };
2104
+ const TIME = {
2105
+ hour: "j",
2106
+ minute: "jm",
2107
+ second: "jms"
2108
+ };
2109
+ const ZONE = {
2110
+ short: "z",
2111
+ long: "zzzz"
2112
+ };
2113
+ /**
2114
+ * `:date`, `:time` and `:datetime` (MF2 semantic skeletons, Draft) as ICU
2115
+ * date skeletons: fields and length pick the fields and the month and
2116
+ * weekday widths, precision the time fields; Intl picks each locale's order.
2117
+ */
2118
+ function dateStyle(fn) {
2119
+ const o = fn.options;
2120
+ const known = fn.name === "date" ? ["fields", "length"] : fn.name === "time" ? ["precision", "timeZoneStyle"] : [
2121
+ "dateFields",
2122
+ "dateLength",
2123
+ "timePrecision",
2124
+ "timeZoneStyle"
2125
+ ];
2126
+ for (const key of o.keys()) if (!known.includes(key)) throw new Unsupported(`:${fn.name} has no option ${key} vi18n supports (Draft options and overrides are not)`);
2127
+ const pick = (table, key, value) => {
2128
+ if (!Object.hasOwn(table, value)) throw invalid(`${key}=${value} is not a value of ${key}`);
2129
+ return table[value];
2130
+ };
2131
+ const date = (fieldsKey, lengthKey) => {
2132
+ const fields = o.get(fieldsKey) ?? "year-month-day";
2133
+ const length = o.get(lengthKey) ?? "medium";
2134
+ const month = pick(MONTH, lengthKey, length);
2135
+ if (!Object.hasOwn(DATE_FIELDS, fields)) throw invalid(`${fieldsKey}=${fields} is not a value of ${fieldsKey}`);
2136
+ return DATE_FIELDS[fields].map((field) => field === "M" ? month : field === "E" ? length === "long" ? "EEEE" : "EEE" : field).join("");
2137
+ };
2138
+ const time = (precisionKey) => {
2139
+ const zone = o.get("timeZoneStyle");
2140
+ return pick(TIME, precisionKey, o.get(precisionKey) ?? "minute") + (zone === void 0 ? "" : pick(ZONE, "timeZoneStyle", zone));
2141
+ };
2142
+ if (fn.name === "date") return ["date", "::" + date("fields", "length")];
2143
+ if (fn.name === "time") return ["time", "::" + time("precision")];
2144
+ return ["date", "::" + date("dateFields", "dateLength") + time("timePrecision")];
2145
+ }
2146
+ //#endregion
2147
+ //#region src/core/mf2/index.ts
2148
+ /**
2149
+ * The message front ends (PLAN §4.2, INTERNALS §2.1.4): ICU MessageFormat 1,
2150
+ * and MessageFormat 2 lowered into the same ICU AST. `parseSource` is what
2151
+ * the codec, the canonical form and `@menapo/vi18n/parse` call.
2152
+ */
2153
+ /**
2154
+ * Parses an MF2 message (simple or complex, normalised by `normalizeSource`)
2155
+ * into the ICU AST.
2156
+ *
2157
+ * @throws {DiagnosticError} `icu-syntax` or `icu-unsupported-type`.
2158
+ */
2159
+ function parseMF2(source) {
2160
+ return lowerMf2(source, parseMf2Syntax(source));
2161
+ }
2162
+ /**
2163
+ * True when the text, after leading whitespace and bidi marks, starts like
2164
+ * an MF2 complex message: `.input`, `.local`, `.match` or `{{`. Such a
2165
+ * message always holds a `{{…}}` pattern, which ICU rejects, so no valid ICU
2166
+ * message reads as MF2.
2167
+ */
2168
+ function isMF2Form(source) {
2169
+ const i = bodyStart(source);
2170
+ return source.startsWith("{{", i) || source.startsWith(".input", i) || source.startsWith(".local", i) || source.startsWith(".match", i);
2171
+ }
2172
+ /** Past MF2's leading `o`: whitespace (with U+3000) and bidi marks. */
2173
+ function bodyStart(source) {
2174
+ let i = 0;
2175
+ for (; i < source.length; i++) {
2176
+ const c = source.charCodeAt(i);
2177
+ if (!(c === 32 || c === 9 || c === 10 || c === 13 || c === 12288) && !(c === 1564 || c === 8206 || c === 8207 || c >= 8294 && c <= 8297)) break;
2178
+ }
2179
+ return i;
2180
+ }
2181
+ /**
2182
+ * Parses a normalised message: as ICU, and as MF2 when the ICU parse fails
2183
+ * and the text has MF2's complex form (or `syntax` says which). A valid ICU
2184
+ * message never changes meaning, and an MF2 message gets MF2's errors.
2185
+ *
2186
+ * @throws {DiagnosticError} `icu-*` from the parser that applies.
2187
+ */
2188
+ function parseSource(source, syntax = "auto") {
2189
+ if (syntax === "mf2") return parseMF2(source);
2190
+ try {
2191
+ return parseICU(source);
2192
+ } catch (error) {
2193
+ if (syntax === "icu" || !isDiagnosticError(error)) throw error;
2194
+ if (isMF2Form(source)) {
2195
+ const message = parseMF2(source);
2196
+ if (!message.every((node) => node.type === "literal") || !source.startsWith("{{", bodyStart(source))) return message;
2197
+ throw hinted(error, "i18next's {{name}} is {name} here, and plain MF2 text needs no {{…}}");
2198
+ }
2199
+ if (!source.includes("{$")) throw error;
2200
+ throw hinted(error, "MessageFormat 2 messages are read as such in their complex form: wrap the pattern in {{…}}");
2201
+ }
2202
+ }
2203
+ function hinted(error, hint) {
2204
+ const { diagnostic } = error;
2205
+ return diagnosticError({
2206
+ ...diagnostic,
2207
+ message: `${diagnostic.message} (${hint})`
2208
+ });
2209
+ }
2210
+ //#endregion
2211
+ //#region src/core/icu/print.ts
2212
+ const HASH = 35;
2213
+ const APOS = 39;
2214
+ const SLASH = 47;
2215
+ const LT = 60;
2216
+ const GT = 62;
2217
+ const LBRACE = 123;
2218
+ const RBRACE = 125;
2219
+ /** "Nothing follows": the end of the whole message. */
2220
+ const END = -1;
2221
+ /** Prints a message in canonical form. Literal text keeps every character. */
2222
+ function printICU(message) {
2223
+ return printNodes(message, false, END);
2224
+ }
2225
+ /**
2226
+ * @param poundSensitive inside any plural/selectordinal option, where `#` must be quoted
2227
+ * @param after first character printed after these nodes: `}` (option), `<` (closing tag) or END
2228
+ */
2229
+ function printNodes(nodes, poundSensitive, after) {
2230
+ let out = "";
2231
+ const count = nodes.length;
2232
+ for (let i = 0; i < count; i++) {
2233
+ const node = nodes[i];
2234
+ if (node.type === "literal") {
2235
+ let value = node.value;
2236
+ while (i + 1 < count && nodes[i + 1].type === "literal") value += nodes[++i].value;
2237
+ const next = i + 1 < count ? firstChar(nodes[i + 1]) : after;
2238
+ out += printLiteral(value, poundSensitive, next);
2239
+ continue;
2240
+ }
2241
+ out += printNode(node, poundSensitive);
2242
+ }
2243
+ return out;
2244
+ }
2245
+ function printNode(node, poundSensitive) {
2246
+ switch (node.type) {
2247
+ case "literal": return printLiteral(node.value, poundSensitive, END);
2248
+ case "argument": return `{${node.name}}`;
2249
+ case "number":
2250
+ case "date":
2251
+ case "time": {
2252
+ const style = node.style;
2253
+ if (style === null) return `{${node.name}, ${node.type}}`;
2254
+ return `{${node.name}, ${node.type}, ${style.kind === "skeleton" ? "::" + style.value : style.value}}`;
2255
+ }
2256
+ case "select": return `{${node.name}, select, ${printOptions(node.options, poundSensitive)}}`;
2257
+ case "plural": {
2258
+ const type = node.pluralType === "cardinal" ? "plural" : "selectordinal";
2259
+ const offset = node.offset === 0 ? "" : `offset:${node.offset} `;
2260
+ return `{${node.name}, ${type}, ${offset}${printOptions(node.options, true)}}`;
2261
+ }
2262
+ case "pound": return "#";
2263
+ case "tag": return node.children === null ? `<${node.name}/>` : `<${node.name}>${printNodes(node.children, poundSensitive, LT)}</${node.name}>`;
2264
+ }
2265
+ }
2266
+ function printOptions(options, poundSensitive) {
2267
+ let out = "";
2268
+ for (let i = 0; i < options.length; i++) {
2269
+ const option = options[i];
2270
+ if (i > 0) out += " ";
2271
+ out += `${option.key} {${printNodes(option.value, poundSensitive, RBRACE)}}`;
2272
+ }
2273
+ return out;
2274
+ }
2275
+ function firstChar(node) {
2276
+ switch (node.type) {
2277
+ case "tag": return LT;
2278
+ case "pound": return HASH;
2279
+ case "literal": return node.value.charCodeAt(0);
2280
+ default: return LBRACE;
2281
+ }
2282
+ }
2283
+ /**
2284
+ * Minimal, deterministic quoting: a run starting at a special character
2285
+ * (`{`, `}`, `#` where pound-sensitive, `<` before a letter or `/`) is wrapped
2286
+ * in apostrophes together with any special characters and apostrophes that
2287
+ * follow it; any other apostrophe is doubled only where a single one would
2288
+ * open a quote.
2289
+ */
2290
+ function printLiteral(value, poundSensitive, after) {
2291
+ if (!needsQuoting(value, poundSensitive)) return value;
2292
+ const length = value.length;
2293
+ let out = "";
2294
+ let run = 0;
2295
+ let i = 0;
2296
+ while (i < length) {
2297
+ const c = value.charCodeAt(i);
2298
+ if (isSpecial(value, i, c, poundSensitive)) {
2299
+ let j = i + 1;
2300
+ while (j < length) {
2301
+ const d = value.charCodeAt(j);
2302
+ if (d !== APOS && !isSpecial(value, j, d, poundSensitive)) break;
2303
+ j++;
2304
+ }
2305
+ out += value.slice(run, i) + "'" + value.slice(i, j).replaceAll("'", "''") + "'";
2306
+ i = j;
2307
+ run = j;
2308
+ continue;
2309
+ }
2310
+ if (c === APOS) {
2311
+ const next = i + 1 < length ? value.charCodeAt(i + 1) : after;
2312
+ if (next === APOS || next === LBRACE || next === RBRACE || next === LT || next === GT || next === HASH && poundSensitive) {
2313
+ out += value.slice(run, i + 1) + "'";
2314
+ run = i + 1;
2315
+ }
2316
+ }
2317
+ i++;
2318
+ }
2319
+ return out + value.slice(run);
2320
+ }
2321
+ function needsQuoting(value, poundSensitive) {
2322
+ for (let i = 0; i < value.length; i++) {
2323
+ const c = value.charCodeAt(i);
2324
+ if (c === APOS || c === LBRACE || c === RBRACE || c === LT || c === HASH && poundSensitive) return true;
2325
+ }
2326
+ return false;
2327
+ }
2328
+ function isSpecial(value, i, c, poundSensitive) {
2329
+ if (c === LBRACE || c === RBRACE) return true;
2330
+ if (c === HASH) return poundSensitive;
2331
+ if (c === LT) {
2332
+ const next = value.charCodeAt(i + 1);
2333
+ return next === SLASH || next >= 65 && next <= 90 || next >= 97 && next <= 122;
2334
+ }
2335
+ return false;
2336
+ }
2337
+ //#endregion
2338
+ //#region src/core/icu/canonical.ts
2339
+ /**
2340
+ * Source normalisation and the canonical form (INTERNALS §2.2.1): the text
2341
+ * that ids hash, catalogs store and translators read.
2342
+ */
2343
+ const CR = /\r\n?/g;
2344
+ /** NFC and LF line ends: what every parser and hash sees. */
2345
+ function normalizeSource(source) {
2346
+ const nfc = source.normalize("NFC");
2347
+ return nfc.includes("\r") ? nfc.replace(CR, "\n") : nfc;
2348
+ }
2349
+ /**
2350
+ * The canonical form of a source string: ICU syntax re-printed, literal text
2351
+ * byte-exact (after normalisation). An MF2 complex message's is the ICU
2352
+ * print of its lowering.
2353
+ *
2354
+ * @throws {DiagnosticError} `icu-*` when the source is not a valid message.
2355
+ */
2356
+ function canonicalize(source) {
2357
+ const normalized = normalizeSource(source);
2358
+ return isPlainText(normalized) ? normalized : printICU(parseSource(normalized));
2359
+ }
2360
+ /**
2361
+ * True when a normalised string has no character the parser or printer treats
2362
+ * specially at the top level (`#` is only special inside plurals).
2363
+ */
2364
+ function isPlainText(text) {
2365
+ for (let i = 0; i < text.length; i++) {
2366
+ const c = text.charCodeAt(i);
2367
+ if (c === 39 || c === 60 || c === 123 || c === 125) return false;
2368
+ }
2369
+ return true;
2370
+ }
2371
+ //#endregion
2372
+ //#region src/core/ir/compile.ts
2373
+ /**
2374
+ * Compiles a parsed ICU message to the JSON IR the runtime interprets
2375
+ * (INTERNALS §2.3, PLAN §5.9).
2376
+ *
2377
+ * A message of plain text stays a string, which is what most messages are and
2378
+ * what the runtime returns without interpretation. Everything else becomes a
2379
+ * flat `Part[]` with adjacent text merged, so the runtime never concatenates
2380
+ * two literals it could have received as one.
2381
+ */
2382
+ function compileMessage(message) {
2383
+ const text = literalText(message);
2384
+ if (text !== null) return text;
2385
+ const parts = [];
2386
+ appendParts(message, parts);
2387
+ return parts;
2388
+ }
2389
+ /** The concatenated text when every node is a literal, otherwise null. */
2390
+ function literalText(nodes) {
2391
+ if (nodes.length === 0) return "";
2392
+ if (nodes.length === 1) {
2393
+ const node = nodes[0];
2394
+ return node.type === "literal" ? node.value : null;
2395
+ }
2396
+ let text = "";
2397
+ for (const node of nodes) {
2398
+ if (node.type !== "literal") return null;
2399
+ text += node.value;
2400
+ }
2401
+ return text;
2402
+ }
2403
+ function appendParts(nodes, out) {
2404
+ for (const node of nodes) {
2405
+ if (node.type !== "literal") {
2406
+ out.push(compileNode(node));
2407
+ continue;
2408
+ }
2409
+ const last = out.length - 1;
2410
+ if (last >= 0 && typeof out[last] === "string") out[last] = out[last] + node.value;
2411
+ else if (node.value !== "") out.push(node.value);
2412
+ }
2413
+ }
2414
+ function compileNode(node) {
2415
+ switch (node.type) {
2416
+ case "argument": return [node.name];
2417
+ case "pound": return 0;
2418
+ case "number":
2419
+ case "date":
2420
+ case "time": {
2421
+ const op = node.type === "number" ? OP.number : node.type === "date" ? OP.date : OP.time;
2422
+ const options = copyOptions(node.options);
2423
+ return options === null ? [node.name, op] : [
2424
+ node.name,
2425
+ op,
2426
+ options
2427
+ ];
2428
+ }
2429
+ case "select": return [
2430
+ node.name,
2431
+ OP.select,
2432
+ compileCases(node.options)
2433
+ ];
2434
+ case "plural": {
2435
+ const op = node.pluralType === "ordinal" ? OP.selectordinal : OP.plural;
2436
+ const cases = compileCases(node.options);
2437
+ return node.offset === 0 ? [
2438
+ node.name,
2439
+ op,
2440
+ cases
2441
+ ] : [
2442
+ node.name,
2443
+ op,
2444
+ cases,
2445
+ node.offset
2446
+ ];
2447
+ }
2448
+ case "tag": {
2449
+ if (node.children === null) return [node.name, OP.tagStandalone];
2450
+ const part = [node.name, OP.tag];
2451
+ appendTagChildren(node.children, part);
2452
+ return part;
2453
+ }
2454
+ }
2455
+ }
2456
+ /** Tag children are spread into the tag part after its name and opcode. */
2457
+ function appendTagChildren(children, part) {
2458
+ for (const child of children) {
2459
+ if (child.type !== "literal") {
2460
+ part.push(compileNode(child));
2461
+ continue;
2462
+ }
2463
+ const last = part.length - 1;
2464
+ if (last >= 2 && typeof part[last] === "string") part[last] = part[last] + child.value;
2465
+ else if (child.value !== "") part.push(child.value);
2466
+ }
2467
+ }
2468
+ /**
2469
+ * Keys are added in option order, but a plain object lists integer-like keys
2470
+ * (`"1"`, not `"01"`) first in ascending order (INTERNALS §2.3). Lookup goes
2471
+ * through `Object.hasOwn`, so only the serialized order is affected.
2472
+ */
2473
+ function compileCases(options) {
2474
+ const cases = {};
2475
+ for (const option of options) {
2476
+ const value = compileMessage(option.value);
2477
+ if (option.key === "__proto__") Object.defineProperty(cases, option.key, {
2478
+ value,
2479
+ enumerable: true,
2480
+ writable: true,
2481
+ configurable: true
2482
+ });
2483
+ else cases[option.key] = value;
2484
+ }
2485
+ return cases;
2486
+ }
2487
+ /**
2488
+ * The IR gets its own options object: the codec memoises ASTs, and the runtime
2489
+ * keys formatter caches on the options object's identity per message.
2490
+ */
2491
+ function copyOptions(options) {
2492
+ if (options === null) return null;
2493
+ let copy = null;
2494
+ for (const key in options) {
2495
+ if (!Object.hasOwn(options, key)) continue;
2496
+ copy ??= {};
2497
+ copy[key] = options[key];
2498
+ }
2499
+ return copy;
2500
+ }
2501
+ //#endregion
2502
+ export { printICU as a, parseICU as c, diagnosticError as d, isDiagnosticError as f, normalizeSource as i, positionAt as l, canonicalize as n, isMF2Form as o, isPlainText as r, parseSource as s, compileMessage as t, quoteStarts as u };