saykit 0.8.0 → 0.9.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.
@@ -31,22 +31,30 @@ interface SelectOptions<Branch = string> {
31
31
  other: Branch;
32
32
  [match: string | number]: Branch;
33
33
  }
34
+ /**
35
+ * An ICU skeleton: a `::`-prefixed description of the parts a value is written
36
+ * with, rather than a name for a whole format. `::currency/EUR` and
37
+ * `::yyyyMMdd` say what to show and leave the arrangement to the locale.
38
+ *
39
+ * A skeleton is where the formats the named styles have no word for live —
40
+ * currency, compact notation, a year and month with no day — so every argument
41
+ * type accepts one alongside its named styles.
42
+ */
43
+ type Skeleton = `::${string}`;
34
44
  /**
35
45
  * Formatting for a `{arg, number}` placeholder.
36
46
  *
37
- * `currency` is not offered: MF1 has nowhere to write the currency code, so
38
- * `{price, number, currency}` formats as a literal `{$price}` rather than an
39
- * amount. A literal pattern such as `#,##0.00` is accepted for the cases the
40
- * named styles do not cover.
47
+ * A literal `NumberFormat` pattern such as `#,##0.00` is also accepted, for the
48
+ * cases neither the named styles nor a skeleton spell more clearly.
41
49
  */
42
50
  interface NumberOptions {
43
- style?: 'integer' | 'percent' | (string & {});
51
+ style?: 'integer' | 'percent' | Skeleton | (string & {});
44
52
  }
45
53
  /**
46
54
  * Formatting for a `{arg, date}` or `{arg, time}` placeholder.
47
55
  */
48
56
  interface DateTimeOptions {
49
- style?: 'short' | 'medium' | 'long' | 'full';
57
+ style?: 'short' | 'medium' | 'long' | 'full' | Skeleton;
50
58
  }
51
59
  //#endregion
52
60
  //#region src/runtime.d.ts
@@ -259,10 +267,12 @@ declare class Say<Locale extends string = string, Loader extends Say.Loader<Loca
259
267
  * say`You have ${say.number(items.length)} items`
260
268
  * say`Battery at ${say.number(level, { style: 'percent' })}`
261
269
  * say`Total: ${say.number({ cartTotal: getTotal() }, { style: '#,##0.00' })}`
270
+ * say`Total: ${say.number(total, { style: '::currency/EUR' })}`
262
271
  * ```
263
272
  *
264
273
  * @param _ Number to format
265
- * @param options Formatting style, either a named style or a literal number pattern
274
+ * @param options Formatting style: a named style, an ICU skeleton such as
275
+ * `::currency/EUR`, or a literal number pattern such as `#,##0.00`
266
276
  * @returns The formatted number
267
277
  * @remark This is a macro and must be used with the relevant saykit plugin
268
278
  */
@@ -274,10 +284,12 @@ declare class Say<Locale extends string = string, Loader extends Say.Loader<Loca
274
284
  * ```ts
275
285
  * say`Published ${say.date(post.publishedAt)}`
276
286
  * say`Published ${say.date(post.publishedAt, { style: 'full' })}`
287
+ * say`Published ${say.date(post.publishedAt, { style: '::yMMMM' })}`
277
288
  * ```
278
289
  *
279
290
  * @param _ Date to format
280
- * @param options Formatting style
291
+ * @param options Formatting style, either a named style or an ICU skeleton
292
+ * such as `::yyyyMMdd`
281
293
  * @returns The formatted date
282
294
  * @remark This is a macro and must be used with the relevant saykit plugin
283
295
  */
@@ -289,14 +301,16 @@ declare class Say<Locale extends string = string, Loader extends Say.Loader<Loca
289
301
  * ```ts
290
302
  * say`Doors open at ${say.time(opensAt)}`
291
303
  * say`Doors open at ${say.time(opensAt, { style: 'short' })}`
304
+ * say`Doors open at ${say.time(opensAt, { style: '::Hm' })}`
292
305
  * ```
