@menapo/vi18n 0.0.0-stage → 0.1.0

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