@alxia/i18n 0.1.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/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/i18n.d.ts +56 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +10 -0
- package/docs/README.md +13 -0
- package/docs/guide.md +592 -0
- package/docs/roadmap.md +50 -0
- package/docs/troubleshooting.md +472 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Steve Tsala
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @alxia/i18n
|
|
2
|
+
|
|
3
|
+
Translations for [alxia](https://www.npmjs.com/package/@alxia/core), on
|
|
4
|
+
[`@nxgt/i18n`](https://www.npmjs.com/package/@nxgt/i18n): `t()` bound to the
|
|
5
|
+
request's language, keys typed by your catalogue, ICU messages — plurals,
|
|
6
|
+
selects, numbers.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
bun add @alxia/i18n @alxia/language @nxgt/i18n@^2 @alxia/core
|
|
10
|
+
bun add -d typescript
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { alxia } from '@alxia/core';
|
|
17
|
+
import { createI18n } from '@alxia/i18n';
|
|
18
|
+
import { resources as shared } from '@nxgt/i18n';
|
|
19
|
+
|
|
20
|
+
const en = { ...shared.en, cart: { items: '{count, plural, =0 {No items} one {One item} other {# items}}' } };
|
|
21
|
+
const fr = { ...shared.fr, cart: { items: '{count, plural, =0 {Aucun article} one {Un article} other {# articles}}' } };
|
|
22
|
+
|
|
23
|
+
export const i18n = createI18n({ resources: { en, fr }, fallback: 'en' });
|
|
24
|
+
|
|
25
|
+
const app = alxia()
|
|
26
|
+
.use(i18n)
|
|
27
|
+
.get('/cart', ({ t, reply }) => reply(200, t('cart.items', { count: 3 }))); // '3 articles' in French
|
|
28
|
+
|
|
29
|
+
app.listen(3000);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **The language** is `@alxia/language`'s, among the catalogues' languages:
|
|
33
|
+
the query, a cookie, `Accept-Language`, then `fallback`. Its options pass
|
|
34
|
+
through: `createI18n({ resources, fallback, order: ['path', 'header'] })`.
|
|
35
|
+
- **Keys** are typed by the fallback's catalogue: `t('cart.itmes')` is a
|
|
36
|
+
compile error. A key a language lacks answers itself.
|
|
37
|
+
- **`@nxgt/i18n`'s shared keys** — `errors.not-found`, `zod.*` — are yours by
|
|
38
|
+
spreading its `resources` into your catalogues.
|
|
39
|
+
|
|
40
|
+
## Reading the app's context
|
|
41
|
+
|
|
42
|
+
Annotate `resolve`'s parameter to speak the language a signed-in user saved.
|
|
43
|
+
The plugin then requires what it reads: an app that does not give `user`
|
|
44
|
+
before it cannot use it.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { alxia, type BaseContext } from '@alxia/core';
|
|
48
|
+
import { createI18n } from '@alxia/i18n';
|
|
49
|
+
|
|
50
|
+
const en = { home: { title: 'Welcome' } };
|
|
51
|
+
const fr = { home: { title: 'Bienvenue' } };
|
|
52
|
+
|
|
53
|
+
const auth = alxia().derive(({ request }) => ({
|
|
54
|
+
user: request.headers.has('authorization') ? { language: 'fr' } : null, // your sign-in
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const byUser = createI18n({
|
|
58
|
+
resources: { en, fr },
|
|
59
|
+
fallback: 'en',
|
|
60
|
+
order: ['query'],
|
|
61
|
+
resolve: ({ user }: BaseContext & { user: { language: string } | null }) => user?.language,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
alxia().use(auth).use(byUser); // auth derives user
|
|
65
|
+
alxia().use(byUser); // a compile error: this app gives no `user`
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Unannotated, `resolve` reads the request alone and the plugin requires
|
|
69
|
+
nothing; annotated `any`, the plugin is refused on every app.
|
|
70
|
+
|
|
71
|
+
## Anywhere
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// a service, or an error's message: no language passed
|
|
75
|
+
export const describeCart = (count: number) => i18n.t('cart.items', { count });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`i18n.t()` translates in the language of the request it runs in — through
|
|
79
|
+
every `await`, an `onError` hook included — and in the fallback outside
|
|
80
|
+
one, or before the language is read: in an `onRequest` hook, or a route
|
|
81
|
+
declared before the plugin
|
|
82
|
+
([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/i18n/docs/troubleshooting.md#i18nt-answers-in-the-fallback-before-the-language-is-read)).
|
|
83
|
+
`i18n.language()` says which.
|
|
84
|
+
|
|
85
|
+
### `@nxgt/i18n`'s own `translate`
|
|
86
|
+
|
|
87
|
+
`createI18n()` registers the request's language as one of `@nxgt/i18n`'s
|
|
88
|
+
language sources: its `getLanguage()` and `translate` — and every nxgt
|
|
89
|
+
package that translates through them, an error's message — speak the alxia
|
|
90
|
+
request's language too.
|
|
91
|
+
|
|
92
|
+
## Cached responses
|
|
93
|
+
|
|
94
|
+
A response in the request's language varies by what decided it: give
|
|
95
|
+
`@alxia/cache` the same headers, `vary: ['accept-language', 'cookie']`.
|
|
96
|
+
|
|
97
|
+
## API
|
|
98
|
+
|
|
99
|
+
| export | |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `createI18n({ resources, fallback, …languageOptions })` | the plugin — routes after it read `t` and `language` — with `t()`, `language()` and `supported` |
|
|
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 |
|
|
104
|
+
|
|
105
|
+
## Documentation
|
|
106
|
+
|
|
107
|
+
- [Guide](https://github.com/softistx/alxia/tree/develop/packages/i18n/docs): the catalogues and every option, reading what an earlier plugin added, what the routes read, ICU messages and typed keys, `t()` outside a route, and `@nxgt/i18n`'s own `translate`.
|
|
108
|
+
- [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/i18n/docs/troubleshooting.md): an error, a key shown instead of a message, or a response in the wrong language, and what to do about it.
|
|
109
|
+
- [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/i18n/docs/roadmap.md): what is coming, and what is not planned.
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type BaseContext, type RequiresOf } from '@alxia/core';
|
|
2
|
+
import { type LanguageOptions } from '@alxia/language';
|
|
3
|
+
import { type Path, type TranslationContext } from '@nxgt/i18n';
|
|
4
|
+
/** Catalogues by language: `{ en: { greeting: 'Hello {name}' }, fr: … }`. */
|
|
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;
|
|
8
|
+
/** A translation of a key, in a language, formatted with ICU's `context`. */
|
|
9
|
+
export type Translate<Key extends string> = (key: Key, context?: TranslationContext) => string;
|
|
10
|
+
/**
|
|
11
|
+
* `Ctx` is the type `resolve`'s parameter is annotated with —
|
|
12
|
+
* `BaseContext & { user: User }`, or `{ user: User }` alone — and
|
|
13
|
+
* `BaseContext` when it is not: what the plugin then requires of the app.
|
|
14
|
+
*/
|
|
15
|
+
export interface I18nOptions<C extends Catalogues, Fallback extends keyof C & string, Ctx extends object = BaseContext> extends Omit<LanguageOptions<keyof C & string, Ctx>, 'supported' | 'fallback'> {
|
|
16
|
+
/**
|
|
17
|
+
* The catalogues, one per language: their keys are the languages
|
|
18
|
+
* supported. Spread `@nxgt/i18n`'s `resources` into them for its shared
|
|
19
|
+
* keys — `errors.not-found`, `zod.*`.
|
|
20
|
+
*/
|
|
21
|
+
readonly resources: C;
|
|
22
|
+
/** The language spoken when the request names none, and whose keys type `t`. */
|
|
23
|
+
readonly fallback: Fallback;
|
|
24
|
+
}
|
|
25
|
+
/** What the routes behind the plugin read. */
|
|
26
|
+
export interface I18nContext<Key extends string> {
|
|
27
|
+
/** Translates into the request's language. */
|
|
28
|
+
readonly t: Translate<Key>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Translations, as a plugin, on [`@nxgt/i18n`](https://www.npmjs.com/package/@nxgt/i18n):
|
|
32
|
+
* `@alxia/language` reads the request's language among the catalogues', and
|
|
33
|
+
* the routes declared after it read `language` and `t`, bound to it. Keys
|
|
34
|
+
* are typed by the fallback's catalogue; messages are ICU — plurals,
|
|
35
|
+
* selects, numbers — and a missing key answers itself.
|
|
36
|
+
*
|
|
37
|
+
* The plugin's own `t()` translates anywhere a request runs — a service, an
|
|
38
|
+
* error's message — in that request's language, and in the fallback outside.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* const i18n = createI18n({ resources: { en, fr }, fallback: 'en' });
|
|
42
|
+
* app.use(i18n).get('/', ({ t, reply }) => reply(200, t('home.title')));
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* The plugin registers the request's language as one of `@nxgt/i18n`'s
|
|
46
|
+
* language sources, so its own `getLanguage()` and `translate` — and every
|
|
47
|
+
* nxgt package that translates through them — speak it too.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createI18n<const C extends Catalogues, const Fallback extends keyof C & string, Ctx extends object = BaseContext>(options: I18nOptions<C, Fallback, Ctx>): import("@alxia/core").Alxia<RequiresOf<Ctx, "resolve"> & import("@alxia/core").Empty & import("@alxia/language").LanguageContext<keyof C & string> & I18nContext<KeyOf<C[Fallback]>>, import("@alxia/core").Empty & {}, "", never> & import("@alxia/core").Requiring<RequiresOf<Ctx, "resolve">> & {
|
|
50
|
+
/** Translates into the current request's language, or the fallback outside one. */
|
|
51
|
+
t: Translate<KeyOf<C[Fallback]>>;
|
|
52
|
+
/** The current request's language, or the fallback outside one. */
|
|
53
|
+
language: () => keyof C & string;
|
|
54
|
+
supported: (keyof C & string)[];
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=i18n.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,UAAU,EACf,UAAU,EACV,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,KAAK,EACV,KAAK,SAAS,GACd,MAAM,QAAQ,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/i18n.ts
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { definePlugin } from "@alxia/core";
|
|
4
|
+
import { language } from "@alxia/language";
|
|
5
|
+
import {
|
|
6
|
+
createTranslator,
|
|
7
|
+
registerLanguageSource
|
|
8
|
+
} from "@nxgt/i18n";
|
|
9
|
+
var requests = new AsyncLocalStorage;
|
|
10
|
+
var requestLanguage = () => requests.getStore()?.language;
|
|
11
|
+
function createI18n(options) {
|
|
12
|
+
const { resources, fallback, resolve, ...detect } = options;
|
|
13
|
+
const supported = Object.keys(resources);
|
|
14
|
+
const translator = createTranslator(resources);
|
|
15
|
+
const current = new AsyncLocalStorage;
|
|
16
|
+
const spoken = () => current.getStore()?.language ?? fallback;
|
|
17
|
+
const translate = (lang) => (key, context) => translator(key, context, lang);
|
|
18
|
+
registerLanguageSource(requestLanguage);
|
|
19
|
+
const detected = language({
|
|
20
|
+
...detect,
|
|
21
|
+
supported,
|
|
22
|
+
fallback,
|
|
23
|
+
...resolve === undefined ? {} : { resolve }
|
|
24
|
+
});
|
|
25
|
+
const plugin = definePlugin()((app) => app.around((_ctx, next) => current.run({}, () => requests.run({}, next))).use(detected).derive(({ language: lang }) => {
|
|
26
|
+
const own = current.getStore();
|
|
27
|
+
if (own !== undefined)
|
|
28
|
+
own.language = lang;
|
|
29
|
+
const heard = requests.getStore();
|
|
30
|
+
if (heard !== undefined)
|
|
31
|
+
heard.language ??= lang;
|
|
32
|
+
return { t: translate(lang) };
|
|
33
|
+
}));
|
|
34
|
+
return Object.assign(plugin, {
|
|
35
|
+
t: (key, context) => translate(spoken())(key, context),
|
|
36
|
+
language: spoken,
|
|
37
|
+
supported
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
createI18n
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
//# debugId=700D328B5FADEAA064756E2164756E21
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/i18n.ts"],
|
|
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"
|
|
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;",
|
|
8
|
+
"debugId": "700D328B5FADEAA064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @alxia/i18n documentation
|
|
2
|
+
|
|
3
|
+
The [package README](../README.md) is the short version. This folder is
|
|
4
|
+
the long one: the catalogues and every option, what the routes behind the
|
|
5
|
+
plugin read, how messages are formatted and keys typed, where the
|
|
6
|
+
request's language reaches and where it does not, and what to do when a
|
|
7
|
+
response shows a key or the wrong language.
|
|
8
|
+
|
|
9
|
+
| Page | Read it when |
|
|
10
|
+
| --- | --- |
|
|
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 |
|
|
13
|
+
| [Roadmap](roadmap.md) | wondering what is coming, and what is not planned |
|