@alxia/language 0.1.2 → 0.2.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 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 plugin added —
47
- the language a signed-in user saved. The plugin then requires it: an app
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().use(auth).use(byUser); // auth derives user
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 plugin is refused
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 plugin adds |
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 plugin: `language`, `languageSource` |
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 plugin added, the typed context, and the headers the plugin adds.
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
@@ -1 +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"}
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,7 @@
1
1
  // src/language.ts
2
- import { definePlugin } from "@alxia/core";
2
+ import {
3
+ defineMiddleware
4
+ } from "@alxia/core";
3
5
 
4
6
  // src/decide.ts
5
7
  import { vary } from "@alxia/core";
@@ -107,11 +109,11 @@ function language(options) {
107
109
  if (!settings.supported.includes(settings.fallback)) {
108
110
  throw new TypeError(`language(): the fallback "${settings.fallback}" is not supported`);
109
111
  }
110
- return definePlugin()((app) => app.derive((ctx) => {
112
+ return defineMiddleware()((ctx, next) => {
111
113
  const found = decide(settings, ctx);
112
114
  respond(settings, ctx, found);
113
- return found;
114
- }));
115
+ return next(found);
116
+ });
115
117
  }
116
118
  export {
117
119
  language,
@@ -120,5 +122,5 @@ export {
120
122
  parseAcceptLanguage
121
123
  };
122
124
 
123
- //# debugId=0E86121386DC727A64756E2164756E21
125
+ //# debugId=CC2D7C8A401D3B8464756E2164756E21
124
126
  //# 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 { 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",
5
+ "import {\n\ttype BaseContext,\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\ttype MiddlewareMark,\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>>>> & MiddlewareMark;\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'>>()((ctx, next) => {\n\t\tconst found: LanguageContext<L> = decide(settings, ctx);\n\t\trespond(settings, ctx, found);\n\t\treturn next(found);\n\t});\n}\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": "0E86121386DC727A64756E2164756E21",
9
+ "mappings": "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",
10
+ "debugId": "CC2D7C8A401D3B8464756E2164756E21",
11
11
  "names": []
12
12
  }
@@ -1,4 +1,4 @@
1
- import { type BaseContext, type RequiresOf } from '@alxia/core';
1
+ import { type BaseContext, type Empty, type Middleware, type MiddlewareMark, 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
- * The request's language, as a plugin: the routes declared after it read
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>>>> & MiddlewareMark;
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>): import("@alxia/core").Alxia<RequiresOf<Ctx, "resolve"> & LanguageContext<L>, import("@alxia/core").Empty, "", never> & import("@alxia/core").Requiring<RequiresOf<Ctx, "resolve">>;
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
@@ -1 +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"}
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,EACf,KAAK,cAAc,EACnB,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,GAAG,cAAc,CAAC;AAE7E;;;;;;;;;;;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,CA6BnD"}
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 plugin read, and what to do when a response is not in
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 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 |
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,7 +28,7 @@ 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
- ): Alxia<RequiresOf<Ctx> & LanguageContext<L>, Empty, '', never> &
31
+ ): Alxia<RequiresOf<Ctx> & LanguageContext<L>, '', never> &
32
32
  Requiring<RequiresOf<Ctx>>;
33
33
 
