@alxia/i18n 0.1.1 → 0.1.2

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
@@ -100,7 +100,8 @@ A response in the request's language varies by what decided it: give
100
100
  | --- | --- |
101
101
  | `createI18n({ resources, fallback, …languageOptions })` | the plugin — routes after it read `t` and `language` — with `t()`, `language()` and `supported` |
102
102
  | `I18nOptions` | its options: `resources`, `fallback`, and every `@alxia/language` option but `supported`; `resolve` may be annotated to read the app's context |
103
- | `KeyOf<Catalogue>`, `Translate<Key>`, `Catalogues`, `I18nContext<Key>` | its types |
103
+ | `KeyOf<Catalogue>` | a catalogue's dotted keys, nine levels deep; a deeper section gives `section.${string}` |
104
+ | `Translate<Key>`, `Catalogues`, `I18nContext<Key>` | its types |
104
105
 
105
106
  ## Documentation
106
107
 
package/dist/i18n.d.ts CHANGED
@@ -1,10 +1,27 @@
1
1
  import { type BaseContext, type RequiresOf } from '@alxia/core';
2
2
  import { type LanguageOptions } from '@alxia/language';
3
- import { type Path, type TranslationContext } from '@nxgt/i18n';
3
+ import { type TranslationContext } from '@nxgt/i18n';
4
4
  /** Catalogues by language: `{ en: { greeting: 'Hello {name}' }, fr: … }`. */
5
5
  export type Catalogues = Readonly<Record<string, Readonly<Record<string, unknown>>>>;
6
- /** Every key of a catalogue, dotted: `users.greeting`. */
7
- export type KeyOf<Catalogue> = Path<Catalogue> & string;
6
+ /**
7
+ * Every key of a catalogue, dotted: `users.greeting`. The keys of
8
+ * `@nxgt/i18n`'s `Path`, nine levels deep: a section nested deeper gives
9
+ * `${section}.${string}`, any key under it.
10
+ *
11
+ * `Path` recurses without a bound. A catalogue that is a type parameter —
12
+ * a function generic over the catalogues it hands to `createI18n` — leaves
13
+ * it deferred, and some checks then unfold it forever (TS2589, "Type
14
+ * instantiation is excessively deep and possibly infinite"). The depth
15
+ * stops them.
16
+ */
17
+ export type KeyOf<Catalogue> = KeysOf<Catalogue, 8> & string;
18
+ /** One level shallower: `Shallower[8]` is `7`, and `Shallower[0]` is `never`. */
19
+ type Shallower = [never, 0, 1, 2, 3, 4, 5, 6, 7];
20
+ /**
21
+ * The dotted keys of `T`, `Depth` more levels down at most. `Path`'s own
22
+ * test, `Record<string, any>`, kept so that the keys stay `Path`'s.
23
+ */
24
+ type KeysOf<T, Depth extends number, K extends keyof T = keyof T> = K extends string ? T[K] extends Record<string, any> ? [Shallower[Depth]] extends [never] ? `${K}.${string}` : `${K}.${KeysOf<T[K], Shallower[Depth]>}` : K : never;
8
25
  /** A translation of a key, in a language, formatted with ICU's `context`. */
9
26
  export type Translate<Key extends string> = (key: Key, context?: TranslationContext) => string;
