@saykit/config 0.6.0 → 0.7.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 +189 -54
- package/dist/features/messages/index.d.cts +87 -3
- package/dist/features/messages/index.d.mts +87 -3
- package/dist/features/messages/index.mjs +184 -55
- 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,54 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_hash = require("../../hash-51ce8YiG.cjs");
|
|
3
|
-
//#region src/features/messages/identifier.ts
|
|
4
|
-
const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
|
|
5
|
-
function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalent = () => false) {
|
|
6
|
-
const reserved = collectAssignedIdentifiers(message, equivalent);
|
|
7
|
-
function next() {
|
|
8
|
-
let identifier = `${sequence.current++}`;
|
|
9
|
-
while (reserved.has(identifier)) identifier = `${sequence.current++}`;
|
|
10
|
-
return identifier;
|
|
11
|
-
}
|
|
12
|
-
function walk(message) {
|
|
13
|
-
if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
|
|
14
|
-
if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
|
|
15
|
-
}
|
|
16
|
-
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
17
|
-
if (message instanceof ChoiceMessage) for (const branch of message.branches) {
|
|
18
|
-
if (branch.identifier === AUTO_INCREMENT_IDENTIFIER) branch.identifier = next();
|
|
19
|
-
walk(branch.value);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
walk(message);
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Collect the identifiers already assigned before this pass runs, so generated
|
|
26
|
-
* sequence numbers never shadow an explicit one (e.g. an element tagged `0`).
|
|
27
|
-
*
|
|
28
|
-
* A name is claimed by what produced it, not by the name alone. Two that differ
|
|
29
|
-
* each compile to their own prop, and a translator moving them around a sentence
|
|
30
|
-
* has to be able to tell them apart, so they are a build error. Repeats are fine
|
|
31
|
-
* when nothing distinguishes them: the same variable interpolated twice is one
|
|
32
|
-
* value, and two identical elements are one tag.
|
|
33
|
-
*/
|
|
34
|
-
function collectAssignedIdentifiers(message, equivalent) {
|
|
35
|
-
const tags = /* @__PURE__ */ new Map();
|
|
36
|
-
const values = /* @__PURE__ */ new Map();
|
|
37
|
-
function claim(claimed, identifier, expression, conflict) {
|
|
38
|
-
if (claimed.has(identifier) && !equivalent(claimed.get(identifier), expression)) throw new Error(conflict);
|
|
39
|
-
claimed.set(identifier, expression);
|
|
40
|
-
}
|
|
41
|
-
function walk(message) {
|
|
42
|
-
if (message instanceof ElementMessage && typeof message.identifier === "string") claim(tags, message.identifier, message.expression, `Duplicate element tag '${message.identifier}', give each element in a message its own tag unless they are identical`);
|
|
43
|
-
if ((message instanceof ArgumentMessage || message instanceof ChoiceMessage) && typeof message.identifier === "string") claim(values, message.identifier, message.expression, `Duplicate placeholder name '${message.identifier}', give each value in a message its own name unless they are identical`);
|
|
44
|
-
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
45
|
-
if (message instanceof ChoiceMessage) for (const branch of message.branches) walk(branch.value);
|
|
46
|
-
}
|
|
47
|
-
walk(message);
|
|
48
|
-
for (const tag of tags.keys()) if (values.has(tag)) throw new Error(`Element tag '${tag}' collides with an argument of the same name`);
|
|
49
|
-
return /* @__PURE__ */ new Set([...tags.keys(), ...values.keys()]);
|
|
50
|
-
}
|
|
51
|
-
//#endregion
|
|
52
3
|
//#region src/features/messages/types.ts
|
|
53
4
|
var Base = class {
|
|
54
5
|
toICUString() {
|
|
@@ -69,10 +20,12 @@ var LiteralMessage = class extends Base {
|
|
|
69
20
|
var ArgumentMessage = class extends Base {
|
|
70
21
|
identifier;
|
|
71
22
|
expression;
|
|
72
|
-
|
|
23
|
+
format;
|
|
24
|
+
constructor(identifier, expression, format) {
|
|
73
25
|
super();
|
|
74
26
|
this.identifier = identifier;
|
|
75
27
|
this.expression = expression;
|
|
28
|
+
this.format = format;
|
|
76
29
|
}
|
|
77
30
|
};
|
|
78
31
|
var ElementMessage = class extends Base {
|
|
@@ -91,12 +44,14 @@ var ChoiceMessage = class extends Base {
|
|
|
91
44
|
identifier;
|
|
92
45
|
branches;
|
|
93
46
|
expression;
|
|
94
|
-
|
|
47
|
+
offset;
|
|
48
|
+
constructor(kind, identifier, branches, expression, offset) {
|
|
95
49
|
super();
|
|
96
50
|
this.kind = kind;
|
|
97
51
|
this.identifier = identifier;
|
|
98
52
|
this.branches = branches;
|
|
99
53
|
this.expression = expression;
|
|
54
|
+
this.offset = offset;
|
|
100
55
|
}
|
|
101
56
|
};
|
|
102
57
|
var CompositeMessage = class extends Base {
|
|
@@ -117,12 +72,120 @@ var CompositeMessage = class extends Base {
|
|
|
117
72
|
}
|
|
118
73
|
};
|
|
119
74
|
//#endregion
|
|
75
|
+
//#region src/features/messages/identifier.ts
|
|
76
|
+
const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
|
|
77
|
+
/**
|
|
78
|
+
* An ICU case, either an exact value or a key. ICU reserves its own pattern
|
|
79
|
+
* syntax, so a key carries no punctuation and no whitespace.
|
|
80
|
+
*/
|
|
81
|
+
const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
|
|
82
|
+
/**
|
|
83
|
+
* The ICU case a branch is written as.
|
|
84
|
+
*
|
|
85
|
+
* Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
|
|
86
|
+
* so is distinct from the CLDR category that would otherwise match it. `select`
|
|
87
|
+
* has no such syntax — its cases are literal string matches, and `=0` there is
|
|
88
|
+
* a parse error — so a numeric key stays bare, where it matches both `0` and
|
|
89
|
+
* `'0'`.
|
|
90
|
+
*
|
|
91
|
+
* Digits are read literally rather than coerced, because everything JavaScript
|
|
92
|
+
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
93
|
+
* `=0`, which would pass for a key that selects zero and quietly leave the
|
|
94
|
+
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
95
|
+
* whitespace or punctuation that made it numeric-looking is caught.
|
|
96
|
+
*/
|
|
97
|
+
function getBranchCase(kind, identifier) {
|
|
98
|
+
const key = String(identifier);
|
|
99
|
+
if (kind === "select") return key;
|
|
100
|
+
return /^\d+$/u.test(key) ? `=${+key}` : key;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
104
|
+
* file and a line.
|
|
105
|
+
*
|
|
106
|
+
* A hyphenated string union is ordinary application code and typechecks,
|
|
107
|
+
* builds, and extracts to a catalogue entry that looks perfectly normal — the
|
|
108
|
+
* only sign of trouble is a parse error at format time, in a message whose
|
|
109
|
+
* source is long gone.
|
|
110
|
+
*/
|
|
111
|
+
function validateBranchIdentifier(kind, identifier) {
|
|
112
|
+
if (typeof identifier !== "string") return;
|
|
113
|
+
const branch = getBranchCase(kind, identifier);
|
|
114
|
+
if (!BRANCH_PATTERN.test(branch)) {
|
|
115
|
+
const suggestion = suggestBranchIdentifier(identifier);
|
|
116
|
+
throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
|
|
117
|
+
}
|
|
118
|
+
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)}'`);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The nearest identifier-safe form of a key, so the error names the fix as well
|
|
122
|
+
* as the problem. The constraint comes from ICU rather than from anything the
|
|
123
|
+
* author wrote, and camel case is how the rest of the codebase already spells a
|
|
124
|
+
* name of more than one word.
|
|
125
|
+
*/
|
|
126
|
+
function suggestBranchIdentifier(identifier) {
|
|
127
|
+
const suggestion = identifier.split(/[^\p{L}\p{N}]+/u).filter(Boolean).map((word, index) => index === 0 ? word : word[0].toUpperCase() + word.slice(1)).join("");
|
|
128
|
+
if (!BRANCH_PATTERN.test(suggestion) || !Number.isNaN(+suggestion)) return void 0;
|
|
129
|
+
return suggestion;
|
|
130
|
+
}
|
|
131
|
+
function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalent = () => false) {
|
|
132
|
+
const reserved = collectAssignedIdentifiers(message, equivalent);
|
|
133
|
+
function next() {
|
|
134
|
+
let identifier = `${sequence.current++}`;
|
|
135
|
+
while (reserved.has(identifier)) identifier = `${sequence.current++}`;
|
|
136
|
+
return identifier;
|
|
137
|
+
}
|
|
138
|
+
function walk(message) {
|
|
139
|
+
if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
|
|
140
|
+
if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
|
|
141
|
+
}
|
|
142
|
+
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
143
|
+
if (message instanceof ChoiceMessage) for (const branch of message.branches) {
|
|
144
|
+
if (branch.identifier === AUTO_INCREMENT_IDENTIFIER) branch.identifier = next();
|
|
145
|
+
walk(branch.value);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
walk(message);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Collect the identifiers already assigned before this pass runs, so generated
|
|
152
|
+
* sequence numbers never shadow an explicit one (e.g. an element tagged `0`).
|
|
153
|
+
*
|
|
154
|
+
* A name is claimed by what produced it, not by the name alone. Two that differ
|
|
155
|
+
* each compile to their own prop, and a translator moving them around a sentence
|
|
156
|
+
* has to be able to tell them apart, so they are a build error. Repeats are fine
|
|
157
|
+
* when nothing distinguishes them: the same variable interpolated twice is one
|
|
158
|
+
* value, and two identical elements are one tag.
|
|
159
|
+
*/
|
|
160
|
+
function collectAssignedIdentifiers(message, equivalent) {
|
|
161
|
+
const tags = /* @__PURE__ */ new Map();
|
|
162
|
+
const values = /* @__PURE__ */ new Map();
|
|
163
|
+
function claim(claimed, identifier, expression, conflict) {
|
|
164
|
+
if (claimed.has(identifier) && !equivalent(claimed.get(identifier), expression)) throw new Error(conflict);
|
|
165
|
+
claimed.set(identifier, expression);
|
|
166
|
+
}
|
|
167
|
+
function walk(message) {
|
|
168
|
+
if (message instanceof ElementMessage && typeof message.identifier === "string") claim(tags, message.identifier, message.expression, `Duplicate element tag '${message.identifier}', give each element in a message its own tag unless they are identical`);
|
|
169
|
+
if ((message instanceof ArgumentMessage || message instanceof ChoiceMessage) && typeof message.identifier === "string") claim(values, message.identifier, message.expression, `Duplicate placeholder name '${message.identifier}', give each value in a message its own name unless they are identical`);
|
|
170
|
+
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
171
|
+
if (message instanceof ChoiceMessage) for (const branch of message.branches) walk(branch.value);
|
|
172
|
+
}
|
|
173
|
+
walk(message);
|
|
174
|
+
for (const tag of tags.keys()) if (values.has(tag)) throw new Error(`Element tag '${tag}' collides with an argument of the same name`);
|
|
175
|
+
return /* @__PURE__ */ new Set([...tags.keys(), ...values.keys()]);
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
120
178
|
//#region src/features/messages/convert.ts
|
|
121
179
|
function convertMessageToIcu(message) {
|
|
122
180
|
function internalConvertMessageToIcu(message) {
|
|
123
181
|
switch (true) {
|
|
124
182
|
case message instanceof LiteralMessage: return String(message.text);
|
|
125
|
-
case message instanceof ArgumentMessage:
|
|
183
|
+
case message instanceof ArgumentMessage: {
|
|
184
|
+
const parts = [String(message.identifier)];
|
|
185
|
+
if (message.format) parts.push(message.format.type);
|
|
186
|
+
if (message.format?.style) parts.push(message.format.style);
|
|
187
|
+
return `{${parts.join(", ")}}`;
|
|
188
|
+
}
|
|
126
189
|
case message instanceof ElementMessage: {
|
|
127
190
|
if (message.children.length === 0) return `<${String(message.identifier)}/>`;
|
|
128
191
|
const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
|
|
@@ -130,11 +193,12 @@ function convertMessageToIcu(message) {
|
|
|
130
193
|
}
|
|
131
194
|
case message instanceof ChoiceMessage: {
|
|
132
195
|
const branches = message.branches.map(({ identifier, value }) => ({
|
|
133
|
-
identifier:
|
|
196
|
+
identifier: getBranchCase(message.kind, identifier),
|
|
134
197
|
value: internalConvertMessageToIcu(value)
|
|
135
198
|
})).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
|
|
136
199
|
const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
|
|
137
|
-
|
|
200
|
+
const offset = message.offset === void 0 || message.kind === "select" ? "" : ` offset:${message.offset}`;
|
|
201
|
+
return `{${String(message.identifier)}, ${format},${offset}\n${branches}}`;
|
|
138
202
|
}
|
|
139
203
|
case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m)).join("");
|
|
140
204
|
default: throw new Error("Unknown message type", { cause: message });
|
|
@@ -143,6 +207,73 @@ function convertMessageToIcu(message) {
|
|
|
143
207
|
return internalConvertMessageToIcu(message).trim();
|
|
144
208
|
}
|
|
145
209
|
//#endregion
|
|
210
|
+
//#region src/features/messages/format.ts
|
|
211
|
+
/**
|
|
212
|
+
* The ICU argument types a macro can author, and the named styles each accepts.
|
|
213
|
+
*
|
|
214
|
+
* `currency` is deliberately absent from `number`. MF1 has nowhere to write the
|
|
215
|
+
* currency code — it comes from the formatter's configuration, not the message
|
|
216
|
+
* — so `{price, number, currency}` formats as a literal `{$price}` at runtime
|
|
217
|
+
* rather than an amount. Currency belongs to number skeletons, which the
|
|
218
|
+
* formatter does not accept yet either.
|
|
219
|
+
*
|
|
220
|
+
* `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
|
|
221
|
+
* oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
|
|
222
|
+
* equivalent, and the third is deprecated in ICU itself in favour of `plural`.
|
|
223
|
+
*/
|
|
224
|
+
const ARGUMENT_STYLES = {
|
|
225
|
+
number: ["integer", "percent"],
|
|
226
|
+
date: [
|
|
227
|
+
"short",
|
|
228
|
+
"medium",
|
|
229
|
+
"long",
|
|
230
|
+
"full"
|
|
231
|
+
],
|
|
232
|
+
time: [
|
|
233
|
+
"short",
|
|
234
|
+
"medium",
|
|
235
|
+
"long",
|
|
236
|
+
"full"
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
const ARGUMENT_TYPES = Object.keys(ARGUMENT_STYLES);
|
|
240
|
+
function isArgumentType(kind) {
|
|
241
|
+
return Object.hasOwn(ARGUMENT_STYLES, kind);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* A literal `NumberFormat` pattern, e.g. `#,##0.00`.
|
|
245
|
+
*
|
|
246
|
+
* A pattern has to carry a digit placeholder — `#` or `0` — because that is
|
|
247
|
+
* what makes it a pattern rather than a word. Without that requirement any
|
|
248
|
+
* brace-free string qualifies, which quietly readmits the named styles this
|
|
249
|
+
* module exists to reject: `currency` would sail through as a "pattern" and
|
|
250
|
+
* extract to the `{price, number, currency}` the formatter cannot honour, and
|
|
251
|
+
* so would a plain typo.
|
|
252
|
+
*
|
|
253
|
+
* Braces are excluded separately: ICU reserves them for its own pattern syntax,
|
|
254
|
+
* so a style carrying one would close the argument early and take the rest of
|
|
255
|
+
* the message with it.
|
|
256
|
+
*/
|
|
257
|
+
const LITERAL_STYLE_PATTERN = /^[^{}\r\n]*[#0][^{}\r\n]*$/;
|
|
258
|
+
/**
|
|
259
|
+
* Reject an argument style the formatter cannot honour, while the style is
|
|
260
|
+
* still attached to a file and a line.
|
|
261
|
+
*
|
|
262
|
+
* A style is a bare string in the source and a bare string in the catalogue, so
|
|
263
|
+
* nothing between here and the runtime has an opinion about it. Left unchecked,
|
|
264
|
+
* a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
|
|
265
|
+
* perfectly normal and only misformats once it reaches a user.
|
|
266
|
+
*/
|
|
267
|
+
function validateArgumentStyle(type, style) {
|
|
268
|
+
const named = ARGUMENT_STYLES[type];
|
|
269
|
+
if (named.includes(style)) return;
|
|
270
|
+
if (type === "number" && LITERAL_STYLE_PATTERN.test(style)) return;
|
|
271
|
+
const expected = named.map((s) => `'${s}'`).join(", ");
|
|
272
|
+
throw new Error(`Invalid ${type} style '${style}', expected ${expected}` + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
exports.ARGUMENT_STYLES = ARGUMENT_STYLES;
|
|
276
|
+
exports.ARGUMENT_TYPES = ARGUMENT_TYPES;
|
|
146
277
|
exports.AUTO_INCREMENT_IDENTIFIER = AUTO_INCREMENT_IDENTIFIER;
|
|
147
278
|
exports.ArgumentMessage = ArgumentMessage;
|
|
148
279
|
exports.ChoiceMessage = ChoiceMessage;
|
|
@@ -152,3 +283,7 @@ exports.LiteralMessage = LiteralMessage;
|
|
|
152
283
|
exports.assignSequenceIdentifiers = assignSequenceIdentifiers;
|
|
153
284
|
exports.convertMessageToIcu = convertMessageToIcu;
|
|
154
285
|
exports.generateHash = require_hash.generateHash;
|
|
286
|
+
exports.getBranchCase = getBranchCase;
|
|
287
|
+
exports.isArgumentType = isArgumentType;
|
|
288
|
+
exports.validateArgumentStyle = validateArgumentStyle;
|
|
289
|
+
exports.validateBranchIdentifier = validateBranchIdentifier;
|
|
@@ -1,5 +1,64 @@
|
|
|
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;
|
|
36
|
+
/**
|
|
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'`.
|
|
44
|
+
*
|
|
45
|
+
* Digits are read literally rather than coerced, because everything JavaScript
|
|
46
|
+
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
47
|
+
* `=0`, which would pass for a key that selects zero and quietly leave the
|
|
48
|
+
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
49
|
+
* whitespace or punctuation that made it numeric-looking is caught.
|
|
50
|
+
*/
|
|
51
|
+
declare function getBranchCase(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
|
|
52
|
+
/**
|
|
53
|
+
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
54
|
+
* file and a line.
|
|
55
|
+
*
|
|
56
|
+
* A hyphenated string union is ordinary application code and typechecks,
|
|
57
|
+
* builds, and extracts to a catalogue entry that looks perfectly normal — the
|
|
58
|
+
* only sign of trouble is a parse error at format time, in a message whose
|
|
59
|
+
* source is long gone.
|
|
60
|
+
*/
|
|
61
|
+
declare function validateBranchIdentifier(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): void;
|
|
3
62
|
/**
|
|
4
63
|
* Decides whether two placeholders sharing a name are the same placeholder, and
|
|
5
64
|
* so may share it. Only the syntax that produced them can answer that, so the
|
|
@@ -19,10 +78,23 @@ declare class LiteralMessage extends Base {
|
|
|
19
78
|
readonly text: string;
|
|
20
79
|
constructor(text: string);
|
|
21
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
|
+
}
|
|
22
93
|
declare class ArgumentMessage extends Base {
|
|
23
94
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
24
95
|
readonly expression: any;
|
|
25
|
-
|
|
96
|
+
readonly format?: ArgumentFormat | undefined;
|
|
97
|
+
constructor(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, expression: any, format?: ArgumentFormat | undefined);
|
|
26
98
|
}
|
|
27
99
|
declare class ElementMessage extends Base {
|
|
28
100
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
@@ -38,10 +110,22 @@ declare class ChoiceMessage extends Base {
|
|
|
38
110
|
readonly value: Message;
|
|
39
111
|
}[];
|
|
40
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;
|
|
41
119
|
constructor(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, branches: {
|
|
42
120
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
43
121
|
readonly value: Message;
|
|
44
|
-
}[], 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);
|
|
45
129
|
}
|
|
46
130
|
declare class CompositeMessage extends Base {
|
|
47
131
|
readonly descriptor: {
|
|
@@ -66,4 +150,4 @@ declare function convertMessageToIcu(message: Message): string;
|
|
|
66
150
|
//#region src/features/messages/hash.d.ts
|
|
67
151
|
declare function generateHash(input: string, context?: string): string;
|
|
68
152
|
//#endregion
|
|
69
|
-
export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash };
|
|
153
|
+
export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
|
|
@@ -1,5 +1,64 @@
|
|
|
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;
|
|
36
|
+
/**
|
|
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'`.
|
|
44
|
+
*
|
|
45
|
+
* Digits are read literally rather than coerced, because everything JavaScript
|
|
46
|
+
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
47
|
+
* `=0`, which would pass for a key that selects zero and quietly leave the
|
|
48
|
+
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
49
|
+
* whitespace or punctuation that made it numeric-looking is caught.
|
|
50
|
+
*/
|
|
51
|
+
declare function getBranchCase(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
|
|
52
|
+
/**
|
|
53
|
+
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
54
|
+
* file and a line.
|
|
55
|
+
*
|
|
56
|
+
* A hyphenated string union is ordinary application code and typechecks,
|
|
57
|
+
* builds, and extracts to a catalogue entry that looks perfectly normal — the
|
|
58
|
+
* only sign of trouble is a parse error at format time, in a message whose
|
|
59
|
+
* source is long gone.
|
|
60
|
+
*/
|
|
61
|
+
declare function validateBranchIdentifier(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): void;
|
|
3
62
|
/**
|
|
4
63
|
* Decides whether two placeholders sharing a name are the same placeholder, and
|
|
5
64
|
* so may share it. Only the syntax that produced them can answer that, so the
|
|
@@ -19,10 +78,23 @@ declare class LiteralMessage extends Base {
|
|
|
19
78
|
readonly text: string;
|
|
20
79
|
constructor(text: string);
|
|
21
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
|
+
}
|
|
22
93
|
declare class ArgumentMessage extends Base {
|
|
23
94
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
24
95
|
readonly expression: any;
|
|
25
|
-
|
|
96
|
+
readonly format?: ArgumentFormat | undefined;
|
|
97
|
+
constructor(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, expression: any, format?: ArgumentFormat | undefined);
|
|
26
98
|
}
|
|
27
99
|
declare class ElementMessage extends Base {
|
|
28
100
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
@@ -38,10 +110,22 @@ declare class ChoiceMessage extends Base {
|
|
|
38
110
|
readonly value: Message;
|
|
39
111
|
}[];
|
|
40
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;
|
|
41
119
|
constructor(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, branches: {
|
|
42
120
|
identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
|
|
43
121
|
readonly value: Message;
|
|
44
|
-
}[], 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);
|
|
45
129
|
}
|
|
46
130
|
declare class CompositeMessage extends Base {
|
|
47
131
|
readonly descriptor: {
|
|
@@ -66,4 +150,4 @@ declare function convertMessageToIcu(message: Message): string;
|
|
|
66
150
|
//#region src/features/messages/hash.d.ts
|
|
67
151
|
declare function generateHash(input: string, context?: string): string;
|
|
68
152
|
//#endregion
|
|
69
|
-
export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash };
|
|
153
|
+
export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
|
|
@@ -1,53 +1,4 @@
|
|
|
1
1
|
import { t as generateHash } from "../../hash-DvzpieJD.mjs";
|
|
2
|
-
//#region src/features/messages/identifier.ts
|
|
3
|
-
const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
|
|
4
|
-
function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalent = () => false) {
|
|
5
|
-
const reserved = collectAssignedIdentifiers(message, equivalent);
|
|
6
|
-
function next() {
|
|
7
|
-
let identifier = `${sequence.current++}`;
|
|
8
|
-
while (reserved.has(identifier)) identifier = `${sequence.current++}`;
|
|
9
|
-
return identifier;
|
|
10
|
-
}
|
|
11
|
-
function walk(message) {
|
|
12
|
-
if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
|
|
13
|
-
if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
|
|
14
|
-
}
|
|
15
|
-
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
16
|
-
if (message instanceof ChoiceMessage) for (const branch of message.branches) {
|
|
17
|
-
if (branch.identifier === AUTO_INCREMENT_IDENTIFIER) branch.identifier = next();
|
|
18
|
-
walk(branch.value);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
walk(message);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Collect the identifiers already assigned before this pass runs, so generated
|
|
25
|
-
* sequence numbers never shadow an explicit one (e.g. an element tagged `0`).
|
|
26
|
-
*
|
|
27
|
-
* A name is claimed by what produced it, not by the name alone. Two that differ
|
|
28
|
-
* each compile to their own prop, and a translator moving them around a sentence
|
|
29
|
-
* has to be able to tell them apart, so they are a build error. Repeats are fine
|
|
30
|
-
* when nothing distinguishes them: the same variable interpolated twice is one
|
|
31
|
-
* value, and two identical elements are one tag.
|
|
32
|
-
*/
|
|
33
|
-
function collectAssignedIdentifiers(message, equivalent) {
|
|
34
|
-
const tags = /* @__PURE__ */ new Map();
|
|
35
|
-
const values = /* @__PURE__ */ new Map();
|
|
36
|
-
function claim(claimed, identifier, expression, conflict) {
|
|
37
|
-
if (claimed.has(identifier) && !equivalent(claimed.get(identifier), expression)) throw new Error(conflict);
|
|
38
|
-
claimed.set(identifier, expression);
|
|
39
|
-
}
|
|
40
|
-
function walk(message) {
|
|
41
|
-
if (message instanceof ElementMessage && typeof message.identifier === "string") claim(tags, message.identifier, message.expression, `Duplicate element tag '${message.identifier}', give each element in a message its own tag unless they are identical`);
|
|
42
|
-
if ((message instanceof ArgumentMessage || message instanceof ChoiceMessage) && typeof message.identifier === "string") claim(values, message.identifier, message.expression, `Duplicate placeholder name '${message.identifier}', give each value in a message its own name unless they are identical`);
|
|
43
|
-
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
44
|
-
if (message instanceof ChoiceMessage) for (const branch of message.branches) walk(branch.value);
|
|
45
|
-
}
|
|
46
|
-
walk(message);
|
|
47
|
-
for (const tag of tags.keys()) if (values.has(tag)) throw new Error(`Element tag '${tag}' collides with an argument of the same name`);
|
|
48
|
-
return /* @__PURE__ */ new Set([...tags.keys(), ...values.keys()]);
|
|
49
|
-
}
|
|
50
|
-
//#endregion
|
|
51
2
|
//#region src/features/messages/types.ts
|
|
52
3
|
var Base = class {
|
|
53
4
|
toICUString() {
|
|
@@ -68,10 +19,12 @@ var LiteralMessage = class extends Base {
|
|
|
68
19
|
var ArgumentMessage = class extends Base {
|
|
69
20
|
identifier;
|
|
70
21
|
expression;
|
|
71
|
-
|
|
22
|
+
format;
|
|
23
|
+
constructor(identifier, expression, format) {
|
|
72
24
|
super();
|
|
73
25
|
this.identifier = identifier;
|
|
74
26
|
this.expression = expression;
|
|
27
|
+
this.format = format;
|
|
75
28
|
}
|
|
76
29
|
};
|
|
77
30
|
var ElementMessage = class extends Base {
|
|
@@ -90,12 +43,14 @@ var ChoiceMessage = class extends Base {
|
|
|
90
43
|
identifier;
|
|
91
44
|
branches;
|
|
92
45
|
expression;
|
|
93
|
-
|
|
46
|
+
offset;
|
|
47
|
+
constructor(kind, identifier, branches, expression, offset) {
|
|
94
48
|
super();
|
|
95
49
|
this.kind = kind;
|
|
96
50
|
this.identifier = identifier;
|
|
97
51
|
this.branches = branches;
|
|
98
52
|
this.expression = expression;
|
|
53
|
+
this.offset = offset;
|
|
99
54
|
}
|
|
100
55
|
};
|
|
101
56
|
var CompositeMessage = class extends Base {
|
|
@@ -116,12 +71,120 @@ var CompositeMessage = class extends Base {
|
|
|
116
71
|
}
|
|
117
72
|
};
|
|
118
73
|
//#endregion
|
|
74
|
+
//#region src/features/messages/identifier.ts
|
|
75
|
+
const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
|
|
76
|
+
/**
|
|
77
|
+
* An ICU case, either an exact value or a key. ICU reserves its own pattern
|
|
78
|
+
* syntax, so a key carries no punctuation and no whitespace.
|
|
79
|
+
*/
|
|
80
|
+
const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
|
|
81
|
+
/**
|
|
82
|
+
* The ICU case a branch is written as.
|
|
83
|
+
*
|
|
84
|
+
* Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
|
|
85
|
+
* so is distinct from the CLDR category that would otherwise match it. `select`
|
|
86
|
+
* has no such syntax — its cases are literal string matches, and `=0` there is
|
|
87
|
+
* a parse error — so a numeric key stays bare, where it matches both `0` and
|
|
88
|
+
* `'0'`.
|
|
89
|
+
*
|
|
90
|
+
* Digits are read literally rather than coerced, because everything JavaScript
|
|
91
|
+
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
92
|
+
* `=0`, which would pass for a key that selects zero and quietly leave the
|
|
93
|
+
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
94
|
+
* whitespace or punctuation that made it numeric-looking is caught.
|
|
95
|
+
*/
|
|
96
|
+
function getBranchCase(kind, identifier) {
|
|
97
|
+
const key = String(identifier);
|
|
98
|
+
if (kind === "select") return key;
|
|
99
|
+
return /^\d+$/u.test(key) ? `=${+key}` : key;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
103
|
+
* file and a line.
|
|
104
|
+
*
|
|
105
|
+
* A hyphenated string union is ordinary application code and typechecks,
|
|
106
|
+
* builds, and extracts to a catalogue entry that looks perfectly normal — the
|
|
107
|
+
* only sign of trouble is a parse error at format time, in a message whose
|
|
108
|
+
* source is long gone.
|
|
109
|
+
*/
|
|
110
|
+
function validateBranchIdentifier(kind, identifier) {
|
|
111
|
+
if (typeof identifier !== "string") return;
|
|
112
|
+
const branch = getBranchCase(kind, identifier);
|
|
113
|
+
if (!BRANCH_PATTERN.test(branch)) {
|
|
114
|
+
const suggestion = suggestBranchIdentifier(identifier);
|
|
115
|
+
throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
|
|
116
|
+
}
|
|
117
|
+
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)}'`);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The nearest identifier-safe form of a key, so the error names the fix as well
|
|
121
|
+
* as the problem. The constraint comes from ICU rather than from anything the
|
|
122
|
+
* author wrote, and camel case is how the rest of the codebase already spells a
|
|
123
|
+
* name of more than one word.
|
|
124
|
+
*/
|
|
125
|
+
function suggestBranchIdentifier(identifier) {
|
|
126
|
+
const suggestion = identifier.split(/[^\p{L}\p{N}]+/u).filter(Boolean).map((word, index) => index === 0 ? word : word[0].toUpperCase() + word.slice(1)).join("");
|
|
127
|
+
if (!BRANCH_PATTERN.test(suggestion) || !Number.isNaN(+suggestion)) return void 0;
|
|
128
|
+
return suggestion;
|
|
129
|
+
}
|
|
130
|
+
function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalent = () => false) {
|
|
131
|
+
const reserved = collectAssignedIdentifiers(message, equivalent);
|
|
132
|
+
function next() {
|
|
133
|
+
let identifier = `${sequence.current++}`;
|
|
134
|
+
while (reserved.has(identifier)) identifier = `${sequence.current++}`;
|
|
135
|
+
return identifier;
|
|
136
|
+
}
|
|
137
|
+
function walk(message) {
|
|
138
|
+
if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
|
|
139
|
+
if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
|
|
140
|
+
}
|
|
141
|
+
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
142
|
+
if (message instanceof ChoiceMessage) for (const branch of message.branches) {
|
|
143
|
+
if (branch.identifier === AUTO_INCREMENT_IDENTIFIER) branch.identifier = next();
|
|
144
|
+
walk(branch.value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
walk(message);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Collect the identifiers already assigned before this pass runs, so generated
|
|
151
|
+
* sequence numbers never shadow an explicit one (e.g. an element tagged `0`).
|
|
152
|
+
*
|
|
153
|
+
* A name is claimed by what produced it, not by the name alone. Two that differ
|
|
154
|
+
* each compile to their own prop, and a translator moving them around a sentence
|
|
155
|
+
* has to be able to tell them apart, so they are a build error. Repeats are fine
|
|
156
|
+
* when nothing distinguishes them: the same variable interpolated twice is one
|
|
157
|
+
* value, and two identical elements are one tag.
|
|
158
|
+
*/
|
|
159
|
+
function collectAssignedIdentifiers(message, equivalent) {
|
|
160
|
+
const tags = /* @__PURE__ */ new Map();
|
|
161
|
+
const values = /* @__PURE__ */ new Map();
|
|
162
|
+
function claim(claimed, identifier, expression, conflict) {
|
|
163
|
+
if (claimed.has(identifier) && !equivalent(claimed.get(identifier), expression)) throw new Error(conflict);
|
|
164
|
+
claimed.set(identifier, expression);
|
|
165
|
+
}
|
|
166
|
+
function walk(message) {
|
|
167
|
+
if (message instanceof ElementMessage && typeof message.identifier === "string") claim(tags, message.identifier, message.expression, `Duplicate element tag '${message.identifier}', give each element in a message its own tag unless they are identical`);
|
|
168
|
+
if ((message instanceof ArgumentMessage || message instanceof ChoiceMessage) && typeof message.identifier === "string") claim(values, message.identifier, message.expression, `Duplicate placeholder name '${message.identifier}', give each value in a message its own name unless they are identical`);
|
|
169
|
+
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
170
|
+
if (message instanceof ChoiceMessage) for (const branch of message.branches) walk(branch.value);
|
|
171
|
+
}
|
|
172
|
+
walk(message);
|
|
173
|
+
for (const tag of tags.keys()) if (values.has(tag)) throw new Error(`Element tag '${tag}' collides with an argument of the same name`);
|
|
174
|
+
return /* @__PURE__ */ new Set([...tags.keys(), ...values.keys()]);
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
119
177
|
//#region src/features/messages/convert.ts
|
|
120
178
|
function convertMessageToIcu(message) {
|
|
121
179
|
function internalConvertMessageToIcu(message) {
|
|
122
180
|
switch (true) {
|
|
123
181
|
case message instanceof LiteralMessage: return String(message.text);
|
|
124
|
-
case message instanceof ArgumentMessage:
|
|
182
|
+
case message instanceof ArgumentMessage: {
|
|
183
|
+
const parts = [String(message.identifier)];
|
|
184
|
+
if (message.format) parts.push(message.format.type);
|
|
185
|
+
if (message.format?.style) parts.push(message.format.style);
|
|
186
|
+
return `{${parts.join(", ")}}`;
|
|
187
|
+
}
|
|
125
188
|
case message instanceof ElementMessage: {
|
|
126
189
|
if (message.children.length === 0) return `<${String(message.identifier)}/>`;
|
|
127
190
|
const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
|
|
@@ -129,11 +192,12 @@ function convertMessageToIcu(message) {
|
|
|
129
192
|
}
|
|
130
193
|
case message instanceof ChoiceMessage: {
|
|
131
194
|
const branches = message.branches.map(({ identifier, value }) => ({
|
|
132
|
-
identifier:
|
|
195
|
+
identifier: getBranchCase(message.kind, identifier),
|
|
133
196
|
value: internalConvertMessageToIcu(value)
|
|
134
197
|
})).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
|
|
135
198
|
const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
|
|
136
|
-
|
|
199
|
+
const offset = message.offset === void 0 || message.kind === "select" ? "" : ` offset:${message.offset}`;
|
|
200
|
+
return `{${String(message.identifier)}, ${format},${offset}\n${branches}}`;
|
|
137
201
|
}
|
|
138
202
|
case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m)).join("");
|
|
139
203
|
default: throw new Error("Unknown message type", { cause: message });
|
|
@@ -142,4 +206,69 @@ function convertMessageToIcu(message) {
|
|
|
142
206
|
return internalConvertMessageToIcu(message).trim();
|
|
143
207
|
}
|
|
144
208
|
//#endregion
|
|
145
|
-
|
|
209
|
+
//#region src/features/messages/format.ts
|
|
210
|
+
/**
|
|
211
|
+
* The ICU argument types a macro can author, and the named styles each accepts.
|
|
212
|
+
*
|
|
213
|
+
* `currency` is deliberately absent from `number`. MF1 has nowhere to write the
|
|
214
|
+
* currency code — it comes from the formatter's configuration, not the message
|
|
215
|
+
* — so `{price, number, currency}` formats as a literal `{$price}` at runtime
|
|
216
|
+
* rather than an amount. Currency belongs to number skeletons, which the
|
|
217
|
+
* formatter does not accept yet either.
|
|
218
|
+
*
|
|
219
|
+
* `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
|
|
220
|
+
* oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
|
|
221
|
+
* equivalent, and the third is deprecated in ICU itself in favour of `plural`.
|
|
222
|
+
*/
|
|
223
|
+
const ARGUMENT_STYLES = {
|
|
224
|
+
number: ["integer", "percent"],
|
|
225
|
+
date: [
|
|
226
|
+
"short",
|
|
227
|
+
"medium",
|
|
228
|
+
"long",
|
|
229
|
+
"full"
|
|
230
|
+
],
|
|
231
|
+
time: [
|
|
232
|
+
"short",
|
|
233
|
+
"medium",
|
|
234
|
+
"long",
|
|
235
|
+
"full"
|
|
236
|
+
]
|
|
237
|
+
};
|
|
238
|
+
const ARGUMENT_TYPES = Object.keys(ARGUMENT_STYLES);
|
|
239
|
+
function isArgumentType(kind) {
|
|
240
|
+
return Object.hasOwn(ARGUMENT_STYLES, kind);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* A literal `NumberFormat` pattern, e.g. `#,##0.00`.
|
|
244
|
+
*
|
|
245
|
+
* A pattern has to carry a digit placeholder — `#` or `0` — because that is
|
|
246
|
+
* what makes it a pattern rather than a word. Without that requirement any
|
|
247
|
+
* brace-free string qualifies, which quietly readmits the named styles this
|
|
248
|
+
* module exists to reject: `currency` would sail through as a "pattern" and
|
|
249
|
+
* extract to the `{price, number, currency}` the formatter cannot honour, and
|
|
250
|
+
* so would a plain typo.
|
|
251
|
+
*
|
|
252
|
+
* Braces are excluded separately: ICU reserves them for its own pattern syntax,
|
|
253
|
+
* so a style carrying one would close the argument early and take the rest of
|
|
254
|
+
* the message with it.
|
|
255
|
+
*/
|
|
256
|
+
const LITERAL_STYLE_PATTERN = /^[^{}\r\n]*[#0][^{}\r\n]*$/;
|
|
257
|
+
/**
|
|
258
|
+
* Reject an argument style the formatter cannot honour, while the style is
|
|
259
|
+
* still attached to a file and a line.
|
|
260
|
+
*
|
|
261
|
+
* A style is a bare string in the source and a bare string in the catalogue, so
|
|
262
|
+
* nothing between here and the runtime has an opinion about it. Left unchecked,
|
|
263
|
+
* a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
|
|
264
|
+
* perfectly normal and only misformats once it reaches a user.
|
|
265
|
+
*/
|
|
266
|
+
function validateArgumentStyle(type, style) {
|
|
267
|
+
const named = ARGUMENT_STYLES[type];
|
|
268
|
+
if (named.includes(style)) return;
|
|
269
|
+
if (type === "number" && LITERAL_STYLE_PATTERN.test(style)) return;
|
|
270
|
+
const expected = named.map((s) => `'${s}'`).join(", ");
|
|
271
|
+
throw new Error(`Invalid ${type} style '${style}', expected ${expected}` + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, 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 };
|