293
306
  *
294
307
  * @param _ Date to format
295
- * @param options Formatting style
308
+ * @param options Formatting style, either a named style or an ICU skeleton
309
+ * such as `::Hm`
296
310
  * @returns The formatted time
297
311
  * @remark This is a macro and must be used with the relevant saykit plugin
298
312
  */
299
313
  time(_: Date | number | Named<Date | number>, options?: Disallow<DateTimeOptions, 'id' | 'context'>): string;
300
314
  }
301
315
  //#endregion
302
- export { Awaitable, DateTimeOptions, Disallow, Named, NumberOptions, NumeralOptions, ReadonlySay, Say, SelectOptions, Tuple };
316
+ export { Awaitable, DateTimeOptions, Disallow, Named, NumberOptions, NumeralOptions, ReadonlySay, Say, SelectOptions, Skeleton, Tuple };
package/dist/runtime.mjs CHANGED
@@ -1,117 +1,466 @@
1
- import { mf1ToMessage } from "@messageformat/icu-messageformat-1";
2
- //#region \0@oxc-project+runtime@0.140.0/helpers/esm/checkPrivateRedeclaration.js
3
- function _checkPrivateRedeclaration(e, t) {
4
- if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object");
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 : {};
5
13
  }
6
14
  //#endregion
