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 CHANGED
@@ -4,9 +4,9 @@
4
4
 
5
5
  [![Coverage](https://codecov.io/gh/k0d13/saykit/graph/badge.svg?flag=integration)](https://codecov.io/gh/k0d13/saykit?flags%5B0%5D=integration)
6
6
 
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.
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 its compiled messages, 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`), 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.
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, and `saykit compile` turns each catalogue into a `.js` module of data that you import.
10
10
 
11
11
  ## Install
12
12
 
@@ -20,8 +20,8 @@ You will normally also want [`@saykit/config`](https://github.com/k0d13/saykit/t
20
20
 
21
21
  ```ts
22
22
  import { createCatalogue } from 'saykit';
23
- import en from './locales/en.po';
24
- import fr from './locales/fr.po';
23
+ import en from './locales/en.js';
24
+ import fr from './locales/fr.js';
25
25
 
26
26
  const catalogue = createCatalogue({ en, fr });
27
27
 
package/dist/index.d.mts CHANGED
@@ -1,14 +1,63 @@
1
- //#region src/types.d.ts
2
- type Tuple = [any, ...any[]];
3
- type Disallow<T, K extends PropertyKey> = T & Partial<Record<K, never>>;
4
- type Awaitable<T> = T | PromiseLike<T>;
1
+ /**
2
+ * What each node in a compiled message is, by the number it starts with.
3
+ *
4
+ * The numbers are the format, so a kind is only ever appended, never renumbered or reused.
5
+ */
6
+ export declare const Kind: {
7
+ readonly Value: 0;
8
+ readonly Number: 1;
9
+ readonly DateTime: 2;
10
+ readonly Plural: 3;
11
+ readonly Ordinal: 4;
12
+ readonly Select: 5;
13
+ readonly Hash: 6;
14
+ };
15
+ export type Kind = (typeof Kind)[keyof typeof Kind];
16
+ /**
17
+ * A compiled message.
18
+ *
19
+ * The CLI compiles a catalogue to this: strings and arrays, with styles already resolved to
20
+ * `Intl` options. It is plain JSON, so it can cross a server/client boundary.
21
+ *
22
+ * A node is an array starting with its {@link Kind}; an array starting with anything else is a
23
+ * concatenation. Operands are descriptor keys, never nodes. A message with no placeholders is its
24
+ * own string.
25
+ */
26
+ type Message = string | Message[] | [typeof Kind.Value, string] | [typeof Kind.Number, string, Intl.NumberFormatOptions?, number?] | [typeof Kind.DateTime, string, Intl.DateTimeFormatOptions?] | [typeof Kind.Plural, string, Message.Cases, number?] | [typeof Kind.Ordinal, string, Message.Cases, number?] | [typeof Kind.Select, string, Message.Cases] | [typeof Kind.Hash];
27
+ declare namespace Message {
28
+ /**
29
+ * A plural's or a select's branches, by key. A plural's keys are CLDR
30
+ * categories or an exact `=n`, and `other` is the fallback for both.
31
+ */
32
+ type Cases = {
33
+ [key: string]: Message;
34
+ };
35
+ /** What a compiled message is called with: the descriptor the transform built. */
36
+ type Values = Record<string, unknown>;
37
+ }
38
+ /**
39
+ * Compile one message into the function that formats it.
40
+ *
41
+ * Called once per message per locale; a view memoises the result.
42
+ *
43
+ * @param message The compiled message, as the CLI emitted it
44
+ * @param locale The locale to format in
45
+ * @param options Whether written values are wrapped in bidi isolation marks
46
+ * @returns A function from a descriptor to the formatted string
47
+ */
48
+ export declare function compileMessage(message: Message, locale: string, options?: {
49
+ bidiIsolation?: 'default' | 'none';
50
+ }): (values: Message.Values) => string;
51
+ export type Tuple = [any, ...any[]];
52
+ export type Disallow<T, K extends PropertyKey> = T & Partial<Record<K, never>>;
53
+ export type Awaitable<T> = T | PromiseLike<T>;
5
54
  /**
6
55
  * A value wrapped in the name its ICU placeholder should take, written inline
7
56
  * as a single-key object: `` say`Total: ${{ cartTotal: getTotal() }}` ``. The
8
57
  * transform reads the key at build time and compiles only the value, so this
9
58
  * is never a real object at runtime.
10
59
  */
11
- type Named<T> = {
60
+ export type Named<T> = {
12
61
  [name: string]: T;
13
62
  };
14
63
  /**
@@ -17,7 +66,7 @@ type Named<T> = {
17
66
  * `Branch` is what a case may be written as: text everywhere the message is
18
67
  * text, widening in JSX, where a case showing the number has to be a fragment.
19
68
  */
20
- interface NumeralOptions<Branch = string> extends Omit<Partial<Record<Intl.LDMLPluralRule, Branch>>, 'other'> {
69
+ export interface NumeralOptions<Branch = string> extends Omit<Partial<Record<Intl.LDMLPluralRule, Branch>>, 'other'> {
21
70
  other: Branch;
22
71
  [digit: number]: Branch;
23
72
  /**
@@ -26,7 +75,7 @@ interface NumeralOptions<Branch = string> extends Omit<Partial<Record<Intl.LDMLP
26
75
  */
27
76
  offset?: number;
28
77
  }
29
- interface SelectOptions<Branch = string> {
78
+ export interface SelectOptions<Branch = string> {
30
79
  other: Branch;
31
80
  [match: string | number]: Branch;
32
81
  }
@@ -38,35 +87,38 @@ interface SelectOptions<Branch = string> {
38
87
  * A skeleton is where the formats the named styles have no word for live:
39
88
  * currency, compact notation, a year and month with no day.
40
89
  */
41
- type Skeleton = `::${string}`;
90
+ export type Skeleton = `::${string}`;
42
91
  /**
43
92
  * Formatting for a `{arg, number}` placeholder.
44
93
  *
45
94
  * A literal `NumberFormat` pattern such as `#,##0.00` is also accepted, for the
46
95
  * cases neither the named styles nor a skeleton spell more clearly.
47
96
  */
48
- interface NumberOptions {
97
+ export interface NumberOptions {
49
98
  style?: 'integer' | 'percent' | Skeleton | (string & {});
50
99
  }
51
100
  /** Formatting for a `{arg, date}` or `{arg, time}` placeholder. */
52
- interface DateTimeOptions {
101
+ export interface DateTimeOptions {
53
102
  style?: 'short' | 'medium' | 'long' | 'full' | Skeleton;
54
103
  }
55
- //#endregion
56
- //#region src/view.d.ts
57
104
  declare namespace View {
105
+ /**
106
+ * One locale's messages, compiled.
107
+ *
108
+ * Each is a {@link Message} the CLI compiled from the catalogue. They are plain data, so a
109
+ * server can hand them to a client tree.
110
+ */
58
111
  type Messages = {
59
- [key: string]: string;
112
+ [key: string]: Message;
60
113
  };
61
114
  /**
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.
115
+ * What the transform compiles a message down to: its extracted id, or the compiled message
116
+ * itself when there was nothing to extract. Values sit behind one underscore each.
65
117
  */
66
118
  type Descriptor = ({
67
119
  id: string;
68
120
  } | {
69
- message: string;
121
+ message: Message;
70
122
  }) & {
71
123
  [match: string | number]: unknown;
72
124
  };
@@ -137,9 +189,8 @@ interface View<Locale extends string = string> {
137
189
  /**
138
190
  * Format the message a descriptor names, or the message it carries.
139
191
  *
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.
192
+ * A message with nothing to translate, such as a lone `say.date(x)`, is never extracted:
193
+ * the transform writes the compiled message into the call instead of an id.
143
194
  *
144
195
  * @param descriptor Descriptor to format
145
196
  * @returns The formatted message
@@ -265,18 +316,14 @@ interface View<Locale extends string = string> {
265
316
  * Create a view over one locale and the messages it formats against.
266
317
  *
267
318
  * A catalogue memoises one per locale, which is how application code usually
268
- * reaches one; a single-locale app can build one directly. The format cache
269
- * belongs to the view, so a view built over one set of messages can never be
270
- * served a format compiled from another.
319
+ * reaches one; a single-locale app can build one directly.
271
320
  *
272
321
  * @param locale The locale to bind to
273
322
  * @param messages The messages this view formats against
274
323
  * @param options How this view formats its messages
275
324
  * @returns The view
276
325
  */
277
- declare function createView<Locale extends string>(locale: Locale, messages: View.Messages, options?: View.Options): View<Locale>;
278
- //#endregion
279
- //#region src/catalogue.d.ts
326
+ export declare function createView<Locale extends string>(locale: Locale, messages: View.Messages, options?: View.Options): View<Locale>;
280
327
  declare namespace Catalogue {
281
328
  /**
282
329
  * One guess at a locale, or several. Guesses are read from cookies, headers
@@ -355,8 +402,23 @@ interface Catalogue<Locale extends string = string> {
355
402
  /**
356
403
  * Matches the best locale from a list of guesses.
357
404
  *
405
+ * Guesses are tried in order, and an earlier guess that matches at all wins
406
+ * over a later one that would match more closely. Each guess is
407
+ * canonicalised first, so case does not matter (`en-us` is `en-US`), and
408
+ * then compared in steps, most specific first:
409
+ *
410
+ * 1. The same tag
411
+ * 2. The same language and script, filling in the script a tag implies, so
412
+ * `zh-HK` (Traditional) finds `zh-TW` rather than `zh-CN`
413
+ * 3. The same language subtag, whole, so `fi` never matches `fil`
414
+ *
415
+ * Within a step, the locale written first wins, so with `pt-BR` before
416
+ * `pt-PT`, `pt` matches `pt-BR`.
417
+ *
358
418
  * An absent or empty guess is skipped rather than throwing, which lets a
359
419
  * caller write `match(fromCookie, fromHeader)` without narrowing each first.
420
+ * So is one that is not a well-formed locale tag, unless it is written
421
+ * exactly as one of {@link locales}.
360
422
  *
361
423
  * @param guesses List of locale guesses
362
424
  * @returns The best matching locale, or the first of {@link locales} if no
@@ -391,9 +453,7 @@ interface Catalogue<Locale extends string = string> {
391
453
  * messages, such as `{ bidiIsolation: 'none' }`
392
454
  * @returns The catalogue
393
455
  */
394
- declare function createCatalogue<const Locale extends string = string>(messages: Record<Locale, Catalogue.Source>, options?: Catalogue.Options): Catalogue<Locale>;
395
- //#endregion
396
- //#region src/store.d.ts
456
+ export declare function createCatalogue<const Locale extends string = string>(messages: Record<Locale, Catalogue.Source>, options?: Catalogue.Options): Catalogue<Locale>;
397
457
  declare namespace Store {
398
458
  /** Called after a successful switch, with the view that is now current. */
399
459
  type Listener<Locale extends string> = (view: View<Locale>) => void;
@@ -474,9 +534,7 @@ interface Store<Locale extends string = string> {
474
534
  * @returns The store
475
535
  * @throws If the starting locale has no messages loaded
476
536
  */
477
- declare function createStore<Locale extends string>(catalogue: Catalogue<Locale>, locale?: Locale): Store<Locale>;
478
- //#endregion
479
- //#region src/scope.d.ts
537
+ export declare function createStore<Locale extends string>(catalogue: Catalogue<Locale>, locale?: Locale): Store<Locale>;
480
538
  declare namespace Scope {
481
539
  /**
482
540
  * Where a scope holds the view a piece of work is running under.
@@ -585,6 +643,5 @@ interface Scope {
585
643
  * browser wants
586
644
  * @returns The scope
587
645
  */
588
- declare function createScope(storage?: Scope.Storage): Scope;
589
- //#endregion
590
- export { Awaitable, type Catalogue, DateTimeOptions, Disallow, Named, NumberOptions, NumeralOptions, type Scope, SelectOptions, Skeleton, type Store, Tuple, type View, createCatalogue, createScope, createStore, createView };
646
+ export declare function createScope(storage?: Scope.Storage): Scope;
647
+ export type { Catalogue, Message, Scope, Store, View };