@saykit/config 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -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() {
|
|
@@ -117,6 +68,103 @@ var CompositeMessage = class extends Base {
|
|
|
117
68
|
}
|
|
118
69
|
};
|
|
119
70
|
//#endregion
|
|
71
|
+
//#region src/features/messages/identifier.ts
|
|
72
|
+
const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
|
|
73
|
+
/**
|
|
74
|
+
* An ICU case, either an exact value or a key. ICU reserves its own pattern
|
|
75
|
+
* syntax, so a key carries no punctuation and no whitespace.
|
|
76
|
+
*/
|
|
77
|
+
const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
|
|
78
|
+
/**
|
|
79
|
+
* The ICU case a branch is written as. A number names an exact value rather
|
|
80
|
+
* than a key, and only `plural` and `ordinal` are allowed to select on one.
|
|
81
|
+
*
|
|
82
|
+
* Digits are read literally rather than coerced, because everything JavaScript
|
|
83
|
+
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
84
|
+
* `=0`, which would pass for a key that selects zero and quietly leave the
|
|
85
|
+
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
86
|
+
* whitespace or punctuation that made it numeric-looking is caught.
|
|
87
|
+
*/
|
|
88
|
+
function getBranchCase(identifier) {
|
|
89
|
+
const key = String(identifier);
|
|
90
|
+
return /^\d+$/u.test(key) ? `=${+key}` : key;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
94
|
+
* file and a line.
|
|
95
|
+
*
|
|
96
|
+
* A hyphenated string union is ordinary application code and typechecks,
|
|
97
|
+
* builds, and extracts to a catalogue entry that looks perfectly normal — the
|
|
98
|
+
* only sign of trouble is a parse error at format time, in a message whose
|
|
99
|
+
* source is long gone.
|
|
100
|
+
*/
|
|
101
|
+
function validateBranchIdentifier(kind, identifier) {
|
|
102
|
+
if (typeof identifier !== "string") return;
|
|
103
|
+
const branch = getBranchCase(identifier);
|
|
104
|
+
if (!BRANCH_PATTERN.test(branch)) {
|
|
105
|
+
const suggestion = suggestBranchIdentifier(identifier);
|
|
106
|
+
throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
|
|
107
|
+
}
|
|
108
|
+
if (kind === "select" && branch.startsWith("=")) throw new Error(`Invalid select branch key '${identifier}', a number selects an exact value, which only 'plural' and 'ordinal' accept`);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The nearest identifier-safe form of a key, so the error names the fix as well
|
|
112
|
+
* as the problem. The constraint comes from ICU rather than from anything the
|
|
113
|
+
* author wrote, and camel case is how the rest of the codebase already spells a
|
|
114
|
+
* name of more than one word.
|
|
115
|
+
*/
|
|
116
|
+
function suggestBranchIdentifier(identifier) {
|
|
117
|
+
const suggestion = identifier.split(/[^\p{L}\p{N}]+/u).filter(Boolean).map((word, index) => index === 0 ? word : word[0].toUpperCase() + word.slice(1)).join("");
|
|
118
|
+
if (!BRANCH_PATTERN.test(suggestion) || !Number.isNaN(+suggestion)) return void 0;
|
|
119
|
+
return suggestion;
|
|
120
|
+
}
|
|
121
|
+
function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalent = () => false) {
|
|
122
|
+
const reserved = collectAssignedIdentifiers(message, equivalent);
|
|
123
|
+
function next() {
|
|
124
|
+
let identifier = `${sequence.current++}`;
|
|
125
|
+
while (reserved.has(identifier)) identifier = `${sequence.current++}`;
|
|
126
|
+
return identifier;
|
|
127
|
+
}
|
|
128
|
+
function walk(message) {
|
|
129
|
+
if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
|
|
130
|
+
if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
|
|
131
|
+
}
|
|
132
|
+
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
133
|
+
if (message instanceof ChoiceMessage) for (const branch of message.branches) {
|
|
134
|
+
if (branch.identifier === AUTO_INCREMENT_IDENTIFIER) branch.identifier = next();
|
|
135
|
+
walk(branch.value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
walk(message);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Collect the identifiers already assigned before this pass runs, so generated
|
|
142
|
+
* sequence numbers never shadow an explicit one (e.g. an element tagged `0`).
|
|
143
|
+
*
|
|
144
|
+
* A name is claimed by what produced it, not by the name alone. Two that differ
|
|
145
|
+
* each compile to their own prop, and a translator moving them around a sentence
|
|
146
|
+
* has to be able to tell them apart, so they are a build error. Repeats are fine
|
|
147
|
+
* when nothing distinguishes them: the same variable interpolated twice is one
|
|
148
|
+
* value, and two identical elements are one tag.
|
|
149
|
+
*/
|
|
150
|
+
function collectAssignedIdentifiers(message, equivalent) {
|
|
151
|
+
const tags = /* @__PURE__ */ new Map();
|
|
152
|
+
const values = /* @__PURE__ */ new Map();
|
|
153
|
+
function claim(claimed, identifier, expression, conflict) {
|
|
154
|
+
if (claimed.has(identifier) && !equivalent(claimed.get(identifier), expression)) throw new Error(conflict);
|
|
155
|
+
claimed.set(identifier, expression);
|
|
156
|
+
}
|
|
157
|
+
function walk(message) {
|
|
158
|
+
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`);
|
|
159
|
+
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`);
|
|
160
|
+
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
161
|
+
if (message instanceof ChoiceMessage) for (const branch of message.branches) walk(branch.value);
|
|
162
|
+
}
|
|
163
|
+
walk(message);
|
|
164
|
+
for (const tag of tags.keys()) if (values.has(tag)) throw new Error(`Element tag '${tag}' collides with an argument of the same name`);
|
|
165
|
+
return /* @__PURE__ */ new Set([...tags.keys(), ...values.keys()]);
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
120
168
|
//#region src/features/messages/convert.ts
|
|
121
169
|
function convertMessageToIcu(message) {
|
|
122
170
|
function internalConvertMessageToIcu(message) {
|
|
@@ -130,7 +178,7 @@ function convertMessageToIcu(message) {
|
|
|
130
178
|
}
|
|
131
179
|
case message instanceof ChoiceMessage: {
|
|
132
180
|
const branches = message.branches.map(({ identifier, value }) => ({
|
|
133
|
-
identifier:
|
|
181
|
+
identifier: getBranchCase(identifier),
|
|
134
182
|
value: internalConvertMessageToIcu(value)
|
|
135
183
|
})).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
|
|
136
184
|
const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
|
|
@@ -152,3 +200,5 @@ exports.LiteralMessage = LiteralMessage;
|
|
|
152
200
|
exports.assignSequenceIdentifiers = assignSequenceIdentifiers;
|
|
153
201
|
exports.convertMessageToIcu = convertMessageToIcu;
|
|
154
202
|
exports.generateHash = require_hash.generateHash;
|
|
203
|
+
exports.getBranchCase = getBranchCase;
|
|
204
|
+
exports.validateBranchIdentifier = validateBranchIdentifier;
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
//#region src/features/messages/identifier.d.ts
|
|
2
2
|
declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
|
|
3
|
+
/**
|
|
4
|
+
* The ICU case a branch is written as. A number names an exact value rather
|
|
5
|
+
* than a key, and only `plural` and `ordinal` are allowed to select on one.
|
|
6
|
+
*
|
|
7
|
+
* Digits are read literally rather than coerced, because everything JavaScript
|
|
8
|
+
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
9
|
+
* `=0`, which would pass for a key that selects zero and quietly leave the
|
|
10
|
+
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
11
|
+
* whitespace or punctuation that made it numeric-looking is caught.
|
|
12
|
+
*/
|
|
13
|
+
declare function getBranchCase(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
|
|
14
|
+
/**
|
|
15
|
+
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
16
|
+
* file and a line.
|
|
17
|
+
*
|
|
18
|
+
* A hyphenated string union is ordinary application code and typechecks,
|
|
19
|
+
* builds, and extracts to a catalogue entry that looks perfectly normal — the
|
|
20
|
+
* only sign of trouble is a parse error at format time, in a message whose
|
|
21
|
+
* source is long gone.
|
|
22
|
+
*/
|
|
23
|
+
declare function validateBranchIdentifier(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): void;
|
|
3
24
|
/**
|
|
4
25
|
* Decides whether two placeholders sharing a name are the same placeholder, and
|
|
5
26
|
* so may share it. Only the syntax that produced them can answer that, so the
|
|
@@ -66,4 +87,4 @@ declare function convertMessageToIcu(message: Message): string;
|
|
|
66
87
|
//#region src/features/messages/hash.d.ts
|
|
67
88
|
declare function generateHash(input: string, context?: string): string;
|
|
68
89
|
//#endregion
|
|
69
|
-
export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash };
|
|
90
|
+
export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
//#region src/features/messages/identifier.d.ts
|
|
2
2
|
declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
|
|
3
|
+
/**
|
|
4
|
+
* The ICU case a branch is written as. A number names an exact value rather
|
|
5
|
+
* than a key, and only `plural` and `ordinal` are allowed to select on one.
|
|
6
|
+
*
|
|
7
|
+
* Digits are read literally rather than coerced, because everything JavaScript
|
|
8
|
+
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
9
|
+
* `=0`, which would pass for a key that selects zero and quietly leave the
|
|
10
|
+
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
11
|
+
* whitespace or punctuation that made it numeric-looking is caught.
|
|
12
|
+
*/
|
|
13
|
+
declare function getBranchCase(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
|
|
14
|
+
/**
|
|
15
|
+
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
16
|
+
* file and a line.
|
|
17
|
+
*
|
|
18
|
+
* A hyphenated string union is ordinary application code and typechecks,
|
|
19
|
+
* builds, and extracts to a catalogue entry that looks perfectly normal — the
|
|
20
|
+
* only sign of trouble is a parse error at format time, in a message whose
|
|
21
|
+
* source is long gone.
|
|
22
|
+
*/
|
|
23
|
+
declare function validateBranchIdentifier(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): void;
|
|
3
24
|
/**
|
|
4
25
|
* Decides whether two placeholders sharing a name are the same placeholder, and
|
|
5
26
|
* so may share it. Only the syntax that produced them can answer that, so the
|
|
@@ -66,4 +87,4 @@ declare function convertMessageToIcu(message: Message): string;
|
|
|
66
87
|
//#region src/features/messages/hash.d.ts
|
|
67
88
|
declare function generateHash(input: string, context?: string): string;
|
|
68
89
|
//#endregion
|
|
69
|
-
export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash };
|
|
90
|
+
export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, 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() {
|
|
@@ -116,6 +67,103 @@ var CompositeMessage = class extends Base {
|
|
|
116
67
|
}
|
|
117
68
|
};
|
|
118
69
|
//#endregion
|
|
70
|
+
//#region src/features/messages/identifier.ts
|
|
71
|
+
const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
|
|
72
|
+
/**
|
|
73
|
+
* An ICU case, either an exact value or a key. ICU reserves its own pattern
|
|
74
|
+
* syntax, so a key carries no punctuation and no whitespace.
|
|
75
|
+
*/
|
|
76
|
+
const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
|
|
77
|
+
/**
|
|
78
|
+
* The ICU case a branch is written as. A number names an exact value rather
|
|
79
|
+
* than a key, and only `plural` and `ordinal` are allowed to select on one.
|
|
80
|
+
*
|
|
81
|
+
* Digits are read literally rather than coerced, because everything JavaScript
|
|
82
|
+
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
83
|
+
* `=0`, which would pass for a key that selects zero and quietly leave the
|
|
84
|
+
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
85
|
+
* whitespace or punctuation that made it numeric-looking is caught.
|
|
86
|
+
*/
|
|
87
|
+
function getBranchCase(identifier) {
|
|
88
|
+
const key = String(identifier);
|
|
89
|
+
return /^\d+$/u.test(key) ? `=${+key}` : key;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
93
|
+
* file and a line.
|
|
94
|
+
*
|
|
95
|
+
* A hyphenated string union is ordinary application code and typechecks,
|
|
96
|
+
* builds, and extracts to a catalogue entry that looks perfectly normal — the
|
|
97
|
+
* only sign of trouble is a parse error at format time, in a message whose
|
|
98
|
+
* source is long gone.
|
|
99
|
+
*/
|
|
100
|
+
function validateBranchIdentifier(kind, identifier) {
|
|
101
|
+
if (typeof identifier !== "string") return;
|
|
102
|
+
const branch = getBranchCase(identifier);
|
|
103
|
+
if (!BRANCH_PATTERN.test(branch)) {
|
|
104
|
+
const suggestion = suggestBranchIdentifier(identifier);
|
|
105
|
+
throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
|
|
106
|
+
}
|
|
107
|
+
if (kind === "select" && branch.startsWith("=")) throw new Error(`Invalid select branch key '${identifier}', a number selects an exact value, which only 'plural' and 'ordinal' accept`);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The nearest identifier-safe form of a key, so the error names the fix as well
|
|
111
|
+
* as the problem. The constraint comes from ICU rather than from anything the
|
|
112
|
+
* author wrote, and camel case is how the rest of the codebase already spells a
|
|
113
|
+
* name of more than one word.
|
|
114
|
+
*/
|
|
115
|
+
function suggestBranchIdentifier(identifier) {
|
|
116
|
+
const suggestion = identifier.split(/[^\p{L}\p{N}]+/u).filter(Boolean).map((word, index) => index === 0 ? word : word[0].toUpperCase() + word.slice(1)).join("");
|
|
117
|
+
if (!BRANCH_PATTERN.test(suggestion) || !Number.isNaN(+suggestion)) return void 0;
|
|
118
|
+
return suggestion;
|
|
119
|
+
}
|
|
120
|
+
function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalent = () => false) {
|
|
121
|
+
const reserved = collectAssignedIdentifiers(message, equivalent);
|
|
122
|
+
function next() {
|
|
123
|
+
let identifier = `${sequence.current++}`;
|
|
124
|
+
while (reserved.has(identifier)) identifier = `${sequence.current++}`;
|
|
125
|
+
return identifier;
|
|
126
|
+
}
|
|
127
|
+
function walk(message) {
|
|
128
|
+
if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
|
|
129
|
+
if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
|
|
130
|
+
}
|
|
131
|
+
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
132
|
+
if (message instanceof ChoiceMessage) for (const branch of message.branches) {
|
|
133
|
+
if (branch.identifier === AUTO_INCREMENT_IDENTIFIER) branch.identifier = next();
|
|
134
|
+
walk(branch.value);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
walk(message);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Collect the identifiers already assigned before this pass runs, so generated
|
|
141
|
+
* sequence numbers never shadow an explicit one (e.g. an element tagged `0`).
|
|
142
|
+
*
|
|
143
|
+
* A name is claimed by what produced it, not by the name alone. Two that differ
|
|
144
|
+
* each compile to their own prop, and a translator moving them around a sentence
|
|
145
|
+
* has to be able to tell them apart, so they are a build error. Repeats are fine
|
|
146
|
+
* when nothing distinguishes them: the same variable interpolated twice is one
|
|
147
|
+
* value, and two identical elements are one tag.
|
|
148
|
+
*/
|
|
149
|
+
function collectAssignedIdentifiers(message, equivalent) {
|
|
150
|
+
const tags = /* @__PURE__ */ new Map();
|
|
151
|
+
const values = /* @__PURE__ */ new Map();
|
|
152
|
+
function claim(claimed, identifier, expression, conflict) {
|
|
153
|
+
if (claimed.has(identifier) && !equivalent(claimed.get(identifier), expression)) throw new Error(conflict);
|
|
154
|
+
claimed.set(identifier, expression);
|
|
155
|
+
}
|
|
156
|
+
function walk(message) {
|
|
157
|
+
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`);
|
|
158
|
+
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`);
|
|
159
|
+
if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
|
|
160
|
+
if (message instanceof ChoiceMessage) for (const branch of message.branches) walk(branch.value);
|
|
161
|
+
}
|
|
162
|
+
walk(message);
|
|
163
|
+
for (const tag of tags.keys()) if (values.has(tag)) throw new Error(`Element tag '${tag}' collides with an argument of the same name`);
|
|
164
|
+
return /* @__PURE__ */ new Set([...tags.keys(), ...values.keys()]);
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
119
167
|
//#region src/features/messages/convert.ts
|
|
120
168
|
function convertMessageToIcu(message) {
|
|
121
169
|
function internalConvertMessageToIcu(message) {
|
|
@@ -129,7 +177,7 @@ function convertMessageToIcu(message) {
|
|
|
129
177
|
}
|
|
130
178
|
case message instanceof ChoiceMessage: {
|
|
131
179
|
const branches = message.branches.map(({ identifier, value }) => ({
|
|
132
|
-
identifier:
|
|
180
|
+
identifier: getBranchCase(identifier),
|
|
133
181
|
value: internalConvertMessageToIcu(value)
|
|
134
182
|
})).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
|
|
135
183
|
const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
|
|
@@ -142,4 +190,4 @@ function convertMessageToIcu(message) {
|
|
|
142
190
|
return internalConvertMessageToIcu(message).trim();
|
|
143
191
|
}
|
|
144
192
|
//#endregion
|
|
145
|
-
export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, generateHash };
|
|
193
|
+
export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
|