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