@alxia/language 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +5 -3
- package/dist/index.js.map +3 -3
- package/dist/language.d.ts +2 -2
- package/dist/language.d.ts.map +1 -1
- package/docs/guide.md +6 -3
- package/docs/roadmap.md +1 -1
- package/docs/troubleshooting.md +17 -12
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/language.ts
|
|
2
2
|
import {
|
|
3
|
-
defineMiddleware
|
|
3
|
+
defineMiddleware,
|
|
4
|
+
markFactory
|
|
4
5
|
} from "@alxia/core";
|
|
5
6
|
|
|
6
7
|
// src/decide.ts
|
|
@@ -109,12 +110,13 @@ function language(options) {
|
|
|
109
110
|
if (!settings.supported.includes(settings.fallback)) {
|
|
110
111
|
throw new TypeError(`language(): the fallback "${settings.fallback}" is not supported`);
|
|
111
112
|
}
|
|
112
|
-
return defineMiddleware()((ctx, next)
|
|
113
|
+
return defineMiddleware()(function language(ctx, next) {
|
|
113
114
|
const found = decide(settings, ctx);
|
|
114
115
|
respond(settings, ctx, found);
|
|
115
116
|
return next(found);
|
|
116
117
|
});
|
|
117
118
|
}
|
|
119
|
+
markFactory(language);
|
|
118
120
|
export {
|
|
119
121
|
language,
|
|
120
122
|
match,
|
|
@@ -122,5 +124,5 @@ export {
|
|
|
122
124
|
parseAcceptLanguage
|
|
123
125
|
};
|
|
124
126
|
|
|
125
|
-
//# debugId=
|
|
127
|
+
//# debugId=5CF534D30FB9E72064756E2164756E21
|
|
126
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 {\n\ttype BaseContext,\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\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": "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
|
|
10
|
-
"debugId": "
|
|
9
|
+
"mappings": ";AAAA;AAAA;AAAA;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;;;ADTM,SAAS,QAGf,CACA,SACoD;AAAA,EACpD,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,iBAA6C,EACnD,SAAS,QAAQ,CAAC,KAAK,MAAM;AAAA,IAC5B,MAAM,QAA4B,OAAO,UAAU,GAAG;AAAA,IACtD,QAAQ,UAAU,KAAK,KAAK;AAAA,IAC5B,OAAO,KAAK,KAAK;AAAA,GAEnB;AAAA;AAGD,YAAY,QAAQ;",
|
|
10
|
+
"debugId": "5CF534D30FB9E72064756E2164756E21",
|
|
11
11
|
"names": []
|
|
12
12
|
}
|
package/dist/language.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BaseContext, type Empty, type Middleware, type
|
|
1
|
+
import { type BaseContext, type Empty, type Middleware, type Next, type RequiresOf } from '@alxia/core';
|
|
2
2
|
import type { LanguageContext, LanguageSource } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* `Ctx` is the type `resolve`'s parameter is annotated with —
|
|
@@ -45,7 +45,7 @@ export interface LanguageOptions<L extends string, Ctx extends object = BaseCont
|
|
|
45
45
|
* What `language()` makes: a middleware that gives `language`, one of `L`,
|
|
46
46
|
* and requires `Requires` of the app — what `resolve` reads.
|
|
47
47
|
*/
|
|
48
|
-
export type LanguageMiddleware<L extends string, Requires extends object = Empty> = Middleware<Requires, Promise<Next<LanguageContext<L
|
|
48
|
+
export type LanguageMiddleware<L extends string, Requires extends object = Empty> = Middleware<Requires, Promise<Next<LanguageContext<L>>>>;
|
|
49
49
|
/**
|
|
50
50
|
* The request's language, as a middleware: the routes declared after it read
|
|
51
51
|
* `language`, typed as one of `supported` — never a string a client made
|
package/dist/language.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"language.d.ts","sourceRoot":"","sources":["../src/language.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,WAAW,EAEhB,KAAK,KAAK,EACV,KAAK,UAAU,
|
|
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/guide.md
CHANGED
|
@@ -28,8 +28,11 @@ client that names no language it supports gets `Hello`. `current` is typed
|
|
|
28
28
|
```ts
|
|
29
29
|
function language<const L extends string, Ctx extends object = BaseContext>(
|
|
30
30
|
options: LanguageOptions<L, Ctx>,
|
|
31
|
-
):
|
|
32
|
-
|
|
31
|
+
): LanguageMiddleware<L, RequiresOf<Ctx, 'resolve'>>;
|
|
32
|
+
|
|
33
|
+
// A middleware: it gives `language`, and requires of the app what `resolve` reads.
|
|
34
|
+
type LanguageMiddleware<L extends string, Requires extends object = Empty> =
|
|
35
|
+
Middleware<Requires, Promise<Next<LanguageContext<L>>>>;
|
|
33
36
|
|
|
34
37
|
interface LanguageOptions<L extends string, Ctx extends object = BaseContext> {
|
|
35
38
|
readonly supported: readonly L[];
|
|
@@ -494,7 +497,7 @@ export const app = alxia()
|
|
|
494
497
|
language({
|
|
495
498
|
supported,
|
|
496
499
|
fallback: 'en',
|
|
497
|
-
persist: { secure:
|
|
500
|
+
persist: { secure: Bun.env.NODE_ENV !== 'development' }, // read at runtime: bun build inlines process.env
|
|
498
501
|
}),
|
|
499
502
|
)
|
|
500
503
|
.get('/', ({ language: current, reply }) => reply(200, messages[current].welcome))
|
package/docs/roadmap.md
CHANGED
|
@@ -7,7 +7,7 @@ number on it. Every release, with each change it made, is in
|
|
|
7
7
|
|
|
8
8
|
## Now
|
|
9
9
|
|
|
10
|
-
- **A middleware, not a plugin (0.4).** `app.use(language({ ... }))` runs on every request, a 404 included, which then says `Content-Language` and `Vary` too. `app.plugin(language(...))
|
|
10
|
+
- **A middleware, not a plugin (0.4).** `app.use(language({ ... }))` runs on every request, a 404 included, which then says `Content-Language` and `Vary` too. `app.plugin(language(...))`, the deprecated plugin form, was removed with alxia 0.5: give it to `use`.
|
|
11
11
|
|
|
12
12
|
## Next
|
|
13
13
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -12,7 +12,7 @@ the response does that you did not expect.
|
|
|
12
12
|
|
|
13
13
|
- [`Type '"de"' is not assignable to type '"en" | "fr"'`](#type-de-is-not-assignable-to-type-en--fr)
|
|
14
14
|
- [`Property 'language' does not exist on type 'Context<Empty, "/", Empty>'`](#property-language-does-not-exist-on-type-contextempty--empty)
|
|
15
|
-
- [`Type '
|
|
15
|
+
- [`Type 'LanguageMiddleware<string, Empty>' is not assignable to type '"this looks like a factory given uncalled: call it, as use(cors()) and not use(cors)"'`](#type-languagemiddlewarestring-empty-is-not-assignable-to-type-this-looks-like-a-factory-given-uncalled-call-it-as-usecors-and-not-usecors)
|
|
16
16
|
- [`Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ en: string; fr: string; }'`](#element-implicitly-has-an-any-type-because-expression-of-type-string-cant-be-used-to-index-type--en-string-fr-string-)
|
|
17
17
|
- [`Type 'string | null' is not assignable to type 'string | undefined'`](#type-string--null-is-not-assignable-to-type-string--undefined)
|
|
18
18
|
- [`Type 'Promise<string>' is not assignable to type 'string'`](#type-promisestring-is-not-assignable-to-type-string)
|
|
@@ -92,22 +92,27 @@ const app = alxia()
|
|
|
92
92
|
.get('/', ({ language: current, reply }) => reply(200, current));
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
### `Type '
|
|
95
|
+
### `Type 'LanguageMiddleware<string, Empty>' is not assignable to type '"this looks like a factory given uncalled: call it, as use(cors()) and not use(cors)"'`
|
|
96
96
|
|
|
97
97
|
```text
|
|
98
|
-
error
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Types of parameters 'options' and 'app' are incompatible.
|
|
102
|
-
Type 'Alxia<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)"'.
|
|
103
101
|
```
|
|
104
102
|
|
|
105
|
-
**When:** `app.use(language)`,
|
|
103
|
+
**When:** `app.use(language)`, the factory given uncalled. The message names
|
|
104
|
+
`cors` as its example, whichever factory it is. It also throws where it is
|
|
105
|
+
declared, since alxia 0.5, rather than answering each request with a 500:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
TypeError: use(): argument 1 looks like a factory (language): call it, use(language())
|
|
109
|
+
```
|
|
106
110
|
|
|
107
|
-
**Why:** `language` makes the middleware
|
|
108
|
-
middleware
|
|
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.
|
|
109
114
|
|
|
110
|
-
**Fix:**
|
|
115
|
+
**Fix:** call it with its options:
|
|
111
116
|
|
|
112
117
|
```ts
|
|
113
118
|
alxia().use(language({ supported: ['en', 'fr'], fallback: 'en' }));
|
|
@@ -395,7 +400,7 @@ language({
|
|
|
395
400
|
supported: ['en', 'fr'],
|
|
396
401
|
fallback: 'en',
|
|
397
402
|
order: ['query', 'cookie', 'header'],
|
|
398
|
-
persist: { secure:
|
|
403
|
+
persist: { secure: Bun.env.NODE_ENV !== 'development' }, // read at runtime: bun build inlines process.env
|
|
399
404
|
});
|
|
400
405
|
```
|
|
401
406
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alxia/language",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "The request's language for alxia, typed as the languages you support: from the query, a cookie, the path or Accept-Language, weights and regions included",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@alxia/core": "^0.
|
|
44
|
+
"@alxia/core": "^0.6.0",
|
|
45
45
|
"@types/bun": "^1.4.2"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@alxia/core": "^0.
|
|
48
|
+
"@alxia/core": "^0.6.0",
|
|
49
49
|
"typescript": "^6.0.3 || ^7.0.0"
|
|
50
50
|
}
|
|
51
51
|
}
|