@alxia/i18n 0.2.0 → 0.3.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/dist/i18n.d.ts +3 -3
- package/dist/i18n.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +3 -3
- package/docs/guide.md +6 -9
- package/docs/roadmap.md +1 -1
- package/docs/troubleshooting.md +20 -57
- package/package.json +5 -5
package/dist/i18n.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BaseContext, type Empty, type Middleware, type
|
|
1
|
+
import { type BaseContext, type Empty, type Middleware, type Next, type RequiresOf } from '@alxia/core';
|
|
2
2
|
import { type LanguageContext, type LanguageOptions } from '@alxia/language';
|
|
3
3
|
import { type TranslationContext } from '@nxgt/i18n';
|
|
4
4
|
/** Catalogues by language: `{ en: { greeting: 'Hello {name}' }, fr: … }`. */
|
|
@@ -50,7 +50,7 @@ export interface I18nContext<Key extends string> {
|
|
|
50
50
|
* what `resolve` reads — with a `t` and a `language` of its own for the
|
|
51
51
|
* request running.
|
|
52
52
|
*/
|
|
53
|
-
export type I18nMiddleware<Language extends string, Key extends string, Requires extends object = Empty> = Middleware<Requires, Promise<Next<LanguageContext<Language> & I18nContext<Key>>>> &
|
|
53
|
+
export type I18nMiddleware<Language extends string, Key extends string, Requires extends object = Empty> = Middleware<Requires, Promise<Next<LanguageContext<Language> & I18nContext<Key>>>> & {
|
|
54
54
|
/** Translates into the current request's language, or the fallback outside one. */
|
|
55
55
|
t: Translate<Key>;
|
|
56
56
|
/** The current request's language, or the fallback outside one. */
|
|
@@ -65,7 +65,7 @@ export type I18nMiddleware<Language extends string, Key extends string, Requires
|
|
|
65
65
|
* selects, numbers — and a missing key answers itself.
|
|
66
66
|
*
|
|
67
67
|
* Its own `t()` translates anywhere a request runs after it — a service,
|
|
68
|
-
* the answer to an error,
|
|
68
|
+
* the answer to an error, a try/catch middleware's included — in that
|
|
69
69
|
* request's language, and in the fallback outside.
|
|
70
70
|
*
|
|
71
71
|
* ```ts
|
package/dist/i18n.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,WAAW,EAEhB,KAAK,KAAK,EACV,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
import {
|
|
4
4
|
defineMiddleware,
|
|
5
|
+
markFactory,
|
|
5
6
|
settle
|
|
6
7
|
} from "@alxia/core";
|
|
7
8
|
import {
|
|
@@ -30,7 +31,7 @@ function createI18n(options) {
|
|
|
30
31
|
fallback,
|
|
31
32
|
...resolve === undefined ? {} : { resolve }
|
|
32
33
|
});
|
|
33
|
-
const middleware = defineMiddleware()((ctx, next)
|
|
34
|
+
const middleware = defineMiddleware()(function i18n(ctx, next) {
|
|
34
35
|
const own = {};
|
|
35
36
|
const found = (heard) => {
|
|
36
37
|
own.language = heard.language;
|
|
@@ -50,9 +51,10 @@ function createI18n(options) {
|
|
|
50
51
|
supported
|
|
51
52
|
});
|
|
52
53
|
}
|
|
54
|
+
markFactory(createI18n);
|
|
53
55
|
export {
|
|
54
56
|
createI18n
|
|
55
57
|
};
|
|
56
58
|
|
|
57
|
-
//# debugId=
|
|
59
|
+
//# debugId=3A3F680B7493293D64756E2164756E21
|
|
58
60
|
//# sourceMappingURL=index.js.map
|
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 {\n\ttype BaseContext,\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\
|
|
5
|
+
"import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n\ttype BaseContext,\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\tmarkFactory,\n\ttype Next,\n\ttype NextFunction,\n\ttype RequiresOf,\n\tsettle,\n} from '@alxia/core';\nimport {\n\ttype LanguageContext,\n\ttype LanguageOptions,\n\tlanguage,\n} 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\n * middleware read, when an app uses several. Opened for each request —\n * one made from inside another included — by the first that runs on it.\n */\nconst requests = new AsyncLocalStorage<{ url: URL; language?: string }>();\n\n/** Runs `work` with the request `url`'s store open: the one in force, or a fresh one. */\nfunction hearing<T>(url: URL, work: () => T): T {\n\treturn requests.getStore()?.url === url\n\t\t? work()\n\t\t: requests.run({ url }, work);\n}\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 * What `createI18n()` makes: a middleware that gives `language`, one of\n * `Language`, and `t`, typed by `Key`, and requires `Requires` of the app —\n * what `resolve` reads — with a `t` and a `language` of its own for the\n * request running.\n */\nexport type I18nMiddleware<\n\tLanguage extends string,\n\tKey extends string,\n\tRequires extends object = Empty,\n> = Middleware<\n\tRequires,\n\tPromise<Next<LanguageContext<Language> & I18nContext<Key>>>\n> & {\n\t/** Translates into the current request's language, or the fallback outside one. */\n\tt: Translate<Key>;\n\t/** The current request's language, or the fallback outside one. */\n\tlanguage: () => Language;\n\tsupported: Language[];\n};\n\n/**\n * Translations, as a middleware, 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 * Its own `t()` translates anywhere a request runs after it — a service,\n * the answer to an error, a try/catch middleware's included — in that\n * 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 * It registers the request's language as one of `@nxgt/i18n`'s language\n * sources, so its own `getLanguage()` and `translate` — and every nxgt\n * 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>(\n\toptions: I18nOptions<C, Fallback, Ctx>,\n): I18nMiddleware<\n\tkeyof C & string,\n\tKeyOf<C[Fallback]>,\n\tRequiresOf<Ctx, 'resolve'>\n> {\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 middleware's language for the request running, once\n\t * `@alxia/language` has read it. Opened fresh for each request — one\n\t * made from inside another included — around everything after it, the\n\t * answer to an error included: `settle` answers it inside.\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 `app.plugin` 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\t// What the routes after it read, once `@alxia/language` has read it.\n\ttype Added = LanguageContext<Language> & I18nContext<Key>;\n\tconst middleware = defineMiddleware<RequiresOf<Ctx, 'resolve'>>()(\n\t\tfunction i18n(ctx, next): Promise<Next<Added>> {\n\t\t\tconst own: { language?: Language } = {};\n\t\t\tconst found = (heard: LanguageContext<Language>) => {\n\t\t\t\town.language = heard.language;\n\t\t\t\tconst store = requests.getStore();\n\t\t\t\tif (store !== undefined) store.language ??= heard.language;\n\t\t\t\treturn settle(ctx, next({ ...heard, t: translate(heard.language) }));\n\t\t\t};\n\t\t\t// `language()`'s middleware, run inline: its `next` is this one's.\n\t\t\tconst detecting = Object.assign(found, {\n\t\t\t\tbehind: next.behind,\n\t\t\t}) as unknown as NextFunction;\n\t\t\treturn current.run(own, () =>\n\t\t\t\thearing(ctx.url, () => detected(ctx as never, detecting)),\n\t\t\t) as Promise<Next<Added>>;\n\t\t},\n\t);\n\n\treturn Object.assign(middleware, {\n\t\tt: ((key, context) => translate(spoken())(key, context)) as Translate<Key>,\n\t\tlanguage: spoken,\n\t\tsupported,\n\t});\n}\n\nmarkFactory(createI18n);\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";AAAA;AACA;AAAA;AAAA;AAAA;AAWA;AAAA;AAAA;AAKA;AAAA;AAAA;AAAA;AA6EA,IAAM,WAAW,IAAI;AAGrB,SAAS,OAAU,CAAC,KAAU,MAAkB;AAAA,EAC/C,OAAO,SAAS,SAAS,GAAG,QAAQ,MACjC,KAAK,IACL,SAAS,IAAI,EAAE,IAAI,GAAG,IAAI;AAAA;AAI9B,IAAM,kBAAkB,MAAM,SAAS,SAAS,GAAG;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";AAAA;AACA;AAAA;AAAA;AAAA;AAAA;AAWA;AAAA;AAAA;AAKA;AAAA;AAAA;AAAA;AA6EA,IAAM,WAAW,IAAI;AAGrB,SAAS,OAAU,CAAC,KAAU,MAAkB;AAAA,EAC/C,OAAO,SAAS,SAAS,GAAG,QAAQ,MACjC,KAAK,IACL,SAAS,IAAI,EAAE,IAAI,GAAG,IAAI;AAAA;AAI9B,IAAM,kBAAkB,MAAM,SAAS,SAAS,GAAG;AAiD5C,SAAS,UAIf,CACA,SAKC;AAAA,EAGD,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,EAGD,MAAM,aAAa,iBAA6C,EAC/D,SAAS,IAAI,CAAC,KAAK,MAA4B;AAAA,IAC9C,MAAM,MAA+B,CAAC;AAAA,IACtC,MAAM,QAAQ,CAAC,UAAqC;AAAA,MACnD,IAAI,WAAW,MAAM;AAAA,MACrB,MAAM,QAAQ,SAAS,SAAS;AAAA,MAChC,IAAI,UAAU;AAAA,QAAW,MAAM,aAAa,MAAM;AAAA,MAClD,OAAO,OAAO,KAAK,KAAK,KAAK,OAAO,GAAG,UAAU,MAAM,QAAQ,EAAE,CAAC,CAAC;AAAA;AAAA,IAGpE,MAAM,YAAY,OAAO,OAAO,OAAO;AAAA,MACtC,QAAQ,KAAK;AAAA,IACd,CAAC;AAAA,IACD,OAAO,QAAQ,IAAI,KAAK,MACvB,QAAQ,IAAI,KAAK,MAAM,SAAS,KAAc,SAAS,CAAC,CACzD;AAAA,GAEF;AAAA,EAEA,OAAO,OAAO,OAAO,YAAY;AAAA,IAChC,GAAI,CAAC,KAAK,YAAY,UAAU,OAAO,CAAC,EAAE,KAAK,OAAO;AAAA,IACtD,UAAU;AAAA,IACV;AAAA,EACD,CAAC;AAAA;AAGF,YAAY,UAAU;",
|
|
8
|
+
"debugId": "3A3F680B7493293D64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/docs/guide.md
CHANGED
|
@@ -36,12 +36,11 @@ function createI18n<
|
|
|
36
36
|
Ctx extends object = BaseContext,
|
|
37
37
|
>(
|
|
38
38
|
options: I18nOptions<C, Fallback, Ctx>,
|
|
39
|
-
): Middleware<RequiresOf<Ctx, 'resolve'>, Promise<Next<LanguageContext<keyof C & string> & I18nContext<KeyOf<C[Fallback]>>>>> &
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
};
|
|
39
|
+
): Middleware<RequiresOf<Ctx, 'resolve'>, Promise<Next<LanguageContext<keyof C & string> & I18nContext<KeyOf<C[Fallback]>>>>> & {
|
|
40
|
+
t: Translate<KeyOf<C[Fallback]>>;
|
|
41
|
+
language: () => keyof C & string;
|
|
42
|
+
supported: (keyof C & string)[];
|
|
43
|
+
};
|
|
45
44
|
|
|
46
45
|
interface I18nOptions<C extends Catalogues, Fallback extends keyof C & string, Ctx extends object = BaseContext>
|
|
47
46
|
extends Omit<LanguageOptions<keyof C & string, Ctx>, 'supported' | 'fallback'> {
|
|
@@ -430,10 +429,8 @@ Before it, `i18n.t()` and `i18n.language()` answer in the fallback:
|
|
|
430
429
|
| a middleware, `derive` or route declared after `.use(i18n)`, before and after its `next()`, and what they call | the request's language |
|
|
431
430
|
| a middleware after it that catches an error | the request's language |
|
|
432
431
|
| a middleware after it, on a `404` or a `405` no route matched | the request's language |
|
|
433
|
-
| the deprecated `onError` hook, for what the chain after `use(i18n)` threw | the request's language |
|
|
434
432
|
| a middleware declared before `.use(i18n)`, in and out | the fallback: the language is not read yet |
|
|
435
433
|
| a route declared before `.use(i18n)` | the fallback: the middleware does not run for it |
|
|
436
|
-
| the deprecated `onRequest` and `onResponse` hooks, which run outside the chain | the fallback |
|
|
437
434
|
| code outside any request: start-up, a timer, a queue consumer | the fallback: pass the language, see below |
|
|
438
435
|
|
|
439
436
|
An error-handling middleware translates an error's message with `i18n.t()`.
|
|
@@ -585,7 +582,7 @@ import ownFr from './locales/fr.json';
|
|
|
585
582
|
export const i18n = createI18n({
|
|
586
583
|
resources: { en: { ...shared.en, ...ownEn }, fr: { ...shared.fr, ...ownFr } },
|
|
587
584
|
fallback: 'en',
|
|
588
|
-
persist: { secure:
|
|
585
|
+
persist: { secure: Bun.env.NODE_ENV !== 'development' }, // read at runtime: bun build inlines process.env
|
|
589
586
|
});
|
|
590
587
|
```
|
|
591
588
|
|
package/docs/roadmap.md
CHANGED
|
@@ -7,7 +7,7 @@ number on it. Every release, with each change it made, is in
|
|
|
7
7
|
|
|
8
8
|
## Now
|
|
9
9
|
|
|
10
|
-
- **A middleware, not a plugin (0.4).** `app.use(createI18n({ ... }))` runs on every request, a 404 included; its `t()` works in every middleware after it and in the answer to an error. `app.plugin(i18n)
|
|
10
|
+
- **A middleware, not a plugin (0.4).** `app.use(createI18n({ ... }))` runs on every request, a 404 included; its `t()` works in every middleware after it and in the answer to an error. `app.plugin(i18n)`, the deprecated plugin form, was removed with alxia 0.5: give it to `use`.
|
|
11
11
|
|
|
12
12
|
## Next
|
|
13
13
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -13,9 +13,8 @@ nothing — what the response does that you did not expect.
|
|
|
13
13
|
- [`Type '"de"' is not assignable to type '"en" | "fr"'`](#type-de-is-not-assignable-to-type-en--fr)
|
|
14
14
|
- [`Argument of type '"cart.itmes"' is not assignable to parameter of type '"cart.items"'`](#argument-of-type-cartitmes-is-not-assignable-to-parameter-of-type-cartitems)
|
|
15
15
|
- [`Property 't' does not exist on type 'Context<Empty, "/", Empty>'`](#property-t-does-not-exist-on-type-contextempty--empty)
|
|
16
|
-
- [`Type '
|
|
16
|
+
- [`Type 'I18nMiddleware<string, string, Empty>' is not assignable to type '"this looks like a factory given uncalled: call it, as use(cors()) and not use(cors)"'`](#type-i18nmiddlewarestring-string-empty-is-not-assignable-to-type-this-looks-like-a-factory-given-uncalled-call-it-as-usecors-and-not-usecors)
|
|
17
17
|
- [`Object literal may only specify known properties, and 'supported' does not exist in type 'I18nOptions<…>'`](#object-literal-may-only-specify-known-properties-and-supported-does-not-exist-in-type-i18noptions)
|
|
18
|
-
- [`Cannot invoke an object which is possibly 'undefined'`](#cannot-invoke-an-object-which-is-possibly-undefined)
|
|
19
18
|
- [`t()` accepts any key, typos included](#t-accepts-any-key-typos-included)
|
|
20
19
|
- [`Property 'user' does not exist on type 'BaseContext'`](#property-user-does-not-exist-on-type-basecontext)
|
|
21
20
|
- [`Property 'user' is missing in type 'BaseContext & Empty' but required in type '{ user: User | null; }'`](#property-user-is-missing-in-type-basecontext--empty-but-required-in-type--user-user--null-)
|
|
@@ -147,23 +146,25 @@ const app = alxia()
|
|
|
147
146
|
.get('/', ({ t, reply }) => reply(200, t('home.title')));
|
|
148
147
|
```
|
|
149
148
|
|
|
150
|
-
### `Type '
|
|
149
|
+
### `Type 'I18nMiddleware<string, string, Empty>' is not assignable to type '"this looks like a factory given uncalled: call it, as use(cors()) and not use(cors)"'`
|
|
151
150
|
|
|
152
151
|
```text
|
|
153
|
-
error
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
…
|
|
157
|
-
Types of parameters 'options' and 'ctx' are incompatible.
|
|
158
|
-
Type 'BaseContext & Empty' is missing the following properties from type 'I18nOptions<Readonly<Record<string, Readonly<Record<string, unknown>>>>, string, BaseContext>': resources, fallback
|
|
152
|
+
error TS2345: Argument of type '<const C extends Catalogues, const Fallback extends keyof C & string, Ctx extends object = BaseContext>(options: I18nOptions<C, Fallback, Ctx>) => I18nMiddleware<…>' is not assignable to parameter of type '…'.
|
|
153
|
+
…
|
|
154
|
+
Type 'I18nMiddleware<string, string, Empty>' is not assignable to type '"this looks like a factory given uncalled: call it, as use(cors()) and not use(cors)"'.
|
|
159
155
|
```
|
|
160
156
|
|
|
161
|
-
|
|
157
|
+
**When:** `app.use(createI18n)`, the factory given uncalled. The message names
|
|
158
|
+
`cors` as its example, whichever factory it is. It also throws where it is
|
|
159
|
+
declared, since alxia 0.5, rather than answering each request with a 500:
|
|
162
160
|
|
|
163
|
-
|
|
161
|
+
```text
|
|
162
|
+
TypeError: use(): argument 1 looks like a factory (createI18n): call it, use(createI18n())
|
|
163
|
+
```
|
|
164
164
|
|
|
165
|
-
**Why:** `createI18n` makes the middleware
|
|
166
|
-
middleware
|
|
165
|
+
**Why:** `createI18n` makes the middleware; it is not the middleware. A
|
|
166
|
+
function that returns a function is no middleware, and `createI18n` is marked
|
|
167
|
+
as a factory, so `use`, a route and `plugin` refuse it.
|
|
167
168
|
|
|
168
169
|
**Fix:** call it once, in a module of its own, and use what it returns:
|
|
169
170
|
|
|
@@ -192,42 +193,6 @@ ignored.
|
|
|
192
193
|
createI18n({ resources: { en }, fallback: 'en' });
|
|
193
194
|
```
|
|
194
195
|
|
|
195
|
-
### `Cannot invoke an object which is possibly 'undefined'`
|
|
196
|
-
|
|
197
|
-
```text
|
|
198
|
-
error TS2722: Cannot invoke an object which is possibly 'undefined'.
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
**When:** a deprecated `onError` hook calls `t('…')` from its context. (A
|
|
202
|
-
middleware declared after `.use(i18n)` has no such doubt: `t` is typed there.)
|
|
203
|
-
|
|
204
|
-
**Why:** `onError` also handles what was thrown before the middleware ran —
|
|
205
|
-
by a middleware declared before it — so what it adds is optional there.
|
|
206
|
-
|
|
207
|
-
**Fix:** in an error-handling middleware declared after `.use(i18n)`, catch
|
|
208
|
-
around `next()` and call `t`, or the middleware's own `i18n.t()`, which always
|
|
209
|
-
answers, in the request's language and in the fallback when the error was
|
|
210
|
-
thrown before the language was read:
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
import { alxia, defineMiddleware, HttpError } from '@alxia/core';
|
|
214
|
-
|
|
215
|
-
alxia()
|
|
216
|
-
.use(i18n)
|
|
217
|
-
.use(
|
|
218
|
-
defineMiddleware(async ({ t, reply }, next) => {
|
|
219
|
-
try {
|
|
220
|
-
return await next();
|
|
221
|
-
} catch (error) {
|
|
222
|
-
if (error instanceof HttpError && error.status === 404) {
|
|
223
|
-
return reply(404, { error: t('errors.not-found') });
|
|
224
|
-
}
|
|
225
|
-
throw error;
|
|
226
|
-
}
|
|
227
|
-
}),
|
|
228
|
-
);
|
|
229
|
-
```
|
|
230
|
-
|
|
231
196
|
### `t()` accepts any key, typos included
|
|
232
197
|
|
|
233
198
|
**When:** the catalogues are built at runtime, read with
|
|
@@ -426,17 +391,15 @@ export const i18n = createI18n({ resources: { en, de }, fallback: 'en' });
|
|
|
426
391
|
|
|
427
392
|
**When:** a request is in French, but `i18n.t()`, `i18n.language()` or
|
|
428
393
|
`@nxgt/i18n`'s `translate` answers in the fallback's language in a
|
|
429
|
-
middleware declared before `.use(i18n)`, in a route declared before it
|
|
430
|
-
in the deprecated `onRequest` and `onResponse` hooks.
|
|
394
|
+
middleware declared before `.use(i18n)`, or in a route declared before it.
|
|
431
395
|
|
|
432
396
|
**Why:** the middleware reads the request's language when the request
|
|
433
397
|
reaches it, and holds it for what runs after it, the answer to an error
|
|
434
|
-
included. What is declared before it
|
|
435
|
-
chain, run with no language to answer in.
|
|
398
|
+
included. What is declared before it runs with no language to answer in.
|
|
436
399
|
|
|
437
400
|
**Fix:** translate after the middleware: declare the middlewares and the
|
|
438
|
-
routes that translate after `.use(i18n)`, and move what
|
|
439
|
-
renders
|
|
401
|
+
routes that translate after `.use(i18n)`, and move what a middleware before
|
|
402
|
+
it renders into a middleware declared after it:
|
|
440
403
|
|
|
441
404
|
```ts
|
|
442
405
|
import { alxia, defineMiddleware } from '@alxia/core';
|
|
@@ -478,7 +441,7 @@ alxia()
|
|
|
478
441
|
### `getLanguage()` answers `en` although the fallback is `fr`
|
|
479
442
|
|
|
480
443
|
**When:** outside a request — at start-up, in a timer, a queue consumer —
|
|
481
|
-
or in a
|
|
444
|
+
or in a middleware [before the language is read](#i18nt-answers-in-the-fallback-before-the-language-is-read),
|
|
482
445
|
`@nxgt/i18n`'s `getLanguage()` answers `'en'` while `i18n.language()`
|
|
483
446
|
answers `'fr'`.
|
|
484
447
|
|
|
@@ -512,7 +475,7 @@ alxia()
|
|
|
512
475
|
.use(cache({ ttl: 60, vary: ['accept-language', 'cookie'] }));
|
|
513
476
|
```
|
|
514
477
|
|
|
515
|
-
A middleware
|
|
478
|
+
A middleware that sets `Vary` with `headers.set` replaces the
|
|
516
479
|
middleware's: see `@alxia/language`'s [troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/language/docs/troubleshooting.md#a-cache-serves-one-language-to-every-visitor),
|
|
517
480
|
which also covers a response in the wrong language — `curl` getting the
|
|
518
481
|
fallback, a `?lang=` that does not stick, a `404` on `/fr/products`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alxia/i18n",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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,14 +41,14 @@
|
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@alxia/core": "^0.
|
|
45
|
-
"@alxia/language": "^0.
|
|
44
|
+
"@alxia/core": "^0.5.0",
|
|
45
|
+
"@alxia/language": "^0.3.0",
|
|
46
46
|
"@nxgt/i18n": "^2.0.0",
|
|
47
47
|
"@types/bun": "^1.4.2"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@alxia/core": "^0.
|
|
51
|
-
"@alxia/language": "^0.
|
|
50
|
+
"@alxia/core": "^0.5.0",
|
|
51
|
+
"@alxia/language": "^0.3.0",
|
|
52
52
|
"@nxgt/i18n": "^2.0.0",
|
|
53
53
|
"typescript": "^6.0.3 || ^7.0.0"
|
|
54
54
|
}
|