@alxia/language 0.1.2 → 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/README.md +10 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +3 -3
- package/dist/language.d.ts +8 -3
- package/dist/language.d.ts.map +1 -1
- package/docs/README.md +3 -3
- package/docs/guide.md +42 -36
- package/docs/roadmap.md +1 -1
- package/docs/troubleshooting.md +71 -57
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -39,12 +39,13 @@ matches whatever its case, by its base language (`fr-CA` for `fr`), or by a
|
|
|
39
39
|
region of it (`pt` for `pt-BR`). `languageSource` says which source decided.
|
|
40
40
|
|
|
41
41
|
Every response says `Content-Language`, and `Vary` by the headers it read.
|
|
42
|
-
With `persist`, a language the query named is kept in the cookie.
|
|
42
|
+
With `persist`, a language the query named is kept in the cookie. Given to
|
|
43
|
+
`use` on the app, it runs on every request: a 404 says `Content-Language` too.
|
|
43
44
|
|
|
44
45
|
## Reading the app's context
|
|
45
46
|
|
|
46
|
-
Annotate `resolve`'s parameter to decide by what an earlier
|
|
47
|
-
the language a signed-in user saved. The
|
|
47
|
+
Annotate `resolve`'s parameter to decide by what an earlier middleware added —
|
|
48
|
+
the language a signed-in user saved. The middleware then requires it: an app
|
|
48
49
|
that does not give `user` before it cannot use it.
|
|
49
50
|
|
|
50
51
|
```ts
|
|
@@ -58,11 +59,11 @@ const byUser = language({
|
|
|
58
59
|
resolve: ({ user }: BaseContext & { user: { language: string } | null }) => user?.language,
|
|
59
60
|
});
|
|
60
61
|
|
|
61
|
-
alxia().
|
|
62
|
+
alxia().plugin(auth).use(byUser); // auth derives user
|
|
62
63
|
alxia().use(byUser); // a compile error: this app gives no `user`
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
A `resolve` annotated `any` would require nothing, so the
|
|
66
|
+
A `resolve` annotated `any` would require nothing, so the middleware is refused
|
|
66
67
|
on every app: annotate what it reads, or leave it unannotated.
|
|
67
68
|
|
|
68
69
|
## Options
|
|
@@ -76,21 +77,22 @@ on every app: annotate what it reads, or leave it unannotated.
|
|
|
76
77
|
| `pathIndex` | 0 | |
|
|
77
78
|
| `persist` | `false` | `true`, or `{ maxAge, secure }` |
|
|
78
79
|
| `contentLanguage` | `true` | |
|
|
79
|
-
| `resolve` | none | `(ctx) => string \| undefined`; annotate `ctx` to read what an earlier
|
|
80
|
+
| `resolve` | none | `(ctx) => string \| undefined`; annotate `ctx` to read what an earlier middleware adds |
|
|
80
81
|
| `vary` | none | the headers `resolve` reads, added to `Vary` |
|
|
81
82
|
|
|
82
83
|
## API
|
|
83
84
|
|
|
84
85
|
| export | |
|
|
85
86
|
| --- | --- |
|
|
86
|
-
| `language(options)` | the
|
|
87
|
+
| `language(options)` | the middleware, given to `app.use`: it adds `language` and `languageSource` |
|
|
87
88
|
| `LanguageOptions` | its options: `supported`, `fallback`, `order`, `query`, `cookie`, `pathIndex`, `persist`, `contentLanguage`, `resolve`, `vary` |
|
|
88
89
|
| `negotiate(header, supported)` | the supported language `Accept-Language` prefers |
|
|
89
90
|
| `parseAcceptLanguage(header)`, `match(tag, supported)` | its parts |
|
|
90
91
|
| `LanguageContext`, `LanguageSource`, `Accepted` | its types |
|
|
92
|
+
| `LanguageMiddleware<L, Requires>` | what `language()` returns: a middleware adding `LanguageContext<L>`, requiring of the app what `resolve` reads |
|
|
91
93
|
|
|
92
94
|
## Documentation
|
|
93
95
|
|
|
94
|
-
- [Guide](https://github.com/softistx/alxia/tree/develop/packages/language/docs): every option with its default and an example, how the language is found and `Accept-Language` negotiated, reading what an earlier
|
|
96
|
+
- [Guide](https://github.com/softistx/alxia/tree/develop/packages/language/docs): every option with its default and an example, how the language is found and `Accept-Language` negotiated, reading what an earlier middleware added, the typed context, and the headers the middleware adds.
|
|
95
97
|
- [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/language/docs/troubleshooting.md): an error, or a response in the wrong language, and what to do about it.
|
|
96
98
|
- [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/language/docs/roadmap.md): what is coming, and what is not planned.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type LanguageOptions, language } from './language';
|
|
1
|
+
export { type LanguageMiddleware, type LanguageOptions, language, } from './language';
|
|
2
2
|
export { type Accepted, match, negotiate, parseAcceptLanguage, } from './negotiate';
|
|
3
3
|
export type { LanguageContext, LanguageSource } from './types';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,QAAQ,GACR,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,KAAK,QAAQ,EACb,KAAK,EACL,SAAS,EACT,mBAAmB,GACnB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// src/language.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
defineMiddleware,
|
|
4
|
+
markFactory
|
|
5
|
+
} from "@alxia/core";
|
|
3
6
|
|
|
4
7
|
// src/decide.ts
|
|
5
8
|
import { vary } from "@alxia/core";
|
|
@@ -107,12 +110,13 @@ function language(options) {
|
|
|
107
110
|
if (!settings.supported.includes(settings.fallback)) {
|
|
108
111
|
throw new TypeError(`language(): the fallback "${settings.fallback}" is not supported`);
|
|
109
112
|
}
|
|
110
|
-
return
|
|
113
|
+
return defineMiddleware()(function language(ctx, next) {
|
|
111
114
|
const found = decide(settings, ctx);
|
|
112
115
|
respond(settings, ctx, found);
|
|
113
|
-
return found;
|
|
114
|
-
})
|
|
116
|
+
return next(found);
|
|
117
|
+
});
|
|
115
118
|
}
|
|
119
|
+
markFactory(language);
|
|
116
120
|
export {
|
|
117
121
|
language,
|
|
118
122
|
match,
|
|
@@ -120,5 +124,5 @@ export {
|
|
|
120
124
|
parseAcceptLanguage
|
|
121
125
|
};
|
|
122
126
|
|
|
123
|
-
//# debugId=
|
|
127
|
+
//# debugId=5CF534D30FB9E72064756E2164756E21
|
|
124
128
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/language.ts", "../src/decide.ts", "../src/negotiate.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import {
|
|
5
|
+
"import {\n\ttype BaseContext,\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\tmarkFactory,\n\ttype Next,\n\ttype RequiresOf,\n} from '@alxia/core';\nimport { decide, respond, type Settings } from './decide';\nimport type { LanguageContext, LanguageSource } from './types';\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.\n */\nexport interface LanguageOptions<\n\tL extends string,\n\tCtx extends object = BaseContext,\n> {\n\t/** The languages the app speaks: the context's `language` is one of them. */\n\treadonly supported: readonly L[];\n\t/** The one it speaks when the request names none it does. */\n\treadonly fallback: NoInfer<L>;\n\t/** Where it looks, in order. `query`, `cookie`, then `header` by default. */\n\treadonly order?: readonly LanguageSource[];\n\t/** The query parameter: `?lang=fr`. `lang` by default. */\n\treadonly query?: string;\n\t/** The cookie. `language` by default. */\n\treadonly cookie?: string;\n\t/** The index of the path segment: `/fr/products` is 0. 0 by default. */\n\treadonly pathIndex?: number;\n\t/**\n\t * Whether a language named by the query is kept in the cookie, so the\n\t * next request speaks it too. Off by default.\n\t */\n\treadonly persist?:\n\t\t| boolean\n\t\t| { readonly maxAge?: number; readonly secure?: boolean };\n\t/** Says `Content-Language` on every response. On by default. */\n\treadonly contentLanguage?: boolean;\n\t/**\n\t * Decides itself, after every source: a user's saved preference. Annotate\n\t * its parameter to read what an earlier plugin adds —\n\t * `(ctx: BaseContext & { user: User })` — and the app that uses the\n\t * plugin must then give it.\n\t */\n\treadonly resolve?: (ctx: BaseContext & Ctx) => string | undefined;\n\t/**\n\t * The request headers `resolve` reads, added to `Vary` so a cache keeps\n\t * one response per value: `['authorization']`. None by default.\n\t */\n\treadonly vary?: readonly string[];\n}\n\n/**\n * What `language()` makes: a middleware that gives `language`, one of `L`,\n * and requires `Requires` of the app — what `resolve` reads.\n */\nexport type LanguageMiddleware<\n\tL extends string,\n\tRequires extends object = Empty,\n> = Middleware<Requires, Promise<Next<LanguageContext<L>>>>;\n\n/**\n * The request's language, as a middleware: the routes declared after it read\n * `language`, typed as one of `supported` — never a string a client made\n * up. It is read from the query, a cookie, a path segment and\n * `Accept-Language` — weights, `fr-CA` for `fr`, `fr` for `fr-FR` — in the\n * order given, then `fallback`.\n *\n * ```ts\n * app.use(language({ supported: ['en', 'fr'], fallback: 'en' }))\n * .get('/', ({ language, reply }) => reply(200, language)); // 'en' | 'fr'\n * ```\n */\nexport function language<\n\tconst L extends string,\n\tCtx extends object = BaseContext,\n>(\n\toptions: LanguageOptions<L, Ctx>,\n): LanguageMiddleware<L, RequiresOf<Ctx, 'resolve'>> {\n\tconst settings: Settings<L> = {\n\t\tsupported: options.supported,\n\t\tfallback: options.fallback,\n\t\torder: options.order ?? ['query', 'cookie', 'header'],\n\t\tquery: options.query ?? 'lang',\n\t\tcookie: options.cookie ?? 'language',\n\t\tpathIndex: options.pathIndex ?? 0,\n\t\tpersist:\n\t\t\toptions.persist === true\n\t\t\t\t? {}\n\t\t\t\t: options.persist === false\n\t\t\t\t\t? undefined\n\t\t\t\t\t: options.persist,\n\t\tcontentLanguage: options.contentLanguage !== false,\n\t\t// `app.plugin` has checked that the app gives what `resolve` reads.\n\t\tresolve: options.resolve as Settings<L>['resolve'],\n\t\tvary: options.vary ?? [],\n\t};\n\tif (!settings.supported.includes(settings.fallback)) {\n\t\tthrow new TypeError(\n\t\t\t`language(): the fallback \"${settings.fallback}\" is not supported`,\n\t\t);\n\t}\n\treturn defineMiddleware<RequiresOf<Ctx, 'resolve'>>()(\n\t\tfunction language(ctx, next) {\n\t\t\tconst found: LanguageContext<L> = decide(settings, ctx);\n\t\t\trespond(settings, ctx, found);\n\t\t\treturn next(found);\n\t\t},\n\t);\n}\n\nmarkFactory(language);\n",
|
|
6
6
|
"import { type BaseContext, vary } from '@alxia/core';\nimport { match, negotiate } from './negotiate';\nimport type { LanguageContext, LanguageSource } from './types';\n\n/** `language()`'s options, their defaults applied. */\nexport interface Settings<L extends string> {\n\treadonly supported: readonly L[];\n\treadonly fallback: L;\n\treadonly order: readonly LanguageSource[];\n\treadonly query: string;\n\treadonly cookie: string;\n\treadonly pathIndex: number;\n\treadonly persist:\n\t\t| { readonly maxAge?: number; readonly secure?: boolean }\n\t\t| undefined;\n\treadonly contentLanguage: boolean;\n\treadonly resolve: ((ctx: BaseContext) => string | undefined) | undefined;\n\treadonly vary: readonly string[];\n}\n\n/** The language of one source, if it names one `supported` has. */\nfunction read<L extends string>(\n\tsettings: Settings<L>,\n\tctx: BaseContext,\n\tsource: LanguageSource,\n): L | undefined {\n\tconst { supported } = settings;\n\tswitch (source) {\n\t\tcase 'query': {\n\t\t\tconst value = ctx.url.searchParams.get(settings.query);\n\t\t\treturn value === null ? undefined : match(value, supported);\n\t\t}\n\t\tcase 'cookie': {\n\t\t\tconst value = new Bun.CookieMap(\n\t\t\t\tctx.request.headers.get('cookie') ?? '',\n\t\t\t).get(settings.cookie);\n\t\t\treturn value === null ? undefined : match(value, supported);\n\t\t}\n\t\tcase 'path': {\n\t\t\tconst segment = ctx.url.pathname.split('/').filter(Boolean)[\n\t\t\t\tsettings.pathIndex\n\t\t\t];\n\t\t\treturn segment === undefined ? undefined : match(segment, supported);\n\t\t}\n\t\tcase 'header':\n\t\t\treturn negotiate(ctx.request.headers.get('accept-language'), supported);\n\t}\n}\n\n/** The request's language: each source in `order`, then `resolve`, then `fallback`. */\nexport function decide<L extends string>(\n\tsettings: Settings<L>,\n\tctx: BaseContext,\n): LanguageContext<L> {\n\tfor (const source of settings.order) {\n\t\tconst value = read(settings, ctx, source);\n\t\tif (value !== undefined) return { language: value, languageSource: source };\n\t}\n\tconst resolved = settings.resolve?.(ctx);\n\tconst value =\n\t\tresolved === undefined ? undefined : match(resolved, settings.supported);\n\treturn value === undefined\n\t\t? { language: settings.fallback, languageSource: 'fallback' }\n\t\t: { language: value, languageSource: 'resolve' };\n}\n\n/** What the response says of it: `Vary`, `Content-Language`, the kept cookie. */\nexport function respond<L extends string>(\n\tsettings: Settings<L>,\n\tctx: BaseContext,\n\tfound: LanguageContext<L>,\n): void {\n\tconst { headers, cookies } = ctx.set;\n\tif (settings.order.includes('header')) vary(headers, 'Accept-Language');\n\tif (settings.order.includes('cookie')) vary(headers, 'Cookie');\n\tfor (const name of settings.vary) vary(headers, name);\n\tif (settings.contentLanguage) headers.set('content-language', found.language);\n\tconst { persist } = settings;\n\tif (persist !== undefined && found.languageSource === 'query') {\n\t\tcookies.set(settings.cookie, found.language, {\n\t\t\tpath: '/',\n\t\t\tsameSite: 'lax',\n\t\t\thttpOnly: false,\n\t\t\tsecure: persist.secure ?? true,\n\t\t\tmaxAge: persist.maxAge ?? 365 * 24 * 60 * 60,\n\t\t});\n\t}\n}\n",
|
|
7
7
|
"/** One language a client accepts, with its weight. */\nexport interface Accepted {\n\treadonly tag: string;\n\treadonly q: number;\n}\n\n/** `Accept-Language` as its languages, the most wanted first; a weight of 0 refused. */\nexport function parseAcceptLanguage(\n\theader: string | null | undefined,\n): Accepted[] {\n\tif (!header) return [];\n\treturn header\n\t\t.split(',')\n\t\t.map((part, index) => {\n\t\t\tconst [tag = '', ...params] = part.trim().split(';');\n\t\t\tconst q = params\n\t\t\t\t.map((param) => param.trim())\n\t\t\t\t.find((param) => param.startsWith('q='));\n\t\t\tconst weight = q === undefined ? 1 : Number(q.slice(2));\n\t\t\treturn {\n\t\t\t\ttag: tag.trim(),\n\t\t\t\tq: Number.isFinite(weight) ? weight : 0,\n\t\t\t\tindex,\n\t\t\t};\n\t\t})\n\t\t.filter((entry) => entry.tag !== '' && entry.q > 0)\n\t\t.sort((a, b) => b.q - a.q || a.index - b.index)\n\t\t.map(({ tag, q }) => ({ tag, q }));\n}\n\n/**\n * The supported language a tag names: itself, whatever its case; its base\n * language — `fr-CA` to `fr`; or a region of it — `fr` to `fr-FR`.\n * `undefined` when none.\n */\nexport function match<const L extends string>(\n\ttag: string,\n\tsupported: readonly L[],\n): L | undefined {\n\tconst wanted = tag.toLowerCase();\n\tif (wanted === '*') return supported[0];\n\tconst exact = supported.find((language) => language.toLowerCase() === wanted);\n\tif (exact !== undefined) return exact;\n\tconst base = wanted.split('-')[0] ?? wanted;\n\treturn (\n\t\tsupported.find((language) => language.toLowerCase() === base) ??\n\t\tsupported.find((language) => language.toLowerCase().split('-')[0] === base)\n\t);\n}\n\n/** The supported language `Accept-Language` prefers, or `undefined`. */\nexport function negotiate<const L extends string>(\n\theader: string | null | undefined,\n\tsupported: readonly L[],\n): L | undefined {\n\tfor (const { tag } of parseAcceptLanguage(header)) {\n\t\tconst found = match(tag, supported);\n\t\tif (found !== undefined) return found;\n\t}\n\treturn undefined;\n}\n"
|
|
8
8
|
],
|
|
9
|
-
"mappings": "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
|
|
10
|
-
"debugId": "
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "5CF534D30FB9E72064756E2164756E21",
|
|
11
11
|
"names": []
|
|
12
12
|
}
|
package/dist/language.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BaseContext, type RequiresOf } from '@alxia/core';
|
|
1
|
+
import { type BaseContext, type Empty, type Middleware, type Next, type RequiresOf } from '@alxia/core';
|
|
2
2
|
import type { LanguageContext, LanguageSource } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* `Ctx` is the type `resolve`'s parameter is annotated with —
|
|
@@ -42,7 +42,12 @@ export interface LanguageOptions<L extends string, Ctx extends object = BaseCont
|
|
|
42
42
|
readonly vary?: readonly string[];
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* What `language()` makes: a middleware that gives `language`, one of `L`,
|
|
46
|
+
* and requires `Requires` of the app — what `resolve` reads.
|
|
47
|
+
*/
|
|
48
|
+
export type LanguageMiddleware<L extends string, Requires extends object = Empty> = Middleware<Requires, Promise<Next<LanguageContext<L>>>>;
|
|
49
|
+
/**
|
|
50
|
+
* The request's language, as a middleware: the routes declared after it read
|
|
46
51
|
* `language`, typed as one of `supported` — never a string a client made
|
|
47
52
|
* up. It is read from the query, a cookie, a path segment and
|
|
48
53
|
* `Accept-Language` — weights, `fr-CA` for `fr`, `fr` for `fr-FR` — in the
|
|
@@ -53,5 +58,5 @@ export interface LanguageOptions<L extends string, Ctx extends object = BaseCont
|
|
|
53
58
|
* .get('/', ({ language, reply }) => reply(200, language)); // 'en' | 'fr'
|
|
54
59
|
* ```
|
|
55
60
|
*/
|
|
56
|
-
export declare function language<const L extends string, Ctx extends object = BaseContext>(options: LanguageOptions<L, Ctx>):
|
|
61
|
+
export declare function language<const L extends string, Ctx extends object = BaseContext>(options: LanguageOptions<L, Ctx>): LanguageMiddleware<L, RequiresOf<Ctx, 'resolve'>>;
|
|
57
62
|
//# sourceMappingURL=language.d.ts.map
|
package/dist/language.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"language.d.ts","sourceRoot":"","sources":["../src/language.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"language.d.ts","sourceRoot":"","sources":["../src/language.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,WAAW,EAEhB,KAAK,KAAK,EACV,KAAK,UAAU,EAEf,KAAK,IAAI,EACT,KAAK,UAAU,EACf,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;GAIG;AACH,MAAM,WAAW,eAAe,CAC/B,CAAC,SAAS,MAAM,EAChB,GAAG,SAAS,MAAM,GAAG,WAAW;IAEhC,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC;IACjC,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EACd,OAAO,GACP;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC3D,gEAAgE;IAChE,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,GAAG,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC;IAClE;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,CAC7B,CAAC,SAAS,MAAM,EAChB,QAAQ,SAAS,MAAM,GAAG,KAAK,IAC5B,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE5D;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CACvB,KAAK,CAAC,CAAC,SAAS,MAAM,EACtB,GAAG,SAAS,MAAM,GAAG,WAAW,EAEhC,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,GAAG,CAAC,GAC9B,kBAAkB,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,CA+BnD"}
|
package/docs/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
The [package README](../README.md) is the short version. This folder is
|
|
4
4
|
the long one: every option with its default and an example, how a
|
|
5
5
|
request's language is found and how `Accept-Language` is negotiated, what
|
|
6
|
-
the routes behind the
|
|
6
|
+
the routes behind the middleware read, and what to do when a response is not in
|
|
7
7
|
the language you expected.
|
|
8
8
|
|
|
9
9
|
| Page | Read it when |
|
|
10
10
|
| --- | --- |
|
|
11
|
-
| [Guide](guide.md) | choosing the sources and their order, reading the language from the path, keeping a choice in a cookie, deciding by what an earlier
|
|
12
|
-
| [Troubleshooting](troubleshooting.md) |
|
|
11
|
+
| [Guide](guide.md) | choosing the sources and their order, reading the language from the path, keeping a choice in a cookie, deciding by what an earlier middleware added, typing what the routes read, caching the responses, or testing them |
|
|
12
|
+
| [Troubleshooting](troubleshooting.md) | `language()` threw at start-up, `tsc` refused an option or a route, or a response is in the wrong language |
|
|
13
13
|
| [Roadmap](roadmap.md) | wondering what is coming, and what is not planned |
|
package/docs/guide.md
CHANGED
|
@@ -28,8 +28,11 @@ client that names no language it supports gets `Hello`. `current` is typed
|
|
|
28
28
|
```ts
|
|
29
29
|
function language<const L extends string, Ctx extends object = BaseContext>(
|
|
30
30
|
options: LanguageOptions<L, Ctx>,
|
|
31
|
-
):
|
|
32
|
-
|
|
31
|
+
): LanguageMiddleware<L, RequiresOf<Ctx, 'resolve'>>;
|
|
32
|
+
|
|
33
|
+
// A middleware: it gives `language`, and requires of the app what `resolve` reads.
|
|
34
|
+
type LanguageMiddleware<L extends string, Requires extends object = Empty> =
|
|
35
|
+
Middleware<Requires, Promise<Next<LanguageContext<L>>>>;
|
|
33
36
|
|
|
34
37
|
interface LanguageOptions<L extends string, Ctx extends object = BaseContext> {
|
|
35
38
|
readonly supported: readonly L[];
|
|
@@ -54,11 +57,13 @@ type `resolve`'s parameter is annotated with. `RequiresOf<Ctx>`,
|
|
|
54
57
|
see
|
|
55
58
|
[Reading the app's context](#reading-the-apps-context).
|
|
56
59
|
|
|
57
|
-
`language()` returns
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
`language()` returns a middleware: pass it to `app.use`, called. A `use()` on
|
|
61
|
+
the app runs on every request, in declaration order, a request no route
|
|
62
|
+
matches included, and adds `language` and `languageSource` to what is
|
|
63
|
+
declared **after** it: the middlewares and the routes. Inside a
|
|
64
|
+
[group](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/groups-and-plugins.md)
|
|
65
|
+
it applies to the group's routes only. A route declared before it neither
|
|
66
|
+
runs it nor reads them.
|
|
62
67
|
|
|
63
68
|
It throws once, when it is created, if `fallback` is not one of
|
|
64
69
|
`supported`:
|
|
@@ -82,7 +87,7 @@ for when they cannot.
|
|
|
82
87
|
| `cookie` | `string` | `'language'` | the cookie read, and written by `persist` |
|
|
83
88
|
| `pathIndex` | `number` | `0` | the path segment read by the `path` source: `/fr/products` is 0 |
|
|
84
89
|
| `persist` | `boolean \| { maxAge?, secure? }` | `false` | a language the query named is written to the cookie |
|
|
85
|
-
| `contentLanguage` | `boolean` | `true` | `Content-Language` on every response the
|
|
90
|
+
| `contentLanguage` | `boolean` | `true` | `Content-Language` on every response the middleware runs for |
|
|
86
91
|
| `resolve` | `(ctx: BaseContext & Ctx) => string \| undefined` | none | decides after every source, before `fallback`; its annotated parameter types what it reads |
|
|
87
92
|
| `vary` | `readonly string[]` | none | the request headers `resolve` reads, added to `Vary` |
|
|
88
93
|
|
|
@@ -105,7 +110,7 @@ language({ supported, fallback: 'en' });
|
|
|
105
110
|
```
|
|
106
111
|
|
|
107
112
|
`fallback` must be one of them; the types refuse another (`fallback: 'de'`
|
|
108
|
-
is a compile error), and so does
|
|
113
|
+
is a compile error), and so does `language()`, at start-up.
|
|
109
114
|
|
|
110
115
|
### `order`
|
|
111
116
|
|
|
@@ -141,7 +146,7 @@ language({ supported: ['en', 'fr'], fallback: 'en', order: ['path'], pathIndex:
|
|
|
141
146
|
// /shop/fr/products → 'fr'
|
|
142
147
|
```
|
|
143
148
|
|
|
144
|
-
The
|
|
149
|
+
The middleware reads the segment; it does not route on it or remove it. The
|
|
145
150
|
routes still declare it, as a parameter:
|
|
146
151
|
|
|
147
152
|
```ts
|
|
@@ -150,7 +155,7 @@ const app = alxia()
|
|
|
150
155
|
.get('/:lang/products', ({ language: current, reply }) => reply(200, current));
|
|
151
156
|
|
|
152
157
|
await app.request('/fr/products'); // 'fr'
|
|
153
|
-
await app.request('/products'); // 404: no route matches
|
|
158
|
+
await app.request('/products'); // 404: no route matches (it still says Content-Language)
|
|
154
159
|
```
|
|
155
160
|
|
|
156
161
|
A segment is matched like any other tag, so `/fr-ca/products` is `fr` too.
|
|
@@ -184,8 +189,8 @@ the cookie or the header sets nothing. For the cookie to be read back,
|
|
|
184
189
|
|
|
185
190
|
### `contentLanguage`
|
|
186
191
|
|
|
187
|
-
On by default: every response of a
|
|
188
|
-
`Content-Language: <language
|
|
192
|
+
On by default: every response of a request the middleware runs on says
|
|
193
|
+
`Content-Language: <language>`, a 404 included when it is used on the app. A route that sets its own wins over it:
|
|
189
194
|
|
|
190
195
|
```ts
|
|
191
196
|
.get('/legal', ({ reply }) => reply(200, legalText, { headers: { 'content-language': 'fr' } }));
|
|
@@ -214,27 +219,27 @@ language({
|
|
|
214
219
|
```
|
|
215
220
|
|
|
216
221
|
`vary` names the request headers `resolve` reads, so a shared cache keeps
|
|
217
|
-
one response per value; the
|
|
222
|
+
one response per value; the middleware cannot see what a function reads.
|
|
218
223
|
|
|
219
224
|
It receives the request's `BaseContext` — `request`, `url`, `ip`,
|
|
220
225
|
`pathParams`, `set` — and, when its parameter is annotated, what an earlier
|
|
221
|
-
|
|
226
|
+
middleware added: see [Reading the app's context](#reading-the-apps-context). It
|
|
222
227
|
returns a tag, or `undefined` for none. The tag is matched against
|
|
223
228
|
`supported` like any other: a tag it does not support is ignored, and
|
|
224
229
|
`fallback` decides. It is synchronous: a preference kept in a database is
|
|
225
230
|
either written to the cookie when the user saves it — see
|
|
226
231
|
[the realistic setup](#a-realistic-setup) — or loaded by an async `derive`
|
|
227
|
-
or
|
|
232
|
+
or middleware before `language()`, which `resolve` then reads — see
|
|
228
233
|
[Reading the app's context](#reading-the-apps-context).
|
|
229
234
|
|
|
230
235
|
### Reading the app's context
|
|
231
236
|
|
|
232
|
-
To decide by what an earlier
|
|
237
|
+
To decide by what an earlier middleware added, such as a signed-in `user` and
|
|
233
238
|
the language they saved, annotate `resolve`'s parameter. `language()` infers
|
|
234
239
|
what it reads from that annotation — `supported` still types `language` —
|
|
235
|
-
and the
|
|
240
|
+
and the middleware requires it, as a
|
|
236
241
|
[`definePlugin`](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/writing-a-plugin.md#a-plugin-that-needs-an-earlier-one)
|
|
237
|
-
plugin: an app that does not give `user` before it cannot use it.
|
|
242
|
+
plugin does: an app that does not give `user` before it cannot use it.
|
|
238
243
|
|
|
239
244
|
```ts
|
|
240
245
|
import { alxia, type BaseContext } from '@alxia/core';
|
|
@@ -254,26 +259,26 @@ const byUser = language({
|
|
|
254
259
|
});
|
|
255
260
|
|
|
256
261
|
const app = alxia()
|
|
257
|
-
.
|
|
262
|
+
.plugin(auth) // an app: derives user: User | null
|
|
258
263
|
.use(byUser)
|
|
259
264
|
.get('/', ({ language: current, reply }) => reply(200, current)); // 'en' | 'fr'
|
|
260
265
|
|
|
261
266
|
alxia().use(byUser);
|
|
262
|
-
// error:
|
|
267
|
+
// error: Property 'user' is missing in type 'BaseContext & Empty' but required in type '{ user: User | null; }'
|
|
263
268
|
```
|
|
264
269
|
|
|
265
270
|
The annotation may be `BaseContext & { user: User }` or `{ user: User }`
|
|
266
|
-
alone; either way the
|
|
271
|
+
alone; either way the middleware requires `{ user: User }`. An app whose `user`
|
|
267
272
|
has a type that does not fit it is refused too —
|
|
268
|
-
`
|
|
269
|
-
while a narrower one passes: an app deriving `user: User` may use a
|
|
273
|
+
`Types of property 'user' are incompatible` —
|
|
274
|
+
while a narrower one passes: an app deriving `user: User` may use a middleware
|
|
270
275
|
that reads `User | null`. Annotating a key `BaseContext` already has with
|
|
271
276
|
a type it does not give — `({ url }: { url: string })` — is refused the
|
|
272
277
|
same way.
|
|
273
|
-
A `resolve` left unannotated reads `BaseContext` only, and the
|
|
278
|
+
A `resolve` left unannotated reads `BaseContext` only, and the middleware
|
|
274
279
|
requires nothing.
|
|
275
280
|
Annotated `unknown` or `object`, it requires nothing either. Annotated
|
|
276
|
-
`any`, it would read anything and require nothing, so the
|
|
281
|
+
`any`, it would read anything and require nothing, so the middleware is
|
|
277
282
|
refused on every app instead:
|
|
278
283
|
[`the plugin's resolve reads its context as any: annotate what it reads, or leave it unannotated`](troubleshooting.md#the-plugins-resolve-reads-its-context-as-any-annotate-what-it-reads-or-leave-it-unannotated).
|
|
279
284
|
|
|
@@ -387,11 +392,11 @@ function negotiate<const L extends string>(header: string | null | undefined, su
|
|
|
387
392
|
|
|
388
393
|
The three are exported for code outside a request — a background job
|
|
389
394
|
choosing an email's language from a saved header, a WebSocket upgrade —
|
|
390
|
-
and return the same answers the
|
|
395
|
+
and return the same answers the middleware would.
|
|
391
396
|
|
|
392
397
|
## The typed context
|
|
393
398
|
|
|
394
|
-
|
|
399
|
+
What the routes and middlewares after it read:
|
|
395
400
|
|
|
396
401
|
```ts
|
|
397
402
|
interface LanguageContext<L extends string> {
|
|
@@ -448,7 +453,7 @@ so a shared cache keeps one copy per language rather than serving the first
|
|
|
448
453
|
one to everyone. The query and the path are part of the URL, and need no
|
|
449
454
|
`Vary`. What `resolve` reads is added only when the `vary` option names it.
|
|
450
455
|
|
|
451
|
-
The
|
|
456
|
+
The middleware adds to `ctx.set.headers`, and a reply's own `Vary` adds to it
|
|
452
457
|
rather than replacing it:
|
|
453
458
|
|
|
454
459
|
```ts
|
|
@@ -462,8 +467,11 @@ the cache the same headers, so it keys by them:
|
|
|
462
467
|
`cache({ ttl: 60, vary: ['accept-language', 'cookie'] })`. A response that
|
|
463
468
|
sets a cookie — a `persist` from the query — is not cached.
|
|
464
469
|
|
|
465
|
-
A request that matches no route — a `404`, a `405` —
|
|
466
|
-
|
|
470
|
+
A request that matches no route — a `404`, a `405` — runs the app's
|
|
471
|
+
`use()` middlewares, so with `language()` on the app it carries these
|
|
472
|
+
headers too. Inside a `group`, the middleware runs for the group's routes
|
|
473
|
+
only: an unmatched request, even one under the group's prefix, carries
|
|
474
|
+
none of them.
|
|
467
475
|
|
|
468
476
|
## A realistic setup
|
|
469
477
|
|
|
@@ -489,7 +497,7 @@ export const app = alxia()
|
|
|
489
497
|
language({
|
|
490
498
|
supported,
|
|
491
499
|
fallback: 'en',
|
|
492
|
-
persist: { secure:
|
|
500
|
+
persist: { secure: Bun.env.NODE_ENV !== 'development' }, // read at runtime: bun build inlines process.env
|
|
493
501
|
}),
|
|
494
502
|
)
|
|
495
503
|
.get('/', ({ language: current, reply }) => reply(200, messages[current].welcome))
|
|
@@ -499,11 +507,9 @@ export const app = alxia()
|
|
|
499
507
|
set.cookies.set('language', chosen, { path: '/', sameSite: 'lax', maxAge: 365 * 24 * 60 * 60 });
|
|
500
508
|
return reply(200, { language: chosen });
|
|
501
509
|
});
|
|
502
|
-
|
|
503
|
-
export type App = typeof app;
|
|
504
510
|
```
|
|
505
511
|
|
|
506
|
-
The saved choice is the `language` cookie, which the
|
|
512
|
+
The saved choice is the `language` cookie, which the middleware reads on every
|
|
507
513
|
request after, before the browser's languages. `secure` is off outside
|
|
508
514
|
production, so the cookie survives a plain-`http` development server.
|
|
509
515
|
|
|
@@ -548,6 +554,6 @@ describe('language', () => {
|
|
|
548
554
|
```
|
|
549
555
|
|
|
550
556
|
[`@alxia/i18n`](https://www.npmjs.com/package/@alxia/i18n) builds on this
|
|
551
|
-
|
|
557
|
+
middleware — its options pass through — and adds `t()` bound to the request's
|
|
552
558
|
language. When something does not behave as described here,
|
|
553
559
|
[Troubleshooting](troubleshooting.md) starts from the symptom.
|
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
|
-
|
|
10
|
+
- **A middleware, not a plugin (0.4).** `app.use(language({ ... }))` runs on every request, a 404 included, which then says `Content-Language` and `Vary` too. `app.plugin(language(...))`, 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Troubleshooting
|
|
2
2
|
|
|
3
|
-
Each entry is headed by the text you see: the error
|
|
3
|
+
Each entry is headed by the text you see: the error `language()` throws at
|
|
4
4
|
start-up, an error from `tsc`, or — for a trap that prints nothing — what
|
|
5
5
|
the response does that you did not expect.
|
|
6
6
|
|
|
@@ -12,13 +12,13 @@ the response does that you did not expect.
|
|
|
12
12
|
|
|
13
13
|
- [`Type '"de"' is not assignable to type '"en" | "fr"'`](#type-de-is-not-assignable-to-type-en--fr)
|
|
14
14
|
- [`Property 'language' does not exist on type 'Context<Empty, "/", Empty>'`](#property-language-does-not-exist-on-type-contextempty--empty)
|
|
15
|
-
- [`Type '
|
|
15
|
+
- [`Type 'LanguageMiddleware<string, Empty>' is not assignable to type '"this looks like a factory given uncalled: call it, as use(cors()) and not use(cors)"'`](#type-languagemiddlewarestring-empty-is-not-assignable-to-type-this-looks-like-a-factory-given-uncalled-call-it-as-usecors-and-not-usecors)
|
|
16
16
|
- [`Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ en: string; fr: string; }'`](#element-implicitly-has-an-any-type-because-expression-of-type-string-cant-be-used-to-index-type--en-string-fr-string-)
|
|
17
17
|
- [`Type 'string | null' is not assignable to type 'string | undefined'`](#type-string--null-is-not-assignable-to-type-string--undefined)
|
|
18
18
|
- [`Type 'Promise<string>' is not assignable to type 'string'`](#type-promisestring-is-not-assignable-to-type-string)
|
|
19
19
|
- [`Property 'user' does not exist on type 'BaseContext'`](#property-user-does-not-exist-on-type-basecontext)
|
|
20
|
-
- [`
|
|
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-)
|
|
21
|
+
- [`Types of property 'user' are incompatible`](#types-of-property-user-are-incompatible)
|
|
22
22
|
- [`the plugin's resolve reads its context as any: annotate what it reads, or leave it unannotated`](#the-plugins-resolve-reads-its-context-as-any-annotate-what-it-reads-or-leave-it-unannotated)
|
|
23
23
|
|
|
24
24
|
**Responses**
|
|
@@ -79,13 +79,12 @@ error TS2339: Property 'language' does not exist on type 'Context<Empty, "/", Em
|
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
**When:** a route reads `language` but is declared before
|
|
82
|
-
`.use(language(…))`, or in another app or group than the one that
|
|
82
|
+
`.use(language(…))`, or in another app or group than the one that mounts it.
|
|
83
83
|
|
|
84
|
-
**Why:** the
|
|
85
|
-
|
|
86
|
-
it.
|
|
84
|
+
**Why:** the middleware applies to what is declared after it, at runtime
|
|
85
|
+
and in the types alike. A route before it never runs it.
|
|
87
86
|
|
|
88
|
-
**Fix:** use
|
|
87
|
+
**Fix:** `use` it first:
|
|
89
88
|
|
|
90
89
|
```ts
|
|
91
90
|
const app = alxia()
|
|
@@ -93,21 +92,27 @@ const app = alxia()
|
|
|
93
92
|
.get('/', ({ language: current, reply }) => reply(200, current));
|
|
94
93
|
```
|
|
95
94
|
|
|
96
|
-
### `Type '
|
|
95
|
+
### `Type 'LanguageMiddleware<string, Empty>' is not assignable to type '"this looks like a factory given uncalled: call it, as use(cors()) and not use(cors)"'`
|
|
97
96
|
|
|
98
97
|
```text
|
|
99
|
-
error
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
Types of parameters 'options' and 'app' are incompatible.
|
|
103
|
-
Type 'Alxia<Empty, Empty, "", never>' is missing the following properties from type 'LanguageOptions<string, BaseContext>': supported, fallback
|
|
98
|
+
error TS2345: Argument of type '<const L extends string, Ctx extends object = BaseContext>(options: LanguageOptions<L, Ctx>) => LanguageMiddleware<L, RequiresOf<Ctx, "resolve">>' is not assignable to parameter of type '…'.
|
|
99
|
+
…
|
|
100
|
+
Type 'LanguageMiddleware<string, Empty>' is not assignable to type '"this looks like a factory given uncalled: call it, as use(cors()) and not use(cors)"'.
|
|
104
101
|
```
|
|
105
102
|
|
|
106
|
-
**When:** `app.use(language)`,
|
|
103
|
+
**When:** `app.use(language)`, the factory given uncalled. The message names
|
|
104
|
+
`cors` as its example, whichever factory it is. It also throws where it is
|
|
105
|
+
declared, since alxia 0.5, rather than answering each request with a 500:
|
|
107
106
|
|
|
108
|
-
|
|
107
|
+
```text
|
|
108
|
+
TypeError: use(): argument 1 looks like a factory (language): call it, use(language())
|
|
109
|
+
```
|
|
109
110
|
|
|
110
|
-
**
|
|
111
|
+
**Why:** `language` makes the middleware; it is not the middleware. A
|
|
112
|
+
function that returns a function is no middleware, and `language` is marked
|
|
113
|
+
as a factory, so `use`, a route and `plugin` refuse it.
|
|
114
|
+
|
|
115
|
+
**Fix:** call it with its options:
|
|
111
116
|
|
|
112
117
|
```ts
|
|
113
118
|
alxia().use(language({ supported: ['en', 'fr'], fallback: 'en' }));
|
|
@@ -183,7 +188,7 @@ database or a session store.
|
|
|
183
188
|
**Why:** `resolve` is synchronous: it runs on every request, after the
|
|
184
189
|
sources, and returns a tag or `undefined`.
|
|
185
190
|
|
|
186
|
-
**Fix:** keep the preference where the
|
|
191
|
+
**Fix:** keep the preference where the middleware already reads it. When the
|
|
187
192
|
user saves it, write it to the cookie; every request after reads it from
|
|
188
193
|
there, before `Accept-Language`:
|
|
189
194
|
|
|
@@ -202,7 +207,7 @@ const app = alxia()
|
|
|
202
207
|
|
|
203
208
|
Do the same at sign-in, from the stored preference.
|
|
204
209
|
|
|
205
|
-
Or load it before the
|
|
210
|
+
Or load it before the middleware, in an async `derive` or the middleware that
|
|
206
211
|
signs the user in, and annotate `resolve` to read it — see
|
|
207
212
|
[Reading the app's context](guide.md#reading-the-apps-context):
|
|
208
213
|
|
|
@@ -224,18 +229,18 @@ const app = alxia()
|
|
|
224
229
|
error TS2339: Property 'user' does not exist on type 'BaseContext'.
|
|
225
230
|
```
|
|
226
231
|
|
|
227
|
-
**When:** `resolve` reads something a `derive` or a
|
|
232
|
+
**When:** `resolve` reads something a `derive` or a middleware before
|
|
228
233
|
`language()` added — a user, a session — and its parameter is not
|
|
229
234
|
annotated: `resolve: (ctx) => ctx.user.language`.
|
|
230
235
|
|
|
231
236
|
**Why:** an unannotated `resolve` is typed with the request's `BaseContext`
|
|
232
|
-
— `request`, `url`, `ip`, `pathParams`, `set` — not with what other
|
|
237
|
+
— `request`, `url`, `ip`, `pathParams`, `set` — not with what other middlewares
|
|
233
238
|
added. `language()` is built before it is used, so it cannot see the app it
|
|
234
239
|
will be used on.
|
|
235
240
|
|
|
236
241
|
**Fix:** annotate the parameter with what it reads. `language()` infers it
|
|
237
|
-
from the annotation, and the app that uses the
|
|
238
|
-
before
|
|
242
|
+
from the annotation, and the app that uses the middleware must then give it,
|
|
243
|
+
before it:
|
|
239
244
|
|
|
240
245
|
```ts
|
|
241
246
|
import type { BaseContext } from '@alxia/core';
|
|
@@ -246,55 +251,59 @@ const byUser = language({
|
|
|
246
251
|
resolve: ({ user }: BaseContext & { user: User }) => user.language ?? undefined,
|
|
247
252
|
});
|
|
248
253
|
|
|
249
|
-
alxia().
|
|
254
|
+
alxia().plugin(auth).use(byUser); // auth derives user
|
|
250
255
|
```
|
|
251
256
|
|
|
252
257
|
See [Reading the app's context](guide.md#reading-the-apps-context).
|
|
253
258
|
|
|
254
|
-
### `
|
|
259
|
+
### `Property 'user' is missing in type 'BaseContext & Empty' but required in type '{ user: User | null; }'`
|
|
255
260
|
|
|
256
261
|
```text
|
|
257
262
|
error TS2769: No overload matches this call.
|
|
258
263
|
…
|
|
259
|
-
|
|
260
|
-
|
|
264
|
+
Type 'BaseContext & Empty' is not assignable to type 'MiddlewareContext<{ user: User | null; }>'.
|
|
265
|
+
Property 'user' is missing in type 'BaseContext & Empty' but required in type '{ user: User | null; }'.
|
|
261
266
|
```
|
|
262
267
|
|
|
263
|
-
**When:** the
|
|
268
|
+
**When:** the middleware's `resolve` is annotated to read `user`, and it is
|
|
264
269
|
used on an app — or in a group — whose context has no `user` at that point:
|
|
265
|
-
`alxia().use(byUser)`, or `use(byUser)` before `
|
|
270
|
+
`alxia().use(byUser)`, or `use(byUser)` before `plugin(auth)`.
|
|
266
271
|
|
|
267
|
-
**Why:** an annotated `resolve` makes the
|
|
268
|
-
`use` checks the app's context against it, so `resolve` never runs without
|
|
272
|
+
**Why:** an annotated `resolve` makes the middleware require what it reads, and
|
|
273
|
+
`app.use` checks the app's context against it, so `resolve` never runs without
|
|
269
274
|
it.
|
|
270
275
|
|
|
271
|
-
**Fix:**
|
|
276
|
+
**Fix:** mount what adds `user` first, with the type `resolve`
|
|
272
277
|
reads:
|
|
273
278
|
|
|
274
279
|
```ts
|
|
275
|
-
alxia().
|
|
280
|
+
alxia().plugin(auth).use(byUser);
|
|
276
281
|
```
|
|
277
282
|
|
|
278
283
|
More on this message in
|
|
279
|
-
[`@alxia/core`'s troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/core/docs/troubleshooting.md#the-plugin-reads--which-this-apps-context-does-not-give-
|
|
284
|
+
[`@alxia/core`'s troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/core/docs/troubleshooting.md#the-plugin-reads--which-this-apps-context-does-not-give-add-the-plugin-or-middleware-that-gives-it-first).
|
|
280
285
|
|
|
281
|
-
### `
|
|
286
|
+
### `Types of property 'user' are incompatible`
|
|
282
287
|
|
|
283
288
|
```text
|
|
284
289
|
error TS2769: No overload matches this call.
|
|
285
290
|
…
|
|
286
|
-
Type '{ user: User; }' is not assignable to type '
|
|
291
|
+
Type 'BaseContext & Empty & { user: User | null; }' is not assignable to type 'MiddlewareContext<{ user: User; }>'.
|
|
292
|
+
Type 'BaseContext & Empty & { user: User | null; }' is not assignable to type '{ user: User; }'.
|
|
293
|
+
Types of property 'user' are incompatible.
|
|
294
|
+
Type 'User | null' is not assignable to type 'User'.
|
|
295
|
+
Type 'null' is not assignable to type 'User'.
|
|
287
296
|
```
|
|
288
297
|
|
|
289
298
|
**When:** the app gives a `user`, but of a type that does not fit the one
|
|
290
299
|
`resolve`'s parameter is annotated with: a `User | null` where `resolve`
|
|
291
300
|
reads `User`, or a user of another shape.
|
|
292
301
|
|
|
293
|
-
**Why:** `use` checks each key the
|
|
302
|
+
**Why:** `app.use` checks each key the middleware reads against the app's context;
|
|
294
303
|
a narrower type passes, a wider or different one does not.
|
|
295
304
|
|
|
296
305
|
**Fix:** annotate `resolve` with the type the app gives — `User | null`,
|
|
297
|
-
handled inside — or narrow it in the
|
|
306
|
+
handled inside — or narrow it in the middleware before:
|
|
298
307
|
|
|
299
308
|
```ts
|
|
300
309
|
language({
|
|
@@ -312,20 +321,20 @@ More on this message in
|
|
|
312
321
|
```text
|
|
313
322
|
error TS2769: No overload matches this call.
|
|
314
323
|
…
|
|
315
|
-
|
|
316
|
-
|
|
324
|
+
Type 'BaseContext & Empty' is not assignable to type 'MiddlewareContext<{ readonly '~any': "the plugin's resolve reads its context as any: annotate what it reads, or leave it unannotated"; }>'.
|
|
325
|
+
Property ''~any'' is missing in type 'BaseContext & Empty' but required in type '{ readonly '~any': "the plugin's resolve reads its context as any: annotate what it reads, or leave it unannotated"; }'.
|
|
317
326
|
```
|
|
318
327
|
|
|
319
328
|
**When:** `resolve`'s parameter is annotated `any` —
|
|
320
|
-
`resolve: (ctx: any) => ctx.user.language` — and the
|
|
329
|
+
`resolve: (ctx: any) => ctx.user.language` — and the middleware is used, on
|
|
321
330
|
any app, whatever its context gives.
|
|
322
331
|
|
|
323
332
|
**Why:** an `any` parameter reads any key and says nothing of what it
|
|
324
|
-
reads, so the
|
|
325
|
-
would be accepted, and throw on every request.
|
|
333
|
+
reads, so the middleware would require nothing, and an app without a `user`
|
|
334
|
+
would be accepted, and throw on every request. It is refused
|
|
326
335
|
instead.
|
|
327
336
|
|
|
328
|
-
**Fix:** annotate what `resolve` reads, and
|
|
337
|
+
**Fix:** annotate what `resolve` reads, and mount what adds it
|
|
329
338
|
first:
|
|
330
339
|
|
|
331
340
|
```ts
|
|
@@ -335,7 +344,7 @@ const byUser = language({
|
|
|
335
344
|
resolve: ({ user }: BaseContext & { user: User }) => user.language ?? undefined,
|
|
336
345
|
});
|
|
337
346
|
|
|
338
|
-
alxia().
|
|
347
|
+
alxia().plugin(auth).use(byUser);
|
|
339
348
|
```
|
|
340
349
|
|
|
341
350
|
Or leave it unannotated when it reads only the request:
|
|
@@ -373,7 +382,7 @@ await app.request('/', { headers: { 'accept-language': 'fr-FR,fr;q=0.9,en;q=0.8'
|
|
|
373
382
|
**When:** a language switcher links to `?lang=fr`; that page is in French,
|
|
374
383
|
the next one, without the query, is not.
|
|
375
384
|
|
|
376
|
-
**Why:** the query only decides the request that carries it. The
|
|
385
|
+
**Why:** the query only decides the request that carries it. The middleware
|
|
377
386
|
keeps it for the next ones only when all of these hold:
|
|
378
387
|
|
|
379
388
|
- `persist` is set — it is off by default;
|
|
@@ -391,7 +400,7 @@ language({
|
|
|
391
400
|
supported: ['en', 'fr'],
|
|
392
401
|
fallback: 'en',
|
|
393
402
|
order: ['query', 'cookie', 'header'],
|
|
394
|
-
persist: { secure:
|
|
403
|
+
persist: { secure: Bun.env.NODE_ENV !== 'development' }, // read at runtime: bun build inlines process.env
|
|
395
404
|
});
|
|
396
405
|
```
|
|
397
406
|
|
|
@@ -400,22 +409,28 @@ language({
|
|
|
400
409
|
**When:** behind a CDN, a reverse proxy, or `@alxia/cache`, the first
|
|
401
410
|
visitor's language is served to everyone after.
|
|
402
411
|
|
|
403
|
-
**Why:** the response depends on what the
|
|
404
|
-
on the URL alone ignores it.
|
|
412
|
+
**Why:** the response depends on what the middleware read, and a cache keyed
|
|
413
|
+
on the URL alone ignores it. It says `Vary: Accept-Language,
|
|
405
414
|
Cookie` with the default `order`, and a reply's own `Vary` adds to it. Two
|
|
406
415
|
things still leave a header out:
|
|
407
416
|
|
|
408
417
|
- what `resolve` reads, unless the `vary` option names it;
|
|
409
|
-
-
|
|
410
|
-
`@alxia/core`'s `vary`, which replaces every
|
|
418
|
+
- a middleware that sets `Vary` with
|
|
419
|
+
`headers.set` instead of `@alxia/core`'s `vary`, which replaces every
|
|
420
|
+
name before it.
|
|
411
421
|
|
|
412
422
|
**Fix:** name what `resolve` reads, add to `Vary` rather than setting it,
|
|
413
423
|
and give a cache the same headers:
|
|
414
424
|
|
|
415
425
|
```ts
|
|
416
|
-
import { alxia, vary, withHeaders } from '@alxia/core';
|
|
426
|
+
import { alxia, defineMiddleware, settle, vary, withHeaders } from '@alxia/core';
|
|
417
427
|
|
|
418
428
|
alxia()
|
|
429
|
+
.use(
|
|
430
|
+
defineMiddleware(async (ctx, next) =>
|
|
431
|
+
withHeaders(await settle(ctx, next()), (headers) => vary(headers, 'Accept-Encoding')),
|
|
432
|
+
),
|
|
433
|
+
)
|
|
419
434
|
.use(
|
|
420
435
|
language({
|
|
421
436
|
supported: ['en', 'fr'],
|
|
@@ -423,8 +438,7 @@ alxia()
|
|
|
423
438
|
resolve: (ctx) => ctx.request.headers.get('x-preferred-language') ?? undefined,
|
|
424
439
|
vary: ['X-Preferred-Language'],
|
|
425
440
|
}),
|
|
426
|
-
)
|
|
427
|
-
.onResponse((response) => withHeaders(response, (headers) => vary(headers, 'Accept-Encoding')));
|
|
441
|
+
);
|
|
428
442
|
// Vary: Accept-Language, Cookie, X-Preferred-Language, Accept-Encoding
|
|
429
443
|
```
|
|
430
444
|
|
|
@@ -437,9 +451,9 @@ cache({ ttl: 60, vary: ['accept-language', 'cookie', 'x-preferred-language'] });
|
|
|
437
451
|
**When:** `order` lists `'path'`, and the routes are declared without the
|
|
438
452
|
language segment: `.get('/products', …)`.
|
|
439
453
|
|
|
440
|
-
**Why:** the
|
|
454
|
+
**Why:** the middleware reads the segment at `pathIndex`; it does not remove
|
|
441
455
|
it from the path or route on it. `/fr/products` matches no route, so the
|
|
442
|
-
`404` is answered
|
|
456
|
+
`404` is answered: the language was read, but no route answers in it.
|
|
443
457
|
|
|
444
458
|
**Fix:** declare the segment in the routes:
|
|
445
459
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alxia/language",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The request's language for alxia, typed as the languages you support: from the query, a cookie, the path or Accept-Language, weights and regions included",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@alxia/core": "^0.
|
|
44
|
+
"@alxia/core": "^0.5.0",
|
|
45
45
|
"@types/bun": "^1.4.2"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@alxia/core": "^0.
|
|
48
|
+
"@alxia/core": "^0.5.0",
|
|
49
49
|
"typescript": "^6.0.3 || ^7.0.0"
|
|
50
50
|
}
|
|
51
51
|
}
|