HTTP@rhythmjs/http
i18n
Talk to users in their own terms: per-request language negotiation over any i18next.
Language negotiation with i18next#
i18n({ i18next, ... }) puts a per-request translator on the context: ctx.t, ctx.language, and the scoped instance as ctx.i18n (I18nContext). The instance contract is structural (I18nInstanceLike: only t is required) and every optional member is feature-detected, so i18next major versions cannot break compilation. Per-request isolation prefers cloneInstance(), then getFixedT(language), then a shared changeLanguage(); an uninitialized instance is init()-ed once, lazily.
import i18next from "i18next";
import { i18n, type I18nContext } from "@rhythmjs/http/i18n";
await i18next.init({ fallbackLng: "en", supportedLngs: ["en", "de"], resources });
router.use<I18nContext>(i18n({ i18next, cacheCookie: true })).get("/", (ctx) => {
ctx.response.body = ctx.t("welcome"); // content-language set from ctx.language
});Detection order and matching#
Candidates are tried per source in detection.order - default querystring (?lng=), cookie (i18next), then the Accept-Language header parsed with q-values; a path source reads a configurable segment index. The first source that yields candidates wins. Matching against the supported list (from options, or read off the instance) is case-insensitive and falls from exact tag to base language in both directions (de-AT ↔ de), then to the fallback language.
With cacheCookie: true the resolved language is written back to the lookup cookie (one-year Max-Age) whenever it changed, and unless disabled the response gains a content-language header. parseAcceptLanguage(header) and detectLanguage(request, options?) are exported standalone.