10
27
  /**
@@ -53,4 +70,5 @@ export declare function createI18n<const C extends Catalogues, const Fallback ex
53
70
  language: () => keyof C & string;
54
71
  supported: (keyof C & string)[];
55
72
  };
73
+ export {};
56
74
  //# sourceMappingURL=i18n.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,WAAW,EAAgB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,KAAK,eAAe,EAAY,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAEN,KAAK,IAAI,EAET,KAAK,kBAAkB,EACvB,MAAM,YAAY,CAAC;AAEpB,6EAA6E;AAC7E,MAAM,MAAM,UAAU,GAAG,QAAQ,CAChC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CACjD,CAAC;AAEF,0DAA0D;AAC1D,MAAM,MAAM,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC;AAExD,6EAA6E;AAC7E,MAAM,MAAM,SAAS,CAAC,GAAG,SAAS,MAAM,IAAI,CAC3C,GAAG,EAAE,GAAG,EACR,OAAO,CAAC,EAAE,kBAAkB,KACxB,MAAM,CAAC;AAEZ;;;;GAIG;AACH,MAAM,WAAW,WAAW,CAC3B,CAAC,SAAS,UAAU,EACpB,QAAQ,SAAS,MAAM,CAAC,GAAG,MAAM,EACjC,GAAG,SAAS,MAAM,GAAG,WAAW,CAC/B,SAAQ,IAAI,CACZ,eAAe,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,GAAG,CAAC,EACtC,WAAW,GAAG,UAAU,CACxB;IACD;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;IACtB,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC5B;AAWD,8CAA8C;AAC9C,MAAM,WAAW,WAAW,CAAC,GAAG,SAAS,MAAM;IAC9C,8CAA8C;IAC9C,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU,CACzB,KAAK,CAAC,CAAC,SAAS,UAAU,EAC1B,KAAK,CAAC,QAAQ,SAAS,MAAM,CAAC,GAAG,MAAM,EACvC,GAAG,SAAS,MAAM,GAAG,WAAW,EAC/B,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC;IAiDtC,mFAAmF;OACvB,SAAS,oBAAK;IAC1E,mEAAmE;;;EAIpE"}
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,WAAW,EAAgB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,KAAK,eAAe,EAAY,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAGN,KAAK,kBAAkB,EACvB,MAAM,YAAY,CAAC;AAEpB,6EAA6E;AAC7E,MAAM,MAAM,UAAU,GAAG,QAAQ,CAChC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CACjD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,KAAK,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC;AAE7D,iFAAiF;AACjF,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAEjD;;;GAGG;AACH,KAAK,MAAM,CACV,CAAC,EACD,KAAK,SAAS,MAAM,EACpB,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,CAAC,IACxB,CAAC,SAAS,MAAM,GACjB,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC/B,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACjC,GAAG,CAAC,IAAI,MAAM,EAAE,GAChB,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,GACzC,CAAC,GACF,KAAK,CAAC;AAET,6EAA6E;AAC7E,MAAM,MAAM,SAAS,CAAC,GAAG,SAAS,MAAM,IAAI,CAC3C,GAAG,EAAE,GAAG,EACR,OAAO,CAAC,EAAE,kBAAkB,KACxB,MAAM,CAAC;AAEZ;;;;GAIG;AACH,MAAM,WAAW,WAAW,CAC3B,CAAC,SAAS,UAAU,EACpB,QAAQ,SAAS,MAAM,CAAC,GAAG,MAAM,EACjC,GAAG,SAAS,MAAM,GAAG,WAAW,CAC/B,SAAQ,IAAI,CACZ,eAAe,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,GAAG,CAAC,EACtC,WAAW,GAAG,UAAU,CACxB;IACD;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;IACtB,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC5B;AAWD,8CAA8C;AAC9C,MAAM,WAAW,WAAW,CAAC,GAAG,SAAS,MAAM;IAC9C,8CAA8C;IAC9C,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU,CACzB,KAAK,CAAC,CAAC,SAAS,UAAU,EAC1B,KAAK,CAAC,QAAQ,SAAS,MAAM,CAAC,GAAG,MAAM,EACvC,GAAG,SAAS,MAAM,GAAG,WAAW,EAC/B,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC;IAiDtC,mFAAmF;OACvB,SAAS,oBAAK;IAC1E,mEAAmE;;;EAIpE"}
package/dist/index.js.map CHANGED
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/i18n.ts"],
4
4
  "sourcesContent": [
5
- "import { AsyncLocalStorage } from 'node:async_hooks';\nimport { type BaseContext, definePlugin, type RequiresOf } from '@alxia/core';\nimport { type LanguageOptions, language } from '@alxia/language';\nimport {\n\tcreateTranslator,\n\ttype Path,\n\tregisterLanguageSource,\n\ttype TranslationContext,\n} from '@nxgt/i18n';\n\n/** Catalogues by language: `{ en: { greeting: 'Hello {name}' }, fr: … }`. */\nexport type Catalogues = Readonly<\n\tRecord<string, Readonly<Record<string, unknown>>>\n>;\n\n/** Every key of a catalogue, dotted: `users.greeting`. */\nexport type KeyOf<Catalogue> = Path<Catalogue> & string;\n\n/** A translation of a key, in a language, formatted with ICU's `context`. */\nexport type Translate<Key extends string> = (\n\tkey: Key,\n\tcontext?: TranslationContext,\n) => string;\n\n/**\n * `Ctx` is the type `resolve`'s parameter is annotated with —\n * `BaseContext & { user: User }`, or `{ user: User }` alone — and\n * `BaseContext` when it is not: what the plugin then requires of the app.\n */\nexport interface I18nOptions<\n\tC extends Catalogues,\n\tFallback extends keyof C & string,\n\tCtx extends object = BaseContext,\n> extends Omit<\n\t\tLanguageOptions<keyof C & string, Ctx>,\n\t\t'supported' | 'fallback'\n\t> {\n\t/**\n\t * The catalogues, one per language: their keys are the languages\n\t * supported. Spread `@nxgt/i18n`'s `resources` into them for its shared\n\t * keys — `errors.not-found`, `zod.*`.\n\t */\n\treadonly resources: C;\n\t/** The language spoken when the request names none, and whose keys type `t`. */\n\treadonly fallback: Fallback;\n}\n\n/**\n * The request's language as `@nxgt/i18n` hears it: the first an i18n plugin\n * read, when an app uses several. Opened fresh by each plugin's `around`.\n */\nconst requests = new AsyncLocalStorage<{ language?: string }>();\n\n/** `@nxgt/i18n`'s source: one function, so registering it again keeps one. */\nconst requestLanguage = () => requests.getStore()?.language;\n\n/** What the routes behind the plugin read. */\nexport interface I18nContext<Key extends string> {\n\t/** Translates into the request's language. */\n\treadonly t: Translate<Key>;\n}\n\n/**\n * Translations, as a plugin, on [`@nxgt/i18n`](https://www.npmjs.com/package/@nxgt/i18n):\n * `@alxia/language` reads the request's language among the catalogues', and\n * the routes declared after it read `language` and `t`, bound to it. Keys\n * are typed by the fallback's catalogue; messages are ICU — plurals,\n * selects, numbers — and a missing key answers itself.\n *\n * The plugin's own `t()` translates anywhere a request runs — a service, an\n * error's message — in that request's language, and in the fallback outside.\n *\n * ```ts\n * const i18n = createI18n({ resources: { en, fr }, fallback: 'en' });\n * app.use(i18n).get('/', ({ t, reply }) => reply(200, t('home.title')));\n * ```\n *\n * The plugin registers the request's language as one of `@nxgt/i18n`'s\n * language sources, so its own `getLanguage()` and `translate` — and every\n * nxgt package that translates through them — speak it too.\n */\nexport function createI18n<\n\tconst C extends Catalogues,\n\tconst Fallback extends keyof C & string,\n\tCtx extends object = BaseContext,\n>(options: I18nOptions<C, Fallback, Ctx>) {\n\ttype Language = keyof C & string;\n\ttype Key = KeyOf<C[Fallback]>;\n\tconst { resources, fallback, resolve, ...detect } = options;\n\tconst supported = Object.keys(resources) as Language[];\n\tconst translator = createTranslator<Key>(\n\t\tresources as Record<string, unknown>,\n\t);\n\t/**\n\t * This plugin's language for the request running, once `@alxia/language`\n\t * has read it. A global `around` hook opens it fresh for each request —\n\t * one made from inside another included — so it holds for everything the\n\t * request runs, `onError` hooks too, which run after the route failed.\n\t */\n\tconst current = new AsyncLocalStorage<{ language?: Language }>();\n\tconst spoken = (): Language => current.getStore()?.language ?? fallback;\n\tconst translate =\n\t\t(lang: Language): Translate<Key> =>\n\t\t(key, context) =>\n\t\t\ttranslator(key, context, lang as never);\n\n\t// nxgt's own getLanguage() and translate speak the request's language too.\n\tregisterLanguageSource(requestLanguage);\n\n\t// The plugin requires what `resolve` reads, and `use` checks the app gives\n\t// it; the `language()` inside is then handed `resolve` as reading only\n\t// `BaseContext`, since a context that is a type parameter defers the check.\n\tconst detected = language<Language>({\n\t\t...detect,\n\t\tsupported,\n\t\tfallback,\n\t\t...(resolve === undefined\n\t\t\t? {}\n\t\t\t: { resolve: resolve as (ctx: BaseContext) => string | undefined }),\n\t});\n\tconst plugin = definePlugin<RequiresOf<Ctx, 'resolve'>>()((app) =>\n\t\tapp\n\t\t\t.around((_ctx, next) => current.run({}, () => requests.run({}, next)))\n\t\t\t.use(detected)\n\t\t\t.derive(({ language: lang }): I18nContext<Key> => {\n\t\t\t\tconst own = current.getStore();\n\t\t\t\tif (own !== undefined) own.language = lang;\n\t\t\t\tconst heard = requests.getStore();\n\t\t\t\tif (heard !== undefined) heard.language ??= lang;\n\t\t\t\treturn { t: translate(lang) };\n\t\t\t}),\n\t);\n\n\treturn Object.assign(plugin, {\n\t\t/** Translates into the current request's language, or the fallback outside one. */\n\t\tt: ((key, context) => translate(spoken())(key, context)) as Translate<Key>,\n\t\t/** The current request's language, or the fallback outside one. */\n\t\tlanguage: spoken,\n\t\tsupported,\n\t});\n}\n"
5
+ "import { AsyncLocalStorage } from 'node:async_hooks';\nimport { type BaseContext, definePlugin, type RequiresOf } from '@alxia/core';\nimport { type LanguageOptions, language } from '@alxia/language';\nimport {\n\tcreateTranslator,\n\tregisterLanguageSource,\n\ttype TranslationContext,\n} from '@nxgt/i18n';\n\n/** Catalogues by language: `{ en: { greeting: 'Hello {name}' }, fr: … }`. */\nexport type Catalogues = Readonly<\n\tRecord<string, Readonly<Record<string, unknown>>>\n>;\n\n/**\n * Every key of a catalogue, dotted: `users.greeting`. The keys of\n * `@nxgt/i18n`'s `Path`, nine levels deep: a section nested deeper gives\n * `${section}.${string}`, any key under it.\n *\n * `Path` recurses without a bound. A catalogue that is a type parameter —\n * a function generic over the catalogues it hands to `createI18n` — leaves\n * it deferred, and some checks then unfold it forever (TS2589, \"Type\n * instantiation is excessively deep and possibly infinite\"). The depth\n * stops them.\n */\nexport type KeyOf<Catalogue> = KeysOf<Catalogue, 8> & string;\n\n/** One level shallower: `Shallower[8]` is `7`, and `Shallower[0]` is `never`. */\ntype Shallower = [never, 0, 1, 2, 3, 4, 5, 6, 7];\n\n/**\n * The dotted keys of `T`, `Depth` more levels down at most. `Path`'s own\n * test, `Record<string, any>`, kept so that the keys stay `Path`'s.\n */\ntype KeysOf<\n\tT,\n\tDepth extends number,\n\tK extends keyof T = keyof T,\n> = K extends string\n\t? T[K] extends Record<string, any>\n\t\t? [Shallower[Depth]] extends [never]\n\t\t\t? `${K}.${string}`\n\t\t\t: `${K}.${KeysOf<T[K], Shallower[Depth]>}`\n\t\t: K\n\t: never;\n\n/** A translation of a key, in a language, formatted with ICU's `context`. */\nexport type Translate<Key extends string> = (\n\tkey: Key,\n\tcontext?: TranslationContext,\n) => string;\n\n/**\n * `Ctx` is the type `resolve`'s parameter is annotated with —\n * `BaseContext & { user: User }`, or `{ user: User }` alone — and\n * `BaseContext` when it is not: what the plugin then requires of the app.\n */\nexport interface I18nOptions<\n\tC extends Catalogues,\n\tFallback extends keyof C & string,\n\tCtx extends object = BaseContext,\n> extends Omit<\n\t\tLanguageOptions<keyof C & string, Ctx>,\n\t\t'supported' | 'fallback'\n\t> {\n\t/**\n\t * The catalogues, one per language: their keys are the languages\n\t * supported. Spread `@nxgt/i18n`'s `resources` into them for its shared\n\t * keys — `errors.not-found`, `zod.*`.\n\t */\n\treadonly resources: C;\n\t/** The language spoken when the request names none, and whose keys type `t`. */\n\treadonly fallback: Fallback;\n}\n\n/**\n * The request's language as `@nxgt/i18n` hears it: the first an i18n plugin\n * read, when an app uses several. Opened fresh by each plugin's `around`.\n */\nconst requests = new AsyncLocalStorage<{ language?: string }>();\n\n/** `@nxgt/i18n`'s source: one function, so registering it again keeps one. */\nconst requestLanguage = () => requests.getStore()?.language;\n\n/** What the routes behind the plugin read. */\nexport interface I18nContext<Key extends string> {\n\t/** Translates into the request's language. */\n\treadonly t: Translate<Key>;\n}\n\n/**\n * Translations, as a plugin, on [`@nxgt/i18n`](https://www.npmjs.com/package/@nxgt/i18n):\n * `@alxia/language` reads the request's language among the catalogues', and\n * the routes declared after it read `language` and `t`, bound to it. Keys\n * are typed by the fallback's catalogue; messages are ICU — plurals,\n * selects, numbers — and a missing key answers itself.\n *\n * The plugin's own `t()` translates anywhere a request runs — a service, an\n * error's message — in that request's language, and in the fallback outside.\n *\n * ```ts\n * const i18n = createI18n({ resources: { en, fr }, fallback: 'en' });\n * app.use(i18n).get('/', ({ t, reply }) => reply(200, t('home.title')));\n * ```\n *\n * The plugin registers the request's language as one of `@nxgt/i18n`'s\n * language sources, so its own `getLanguage()` and `translate` — and every\n * nxgt package that translates through them — speak it too.\n */\nexport function createI18n<\n\tconst C extends Catalogues,\n\tconst Fallback extends keyof C & string,\n\tCtx extends object = BaseContext,\n>(options: I18nOptions<C, Fallback, Ctx>) {\n\ttype Language = keyof C & string;\n\ttype Key = KeyOf<C[Fallback]>;\n\tconst { resources, fallback, resolve, ...detect } = options;\n\tconst supported = Object.keys(resources) as Language[];\n\tconst translator = createTranslator<Key>(\n\t\tresources as Record<string, unknown>,\n\t);\n\t/**\n\t * This plugin's language for the request running, once `@alxia/language`\n\t * has read it. A global `around` hook opens it fresh for each request —\n\t * one made from inside another included — so it holds for everything the\n\t * request runs, `onError` hooks too, which run after the route failed.\n\t */\n\tconst current = new AsyncLocalStorage<{ language?: Language }>();\n\tconst spoken = (): Language => current.getStore()?.language ?? fallback;\n\tconst translate =\n\t\t(lang: Language): Translate<Key> =>\n\t\t(key, context) =>\n\t\t\ttranslator(key, context, lang as never);\n\n\t// nxgt's own getLanguage() and translate speak the request's language too.\n\tregisterLanguageSource(requestLanguage);\n\n\t// The plugin requires what `resolve` reads, and `use` checks the app gives\n\t// it; the `language()` inside is then handed `resolve` as reading only\n\t// `BaseContext`, since a context that is a type parameter defers the check.\n\tconst detected = language<Language>({\n\t\t...detect,\n\t\tsupported,\n\t\tfallback,\n\t\t...(resolve === undefined\n\t\t\t? {}\n\t\t\t: { resolve: resolve as (ctx: BaseContext) => string | undefined }),\n\t});\n\tconst plugin = definePlugin<RequiresOf<Ctx, 'resolve'>>()((app) =>\n\t\tapp\n\t\t\t.around((_ctx, next) => current.run({}, () => requests.run({}, next)))\n\t\t\t.use(detected)\n\t\t\t.derive(({ language: lang }): I18nContext<Key> => {\n\t\t\t\tconst own = current.getStore();\n\t\t\t\tif (own !== undefined) own.language = lang;\n\t\t\t\tconst heard = requests.getStore();\n\t\t\t\tif (heard !== undefined) heard.language ??= lang;\n\t\t\t\treturn { t: translate(lang) };\n\t\t\t}),\n\t);\n\n\treturn Object.assign(plugin, {\n\t\t/** Translates into the current request's language, or the fallback outside one. */\n\t\tt: ((key, context) => translate(spoken())(key, context)) as Translate<Key>,\n\t\t/** The current request's language, or the fallback outside one. */\n\t\tlanguage: spoken,\n\t\tsupported,\n\t});\n}\n"
6
6
  ],
