Skip to content

Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors

FixDevs ·

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 English

Or namespace loading fails:

i18next: key "common:greeting" for languages "en" won't get resolved

Why This Happens

i18next is the most widely used JavaScript internationalization framework. react-i18next provides React bindings. Common issues:

  • i18next must be initialized before renderinguseTranslation() 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 useuseTranslation('dashboard') loads the dashboard namespace. 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.

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 error

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.

For related i18n issues, see Fix: next-intl Not Working and Fix: date-fns Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles