saykit 0.10.0 → 0.11.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/README.md +3 -1
- package/dist/index.d.mts +47 -15
- package/dist/index.mjs +20 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
The core runtime for [SayKit](https://saykit.js.org). Exports `createCatalogue`, which holds your locales and where each one's messages come from, `createView`, which binds one locale and formats messages using ICU MessageFormat, and `createStore`, which holds the current view and swaps it when you switch locale.
|
|
8
8
|
|
|
9
|
-
You author messages with the `` say`...` `` tagged template (and `say.plural`, `say.ordinal`, `say.select`)
|
|
9
|
+
You author messages with the `` say`...` `` tagged template (and `say.plural`, `say.ordinal`, `say.select`), and format numbers, dates and times for the locale with `say.number`, `say.date` and `say.time`; a SayKit build-tool plugin rewrites them at build time into small runtime calls.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -29,6 +29,8 @@ const say = catalogue.locale('en');
|
|
|
29
29
|
|
|
30
30
|
say`Hello, ${name}!`;
|
|
31
31
|
say.plural(count, { one: '1 item', other: `${count} items` });
|
|
32
|
+
say`Total: ${say.number(total, { style: '::currency/EUR' })}`;
|
|
33
|
+
say.date(new Date(), { style: 'long' });
|
|
32
34
|
```
|
|
33
35
|
|
|
34
36
|
In a browser, where the locale can change, hold a store instead of a view:
|
package/dist/index.d.mts
CHANGED
|
@@ -58,6 +58,32 @@ declare namespace View {
|
|
|
58
58
|
type Messages = {
|
|
59
59
|
[key: string]: string;
|
|
60
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* What the transform compiles a message down to: the id it was extracted
|
|
63
|
+
* under, or the ICU message itself when there was nothing to extract, with
|
|
64
|
+
* its values behind one underscore each.
|
|
65
|
+
*/
|
|
66
|
+
type Descriptor = ({
|
|
67
|
+
id: string;
|
|
68
|
+
} | {
|
|
69
|
+
message: string;
|
|
70
|
+
}) & {
|
|
71
|
+
[match: string | number]: unknown;
|
|
72
|
+
};
|
|
73
|
+
interface Options {
|
|
74
|
+
/**
|
|
75
|
+
* Whether a substituted value is wrapped in Unicode bidi isolation marks,
|
|
76
|
+
* U+2068 and U+2069, so right-to-left text cannot reorder the sentence
|
|
77
|
+
* around it.
|
|
78
|
+
*
|
|
79
|
+
* `'none'` leaves values byte-for-byte as given, for output that is parsed
|
|
80
|
+
* after it is formatted: a Discord mention such as `<#123>` stops being
|
|
81
|
+
* one once the id inside it is isolated.
|
|
82
|
+
*
|
|
83
|
+
* @default 'default'
|
|
84
|
+
*/
|
|
85
|
+
bidiIsolation?: 'default' | 'none';
|
|
86
|
+
}
|
|
61
87
|
}
|
|
62
88
|
/**
|
|
63
89
|
* One locale, bound to the messages it formats against.
|
|
@@ -104,16 +130,22 @@ interface View<Locale extends string = string> {
|
|
|
104
130
|
/** The messages this view formats against. */
|
|
105
131
|
readonly messages: Readonly<View.Messages>;
|
|
106
132
|
/**
|
|
107
|
-
*
|
|
133
|
+
* How this view formats its messages. Plain data, so it can cross a
|
|
134
|
+
* server/client boundary alongside {@link locale} and {@link messages}.
|
|
135
|
+
*/
|
|
136
|
+
readonly options: Readonly<View.Options>;
|
|
137
|
+
/**
|
|
138
|
+
* Format the message a descriptor names, or the message it carries.
|
|
139
|
+
*
|
|
140
|
+
* A message with nothing to translate, a lone `say.date(x)` with no text
|
|
141
|
+
* around it, is never extracted: the transform writes the ICU into the call
|
|
142
|
+
* instead of an id.
|
|
108
143
|
*
|
|
109
144
|
* @param descriptor Descriptor to format
|
|
110
145
|
* @returns The formatted message
|
|
111
146
|
* @throws If the id has no message
|
|
112
147
|
*/
|
|
113
|
-
call(descriptor:
|
|
114
|
-
id: string;
|
|
115
|
-
[match: string | number]: unknown;
|
|
116
|
-
}): string;
|
|
148
|
+
call(descriptor: View.Descriptor): string;
|
|
117
149
|
/**
|
|
118
150
|
* Define a pluralised message.
|
|
119
151
|
*
|
|
@@ -239,9 +271,10 @@ interface View<Locale extends string = string> {
|
|
|
239
271
|
*
|
|
240
272
|
* @param locale The locale to bind to
|
|
241
273
|
* @param messages The messages this view formats against
|
|
274
|
+
* @param options How this view formats its messages
|
|
242
275
|
* @returns The view
|
|
243
276
|
*/
|
|
244
|
-
declare function createView<Locale extends string>(locale: Locale, messages: View.Messages): View<Locale>;
|
|
277
|
+
declare function createView<Locale extends string>(locale: Locale, messages: View.Messages, options?: View.Options): View<Locale>;
|
|
245
278
|
//#endregion
|
|
246
279
|
//#region src/catalogue.d.ts
|
|
247
280
|
declare namespace Catalogue {
|
|
@@ -268,13 +301,8 @@ declare namespace Catalogue {
|
|
|
268
301
|
type Produced = View.Messages | {
|
|
269
302
|
default: View.Messages;
|
|
270
303
|
};
|
|
271
|
-
/**
|
|
272
|
-
|
|
273
|
-
*
|
|
274
|
-
* The keys are the catalogue's locales, in the order they are written, and
|
|
275
|
-
* the first of them is the default locale.
|
|
276
|
-
*/
|
|
277
|
-
type Options<Locale extends string> = Record<Locale, Source>;
|
|
304
|
+
/** How every view the catalogue hands back formats its messages. */
|
|
305
|
+
type Options = View.Options;
|
|
278
306
|
}
|
|
279
307
|
/**
|
|
280
308
|
* Everything an application knows how to say: its locales and where their
|
|
@@ -356,10 +384,14 @@ interface Catalogue<Locale extends string = string> {
|
|
|
356
384
|
* const say = catalogue.locale('en');
|
|
357
385
|
* ```
|
|
358
386
|
*
|
|
359
|
-
* @param messages Where each locale's messages come from, keyed by locale
|
|
387
|
+
* @param messages Where each locale's messages come from, keyed by locale. The
|
|
388
|
+
* keys are the catalogue's locales, in the order they are written, and the
|
|
389
|
+
* first of them is the default locale
|
|
390
|
+
* @param options How every view the catalogue hands back formats its
|
|
391
|
+
* messages, such as `{ bidiIsolation: 'none' }`
|
|
360
392
|
* @returns The catalogue
|
|
361
393
|
*/
|
|
362
|
-
declare function createCatalogue<const Locale extends string = string>(messages: Catalogue.Options
|
|
394
|
+
declare function createCatalogue<const Locale extends string = string>(messages: Record<Locale, Catalogue.Source>, options?: Catalogue.Options): Catalogue<Locale>;
|
|
363
395
|
//#endregion
|
|
364
396
|
//#region src/store.d.ts
|
|
365
397
|
declare namespace Store {
|
package/dist/index.mjs
CHANGED
|
@@ -420,28 +420,33 @@ const functions = {
|
|
|
420
420
|
};
|
|
421
421
|
//#endregion
|
|
422
422
|
//#region src/messageformat/index.ts
|
|
423
|
-
function compile(locale, source) {
|
|
424
|
-
return new MessageFormat(locale, toMessage(parse(source)), {
|
|
423
|
+
function compile(locale, source, options) {
|
|
424
|
+
return new MessageFormat(locale, toMessage(parse(source)), {
|
|
425
|
+
...options,
|
|
426
|
+
functions
|
|
427
|
+
});
|
|
425
428
|
}
|
|
426
429
|
//#endregion
|
|
427
430
|
//#region src/view.ts
|
|
428
431
|
function resolveDescriptorValues(descriptor) {
|
|
429
|
-
return Object.fromEntries(Object.entries(descriptor).filter(([key]) => key !== "id").map(([key, value]) => [key.startsWith("_") ? key.slice(1) : key, value]));
|
|
432
|
+
return Object.fromEntries(Object.entries(descriptor).filter(([key]) => key !== "id" && key !== "message").map(([key, value]) => [key.startsWith("_") ? key.slice(1) : key, value]));
|
|
430
433
|
}
|
|
431
434
|
function macro(name) {
|
|
432
435
|
throw new Error(`'say.${name}' is a macro and must be used with the relevant saykit plugin`);
|
|
433
436
|
}
|
|
434
|
-
function createView(locale, messages) {
|
|
437
|
+
function createView(locale, messages, options = {}) {
|
|
438
|
+
const { bidiIsolation } = options;
|
|
439
|
+
const ownOptions = Object.freeze(bidiIsolation ? { bidiIsolation } : {});
|
|
435
440
|
const own = Object.freeze({ ...messages });
|
|
436
441
|
const formats = /* @__PURE__ */ new Map();
|
|
437
442
|
const say = (() => {
|
|
438
443
|
throw new Error("'say' is a macro and must be used with the relevant saykit plugin");
|
|
439
444
|
});
|
|
440
445
|
function call(descriptor) {
|
|
441
|
-
const message = own[
|
|
442
|
-
if (typeof message !== "string") throw new Error(`Message for ${
|
|
443
|
-
let format = formats.get(
|
|
444
|
-
if (!format) formats.set(
|
|
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 }));
|
|
445
450
|
return String(format.format(resolveDescriptorValues(descriptor)));
|
|
446
451
|
}
|
|
447
452
|
return Object.freeze(Object.defineProperties(say, {
|
|
@@ -453,6 +458,10 @@ function createView(locale, messages) {
|
|
|
453
458
|
value: own,
|
|
454
459
|
enumerable: true
|
|
455
460
|
},
|
|
461
|
+
options: {
|
|
462
|
+
value: ownOptions,
|
|
463
|
+
enumerable: true
|
|
464
|
+
},
|
|
456
465
|
call: { value: call },
|
|
457
466
|
plural: { value: () => macro("plural") },
|
|
458
467
|
ordinal: { value: () => macro("ordinal") },
|
|
@@ -465,7 +474,8 @@ function createView(locale, messages) {
|
|
|
465
474
|
}
|
|
466
475
|
//#endregion
|
|
467
476
|
//#region src/catalogue.ts
|
|
468
|
-
function createCatalogue(messages) {
|
|
477
|
+
function createCatalogue(messages, options = {}) {
|
|
478
|
+
const { bidiIsolation } = options;
|
|
469
479
|
const sources = Object.assign(Object.create(null), messages);
|
|
470
480
|
const keys = Object.freeze(Object.keys(sources));
|
|
471
481
|
if (keys.length === 0) throw new Error("A catalogue needs at least one locale, none were given");
|
|
@@ -484,7 +494,7 @@ function createCatalogue(messages) {
|
|
|
484
494
|
const messages = store.get(locale);
|
|
485
495
|
if (!messages) throw new Error(typeof sources[locale] === "function" ? `Messages for locale '${locale}' have not been loaded yet` : `No messages for locale '${locale}'`);
|
|
486
496
|
let view = views.get(locale);
|
|
487
|
-
if (!view) views.set(locale, view = createView(locale, messages));
|
|
497
|
+
if (!view) views.set(locale, view = createView(locale, messages, { bidiIsolation }));
|
|
488
498
|
return view;
|
|
489
499
|
},
|
|
490
500
|
loaded(locale) {
|