7
- "mappings": ";AAAA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAgDA,IAAM,WAAW,IAAI;AAGrB,IAAM,kBAAkB,MAAM,SAAS,SAAS,GAAG;AA2B5C,SAAS,UAIf,CAAC,SAAwC;AAAA,EAGzC,QAAQ,WAAW,UAAU,YAAY,WAAW;AAAA,EACpD,MAAM,YAAY,OAAO,KAAK,SAAS;AAAA,EACvC,MAAM,aAAa,iBAClB,SACD;AAAA,EAOA,MAAM,UAAU,IAAI;AAAA,EACpB,MAAM,SAAS,MAAgB,QAAQ,SAAS,GAAG,YAAY;AAAA,EAC/D,MAAM,YACL,CAAC,SACD,CAAC,KAAK,YACL,WAAW,KAAK,SAAS,IAAa;AAAA,EAGxC,uBAAuB,eAAe;AAAA,EAKtC,MAAM,WAAW,SAAmB;AAAA,OAChC;AAAA,IACH;AAAA,IACA;AAAA,OACI,YAAY,YACb,CAAC,IACD,EAAE,QAA6D;AAAA,EACnE,CAAC;AAAA,EACD,MAAM,SAAS,aAAyC,EAAE,CAAC,QAC1D,IACE,OAAO,CAAC,MAAM,SAAS,QAAQ,IAAI,CAAC,GAAG,MAAM,SAAS,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,EACpE,IAAI,QAAQ,EACZ,OAAO,GAAG,UAAU,WAA6B;AAAA,IACjD,MAAM,MAAM,QAAQ,SAAS;AAAA,IAC7B,IAAI,QAAQ;AAAA,MAAW,IAAI,WAAW;AAAA,IACtC,MAAM,QAAQ,SAAS,SAAS;AAAA,IAChC,IAAI,UAAU;AAAA,MAAW,MAAM,aAAa;AAAA,IAC5C,OAAO,EAAE,GAAG,UAAU,IAAI,EAAE;AAAA,GAC5B,CACH;AAAA,EAEA,OAAO,OAAO,OAAO,QAAQ;AAAA,IAE5B,GAAI,CAAC,KAAK,YAAY,UAAU,OAAO,CAAC,EAAE,KAAK,OAAO;AAAA,IAEtD,UAAU;AAAA,IACV;AAAA,EACD,CAAC;AAAA;",
7
+ "mappings": ";AAAA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AA4EA,IAAM,WAAW,IAAI;AAGrB,IAAM,kBAAkB,MAAM,SAAS,SAAS,GAAG;AA2B5C,SAAS,UAIf,CAAC,SAAwC;AAAA,EAGzC,QAAQ,WAAW,UAAU,YAAY,WAAW;AAAA,EACpD,MAAM,YAAY,OAAO,KAAK,SAAS;AAAA,EACvC,MAAM,aAAa,iBAClB,SACD;AAAA,EAOA,MAAM,UAAU,IAAI;AAAA,EACpB,MAAM,SAAS,MAAgB,QAAQ,SAAS,GAAG,YAAY;AAAA,EAC/D,MAAM,YACL,CAAC,SACD,CAAC,KAAK,YACL,WAAW,KAAK,SAAS,IAAa;AAAA,EAGxC,uBAAuB,eAAe;AAAA,EAKtC,MAAM,WAAW,SAAmB;AAAA,OAChC;AAAA,IACH;AAAA,IACA;AAAA,OACI,YAAY,YACb,CAAC,IACD,EAAE,QAA6D;AAAA,EACnE,CAAC;AAAA,EACD,MAAM,SAAS,aAAyC,EAAE,CAAC,QAC1D,IACE,OAAO,CAAC,MAAM,SAAS,QAAQ,IAAI,CAAC,GAAG,MAAM,SAAS,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,EACpE,IAAI,QAAQ,EACZ,OAAO,GAAG,UAAU,WAA6B;AAAA,IACjD,MAAM,MAAM,QAAQ,SAAS;AAAA,IAC7B,IAAI,QAAQ;AAAA,MAAW,IAAI,WAAW;AAAA,IACtC,MAAM,QAAQ,SAAS,SAAS;AAAA,IAChC,IAAI,UAAU;AAAA,MAAW,MAAM,aAAa;AAAA,IAC5C,OAAO,EAAE,GAAG,UAAU,IAAI,EAAE;AAAA,GAC5B,CACH;AAAA,EAEA,OAAO,OAAO,OAAO,QAAQ;AAAA,IAE5B,GAAI,CAAC,KAAK,YAAY,UAAU,OAAO,CAAC,EAAE,KAAK,OAAO;AAAA,IAEtD,UAAU;AAAA,IACV;AAAA,EACD,CAAC;AAAA;",
8
8
  "debugId": "700D328B5FADEAA064756E2164756E21",
9
9
  "names": []
10
10
  }
package/docs/README.md CHANGED
@@ -9,5 +9,5 @@ response shows a key or the wrong language.
9
9
  | Page | Read it when |
10
10
  | --- | --- |
11
11
  | [Guide](guide.md) | writing the catalogues, choosing where the language is read from, reading a user's saved language from an earlier plugin, typing a function that translates, translating in a service or an error handler, caching the responses, or testing them |
12
- | [Troubleshooting](troubleshooting.md) | `createI18n()` threw at start-up, `tsc` refused a key, an option or `use(i18n)`, the log shows an ICU error, or a response shows a key or the wrong language |
12
+ | [Troubleshooting](troubleshooting.md) | `createI18n()` threw at start-up, `tsc` refused a key, an option or `use(i18n)`, a function generic over its catalogues hit TS2589, the log shows an ICU error, or a response shows a key or the wrong language |
13
13
  | [Roadmap](roadmap.md) | wondering what is coming, and what is not planned |
package/docs/guide.md CHANGED
@@ -50,7 +50,7 @@ interface I18nOptions<C extends Catalogues, Fallback extends keyof C & string, C
50
50
  }
51
51
 
52
52
  type Catalogues = Readonly<Record<string, Readonly<Record<string, unknown>>>>;
