@alxia/language
The request's language for alxia, typed as the languages you support — never a string a client made up. No dependency.
bun add @alxia/language @alxia/core
bun add -d typescript
Usage
import { alxia } from '@alxia/core';
import { language } from '@alxia/language';
const supported = ['en', 'fr', 'pt-BR'] as const;
const greetings: Record<(typeof supported)[number], string> = { en: 'Hello', fr: 'Bonjour', 'pt-BR': 'Olá' };
const app = alxia()
.use(language({ supported, fallback: 'en' }))
.get('/', ({ language, reply }) => reply(200, greetings[language])); // 'en' | 'fr' | 'pt-BR'
app.listen(3000);
It reads, in order:
| source | |
|---|---|
query | ?lang=fr |
cookie | language=fr |
path | /fr/products, at pathIndex |
header | Accept-Language, by weight: de, fr-CA;q=0.8 is fr |
then resolve(ctx) — a user's saved preference — then fallback. A tag
matches whatever its case, by its base language (fr-CA for fr), or by a
region of it (pt for pt-BR). languageSource says which source decided.
Every response says Content-Language, and Vary by the headers it read.
With persist, a language the query named is kept in the cookie. Given to
use on the app, it runs on every request: a 404 says Content-Language too.
Reading the app's context
Annotate resolve's parameter to decide by what an earlier middleware added —
the language a signed-in user saved. The middleware then requires it: an app
that does not give user before it cannot use it.
import { alxia, type BaseContext } from '@alxia/core';
import { language } from '@alxia/language';
const byUser = language({
supported: ['en', 'fr'],
fallback: 'en',
order: ['query', 'cookie'],
resolve: ({ user }: BaseContext & { user: { language: string } | null }) => user?.language,
});
alxia().plugin(auth).use(byUser); // auth derives user
alxia().use(byUser); // a compile error: this app gives no `user`
A resolve annotated any would require nothing, so the middleware is refused
on every app: annotate what it reads, or leave it unannotated.
Options
| option | default | |
|---|---|---|
supported | required | the languages: language's type |
fallback | required | one of them; the types refuse another |
order | ['query', 'cookie', 'header'] | |
query, cookie | lang, language | their names |
pathIndex | 0 | |
persist | false | true, or { maxAge, secure } |
contentLanguage | true | |
resolve | none | (ctx) => string | undefined; annotate ctx to read what an earlier middleware adds |
vary | none | the headers resolve reads, added to Vary |
API
| export | |
|---|---|
language(options) | the middleware, given to app.use: it adds language and languageSource |
LanguageOptions | its options: supported, fallback, order, query, cookie, pathIndex, persist, contentLanguage, resolve, vary |
negotiate(header, supported) | the supported language Accept-Language prefers |
parseAcceptLanguage(header), match(tag, supported) | its parts |
LanguageContext, LanguageSource, Accepted | its types |
LanguageMiddleware<L, Requires> | what language() returns: a middleware adding LanguageContext<L>, requiring of the app what resolve reads |
Documentation
- Guide: every option with its default and an example, how the language is found and
Accept-Languagenegotiated, reading what an earlier middleware added, the typed context, and the headers the middleware adds. - Troubleshooting: an error, or a response in the wrong language, and what to do about it.
- Roadmap: what is coming, and what is not planned.