34
34
  interface LanguageOptions<L extends string, Ctx extends object = BaseContext> {
@@ -54,11 +54,13 @@ type `resolve`'s parameter is annotated with. `RequiresOf<Ctx>`,
54
54
  see
55
55
  [Reading the app's context](#reading-the-apps-context).
56
56
 
57
- `language()` returns an app plugin: pass it to `use`, called. It is a
58
- `derive`, so it applies to the routes declared **after** it — in the same
59
- app, or inside the [group](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/groups-and-plugins.md)
60
- it is used in — and adds `language` and `languageSource` to their context.
61
- A route declared before it neither runs it nor reads them.
57
+ `language()` returns a middleware: pass it to `app.use`, called. A `use()` on
58
+ the app runs on every request, in declaration order, a request no route
59
+ matches included, and adds `language` and `languageSource` to what is
60
+ declared **after** it: the middlewares and the routes. Inside a
61
+ [group](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/groups-and-plugins.md)
62
+ it applies to the group's routes only. A route declared before it neither
63
+ runs it nor reads them.
62
64
 
63
65
  It throws once, when it is created, if `fallback` is not one of
64
66
  `supported`:
@@ -82,7 +84,7 @@ for when they cannot.
82
84
  | `cookie` | `string` | `'language'` | the cookie read, and written by `persist` |
83
85
  | `pathIndex` | `number` | `0` | the path segment read by the `path` source: `/fr/products` is 0 |
84
86
  | `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 plugin runs for |
87
+ | `contentLanguage` | `boolean` | `true` | `Content-Language` on every response the middleware runs for |
86
88
  | `resolve` | `(ctx: BaseContext & Ctx) => string \| undefined` | none | decides after every source, before `fallback`; its annotated parameter types what it reads |
87
89
  | `vary` | `readonly string[]` | none | the request headers `resolve` reads, added to `Vary` |
88
90
 
@@ -105,7 +107,7 @@ language({ supported, fallback: 'en' });
105
107
  ```
106
108
 
107
109
  `fallback` must be one of them; the types refuse another (`fallback: 'de'`
108
- is a compile error), and so does the plugin, at start-up.
110
+ is a compile error), and so does `language()`, at start-up.
109
111
 
110
112
  ### `order`
111
113
 
@@ -141,7 +143,7 @@ language({ supported: ['en', 'fr'], fallback: 'en', order: ['path'], pathIndex:
141
143
  // /shop/fr/products → 'fr'
142
144
  ```
143
145
 
144
- The plugin reads the segment; it does not route on it or remove it. The
146
+ The middleware reads the segment; it does not route on it or remove it. The
145
147
  routes still declare it, as a parameter:
146
148
 
147
149
  ```ts
@@ -150,7 +152,7 @@ const app = alxia()
150
152
  .get('/:lang/products', ({ language: current, reply }) => reply(200, current));
151
153
 
152
154
  await app.request('/fr/products'); // 'fr'
153
- await app.request('/products'); // 404: no route matches; the plugin never runs
155
+ await app.request('/products'); // 404: no route matches (it still says Content-Language)
154
156
  ```
155
157
 
156
158
  A segment is matched like any other tag, so `/fr-ca/products` is `fr` too.
@@ -184,8 +186,8 @@ the cookie or the header sets nothing. For the cookie to be read back,
184
186
 
185
187
  ### `contentLanguage`
186
188
 
187
- On by default: every response of a route behind the plugin says
188
- `Content-Language: <language>`. A route that sets its own wins over it:
189
+ On by default: every response of a request the middleware runs on says
190
+ `Content-Language: <language>`, a 404 included when it is used on the app. A route that sets its own wins over it:
189
191
 
190
192
  ```ts
191
193
  .get('/legal', ({ reply }) => reply(200, legalText, { headers: { 'content-language': 'fr' } }));
@@ -214,27 +216,27 @@ language({
214
216
  ```
215
217
 
216
218
  `vary` names the request headers `resolve` reads, so a shared cache keeps
217
- one response per value; the plugin cannot see what a function reads.
219
+ one response per value; the middleware cannot see what a function reads.
218
220
 
219
221
  It receives the request's `BaseContext` — `request`, `url`, `ip`,
220
222
  `pathParams`, `set` — and, when its parameter is annotated, what an earlier
221
- plugin added: see [Reading the app's context](#reading-the-apps-context). It
223
+ middleware added: see [Reading the app's context](#reading-the-apps-context). It
222
224
  returns a tag, or `undefined` for none. The tag is matched against
223
225
  `supported` like any other: a tag it does not support is ignored, and
224
226
  `fallback` decides. It is synchronous: a preference kept in a database is
225
227
  either written to the cookie when the user saves it — see
226
228
  [the realistic setup](#a-realistic-setup) — or loaded by an async `derive`
227
- or plugin before `language()`, which `resolve` then reads — see
229
+ or middleware before `language()`, which `resolve` then reads — see
228
230
  [Reading the app's context](#reading-the-apps-context).
229
231
 
230
232
  ### Reading the app's context
231
233
 
232
- To decide by what an earlier plugin added, such as a signed-in `user` and
234
+ To decide by what an earlier middleware added, such as a signed-in `user` and
233
235
  the language they saved, annotate `resolve`'s parameter. `language()` infers
234
236
  what it reads from that annotation — `supported` still types `language` —
235
- and the plugin is a
237
+ and the middleware requires it, as a
236
238
  [`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.
239
+ plugin does: an app that does not give `user` before it cannot use it.
238
240
 
239
241
  ```ts
240
242
  import { alxia, type BaseContext } from '@alxia/core';
@@ -254,26 +256,26 @@ const byUser = language({
254
256
  });
255
257
 
256
258
  const app = alxia()
257
- .use(auth) // derives user: User | null
259
+ .plugin(auth) // an app: derives user: User | null
258
260
  .use(byUser)
259
261
  .get('/', ({ language: current, reply }) => reply(200, current)); // 'en' | 'fr'
260
262
 
261
263
  alxia().use(byUser);
262
- // error: the plugin reads "user", which this app's context does not give: use the plugin that adds it first
264
+ // error: Property 'user' is missing in type 'BaseContext & Empty' but required in type '{ user: User | null; }'
263
265
  ```
264
266
 
265
267
  The annotation may be `BaseContext & { user: User }` or `{ user: User }`
266
- alone; either way the plugin requires `{ user: User }`. An app whose `user`
268
+ alone; either way the middleware requires `{ user: User }`. An app whose `user`
267
269
  has a type that does not fit it is refused too —
268
- `the plugin reads "user", which this app's context gives with another type` —
269
- while a narrower one passes: an app deriving `user: User` may use a plugin
270
+ `Types of property 'user' are incompatible` —
271
+ while a narrower one passes: an app deriving `user: User` may use a middleware
270
272
  that reads `User | null`. Annotating a key `BaseContext` already has with
271
273
  a type it does not give — `({ url }: { url: string })` — is refused the
272
274
  same way.
273
- A `resolve` left unannotated reads `BaseContext` only, and the plugin
275
+ A `resolve` left unannotated reads `BaseContext` only, and the middleware
274
276
  requires nothing.
275
277
  Annotated `unknown` or `object`, it requires nothing either. Annotated
276
- `any`, it would read anything and require nothing, so the plugin is
278
+ `any`, it would read anything and require nothing, so the middleware is
277
279
  refused on every app instead:
278
280
  [`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
281
 
@@ -387,11 +389,11 @@ function negotiate<const L extends string>(header: string | null | undefined, su
387
389
 
388
390
  The three are exported for code outside a request — a background job
389
391
  choosing an email's language from a saved header, a WebSocket upgrade —
390
- and return the same answers the plugin would.
392
+ and return the same answers the middleware would.
391
393
 
392
394
  ## The typed context
393
395
 
394
- The routes behind the plugin read:
396
+ What the routes and middlewares after it read:
395
397
 
396
398
  ```ts
397
399
  interface LanguageContext<L extends string> {
@@ -448,7 +450,7 @@ so a shared cache keeps one copy per language rather than serving the first
448
450
  one to everyone. The query and the path are part of the URL, and need no
449
451
  `Vary`. What `resolve` reads is added only when the `vary` option names it.
450
452
 
451
- The plugin adds to `ctx.set.headers`, and a reply's own `Vary` adds to it
453
+ The middleware adds to `ctx.set.headers`, and a reply's own `Vary` adds to it
452
454
  rather than replacing it:
453
455
 
454
456
  ```ts
@@ -462,8 +464,11 @@ the cache the same headers, so it keys by them:
462
464
  `cache({ ttl: 60, vary: ['accept-language', 'cookie'] })`. A response that
463
465
  sets a cookie — a `persist` from the query — is not cached.
464
466
 
465
- A request that matches no route — a `404`, a `405` — never reaches the
466
- plugin, so it carries none of these headers.
467
+ A request that matches no route — a `404`, a `405` — runs the app's
468
+ `use()` middlewares, so with `language()` on the app it carries these
469
+ headers too. Inside a `group`, the middleware runs for the group's routes
470
+ only: an unmatched request, even one under the group's prefix, carries
471
+ none of them.
467
472
 
468
473
  ## A realistic setup
469
474
 
@@ -499,11 +504,9 @@ export const app = alxia()
499
504
  set.cookies.set('language', chosen, { path: '/', sameSite: 'lax', maxAge: 365 * 24 * 60 * 60 });
500
505
  return reply(200, { language: chosen });
501
506
  });
502
-
503
- export type App = typeof app;
504
507
  ```
505
508
 
506
- The saved choice is the `language` cookie, which the plugin reads on every
509
+ The saved choice is the `language` cookie, which the middleware reads on every
507
510
  request after, before the browser's languages. `secure` is off outside
508
511
  production, so the cookie survives a plain-`http` development server.
509
512
 
@@ -548,6 +551,6 @@ describe('language', () => {
548
551
  ```
549
552
 
550
553
  [`@alxia/i18n`](https://www.npmjs.com/package/@alxia/i18n) builds on this
551
- plugin — its options pass through — and adds `t()` bound to the request's
554
+ middleware — its options pass through — and adds `t()` bound to the request's
552
555
  language. When something does not behave as described here,
553
556
  [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
- Nothing scheduled yet.
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(...))` still works, deprecated.
11
11
 
12
12
  ## Next
13
13
 
@@ -1,6 +1,6 @@
1
1
  # Troubleshooting
2
2
 
3
- Each entry is headed by the text you see: the error the plugin throws at
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 'Alxia<Empty, Empty, "", never>' is missing the following properties from type 'LanguageOptions<string, BaseContext>': supported, fallback`](#type-alxiaempty-empty--never-is-missing-the-following-properties-from-type-languageoptionsstring-basecontext-supported-fallback)
15
+ - [`Type 'Alxia<Empty, "", never>' is missing the following properties from type 'LanguageOptions<string, BaseContext>': supported, fallback`](#type-alxiaempty--never-is-missing-the-following-properties-from-type-languageoptionsstring-basecontext-supported-fallback)
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
- - [`the plugin reads "user", which this app's context does not give: use the plugin that adds it first`](#the-plugin-reads-user-which-this-apps-context-does-not-give-use-the-plugin-that-adds-it-first)
21
- - [`the plugin reads "user", which this app's context gives with another type`](#the-plugin-reads-user-which-this-apps-context-gives-with-another-type)
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 uses it.
82
+ `.use(language(…))`, or in another app or group than the one that mounts it.
83
83
 
84
- **Why:** the plugin is a route hook: it applies to the routes declared
85
- after it, at runtime and in the types alike. A route before it never runs
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 the plugin first:
87
+ **Fix:** `use` it first:
89
88
 
90
89
  ```ts
91
90
  const app = alxia()
@@ -93,19 +92,20 @@ const app = alxia()
93
92
  .get('/', ({ language: current, reply }) => reply(200, current));
94
93
  ```
95
94
 
96
- ### `Type 'Alxia<Empty, Empty, "", never>' is missing the following properties from type 'LanguageOptions<string, BaseContext>': supported, fallback`
95
+ ### `Type 'Alxia<Empty, "", never>' is missing the following properties from type 'LanguageOptions<string, BaseContext>': supported, fallback`
97
96
 
98
97
  ```text
99
98
  error TS2769: No overload matches this call.
100
- Overload 1 of 2, '(plugin: (app: Alxia<Empty, Empty, "", never>) => …): Alxia<…>', gave the following error.
101
- Argument of type '<const L extends string, Ctx extends object = BaseContext>(options: LanguageOptions<L, Ctx>) => Alxia<RequiresOf<Ctx, "resolve"> & LanguageContext<L>, Empty, "", never> & Requiring<...>' is not assignable to parameter of type '(app: Alxia<Empty, Empty, "", never>) => …'.
99
+ Overload 1 of 11, '(plugin: (app: Alxia<Empty, "", never>) => AnyAlxia): AnyAlxia', gave the following error.
100
+ Argument of type '<const L extends string, Ctx extends object = BaseContext>(options: LanguageOptions<L, Ctx>) => Middleware<…>' is not assignable to parameter of type '(app: Alxia<Empty, "", never>) => AnyAlxia'.
102
101
  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
102
+ Type 'Alxia<Empty, "", never>' is missing the following properties from type 'LanguageOptions<string, BaseContext>': supported, fallback
104
103
  ```
105
104
 
106
105
  **When:** `app.use(language)`, without calling it.
107
106
 
108
- **Why:** `language` makes the plugin from its options; it is not the plugin.
107
+ **Why:** `language` makes the middleware from its options; it is not the
108
+ middleware.
109
109
 
110
110
  **Fix:**
111
111
 
@@ -183,7 +183,7 @@ database or a session store.
183
183
  **Why:** `resolve` is synchronous: it runs on every request, after the
184
184
  sources, and returns a tag or `undefined`.
185
185
 
186
- **Fix:** keep the preference where the plugin already reads it. When the
186
+ **Fix:** keep the preference where the middleware already reads it. When the
187
187
  user saves it, write it to the cookie; every request after reads it from
188
188
  there, before `Accept-Language`:
189
189
 
@@ -202,7 +202,7 @@ const app = alxia()
202
202
 
203
203
  Do the same at sign-in, from the stored preference.
204
204
 
205
- Or load it before the plugin, in an async `derive` or the plugin that
205
+ Or load it before the middleware, in an async `derive` or the middleware that
206
206
  signs the user in, and annotate `resolve` to read it — see
207
207
  [Reading the app's context](guide.md#reading-the-apps-context):
208
208
 
@@ -224,18 +224,18 @@ const app = alxia()
224
224
  error TS2339: Property 'user' does not exist on type 'BaseContext'.
225
225
  ```
226
226
 
227
- **When:** `resolve` reads something a `derive` or a plugin before
227
+ **When:** `resolve` reads something a `derive` or a middleware before
228
228
  `language()` added — a user, a session — and its parameter is not
229
229
  annotated: `resolve: (ctx) => ctx.user.language`.
230
230
 
231
231
  **Why:** an unannotated `resolve` is typed with the request's `BaseContext`
232
- — `request`, `url`, `ip`, `pathParams`, `set` — not with what other hooks
232
+ — `request`, `url`, `ip`, `pathParams`, `set` — not with what other middlewares
233
233
  added. `language()` is built before it is used, so it cannot see the app it
234
234
  will be used on.
235
235
 
236
236
  **Fix:** annotate the parameter with what it reads. `language()` infers it
237
- from the annotation, and the app that uses the plugin must then give it,
238
- before the plugin:
237
+ from the annotation, and the app that uses the middleware must then give it,
238
+ before it:
239
239
 
240
240
  ```ts
241
241
  import type { BaseContext } from '@alxia/core';
@@ -246,55 +246,59 @@ const byUser = language({
246
246
  resolve: ({ user }: BaseContext & { user: User }) => user.language ?? undefined,
247
247
  });
248
248
 
249
- alxia().use(auth).use(byUser); // auth derives user
249
+ alxia().plugin(auth).use(byUser); // auth derives user
250
250
  ```
251
251
 
252
252
  See [Reading the app's context](guide.md#reading-the-apps-context).
253
253
 
254
- ### `the plugin reads "user", which this app's context does not give: use the plugin that adds it first`
254
+ ### `Property 'user' is missing in type 'BaseContext & Empty' but required in type '{ user: User | null; }'`
255
255
 
256
256
  ```text
257
257
  error TS2769: No overload matches this call.
258
258
  …
259
- Types of property ''~requires'' are incompatible.
260
- Type '{ user: User; }' is not assignable to type '"the plugin reads \"user\", which this app's context does not give: use the plugin that adds it first"'.
259
+ Type 'BaseContext & Empty' is not assignable to type 'MiddlewareContext<{ user: User | null; }>'.
260
+ Property 'user' is missing in type 'BaseContext & Empty' but required in type '{ user: User | null; }'.
261
261
  ```
262
262
 
263
- **When:** the plugin's `resolve` is annotated to read `user`, and it is
263
+ **When:** the middleware's `resolve` is annotated to read `user`, and it is
264
264
  used on an app — or in a group — whose context has no `user` at that point:
265
- `alxia().use(byUser)`, or `use(byUser)` before `use(auth)`.
265
+ `alxia().use(byUser)`, or `use(byUser)` before `plugin(auth)`.
266
266
 
267
- **Why:** an annotated `resolve` makes the plugin require what it reads, and
268
- `use` checks the app's context against it, so `resolve` never runs without
267
+ **Why:** an annotated `resolve` makes the middleware require what it reads, and
268
+ `app.use` checks the app's context against it, so `resolve` never runs without
269
269
  it.
270
270
 
271
- **Fix:** use the plugin that adds `user` first, with the type `resolve`
271
+ **Fix:** mount what adds `user` first, with the type `resolve`
272
272
  reads:
273
273
 
274
274
  ```ts
275
- alxia().use(auth).use(byUser);
275
+ alxia().plugin(auth).use(byUser);
276
276
  ```
277
277
 
278
278
  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-use-the-plugin-that-adds-it-first).
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-add-the-plugin-or-middleware-that-gives-it-first).
280
280
 
281
- ### `the plugin reads "user", which this app's context gives with another type`
281
+ ### `Types of property 'user' are incompatible`
282
282
 
283
283
  ```text
284
284
  error TS2769: No overload matches this call.
285
285
  …
286
- Type '{ user: User; }' is not assignable to type '"the plugin reads \"user\", which this app's context gives with another type"'.
286
+ Type 'BaseContext & Empty & { user: User | null; }' is not assignable to type 'MiddlewareContext<{ user: User; }>'.
287
+ Type 'BaseContext & Empty & { user: User | null; }' is not assignable to type '{ user: User; }'.
288
+ Types of property 'user' are incompatible.
289
+ Type 'User | null' is not assignable to type 'User'.
290
+ Type 'null' is not assignable to type 'User'.
287
291
  ```
288
292
 
289
293
  **When:** the app gives a `user`, but of a type that does not fit the one
290
294
  `resolve`'s parameter is annotated with: a `User | null` where `resolve`
291
295
  reads `User`, or a user of another shape.
292
296
 
293
- **Why:** `use` checks each key the plugin reads against the app's context;
297
+ **Why:** `app.use` checks each key the middleware reads against the app's context;
294
298
  a narrower type passes, a wider or different one does not.
295
299
 
296
300
  **Fix:** annotate `resolve` with the type the app gives — `User | null`,
297
- handled inside — or narrow it in the plugin before:
301
+ handled inside — or narrow it in the middleware before:
298
302
 
299
303
  ```ts
300
304
  language({
@@ -312,20 +316,20 @@ More on this message in
312
316
  ```text
313
317
  error TS2769: No overload matches this call.
314
318
  …
315
- Types of property ''~requires'' are incompatible.
316
- Type '{ readonly '~any': "the plugin's resolve reads its context as any: annotate what it reads, or leave it unannotated"; }' is not assignable to type '"the plugin's resolve reads its context as any: annotate what it reads, or leave it unannotated"'.
319
+ 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"; }>'.
320
+ 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
321
  ```
318
322
 
319
323
  **When:** `resolve`'s parameter is annotated `any` —
320
- `resolve: (ctx: any) => ctx.user.language` — and the plugin is used, on
324
+ `resolve: (ctx: any) => ctx.user.language` — and the middleware is used, on
321
325
  any app, whatever its context gives.
322
326
 
323
327
  **Why:** an `any` parameter reads any key and says nothing of what it
324
- reads, so the plugin would require nothing, and an app without a `user`
325
- would be accepted, and throw on every request. The plugin is refused
328
+ reads, so the middleware would require nothing, and an app without a `user`
329
+ would be accepted, and throw on every request. It is refused
326
330
  instead.
327
331
 
328
- **Fix:** annotate what `resolve` reads, and use the plugin that adds it
332
+ **Fix:** annotate what `resolve` reads, and mount what adds it
329
333
  first:
330
334
 
331
335
  ```ts
@@ -335,7 +339,7 @@ const byUser = language({
335
339
  resolve: ({ user }: BaseContext & { user: User }) => user.language ?? undefined,
336
340
  });
337
341
 
338
- alxia().use(auth).use(byUser);
342
+ alxia().plugin(auth).use(byUser);
339
343
  ```
340
344
 
341
345
  Or leave it unannotated when it reads only the request:
@@ -373,7 +377,7 @@ await app.request('/', { headers: { 'accept-language': 'fr-FR,fr;q=0.9,en;q=0.8'
373
377
  **When:** a language switcher links to `?lang=fr`; that page is in French,
374
378
  the next one, without the query, is not.
375
379
 
376
- **Why:** the query only decides the request that carries it. The plugin
380
+ **Why:** the query only decides the request that carries it. The middleware
377
381
  keeps it for the next ones only when all of these hold:
378
382
 
379
383
  - `persist` is set — it is off by default;
@@ -400,22 +404,28 @@ language({
400
404
  **When:** behind a CDN, a reverse proxy, or `@alxia/cache`, the first
401
405
  visitor's language is served to everyone after.
402
406
 
403
- **Why:** the response depends on what the plugin read, and a cache keyed
404
- on the URL alone ignores it. The plugin says `Vary: Accept-Language,
407
+ **Why:** the response depends on what the middleware read, and a cache keyed
408
+ on the URL alone ignores it. It says `Vary: Accept-Language,
405
409
  Cookie` with the default `order`, and a reply's own `Vary` adds to it. Two
406
410
  things still leave a header out:
407
411
 
408
412
  - what `resolve` reads, unless the `vary` option names it;
409
- - an `onResponse` hook that sets `Vary` with `headers.set` instead of
410
- `@alxia/core`'s `vary`, which replaces every name before it.
413
+ - a middleware that sets `Vary` with
414
+ `headers.set` instead of `@alxia/core`'s `vary`, which replaces every
415
+ name before it.
411
416
 
412
417
  **Fix:** name what `resolve` reads, add to `Vary` rather than setting it,
413
418
  and give a cache the same headers:
414
419
 
415
420
  ```ts
416
- import { alxia, vary, withHeaders } from '@alxia/core';
421
+ import { alxia, defineMiddleware, settle, vary, withHeaders } from '@alxia/core';
417
422
 
418
423
  alxia()
424
+ .use(
425
+ defineMiddleware(async (ctx, next) =>
426
+ withHeaders(await settle(ctx, next()), (headers) => vary(headers, 'Accept-Encoding')),
427
+ ),
428
+ )
419
429
  .use(
420
430
  language({
421
431
  supported: ['en', 'fr'],
@@ -423,8 +433,7 @@ alxia()
423
433
  resolve: (ctx) => ctx.request.headers.get('x-preferred-language') ?? undefined,
424
434
  vary: ['X-Preferred-Language'],
425
435
  }),
426
- )
427
- .onResponse((response) => withHeaders(response, (headers) => vary(headers, 'Accept-Encoding')));
436
+ );
428
437
  // Vary: Accept-Language, Cookie, X-Preferred-Language, Accept-Encoding
429
438
  ```
430
439
 
@@ -437,9 +446,9 @@ cache({ ttl: 60, vary: ['accept-language', 'cookie', 'x-preferred-language'] });
437
446
  **When:** `order` lists `'path'`, and the routes are declared without the
438
447
  language segment: `.get('/products', …)`.
439
448
 
440
- **Why:** the plugin reads the segment at `pathIndex`; it does not remove
449
+ **Why:** the middleware reads the segment at `pathIndex`; it does not remove
441
450
  it from the path or route on it. `/fr/products` matches no route, so the
442
- `404` is answered before the plugin runs.
451
+ `404` is answered: the language was read, but no route answers in it.
443
452
 
444
453
  **Fix:** declare the segment in the routes:
445
454
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alxia/language",
3
- "version": "0.1.2",
3
+ "version": "0.2.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.3.0",
44
+ "@alxia/core": "^0.4.0",
45
45
  "@types/bun": "^1.4.2"
46
46
  },
47
47
  "peerDependencies": {
48
- "@alxia/core": "^0.3.0",
48
+ "@alxia/core": "^0.4.0",
49
49
  "typescript": "^6.0.3 || ^7.0.0"
50
50
  }
51
51
  }