@saykit/config 0.8.0 → 0.9.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.
- package/dist/commands/index.cjs +2 -16
- package/dist/commands/index.mjs +2 -16
- package/dist/features/catalogue/index.cjs +1 -17
- package/dist/features/catalogue/index.mjs +1 -17
- package/dist/features/loader/index.cjs +1 -1
- package/dist/features/loader/index.mjs +1 -1
- package/dist/features/messages/index.cjs +22 -112
- package/dist/features/messages/index.d.cts +17 -3
- package/dist/features/messages/index.d.mts +17 -3
- package/dist/features/messages/index.mjs +22 -112
- package/dist/index.cjs +0 -18
- package/dist/index.mjs +0 -18
- package/dist/{loader-DwF1lM3f.mjs → loader-C6XxYhMl.mjs} +0 -17
- package/dist/{loader-BdCeoj3-.cjs → loader-XE7SprlF.cjs} +0 -17
- package/dist/{storage-B6mn0s3V.mjs → storage-8485YdBh.mjs} +0 -8
- package/dist/{storage-DqhzA5H8.cjs → storage-DSdC-TYq.cjs} +0 -8
- package/package.json +3 -1
package/dist/commands/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_storage = require("../storage-
|
|
3
|
-
const require_loader = require("../loader-
|
|
2
|
+
const require_storage = require("../storage-DSdC-TYq.cjs");
|
|
3
|
+
const require_loader = require("../loader-XE7SprlF.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");
|
|
@@ -76,12 +76,6 @@ async function extractMessagesFromFile(path, bucket) {
|
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
78
|
//#region src/features/watch.ts
|
|
79
|
-
/**
|
|
80
|
-
* Expand a buckets include and exclude patterns into a flat list of file paths.
|
|
81
|
-
*
|
|
82
|
-
* We stat each match instead of using `glob`'s `withFileTypes` option, which
|
|
83
|
-
* Bun's `node:fs/promises` compatibility layer does not yet support.
|
|
84
|
-
*/
|
|
85
79
|
async function globBucket(bucket) {
|
|
86
80
|
const paths = [];
|
|
87
81
|
for await (const path of (0, node_fs_promises.glob)(bucket.include, { exclude: bucket.exclude })) try {
|
|
@@ -91,14 +85,6 @@ async function globBucket(bucket) {
|
|
|
91
85
|
}
|
|
92
86
|
return paths;
|
|
93
87
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Watches a path for changes, emitting a debounced event every set delay.
|
|
96
|
-
*
|
|
97
|
-
* Unlike Node's native `fs.watch` method, this:
|
|
98
|
-
* - coalesces rapid consecutive events per file
|
|
99
|
-
* - emits only the final event after `delay` ms of inactivity
|
|
100
|
-
* - deduplicates events by filename
|
|
101
|
-
*/
|
|
102
88
|
async function* watchDebounced(path, options, delay = 300) {
|
|
103
89
|
const timers = /* @__PURE__ */ new Map();
|
|
104
90
|
const queue = /* @__PURE__ */ new Map();
|
package/dist/commands/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as mergeExtractedMessages, i as expandBucketOutputPath, n as writeCatalogueMessages, o as pruneLocaleMessages, t as readCatalogueMessages } from "../storage-
|
|
3
|
-
import { t as resolveConfig } from "../loader-
|
|
2
|
+
import { a as mergeExtractedMessages, i as expandBucketOutputPath, n as writeCatalogueMessages, o as pruneLocaleMessages, t as readCatalogueMessages } from "../storage-8485YdBh.mjs";
|
|
3
|
+
import { t as resolveConfig } from "../loader-C6XxYhMl.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";
|
|
@@ -76,12 +76,6 @@ async function extractMessagesFromFile(path, bucket) {
|
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
78
|
//#region src/features/watch.ts
|
|
79
|
-
/**
|
|
80
|
-
* Expand a buckets include and exclude patterns into a flat list of file paths.
|
|
81
|
-
*
|
|
82
|
-
* We stat each match instead of using `glob`'s `withFileTypes` option, which
|
|
83
|
-
* Bun's `node:fs/promises` compatibility layer does not yet support.
|
|
84
|
-
*/
|
|
85
79
|
async function globBucket(bucket) {
|
|
86
80
|
const paths = [];
|
|
87
81
|
for await (const path of glob(bucket.include, { exclude: bucket.exclude })) try {
|
|
@@ -91,14 +85,6 @@ async function globBucket(bucket) {
|
|
|
91
85
|
}
|
|
92
86
|
return paths;
|
|
93
87
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Watches a path for changes, emitting a debounced event every set delay.
|
|
96
|
-
*
|
|
97
|
-
* Unlike Node's native `fs.watch` method, this:
|
|
98
|
-
* - coalesces rapid consecutive events per file
|
|
99
|
-
* - emits only the final event after `delay` ms of inactivity
|
|
100
|
-
* - deduplicates events by filename
|
|
101
|
-
*/
|
|
102
88
|
async function* watchDebounced(path, options, delay = 300) {
|
|
103
89
|
const timers = /* @__PURE__ */ new Map();
|
|
104
90
|
const queue = /* @__PURE__ */ new Map();
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_hash = require("../../hash-51ce8YiG.cjs");
|
|
3
|
-
const require_storage = require("../../storage-
|
|
3
|
+
const require_storage = require("../../storage-DSdC-TYq.cjs");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
//#region src/features/catalogue/record.ts
|
|
6
|
-
/**
|
|
7
|
-
* Resolve the fallback chain for a locale, most specific first. The source
|
|
8
|
-
* locale (the first configured locale) is always the final fallback, so an
|
|
9
|
-
* untranslated key ultimately resolves to the source string.
|
|
10
|
-
*/
|
|
11
6
|
function resolveFallbackChain(config, locale) {
|
|
12
7
|
const source = config.locales[0];
|
|
13
8
|
const configured = config.fallbackLocales?.[locale];
|
|
@@ -18,11 +13,6 @@ function resolveFallbackChain(config, locale) {
|
|
|
18
13
|
source
|
|
19
14
|
]));
|
|
20
15
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Resolve the catalogue files that contribute to a locale module, most specific
|
|
23
|
-
* first. When the path does not map to a configured locale it is loaded on its
|
|
24
|
-
* own.
|
|
25
|
-
*/
|
|
26
16
|
function resolveCatalogueSources(config, bucket, path) {
|
|
27
17
|
const resolved = (0, node_path.resolve)(path);
|
|
28
18
|
const locale = config.locales.find((l) => require_storage.expandBucketOutputPath(bucket, l) === resolved);
|
|
@@ -31,12 +21,6 @@ function resolveCatalogueSources(config, bucket, path) {
|
|
|
31
21
|
sources: locale ? resolveFallbackChain(config, locale).map((l) => require_storage.expandBucketOutputPath(bucket, l)) : [resolved]
|
|
32
22
|
};
|
|
33
23
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Assemble a `{ id: string }` record from catalogue file contents. `contents`
|
|
36
|
-
* must be aligned with the `sources` returned by {@link resolveCatalogueSources}
|
|
37
|
-
* (most specific first): more specific locales override their fallbacks, and any
|
|
38
|
-
* key still untranslated falls back to its source message.
|
|
39
|
-
*/
|
|
40
24
|
function assembleCatalogueRecord(bucket, contents) {
|
|
41
25
|
const record = {};
|
|
42
26
|
for (const content of [...contents].reverse()) {
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { t as generateHash } from "../../hash-DvzpieJD.mjs";
|
|
2
|
-
import { a as mergeExtractedMessages, i as expandBucketOutputPath, n as writeCatalogueMessages, o as pruneLocaleMessages, r as declarationPathFor, t as readCatalogueMessages } from "../../storage-
|
|
2
|
+
import { a as mergeExtractedMessages, i as expandBucketOutputPath, n as writeCatalogueMessages, o as pruneLocaleMessages, r as declarationPathFor, t as readCatalogueMessages } from "../../storage-8485YdBh.mjs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
//#region src/features/catalogue/record.ts
|
|
5
|
-
/**
|
|
6
|
-
* Resolve the fallback chain for a locale, most specific first. The source
|
|
7
|
-
* locale (the first configured locale) is always the final fallback, so an
|
|
8
|
-
* untranslated key ultimately resolves to the source string.
|
|
9
|
-
*/
|
|
10
5
|
function resolveFallbackChain(config, locale) {
|
|
11
6
|
const source = config.locales[0];
|
|
12
7
|
const configured = config.fallbackLocales?.[locale];
|
|
@@ -17,11 +12,6 @@ function resolveFallbackChain(config, locale) {
|
|
|
17
12
|
source
|
|
18
13
|
]));
|
|
19
14
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Resolve the catalogue files that contribute to a locale module, most specific
|
|
22
|
-
* first. When the path does not map to a configured locale it is loaded on its
|
|
23
|
-
* own.
|
|
24
|
-
*/
|
|
25
15
|
function resolveCatalogueSources(config, bucket, path) {
|
|
26
16
|
const resolved = resolve(path);
|
|
27
17
|
const locale = config.locales.find((l) => expandBucketOutputPath(bucket, l) === resolved);
|
|
@@ -30,12 +20,6 @@ function resolveCatalogueSources(config, bucket, path) {
|
|
|
30
20
|
sources: locale ? resolveFallbackChain(config, locale).map((l) => expandBucketOutputPath(bucket, l)) : [resolved]
|
|
31
21
|
};
|
|
32
22
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Assemble a `{ id: string }` record from catalogue file contents. `contents`
|
|
35
|
-
* must be aligned with the `sources` returned by {@link resolveCatalogueSources}
|
|
36
|
-
* (most specific first): more specific locales override their fallbacks, and any
|
|
37
|
-
* key still untranslated falls back to its source message.
|
|
38
|
-
*/
|
|
39
23
|
function assembleCatalogueRecord(bucket, contents) {
|
|
40
24
|
const record = {};
|
|
41
25
|
for (const content of [...contents].reverse()) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_loader = require("../../loader-
|
|
2
|
+
const require_loader = require("../../loader-XE7SprlF.cjs");
|
|
3
3
|
exports.resolveConfig = require_loader.resolveConfig;
|
|
4
4
|
exports.resolveConfigFile = require_loader.resolveConfigFile;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as resolveConfigFile, t as resolveConfig } from "../../loader-
|
|
1
|
+
import { n as resolveConfigFile, t as resolveConfig } from "../../loader-C6XxYhMl.mjs";
|
|
2
2
|
export { resolveConfig, resolveConfigFile };
|
|
@@ -1,29 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_hash = require("../../hash-51ce8YiG.cjs");
|
|
3
|
+
let _messageformat_date_skeleton = require("@messageformat/date-skeleton");
|
|
4
|
+
let _messageformat_number_skeleton = require("@messageformat/number-skeleton");
|
|
3
5
|
//#region src/features/messages/escape.ts
|
|
4
|
-
/**
|
|
5
|
-
* Quoting for text that has to survive a message format unchanged.
|
|
6
|
-
*
|
|
7
|
-
* This lives beside the converter rather than in a transform because it belongs
|
|
8
|
-
* to the format being written, not to the syntax being read. A literal is the
|
|
9
|
-
* same literal whether it came from a template literal or from JSX, and the
|
|
10
|
-
* moment there is a second target format its reserved characters — and the way
|
|
11
|
-
* it spells an escape — are its own.
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* Escape literal text so ICU MessageFormat 1 reads it as the text it is.
|
|
15
|
-
*
|
|
16
|
-
* MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
|
|
17
|
-
* doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
|
|
18
|
-
* only opens a quoted run when a character that needs quoting follows it —
|
|
19
|
-
* anything else and the apostrophe is itself literal. So `'#'` is a real escape
|
|
20
|
-
* inside a plural and the three characters `'#'` anywhere else, which is why
|
|
21
|
-
* `hash` has to be told rather than assumed.
|
|
22
|
-
*
|
|
23
|
-
* @param text Literal text as the author wrote it
|
|
24
|
-
* @param hash Whether `#` stands for a number here, i.e. whether some `plural`
|
|
25
|
-
* or `ordinal` encloses this text
|
|
26
|
-
*/
|
|
27
6
|
function escapeIcuLiteral(text, hash = false) {
|
|
28
7
|
return text.replace(/'/gu, "''").replace(hash ? /[{}#]+/gu : /[{}]+/gu, (run) => `'${run}'`);
|
|
29
8
|
}
|
|
@@ -102,40 +81,12 @@ var CompositeMessage = class extends Base {
|
|
|
102
81
|
//#endregion
|
|
103
82
|
//#region src/features/messages/identifier.ts
|
|
104
83
|
const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
|
|
105
|
-
/**
|
|
106
|
-
* An ICU case, either an exact value or a key. ICU reserves its own pattern
|
|
107
|
-
* syntax, so a key carries no punctuation and no whitespace.
|
|
108
|
-
*/
|
|
109
84
|
const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
|
|
110
|
-
/**
|
|
111
|
-
* The ICU case a branch is written as.
|
|
112
|
-
*
|
|
113
|
-
* Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
|
|
114
|
-
* so is distinct from the CLDR category that would otherwise match it. `select`
|
|
115
|
-
* has no such syntax — its cases are literal string matches, and `=0` there is
|
|
116
|
-
* a parse error — so a numeric key stays bare, where it matches both `0` and
|
|
117
|
-
* `'0'`.
|
|
118
|
-
*
|
|
119
|
-
* Digits are read literally rather than coerced, because everything JavaScript
|
|
120
|
-
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
121
|
-
* `=0`, which would pass for a key that selects zero and quietly leave the
|
|
122
|
-
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
123
|
-
* whitespace or punctuation that made it numeric-looking is caught.
|
|
124
|
-
*/
|
|
125
85
|
function getBranchCase(kind, identifier) {
|
|
126
86
|
const key = String(identifier);
|
|
127
87
|
if (kind === "select") return key;
|
|
128
88
|
return /^\d+$/u.test(key) ? `=${+key}` : key;
|
|
129
89
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
132
|
-
* file and a line.
|
|
133
|
-
*
|
|
134
|
-
* A hyphenated string union is ordinary application code and typechecks,
|
|
135
|
-
* builds, and extracts to a catalogue entry that looks perfectly normal — the
|
|
136
|
-
* only sign of trouble is a parse error at format time, in a message whose
|
|
137
|
-
* source is long gone.
|
|
138
|
-
*/
|
|
139
90
|
function validateBranchIdentifier(kind, identifier) {
|
|
140
91
|
if (typeof identifier !== "string") return;
|
|
141
92
|
const branch = getBranchCase(kind, identifier);
|
|
@@ -145,12 +96,6 @@ function validateBranchIdentifier(kind, identifier) {
|
|
|
145
96
|
}
|
|
146
97
|
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)}'`);
|
|
147
98
|
}
|
|
148
|
-
/**
|
|
149
|
-
* The nearest identifier-safe form of a key, so the error names the fix as well
|
|
150
|
-
* as the problem. The constraint comes from ICU rather than from anything the
|
|
151
|
-
* author wrote, and camel case is how the rest of the codebase already spells a
|
|
152
|
-
* name of more than one word.
|
|
153
|
-
*/
|
|
154
99
|
function suggestBranchIdentifier(identifier) {
|
|
155
100
|
const suggestion = identifier.split(/[^\p{L}\p{N}]+/u).filter(Boolean).map((word, index) => index === 0 ? word : word[0].toUpperCase() + word.slice(1)).join("");
|
|
156
101
|
if (!BRANCH_PATTERN.test(suggestion) || !Number.isNaN(+suggestion)) return void 0;
|
|
@@ -167,14 +112,6 @@ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalen
|
|
|
167
112
|
tags: [],
|
|
168
113
|
values: []
|
|
169
114
|
};
|
|
170
|
-
/**
|
|
171
|
-
* The number this expression already has, or a fresh one. An anonymous
|
|
172
|
-
* placeholder is named by its position, so the same value written twice would
|
|
173
|
-
* otherwise arrive as two names — `{0} x {1}` for one length, which asks a
|
|
174
|
-
* translator to keep two holes in step and the caller to supply one value
|
|
175
|
-
* under two props. This is the rule explicit names already follow, where a
|
|
176
|
-
* repeat is allowed precisely when nothing distinguishes it.
|
|
177
|
-
*/
|
|
178
115
|
function number(assigned, expression) {
|
|
179
116
|
const seen = assigned.find(([e]) => equivalent(e, expression));
|
|
180
117
|
if (seen) return seen[1];
|
|
@@ -194,16 +131,6 @@ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalen
|
|
|
194
131
|
}
|
|
195
132
|
walk(message);
|
|
196
133
|
}
|
|
197
|
-
/**
|
|
198
|
-
* Collect the identifiers already assigned before this pass runs, so generated
|
|
199
|
-
* sequence numbers never shadow an explicit one (e.g. an element tagged `0`).
|
|
200
|
-
*
|
|
201
|
-
* A name is claimed by what produced it, not by the name alone. Two that differ
|
|
202
|
-
* each compile to their own prop, and a translator moving them around a sentence
|
|
203
|
-
* has to be able to tell them apart, so they are a build error. Repeats are fine
|
|
204
|
-
* when nothing distinguishes them: the same variable interpolated twice is one
|
|
205
|
-
* value, and two identical elements are one tag.
|
|
206
|
-
*/
|
|
207
134
|
function collectAssignedIdentifiers(message, equivalent) {
|
|
208
135
|
const tags = /* @__PURE__ */ new Map();
|
|
209
136
|
const values = /* @__PURE__ */ new Map();
|
|
@@ -257,19 +184,6 @@ function convertMessageToIcu(message) {
|
|
|
257
184
|
}
|
|
258
185
|
//#endregion
|
|
259
186
|
//#region src/features/messages/format.ts
|
|
260
|
-
/**
|
|
261
|
-
* The ICU argument types a macro can author, and the named styles each accepts.
|
|
262
|
-
*
|
|
263
|
-
* `currency` is deliberately absent from `number`. MF1 has nowhere to write the
|
|
264
|
-
* currency code — it comes from the formatter's configuration, not the message
|
|
265
|
-
* — so `{price, number, currency}` formats as a literal `{$price}` at runtime
|
|
266
|
-
* rather than an amount. Currency belongs to number skeletons, which the
|
|
267
|
-
* formatter does not accept yet either.
|
|
268
|
-
*
|
|
269
|
-
* `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
|
|
270
|
-
* oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
|
|
271
|
-
* equivalent, and the third is deprecated in ICU itself in favour of `plural`.
|
|
272
|
-
*/
|
|
273
187
|
const ARGUMENT_STYLES = {
|
|
274
188
|
number: ["integer", "percent"],
|
|
275
189
|
date: [
|
|
@@ -289,36 +203,32 @@ const ARGUMENT_TYPES = Object.keys(ARGUMENT_STYLES);
|
|
|
289
203
|
function isArgumentType(kind) {
|
|
290
204
|
return Object.hasOwn(ARGUMENT_STYLES, kind);
|
|
291
205
|
}
|
|
292
|
-
/**
|
|
293
|
-
* A literal `NumberFormat` pattern, e.g. `#,##0.00`.
|
|
294
|
-
*
|
|
295
|
-
* A pattern has to carry a digit placeholder — `#` or `0` — because that is
|
|
296
|
-
* what makes it a pattern rather than a word. Without that requirement any
|
|
297
|
-
* brace-free string qualifies, which quietly readmits the named styles this
|
|
298
|
-
* module exists to reject: `currency` would sail through as a "pattern" and
|
|
299
|
-
* extract to the `{price, number, currency}` the formatter cannot honour, and
|
|
300
|
-
* so would a plain typo.
|
|
301
|
-
*
|
|
302
|
-
* Braces are excluded separately: ICU reserves them for its own pattern syntax,
|
|
303
|
-
* so a style carrying one would close the argument early and take the rest of
|
|
304
|
-
* the message with it.
|
|
305
|
-
*/
|
|
306
206
|
const LITERAL_STYLE_PATTERN = /^[^{}\r\n]*[#0][^{}\r\n]*$/;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
207
|
+
const SKELETON = "::";
|
|
208
|
+
function invalidSkeleton(type, skeleton) {
|
|
209
|
+
if (skeleton === "") return "it names no fields";
|
|
210
|
+
const errors = [];
|
|
211
|
+
if (type === "number") {
|
|
212
|
+
(0, _messageformat_number_skeleton.getNumberFormatOptions)((0, _messageformat_number_skeleton.parseNumberSkeleton)(skeleton, (error) => errors.push(String(error))), (stem) => {
|
|
213
|
+
if (stem !== "scale") errors.push(`unsupported stem '${stem}'`);
|
|
214
|
+
});
|
|
215
|
+
return errors[0];
|
|
216
|
+
}
|
|
217
|
+
const options = (0, _messageformat_date_skeleton.getDateTimeFormatOptions)((0, _messageformat_date_skeleton.parseDateTokens)(skeleton), (_type, message) => errors.push(message));
|
|
218
|
+
if (errors.length > 0) return errors[0];
|
|
219
|
+
if (Object.keys(options).length === 0) return "it names no fields";
|
|
220
|
+
}
|
|
316
221
|
function validateArgumentStyle(type, style) {
|
|
317
222
|
const named = ARGUMENT_STYLES[type];
|
|
318
223
|
if (named.includes(style)) return;
|
|
224
|
+
if (style.startsWith(SKELETON)) {
|
|
225
|
+
const reason = invalidSkeleton(type, style.slice(2));
|
|
226
|
+
if (!reason) return;
|
|
227
|
+
throw new Error(`Invalid ${type} skeleton '${style}': ${reason}`);
|
|
228
|
+
}
|
|
319
229
|
if (type === "number" && LITERAL_STYLE_PATTERN.test(style)) return;
|
|
320
230
|
const expected = named.map((s) => `'${s}'`).join(", ");
|
|
321
|
-
throw new Error(`Invalid ${type} style '${style}', expected ${expected}` + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
|
|
231
|
+
throw new Error(`Invalid ${type} style '${style}', expected ${expected}, or a skeleton such as ` + (type === "number" ? "::currency/EUR" : "::yyyyMMdd") + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
|
|
322
232
|
}
|
|
323
233
|
//#endregion
|
|
324
234
|
exports.ARGUMENT_STYLES = ARGUMENT_STYLES;
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The ICU argument types a macro can author, and the named styles each accepts.
|
|
4
4
|
*
|
|
5
|
+
* A style may also be a skeleton, which is where the formats these names have
|
|
6
|
+
* no word for live — see {@link validateArgumentStyle}.
|
|
7
|
+
*
|
|
5
8
|
* `currency` is deliberately absent from `number`. MF1 has nowhere to write the
|
|
6
9
|
* currency code — it comes from the formatter's configuration, not the message
|
|
7
|
-
* — so `{price, number, currency}` formats as a
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* — so `{price, number, currency}` formats as a bare number at runtime rather
|
|
11
|
+
* than an amount. Currency belongs to a skeleton, `::currency/EUR`, which names
|
|
12
|
+
* the code and which the formatter does honour.
|
|
10
13
|
*
|
|
11
14
|
* `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
|
|
12
15
|
* oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
|
|
@@ -28,6 +31,9 @@ declare function isArgumentType(kind: string): kind is ArgumentType;
|
|
|
28
31
|
* nothing between here and the runtime has an opinion about it. Left unchecked,
|
|
29
32
|
* a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
|
|
30
33
|
* perfectly normal and only misformats once it reaches a user.
|
|
34
|
+
*
|
|
35
|
+
* Three forms are accepted: a named style, a `::`-prefixed skeleton, and — for
|
|
36
|
+
* a number — a literal pattern such as `#,##0.00`.
|
|
31
37
|
*/
|
|
32
38
|
declare function validateArgumentStyle(type: ArgumentType, style: string): void;
|
|
33
39
|
//#endregion
|
|
@@ -174,6 +180,14 @@ declare function convertMessageToIcu(message: Message): string;
|
|
|
174
180
|
declare function escapeIcuLiteral(text: string, hash?: boolean): string;
|
|
175
181
|
//#endregion
|
|
176
182
|
//#region src/features/messages/hash.d.ts
|
|
183
|
+
/**
|
|
184
|
+
* Hashed in userland rather than with `node:crypto`, because this runs wherever
|
|
185
|
+
* a bundler runs it. `createHash` is one of the entry points `unenv` leaves
|
|
186
|
+
* unimplemented — "[unenv] crypto.createHash is not implemented yet!" — so a
|
|
187
|
+
* plugin reaching for it throws in a Nitro or workerd build. The Web Crypto
|
|
188
|
+
* equivalent, `subtle.digest`, is async, and an id is resolved from a
|
|
189
|
+
* synchronous extraction pass.
|
|
190
|
+
*/
|
|
177
191
|
declare function generateHash(input: string, context?: string): string;
|
|
178
192
|
//#endregion
|
|
179
193
|
export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The ICU argument types a macro can author, and the named styles each accepts.
|
|
4
4
|
*
|
|
5
|
+
* A style may also be a skeleton, which is where the formats these names have
|
|
6
|
+
* no word for live — see {@link validateArgumentStyle}.
|
|
7
|
+
*
|
|
5
8
|
* `currency` is deliberately absent from `number`. MF1 has nowhere to write the
|
|
6
9
|
* currency code — it comes from the formatter's configuration, not the message
|
|
7
|
-
* — so `{price, number, currency}` formats as a
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* — so `{price, number, currency}` formats as a bare number at runtime rather
|
|
11
|
+
* than an amount. Currency belongs to a skeleton, `::currency/EUR`, which names
|
|
12
|
+
* the code and which the formatter does honour.
|
|
10
13
|
*
|
|
11
14
|
* `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
|
|
12
15
|
* oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
|
|
@@ -28,6 +31,9 @@ declare function isArgumentType(kind: string): kind is ArgumentType;
|
|
|
28
31
|
* nothing between here and the runtime has an opinion about it. Left unchecked,
|
|
29
32
|
* a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
|
|
30
33
|
* perfectly normal and only misformats once it reaches a user.
|
|
34
|
+
*
|
|
35
|
+
* Three forms are accepted: a named style, a `::`-prefixed skeleton, and — for
|
|
36
|
+
* a number — a literal pattern such as `#,##0.00`.
|
|
31
37
|
*/
|
|
32
38
|
declare function validateArgumentStyle(type: ArgumentType, style: string): void;
|
|
33
39
|
//#endregion
|
|
@@ -174,6 +180,14 @@ declare function convertMessageToIcu(message: Message): string;
|
|
|
174
180
|
declare function escapeIcuLiteral(text: string, hash?: boolean): string;
|
|
175
181
|
//#endregion
|
|
176
182
|
//#region src/features/messages/hash.d.ts
|
|
183
|
+
/**
|
|
184
|
+
* Hashed in userland rather than with `node:crypto`, because this runs wherever
|
|
185
|
+
* a bundler runs it. `createHash` is one of the entry points `unenv` leaves
|
|
186
|
+
* unimplemented — "[unenv] crypto.createHash is not implemented yet!" — so a
|
|
187
|
+
* plugin reaching for it throws in a Nitro or workerd build. The Web Crypto
|
|
188
|
+
* equivalent, `subtle.digest`, is async, and an id is resolved from a
|
|
189
|
+
* synchronous extraction pass.
|
|
190
|
+
*/
|
|
177
191
|
declare function generateHash(input: string, context?: string): string;
|
|
178
192
|
//#endregion
|
|
179
193
|
export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
|
|
@@ -1,28 +1,7 @@
|
|
|
1
1
|
import { t as generateHash } from "../../hash-DvzpieJD.mjs";
|
|
2
|
+
import { getDateTimeFormatOptions, parseDateTokens } from "@messageformat/date-skeleton";
|
|
3
|
+
import { getNumberFormatOptions, parseNumberSkeleton } from "@messageformat/number-skeleton";
|
|
2
4
|
//#region src/features/messages/escape.ts
|
|
3
|
-
/**
|
|
4
|
-
* Quoting for text that has to survive a message format unchanged.
|
|
5
|
-
*
|
|
6
|
-
* This lives beside the converter rather than in a transform because it belongs
|
|
7
|
-
* to the format being written, not to the syntax being read. A literal is the
|
|
8
|
-
* same literal whether it came from a template literal or from JSX, and the
|
|
9
|
-
* moment there is a second target format its reserved characters — and the way
|
|
10
|
-
* it spells an escape — are its own.
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* Escape literal text so ICU MessageFormat 1 reads it as the text it is.
|
|
14
|
-
*
|
|
15
|
-
* MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
|
|
16
|
-
* doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
|
|
17
|
-
* only opens a quoted run when a character that needs quoting follows it —
|
|
18
|
-
* anything else and the apostrophe is itself literal. So `'#'` is a real escape
|
|
19
|
-
* inside a plural and the three characters `'#'` anywhere else, which is why
|
|
20
|
-
* `hash` has to be told rather than assumed.
|
|
21
|
-
*
|
|
22
|
-
* @param text Literal text as the author wrote it
|
|
23
|
-
* @param hash Whether `#` stands for a number here, i.e. whether some `plural`
|
|
24
|
-
* or `ordinal` encloses this text
|
|
25
|
-
*/
|
|
26
5
|
function escapeIcuLiteral(text, hash = false) {
|
|
27
6
|
return text.replace(/'/gu, "''").replace(hash ? /[{}#]+/gu : /[{}]+/gu, (run) => `'${run}'`);
|
|
28
7
|
}
|
|
@@ -101,40 +80,12 @@ var CompositeMessage = class extends Base {
|
|
|
101
80
|
//#endregion
|
|
102
81
|
//#region src/features/messages/identifier.ts
|
|
103
82
|
const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
|
|
104
|
-
/**
|
|
105
|
-
* An ICU case, either an exact value or a key. ICU reserves its own pattern
|
|
106
|
-
* syntax, so a key carries no punctuation and no whitespace.
|
|
107
|
-
*/
|
|
108
83
|
const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
|
|
109
|
-
/**
|
|
110
|
-
* The ICU case a branch is written as.
|
|
111
|
-
*
|
|
112
|
-
* Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
|
|
113
|
-
* so is distinct from the CLDR category that would otherwise match it. `select`
|
|
114
|
-
* has no such syntax — its cases are literal string matches, and `=0` there is
|
|
115
|
-
* a parse error — so a numeric key stays bare, where it matches both `0` and
|
|
116
|
-
* `'0'`.
|
|
117
|
-
*
|
|
118
|
-
* Digits are read literally rather than coerced, because everything JavaScript
|
|
119
|
-
* is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
|
|
120
|
-
* `=0`, which would pass for a key that selects zero and quietly leave the
|
|
121
|
-
* author's own key out of the catalogue. Anything else stays a key, where the
|
|
122
|
-
* whitespace or punctuation that made it numeric-looking is caught.
|
|
123
|
-
*/
|
|
124
84
|
function getBranchCase(kind, identifier) {
|
|
125
85
|
const key = String(identifier);
|
|
126
86
|
if (kind === "select") return key;
|
|
127
87
|
return /^\d+$/u.test(key) ? `=${+key}` : key;
|
|
128
88
|
}
|
|
129
|
-
/**
|
|
130
|
-
* Reject a branch key ICU cannot express, while the key is still attached to a
|
|
131
|
-
* file and a line.
|
|
132
|
-
*
|
|
133
|
-
* A hyphenated string union is ordinary application code and typechecks,
|
|
134
|
-
* builds, and extracts to a catalogue entry that looks perfectly normal — the
|
|
135
|
-
* only sign of trouble is a parse error at format time, in a message whose
|
|
136
|
-
* source is long gone.
|
|
137
|
-
*/
|
|
138
89
|
function validateBranchIdentifier(kind, identifier) {
|
|
139
90
|
if (typeof identifier !== "string") return;
|
|
140
91
|
const branch = getBranchCase(kind, identifier);
|
|
@@ -144,12 +95,6 @@ function validateBranchIdentifier(kind, identifier) {
|
|
|
144
95
|
}
|
|
145
96
|
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)}'`);
|
|
146
97
|
}
|
|
147
|
-
/**
|
|
148
|
-
* The nearest identifier-safe form of a key, so the error names the fix as well
|
|
149
|
-
* as the problem. The constraint comes from ICU rather than from anything the
|
|
150
|
-
* author wrote, and camel case is how the rest of the codebase already spells a
|
|
151
|
-
* name of more than one word.
|
|
152
|
-
*/
|
|
153
98
|
function suggestBranchIdentifier(identifier) {
|
|
154
99
|
const suggestion = identifier.split(/[^\p{L}\p{N}]+/u).filter(Boolean).map((word, index) => index === 0 ? word : word[0].toUpperCase() + word.slice(1)).join("");
|
|
155
100
|
if (!BRANCH_PATTERN.test(suggestion) || !Number.isNaN(+suggestion)) return void 0;
|
|
@@ -166,14 +111,6 @@ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalen
|
|
|
166
111
|
tags: [],
|
|
167
112
|
values: []
|
|
168
113
|
};
|
|
169
|
-
/**
|
|
170
|
-
* The number this expression already has, or a fresh one. An anonymous
|
|
171
|
-
* placeholder is named by its position, so the same value written twice would
|
|
172
|
-
* otherwise arrive as two names — `{0} x {1}` for one length, which asks a
|
|
173
|
-
* translator to keep two holes in step and the caller to supply one value
|
|
174
|
-
* under two props. This is the rule explicit names already follow, where a
|
|
175
|
-
* repeat is allowed precisely when nothing distinguishes it.
|
|
176
|
-
*/
|
|
177
114
|
function number(assigned, expression) {
|
|
178
115
|
const seen = assigned.find(([e]) => equivalent(e, expression));
|
|
179
116
|
if (seen) return seen[1];
|
|
@@ -193,16 +130,6 @@ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalen
|
|
|
193
130
|
}
|
|
194
131
|
walk(message);
|
|
195
132
|
}
|
|
196
|
-
/**
|
|
197
|
-
* Collect the identifiers already assigned before this pass runs, so generated
|
|
198
|
-
* sequence numbers never shadow an explicit one (e.g. an element tagged `0`).
|
|
199
|
-
*
|
|
200
|
-
* A name is claimed by what produced it, not by the name alone. Two that differ
|
|
201
|
-
* each compile to their own prop, and a translator moving them around a sentence
|
|
202
|
-
* has to be able to tell them apart, so they are a build error. Repeats are fine
|
|
203
|
-
* when nothing distinguishes them: the same variable interpolated twice is one
|
|
204
|
-
* value, and two identical elements are one tag.
|
|
205
|
-
*/
|
|
206
133
|
function collectAssignedIdentifiers(message, equivalent) {
|
|
207
134
|
const tags = /* @__PURE__ */ new Map();
|
|
208
135
|
const values = /* @__PURE__ */ new Map();
|
|
@@ -256,19 +183,6 @@ function convertMessageToIcu(message) {
|
|
|
256
183
|
}
|
|
257
184
|
//#endregion
|
|
258
185
|
//#region src/features/messages/format.ts
|
|
259
|
-
/**
|
|
260
|
-
* The ICU argument types a macro can author, and the named styles each accepts.
|
|
261
|
-
*
|
|
262
|
-
* `currency` is deliberately absent from `number`. MF1 has nowhere to write the
|
|
263
|
-
* currency code — it comes from the formatter's configuration, not the message
|
|
264
|
-
* — so `{price, number, currency}` formats as a literal `{$price}` at runtime
|
|
265
|
-
* rather than an amount. Currency belongs to number skeletons, which the
|
|
266
|
-
* formatter does not accept yet either.
|
|
267
|
-
*
|
|
268
|
-
* `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
|
|
269
|
-
* oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
|
|
270
|
-
* equivalent, and the third is deprecated in ICU itself in favour of `plural`.
|
|
271
|
-
*/
|
|
272
186
|
const ARGUMENT_STYLES = {
|
|
273
187
|
number: ["integer", "percent"],
|
|
274
188
|
date: [
|
|
@@ -288,36 +202,32 @@ const ARGUMENT_TYPES = Object.keys(ARGUMENT_STYLES);
|
|
|
288
202
|
function isArgumentType(kind) {
|
|
289
203
|
return Object.hasOwn(ARGUMENT_STYLES, kind);
|
|
290
204
|
}
|
|
291
|
-
/**
|
|
292
|
-
* A literal `NumberFormat` pattern, e.g. `#,##0.00`.
|
|
293
|
-
*
|
|
294
|
-
* A pattern has to carry a digit placeholder — `#` or `0` — because that is
|
|
295
|
-
* what makes it a pattern rather than a word. Without that requirement any
|
|
296
|
-
* brace-free string qualifies, which quietly readmits the named styles this
|
|
297
|
-
* module exists to reject: `currency` would sail through as a "pattern" and
|
|
298
|
-
* extract to the `{price, number, currency}` the formatter cannot honour, and
|
|
299
|
-
* so would a plain typo.
|
|
300
|
-
*
|
|
301
|
-
* Braces are excluded separately: ICU reserves them for its own pattern syntax,
|
|
302
|
-
* so a style carrying one would close the argument early and take the rest of
|
|
303
|
-
* the message with it.
|
|
304
|
-
*/
|
|
305
205
|
const LITERAL_STYLE_PATTERN = /^[^{}\r\n]*[#0][^{}\r\n]*$/;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
206
|
+
const SKELETON = "::";
|
|
207
|
+
function invalidSkeleton(type, skeleton) {
|
|
208
|
+
if (skeleton === "") return "it names no fields";
|
|
209
|
+
const errors = [];
|
|
210
|
+
if (type === "number") {
|
|
211
|
+
getNumberFormatOptions(parseNumberSkeleton(skeleton, (error) => errors.push(String(error))), (stem) => {
|
|
212
|
+
if (stem !== "scale") errors.push(`unsupported stem '${stem}'`);
|
|
213
|
+
});
|
|
214
|
+
return errors[0];
|
|
215
|
+
}
|
|
216
|
+
const options = getDateTimeFormatOptions(parseDateTokens(skeleton), (_type, message) => errors.push(message));
|
|
217
|
+
if (errors.length > 0) return errors[0];
|
|
218
|
+
if (Object.keys(options).length === 0) return "it names no fields";
|
|
219
|
+
}
|
|
315
220
|
function validateArgumentStyle(type, style) {
|
|
316
221
|
const named = ARGUMENT_STYLES[type];
|
|
317
222
|
if (named.includes(style)) return;
|
|
223
|
+
if (style.startsWith(SKELETON)) {
|
|
224
|
+
const reason = invalidSkeleton(type, style.slice(2));
|
|
225
|
+
if (!reason) return;
|
|
226
|
+
throw new Error(`Invalid ${type} skeleton '${style}': ${reason}`);
|
|
227
|
+
}
|
|
318
228
|
if (type === "number" && LITERAL_STYLE_PATTERN.test(style)) return;
|
|
319
229
|
const expected = named.map((s) => `'${s}'`).join(", ");
|
|
320
|
-
throw new Error(`Invalid ${type} style '${style}', expected ${expected}` + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
|
|
230
|
+
throw new Error(`Invalid ${type} style '${style}', expected ${expected}, or a skeleton such as ` + (type === "number" ? "::currency/EUR" : "::yyyyMMdd") + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
|
|
321
231
|
}
|
|
322
232
|
//#endregion
|
|
323
233
|
export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
|
package/dist/index.cjs
CHANGED
|
@@ -43,11 +43,6 @@ const Transformer = zod.object({
|
|
|
43
43
|
extract: zod.custom((v) => typeof v === "function"),
|
|
44
44
|
transform: zod.custom((v) => typeof v === "function")
|
|
45
45
|
});
|
|
46
|
-
/**
|
|
47
|
-
* A message declared in the config rather than found in source. The shorthand
|
|
48
|
-
* is the source string; the object form adds the metadata a descriptor would
|
|
49
|
-
* otherwise carry.
|
|
50
|
-
*/
|
|
51
46
|
const DeclaredMessage = zod.string().or(zod.object({
|
|
52
47
|
message: zod.string(),
|
|
53
48
|
context: zod.string().optional(),
|
|
@@ -62,13 +57,6 @@ const Bucket = zod.object({
|
|
|
62
57
|
zod.string(),
|
|
63
58
|
".{extension}"
|
|
64
59
|
]),
|
|
65
|
-
/**
|
|
66
|
-
* Messages that belong in the catalogue but have no call site to extract
|
|
67
|
-
* them from — strings owned by a non-JavaScript artefact such as a manifest,
|
|
68
|
-
* a store listing, or an email subject. Keyed by id, so the id is stable and
|
|
69
|
-
* hand-written by definition, and merged into every extraction alongside
|
|
70
|
-
* whatever the transformers find.
|
|
71
|
-
*/
|
|
72
60
|
messages: zod.record(zod.string(), DeclaredMessage).optional(),
|
|
73
61
|
formatter: Formatter,
|
|
74
62
|
transformer: Transformer.transform((t) => [t]).or(Transformer.array()).transform((t) => ({
|
|
@@ -94,12 +82,6 @@ const Bucket = zod.object({
|
|
|
94
82
|
}));
|
|
95
83
|
const Config = zod.object({
|
|
96
84
|
locales: zod.tuple([zod.string()], zod.string()),
|
|
97
|
-
/**
|
|
98
|
-
* Per-locale fallback chains, most specific first, e.g.
|
|
99
|
-
* `{ 'en-NZ': ['en-GB'], 'es-MX': 'es' }`. The source locale (the first entry
|
|
100
|
-
* in {@link Config.locales}) is always appended as the final fallback, so an
|
|
101
|
-
* untranslated key ultimately resolves to the source string.
|
|
102
|
-
*/
|
|
103
85
|
fallbackLocales: zod.record(zod.string(), zod.string().or(zod.string().array())).optional(),
|
|
104
86
|
buckets: Bucket.array()
|
|
105
87
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -18,11 +18,6 @@ const Transformer = z.object({
|
|
|
18
18
|
extract: z.custom((v) => typeof v === "function"),
|
|
19
19
|
transform: z.custom((v) => typeof v === "function")
|
|
20
20
|
});
|
|
21
|
-
/**
|
|
22
|
-
* A message declared in the config rather than found in source. The shorthand
|
|
23
|
-
* is the source string; the object form adds the metadata a descriptor would
|
|
24
|
-
* otherwise carry.
|
|
25
|
-
*/
|
|
26
21
|
const DeclaredMessage = z.string().or(z.object({
|
|
27
22
|
message: z.string(),
|
|
28
23
|
context: z.string().optional(),
|
|
@@ -37,13 +32,6 @@ const Bucket = z.object({
|
|
|
37
32
|
z.string(),
|
|
38
33
|
".{extension}"
|
|
39
34
|
]),
|
|
40
|
-
/**
|
|
41
|
-
* Messages that belong in the catalogue but have no call site to extract
|
|
42
|
-
* them from — strings owned by a non-JavaScript artefact such as a manifest,
|
|
43
|
-
* a store listing, or an email subject. Keyed by id, so the id is stable and
|
|
44
|
-
* hand-written by definition, and merged into every extraction alongside
|
|
45
|
-
* whatever the transformers find.
|
|
46
|
-
*/
|
|
47
35
|
messages: z.record(z.string(), DeclaredMessage).optional(),
|
|
48
36
|
formatter: Formatter,
|
|
49
37
|
transformer: Transformer.transform((t) => [t]).or(Transformer.array()).transform((t) => ({
|
|
@@ -69,12 +57,6 @@ const Bucket = z.object({
|
|
|
69
57
|
}));
|
|
70
58
|
const Config = z.object({
|
|
71
59
|
locales: z.tuple([z.string()], z.string()),
|
|
72
|
-
/**
|
|
73
|
-
* Per-locale fallback chains, most specific first, e.g.
|
|
74
|
-
* `{ 'en-NZ': ['en-GB'], 'es-MX': 'es' }`. The source locale (the first entry
|
|
75
|
-
* in {@link Config.locales}) is always appended as the final fallback, so an
|
|
76
|
-
* untranslated key ultimately resolves to the source string.
|
|
77
|
-
*/
|
|
78
60
|
fallbackLocales: z.record(z.string(), z.string().or(z.string().array())).optional(),
|
|
79
61
|
buckets: Bucket.array()
|
|
80
62
|
});
|
|
@@ -22,7 +22,6 @@ function findConfigFile(moduleName, projectDir) {
|
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/features/loader/module.ts
|
|
25
|
-
/** Requires `path` without leaving it in (or reading it from) the require cache. */
|
|
26
25
|
function requireFresh(require, path) {
|
|
27
26
|
const resolved = require.resolve(path);
|
|
28
27
|
delete require.cache[resolved];
|
|
@@ -35,11 +34,6 @@ const TYPESCRIPT = /* @__PURE__ */ new Set([
|
|
|
35
34
|
".mts",
|
|
36
35
|
".cts"
|
|
37
36
|
]);
|
|
38
|
-
/**
|
|
39
|
-
* Adds what the runtime cannot: why a config it could not read is one it will
|
|
40
|
-
* never read. Everything else is the config's own problem, and its error
|
|
41
|
-
* already describes that better than we could.
|
|
42
|
-
*/
|
|
43
37
|
function diagnose(error, path) {
|
|
44
38
|
const code = error?.code;
|
|
45
39
|
if (code === "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX") return "Enums, namespaces and parameter properties are not erasable, so no runtime will read them.";
|
|
@@ -47,12 +41,6 @@ function diagnose(error, path) {
|
|
|
47
41
|
if ((code === "ERR_UNKNOWN_FILE_EXTENSION" || !code && error instanceof SyntaxError) && TYPESCRIPT.has(extname(path).toLowerCase())) return "Reading this config needs Node 22.18+, or a runtime that loads TypeScript itself (Bun, Deno, tsx).";
|
|
48
42
|
return null;
|
|
49
43
|
}
|
|
50
|
-
/**
|
|
51
|
-
* Loads a config file as it sits on disk, leaving the runtime to deal with the
|
|
52
|
-
* extension. Nothing is copied or rewritten, so `__dirname`,
|
|
53
|
-
* `import.meta.dirname`, relative specifiers and `require.resolve` all resolve
|
|
54
|
-
* against the config's own directory.
|
|
55
|
-
*/
|
|
56
44
|
function loadModule(path) {
|
|
57
45
|
try {
|
|
58
46
|
return requireFresh(createRequire(path), path);
|
|
@@ -73,11 +61,6 @@ const configLoaders = Object.freeze({
|
|
|
73
61
|
});
|
|
74
62
|
//#endregion
|
|
75
63
|
//#region src/features/loader/resolve.ts
|
|
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
64
|
function resolveConfigFile(name = "saykit") {
|
|
82
65
|
const file = findConfigFile(name, process.cwd());
|
|
83
66
|
if (!file) throw new Error(`Could not find config file for "${name}"`);
|
|
@@ -22,7 +22,6 @@ function findConfigFile(moduleName, projectDir) {
|
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/features/loader/module.ts
|
|
25
|
-
/** Requires `path` without leaving it in (or reading it from) the require cache. */
|
|
26
25
|
function requireFresh(require, path) {
|
|
27
26
|
const resolved = require.resolve(path);
|
|
28
27
|
delete require.cache[resolved];
|
|
@@ -35,11 +34,6 @@ const TYPESCRIPT = /* @__PURE__ */ new Set([
|
|
|
35
34
|
".mts",
|
|
36
35
|
".cts"
|
|
37
36
|
]);
|
|
38
|
-
/**
|
|
39
|
-
* Adds what the runtime cannot: why a config it could not read is one it will
|
|
40
|
-
* never read. Everything else is the config's own problem, and its error
|
|
41
|
-
* already describes that better than we could.
|
|
42
|
-
*/
|
|
43
37
|
function diagnose(error, path) {
|
|
44
38
|
const code = error?.code;
|
|
45
39
|
if (code === "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX") return "Enums, namespaces and parameter properties are not erasable, so no runtime will read them.";
|
|
@@ -47,12 +41,6 @@ function diagnose(error, path) {
|
|
|
47
41
|
if ((code === "ERR_UNKNOWN_FILE_EXTENSION" || !code && error instanceof SyntaxError) && TYPESCRIPT.has((0, node_path.extname)(path).toLowerCase())) return "Reading this config needs Node 22.18+, or a runtime that loads TypeScript itself (Bun, Deno, tsx).";
|
|
48
42
|
return null;
|
|
49
43
|
}
|
|
50
|
-
/**
|
|
51
|
-
* Loads a config file as it sits on disk, leaving the runtime to deal with the
|
|
52
|
-
* extension. Nothing is copied or rewritten, so `__dirname`,
|
|
53
|
-
* `import.meta.dirname`, relative specifiers and `require.resolve` all resolve
|
|
54
|
-
* against the config's own directory.
|
|
55
|
-
*/
|
|
56
44
|
function loadModule(path) {
|
|
57
45
|
try {
|
|
58
46
|
return requireFresh((0, node_module.createRequire)(path), path);
|
|
@@ -73,11 +61,6 @@ const configLoaders = Object.freeze({
|
|
|
73
61
|
});
|
|
74
62
|
//#endregion
|
|
75
63
|
//#region src/features/loader/resolve.ts
|
|
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
64
|
function resolveConfigFile(name = "saykit") {
|
|
82
65
|
const file = findConfigFile(name, process.cwd());
|
|
83
66
|
if (!file) throw new Error(`Could not find config file for "${name}"`);
|
|
@@ -30,14 +30,6 @@ function pruneLocaleMessages(existingMessages, sourceMessages) {
|
|
|
30
30
|
function expandBucketOutputPath(bucket, locale, extension = bucket.formatter.extension) {
|
|
31
31
|
return resolve(bucket.output.replaceAll("{locale}", locale).replaceAll("{extension}", extension.slice(1)));
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
* The declaration file that types a catalogue, e.g. `en.json` -> `en.d.json.ts`.
|
|
35
|
-
*
|
|
36
|
-
* TypeScript resolves `./en.json` by stripping the extension and looking for
|
|
37
|
-
* `en.d.json.ts`; the `en.json.d.ts` form is only consulted for extensions the
|
|
38
|
-
* resolver does not recognise. Non-JS extensions additionally require
|
|
39
|
-
* `allowArbitraryExtensions` in the consumer's tsconfig.
|
|
40
|
-
*/
|
|
41
33
|
function declarationPathFor(cataloguePath) {
|
|
42
34
|
const { dir, name, ext } = parse(cataloguePath);
|
|
43
35
|
return resolve(dir, `${name}.d${ext}.ts`);
|
|
@@ -30,14 +30,6 @@ function pruneLocaleMessages(existingMessages, sourceMessages) {
|
|
|
30
30
|
function expandBucketOutputPath(bucket, locale, extension = bucket.formatter.extension) {
|
|
31
31
|
return (0, node_path.resolve)(bucket.output.replaceAll("{locale}", locale).replaceAll("{extension}", extension.slice(1)));
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
* The declaration file that types a catalogue, e.g. `en.json` -> `en.d.json.ts`.
|
|
35
|
-
*
|
|
36
|
-
* TypeScript resolves `./en.json` by stripping the extension and looking for
|
|
37
|
-
* `en.d.json.ts`; the `en.json.d.ts` form is only consulted for extensions the
|
|
38
|
-
* resolver does not recognise. Non-JS extensions additionally require
|
|
39
|
-
* `allowArbitraryExtensions` in the consumer's tsconfig.
|
|
40
|
-
*/
|
|
41
33
|
function declarationPathFor(cataloguePath) {
|
|
42
34
|
const { dir, name, ext } = (0, node_path.parse)(cataloguePath);
|
|
43
35
|
return (0, node_path.resolve)(dir, `${name}.d${ext}.ts`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saykit/config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "CLI and configuration tooling for saykit",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -74,6 +74,8 @@
|
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@commander-js/extra-typings": "^15.0.0",
|
|
77
|
+
"@messageformat/date-skeleton": "2.0.0-0",
|
|
78
|
+
"@messageformat/number-skeleton": "2.0.0-0",
|
|
77
79
|
"commander": "^15.0.0",
|
|
78
80
|
"js-sha256": "^0.12.0",
|
|
79
81
|
"picomatch": "^4.0.5",
|