@saykit/config 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/index.cjs +1 -1
- package/dist/commands/index.mjs +1 -1
- package/dist/features/loader/index.cjs +2 -1
- package/dist/features/loader/index.d.cts +7 -1
- package/dist/features/loader/index.d.mts +7 -1
- package/dist/features/loader/index.mjs +2 -2
- package/dist/features/messages/index.cjs +152 -17
- package/dist/features/messages/index.d.cts +95 -6
- package/dist/features/messages/index.d.mts +95 -6
- package/dist/features/messages/index.mjs +148 -18
- package/dist/{loader-A1uS7qkK.cjs → loader-BdCeoj3-.cjs} +18 -3
- package/dist/{loader-DLyuZzF2.mjs → loader-DwF1lM3f.mjs} +13 -4
- package/package.json +1 -1
package/dist/commands/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const require_storage = require("../storage-DqhzA5H8.cjs");
|
|
3
|
-
const require_loader = require("../loader-
|
|
3
|
+
const require_loader = require("../loader-BdCeoj3-.cjs");
|
|
4
4
|
let _commander_js_extra_typings = require("@commander-js/extra-typings");
|
|
5
5
|
let node_fs_promises = require("node:fs/promises");
|
|
6
6
|
let node_path = require("node:path");
|
package/dist/commands/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as mergeExtractedMessages, i as expandBucketOutputPath, n as writeCatalogueMessages, o as pruneLocaleMessages, t as readCatalogueMessages } from "../storage-B6mn0s3V.mjs";
|
|
3
|
-
import { t as resolveConfig } from "../loader-
|
|
3
|
+
import { t as resolveConfig } from "../loader-DwF1lM3f.mjs";
|
|
4
4
|
import { Command, program } from "@commander-js/extra-typings";
|
|
5
5
|
import { access, glob, readFile, stat, watch } from "node:fs/promises";
|
|
6
6
|
import { join, relative } from "node:path";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_loader = require("../../loader-
|
|
2
|
+
const require_loader = require("../../loader-BdCeoj3-.cjs");
|
|
3
3
|
exports.resolveConfig = require_loader.resolveConfig;
|
|
4
|
+
exports.resolveConfigFile = require_loader.resolveConfigFile;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { n as Config } from "../../shapes-CRlXtss0.cjs";
|
|
2
2
|
//#region src/features/loader/resolve.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The config file {@link resolveConfig} would load, for callers that need the
|
|
5
|
+
* path itself rather than its contents — salting a bundler's cache key with it,
|
|
6
|
+
* for one, since what a catalogue assembles into depends on the config.
|
|
7
|
+
*/
|
|
8
|
+
declare function resolveConfigFile(name?: string): string;
|
|
3
9
|
declare function resolveConfig(name?: string): Config;
|
|
4
10
|
//#endregion
|
|
5
|
-
export { resolveConfig };
|
|
11
|
+
export { resolveConfig, resolveConfigFile };
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { n as Config } from "../../shapes-CRlXtss0.mjs";
|
|
2
2
|
//#region src/features/loader/resolve.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The config file {@link resolveConfig} would load, for callers that need the
|
|
5
|
+
* path itself rather than its contents — salting a bundler's cache key with it,
|
|
6
|
+
* for one, since what a catalogue assembles into depends on the config.
|
|
7
|
+
*/
|
|
8
|
+
declare function resolveConfigFile(name?: string): string;
|
|
3
9
|
declare function resolveConfig(name?: string): Config;
|
|
4
10
|
//#endregion
|
|
5
|
-
export { resolveConfig };
|
|
11
|
+
export { resolveConfig, resolveConfigFile };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as resolveConfig } from "../../loader-
|
|
2
|
-
export { resolveConfig };
|
|
1
|
+
import { n as resolveConfigFile, t as resolveConfig } from "../../loader-DwF1lM3f.mjs";
|
|
2
|
+
export { resolveConfig, resolveConfigFile };
|
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_hash = require("../../hash-51ce8YiG.cjs");
|
|
3
|
+
//#region src/features/messages/escape.ts
|
|
4
|
+
/**
|
|
5
|
+
* Quoting for text that has to survive a message format unchanged.
|
|
6
|
+
*
|
|
7
|
+
* This lives beside the converter rather than in a transform because it belongs
|
|
8
|
+
* to the format being written, not to the syntax being read. A literal is the
|
|
9
|
+
* same literal whether it came from a template literal or from JSX, and the
|
|
10
|
+
* moment there is a second target format its reserved characters — and the way
|
|
11
|
+
* it spells an escape — are its own.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Escape literal text so ICU MessageFormat 1 reads it as the text it is.
|
|
15
|
+
*
|
|
16
|
+
* MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
|
|
17
|
+
* doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
|
|
18
|
+
* only opens a quoted run when a character that needs quoting follows it —
|
|
19
|
+
* anything else and the apostrophe is itself literal. So `'#'` is a real escape
|
|
20
|
+
* inside a plural and the three characters `'#'` anywhere else, which is why
|
|
21
|
+
* `hash` has to be told rather than assumed.
|
|
22
|
+
*
|
|
23
|
+
* @param text Literal text as the author wrote it
|
|
24
|
+
* @param hash Whether `#` stands for a number here, i.e. whether some `plural`
|
|
25
|
+
* or `ordinal` encloses this text
|
|
26
|
+
*/
|
|
27
|
+
function escapeIcuLiteral(text, hash = false) {
|
|
28
|
+
return text.replace(/'/gu, "''").replace(hash ? /[{}#]+/gu : /[{}]+/gu, (run) => `'${run}'`);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
3
31
|
//#region src/features/messages/types.ts
|
|
4
32
|
var Base = class {
|
|
5
33
|
toICUString() {
|
|
@@ -20,10 +48,12 @@ var LiteralMessage = class extends Base {
|
|
|
20
48
|
var ArgumentMessage = class extends Base {
|
|
21
49
|
identifier;
|
|
22
50
|
expression;
|
|
23
|
-
|
|
51
|
+
format;
|
|
52
|
+
constructor(identifier, expression, format) {
|
|
24
53
|
super();
|
|
25
54
|
this.identifier = identifier;
|
|
26
55
|
this.expression = expression;
|
|
56
|
+
this.format = format;
|
|
27
57
|
}
|
|
28
58
|
};
|
|
29
59
|
var ElementMessage = class extends Base {
|
|
@@ -42,12 +72,14 @@ var ChoiceMessage = class extends Base {
|
|
|
42
72
|
identifier;
|
|
43
73
|
branches;
|
|
44
74
|
expression;
|
|
45
|
-
|
|
75
|
+
offset;
|
|
76
|
+
constructor(kind, identifier, branches, expression, offset) {
|
|
46
77
|
super();
|
|
47
78
|
this.kind = kind;
|
|
48
79
|
this.identifier = identifier;
|
|
49
80
|
this.branches = branches;
|
|
50
81
|
this.expression = expression;
|
|
82
|
+
this.offset = offset;
|
|
51
83
|
}
|
|
52
84
|
};
|
|
53
85
|
var CompositeMessage = class extends Base {
|
|
@@ -76,8 +108,13 @@ const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
|
|
|
76
108
|
*/
|
|
77
109
|
const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
|
|
78
110
|
/**
|
|
79
|
-
* The ICU case a branch is written as.
|
|
80
|
-
*
|
|
111
|
+
* The ICU case a branch is written as.
|
|
112
|
+
*
|
|
113
|
+
* Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
|
|
114
|
+
* so is distinct from the CLDR category that would otherwise match it. `select`
|
|
115
|
+
* has no such syntax — its cases are literal string matches, and `=0` there is
|
|
116
|
+
* a parse error — so a numeric key stays bare, where it matches both `0` and
|
|
117
|
+
* `'0'`.
|
|
81
118
|
*
|
|
82
119
|
* Digits are read literally rather than coerced, because everything JavaScript
|
|
83
120
|
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
@@ -85,8 +122,9 @@ const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$
|
|
|
85
122
|
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
86
123
|
* whitespace or punctuation that made it numeric-looking is caught.
|
|
87
124
|
*/
|
|
88
|
-
function getBranchCase(identifier) {
|
|
125
|
+
function getBranchCase(kind, identifier) {
|
|
89
126
|
const key = String(identifier);
|
|
127
|
+
if (kind === "select") return key;
|
|
90
128
|
return /^\d+$/u.test(key) ? `=${+key}` : key;
|
|
91
129
|
}
|
|
92
130
|
/**
|
|
@@ -100,12 +138,12 @@ function getBranchCase(identifier) {
|
|
|
100
138
|
*/
|
|
101
139
|
function validateBranchIdentifier(kind, identifier) {
|
|
102
140
|
if (typeof identifier !== "string") return;
|
|
103
|
-
const branch = getBranchCase(identifier);
|
|
141
|
+
const branch = getBranchCase(kind, identifier);
|
|
104
142
|
if (!BRANCH_PATTERN.test(branch)) {
|
|
105
143
|
const suggestion = suggestBranchIdentifier(identifier);
|
|
106
144
|
throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
|
|
107
145
|
}
|
|
108
|
-
if (kind === "select" &&
|
|
146
|
+
if (kind === "select" && /^=\d+$/u.test(branch)) throw new Error(`Invalid select branch key '${identifier}', an exact value is only meaningful to 'plural' and 'ordinal', write it as '${branch.slice(1)}'`);
|
|
109
147
|
}
|
|
110
148
|
/**
|
|
111
149
|
* The nearest identifier-safe form of a key, so the error names the fix as well
|
|
@@ -125,9 +163,28 @@ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalen
|
|
|
125
163
|
while (reserved.has(identifier)) identifier = `${sequence.current++}`;
|
|
126
164
|
return identifier;
|
|
127
165
|
}
|
|
166
|
+
const numbered = {
|
|
167
|
+
tags: [],
|
|
168
|
+
values: []
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* The number this expression already has, or a fresh one. An anonymous
|
|
172
|
+
* placeholder is named by its position, so the same value written twice would
|
|
173
|
+
* otherwise arrive as two names — `{0} x {1}` for one length, which asks a
|
|
174
|
+
* translator to keep two holes in step and the caller to supply one value
|
|
175
|
+
* under two props. This is the rule explicit names already follow, where a
|
|
176
|
+
* repeat is allowed precisely when nothing distinguishes it.
|
|
177
|
+
*/
|
|
178
|
+
function number(assigned, expression) {
|
|
179
|
+
const seen = assigned.find(([e]) => equivalent(e, expression));
|
|
180
|
+
if (seen) return seen[1];
|
|
181
|
+
const identifier = next();
|
|
182
|
+
assigned.push([expression, identifier]);
|
|
183
|
+
return identifier;
|
|
184
|
+
}
|
|
128
185
|
function walk(message) {
|
|
129
186
|
if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
|
|
130
|
-
if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier =
|
|
187
|
+
if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = number(message instanceof ElementMessage ? numbered.tags : numbered.values, message.expression);
|
|
131
188
|
}
|
|
132
189
|
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
133
190
|
if (message instanceof ChoiceMessage) for (const branch of message.branches) {
|
|
@@ -167,30 +224,105 @@ function collectAssignedIdentifiers(message, equivalent) {
|
|
|
167
224
|
//#endregion
|
|
168
225
|
//#region src/features/messages/convert.ts
|
|
169
226
|
function convertMessageToIcu(message) {
|
|
170
|
-
function internalConvertMessageToIcu(message) {
|
|
227
|
+
function internalConvertMessageToIcu(message, hash) {
|
|
171
228
|
switch (true) {
|
|
172
|
-
case message instanceof LiteralMessage: return String(message.text);
|
|
173
|
-
case message instanceof ArgumentMessage:
|
|
229
|
+
case message instanceof LiteralMessage: return escapeIcuLiteral(String(message.text), hash !== void 0);
|
|
230
|
+
case message instanceof ArgumentMessage: {
|
|
231
|
+
if (!message.format && hash !== void 0 && String(message.identifier) === hash) return "#";
|
|
232
|
+
const parts = [String(message.identifier)];
|
|
233
|
+
if (message.format) parts.push(message.format.type);
|
|
234
|
+
if (message.format?.style) parts.push(message.format.style);
|
|
235
|
+
return `{${parts.join(", ")}}`;
|
|
236
|
+
}
|
|
174
237
|
case message instanceof ElementMessage: {
|
|
175
238
|
if (message.children.length === 0) return `<${String(message.identifier)}/>`;
|
|
176
|
-
const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
|
|
239
|
+
const children = message.children.map((m) => internalConvertMessageToIcu(m, hash)).join("");
|
|
177
240
|
return `<${String(message.identifier)}>${children}</${String(message.identifier)}>`;
|
|
178
241
|
}
|
|
179
242
|
case message instanceof ChoiceMessage: {
|
|
243
|
+
const scope = message.kind === "select" ? hash : String(message.identifier);
|
|
180
244
|
const branches = message.branches.map(({ identifier, value }) => ({
|
|
181
|
-
identifier: getBranchCase(identifier),
|
|
182
|
-
value: internalConvertMessageToIcu(value)
|
|
245
|
+
identifier: getBranchCase(message.kind, identifier),
|
|
246
|
+
value: internalConvertMessageToIcu(value, scope)
|
|
183
247
|
})).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
|
|
184
248
|
const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
|
|
185
|
-
|
|
249
|
+
const offset = message.offset === void 0 || message.kind === "select" ? "" : ` offset:${message.offset}`;
|
|
250
|
+
return `{${String(message.identifier)}, ${format},${offset}\n${branches}}`;
|
|
186
251
|
}
|
|
187
|
-
case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m)).join("");
|
|
252
|
+
case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m, hash)).join("");
|
|
188
253
|
default: throw new Error("Unknown message type", { cause: message });
|
|
189
254
|
}
|
|
190
255
|
}
|
|
191
|
-
return internalConvertMessageToIcu(message).trim();
|
|
256
|
+
return internalConvertMessageToIcu(message, void 0).trim();
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/features/messages/format.ts
|
|
260
|
+
/**
|
|
261
|
+
* The ICU argument types a macro can author, and the named styles each accepts.
|
|
262
|
+
*
|
|
263
|
+
* `currency` is deliberately absent from `number`. MF1 has nowhere to write the
|
|
264
|
+
* currency code — it comes from the formatter's configuration, not the message
|
|
265
|
+
* — so `{price, number, currency}` formats as a literal `{$price}` at runtime
|
|
266
|
+
* rather than an amount. Currency belongs to number skeletons, which the
|
|
267
|
+
* formatter does not accept yet either.
|
|
268
|
+
*
|
|
269
|
+
* `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
|
|
270
|
+
* oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
|
|
271
|
+
* equivalent, and the third is deprecated in ICU itself in favour of `plural`.
|
|
272
|
+
*/
|
|
273
|
+
const ARGUMENT_STYLES = {
|
|
274
|
+
number: ["integer", "percent"],
|
|
275
|
+
date: [
|
|
276
|
+
"short",
|
|
277
|
+
"medium",
|
|
278
|
+
"long",
|
|
279
|
+
"full"
|
|
280
|
+
],
|
|
281
|
+
time: [
|
|
282
|
+
"short",
|
|
283
|
+
"medium",
|
|
284
|
+
"long",
|
|
285
|
+
"full"
|
|
286
|
+
]
|
|
287
|
+
};
|
|
288
|
+
const ARGUMENT_TYPES = Object.keys(ARGUMENT_STYLES);
|
|
289
|
+
function isArgumentType(kind) {
|
|
290
|
+
return Object.hasOwn(ARGUMENT_STYLES, kind);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* A literal `NumberFormat` pattern, e.g. `#,##0.00`.
|
|
294
|
+
*
|
|
295
|
+
* A pattern has to carry a digit placeholder — `#` or `0` — because that is
|
|
296
|
+
* what makes it a pattern rather than a word. Without that requirement any
|
|
297
|
+
* brace-free string qualifies, which quietly readmits the named styles this
|
|
298
|
+
* module exists to reject: `currency` would sail through as a "pattern" and
|
|
299
|
+
* extract to the `{price, number, currency}` the formatter cannot honour, and
|
|
300
|
+
* so would a plain typo.
|
|
301
|
+
*
|
|
302
|
+
* Braces are excluded separately: ICU reserves them for its own pattern syntax,
|
|
303
|
+
* so a style carrying one would close the argument early and take the rest of
|
|
304
|
+
* the message with it.
|
|
305
|
+
*/
|
|
306
|
+
const LITERAL_STYLE_PATTERN = /^[^{}\r\n]*[#0][^{}\r\n]*$/;
|
|
307
|
+
/**
|
|
308
|
+
* Reject an argument style the formatter cannot honour, while the style is
|
|
309
|
+
* still attached to a file and a line.
|
|
310
|
+
*
|
|
311
|
+
* A style is a bare string in the source and a bare string in the catalogue, so
|
|
312
|
+
* nothing between here and the runtime has an opinion about it. Left unchecked,
|
|
313
|
+
* a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
|
|
314
|
+
* perfectly normal and only misformats once it reaches a user.
|
|
315
|
+
*/
|
|
316
|
+
function validateArgumentStyle(type, style) {
|
|
317
|
+
const named = ARGUMENT_STYLES[type];
|
|
318
|
+
if (named.includes(style)) return;
|
|
319
|
+
if (type === "number" && LITERAL_STYLE_PATTERN.test(style)) return;
|
|
320
|
+
const expected = named.map((s) => `'${s}'`).join(", ");
|
|
321
|
+
throw new Error(`Invalid ${type} style '${style}', expected ${expected}` + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
|
|
192
322
|
}
|
|
193
323
|
//#endregion
|
|
324
|
+
exports.ARGUMENT_STYLES = ARGUMENT_STYLES;
|
|
325
|
+
exports.ARGUMENT_TYPES = ARGUMENT_TYPES;
|
|
194
326
|
exports.AUTO_INCREMENT_IDENTIFIER = AUTO_INCREMENT_IDENTIFIER;
|
|
195
327
|
exports.ArgumentMessage = ArgumentMessage;
|
|
196
328
|
exports.ChoiceMessage = ChoiceMessage;
|
|
@@ -199,6 +331,9 @@ exports.ElementMessage = ElementMessage;
|
|
|
199
331
|
exports.LiteralMessage = LiteralMessage;
|
|
200
332
|
exports.assignSequenceIdentifiers = assignSequenceIdentifiers;
|
|
201
333
|
exports.convertMessageToIcu = convertMessageToIcu;
|
|
334
|
+
exports.escapeIcuLiteral = escapeIcuLiteral;
|
|
202
335
|
exports.generateHash = require_hash.generateHash;
|
|
203
336
|
exports.getBranchCase = getBranchCase;
|
|
337
|
+
exports.isArgumentType = isArgumentType;
|
|
338
|
+
exports.validateArgumentStyle = validateArgumentStyle;
|
|
204
339
|
exports.validateBranchIdentifier = validateBranchIdentifier;
|
|
@@ -1,8 +1,46 @@
|
|
|
1
|
+
//#region src/features/messages/format.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The ICU argument types a macro can author, and the named styles each accepts.
|
|
4
|
+
*
|
|
5
|
+
* `currency` is deliberately absent from `number`. MF1 has nowhere to write the
|
|
6
|
+
* currency code — it comes from the formatter's configuration, not the message
|
|
7
|
+
* — so `{price, number, currency}` formats as a literal `{$price}` at runtime
|
|
8
|
+
* rather than an amount. Currency belongs to number skeletons, which the
|
|
9
|
+
* formatter does not accept yet either.
|
|
10
|
+
*
|
|
11
|
+
* `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
|
|
12
|
+
* oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
|
|
13
|
+
* equivalent, and the third is deprecated in ICU itself in favour of `plural`.
|
|
14
|
+
*/
|
|
15
|
+
declare const ARGUMENT_STYLES: {
|
|
16
|
+
readonly number: readonly ["integer", "percent"];
|
|
17
|
+
readonly date: readonly ["short", "medium", "long", "full"];
|
|
18
|
+
readonly time: readonly ["short", "medium", "long", "full"];
|
|
19
|
+
};
|
|
20
|
+
type ArgumentType = keyof typeof ARGUMENT_STYLES;
|
|
21
|
+
declare const ARGUMENT_TYPES: ArgumentType[];
|
|
22
|
+
declare function isArgumentType(kind: string): kind is ArgumentType;
|
|
23
|
+
/**
|
|
24
|
+
* Reject an argument style the formatter cannot honour, while the style is
|
|
25
|
+
* still attached to a file and a line.
|
|
26
|
+
*
|
|
27
|
+
* A style is a bare string in the source and a bare string in the catalogue, so
|
|
28
|
+
* nothing between here and the runtime has an opinion about it. Left unchecked,
|
|
29
|
+
* a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
|
|
30
|
+
* perfectly normal and only misformats once it reaches a user.
|
|
31
|
+
*/
|
|
32
|
+
declare function validateArgumentStyle(type: ArgumentType, style: string): void;
|
|
33
|
+
//#endregion
|
|
1
34
|
//#region src/features/messages/identifier.d.ts
|
|
2
35
|
declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
|
|
3
36
|
/**
|
|
4
|
-
* The ICU case a branch is written as.
|
|
5
|
-
*
|
|
37
|
+
* The ICU case a branch is written as.
|
|
38
|
+
*
|
|
39
|
+
* Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
|
|
40
|
+
* so is distinct from the CLDR category that would otherwise match it. `select`
|
|
41
|
+
* has no such syntax — its cases are literal string matches, and `=0` there is
|
|
42
|
+
* a parse error — so a numeric key stays bare, where it matches both `0` and
|
|
43
|
+
* `'0'`.
|
|
6
44
|
*
|
|
7
45
|
* Digits are read literally rather than coerced, because everything JavaScript
|
|
8
46
|
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
@@ -10,7 +48,7 @@ declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
|
|
|
10
48
|
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
11
49
|
* whitespace or punctuation that made it numeric-looking is caught.
|
|
12
50
|
*/
|
|
13
|
-
declare function getBranchCase(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
|
|
51
|
+
declare function getBranchCase(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
|
|
14
52
|
/**
|
|
15
53
|
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
16
54
|
* file and a line.
|
|
@@ -40,10 +78,23 @@ declare class LiteralMessage extends Base {
|
|
|
40
78
|
readonly text: string;
|
|
41
79
|
constructor(text: string);
|
|
42
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* An ICU argument type and style, e.g. `{n, number, percent}`. A style is
|
|
83
|
+
* optional — `{n, number}` is the type's default formatting.
|
|
84
|
+
*
|
|
85
|
+
* Both are kept as the ICU strings they are written as, rather than as `Intl`
|
|
86
|
+
* options, because the catalogue is the source of truth and only the ICU
|
|
87
|
+
* spelling round-trips back out of it.
|
|
88
|
+
*/
|
|
89
|
+
interface ArgumentFormat {
|
|
90
|
+
type: ArgumentType;
|
|
91
|
+
style?: string;
|
|
92
|
+
}
|
|
43
93
|
declare class ArgumentMessage extends Base {
|
|
44
94
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
45
95
|
readonly expression: any;
|
|
46
|
-
|
|
96
|
+
readonly format?: ArgumentFormat | undefined;
|
|
97
|
+
constructor(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, expression: any, format?: ArgumentFormat | undefined);
|
|
47
98
|
}
|
|
48
99
|
declare class ElementMessage extends Base {
|
|
49
100
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
@@ -59,10 +110,22 @@ declare class ChoiceMessage extends Base {
|
|
|
59
110
|
readonly value: Message;
|
|
60
111
|
}[];
|
|
61
112
|
readonly expression: any;
|
|
113
|
+
/**
|
|
114
|
+
* Subtracted from the selector before `#` is formatted, so "You and 2
|
|
115
|
+
* others" can select on a total of three. Only `plural` and `ordinal`
|
|
116
|
+
* accept one; `select` has no number to offset.
|
|
117
|
+
*/
|
|
118
|
+
readonly offset?: number | undefined;
|
|
62
119
|
constructor(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, branches: {
|
|
63
120
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
64
121
|
readonly value: Message;
|
|
65
|
-
}[], expression: any
|
|
122
|
+
}[], expression: any,
|
|
123
|
+
/**
|
|
124
|
+
* Subtracted from the selector before `#` is formatted, so "You and 2
|
|
125
|
+
* others" can select on a total of three. Only `plural` and `ordinal`
|
|
126
|
+
* accept one; `select` has no number to offset.
|
|
127
|
+
*/
|
|
128
|
+
offset?: number | undefined);
|
|
66
129
|
}
|
|
67
130
|
declare class CompositeMessage extends Base {
|
|
68
131
|
readonly descriptor: {
|
|
@@ -84,7 +147,33 @@ type Message = LiteralMessage | ArgumentMessage | ElementMessage | ChoiceMessage
|
|
|
84
147
|
//#region src/features/messages/convert.d.ts
|
|
85
148
|
declare function convertMessageToIcu(message: Message): string;
|
|
86
149
|
//#endregion
|
|
150
|
+
//#region src/features/messages/escape.d.ts
|
|
151
|
+
/**
|
|
152
|
+
* Quoting for text that has to survive a message format unchanged.
|
|
153
|
+
*
|
|
154
|
+
* This lives beside the converter rather than in a transform because it belongs
|
|
155
|
+
* to the format being written, not to the syntax being read. A literal is the
|
|
156
|
+
* same literal whether it came from a template literal or from JSX, and the
|
|
157
|
+
* moment there is a second target format its reserved characters — and the way
|
|
158
|
+
* it spells an escape — are its own.
|
|
159
|
+
*/
|
|
160
|
+
/**
|
|
161
|
+
* Escape literal text so ICU MessageFormat 1 reads it as the text it is.
|
|
162
|
+
*
|
|
163
|
+
* MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
|
|
164
|
+
* doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
|
|
165
|
+
* only opens a quoted run when a character that needs quoting follows it —
|
|
166
|
+
* anything else and the apostrophe is itself literal. So `'#'` is a real escape
|
|
167
|
+
* inside a plural and the three characters `'#'` anywhere else, which is why
|
|
168
|
+
* `hash` has to be told rather than assumed.
|
|
169
|
+
*
|
|
170
|
+
* @param text Literal text as the author wrote it
|
|
171
|
+
* @param hash Whether `#` stands for a number here, i.e. whether some `plural`
|
|
172
|
+
* or `ordinal` encloses this text
|
|
173
|
+
*/
|
|
174
|
+
declare function escapeIcuLiteral(text: string, hash?: boolean): string;
|
|
175
|
+
//#endregion
|
|
87
176
|
//#region src/features/messages/hash.d.ts
|
|
88
177
|
declare function generateHash(input: string, context?: string): string;
|
|
89
178
|
//#endregion
|
|
90
|
-
export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
|
|
179
|
+
export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
|
|
@@ -1,8 +1,46 @@
|
|
|
1
|
+
//#region src/features/messages/format.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The ICU argument types a macro can author, and the named styles each accepts.
|
|
4
|
+
*
|
|
5
|
+
* `currency` is deliberately absent from `number`. MF1 has nowhere to write the
|
|
6
|
+
* currency code — it comes from the formatter's configuration, not the message
|
|
7
|
+
* — so `{price, number, currency}` formats as a literal `{$price}` at runtime
|
|
8
|
+
* rather than an amount. Currency belongs to number skeletons, which the
|
|
9
|
+
* formatter does not accept yet either.
|
|
10
|
+
*
|
|
11
|
+
* `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
|
|
12
|
+
* oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
|
|
13
|
+
* equivalent, and the third is deprecated in ICU itself in favour of `plural`.
|
|
14
|
+
*/
|
|
15
|
+
declare const ARGUMENT_STYLES: {
|
|
16
|
+
readonly number: readonly ["integer", "percent"];
|
|
17
|
+
readonly date: readonly ["short", "medium", "long", "full"];
|
|
18
|
+
readonly time: readonly ["short", "medium", "long", "full"];
|
|
19
|
+
};
|
|
20
|
+
type ArgumentType = keyof typeof ARGUMENT_STYLES;
|
|
21
|
+
declare const ARGUMENT_TYPES: ArgumentType[];
|
|
22
|
+
declare function isArgumentType(kind: string): kind is ArgumentType;
|
|
23
|
+
/**
|
|
24
|
+
* Reject an argument style the formatter cannot honour, while the style is
|
|
25
|
+
* still attached to a file and a line.
|
|
26
|
+
*
|
|
27
|
+
* A style is a bare string in the source and a bare string in the catalogue, so
|
|
28
|
+
* nothing between here and the runtime has an opinion about it. Left unchecked,
|
|
29
|
+
* a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
|
|
30
|
+
* perfectly normal and only misformats once it reaches a user.
|
|
31
|
+
*/
|
|
32
|
+
declare function validateArgumentStyle(type: ArgumentType, style: string): void;
|
|
33
|
+
//#endregion
|
|
1
34
|
//#region src/features/messages/identifier.d.ts
|
|
2
35
|
declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
|
|
3
36
|
/**
|
|
4
|
-
* The ICU case a branch is written as.
|
|
5
|
-
*
|
|
37
|
+
* The ICU case a branch is written as.
|
|
38
|
+
*
|
|
39
|
+
* Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
|
|
40
|
+
* so is distinct from the CLDR category that would otherwise match it. `select`
|
|
41
|
+
* has no such syntax — its cases are literal string matches, and `=0` there is
|
|
42
|
+
* a parse error — so a numeric key stays bare, where it matches both `0` and
|
|
43
|
+
* `'0'`.
|
|
6
44
|
*
|
|
7
45
|
* Digits are read literally rather than coerced, because everything JavaScript
|
|
8
46
|
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
@@ -10,7 +48,7 @@ declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
|
|
|
10
48
|
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
11
49
|
* whitespace or punctuation that made it numeric-looking is caught.
|
|
12
50
|
*/
|
|
13
|
-
declare function getBranchCase(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
|
|
51
|
+
declare function getBranchCase(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
|
|
14
52
|
/**
|
|
15
53
|
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
16
54
|
* file and a line.
|
|
@@ -40,10 +78,23 @@ declare class LiteralMessage extends Base {
|
|
|
40
78
|
readonly text: string;
|
|
41
79
|
constructor(text: string);
|
|
42
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* An ICU argument type and style, e.g. `{n, number, percent}`. A style is
|
|
83
|
+
* optional — `{n, number}` is the type's default formatting.
|
|
84
|
+
*
|
|
85
|
+
* Both are kept as the ICU strings they are written as, rather than as `Intl`
|
|
86
|
+
* options, because the catalogue is the source of truth and only the ICU
|
|
87
|
+
* spelling round-trips back out of it.
|
|
88
|
+
*/
|
|
89
|
+
interface ArgumentFormat {
|
|
90
|
+
type: ArgumentType;
|
|
91
|
+
style?: string;
|
|
92
|
+
}
|
|
43
93
|
declare class ArgumentMessage extends Base {
|
|
44
94
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
45
95
|
readonly expression: any;
|
|
46
|
-
|
|
96
|
+
readonly format?: ArgumentFormat | undefined;
|
|
97
|
+
constructor(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, expression: any, format?: ArgumentFormat | undefined);
|
|
47
98
|
}
|
|
48
99
|
declare class ElementMessage extends Base {
|
|
49
100
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
@@ -59,10 +110,22 @@ declare class ChoiceMessage extends Base {
|
|
|
59
110
|
readonly value: Message;
|
|
60
111
|
}[];
|
|
61
112
|
readonly expression: any;
|
|
113
|
+
/**
|
|
114
|
+
* Subtracted from the selector before `#` is formatted, so "You and 2
|
|
115
|
+
* others" can select on a total of three. Only `plural` and `ordinal`
|
|
116
|
+
* accept one; `select` has no number to offset.
|
|
117
|
+
*/
|
|
118
|
+
readonly offset?: number | undefined;
|
|
62
119
|
constructor(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, branches: {
|
|
63
120
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
64
121
|
readonly value: Message;
|
|
65
|
-
}[], expression: any
|
|
122
|
+
}[], expression: any,
|
|
123
|
+
/**
|
|
124
|
+
* Subtracted from the selector before `#` is formatted, so "You and 2
|
|
125
|
+
* others" can select on a total of three. Only `plural` and `ordinal`
|
|
126
|
+
* accept one; `select` has no number to offset.
|
|
127
|
+
*/
|
|
128
|
+
offset?: number | undefined);
|
|
66
129
|
}
|
|
67
130
|
declare class CompositeMessage extends Base {
|
|
68
131
|
readonly descriptor: {
|
|
@@ -84,7 +147,33 @@ type Message = LiteralMessage | ArgumentMessage | ElementMessage | ChoiceMessage
|
|
|
84
147
|
//#region src/features/messages/convert.d.ts
|
|
85
148
|
declare function convertMessageToIcu(message: Message): string;
|
|
86
149
|
//#endregion
|
|
150
|
+
//#region src/features/messages/escape.d.ts
|
|
151
|
+
/**
|
|
152
|
+
* Quoting for text that has to survive a message format unchanged.
|
|
153
|
+
*
|
|
154
|
+
* This lives beside the converter rather than in a transform because it belongs
|
|
155
|
+
* to the format being written, not to the syntax being read. A literal is the
|
|
156
|
+
* same literal whether it came from a template literal or from JSX, and the
|
|
157
|
+
* moment there is a second target format its reserved characters — and the way
|
|
158
|
+
* it spells an escape — are its own.
|
|
159
|
+
*/
|
|
160
|
+
/**
|
|
161
|
+
* Escape literal text so ICU MessageFormat 1 reads it as the text it is.
|
|
162
|
+
*
|
|
163
|
+
* MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
|
|
164
|
+
* doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
|
|
165
|
+
* only opens a quoted run when a character that needs quoting follows it —
|
|
166
|
+
* anything else and the apostrophe is itself literal. So `'#'` is a real escape
|
|
167
|
+
* inside a plural and the three characters `'#'` anywhere else, which is why
|
|
168
|
+
* `hash` has to be told rather than assumed.
|
|
169
|
+
*
|
|
170
|
+
* @param text Literal text as the author wrote it
|
|
171
|
+
* @param hash Whether `#` stands for a number here, i.e. whether some `plural`
|
|
172
|
+
* or `ordinal` encloses this text
|
|
173
|
+
*/
|
|
174
|
+
declare function escapeIcuLiteral(text: string, hash?: boolean): string;
|
|
175
|
+
//#endregion
|
|
87
176
|
//#region src/features/messages/hash.d.ts
|
|
88
177
|
declare function generateHash(input: string, context?: string): string;
|
|
89
178
|
//#endregion
|
|
90
|
-
export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
|
|
179
|
+
export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
|
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
import { t as generateHash } from "../../hash-DvzpieJD.mjs";
|
|
2
|
+
//#region src/features/messages/escape.ts
|
|
3
|
+
/**
|
|
4
|
+
* Quoting for text that has to survive a message format unchanged.
|
|
5
|
+
*
|
|
6
|
+
* This lives beside the converter rather than in a transform because it belongs
|
|
7
|
+
* to the format being written, not to the syntax being read. A literal is the
|
|
8
|
+
* same literal whether it came from a template literal or from JSX, and the
|
|
9
|
+
* moment there is a second target format its reserved characters — and the way
|
|
10
|
+
* it spells an escape — are its own.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Escape literal text so ICU MessageFormat 1 reads it as the text it is.
|
|
14
|
+
*
|
|
15
|
+
* MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
|
|
16
|
+
* doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
|
|
17
|
+
* only opens a quoted run when a character that needs quoting follows it —
|
|
18
|
+
* anything else and the apostrophe is itself literal. So `'#'` is a real escape
|
|
19
|
+
* inside a plural and the three characters `'#'` anywhere else, which is why
|
|
20
|
+
* `hash` has to be told rather than assumed.
|
|
21
|
+
*
|
|
22
|
+
* @param text Literal text as the author wrote it
|
|
23
|
+
* @param hash Whether `#` stands for a number here, i.e. whether some `plural`
|
|
24
|
+
* or `ordinal` encloses this text
|
|
25
|
+
*/
|
|
26
|
+
function escapeIcuLiteral(text, hash = false) {
|
|
27
|
+
return text.replace(/'/gu, "''").replace(hash ? /[{}#]+/gu : /[{}]+/gu, (run) => `'${run}'`);
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
2
30
|
//#region src/features/messages/types.ts
|
|
3
31
|
var Base = class {
|
|
4
32
|
toICUString() {
|
|
@@ -19,10 +47,12 @@ var LiteralMessage = class extends Base {
|
|
|
19
47
|
var ArgumentMessage = class extends Base {
|
|
20
48
|
identifier;
|
|
21
49
|
expression;
|
|
22
|
-
|
|
50
|
+
format;
|
|
51
|
+
constructor(identifier, expression, format) {
|
|
23
52
|
super();
|
|
24
53
|
this.identifier = identifier;
|
|
25
54
|
this.expression = expression;
|
|
55
|
+
this.format = format;
|
|
26
56
|
}
|
|
27
57
|
};
|
|
28
58
|
var ElementMessage = class extends Base {
|
|
@@ -41,12 +71,14 @@ var ChoiceMessage = class extends Base {
|
|
|
41
71
|
identifier;
|
|
42
72
|
branches;
|
|
43
73
|
expression;
|
|
44
|
-
|
|
74
|
+
offset;
|
|
75
|
+
constructor(kind, identifier, branches, expression, offset) {
|
|
45
76
|
super();
|
|
46
77
|
this.kind = kind;
|
|
47
78
|
this.identifier = identifier;
|
|
48
79
|
this.branches = branches;
|
|
49
80
|
this.expression = expression;
|
|
81
|
+
this.offset = offset;
|
|
50
82
|
}
|
|
51
83
|
};
|
|
52
84
|
var CompositeMessage = class extends Base {
|
|
@@ -75,8 +107,13 @@ const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
|
|
|
75
107
|
*/
|
|
76
108
|
const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
|
|
77
109
|
/**
|
|
78
|
-
* The ICU case a branch is written as.
|
|
79
|
-
*
|
|
110
|
+
* The ICU case a branch is written as.
|
|
111
|
+
*
|
|
112
|
+
* Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
|
|
113
|
+
* so is distinct from the CLDR category that would otherwise match it. `select`
|
|
114
|
+
* has no such syntax — its cases are literal string matches, and `=0` there is
|
|
115
|
+
* a parse error — so a numeric key stays bare, where it matches both `0` and
|
|
116
|
+
* `'0'`.
|
|
80
117
|
*
|
|
81
118
|
* Digits are read literally rather than coerced, because everything JavaScript
|
|
82
119
|
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
@@ -84,8 +121,9 @@ const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$
|
|
|
84
121
|
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
85
122
|
* whitespace or punctuation that made it numeric-looking is caught.
|
|
86
123
|
*/
|
|
87
|
-
function getBranchCase(identifier) {
|
|
124
|
+
function getBranchCase(kind, identifier) {
|
|
88
125
|
const key = String(identifier);
|
|
126
|
+
if (kind === "select") return key;
|
|
89
127
|
return /^\d+$/u.test(key) ? `=${+key}` : key;
|
|
90
128
|
}
|
|
91
129
|
/**
|
|
@@ -99,12 +137,12 @@ function getBranchCase(identifier) {
|
|
|
99
137
|
*/
|
|
100
138
|
function validateBranchIdentifier(kind, identifier) {
|
|
101
139
|
if (typeof identifier !== "string") return;
|
|
102
|
-
const branch = getBranchCase(identifier);
|
|
140
|
+
const branch = getBranchCase(kind, identifier);
|
|
103
141
|
if (!BRANCH_PATTERN.test(branch)) {
|
|
104
142
|
const suggestion = suggestBranchIdentifier(identifier);
|
|
105
143
|
throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
|
|
106
144
|
}
|
|
107
|
-
if (kind === "select" &&
|
|
145
|
+
if (kind === "select" && /^=\d+$/u.test(branch)) throw new Error(`Invalid select branch key '${identifier}', an exact value is only meaningful to 'plural' and 'ordinal', write it as '${branch.slice(1)}'`);
|
|
108
146
|
}
|
|
109
147
|
/**
|
|
110
148
|
* The nearest identifier-safe form of a key, so the error names the fix as well
|
|
@@ -124,9 +162,28 @@ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalen
|
|
|
124
162
|
while (reserved.has(identifier)) identifier = `${sequence.current++}`;
|
|
125
163
|
return identifier;
|
|
126
164
|
}
|
|
165
|
+
const numbered = {
|
|
166
|
+
tags: [],
|
|
167
|
+
values: []
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* The number this expression already has, or a fresh one. An anonymous
|
|
171
|
+
* placeholder is named by its position, so the same value written twice would
|
|
172
|
+
* otherwise arrive as two names — `{0} x {1}` for one length, which asks a
|
|
173
|
+
* translator to keep two holes in step and the caller to supply one value
|
|
174
|
+
* under two props. This is the rule explicit names already follow, where a
|
|
175
|
+
* repeat is allowed precisely when nothing distinguishes it.
|
|
176
|
+
*/
|
|
177
|
+
function number(assigned, expression) {
|
|
178
|
+
const seen = assigned.find(([e]) => equivalent(e, expression));
|
|
179
|
+
if (seen) return seen[1];
|
|
180
|
+
const identifier = next();
|
|
181
|
+
assigned.push([expression, identifier]);
|
|
182
|
+
return identifier;
|
|
183
|
+
}
|
|
127
184
|
function walk(message) {
|
|
128
185
|
if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
|
|
129
|
-
if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier =
|
|
186
|
+
if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = number(message instanceof ElementMessage ? numbered.tags : numbered.values, message.expression);
|
|
130
187
|
}
|
|
131
188
|
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
132
189
|
if (message instanceof ChoiceMessage) for (const branch of message.branches) {
|
|
@@ -166,28 +223,101 @@ function collectAssignedIdentifiers(message, equivalent) {
|
|
|
166
223
|
//#endregion
|
|
167
224
|
//#region src/features/messages/convert.ts
|
|
168
225
|
function convertMessageToIcu(message) {
|
|
169
|
-
function internalConvertMessageToIcu(message) {
|
|
226
|
+
function internalConvertMessageToIcu(message, hash) {
|
|
170
227
|
switch (true) {
|
|
171
|
-
case message instanceof LiteralMessage: return String(message.text);
|
|
172
|
-
case message instanceof ArgumentMessage:
|
|
228
|
+
case message instanceof LiteralMessage: return escapeIcuLiteral(String(message.text), hash !== void 0);
|
|
229
|
+
case message instanceof ArgumentMessage: {
|
|
230
|
+
if (!message.format && hash !== void 0 && String(message.identifier) === hash) return "#";
|
|
231
|
+
const parts = [String(message.identifier)];
|
|
232
|
+
if (message.format) parts.push(message.format.type);
|
|
233
|
+
if (message.format?.style) parts.push(message.format.style);
|
|
234
|
+
return `{${parts.join(", ")}}`;
|
|
235
|
+
}
|
|
173
236
|
case message instanceof ElementMessage: {
|
|
174
237
|
if (message.children.length === 0) return `<${String(message.identifier)}/>`;
|
|
175
|
-
const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
|
|
238
|
+
const children = message.children.map((m) => internalConvertMessageToIcu(m, hash)).join("");
|
|
176
239
|
return `<${String(message.identifier)}>${children}</${String(message.identifier)}>`;
|
|
177
240
|
}
|
|
178
241
|
case message instanceof ChoiceMessage: {
|
|
242
|
+
const scope = message.kind === "select" ? hash : String(message.identifier);
|
|
179
243
|
const branches = message.branches.map(({ identifier, value }) => ({
|
|
180
|
-
identifier: getBranchCase(identifier),
|
|
181
|
-
value: internalConvertMessageToIcu(value)
|
|
244
|
+
identifier: getBranchCase(message.kind, identifier),
|
|
245
|
+
value: internalConvertMessageToIcu(value, scope)
|
|
182
246
|
})).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
|
|
183
247
|
const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
|
|
184
|
-
|
|
248
|
+
const offset = message.offset === void 0 || message.kind === "select" ? "" : ` offset:${message.offset}`;
|
|
249
|
+
return `{${String(message.identifier)}, ${format},${offset}\n${branches}}`;
|
|
185
250
|
}
|
|
186
|
-
case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m)).join("");
|
|
251
|
+
case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m, hash)).join("");
|
|
187
252
|
default: throw new Error("Unknown message type", { cause: message });
|
|
188
253
|
}
|
|
189
254
|
}
|
|
190
|
-
return internalConvertMessageToIcu(message).trim();
|
|
255
|
+
return internalConvertMessageToIcu(message, void 0).trim();
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/features/messages/format.ts
|
|
259
|
+
/**
|
|
260
|
+
* The ICU argument types a macro can author, and the named styles each accepts.
|
|
261
|
+
*
|
|
262
|
+
* `currency` is deliberately absent from `number`. MF1 has nowhere to write the
|
|
263
|
+
* currency code — it comes from the formatter's configuration, not the message
|
|
264
|
+
* — so `{price, number, currency}` formats as a literal `{$price}` at runtime
|
|
265
|
+
* rather than an amount. Currency belongs to number skeletons, which the
|
|
266
|
+
* formatter does not accept yet either.
|
|
267
|
+
*
|
|
268
|
+
* `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
|
|
269
|
+
* oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
|
|
270
|
+
* equivalent, and the third is deprecated in ICU itself in favour of `plural`.
|
|
271
|
+
*/
|
|
272
|
+
const ARGUMENT_STYLES = {
|
|
273
|
+
number: ["integer", "percent"],
|
|
274
|
+
date: [
|
|
275
|
+
"short",
|
|
276
|
+
"medium",
|
|
277
|
+
"long",
|
|
278
|
+
"full"
|
|
279
|
+
],
|
|
280
|
+
time: [
|
|
281
|
+
"short",
|
|
282
|
+
"medium",
|
|
283
|
+
"long",
|
|
284
|
+
"full"
|
|
285
|
+
]
|
|
286
|
+
};
|
|
287
|
+
const ARGUMENT_TYPES = Object.keys(ARGUMENT_STYLES);
|
|
288
|
+
function isArgumentType(kind) {
|
|
289
|
+
return Object.hasOwn(ARGUMENT_STYLES, kind);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* A literal `NumberFormat` pattern, e.g. `#,##0.00`.
|
|
293
|
+
*
|
|
294
|
+
* A pattern has to carry a digit placeholder — `#` or `0` — because that is
|
|
295
|
+
* what makes it a pattern rather than a word. Without that requirement any
|
|
296
|
+
* brace-free string qualifies, which quietly readmits the named styles this
|
|
297
|
+
* module exists to reject: `currency` would sail through as a "pattern" and
|
|
298
|
+
* extract to the `{price, number, currency}` the formatter cannot honour, and
|
|
299
|
+
* so would a plain typo.
|
|
300
|
+
*
|
|
301
|
+
* Braces are excluded separately: ICU reserves them for its own pattern syntax,
|
|
302
|
+
* so a style carrying one would close the argument early and take the rest of
|
|
303
|
+
* the message with it.
|
|
304
|
+
*/
|
|
305
|
+
const LITERAL_STYLE_PATTERN = /^[^{}\r\n]*[#0][^{}\r\n]*$/;
|
|
306
|
+
/**
|
|
307
|
+
* Reject an argument style the formatter cannot honour, while the style is
|
|
308
|
+
* still attached to a file and a line.
|
|
309
|
+
*
|
|
310
|
+
* A style is a bare string in the source and a bare string in the catalogue, so
|
|
311
|
+
* nothing between here and the runtime has an opinion about it. Left unchecked,
|
|
312
|
+
* a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
|
|
313
|
+
* perfectly normal and only misformats once it reaches a user.
|
|
314
|
+
*/
|
|
315
|
+
function validateArgumentStyle(type, style) {
|
|
316
|
+
const named = ARGUMENT_STYLES[type];
|
|
317
|
+
if (named.includes(style)) return;
|
|
318
|
+
if (type === "number" && LITERAL_STYLE_PATTERN.test(style)) return;
|
|
319
|
+
const expected = named.map((s) => `'${s}'`).join(", ");
|
|
320
|
+
throw new Error(`Invalid ${type} style '${style}', expected ${expected}` + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
|
|
191
321
|
}
|
|
192
322
|
//#endregion
|
|
193
|
-
export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
|
|
323
|
+
export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
|
|
@@ -73,13 +73,22 @@ const configLoaders = Object.freeze({
|
|
|
73
73
|
});
|
|
74
74
|
//#endregion
|
|
75
75
|
//#region src/features/loader/resolve.ts
|
|
76
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The config file {@link resolveConfig} would load, for callers that need the
|
|
78
|
+
* path itself rather than its contents — salting a bundler's cache key with it,
|
|
79
|
+
* for one, since what a catalogue assembles into depends on the config.
|
|
80
|
+
*/
|
|
81
|
+
function resolveConfigFile(name = "saykit") {
|
|
77
82
|
const file = findConfigFile(name, process.cwd());
|
|
78
83
|
if (!file) throw new Error(`Could not find config file for "${name}"`);
|
|
79
|
-
|
|
84
|
+
return file.id;
|
|
85
|
+
}
|
|
86
|
+
function resolveConfig(name = "saykit") {
|
|
87
|
+
const id = resolveConfigFile(name);
|
|
88
|
+
const ext = (0, node_path.extname)(id).toLowerCase();
|
|
80
89
|
const load = ext in configLoaders ? configLoaders[ext] : null;
|
|
81
90
|
if (!load) throw new Error(`Unsupported config file type "${ext}" for "${name}"`);
|
|
82
|
-
const config = load(
|
|
91
|
+
const config = load(id);
|
|
83
92
|
if (!config || typeof config !== "object") throw new Error(`Invalid config file for "${name}"`);
|
|
84
93
|
return config;
|
|
85
94
|
}
|
|
@@ -90,3 +99,9 @@ Object.defineProperty(exports, "resolveConfig", {
|
|
|
90
99
|
return resolveConfig;
|
|
91
100
|
}
|
|
92
101
|
});
|
|
102
|
+
Object.defineProperty(exports, "resolveConfigFile", {
|
|
103
|
+
enumerable: true,
|
|
104
|
+
get: function() {
|
|
105
|
+
return resolveConfigFile;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
@@ -73,15 +73,24 @@ const configLoaders = Object.freeze({
|
|
|
73
73
|
});
|
|
74
74
|
//#endregion
|
|
75
75
|
//#region src/features/loader/resolve.ts
|
|
76
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The config file {@link resolveConfig} would load, for callers that need the
|
|
78
|
+
* path itself rather than its contents — salting a bundler's cache key with it,
|
|
79
|
+
* for one, since what a catalogue assembles into depends on the config.
|
|
80
|
+
*/
|
|
81
|
+
function resolveConfigFile(name = "saykit") {
|
|
77
82
|
const file = findConfigFile(name, process.cwd());
|
|
78
83
|
if (!file) throw new Error(`Could not find config file for "${name}"`);
|
|
79
|
-
|
|
84
|
+
return file.id;
|
|
85
|
+
}
|
|
86
|
+
function resolveConfig(name = "saykit") {
|
|
87
|
+
const id = resolveConfigFile(name);
|
|
88
|
+
const ext = extname(id).toLowerCase();
|
|
80
89
|
const load = ext in configLoaders ? configLoaders[ext] : null;
|
|
81
90
|
if (!load) throw new Error(`Unsupported config file type "${ext}" for "${name}"`);
|
|
82
|
-
const config = load(
|
|
91
|
+
const config = load(id);
|
|
83
92
|
if (!config || typeof config !== "object") throw new Error(`Invalid config file for "${name}"`);
|
|
84
93
|
return config;
|
|
85
94
|
}
|
|
86
95
|
//#endregion
|
|
87
|
-
export { resolveConfig as t };
|
|
96
|
+
export { resolveConfigFile as n, resolveConfig as t };
|