To avoid shipping all translations to the client, use namespaces as code-splitting boundaries. The i18next runtime loads translations per namespace, per language.
The Recipe
- One namespace per route/feature: Scope keys via
useTranslation('checkout') or t('checkout:title'). Configure the extractor to output one file per namespace:
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: 'src/**/*.{ts,tsx}',
output: 'src/locales/{{language}}/{{namespace}}.json',
}
});
- Lazy-load namespaces with dynamic imports: Use
i18next-resources-to-backend to allow bundlers to create separate chunks for each namespace/language pair:
import i18next from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
// Each (lng, ns) pair becomes a separate chunk
i18next
.use(resourcesToBackend((lng, ns) => import(`./locales/${lng}/${ns}.json`)))
.init({
fallbackLng: 'en',
defaultNS: 'app',
ns: ['app'], // Only the app-shell namespace loads upfront
});
- Load on demand: Use
useTranslation('checkout') (react-i18next) or i18next.loadNamespaces('checkout') to fetch the specific chunk when the route renders.