@alxia/language 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 +96 -0
- package/dist/decide.d.ts +23 -0
- package/dist/decide.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +124 -0
- package/dist/index.js.map +12 -0
- package/dist/language.d.ts +57 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/negotiate.d.ts +16 -0
- package/dist/negotiate.d.ts.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -0
- package/docs/README.md +13 -0
- package/docs/guide.md +553 -0
- package/docs/roadmap.md +51 -0
- package/docs/troubleshooting.md +482 -0
- package/package.json +51 -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,96 @@
|
|
|
1
|
+
# @alxia/language
|
|
2
|
+
|
|
3
|
+
The request's language for [alxia](https://www.npmjs.com/package/@alxia/core),
|
|
4
|
+
typed as the languages you support — never a string a client made up. No
|
|
5
|
+
dependency.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @alxia/language @alxia/core
|
|
9
|
+
bun add -d typescript
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { alxia } from '@alxia/core';
|
|
16
|
+
import { language } from '@alxia/language';
|
|
17
|
+
|
|
18
|
+
const supported = ['en', 'fr', 'pt-BR'] as const;
|
|
19
|
+
const greetings: Record<(typeof supported)[number], string> = { en: 'Hello', fr: 'Bonjour', 'pt-BR': 'Olá' };
|
|
20
|
+
|
|
21
|
+
const app = alxia()
|
|
22
|
+
.use(language({ supported, fallback: 'en' }))
|
|
23
|
+
.get('/', ({ language, reply }) => reply(200, greetings[language])); // 'en' | 'fr' | 'pt-BR'
|
|
24
|
+
|
|
25
|
+
app.listen(3000);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It reads, in `order`:
|
|
29
|
+
|
|
30
|
+
| source | |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `query` | `?lang=fr` |
|
|
33
|
+
| `cookie` | `language=fr` |
|
|
34
|
+
| `path` | `/fr/products`, at `pathIndex` |
|
|
35
|
+
| `header` | `Accept-Language`, by weight: `de, fr-CA;q=0.8` is `fr` |
|
|
36
|
+
|
|
37
|
+
then `resolve(ctx)` — a user's saved preference — then `fallback`. A tag
|
|
38
|
+
matches whatever its case, by its base language (`fr-CA` for `fr`), or by a
|
|
39
|
+
region of it (`pt` for `pt-BR`). `languageSource` says which source decided.
|
|
40
|
+
|
|
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.
|
|
43
|
+
|
|
44
|
+
## Reading the app's context
|
|
45
|
+
|
|
46
|
+
Annotate `resolve`'s parameter to decide by what an earlier plugin added —
|
|
47
|
+
the language a signed-in user saved. The plugin then requires it: an app
|
|
48
|
+
that does not give `user` before it cannot use it.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { alxia, type BaseContext } from '@alxia/core';
|
|
52
|
+
import { language } from '@alxia/language';
|
|
53
|
+
|
|
54
|
+
const byUser = language({
|
|
55
|
+
supported: ['en', 'fr'],
|
|
56
|
+
fallback: 'en',
|
|
57
|
+
order: ['query', 'cookie'],
|
|
58
|
+
resolve: ({ user }: BaseContext & { user: { language: string } | null }) => user?.language,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
alxia().use(auth).use(byUser); // auth derives user
|
|
62
|
+
alxia().use(byUser); // a compile error: this app gives no `user`
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A `resolve` annotated `any` would require nothing, so the plugin is refused
|
|
66
|
+
on every app: annotate what it reads, or leave it unannotated.
|
|
67
|
+
|
|
68
|
+
## Options
|
|
69
|
+
|
|
70
|
+
| option | default | |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `supported` | required | the languages: `language`'s type |
|
|
73
|
+
| `fallback` | required | one of them; the types refuse another |
|
|
74
|
+
| `order` | `['query', 'cookie', 'header']` | |
|
|
75
|
+
| `query`, `cookie` | `lang`, `language` | their names |
|
|
76
|
+
| `pathIndex` | 0 | |
|
|
77
|
+
| `persist` | `false` | `true`, or `{ maxAge, secure }` |
|
|
78
|
+
| `contentLanguage` | `true` | |
|
|
79
|
+
| `resolve` | none | `(ctx) => string \| undefined`; annotate `ctx` to read what an earlier plugin adds |
|
|
80
|
+
| `vary` | none | the headers `resolve` reads, added to `Vary` |
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
| export | |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `language(options)` | the plugin: `language`, `languageSource` |
|
|
87
|
+
| `LanguageOptions` | its options: `supported`, `fallback`, `order`, `query`, `cookie`, `pathIndex`, `persist`, `contentLanguage`, `resolve`, `vary` |
|
|
88
|
+
| `negotiate(header, supported)` | the supported language `Accept-Language` prefers |
|
|
89
|
+
| `parseAcceptLanguage(header)`, `match(tag, supported)` | its parts |
|
|
90
|
+
| `LanguageContext`, `LanguageSource`, `Accepted` | its types |
|
|
91
|
+
|
|
92
|
+
## Documentation
|
|
93
|
+
|
|
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 plugin added, the typed context, and the headers the plugin adds.
|
|
95
|
+
- [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
|
+
- [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/language/docs/roadmap.md): what is coming, and what is not planned.
|
package/dist/decide.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type BaseContext } from '@alxia/core';
|
|
2
|
+
import type { LanguageContext, LanguageSource } from './types';
|
|
3
|
+
/** `language()`'s options, their defaults applied. */
|
|
4
|
+
export interface Settings<L extends string> {
|
|
5
|
+
readonly supported: readonly L[];
|
|
6
|
+
readonly fallback: L;
|
|
7
|
+
readonly order: readonly LanguageSource[];
|
|
8
|
+
readonly query: string;
|
|
9
|
+
readonly cookie: string;
|
|
10
|
+
readonly pathIndex: number;
|
|
11
|
+
readonly persist: {
|
|
12
|
+
readonly maxAge?: number;
|
|
13
|
+
readonly secure?: boolean;
|
|
14
|
+
} | undefined;
|
|
15
|
+
readonly contentLanguage: boolean;
|
|
16
|
+
readonly resolve: ((ctx: BaseContext) => string | undefined) | undefined;
|
|
17
|
+
readonly vary: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
/** The request's language: each source in `order`, then `resolve`, then `fallback`. */
|
|
20
|
+
export declare function decide<L extends string>(settings: Settings<L>, ctx: BaseContext): LanguageContext<L>;
|
|
21
|
+
/** What the response says of it: `Vary`, `Content-Language`, the kept cookie. */
|
|
22
|
+
export declare function respond<L extends string>(settings: Settings<L>, ctx: BaseContext, found: LanguageContext<L>): void;
|
|
23
|
+
//# sourceMappingURL=decide.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../src/decide.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAQ,MAAM,aAAa,CAAC;AAErD,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE/D,sDAAsD;AACtD,MAAM,WAAW,QAAQ,CAAC,CAAC,SAAS,MAAM;IACzC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EACb;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GACvD,SAAS,CAAC;IACb,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,WAAW,KAAK,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IACzE,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AA+BD,uFAAuF;AACvF,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,EACtC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,EACrB,GAAG,EAAE,WAAW,GACd,eAAe,CAAC,CAAC,CAAC,CAWpB;AAED,iFAAiF;AACjF,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EACvC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,EACrB,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,GACvB,IAAI,CAgBN"}
|
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,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC5D,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
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// src/language.ts
|
|
2
|
+
import { definePlugin } from "@alxia/core";
|
|
3
|
+
|
|
4
|
+
// src/decide.ts
|
|
5
|
+
import { vary } from "@alxia/core";
|
|
6
|
+
|
|
7
|
+
// src/negotiate.ts
|
|
8
|
+
function parseAcceptLanguage(header) {
|
|
9
|
+
if (!header)
|
|
10
|
+
return [];
|
|
11
|
+
return header.split(",").map((part, index) => {
|
|
12
|
+
const [tag = "", ...params] = part.trim().split(";");
|
|
13
|
+
const q = params.map((param) => param.trim()).find((param) => param.startsWith("q="));
|
|
14
|
+
const weight = q === undefined ? 1 : Number(q.slice(2));
|
|
15
|
+
return {
|
|
16
|
+
tag: tag.trim(),
|
|
17
|
+
q: Number.isFinite(weight) ? weight : 0,
|
|
18
|
+
index
|
|
19
|
+
};
|
|
20
|
+
}).filter((entry) => entry.tag !== "" && entry.q > 0).sort((a, b) => b.q - a.q || a.index - b.index).map(({ tag, q }) => ({ tag, q }));
|
|
21
|
+
}
|
|
22
|
+
function match(tag, supported) {
|
|
23
|
+
const wanted = tag.toLowerCase();
|
|
24
|
+
if (wanted === "*")
|
|
25
|
+
return supported[0];
|
|
26
|
+
const exact = supported.find((language) => language.toLowerCase() === wanted);
|
|
27
|
+
if (exact !== undefined)
|
|
28
|
+
return exact;
|
|
29
|
+
const base = wanted.split("-")[0] ?? wanted;
|
|
30
|
+
return supported.find((language) => language.toLowerCase() === base) ?? supported.find((language) => language.toLowerCase().split("-")[0] === base);
|
|
31
|
+
}
|
|
32
|
+
function negotiate(header, supported) {
|
|
33
|
+
for (const { tag } of parseAcceptLanguage(header)) {
|
|
34
|
+
const found = match(tag, supported);
|
|
35
|
+
if (found !== undefined)
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/decide.ts
|
|
42
|
+
function read(settings, ctx, source) {
|
|
43
|
+
const { supported } = settings;
|
|
44
|
+
switch (source) {
|
|
45
|
+
case "query": {
|
|
46
|
+
const value = ctx.url.searchParams.get(settings.query);
|
|
47
|
+
return value === null ? undefined : match(value, supported);
|
|
48
|
+
}
|
|
49
|
+
case "cookie": {
|
|
50
|
+
const value = new Bun.CookieMap(ctx.request.headers.get("cookie") ?? "").get(settings.cookie);
|
|
51
|
+
return value === null ? undefined : match(value, supported);
|
|
52
|
+
}
|
|
53
|
+
case "path": {
|
|
54
|
+
const segment = ctx.url.pathname.split("/").filter(Boolean)[settings.pathIndex];
|
|
55
|
+
return segment === undefined ? undefined : match(segment, supported);
|
|
56
|
+
}
|
|
57
|
+
case "header":
|
|
58
|
+
return negotiate(ctx.request.headers.get("accept-language"), supported);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function decide(settings, ctx) {
|
|
62
|
+
for (const source of settings.order) {
|
|
63
|
+
const value = read(settings, ctx, source);
|
|
64
|
+
if (value !== undefined)
|
|
65
|
+
return { language: value, languageSource: source };
|
|
66
|
+
}
|
|
67
|
+
const resolved = settings.resolve?.(ctx);
|
|
68
|
+
const value = resolved === undefined ? undefined : match(resolved, settings.supported);
|
|
69
|
+
return value === undefined ? { language: settings.fallback, languageSource: "fallback" } : { language: value, languageSource: "resolve" };
|
|
70
|
+
}
|
|
71
|
+
function respond(settings, ctx, found) {
|
|
72
|
+
const { headers, cookies } = ctx.set;
|
|
73
|
+
if (settings.order.includes("header"))
|
|
74
|
+
vary(headers, "Accept-Language");
|
|
75
|
+
if (settings.order.includes("cookie"))
|
|
76
|
+
vary(headers, "Cookie");
|
|
77
|
+
for (const name of settings.vary)
|
|
78
|
+
vary(headers, name);
|
|
79
|
+
if (settings.contentLanguage)
|
|
80
|
+
headers.set("content-language", found.language);
|
|
81
|
+
const { persist } = settings;
|
|
82
|
+
if (persist !== undefined && found.languageSource === "query") {
|
|
83
|
+
cookies.set(settings.cookie, found.language, {
|
|
84
|
+
path: "/",
|
|
85
|
+
sameSite: "lax",
|
|
86
|
+
httpOnly: false,
|
|
87
|
+
secure: persist.secure ?? true,
|
|
88
|
+
maxAge: persist.maxAge ?? 365 * 24 * 60 * 60
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/language.ts
|
|
94
|
+
function language(options) {
|
|
95
|
+
const settings = {
|
|
96
|
+
supported: options.supported,
|
|
97
|
+
fallback: options.fallback,
|
|
98
|
+
order: options.order ?? ["query", "cookie", "header"],
|
|
99
|
+
query: options.query ?? "lang",
|
|
100
|
+
cookie: options.cookie ?? "language",
|
|
101
|
+
pathIndex: options.pathIndex ?? 0,
|
|
102
|
+
persist: options.persist === true ? {} : options.persist === false ? undefined : options.persist,
|
|
103
|
+
contentLanguage: options.contentLanguage !== false,
|
|
104
|
+
resolve: options.resolve,
|
|
105
|
+
vary: options.vary ?? []
|
|
106
|
+
};
|
|
107
|
+
if (!settings.supported.includes(settings.fallback)) {
|
|
108
|
+
throw new TypeError(`language(): the fallback "${settings.fallback}" is not supported`);
|
|
109
|
+
}
|
|
110
|
+
return definePlugin()((app) => app.derive((ctx) => {
|
|
111
|
+
const found = decide(settings, ctx);
|
|
112
|
+
respond(settings, ctx, found);
|
|
113
|
+
return found;
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
export {
|
|
117
|
+
language,
|
|
118
|
+
match,
|
|
119
|
+
negotiate,
|
|
120
|
+
parseAcceptLanguage
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
//# debugId=0E86121386DC727A64756E2164756E21
|
|
124
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/language.ts", "../src/decide.ts", "../src/negotiate.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { type BaseContext, definePlugin, type RequiresOf } 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 * The request's language, as a plugin: 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>(options: LanguageOptions<L, Ctx>) {\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// `use` 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 definePlugin<RequiresOf<Ctx, 'resolve'>>()((app) =>\n\t\tapp.derive((ctx): LanguageContext<L> => {\n\t\t\tconst found = decide(settings, ctx);\n\t\t\trespond(settings, ctx, found);\n\t\t\treturn found;\n\t\t}),\n\t);\n}\n",
|
|
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
|
+
"/** 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
|
+
],
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "0E86121386DC727A64756E2164756E21",
|
|
11
|
+
"names": []
|
|
12
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type BaseContext, type RequiresOf } from '@alxia/core';
|
|
2
|
+
import type { LanguageContext, LanguageSource } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* `Ctx` is the type `resolve`'s parameter is annotated with —
|
|
5
|
+
* `BaseContext & { user: User }`, or `{ user: User }` alone — and
|
|
6
|
+
* `BaseContext` when it is not.
|
|
7
|
+
*/
|
|
8
|
+
export interface LanguageOptions<L extends string, Ctx extends object = BaseContext> {
|
|
9
|
+
/** The languages the app speaks: the context's `language` is one of them. */
|
|
10
|
+
readonly supported: readonly L[];
|
|
11
|
+
/** The one it speaks when the request names none it does. */
|
|
12
|
+
readonly fallback: NoInfer<L>;
|
|
13
|
+
/** Where it looks, in order. `query`, `cookie`, then `header` by default. */
|
|
14
|
+
readonly order?: readonly LanguageSource[];
|
|
15
|
+
/** The query parameter: `?lang=fr`. `lang` by default. */
|
|
16
|
+
readonly query?: string;
|
|
17
|
+
/** The cookie. `language` by default. */
|
|
18
|
+
readonly cookie?: string;
|
|
19
|
+
/** The index of the path segment: `/fr/products` is 0. 0 by default. */
|
|
20
|
+
readonly pathIndex?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a language named by the query is kept in the cookie, so the
|
|
23
|
+
* next request speaks it too. Off by default.
|
|
24
|
+
*/
|
|
25
|
+
readonly persist?: boolean | {
|
|
26
|
+
readonly maxAge?: number;
|
|
27
|
+
readonly secure?: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** Says `Content-Language` on every response. On by default. */
|
|
30
|
+
readonly contentLanguage?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Decides itself, after every source: a user's saved preference. Annotate
|
|
33
|
+
* its parameter to read what an earlier plugin adds —
|
|
34
|
+
* `(ctx: BaseContext & { user: User })` — and the app that uses the
|
|
35
|
+
* plugin must then give it.
|
|
36
|
+
*/
|
|
37
|
+
readonly resolve?: (ctx: BaseContext & Ctx) => string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The request headers `resolve` reads, added to `Vary` so a cache keeps
|
|
40
|
+
* one response per value: `['authorization']`. None by default.
|
|
41
|
+
*/
|
|
42
|
+
readonly vary?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The request's language, as a plugin: the routes declared after it read
|
|
46
|
+
* `language`, typed as one of `supported` — never a string a client made
|
|
47
|
+
* up. It is read from the query, a cookie, a path segment and
|
|
48
|
+
* `Accept-Language` — weights, `fr-CA` for `fr`, `fr` for `fr-FR` — in the
|
|
49
|
+
* order given, then `fallback`.
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* app.use(language({ supported: ['en', 'fr'], fallback: 'en' }))
|
|
53
|
+
* .get('/', ({ language, reply }) => reply(200, language)); // 'en' | 'fr'
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function language<const L extends string, Ctx extends object = BaseContext>(options: LanguageOptions<L, Ctx>): import("@alxia/core").Alxia<RequiresOf<Ctx, "resolve"> & LanguageContext<L>, import("@alxia/core").Empty, "", never> & import("@alxia/core").Requiring<RequiresOf<Ctx, "resolve">>;
|
|
57
|
+
//# sourceMappingURL=language.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language.d.ts","sourceRoot":"","sources":["../src/language.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAgB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9E,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;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CACvB,KAAK,CAAC,CAAC,SAAS,MAAM,EACtB,GAAG,SAAS,MAAM,GAAG,WAAW,EAC/B,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,GAAG,CAAC,sLA+BjC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** One language a client accepts, with its weight. */
|
|
2
|
+
export interface Accepted {
|
|
3
|
+
readonly tag: string;
|
|
4
|
+
readonly q: number;
|
|
5
|
+
}
|
|
6
|
+
/** `Accept-Language` as its languages, the most wanted first; a weight of 0 refused. */
|
|
7
|
+
export declare function parseAcceptLanguage(header: string | null | undefined): Accepted[];
|
|
8
|
+
/**
|
|
9
|
+
* The supported language a tag names: itself, whatever its case; its base
|
|
10
|
+
* language — `fr-CA` to `fr`; or a region of it — `fr` to `fr-FR`.
|
|
11
|
+
* `undefined` when none.
|
|
12
|
+
*/
|
|
13
|
+
export declare function match<const L extends string>(tag: string, supported: readonly L[]): L | undefined;
|
|
14
|
+
/** The supported language `Accept-Language` prefers, or `undefined`. */
|
|
15
|
+
export declare function negotiate<const L extends string>(header: string | null | undefined, supported: readonly L[]): L | undefined;
|
|
16
|
+
//# sourceMappingURL=negotiate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"negotiate.d.ts","sourceRoot":"","sources":["../src/negotiate.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wFAAwF;AACxF,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,QAAQ,EAAE,CAmBZ;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAC3C,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,SAAS,CAAC,EAAE,GACrB,CAAC,GAAG,SAAS,CAUf;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAC/C,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,SAAS,EAAE,SAAS,CAAC,EAAE,GACrB,CAAC,GAAG,SAAS,CAMf"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Where a language is read from. */
|
|
2
|
+
export type LanguageSource = 'query' | 'cookie' | 'path' | 'header';
|
|
3
|
+
/** What the routes behind the plugin read. */
|
|
4
|
+
export interface LanguageContext<L extends string> {
|
|
5
|
+
readonly language: L;
|
|
6
|
+
/** Where it came from: `fallback` when nothing named one. */
|
|
7
|
+
readonly languageSource: LanguageSource | 'resolve' | 'fallback';
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,qCAAqC;AACrC,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEpE,8CAA8C;AAC9C,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,MAAM;IAChD,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,cAAc,EAAE,cAAc,GAAG,SAAS,GAAG,UAAU,CAAC;CACjE"}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @alxia/language documentation
|
|
2
|
+
|
|
3
|
+
The [package README](../README.md) is the short version. This folder is
|
|
4
|
+
the long one: every option with its default and an example, how a
|
|
5
|
+
request's language is found and how `Accept-Language` is negotiated, what
|
|
6
|
+
the routes behind the plugin read, and what to do when a response is not in
|
|
7
|
+
the language you expected.
|
|
8
|
+
|
|
9
|
+
| Page | Read it when |
|
|
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 plugin added, typing what the routes read, caching the responses, or testing them |
|
|
12
|
+
| [Troubleshooting](troubleshooting.md) | the plugin threw at start-up, `tsc` refused an option or a route, or a response is in the wrong language |
|
|
13
|
+
| [Roadmap](roadmap.md) | wondering what is coming, and what is not planned |
|