saykit 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/index.d.mts +92 -35
- package/dist/index.mjs +211 -438
- package/package.json +3 -8
- package/skills/saykit/SKILL.md +375 -0
package/dist/index.mjs
CHANGED
|
@@ -1,453 +1,205 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const literal = (value) => ({
|
|
8
|
-
type: "literal",
|
|
9
|
-
value
|
|
10
|
-
});
|
|
11
|
-
function options(value) {
|
|
12
|
-
return typeof value === "object" && value !== null ? value : {};
|
|
1
|
+
// src/runtime.ts
|
|
2
|
+
const formatters = /* @__PURE__ */ new Map();
|
|
3
|
+
function memoise(key, make) {
|
|
4
|
+
let formatter = formatters.get(key);
|
|
5
|
+
if (formatter === void 0) formatters.set(key, formatter = make());
|
|
6
|
+
return formatter;
|
|
13
7
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
var StyleError = class extends Error {};
|
|
17
|
-
const DATE_STYLES = {
|
|
18
|
-
short: {
|
|
19
|
-
year: "numeric",
|
|
20
|
-
month: "numeric",
|
|
21
|
-
day: "numeric"
|
|
22
|
-
},
|
|
23
|
-
medium: {
|
|
24
|
-
year: "numeric",
|
|
25
|
-
month: "short",
|
|
26
|
-
day: "numeric"
|
|
27
|
-
},
|
|
28
|
-
long: {
|
|
29
|
-
year: "numeric",
|
|
30
|
-
month: "long",
|
|
31
|
-
day: "numeric"
|
|
32
|
-
},
|
|
33
|
-
full: {
|
|
34
|
-
year: "numeric",
|
|
35
|
-
month: "long",
|
|
36
|
-
day: "numeric",
|
|
37
|
-
weekday: "long"
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
const TIME_STYLES = {
|
|
41
|
-
short: {
|
|
42
|
-
hour: "numeric",
|
|
43
|
-
minute: "numeric"
|
|
44
|
-
},
|
|
45
|
-
medium: {
|
|
46
|
-
hour: "numeric",
|
|
47
|
-
minute: "numeric",
|
|
48
|
-
second: "numeric"
|
|
49
|
-
},
|
|
50
|
-
long: {
|
|
51
|
-
hour: "numeric",
|
|
52
|
-
minute: "numeric",
|
|
53
|
-
second: "numeric",
|
|
54
|
-
timeZoneName: "short"
|
|
55
|
-
},
|
|
56
|
-
full: {
|
|
57
|
-
hour: "numeric",
|
|
58
|
-
minute: "numeric",
|
|
59
|
-
second: "numeric",
|
|
60
|
-
timeZoneName: "long"
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
function dateTimeStyle(kind, style) {
|
|
64
|
-
const named = kind === "date" ? DATE_STYLES : TIME_STYLES;
|
|
65
|
-
if (style.startsWith("::")) {
|
|
66
|
-
const errors = [];
|
|
67
|
-
const opt = getDateTimeFormatOptions(parseDateTokens(style.slice(2)), (_type, message) => errors.push(message));
|
|
68
|
-
if (errors.length > 0) throw new StyleError(errors[0]);
|
|
69
|
-
if (Object.keys(opt).length === 0) throw new StyleError(`Empty skeleton ${style}`);
|
|
70
|
-
return opt;
|
|
71
|
-
}
|
|
72
|
-
if (style === "") return named.medium;
|
|
73
|
-
if (Object.hasOwn(named, style)) return named[style];
|
|
74
|
-
throw new StyleError(`Unsupported ${kind} style ${style}`);
|
|
8
|
+
function numberFormat(locale, options) {
|
|
9
|
+
return memoise(`n${locale}${JSON.stringify(options ?? null)}`, () => new Intl.NumberFormat(locale, options));
|
|
75
10
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
if (style === "integer") return { maximumFractionDigits: 0 };
|
|
79
|
-
if (style === "percent") return { style: "percent" };
|
|
80
|
-
const errors = [];
|
|
81
|
-
const onError = (error) => errors.push(String(error));
|
|
82
|
-
const skeleton = style.startsWith("::") ? parseNumberSkeleton(style.slice(2), onError) : parseNumberPattern(style, "XXX", onError);
|
|
83
|
-
let scale;
|
|
84
|
-
const opt = getNumberFormatOptions(skeleton, (stem, option) => {
|
|
85
|
-
if (stem === "scale") scale = Number(option);
|
|
86
|
-
else errors.push(`Unsupported number stem ${stem}`);
|
|
87
|
-
});
|
|
88
|
-
if (errors.length > 0) throw new StyleError(errors[0]);
|
|
89
|
-
return scale === void 0 ? opt : {
|
|
90
|
-
...opt,
|
|
91
|
-
scale
|
|
92
|
-
};
|
|
11
|
+
function dateTimeFormat(locale, options) {
|
|
12
|
+
return memoise(`d${locale}${JSON.stringify(options ?? null)}`, () => new Intl.DateTimeFormat(locale, options));
|
|
93
13
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const isSelect = (token) => token.type === "plural" || token.type === "select" || token.type === "selectordinal";
|
|
97
|
-
const asKey = (key) => /^=\d+$/.test(key) ? Number(key.slice(1)) : key;
|
|
98
|
-
function functionRef(token) {
|
|
99
|
-
let style = "";
|
|
100
|
-
for (const part of token.param ?? []) {
|
|
101
|
-
if (part.type !== "content") throw new Error(`Unsupported style part: ${part.type}`);
|
|
102
|
-
style += part.value;
|
|
103
|
-
}
|
|
104
|
-
style = style.trim();
|
|
105
|
-
const ref = (name, value) => ({
|
|
106
|
-
type: "function",
|
|
107
|
-
name,
|
|
108
|
-
...value === void 0 ? {} : { options: { options: literal(value) } }
|
|
109
|
-
});
|
|
110
|
-
try {
|
|
111
|
-
switch (token.key) {
|
|
112
|
-
case "date":
|
|
113
|
-
case "time": return ref("say:datetime", dateTimeStyle(token.key, style));
|
|
114
|
-
case "number": return ref("say:number", numberStyle(style));
|
|
115
|
-
case "duration": return ref("say:duration");
|
|
116
|
-
default: throw new StyleError(`Unsupported argument type ${token.key}`);
|
|
117
|
-
}
|
|
118
|
-
} catch (error) {
|
|
119
|
-
/* v8 ignore next */
|
|
120
|
-
if (!(error instanceof StyleError)) throw error;
|
|
121
|
-
switch (token.key) {
|
|
122
|
-
case "date":
|
|
123
|
-
case "time": return ref("say:datetime", dateTimeStyle(token.key, ""));
|
|
124
|
-
case "number": return ref("say:number", {});
|
|
125
|
-
default: return ref("say:string");
|
|
126
|
-
}
|
|
127
|
-
}
|
|
14
|
+
function pluralRules(locale, type) {
|
|
15
|
+
return memoise(`p${locale}${type}`, () => new Intl.PluralRules(locale, { type }));
|
|
128
16
|
}
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
type: "variable",
|
|
136
|
-
name: token.arg
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
case "function": return {
|
|
140
|
-
type: "expression",
|
|
141
|
-
arg: {
|
|
142
|
-
type: "variable",
|
|
143
|
-
name: token.arg
|
|
144
|
-
},
|
|
145
|
-
functionRef: functionRef(token)
|
|
146
|
-
};
|
|
147
|
-
case "octothorpe":
|
|
148
|
-
/* v8 ignore next */
|
|
149
|
-
return plural ? {
|
|
150
|
-
type: "expression",
|
|
151
|
-
arg: {
|
|
152
|
-
type: "variable",
|
|
153
|
-
name: plural
|
|
154
|
-
}
|
|
155
|
-
} : "#";
|
|
156
|
-
/* v8 ignore next 2 -- the token union has no other members */
|
|
157
|
-
default: throw new Error(`Unsupported token type: ${token.type}`);
|
|
158
|
-
}
|
|
17
|
+
function toNumeric(value) {
|
|
18
|
+
const raw = typeof value === "object" && value !== null ? value.valueOf() : value;
|
|
19
|
+
if (typeof raw === "bigint") return raw;
|
|
20
|
+
if (raw === null || raw === "") return void 0;
|
|
21
|
+
const number = Number(raw);
|
|
22
|
+
return Number.isNaN(number) ? void 0 : number;
|
|
159
23
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (existing) {
|
|
166
|
-
existing.keys.push(...selector.keys);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const taken = selectors.filter((s) => s.arg === selector.arg).length;
|
|
170
|
-
const name = taken === 0 ? selector.arg : `${selector.arg}#${taken}`;
|
|
171
|
-
selectors.push({
|
|
172
|
-
...selector,
|
|
173
|
-
name
|
|
174
|
-
});
|
|
175
|
-
};
|
|
176
|
-
for (const token of tokens) {
|
|
177
|
-
if (!isSelect(token)) continue;
|
|
178
|
-
add({
|
|
179
|
-
arg: token.arg,
|
|
180
|
-
type: token.type,
|
|
181
|
-
offset: token.pluralOffset ?? 0,
|
|
182
|
-
keys: token.cases.map((c) => token.type === "select" ? c.key : asKey(c.key))
|
|
183
|
-
});
|
|
184
|
-
for (const c of token.cases) for (const inner of findSelectors(c.tokens)) add(inner);
|
|
185
|
-
}
|
|
186
|
-
return selectors;
|
|
187
|
-
}
|
|
188
|
-
function declaration({ arg, name, type, offset }) {
|
|
189
|
-
const functionRef = type === "select" ? {
|
|
190
|
-
type: "function",
|
|
191
|
-
name: "say:string"
|
|
192
|
-
} : {
|
|
193
|
-
type: "function",
|
|
194
|
-
name: "say:plural",
|
|
195
|
-
options: { options: literal({
|
|
196
|
-
offset,
|
|
197
|
-
ordinal: type === "selectordinal"
|
|
198
|
-
}) }
|
|
199
|
-
};
|
|
200
|
-
const value = {
|
|
201
|
-
type: "expression",
|
|
202
|
-
arg: {
|
|
203
|
-
type: "variable",
|
|
204
|
-
name: arg
|
|
205
|
-
},
|
|
206
|
-
functionRef
|
|
207
|
-
};
|
|
208
|
-
return name === arg ? {
|
|
209
|
-
type: "input",
|
|
210
|
-
name,
|
|
211
|
-
value
|
|
212
|
-
} : {
|
|
213
|
-
type: "local",
|
|
214
|
-
name,
|
|
215
|
-
value
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
function sortKeys(keys) {
|
|
219
|
-
const rank = (key) => typeof key === "number" ? 0 : key === "other" ? 2 : 1;
|
|
220
|
-
return Array.from(new Set(keys)).sort((a, b) => rank(a) - rank(b));
|
|
24
|
+
function toDate(value) {
|
|
25
|
+
let raw = value;
|
|
26
|
+
if (typeof raw === "object" && raw !== null && !(raw instanceof Date)) raw = raw.valueOf();
|
|
27
|
+
if (typeof raw === "number" || typeof raw === "string") raw = new Date(raw);
|
|
28
|
+
return raw instanceof Date && !Number.isNaN(raw.getTime()) ? raw : void 0;
|
|
221
29
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for (const c of token.cases) {
|
|
252
|
-
const key = token.type === "select" ? c.key : asKey(c.key);
|
|
253
|
-
fill(c.tokens, inner, [...filter, {
|
|
254
|
-
index,
|
|
255
|
-
key
|
|
256
|
-
}]);
|
|
257
|
-
}
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
for (const variant of variants) {
|
|
261
|
-
if (!filter.every(({ index, key }) => {
|
|
262
|
-
const vk = variant.keys[index];
|
|
263
|
-
return vk.type === "*" ? key === "other" : String(key) === vk.value;
|
|
264
|
-
})) continue;
|
|
265
|
-
const part = toPart(token, plural);
|
|
266
|
-
const last = variant.value.length - 1;
|
|
267
|
-
if (typeof part === "string" && typeof variant.value[last] === "string") variant.value[last] += part;
|
|
268
|
-
else variant.value.push(part);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
fill(ast, null, []);
|
|
273
|
-
return {
|
|
274
|
-
type: "select",
|
|
275
|
-
declarations: selectors.map(declaration),
|
|
276
|
-
selectors: selectors.map((s) => ({
|
|
277
|
-
type: "variable",
|
|
278
|
-
name: s.name
|
|
279
|
-
})),
|
|
280
|
-
variants
|
|
281
|
-
};
|
|
30
|
+
// src/message.ts
|
|
31
|
+
const Kind = {
|
|
32
|
+
Value: 0,
|
|
33
|
+
Number: 1,
|
|
34
|
+
DateTime: 2,
|
|
35
|
+
Plural: 3,
|
|
36
|
+
Ordinal: 4,
|
|
37
|
+
Select: 5,
|
|
38
|
+
Hash: 6
|
|
39
|
+
};
|
|
40
|
+
const FSI = String.fromCharCode(8296);
|
|
41
|
+
const RLI = String.fromCharCode(8295);
|
|
42
|
+
const PDI = String.fromCharCode(8297);
|
|
43
|
+
const RTL = /* @__PURE__ */ new Set([
|
|
44
|
+
"Adlm",
|
|
45
|
+
"Arab",
|
|
46
|
+
"Hebr",
|
|
47
|
+
"Mand",
|
|
48
|
+
"Nkoo",
|
|
49
|
+
"Rohg",
|
|
50
|
+
"Syrc",
|
|
51
|
+
"Thaa"
|
|
52
|
+
]);
|
|
53
|
+
function localeDir(locale) {
|
|
54
|
+
try {
|
|
55
|
+
const script = new Intl.Locale(locale).maximize().script;
|
|
56
|
+
if (script) return RTL.has(script) ? "rtl" : "ltr";
|
|
57
|
+
} catch {}
|
|
58
|
+
return "auto";
|
|
282
59
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return dir === "ltr" || dir === "rtl" ? {
|
|
289
|
-
type,
|
|
290
|
-
dir,
|
|
291
|
-
locale,
|
|
292
|
-
parts
|
|
293
|
-
} : {
|
|
294
|
-
type,
|
|
295
|
-
locale,
|
|
296
|
-
parts
|
|
297
|
-
};
|
|
60
|
+
function isolate(ctx, written, localised) {
|
|
61
|
+
if (!ctx.dir) return written;
|
|
62
|
+
if (!localised) return FSI + written + PDI;
|
|
63
|
+
if (ctx.dir === "ltr") return written;
|
|
64
|
+
return (ctx.dir === "rtl" ? RLI : FSI) + written + PDI;
|
|
298
65
|
}
|
|
299
|
-
function
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const number = Number(value);
|
|
303
|
-
if (Number.isNaN(number) && value !== void 0) throw new Error("Input is not numeric");
|
|
304
|
-
return number;
|
|
66
|
+
function unformattable(kind, value, error) {
|
|
67
|
+
console.warn(`saykit: cannot format ${String(value)} as a ${kind}`, ...error ? [error] : []);
|
|
68
|
+
return String(value);
|
|
305
69
|
}
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
70
|
+
function prepare(kind, make) {
|
|
71
|
+
try {
|
|
72
|
+
return make();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.warn(`saykit: cannot build a ${kind} formatter`, error);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
312
77
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
get options() {
|
|
324
|
-
return { ...opt };
|
|
325
|
-
},
|
|
326
|
-
toParts: () => [part("number", resolved(), format().formatToParts(value))],
|
|
327
|
-
toString: () => format().format(value),
|
|
328
|
-
valueOf: () => value
|
|
78
|
+
function formatNumber(ctx, value, out, options, scale) {
|
|
79
|
+
const format = prepare("number", () => numberFormat(ctx.locale, options));
|
|
80
|
+
return (values) => {
|
|
81
|
+
const raw = value(values);
|
|
82
|
+
if (raw === void 0) return void 0;
|
|
83
|
+
const number = toNumeric(raw);
|
|
84
|
+
if (number === void 0 || !format) return unformattable("number", raw);
|
|
85
|
+
let scaled = number;
|
|
86
|
+
if (scale !== void 0) scaled = typeof number === "bigint" && Number.isInteger(scale) ? number * BigInt(scale) : Number(number) * scale;
|
|
87
|
+
return out ? isolate(ctx, format.format(scaled), true) : format.format(scaled);
|
|
329
88
|
};
|
|
330
89
|
}
|
|
331
|
-
function
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const format = () => dtf ??= new Intl.DateTimeFormat(locales, opt);
|
|
335
|
-
const resolved = () => locale ??= format().resolvedOptions().locale;
|
|
90
|
+
function arms(cases, ctx, out) {
|
|
91
|
+
const built = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const [key, node] of Object.entries(cases)) built.set(key, build(node, ctx, out));
|
|
336
93
|
return {
|
|
337
|
-
|
|
338
|
-
get
|
|
339
|
-
return getLocaleDir(resolved());
|
|
340
|
-
},
|
|
341
|
-
get options() {
|
|
342
|
-
return { ...opt };
|
|
343
|
-
},
|
|
344
|
-
toParts: () => [part("datetime", resolved(), format().formatToParts(value))],
|
|
345
|
-
toString: () => format().format(value),
|
|
346
|
-
valueOf: () => value
|
|
94
|
+
built,
|
|
95
|
+
other: built.get("other")
|
|
347
96
|
};
|
|
348
97
|
}
|
|
349
|
-
function
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const parts = hours > 0 ? [
|
|
358
|
-
hours,
|
|
359
|
-
minutes % 60,
|
|
360
|
-
written
|
|
361
|
-
] : [minutes, written];
|
|
362
|
-
const first = parts.shift();
|
|
363
|
-
const pad = (n) => Number(n) < 10 ? `0${n}` : String(n);
|
|
364
|
-
return sign + [first, ...parts.map(pad)].join(":");
|
|
365
|
-
}
|
|
366
|
-
//#endregion
|
|
367
|
-
//#region src/messageformat/functions.ts
|
|
368
|
-
const functions = {
|
|
369
|
-
"say:number": (ctx, opt, operand) => {
|
|
370
|
-
const { scale, ...nf } = options(opt.options);
|
|
371
|
-
const value = numeric(operand);
|
|
372
|
-
return number(ctx.locales, scale ? Number(value) * scale : value, nf);
|
|
373
|
-
},
|
|
374
|
-
"say:datetime": (ctx, opt, operand) => datetime(ctx.locales, temporal(operand), options(opt.options)),
|
|
375
|
-
"say:duration": (_ctx, _opt, operand) => {
|
|
376
|
-
const value = Number(numeric(operand));
|
|
377
|
-
const str = duration(value);
|
|
378
|
-
return {
|
|
379
|
-
type: "say:duration",
|
|
380
|
-
toParts: () => [{
|
|
381
|
-
type: "say:duration",
|
|
382
|
-
value: str
|
|
383
|
-
}],
|
|
384
|
-
toString: () => str,
|
|
385
|
-
valueOf: () => value
|
|
386
|
-
};
|
|
387
|
-
},
|
|
388
|
-
"say:plural": (ctx, opt, operand) => {
|
|
389
|
-
const { offset = 0, ordinal = false } = options(opt.options);
|
|
390
|
-
const value = numeric(operand);
|
|
391
|
-
const shifted = typeof value === "bigint" ? value - BigInt(offset) : value - offset;
|
|
392
|
-
const result = number(ctx.locales, shifted, {});
|
|
393
|
-
result.valueOf = () => value;
|
|
394
|
-
let rules;
|
|
395
|
-
result.selectKey = (keys) => {
|
|
396
|
-
const exact = String(value);
|
|
397
|
-
if (keys.has(exact)) return exact;
|
|
398
|
-
rules ??= new Intl.PluralRules(ctx.locales, {
|
|
399
|
-
localeMatcher: ctx.localeMatcher,
|
|
400
|
-
type: ordinal ? "ordinal" : "cardinal"
|
|
401
|
-
});
|
|
402
|
-
const category = rules.select(Number(shifted));
|
|
403
|
-
return keys.has(category) ? category : null;
|
|
404
|
-
};
|
|
405
|
-
return result;
|
|
406
|
-
},
|
|
407
|
-
"say:string": (_ctx, _opt, operand) => {
|
|
408
|
-
const str = operand === void 0 ? "" : String(operand);
|
|
409
|
-
return {
|
|
410
|
-
type: "string",
|
|
411
|
-
selectKey: (keys) => keys.has(str) ? str : null,
|
|
412
|
-
toParts: () => [{
|
|
413
|
-
type: "string",
|
|
414
|
-
value: str
|
|
415
|
-
}],
|
|
416
|
-
toString: () => str,
|
|
417
|
-
valueOf: () => str
|
|
98
|
+
function build(node, ctx, out) {
|
|
99
|
+
if (typeof node === "string") return () => node;
|
|
100
|
+
if (typeof node[0] !== "number") {
|
|
101
|
+
const parts = node.map((part) => build(part, ctx, out));
|
|
102
|
+
return (values) => {
|
|
103
|
+
let written = "";
|
|
104
|
+
for (const part of parts) written += part(values) ?? "";
|
|
105
|
+
return written;
|
|
418
106
|
};
|
|
419
107
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
108
|
+
const read = (key) => (values) => values[key];
|
|
109
|
+
switch (node[0]) {
|
|
110
|
+
case Kind.Value: {
|
|
111
|
+
const key = node[1];
|
|
112
|
+
if (!out) return read(key);
|
|
113
|
+
return (values) => {
|
|
114
|
+
const value = values[key];
|
|
115
|
+
return value === void 0 ? value : isolate(ctx, String(value), false);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
case Kind.Number: return formatNumber(ctx, read(node[1]), out, node[2], node[3]);
|
|
119
|
+
case Kind.DateTime: {
|
|
120
|
+
const value = read(node[1]);
|
|
121
|
+
const format = prepare("date", () => dateTimeFormat(ctx.locale, node[2]));
|
|
122
|
+
return (values) => {
|
|
123
|
+
const raw = value(values);
|
|
124
|
+
if (raw === void 0) return void 0;
|
|
125
|
+
const date = toDate(raw);
|
|
126
|
+
if (!date || !format) return unformattable("date", raw);
|
|
127
|
+
return out ? isolate(ctx, format.format(date), true) : format.format(date);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
case Kind.Plural:
|
|
131
|
+
case Kind.Ordinal: {
|
|
132
|
+
const value = read(node[1]);
|
|
133
|
+
const offset = node[3] ?? 0;
|
|
134
|
+
const rules = pluralRules(ctx.locale, node[0] === Kind.Ordinal ? "ordinal" : "cardinal");
|
|
135
|
+
const shift = (number) => offset === 0 ? number : typeof number === "bigint" ? number - BigInt(offset) : number - offset;
|
|
136
|
+
const hash = (values) => {
|
|
137
|
+
const number = toNumeric(value(values));
|
|
138
|
+
return number === void 0 ? void 0 : shift(number);
|
|
139
|
+
};
|
|
140
|
+
const { built, other } = arms(node[2], {
|
|
141
|
+
...ctx,
|
|
142
|
+
hash
|
|
143
|
+
}, out);
|
|
144
|
+
return (values) => {
|
|
145
|
+
const number = toNumeric(value(values));
|
|
146
|
+
let arm;
|
|
147
|
+
if (number !== void 0) {
|
|
148
|
+
arm = built.get(`=${String(number)}`);
|
|
149
|
+
arm ??= built.get(rules.select(Number(shift(number))));
|
|
150
|
+
}
|
|
151
|
+
return (arm ?? other)?.(values);
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case Kind.Select: {
|
|
155
|
+
const value = read(node[1]);
|
|
156
|
+
const { built, other } = arms(node[2], ctx, out);
|
|
157
|
+
return (values) => {
|
|
158
|
+
const raw = value(values);
|
|
159
|
+
return ((raw === void 0 ? void 0 : built.get(String(raw))) ?? other)?.(values);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
default:
|
|
163
|
+
if (!ctx.hash) return () => "#";
|
|
164
|
+
return formatNumber(ctx, ctx.hash, out);
|
|
165
|
+
}
|
|
428
166
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
167
|
+
function compileMessage(message, locale, options = {}) {
|
|
168
|
+
if (typeof message === "string") return () => message;
|
|
169
|
+
const formatter = build(message, {
|
|
170
|
+
locale,
|
|
171
|
+
dir: options.bidiIsolation === "none" ? void 0 : localeDir(locale),
|
|
172
|
+
hash: void 0
|
|
173
|
+
}, true);
|
|
174
|
+
return (values) => String(formatter(values) ?? "");
|
|
433
175
|
}
|
|
176
|
+
// src/view.ts
|
|
434
177
|
function macro(name) {
|
|
435
178
|
throw new Error(`'say.${name}' is a macro and must be used with the relevant saykit plugin`);
|
|
436
179
|
}
|
|
180
|
+
function snapshot(value) {
|
|
181
|
+
if (typeof value !== "object" || value === null) return value;
|
|
182
|
+
if (Array.isArray(value)) return Object.freeze(value.map(snapshot));
|
|
183
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, node]) => [key, snapshot(node)])));
|
|
184
|
+
}
|
|
437
185
|
function createView(locale, messages, options = {}) {
|
|
438
186
|
const { bidiIsolation } = options;
|
|
439
187
|
const ownOptions = Object.freeze(bidiIsolation ? { bidiIsolation } : {});
|
|
440
|
-
const own =
|
|
441
|
-
const formats = /* @__PURE__ */ new Map();
|
|
188
|
+
const own = snapshot(messages);
|
|
442
189
|
const say = (() => {
|
|
443
190
|
throw new Error("'say' is a macro and must be used with the relevant saykit plugin");
|
|
444
191
|
});
|
|
192
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
445
193
|
function call(descriptor) {
|
|
446
|
-
const { id, message
|
|
447
|
-
if (
|
|
448
|
-
let format =
|
|
449
|
-
if (!format)
|
|
450
|
-
|
|
194
|
+
const { id, message } = descriptor;
|
|
195
|
+
if (message !== void 0) return compileMessage(message, locale, ownOptions)(descriptor);
|
|
196
|
+
let format = compiled.get(id);
|
|
197
|
+
if (!format) {
|
|
198
|
+
const message = Object.hasOwn(own, id) ? own[id] : void 0;
|
|
199
|
+
if (message === void 0) throw new Error(`No message for ${id} in locale '${locale}'`);
|
|
200
|
+
compiled.set(id, format = compileMessage(message, locale, ownOptions));
|
|
201
|
+
}
|
|
202
|
+
return format(descriptor);
|
|
451
203
|
}
|
|
452
204
|
return Object.freeze(Object.defineProperties(say, {
|
|
453
205
|
locale: {
|
|
@@ -472,19 +224,23 @@ function createView(locale, messages, options = {}) {
|
|
|
472
224
|
[Symbol.for("nodejs.util.inspect.custom")]: { value: (_depth, context, inspect) => `View<${inspect(locale, context)}> {}` }
|
|
473
225
|
}));
|
|
474
226
|
}
|
|
475
|
-
|
|
476
|
-
//#region src/catalogue.ts
|
|
227
|
+
// src/catalogue.ts
|
|
477
228
|
function createCatalogue(messages, options = {}) {
|
|
478
229
|
const { bidiIsolation } = options;
|
|
479
230
|
const sources = Object.assign(Object.create(null), messages);
|
|
480
231
|
const keys = Object.freeze(Object.keys(sources));
|
|
481
232
|
if (keys.length === 0) throw new Error("A catalogue needs at least one locale, none were given");
|
|
482
233
|
const locales = keys;
|
|
234
|
+
const tags = locales.flatMap((locale) => {
|
|
235
|
+
const tag = parseTag(locale);
|
|
236
|
+
return tag ? [[locale, tag]] : [];
|
|
237
|
+
});
|
|
483
238
|
const store = /* @__PURE__ */ new Map();
|
|
484
239
|
const views = /* @__PURE__ */ new Map();
|
|
485
240
|
const loading = /* @__PURE__ */ new Map();
|
|
486
241
|
function fill(locale, produced) {
|
|
487
|
-
const
|
|
242
|
+
const { default: inner } = produced;
|
|
243
|
+
const messages = typeof inner === "object" && !Array.isArray(inner) ? inner : produced;
|
|
488
244
|
if (!store.has(locale)) store.set(locale, messages);
|
|
489
245
|
return catalogue.locale(locale);
|
|
490
246
|
}
|
|
@@ -519,14 +275,15 @@ function createCatalogue(messages, options = {}) {
|
|
|
519
275
|
return task;
|
|
520
276
|
},
|
|
521
277
|
match(...guesses) {
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
278
|
+
for (const guess of guesses.flat()) {
|
|
279
|
+
if (typeof guess !== "string" || guess === "") continue;
|
|
280
|
+
const tag = parseTag(guess);
|
|
281
|
+
if (!tag) {
|
|
282
|
+
if (locales.includes(guess)) return guess;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const found = tags.find(([, t]) => t.canonical === tag.canonical) ?? (tag.script ? tags.find(([, t]) => t.language === tag.language && t.script === tag.script) : void 0) ?? tags.find(([, t]) => t.language === tag.language);
|
|
286
|
+
if (found) return found[0];
|
|
530
287
|
}
|
|
531
288
|
return locales[0];
|
|
532
289
|
},
|
|
@@ -540,8 +297,26 @@ function createCatalogue(messages, options = {}) {
|
|
|
540
297
|
}
|
|
541
298
|
return Object.freeze(catalogue);
|
|
542
299
|
}
|
|
543
|
-
|
|
544
|
-
|
|
300
|
+
function parseTag(input) {
|
|
301
|
+
let canonical;
|
|
302
|
+
try {
|
|
303
|
+
[canonical] = Intl.getCanonicalLocales(input);
|
|
304
|
+
} catch {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (!canonical) return void 0;
|
|
308
|
+
const language = canonical.split("-")[0];
|
|
309
|
+
let script;
|
|
310
|
+
try {
|
|
311
|
+
script = new Intl.Locale(canonical).maximize().script;
|
|
312
|
+
} catch {}
|
|
313
|
+
return {
|
|
314
|
+
canonical,
|
|
315
|
+
language,
|
|
316
|
+
script
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
// src/scope.ts
|
|
545
320
|
const NO_VIEW = "No view is in scope for 'say'. Run the work inside 'scope.run(view, callback)', or establish one with 'scope.use(store)'.";
|
|
546
321
|
function createVariableStorage() {
|
|
547
322
|
let running;
|
|
@@ -612,8 +387,7 @@ function createScope(storage = createVariableStorage()) {
|
|
|
612
387
|
return `Scope<${view ? inspect(view.locale, context) : "unset"}> {}`;
|
|
613
388
|
} } }));
|
|
614
389
|
}
|
|
615
|
-
|
|
616
|
-
//#region src/store.ts
|
|
390
|
+
// src/store.ts
|
|
617
391
|
function createStore(catalogue, locale = catalogue.locales[0]) {
|
|
618
392
|
let current = catalogue.locale(locale);
|
|
619
393
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -666,5 +440,4 @@ function createStore(catalogue, locale = catalogue.locales[0]) {
|
|
|
666
440
|
}
|
|
667
441
|
}, { [Symbol.for("nodejs.util.inspect.custom")]: { value: (_depth, context, inspect) => `Store<${inspect(current.locale, context)}> {}` } }));
|
|
668
442
|
}
|
|
669
|
-
|
|
670
|
-
export { createCatalogue, createScope, createStore, createView };
|
|
443
|
+
export { Kind, compileMessage, createCatalogue, createScope, createStore, createView };
|