Per-request Locale Handling
Problem
In an SSR context, a shared mutable i18n singleton causes locale state to leak across concurrent requests. Request A sets locale: 'fr'; request B, arriving mid-render, inadvertently reads French strings.
Solution
Maintain a shared i18n instance at module scope and call fork() per request. The fork inherits the parent's catalog snapshots and loaders so nothing is re-registered. Each fork has its own locale, version counter, and subscriber set.
ts
import { createI18n } from '@vielzeug/lingua';
// Shared instance — set up once at server startup
export const sharedI18n = createI18n({
fallback: 'en',
locale: 'en',
catalogs: {
en: { title: 'Home' },
fr: () => import('./locales/fr.json').then((m) => m.default),
de: () => import('./locales/de.json').then((m) => m.default),
},
});
// In your request handler
const requestLocale = req.headers['accept-language']?.split(',')[0] ?? 'en';
const reqI18n = sharedI18n.fork({ locale: 'en' });
// setLocale() loads if needed and switches atomically
if (requestLocale !== 'en') {
await reqI18n.setLocale(requestLocale);
}
const html = `<h1>${reqI18n.t('title')}</h1>`;Pitfalls
fork()copies the catalog snapshot at call time. If the shared instance loads a new locale after a fork is created, the fork does not automatically gain access to it. For preloaded locales this is fine — the fork inherits loaders and will load on demand viasetLocale().setLocale()throws[lingua/E001]if the locale is not registered. Validate the request locale againstsharedI18n.getSupportedLocales()before switching to avoid a 500 error from an unexpected locale header.- Do not share the per-request fork across async boundaries (e.g. via a module-level variable) — pass it explicitly to rendering functions.