@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.
@@ -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-A1uS7qkK.cjs");
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");
@@ -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-DLyuZzF2.mjs";
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-A1uS7qkK.cjs");
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-DLyuZzF2.mjs";
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
- constructor(identifier, expression) {
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
- constructor(kind, identifier, branches, expression) {
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: return `{${String(message.identifier)}}`;
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: Number.isNaN(+String(identifier)) ? String(identifier) : `=${+String(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
- return `{${String(message.identifier)}, ${format},\n${branches}}`;
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
- constructor(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, expression: any);
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
- constructor(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, expression: any);
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
- constructor(identifier, expression) {
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
- constructor(kind, identifier, branches, expression) {
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: return `{${String(message.identifier)}}`;
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: Number.isNaN(+String(identifier)) ? String(identifier) : `=${+String(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
- return `{${String(message.identifier)}, ${format},\n${branches}}`;
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
- export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, generateHash };
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
- function resolveConfig(name = "saykit") {
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
- const ext = (0, node_path.extname)(file.id).toLowerCase();
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(file.id);
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
- function resolveConfig(name = "saykit") {
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
- const ext = extname(file.id).toLowerCase();
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(file.id);
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saykit/config",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "CLI and configuration tooling for saykit",
5
5
  "keywords": [
6
6
  "cli",