7
- //#region \0@oxc-project+runtime@0.140.0/helpers/esm/classPrivateMethodInitSpec.js
8
- function _classPrivateMethodInitSpec(e, a) {
9
- _checkPrivateRedeclaration(e, a), a.add(e);
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}`);
10
75
  }
11
- //#endregion
12
- //#region \0@oxc-project+runtime@0.140.0/helpers/esm/classPrivateFieldInitSpec.js
13
- function _classPrivateFieldInitSpec(e, t, a) {
14
- _checkPrivateRedeclaration(e, t), t.set(e, a);
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
+ };
15
93
  }
16
94
  //#endregion
17
- //#region \0@oxc-project+runtime@0.140.0/helpers/esm/assertClassBrand.js
18
- function _assertClassBrand(e, t, n) {
19
- if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n;
20
- throw new TypeError("Private element is not present on this object");
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
+ }
128
+ }
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
+ }
159
+ }
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));
221
+ }
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
+ };
21
282
  }
22
283
  //#endregion
23
- //#region \0@oxc-project+runtime@0.140.0/helpers/esm/classPrivateFieldSet2.js
24
- function _classPrivateFieldSet2(s, a, r) {
25
- return s.set(_assertClassBrand(s, a), r), r;
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
+ };
298
+ }
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;
305
+ }
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;
26
312
  }
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
329
+ };
330
+ }
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;
336
+ 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
347
+ };
348
+ }
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
418
+ };
419
+ }
420
+ };
27
421
  //#endregion
28
- //#region \0@oxc-project+runtime@0.140.0/helpers/esm/classPrivateFieldGet2.js
29
- function _classPrivateFieldGet2(s, a) {
30
- return s.get(_assertClassBrand(s, a));
422
+ //#region src/messageformat/index.ts
423
+ function compile(locale, source) {
424
+ return new MessageFormat(locale, toMessage(parse(source)), { functions });
31
425
  }
32
426
  //#endregion
33
427
  //#region src/runtime.ts
34
- let _Symbol$iterator;
35
- let _Symbol$for;
36
- /**
37
- * Map a descriptor's keys back to the placeholders the message names. The
38
- * transform emits every value with one underscore in front, which keeps a
39
- * message's values out of the descriptor's own namespace, so stripping exactly
40
- * one is the whole inverse — `_0` is `0`, and `__total` is a placeholder named
41
- * `_total`. Keys without one are passed through, so a hand-written
42
- * `call({ id, name })` still formats `{name}`.
43
- *
44
- * Built from the descriptor's own entries so a value named `__proto__` stays a
45
- * placeholder rather than reaching through to the prototype.
46
- */
47
428
  function resolveDescriptorValues(descriptor) {
48
429
  return Object.fromEntries(Object.entries(descriptor).filter(([key]) => key !== "id").map(([key, value]) => [key.startsWith("_") ? key.slice(1) : key, value]));
49
430
  }
50
- var _locales = /* @__PURE__ */ new WeakMap();
51
- var _loader = /* @__PURE__ */ new WeakMap();
52
- var _messages = /* @__PURE__ */ new WeakMap();
53
- var _formats = /* @__PURE__ */ new WeakMap();
54
- var _active = /* @__PURE__ */ new WeakMap();
55
- var _Say_brand = /* @__PURE__ */ new WeakSet();
56
- _Symbol$iterator = Symbol.iterator;
57
- _Symbol$for = Symbol.for("nodejs.util.inspect.custom");
58
431
  var Say = class Say {
432
+ #locales;
433
+ #loader;
434
+ #messages;
435
+ #formats;
436
+ #active;
59
437
  constructor(options) {
60
- _classPrivateMethodInitSpec(this, _Say_brand);
61
- _classPrivateFieldInitSpec(this, _locales, void 0);
62
- _classPrivateFieldInitSpec(this, _loader, void 0);
63
- _classPrivateFieldInitSpec(this, _messages, void 0);
64
- _classPrivateFieldInitSpec(this, _formats, void 0);
65
- _classPrivateFieldInitSpec(this, _active, void 0);
66
- _classPrivateFieldSet2(_locales, this, options.locales);
67
- _classPrivateFieldSet2(_loader, this, options.loader);
68
- _classPrivateFieldSet2(_messages, this, /* @__PURE__ */ new Map());
69
- _classPrivateFieldSet2(_formats, this, /* @__PURE__ */ new Map());
438
+ this.#locales = options.locales;
439
+ this.#loader = options.loader;
440
+ this.#messages = /* @__PURE__ */ new Map();
441
+ this.#formats = /* @__PURE__ */ new Map();
70
442
  if (options.messages) this.assign(options.messages);
71
443
  }
72
- /**
73
- * The currently active locale.
74
- *
75
- * @throws If no locale is active
76
- */
77
444
  get locale() {
78
- if (!_classPrivateFieldGet2(_active, this)) throw new Error("No active locale");
79
- return _classPrivateFieldGet2(_active, this);
445
+ if (!this.#active) throw new Error("No active locale");
446
+ return this.#active;
80
447
  }
81
- /**
82
- * All available messages mapped by locale.
83
- *
84
- * @throws If no locale is active
85
- * @throws If no messages are available for the active locale
86
- */
87
448
  get messages() {
88
449
  /* v8 ignore next */
89
- if (!_classPrivateFieldGet2(_messages, this).has(this.locale)) throw new Error("No messages loaded for locale");
90
- return _classPrivateFieldGet2(_messages, this).get(this.locale);
450
+ if (!this.#messages.has(this.locale)) throw new Error("No messages loaded for locale");
451
+ return this.#messages.get(this.locale);
91
452
  }
92
- /**
93
- * All available locales.
94
- */
95
453
  get locales() {
96
- return _classPrivateFieldGet2(_locales, this);
454
+ return this.#locales;
97
455
  }
98
- /**
99
- * Loads messages for the given locales.
100
- * If no locales are provided, all available locales are loaded.
101
- * Requires a {@link Say.Loader} to be provided.
102
- * If `loader` returns a promise, so will this method.
103
- *
104
- * @param locales Locales to load messages for, defaults to {@link Say.locales}
105
- * @returns This
106
- */
107
456
  load(...locales) {
108
457
  if (Object.isFrozen(this)) throw new Error("Cannot load messages on a frozen Say");
109
- if (locales.length === 0) locales = _classPrivateFieldGet2(_locales, this);
458
+ if (locales.length === 0) locales = this.#locales;
110
459
  const tasks = [];
111
460
  for (const locale of locales) {
112
- if (_classPrivateFieldGet2(_messages, this).has(locale)) continue;
113
- if (!_classPrivateFieldGet2(_loader, this)) throw new Error("No loader provided, cannot load messages");
114
- const result = _classPrivateFieldGet2(_loader, this).call(this, locale);
461
+ if (this.#messages.has(locale)) continue;
462
+ if (!this.#loader) throw new Error("No loader provided, cannot load messages");
463
+ const result = this.#loader(locale);
115
464
  if (result instanceof Promise) {
116
465
  const task = result.then((m) => this.assign(locale, m));
117
466
  tasks.push(task);
@@ -121,209 +470,75 @@ var Say = class Say {
121
470
  }
122
471
  assign(localeOrMessages, maybeMessages) {
123
472
  if (Object.isFrozen(this)) throw new Error("Cannot assign messages on a frozen Say");
124
- if (typeof localeOrMessages === "string") _classPrivateFieldGet2(_messages, this).set(localeOrMessages, maybeMessages);
125
- else for (const locale in localeOrMessages) _classPrivateFieldGet2(_messages, this).set(locale, localeOrMessages[locale]);
473
+ if (typeof localeOrMessages === "string") this.#messages.set(localeOrMessages, maybeMessages);
474
+ else for (const locale in localeOrMessages) this.#messages.set(locale, localeOrMessages[locale]);
126
475
  return this;
127
476
  }
128
- /**
129
- * Set the active locale.
130
- *
131
- * @param locale Locale to set
132
- * @returns This
133
- * @throws If locale is not available
134
- */
135
477
  activate(locale) {
136
478
  if (Object.isFrozen(this)) throw new Error("Cannot activate locale on a frozen Say");
137
- if (!_classPrivateFieldGet2(_messages, this).has(locale)) throw new Error("No messages loaded for locale");
138
- _classPrivateFieldSet2(_active, this, locale);
479
+ if (!this.#messages.has(locale)) throw new Error("No messages loaded for locale");
480
+ this.#active = locale;
139
481
  return this;
140
482
  }
141
- /**
142
- * Creates a clone of the Say instance, with the same locales and messages.
143
- *
144
- * @returns A clone of the Say instance
145
- */
146
483
  clone() {
147
484
  const copy = new Say({
148
- locales: _classPrivateFieldGet2(_locales, this),
149
- messages: Object.fromEntries(_classPrivateFieldGet2(_messages, this)),
150
- loader: _classPrivateFieldGet2(_loader, this)
485
+ locales: this.#locales,
486
+ messages: Object.fromEntries(this.#messages),
487
+ loader: this.#loader
151
488
  });
152
- _classPrivateFieldSet2(_active, copy, _classPrivateFieldGet2(_active, this));
489
+ copy.#active = this.#active;
153
490
  return copy;
154
491
  }
155
- /**
156
- * Make this `Say` instance immutable.
157
- */
158
492
  freeze() {
159
493
  return Object.freeze(this);
160
494
  }
161
- *[_Symbol$iterator]() {
162
- for (const l of _classPrivateFieldGet2(_locales, this)) yield [this.clone().activate(l).freeze(), l];
495
+ *[Symbol.iterator]() {
496
+ for (const l of this.#locales) yield [this.clone().activate(l).freeze(), l];
163
497
  }
164
- /**
165
- * Matches the best locale from a list of guesses.
166
- *
167
- * @param guesses List of locale guesses
168
- *
169
- * @returns The best matching locale, or the first locale if no matches are found
170
- */
171
498
  match(...guesses) {
172
499
  const flat = guesses.flat();
173
- if (flat.length === 0) return _classPrivateFieldGet2(_locales, this)[0];
500
+ if (flat.length === 0) return this.#locales[0];
174
501
  for (const guess of flat) {
175
- if (_classPrivateFieldGet2(_locales, this).includes(guess)) return guess;
502
+ if (this.#locales.includes(guess)) return guess;
176
503
  const prefix = guess.split("-")[0];
177
504
  if (!prefix) continue;
178
- const match = _classPrivateFieldGet2(_locales, this).find((l) => l.startsWith(prefix));
505
+ const match = this.#locales.find((l) => l.startsWith(prefix));
179
506
  if (match) return match;
180
507
  }
181
- return _classPrivateFieldGet2(_locales, this)[0];
508
+ return this.#locales[0];
182
509
  }
183
- /**
184
- * Get the translation for a descriptor.
185
- *
186
- * @param descriptor Descriptor to get the translation for
187
- * @returns The translation string for the descriptor
188
- * @throws If no locale is active
189
- * @throws If no messages are available for the active locale
190
- * @throws If descriptor id is not found
191
- */
192
510
  call(descriptor) {
193
- return _assertClassBrand(_Say_brand, this, _call).call(this, this.locale, this.messages, descriptor);
511
+ return this.#call(this.locale, this.messages, descriptor);
194
512
  }
195
- [_Symbol$for](_depth, context, inspect) {
196
- if (_classPrivateFieldGet2(_active, this)) return `${this.constructor.name}<${inspect(_classPrivateFieldGet2(_active, this), context)}> {}`;
513
+ #call(locale, messages, descriptor) {
514
+ const message = messages[descriptor.id];
515
+ if (typeof message !== "string") throw new Error(`Message for ${descriptor.id} is not a string`);
516
+ const key = `${locale}:${descriptor.id}`;
517
+ const format = this.#formats.get(key) ?? this.#formats.set(key, compile(locale, message)).get(key);
518
+ return String(format.format(resolveDescriptorValues(descriptor)));
519
+ }
520
+ [Symbol.for("nodejs.util.inspect.custom")](_depth, context, inspect) {
521
+ if (this.#active) return `${this.constructor.name}<${inspect(this.#active, context)}> {}`;
197
522
  else return `${this.constructor.name} {}`;
198
523
  }
199
- /**
200
- * Define a pluralised message.
201
- *
202
- * @example
203
- * ```ts
204
- * say.plural(count, {
205
- * one: 'You have 1 item',
206
- * other: `You have ${count} items`,
207
- * })
208
- * ```
209
- *
210
- * Interpolating the selector into a branch extracts as ICU's `#`, the number
211
- * the message branched on. A `#` you write yourself is text.
212
- * @param _ Number to determine the plural form of
213
- * @param options Pluralisation rules keyed by CLDR categories or specific numbers
214
- * @returns The plural form of the number
215
- * @remark This is a macro and must be used with the relevant saykit plugin
216
- */
217
524
  plural(_, options) {
218
525
  throw new Error("'Say#plural' is a macro and must be used with the relevant saykit plugin");
219
526
  }
220
- /**
221
- * Define an ordinal message (e.g. "1st", "2nd", "3rd").
222
- *
223
- * Interpolating the selector into a branch extracts as ICU's `#`, the number
224
- * the message branched on. A `#` you write yourself is text.
225
- *
226
- * @example
227
- * ```ts
228
- * say.ordinal(position, {
229
- * 1: `${position}st`,
230
- * 2: `${position}nd`,
231
- * 3: `${position}rd`,
232
- * other: `${position}th`,
233
- * })
234
- * ```
235
- *
236
- * @param _ Number to determine the ordinal form of
237
- * @param options Ordinal rules keyed by CLDR categories or specific numbers
238
- * @returns The ordinal form of the number
239
- * @remark This is a macro and must be used with the relevant saykit plugin
240
- */
241
527
  ordinal(_, options) {
242
528
  throw new Error("'Say#ordinal' is a macro and must be used with the relevant saykit plugin");
243
529
  }
244
- /**
245
- * Define a select message, useful for handling gender, status, or other categories.
246
- *
247
- * @example
248
- * ```ts
249
- * say.select(gender, {
250
- * male: 'He',
251
- * female: 'She',
252
- * other: 'They',
253
- * })
254
- * ```
255
- *
256
- * @param _ Selector value to determine which option is chosen
257
- * @param options A mapping of possible selector values to message strings
258
- * @returns The select form of the value
259
- * @remark This is a macro and must be used with the relevant saykit plugin
260
- */
261
530
  select(_, options) {
262
531
  throw new Error("'Say#select' is a macro and must be used with the relevant saykit plugin");
263
532
  }
264
- /**
265
- * Format a number the way the active locale writes one, with its own grouping
266
- * separators and decimal mark.
267
- *
268
- * Unlike `plural`, `ordinal`, and `select`, this is a fragment rather than a
269
- * whole message, and is normally written inside one.
270
- *
271
- * @example
272
- * ```ts
273
- * say`You have ${say.number(items.length)} items`
274
- * say`Battery at ${say.number(level, { style: 'percent' })}`
275
- * say`Total: ${say.number({ cartTotal: getTotal() }, { style: '#,##0.00' })}`
276
- * ```
277
- *
278
- * @param _ Number to format
279
- * @param options Formatting style, either a named style or a literal number pattern
280
- * @returns The formatted number
281
- * @remark This is a macro and must be used with the relevant saykit plugin
282
- */
283
533
  number(_, options) {
284
534
  throw new Error("'Say#number' is a macro and must be used with the relevant saykit plugin");
285
535
  }
286
- /**
287
- * Format the date portion of a value the way the active locale writes one.
288
- *
289
- * @example
290
- * ```ts
291
- * say`Published ${say.date(post.publishedAt)}`
292
- * say`Published ${say.date(post.publishedAt, { style: 'full' })}`
293
- * ```
294
- *
295
- * @param _ Date to format
296
- * @param options Formatting style
297
- * @returns The formatted date
298
- * @remark This is a macro and must be used with the relevant saykit plugin
299
- */
300
536
  date(_, options) {
301
537
  throw new Error("'Say#date' is a macro and must be used with the relevant saykit plugin");
302
538
  }
303
- /**
304
- * Format the time portion of a value the way the active locale writes one.
305
- *
306
- * @example
307
- * ```ts
308
- * say`Doors open at ${say.time(opensAt)}`
309
- * say`Doors open at ${say.time(opensAt, { style: 'short' })}`
310
- * ```
311
- *
312
- * @param _ Date to format
313
- * @param options Formatting style
314
- * @returns The formatted time
315
- * @remark This is a macro and must be used with the relevant saykit plugin
316
- */
317
539
  time(_, options) {
318
540
  throw new Error("'Say#time' is a macro and must be used with the relevant saykit plugin");
319
541
  }
320
542
  };