53
- type KeyOf<Catalogue> = Path<Catalogue> & string; // every key, dotted: 'home.title'
53
+ type KeyOf<Catalogue> = …; // every key, dotted: 'home.title', nine levels deep
54
54
  type Translate<Key extends string> = (key: Key, context?: TranslationContext) => string;
55
55
  interface I18nContext<Key extends string> {
56
56
  readonly t: Translate<Key>;
@@ -364,6 +364,24 @@ type Key = KeyOf<typeof en>; // 'cart.items'
364
364
  export const describeCart = (t: Translate<Key>, count: number) => t('cart.items', { count });
365
365
  ```
366
366
 
367
+ `KeyOf` gives the keys of `@nxgt/i18n`'s `Path`, nine levels deep; a section
368
+ nested deeper gives `section.${string}`, any key under it. The bound is what
369
+ lets a function generic over its catalogues hand them to `createI18n`:
370
+
371
+ ```ts
372
+ import { alxia } from '@alxia/core';
373
+ import { type Catalogues, createI18n } from '@alxia/i18n';
374
+
375
+ export function translatedWith<const C extends Catalogues, const Fallback extends keyof C & string>(
376
+ resources: C,
377
+ fallback: Fallback,
378
+ ) {
379
+ return alxia()
380
+ .use(createI18n({ resources, fallback }))
381
+ .get('/', ({ language, reply }) => reply(200, language));
382
+ }
383
+ ```
384
+
367
385
  ## Outside a route
368
386
 
369
387
  `i18n.t()` translates in the language of the request it runs in, and
@@ -79,6 +79,31 @@ compile-time form of [the start-up error](#typeerror-language-the-fallback-de-is
79
79
  createI18n({ resources: { en, fr }, fallback: 'en' });
80
80
  ```
81
81
 
82
+ ### `Type instantiation is excessively deep and possibly infinite`
83
+
84
+ ```text
85
+ error TS2589: Type instantiation is excessively deep and possibly infinite.
86
+ ```
87
+
88
+ **When:** a function generic over its catalogues hands them to
89
+ `createI18n`, then uses the plugin, with `@alxia/i18n` 0.1.1 or earlier:
90
+
91
+ ```ts
92
+ function translatedWith<const C extends Catalogues, const Fallback extends keyof C & string>(
93
+ resources: C,
94
+ fallback: Fallback,
95
+ ) {
96
+ return alxia().use(createI18n({ resources, fallback }));
97
+ }
98
+ ```
99
+
100
+ **Why:** `KeyOf` was `@nxgt/i18n`'s `Path`, which recurses without a bound.
101
+ With `C` a type parameter, the keys stay deferred, and some checks then
102
+ unfold them forever. A tsconfig whose `types` leaves out `bun` is one
103
+ setting that triggers them.
104
+
105
+ **Fix:** upgrade `@alxia/i18n`: `KeyOf` now reads nine levels deep at most.
106
+
82
107
  ### `Argument of type '"cart.itmes"' is not assignable to parameter of type '"cart.items"'`
83
108
 
84
109
  ```text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alxia/i18n",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Translations for alxia on @nxgt/i18n: t() bound to the request's language, keys typed by your catalogue, ICU messages",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,13 +41,13 @@
41
41
  ]
42
42
  },
43
43
  "devDependencies": {
44
- "@alxia/core": "^0.2.0",
44
+ "@alxia/core": "^0.2.2",
45
45
  "@alxia/language": "^0.1.1",
46
46
  "@nxgt/i18n": "^2.0.0",
47
47
  "@types/bun": "^1.4.2"
48
48
  },
49
49
  "peerDependencies": {
50
- "@alxia/core": "^0.2.0",
50
+ "@alxia/core": "^0.2.2",
51
51
  "@alxia/language": "^0.1.1",
52
52
  "@nxgt/i18n": "^2.0.0",
53
53
  "typescript": "^6.0.3 || ^7.0.0"