Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
Part of: React & Frontend Errors
Quick Answer
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
The Problem
Translation keys render as raw strings instead of translated text:
const { t } = useTranslation();
return <h1>{t('welcome')}</h1>;
// Renders: "welcome" instead of "Welcome to our app"Or language switching does nothing:
i18n.changeLanguage('ja');
// UI doesn't update — still shows EnglishOr namespace loading fails:
i18next: key "common:greeting" for languages "en" won't get resolvedWhy This Happens
i18next is the most widely used JavaScript internationalization framework. react-i18next provides React bindings. Common issues:
- i18next must be initialized before rendering —
useTranslation()returns raw keys if i18next hasn’t loaded translations yet. Initialization is async when loading translations from files or URLs. - Translation files must match the configured path — i18next-http-backend fetches translations from a URL pattern like
/locales/{{lng}}/{{ns}}.json. If the file doesn’t exist at that path, translations silently fail. - Namespaces must be loaded before use —
useTranslation('dashboard')loads thedashboardnamespace. If it doesn’t exist or hasn’t loaded, keys from that namespace return as-is. - Language detection has a priority order — i18next-browser-languagedetector checks cookies, localStorage, navigator.language, etc. If the detected language doesn’t have translations, it falls back to
fallbackLng.
The core problem with i18next failures is that they are almost always silent. There is no exception when a translation key is missing — by default i18next returns the key itself as a string and emits a console warning that nobody sees in production. A user opens your app and sees dashboard.title.welcome rendered as a literal string in the UI. Your CI passes, your tests pass, your production monitoring shows zero errors, and your conversion rate drops because customers think the site is broken.
The async initialization model is the second major source of incidents. When you load translations from JSON over HTTP, i18next returns a promise and useTranslation() reports ready: false until that promise resolves. If you forget to wrap the tree in <Suspense> or to check ready, the first render returns raw keys and React caches that render in production builds. The user sees garbage strings, refreshes the page, and the second render works — which makes the bug nearly impossible to reproduce in QA.
From a production incident lens, missing translation keys are the canonical case of a low-severity, high-frequency bug. They rarely page on-call but they accumulate as a tax on every release. A deployment that misses a namespace for one locale degrades the experience for thousands of users without ever triggering an alert. The fix is to instrument i18next’s missingKeyHandler, ship every missing key to your error tracker (Sentry, Datadog, etc.), and gate releases behind a translation-coverage check that fails CI when any namespace has fewer keys than its English baseline.
Fix 1: React Setup
npm install i18next react-i18next i18next-http-backend i18next-browser-languagedetector// lib/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(HttpBackend) // Load translations from files
.use(LanguageDetector) // Auto-detect user language
.use(initReactI18next) // React bindings
.init({
fallbackLng: 'en',
supportedLngs: ['en', 'ja', 'es', 'fr', 'de'],
// Default namespace
defaultNS: 'common',
ns: ['common', 'auth', 'dashboard'],
// Backend — where to load translation files
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
// Language detection order
detection: {
order: ['localStorage', 'cookie', 'navigator', 'htmlTag'],
caches: ['localStorage', 'cookie'],
},
// Interpolation
interpolation: {
escapeValue: false, // React already escapes
},
// Debug — shows warnings in console
debug: process.env.NODE_ENV === 'development',
});
export default i18n;// public/locales/en/common.json
{
"welcome": "Welcome to our app",
"greeting": "Hello, {{name}}!",
"items_count": "{{count}} item",
"items_count_plural": "{{count}} items",
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
}
}
// public/locales/ja/common.json
{
"welcome": "アプリへようこそ",
"greeting": "こんにちは、{{name}}さん!",
"items_count": "{{count}}個のアイテム",
"nav": {
"home": "ホーム",
"about": "概要",
"contact": "お問い合わせ"
}
}// main.tsx — import i18n before rendering
import './lib/i18n'; // Must import before App
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<div>Loading translations...</div>}>
<MainContent />
</Suspense>
);
}Fix 2: Use Translations in Components
'use client';
import { useTranslation, Trans } from 'react-i18next';
function HomePage() {
const { t, i18n } = useTranslation(); // Uses 'common' namespace by default
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('greeting', { name: 'Alice' })}</p>
<p>{t('items_count', { count: 5 })}</p>
{/* Nested keys */}
<nav>
<a href="/">{t('nav.home')}</a>
<a href="/about">{t('nav.about')}</a>
</nav>
{/* Different namespace */}
<DashboardContent />
{/* Language switcher */}
<div>
<button onClick={() => i18n.changeLanguage('en')}>English</button>
<button onClick={() => i18n.changeLanguage('ja')}>日本語</button>
<button onClick={() => i18n.changeLanguage('es')}>Español</button>
</div>
{/* Current language */}
<p>Current: {i18n.language}</p>
</div>
);
}
// Use a specific namespace
function DashboardContent() {
const { t } = useTranslation('dashboard');
return <h2>{t('title')}</h2>; // Reads from dashboard.json
}
// Multiple namespaces
function AuthPage() {
const { t } = useTranslation(['auth', 'common']);
return (
<div>
<h1>{t('auth:login_title')}</h1>
<p>{t('common:welcome')}</p>
</div>
);
}
// Trans component — for HTML in translations
// common.json: { "terms": "By signing up, you agree to our <link>Terms</link>" }
function Terms() {
const { t } = useTranslation();
return (
<Trans
i18nKey="terms"
components={{ link: <a href="/terms" /> }}
/>
);
}Fix 3: Pluralization and Formatting
// public/locales/en/common.json
{
"items_zero": "No items",
"items_one": "{{count}} item",
"items_other": "{{count}} items",
"messages": {
"unread_zero": "No unread messages",
"unread_one": "You have {{count}} unread message",
"unread_other": "You have {{count}} unread messages"
},
"date_format": "Last updated: {{date, datetime}}",
"price": "Price: {{amount, currency(USD)}}",
"relative_time": "{{val, relativetime(quarter)}}"
}// Pluralization
t('items', { count: 0 }); // "No items"
t('items', { count: 1 }); // "1 item"
t('items', { count: 5 }); // "5 items"
// Date formatting
t('date_format', { date: new Date(), formatParams: { date: { dateStyle: 'long' } } });
// "Last updated: March 29, 2026"
// Number formatting
t('price', { amount: 49.99 });
// "Price: $49.99"
// i18n config for formatting
i18n.init({
interpolation: {
escapeValue: false,
format: (value, format, lng) => {
if (format === 'uppercase') return value.toUpperCase();
if (value instanceof Date) {
return new Intl.DateTimeFormat(lng).format(value);
}
return value;
},
},
});Fix 4: Next.js App Router Integration
// For Next.js, consider next-intl (simpler) or use i18next with SSR:
// lib/i18n-server.ts — server-side translations
import { createInstance } from 'i18next';
import { initReactI18next } from 'react-i18next/initReactI18next';
import resourcesToBackend from 'i18next-resources-to-backend';
export async function initI18n(lng: string, ns: string | string[] = 'common') {
const i18nInstance = createInstance();
await i18nInstance
.use(initReactI18next)
.use(resourcesToBackend((language: string, namespace: string) =>
import(`@/locales/${language}/${namespace}.json`)
))
.init({
lng,
ns,
fallbackLng: 'en',
interpolation: { escapeValue: false },
});
return i18nInstance;
}
// app/[lng]/page.tsx — Server Component with translations
export default async function HomePage({ params }: { params: { lng: string } }) {
const { lng } = await params;
const i18n = await initI18n(lng);
const t = i18n.getFixedT(lng, 'common');
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('greeting', { name: 'Alice' })}</p>
</div>
);
}
// middleware.ts — language detection and routing
import { NextResponse, type NextRequest } from 'next/server';
const locales = ['en', 'ja', 'es'];
const defaultLocale = 'en';
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Check if pathname already has a locale
const hasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (hasLocale) return;
// Detect locale from Accept-Language header
const acceptLang = request.headers.get('accept-language') || '';
const detected = locales.find(l => acceptLang.includes(l)) || defaultLocale;
return NextResponse.redirect(new URL(`/${detected}${pathname}`, request.url));
}
export const config = {
matcher: ['/((?!api|_next|.*\\..*).*)'],
};Fix 5: TypeScript Type Safety
// i18n.d.ts — type-safe translation keys
import 'i18next';
import common from '../public/locales/en/common.json';
import auth from '../public/locales/en/auth.json';
import dashboard from '../public/locales/en/dashboard.json';
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'common';
resources: {
common: typeof common;
auth: typeof auth;
dashboard: typeof dashboard;
};
}
}
// Now t('welcome') autocompletes
// t('nonexistent') shows a TypeScript errorProduction Incident: Missing Keys Silently Break Production UI
The most painful i18next incidents are the ones nobody notices for days. A translator updates en/common.json with a new dashboard.welcome key. The developer ships a new button that calls t('dashboard.welcome'). The Japanese, Spanish, and German common.json files don’t get the new key. Users in those locales see dashboard.welcome rendered as a raw string in the UI.
The default i18next behavior — returnNull: false, return key as string, emit a console warning — is the exact behavior that hides the bug from your error tracker. Sentry sees zero events. Your synthetic monitoring is in English, so it sees nothing wrong. You only find out when a customer support ticket arrives with a screenshot.
Wire up a missingKeyHandler that reports to your error tracker:
import * as Sentry from '@sentry/react';
i18n.init({
saveMissing: true,
missingKeyHandler: (lngs, ns, key, fallbackValue) => {
Sentry.captureMessage('i18n missing key', {
level: 'warning',
tags: { ns, locale: lngs[0] },
extra: { key, fallbackValue },
});
},
});Add a CI check that compares the key set of each locale to the English baseline:
# scripts/check-i18n-coverage.sh
for locale in ja es fr de; do
diff <(jq -r 'paths(scalars) | join(".")' public/locales/en/common.json | sort) \
<(jq -r 'paths(scalars) | join(".")' public/locales/$locale/common.json | sort) \
&& echo "$locale: OK" || (echo "$locale: missing keys" && exit 1)
doneThe blast radius of a missing-key incident is wide because i18next silently degrades rather than failing fast. Loud failure modes — a thrown error, a hard 500 — are easier to triage than silent degradation. If you can tolerate the strictness, set returnNull: true and a fallbackLng that renders the English string instead of the key. Users see English instead of dashboard.welcome, which is much better than a broken UI and gives you time to ship the translation.
Fix 6: Lazy Loading and Performance
// Load namespaces on demand
i18n.init({
partialBundledLanguages: true,
ns: ['common'], // Only load common initially
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
});
// Load additional namespace when needed
function DashboardPage() {
const { t, ready } = useTranslation('dashboard', { useSuspense: false });
if (!ready) return <div>Loading...</div>;
return <h1>{t('title')}</h1>;
}
// Preload a namespace
i18n.loadNamespaces('settings').then(() => {
// Namespace is now available
});
// Preload a language
i18n.loadLanguages('ja').then(() => {
// Japanese translations loaded
});Still Not Working?
Keys render as-is (e.g., “welcome” instead of translated text) — translations haven’t loaded. Check the network tab for the JSON file request. Verify the file path matches loadPath in the backend config. Also wrap your app in <Suspense> — i18next loads translations async.
changeLanguage() doesn’t update the UI — ensure react-i18next is initialized with use(initReactI18next). Without it, React doesn’t re-render when the language changes. Also check that translations for the target language exist.
Pluralization returns the wrong form — i18next v21+ uses ICU-style plural keys: _zero, _one, _two, _few, _many, _other. The old format (_plural) still works but only for English. For other languages, use the correct plural forms for that locale.
TypeScript doesn’t autocomplete keys — add the CustomTypeOptions module augmentation. The resource types must match your actual JSON structure. Restart the TypeScript language server after adding the declarations.
Translations work in dev but break on the production build — i18next-http-backend fetches translation JSON from the public directory at runtime. Production builds with strict CORS or aggressive CDN caching can serve stale or 404 responses. Bundle the translations using i18next-resources-to-backend and dynamic imports so the JSON ships with your JavaScript bundle.
changeLanguage() works but the page reloads instead of updating in place — your language switcher is probably calling router.push('/?lng=ja') instead of i18n.changeLanguage('ja'). Use the i18n instance method directly; the router push triggers a full page navigation. For Next.js App Router with locale-prefixed routes, use the router but call changeLanguage() on the new locale segment.
Hydration mismatch errors after enabling i18n — server and client render different translations because language detection differs. The server may detect from cookies while the client falls back to navigator.language. Persist the detected language in a cookie during SSR and pass it to the client. See Fix: React Hydration Error for the underlying mismatch pattern.
For related i18n issues, see Fix: next-intl Not Working, Fix: Lingui Not Working, and Fix: date-fns Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid
How to fix Mapbox GL JS issues — access token setup, React integration with react-map-gl, markers and popups, custom layers, geocoding, directions, and Next.js configuration.
Fix: React PDF Not Working — PDF Not Rendering, Worker Error, or Pages Blank
How to fix react-pdf and @react-pdf/renderer issues — PDF viewer setup, worker configuration, page rendering, text selection, annotations, and generating PDFs in React.
Fix: Million.js Not Working — Compiler Errors, Components Not Optimized, or React Compatibility Issues
How to fix Million.js issues — compiler setup with Vite and Next.js, block() optimization rules, component compatibility constraints, automatic mode, and debugging performance gains.
Fix: Radix UI Not Working — Popover Not Opening, Dialog Closing Immediately, or Styling Breaking
How to fix Radix UI issues — Popover and Dialog setup, controlled vs uncontrolled state, portal rendering, animation with CSS or Framer Motion, accessibility traps, and Tailwind CSS integration.