@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 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,8 @@
1
1
  // src/language.ts
2
- import { definePlugin } from "@alxia/core";
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 definePlugin()((app) => app.derive((ctx) => {
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=0E86121386DC727A64756E2164756E21
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 { 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\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": ";AAAA;;;ACAA;;;ACOO,SAAS,mBAAmB,CAClC,QACa;AAAA,EACb,IAAI,CAAC;AAAA,IAAQ,OAAO,CAAC;AAAA,EACrB,OAAO,OACL,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,UAAU;AAAA,IACrB,OAAO,MAAM,OAAO,UAAU,KAAK,KAAK,EAAE,MAAM,GAAG;AAAA,IACnD,MAAM,IAAI,OACR,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAC3B,KAAK,CAAC,UAAU,MAAM,WAAW,IAAI,CAAC;AAAA,IACxC,MAAM,SAAS,MAAM,YAAY,IAAI,OAAO,EAAE,MAAM,CAAC,CAAC;AAAA,IACtD,OAAO;AAAA,MACN,KAAK,IAAI,KAAK;AAAA,MACd,GAAG,OAAO,SAAS,MAAM,IAAI,SAAS;AAAA,MACtC;AAAA,IACD;AAAA,GACA,EACA,OAAO,CAAC,UAAU,MAAM,QAAQ,MAAM,MAAM,IAAI,CAAC,EACjD,KAAK,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAC7C,IAAI,GAAG,KAAK,SAAS,EAAE,KAAK,EAAE,EAAE;AAAA;AAQ5B,SAAS,KAA6B,CAC5C,KACA,WACgB;AAAA,EAChB,MAAM,SAAS,IAAI,YAAY;AAAA,EAC/B,IAAI,WAAW;AAAA,IAAK,OAAO,UAAU;AAAA,EACrC,MAAM,QAAQ,UAAU,KAAK,CAAC,aAAa,SAAS,YAAY,MAAM,MAAM;AAAA,EAC5E,IAAI,UAAU;AAAA,IAAW,OAAO;AAAA,EAChC,MAAM,OAAO,OAAO,MAAM,GAAG,EAAE,MAAM;AAAA,EACrC,OACC,UAAU,KAAK,CAAC,aAAa,SAAS,YAAY,MAAM,IAAI,KAC5D,UAAU,KAAK,CAAC,aAAa,SAAS,YAAY,EAAE,MAAM,GAAG,EAAE,OAAO,IAAI;AAAA;AAKrE,SAAS,SAAiC,CAChD,QACA,WACgB;AAAA,EAChB,aAAa,SAAS,oBAAoB,MAAM,GAAG;AAAA,IAClD,MAAM,QAAQ,MAAM,KAAK,SAAS;AAAA,IAClC,IAAI,UAAU;AAAA,MAAW,OAAO;AAAA,EACjC;AAAA,EACA;AAAA;;;ADtCD,SAAS,IAAsB,CAC9B,UACA,KACA,QACgB;AAAA,EAChB,QAAQ,cAAc;AAAA,EACtB,QAAQ;AAAA,SACF,SAAS;AAAA,MACb,MAAM,QAAQ,IAAI,IAAI,aAAa,IAAI,SAAS,KAAK;AAAA,MACrD,OAAO,UAAU,OAAO,YAAY,MAAM,OAAO,SAAS;AAAA,IAC3D;AAAA,SACK,UAAU;AAAA,MACd,MAAM,QAAQ,IAAI,IAAI,UACrB,IAAI,QAAQ,QAAQ,IAAI,QAAQ,KAAK,EACtC,EAAE,IAAI,SAAS,MAAM;AAAA,MACrB,OAAO,UAAU,OAAO,YAAY,MAAM,OAAO,SAAS;AAAA,IAC3D;AAAA,SACK,QAAQ;AAAA,MACZ,MAAM,UAAU,IAAI,IAAI,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO,EACzD,SAAS;AAAA,MAEV,OAAO,YAAY,YAAY,YAAY,MAAM,SAAS,SAAS;AAAA,IACpE;AAAA,SACK;AAAA,MACJ,OAAO,UAAU,IAAI,QAAQ,QAAQ,IAAI,iBAAiB,GAAG,SAAS;AAAA;AAAA;AAKlE,SAAS,MAAwB,CACvC,UACA,KACqB;AAAA,EACrB,WAAW,UAAU,SAAS,OAAO;AAAA,IACpC,MAAM,QAAQ,KAAK,UAAU,KAAK,MAAM;AAAA,IACxC,IAAI,UAAU;AAAA,MAAW,OAAO,EAAE,UAAU,OAAO,gBAAgB,OAAO;AAAA,EAC3E;AAAA,EACA,MAAM,WAAW,SAAS,UAAU,GAAG;AAAA,EACvC,MAAM,QACL,aAAa,YAAY,YAAY,MAAM,UAAU,SAAS,SAAS;AAAA,EACxE,OAAO,UAAU,YACd,EAAE,UAAU,SAAS,UAAU,gBAAgB,WAAW,IAC1D,EAAE,UAAU,OAAO,gBAAgB,UAAU;AAAA;AAI1C,SAAS,OAAyB,CACxC,UACA,KACA,OACO;AAAA,EACP,QAAQ,SAAS,YAAY,IAAI;AAAA,EACjC,IAAI,SAAS,MAAM,SAAS,QAAQ;AAAA,IAAG,KAAK,SAAS,iBAAiB;AAAA,EACtE,IAAI,SAAS,MAAM,SAAS,QAAQ;AAAA,IAAG,KAAK,SAAS,QAAQ;AAAA,EAC7D,WAAW,QAAQ,SAAS;AAAA,IAAM,KAAK,SAAS,IAAI;AAAA,EACpD,IAAI,SAAS;AAAA,IAAiB,QAAQ,IAAI,oBAAoB,MAAM,QAAQ;AAAA,EAC5E,QAAQ,YAAY;AAAA,EACpB,IAAI,YAAY,aAAa,MAAM,mBAAmB,SAAS;AAAA,IAC9D,QAAQ,IAAI,SAAS,QAAQ,MAAM,UAAU;AAAA,MAC5C,MAAM;AAAA,MACN,UAAU;AAAA,MACV,UAAU;AAAA,MACV,QAAQ,QAAQ,UAAU;AAAA,MAC1B,QAAQ,QAAQ,UAAU,MAAM,KAAK,KAAK;AAAA,IAC3C,CAAC;AAAA,EACF;AAAA;;;AD1BM,SAAS,QAGf,CAAC,SAAkC;AAAA,EACnC,MAAM,WAAwB;AAAA,IAC7B,WAAW,QAAQ;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,OAAO,QAAQ,SAAS,CAAC,SAAS,UAAU,QAAQ;AAAA,IACpD,OAAO,QAAQ,SAAS;AAAA,IACxB,QAAQ,QAAQ,UAAU;AAAA,IAC1B,WAAW,QAAQ,aAAa;AAAA,IAChC,SACC,QAAQ,YAAY,OACjB,CAAC,IACD,QAAQ,YAAY,QACnB,YACA,QAAQ;AAAA,IACb,iBAAiB,QAAQ,oBAAoB;AAAA,IAE7C,SAAS,QAAQ;AAAA,IACjB,MAAM,QAAQ,QAAQ,CAAC;AAAA,EACxB;AAAA,EACA,IAAI,CAAC,SAAS,UAAU,SAAS,SAAS,QAAQ,GAAG;AAAA,IACpD,MAAM,IAAI,UACT,6BAA6B,SAAS,4BACvC;AAAA,EACD;AAAA,EACA,OAAO,aAAyC,EAAE,CAAC,QAClD,IAAI,OAAO,CAAC,QAA4B;AAAA,IACvC,MAAM,QAAQ,OAAO,UAAU,GAAG;AAAA,IAClC,QAAQ,UAAU,KAAK,KAAK;AAAA,IAC5B,OAAO;AAAA,GACP,CACF;AAAA;",
10
- "debugId": "0E86121386DC727A64756E2164756E21",
9
+ "mappings": "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",
10
+ "debugId": "5CF534D30FB9E72064756E2164756E21",
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 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>>>>;
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,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 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,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
- ): Alxia<RequiresOf<Ctx> & LanguageContext<L>, Empty, '', never> &
32
- Requiring<RequiresOf<Ctx>>;
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 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.
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 plugin runs for |
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 the plugin, at start-up.
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 plugin reads the segment; it does not route on it or remove it. 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; the plugin never runs
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 route behind the plugin says
188
- `Content-Language: <language>`. A route that sets its own wins over it:
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 plugin cannot see what a function reads.
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
- plugin added: see [Reading the app's context](#reading-the-apps-context). It
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 plugin before `language()`, which `resolve` then reads — see
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 plugin added, such as a signed-in `user` and
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 plugin is a
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
- .use(auth) // derives user: User | null
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: the plugin reads "user", which this app's context does not give: use the plugin that adds it first
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 plugin requires `{ user: User }`. An app whose `user`
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
- `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
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 plugin
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 plugin is
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 plugin would.
395
+ and return the same answers the middleware would.
391
396
 
392
397
  ## The typed context
393
398
 
394
- The routes behind the plugin read:
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 plugin adds to `ctx.set.headers`, and a reply's own `Vary` adds to it
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` — never reaches the
466
- plugin, so it carries none of these headers.
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: process.env['NODE_ENV'] === 'production' },
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 plugin reads on every
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
- plugin — its options pass through — and adds `t()` bound to the request's
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
- 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(...))`, the deprecated plugin form, was removed with alxia 0.5: give it to `use`.
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 '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
- - [`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,21 +92,27 @@ 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 '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 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>) => …'.
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)`, without calling it.
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
- **Why:** `language` makes the plugin from its options; it is not the plugin.
107
+ ```text
108
+ TypeError: use(): argument 1 looks like a factory (language): call it, use(language())
109
+ ```
109
110
 
110
- **Fix:**
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 plugin already reads it. When 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 plugin, in an async `derive` or the plugin that
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 plugin before
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 hooks
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 plugin must then give it,
238
- before the plugin:
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().use(auth).use(byUser); // auth derives user
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
- ### `the plugin reads "user", which this app's context does not give: use the plugin that adds it first`
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
- 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"'.
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 plugin's `resolve` is annotated to read `user`, and it is
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 `use(auth)`.
270
+ `alxia().use(byUser)`, or `use(byUser)` before `plugin(auth)`.
266
271
 
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
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:** use the plugin that adds `user` first, with the type `resolve`
276
+ **Fix:** mount what adds `user` first, with the type `resolve`
272
277
  reads:
273
278
 
274
279
  ```ts
275
- alxia().use(auth).use(byUser);
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-use-the-plugin-that-adds-it-first).
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
- ### `the plugin reads "user", which this app's context gives with another type`
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 '"the plugin reads \"user\", which this app's context gives with another 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 plugin reads against the app's context;
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 plugin before:
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
- 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"'.
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 plugin is used, on
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 plugin would require nothing, and an app without a `user`
325
- would be accepted, and throw on every request. The plugin is refused
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 use the plugin that adds it
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().use(auth).use(byUser);
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 plugin
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: process.env['NODE_ENV'] === 'production' },
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 plugin read, and a cache keyed
404
- on the URL alone ignores it. The plugin says `Vary: Accept-Language,
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
- - an `onResponse` hook that sets `Vary` with `headers.set` instead of
410
- `@alxia/core`'s `vary`, which replaces every name before it.
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 plugin reads the segment at `pathIndex`; it does not remove
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 before the plugin runs.
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.1.2",
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.3.0",
44
+ "@alxia/core": "^0.5.0",
45
45
  "@types/bun": "^1.4.2"
46
46
  },
47
47
  "peerDependencies": {
48
- "@alxia/core": "^0.3.0",
48
+ "@alxia/core": "^0.5.0",
49
49
  "typescript": "^6.0.3 || ^7.0.0"
50
50
  }
51
51
  }