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/dist/index.mjs CHANGED
@@ -1,453 +1,205 @@
1
- import { parse } from "@messageformat/parser";
2
- import { MessageFormat } from "messageformat";
3
- import { getDateTimeFormatOptions, parseDateTokens } from "@messageformat/date-skeleton";
4
- import { getNumberFormatOptions, parseNumberPattern, parseNumberSkeleton } from "@messageformat/number-skeleton";
5
- import { getLocaleDir } from "messageformat/functions";
6
- //#region src/messageformat/options.ts
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
- //#endregion
15
- //#region src/messageformat/styles.ts
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 numberStyle(style) {
77
- if (style === "") return {};
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
- //#endregion
95
- //#region src/messageformat/convert.ts
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 toPart(token, plural) {
130
- switch (token.type) {
131
- case "content": return token.value;
132
- case "argument": return {
133
- type: "expression",
134
- arg: {
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
- const sameSelector = (a) => (b) => a.arg === b.arg && a.type === b.type && a.offset === b.offset;
161
- function findSelectors(tokens) {
162
- const selectors = [];
163
- const add = (selector) => {
164
- const existing = selectors.find(sameSelector(selector));
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
- function toMessage(ast) {
223
- const selectors = findSelectors(ast);
224
- if (selectors.length === 0) return {
225
- type: "message",
226
- declarations: [],
227
- pattern: ast.map((t) => toPart(t, null))
228
- };
229
- let tuples = [[]];
230
- for (const selector of selectors) {
231
- const keys = sortKeys(selector.keys);
232
- tuples = tuples.flatMap((tuple) => keys.map((key) => [...tuple, key]));
233
- }
234
- const variants = tuples.map((tuple) => ({
235
- keys: tuple.map((key) => key === "other" ? { type: "*" } : {
236
- type: "literal",
237
- quoted: false,
238
- value: String(key)
239
- }),
240
- value: []
241
- }));
242
- function fill(tokens, plural, filter) {
243
- for (const token of tokens) {
244
- if (isSelect(token)) {
245
- const index = selectors.findIndex(sameSelector({
246
- arg: token.arg,
247
- type: token.type,
248
- offset: token.pluralOffset ?? 0
249
- }));
250
- const inner = token.type === "select" ? plural : selectors[index].name;
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
- //#endregion
284
- //#region src/messageformat/values.ts
285
- function part(type, locale, parts) {
286
- const dir = getLocaleDir(locale);
287
- /* v8 ignore next 3 */
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 numeric(operand) {
300
- const value = typeof operand === "object" && operand !== null ? operand.valueOf() : operand;
301
- if (typeof value === "bigint") return value;
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 temporal(operand) {
307
- let value = operand;
308
- if (typeof value === "object" && value !== null && !(value instanceof Date)) value = value.valueOf();
309
- if (typeof value === "number" || typeof value === "string") value = new Date(value);
310
- if (!(value instanceof Date) || Number.isNaN(value.getTime())) throw new Error("Input is not a valid date");
311
- return value;
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 number(locales, value, opt) {
314
- let nf;
315
- let locale;
316
- const format = () => nf ??= new Intl.NumberFormat(locales, opt);
317
- const resolved = () => locale ??= format().resolvedOptions().locale;
318
- return {
319
- type: "number",
320
- get dir() {
321
- return getLocaleDir(resolved());
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 datetime(locales, value, opt) {
332
- let dtf;
333
- let locale;
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
- type: "datetime",
338
- get dir() {
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 duration(seconds) {
350
- if (!Number.isFinite(seconds)) return String(seconds);
351
- const sign = seconds < 0 ? "-" : "";
352
- const total = Math.round(Math.abs(seconds) * 1e3) / 1e3;
353
- const secs = total % 60;
354
- const minutes = Math.floor(total / 60);
355
- const hours = Math.floor(minutes / 60);
356
- const written = Math.round(secs) === secs ? String(secs) : secs.toFixed(3);
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
- //#endregion
422
- //#region src/messageformat/index.ts
423
- function compile(locale, source, options) {
424
- return new MessageFormat(locale, toMessage(parse(source)), {
425
- ...options,
426
- functions
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
- //#endregion
430
- //#region src/view.ts
431
- function resolveDescriptorValues(descriptor) {
432
- return Object.fromEntries(Object.entries(descriptor).filter(([key]) => key !== "id" && key !== "message").map(([key, value]) => [key.startsWith("_") ? key.slice(1) : key, value]));
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 = Object.freeze({ ...messages });
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 = own[id] } = descriptor;
447
- if (typeof message !== "string") throw new Error(`Message for ${id} is not a string`);
448
- let format = formats.get(message);
449
- if (!format) formats.set(message, format = compile(locale, message, { bidiIsolation }));
450
- return String(format.format(resolveDescriptorValues(descriptor)));
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
- //#endregion
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 messages = typeof produced.default === "object" ? produced.default : produced;
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 flat = guesses.flat().filter((guess) => typeof guess === "string" && guess !== "");
523
- if (flat.length === 0) return locales[0];
524
- for (const guess of flat) {
525
- if (locales.includes(guess)) return guess;
526
- const prefix = guess.split("-")[0];
527
- if (!prefix) continue;
528
- const match = locales.find((l) => l.startsWith(prefix));
529
- if (match) return match;
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
- //#endregion
544
- //#region src/scope.ts
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
- //#endregion
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
- //#endregion
670
- export { createCatalogue, createScope, createStore, createView };
443
+ export { Kind, compileMessage, createCatalogue, createScope, createStore, createView };