321
- function _call(locale, messages, descriptor) {
322
- const message = messages[descriptor.id];
323
- if (typeof message !== "string") throw new Error(`Message for ${descriptor.id} is not a string`);
324
- const key = `${locale}:${descriptor.id}`;
325
- const format = _classPrivateFieldGet2(_formats, this).get(key) ?? _classPrivateFieldGet2(_formats, this).set(key, mf1ToMessage(locale, message)).get(key);
326
- return String(format.format(resolveDescriptorValues(descriptor)));
327
- }
328
543
  //#endregion
329
544
  export { Say };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saykit",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Type-safe i18n library with compile-time macro transforms",
5
5
  "keywords": [
6
6
  "i18n",
@@ -25,6 +25,7 @@
25
25
  "!dist/**/*.map"
26
26
  ],
27
27
  "type": "module",
28
+ "sideEffects": false,
28
29
  "exports": {
29
30
  ".": {
30
31
  "types": "./dist/runtime.d.mts",
@@ -36,7 +37,10 @@
36
37
  "provenance": true
37
38
  },
38
39
  "dependencies": {
39
- "@messageformat/icu-messageformat-1": "^0.12.0"
40
+ "@messageformat/date-skeleton": "2.0.0-0",
41
+ "@messageformat/number-skeleton": "2.0.0-0",
42
+ "@messageformat/parser": "^5.1.1",
43
+ "messageformat": "^4.0.0"
40
44
  },
41
45
  "scripts": {
42
46
  "check": "tsc --noEmit",