@saykit/config 0.6.1 → 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 };
@@ -20,10 +20,12 @@ var LiteralMessage = class extends Base {
20
20
  var ArgumentMessage = class extends Base {
21
21
  identifier;
22
22
  expression;
23
- constructor(identifier, expression) {
23
+ format;
24
+ constructor(identifier, expression, format) {
24
25
  super();
25
26
  this.identifier = identifier;
26
27
  this.expression = expression;
28
+ this.format = format;
27
29
  }
28
30
  };
29
31
  var ElementMessage = class extends Base {
@@ -42,12 +44,14 @@ var ChoiceMessage = class extends Base {
42
44
  identifier;
43
45
  branches;
44
46
  expression;
45
- constructor(kind, identifier, branches, expression) {
47
+ offset;
48
+ constructor(kind, identifier, branches, expression, offset) {
46
49
  super();
47
50
  this.kind = kind;
48
51
  this.identifier = identifier;
49
52
  this.branches = branches;
50
53
  this.expression = expression;
54
+ this.offset = offset;
51
55
  }
52
56
  };
53
57
  var CompositeMessage = class extends Base {
@@ -76,8 +80,13 @@ const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
76
80
  */
77
81
  const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
78
82
  /**
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.
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'`.
81
90
  *
82
91
  * Digits are read literally rather than coerced, because everything JavaScript
83
92
  * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
@@ -85,8 +94,9 @@ const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$
85
94
  * author's own key out of the catalogue. Anything else stays a key, where the
86
95
  * whitespace or punctuation that made it numeric-looking is caught.
87
96
  */
88
- function getBranchCase(identifier) {
97
+ function getBranchCase(kind, identifier) {
89
98
  const key = String(identifier);
99
+ if (kind === "select") return key;
90
100
  return /^\d+$/u.test(key) ? `=${+key}` : key;
91
101
  }
92
102
  /**
@@ -100,12 +110,12 @@ function getBranchCase(identifier) {
100
110
  */
101
111
  function validateBranchIdentifier(kind, identifier) {
102
112
  if (typeof identifier !== "string") return;
103
- const branch = getBranchCase(identifier);
113
+ const branch = getBranchCase(kind, identifier);
104
114
  if (!BRANCH_PATTERN.test(branch)) {
105
115
  const suggestion = suggestBranchIdentifier(identifier);
106
116
  throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
107
117
  }
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`);
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)}'`);
109
119
  }
110
120
  /**
111
121
  * The nearest identifier-safe form of a key, so the error names the fix as well
@@ -170,7 +180,12 @@ function convertMessageToIcu(message) {
170
180
  function internalConvertMessageToIcu(message) {
171
181
  switch (true) {
172
182
  case message instanceof LiteralMessage: return String(message.text);
173
- 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
+ }
174
189
  case message instanceof ElementMessage: {
175
190
  if (message.children.length === 0) return `<${String(message.identifier)}/>`;
176
191
  const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
@@ -178,11 +193,12 @@ function convertMessageToIcu(message) {
178
193
  }
179
194
  case message instanceof ChoiceMessage: {
180
195
  const branches = message.branches.map(({ identifier, value }) => ({
181
- identifier: getBranchCase(identifier),
196
+ identifier: getBranchCase(message.kind, identifier),
182
197
  value: internalConvertMessageToIcu(value)
183
198
  })).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
184
199
  const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
185
- 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}}`;
186
202
  }
187
203
  case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m)).join("");
188
204
  default: throw new Error("Unknown message type", { cause: message });
@@ -191,6 +207,73 @@ function convertMessageToIcu(message) {
191
207
  return internalConvertMessageToIcu(message).trim();
192
208
  }
193
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;
194
277
  exports.AUTO_INCREMENT_IDENTIFIER = AUTO_INCREMENT_IDENTIFIER;
195
278
  exports.ArgumentMessage = ArgumentMessage;
196
279
  exports.ChoiceMessage = ChoiceMessage;
@@ -201,4 +284,6 @@ exports.assignSequenceIdentifiers = assignSequenceIdentifiers;
201
284
  exports.convertMessageToIcu = convertMessageToIcu;
202
285
  exports.generateHash = require_hash.generateHash;
203
286
  exports.getBranchCase = getBranchCase;
287
+ exports.isArgumentType = isArgumentType;
288
+ exports.validateArgumentStyle = validateArgumentStyle;
204
289
  exports.validateBranchIdentifier = validateBranchIdentifier;
@@ -1,8 +1,46 @@
1
+ //#region src/features/messages/format.d.ts
2
+ /**
3
+ * The ICU argument types a macro can author, and the named styles each accepts.
4
+ *
5
+ * `currency` is deliberately absent from `number`. MF1 has nowhere to write the
6
+ * currency code — it comes from the formatter's configuration, not the message
7
+ * — so `{price, number, currency}` formats as a literal `{$price}` at runtime
8
+ * rather than an amount. Currency belongs to number skeletons, which the
9
+ * formatter does not accept yet either.
10
+ *
11
+ * `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
12
+ * oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
13
+ * equivalent, and the third is deprecated in ICU itself in favour of `plural`.
14
+ */
15
+ declare const ARGUMENT_STYLES: {
16
+ readonly number: readonly ["integer", "percent"];
17
+ readonly date: readonly ["short", "medium", "long", "full"];
18
+ readonly time: readonly ["short", "medium", "long", "full"];
19
+ };
20
+ type ArgumentType = keyof typeof ARGUMENT_STYLES;
21
+ declare const ARGUMENT_TYPES: ArgumentType[];
22
+ declare function isArgumentType(kind: string): kind is ArgumentType;
23
+ /**
24
+ * Reject an argument style the formatter cannot honour, while the style is
25
+ * still attached to a file and a line.
26
+ *
27
+ * A style is a bare string in the source and a bare string in the catalogue, so
28
+ * nothing between here and the runtime has an opinion about it. Left unchecked,
29
+ * a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
30
+ * perfectly normal and only misformats once it reaches a user.
31
+ */
32
+ declare function validateArgumentStyle(type: ArgumentType, style: string): void;
33
+ //#endregion
1
34
  //#region src/features/messages/identifier.d.ts
2
35
  declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
3
36
  /**
4
- * The ICU case a branch is written as. A number names an exact value rather
5
- * than a key, and only `plural` and `ordinal` are allowed to select on one.
37
+ * The ICU case a branch is written as.
38
+ *
39
+ * Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
40
+ * so is distinct from the CLDR category that would otherwise match it. `select`
41
+ * has no such syntax — its cases are literal string matches, and `=0` there is
42
+ * a parse error — so a numeric key stays bare, where it matches both `0` and
43
+ * `'0'`.
6
44
  *
7
45
  * Digits are read literally rather than coerced, because everything JavaScript
8
46
  * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
@@ -10,7 +48,7 @@ declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
10
48
  * author's own key out of the catalogue. Anything else stays a key, where the
11
49
  * whitespace or punctuation that made it numeric-looking is caught.
12
50
  */
13
- declare function getBranchCase(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
51
+ declare function getBranchCase(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
14
52
  /**
15
53
  * Reject a branch key ICU cannot express, while the key is still attached to a
16
54
  * file and a line.
@@ -40,10 +78,23 @@ declare class LiteralMessage extends Base {
40
78
  readonly text: string;
41
79
  constructor(text: string);
42
80
  }
81
+ /**
82
+ * An ICU argument type and style, e.g. `{n, number, percent}`. A style is
83
+ * optional — `{n, number}` is the type's default formatting.
84
+ *
85
+ * Both are kept as the ICU strings they are written as, rather than as `Intl`
86
+ * options, because the catalogue is the source of truth and only the ICU
87
+ * spelling round-trips back out of it.
88
+ */
89
+ interface ArgumentFormat {
90
+ type: ArgumentType;
91
+ style?: string;
92
+ }
43
93
  declare class ArgumentMessage extends Base {
44
94
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
45
95
  readonly expression: any;
46
- 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);
47
98
  }
48
99
  declare class ElementMessage extends Base {
49
100
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
@@ -59,10 +110,22 @@ declare class ChoiceMessage extends Base {
59
110
  readonly value: Message;
60
111
  }[];
61
112
  readonly expression: any;
113
+ /**
114
+ * Subtracted from the selector before `#` is formatted, so "You and 2
115
+ * others" can select on a total of three. Only `plural` and `ordinal`
116
+ * accept one; `select` has no number to offset.
117
+ */
118
+ readonly offset?: number | undefined;
62
119
  constructor(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, branches: {
63
120
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
64
121
  readonly value: Message;
65
- }[], expression: any);
122
+ }[], expression: any,
123
+ /**
124
+ * Subtracted from the selector before `#` is formatted, so "You and 2
125
+ * others" can select on a total of three. Only `plural` and `ordinal`
126
+ * accept one; `select` has no number to offset.
127
+ */
128
+ offset?: number | undefined);
66
129
  }
67
130
  declare class CompositeMessage extends Base {
68
131
  readonly descriptor: {
@@ -87,4 +150,4 @@ declare function convertMessageToIcu(message: Message): string;
87
150
  //#region src/features/messages/hash.d.ts
88
151
  declare function generateHash(input: string, context?: string): string;
89
152
  //#endregion
90
- export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
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,8 +1,46 @@
1
+ //#region src/features/messages/format.d.ts
2
+ /**
3
+ * The ICU argument types a macro can author, and the named styles each accepts.
4
+ *
5
+ * `currency` is deliberately absent from `number`. MF1 has nowhere to write the
6
+ * currency code — it comes from the formatter's configuration, not the message
7
+ * — so `{price, number, currency}` formats as a literal `{$price}` at runtime
8
+ * rather than an amount. Currency belongs to number skeletons, which the
9
+ * formatter does not accept yet either.
10
+ *
11
+ * `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
12
+ * oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
13
+ * equivalent, and the third is deprecated in ICU itself in favour of `plural`.
14
+ */
15
+ declare const ARGUMENT_STYLES: {
16
+ readonly number: readonly ["integer", "percent"];
17
+ readonly date: readonly ["short", "medium", "long", "full"];
18
+ readonly time: readonly ["short", "medium", "long", "full"];
19
+ };
20
+ type ArgumentType = keyof typeof ARGUMENT_STYLES;
21
+ declare const ARGUMENT_TYPES: ArgumentType[];
22
+ declare function isArgumentType(kind: string): kind is ArgumentType;
23
+ /**
24
+ * Reject an argument style the formatter cannot honour, while the style is
25
+ * still attached to a file and a line.
26
+ *
27
+ * A style is a bare string in the source and a bare string in the catalogue, so
28
+ * nothing between here and the runtime has an opinion about it. Left unchecked,
29
+ * a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
30
+ * perfectly normal and only misformats once it reaches a user.
31
+ */
32
+ declare function validateArgumentStyle(type: ArgumentType, style: string): void;
33
+ //#endregion
1
34
  //#region src/features/messages/identifier.d.ts
2
35
  declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
3
36
  /**
4
- * The ICU case a branch is written as. A number names an exact value rather
5
- * than a key, and only `plural` and `ordinal` are allowed to select on one.
37
+ * The ICU case a branch is written as.
38
+ *
39
+ * Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
40
+ * so is distinct from the CLDR category that would otherwise match it. `select`
41
+ * has no such syntax — its cases are literal string matches, and `=0` there is
42
+ * a parse error — so a numeric key stays bare, where it matches both `0` and
43
+ * `'0'`.
6
44
  *
7
45
  * Digits are read literally rather than coerced, because everything JavaScript
8
46
  * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
@@ -10,7 +48,7 @@ declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
10
48
  * author's own key out of the catalogue. Anything else stays a key, where the
11
49
  * whitespace or punctuation that made it numeric-looking is caught.
12
50
  */
13
- declare function getBranchCase(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
51
+ declare function getBranchCase(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
14
52
  /**
15
53
  * Reject a branch key ICU cannot express, while the key is still attached to a
16
54
  * file and a line.
@@ -40,10 +78,23 @@ declare class LiteralMessage extends Base {
40
78
  readonly text: string;
41
79
  constructor(text: string);
42
80
  }
81
+ /**
82
+ * An ICU argument type and style, e.g. `{n, number, percent}`. A style is
83
+ * optional — `{n, number}` is the type's default formatting.
84
+ *
85
+ * Both are kept as the ICU strings they are written as, rather than as `Intl`
86
+ * options, because the catalogue is the source of truth and only the ICU
87
+ * spelling round-trips back out of it.
88
+ */
89
+ interface ArgumentFormat {
90
+ type: ArgumentType;
91
+ style?: string;
92
+ }
43
93
  declare class ArgumentMessage extends Base {
44
94
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
45
95
  readonly expression: any;
46
- 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);
47
98
  }
48
99
  declare class ElementMessage extends Base {
49
100
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
@@ -59,10 +110,22 @@ declare class ChoiceMessage extends Base {
59
110
  readonly value: Message;
60
111
  }[];
61
112
  readonly expression: any;
113
+ /**
114
+ * Subtracted from the selector before `#` is formatted, so "You and 2
115
+ * others" can select on a total of three. Only `plural` and `ordinal`
116
+ * accept one; `select` has no number to offset.
117
+ */
118
+ readonly offset?: number | undefined;
62
119
  constructor(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, branches: {
63
120
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
64
121
  readonly value: Message;
65
- }[], expression: any);
122
+ }[], expression: any,
123
+ /**
124
+ * Subtracted from the selector before `#` is formatted, so "You and 2
125
+ * others" can select on a total of three. Only `plural` and `ordinal`
126
+ * accept one; `select` has no number to offset.
127
+ */
128
+ offset?: number | undefined);
66
129
  }
67
130
  declare class CompositeMessage extends Base {
68
131
  readonly descriptor: {
@@ -87,4 +150,4 @@ declare function convertMessageToIcu(message: Message): string;
87
150
  //#region src/features/messages/hash.d.ts
88
151
  declare function generateHash(input: string, context?: string): string;
89
152
  //#endregion
90
- export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
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 };
@@ -19,10 +19,12 @@ var LiteralMessage = class extends Base {
19
19
  var ArgumentMessage = class extends Base {
20
20
  identifier;
21
21
  expression;
22
- constructor(identifier, expression) {
22
+ format;
23
+ constructor(identifier, expression, format) {
23
24
  super();
24
25
  this.identifier = identifier;
25
26
  this.expression = expression;
27
+ this.format = format;
26
28
  }
27
29
  };
28
30
  var ElementMessage = class extends Base {
@@ -41,12 +43,14 @@ var ChoiceMessage = class extends Base {
41
43
  identifier;
42
44
  branches;
43
45
  expression;
44
- constructor(kind, identifier, branches, expression) {
46
+ offset;
47
+ constructor(kind, identifier, branches, expression, offset) {
45
48
  super();
46
49
  this.kind = kind;
47
50
  this.identifier = identifier;
48
51
  this.branches = branches;
49
52
  this.expression = expression;
53
+ this.offset = offset;
50
54
  }
51
55
  };
52
56
  var CompositeMessage = class extends Base {
@@ -75,8 +79,13 @@ const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
75
79
  */
76
80
  const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
77
81
  /**
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.
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'`.
80
89
  *
81
90
  * Digits are read literally rather than coerced, because everything JavaScript
82
91
  * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
@@ -84,8 +93,9 @@ const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$
84
93
  * author's own key out of the catalogue. Anything else stays a key, where the
85
94
  * whitespace or punctuation that made it numeric-looking is caught.
86
95
  */
87
- function getBranchCase(identifier) {
96
+ function getBranchCase(kind, identifier) {
88
97
  const key = String(identifier);
98
+ if (kind === "select") return key;
89
99
  return /^\d+$/u.test(key) ? `=${+key}` : key;
90
100
  }
91
101
  /**
@@ -99,12 +109,12 @@ function getBranchCase(identifier) {
99
109
  */
100
110
  function validateBranchIdentifier(kind, identifier) {
101
111
  if (typeof identifier !== "string") return;
102
- const branch = getBranchCase(identifier);
112
+ const branch = getBranchCase(kind, identifier);
103
113
  if (!BRANCH_PATTERN.test(branch)) {
104
114
  const suggestion = suggestBranchIdentifier(identifier);
105
115
  throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
106
116
  }
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`);
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)}'`);
108
118
  }
109
119
  /**
110
120
  * The nearest identifier-safe form of a key, so the error names the fix as well
@@ -169,7 +179,12 @@ function convertMessageToIcu(message) {
169
179
  function internalConvertMessageToIcu(message) {
170
180
  switch (true) {
171
181
  case message instanceof LiteralMessage: return String(message.text);
172
- 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
+ }
173
188
  case message instanceof ElementMessage: {
174
189
  if (message.children.length === 0) return `<${String(message.identifier)}/>`;
175
190
  const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
@@ -177,11 +192,12 @@ function convertMessageToIcu(message) {
177
192
  }
178
193
  case message instanceof ChoiceMessage: {
179
194
  const branches = message.branches.map(({ identifier, value }) => ({
180
- identifier: getBranchCase(identifier),
195
+ identifier: getBranchCase(message.kind, identifier),
181
196
  value: internalConvertMessageToIcu(value)
182
197
  })).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
183
198
  const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
184
- 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}}`;
185
201
  }
186
202
  case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m)).join("");
187
203
  default: throw new Error("Unknown message type", { cause: message });
@@ -190,4 +206,69 @@ function convertMessageToIcu(message) {
190
206
  return internalConvertMessageToIcu(message).trim();
191
207
  }
192
208
  //#endregion
193
- export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
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.1",
3
+ "version": "0.7.0",
4
4
  "description": "CLI and configuration tooling for saykit",
5
5
  "keywords": [
6
6
  "cli",