IntlPull
Technical
18 min read

Next.js i18n Translations Not Working? Complete Troubleshooting Guide (2026)

Fix common Next.js internationalization issues: missing translations, t() returning keys, JSON file errors, middleware problems, hydration mismatches, and more. Debug guide for next-intl, react-i18next.

IntlPull Team
IntlPull Team
03 Feb 2026, 11:44 AM [PST]
On this page
Summary

Fix common Next.js internationalization issues: missing translations, t() returning keys, JSON file errors, middleware problems, hydration mismatches, and more. Debug guide for next-intl, react-i18next.

Quick Answer

If your Next.js translations aren't working, check these in order: (1) Translation key exists in JSON file with correct nesting, (2) JSON file is valid (no trailing commas), (3) Locale is correctly detected in middleware, (4) Provider/context wraps your components, (5) Correct hook used (Server vs Client Component). 90% of issues are typos in keys or missing JSON entries.


I've debugged hundreds of i18n setups across React and Next.js projects. The good news? Translation bugs are almost always one of about 10 predictable issues. The bad news? They can be incredibly frustrating to diagnose when you don't know what to look for.

This guide covers every translation issue I've encountered in production, organized from most common to least. I'm focusing on Next.js 14/15 with App Router, but most of these apply to Pages Router too.

Issue #1: Translation Key Returns Instead of Translation

Symptom: You see common.buttons.submit on screen instead of "Submit"

This is the #1 issue, and it usually means one of these:

The Key Doesn't Exist in Your JSON

JSON
1// ❌ Your code uses: t('common.buttons.submit')
2// But your en.json has:
3{
4  "common": {
5    "button": {  // Note: "button" not "buttons"
6      "submit": "Submit"
7    }
8  }
9}

Fix: Check your JSON structure carefully. Use an IDE with JSON path preview or a tool like IntlPull that validates keys.

You're Using the Wrong Namespace

TSX
1// ❌ Wrong: looking in default namespace
2const t = useTranslations();
3t('checkout.title');
4
5// ✅ Correct: specify the namespace
6const t = useTranslations('checkout');
7t('title');

The JSON File Isn't Being Loaded

Check your i18n configuration:

TypeScript
1// i18n/request.ts for next-intl
2import { getRequestConfig } from 'next-intl/server';
3
4export default getRequestConfig(async ({ locale }) => ({
5  messages: (await import(`../messages/${locale}.json`)).default
6}));

Verify the file path is correct. A common mistake is putting messages in /public/locales when your config expects /messages.


Issue #2: Invalid JSON Syntax

Symptom: App crashes with "Unexpected token" or translations silently fail

JSON is strict. These will break everything:

JSON
1// ❌ Trailing comma (most common)
2{
3  "welcome": "Hello",
4  "goodbye": "Bye",  // <-- This comma breaks it
5}
6
7// ❌ Single quotes
8{
9  'welcome': 'Hello'  // Must use double quotes
10}
11
12// ❌ Unescaped quotes in values
13{
14  "message": "Click "here" to continue"  // Needs escaping
15}
16
17// ✅ Correct
18{
19  "welcome": "Hello",
20  "goodbye": "Bye",
21  "message": "Click \"here\" to continue"
22}

Fix: Use a JSON validator. VS Code highlights syntax errors. Run cat en.json | python -m json.tool to validate from CLI.


Issue #3: Missing Translation for Specific Locale

Symptom: English works, but Spanish/German/etc. shows keys

You've added the key to en.json but forgot the other locales:

/messages
  en.json  ✅ Has "checkout.newFeature": "Try our new feature"
  es.json  ❌ Missing this key entirely
  de.json  ❌ Missing this key entirely

Fix: Use a translation management tool like IntlPull that tracks missing translations across locales. Or set up a fallback:

TypeScript
1// next-intl config with fallback
2export default getRequestConfig(async ({ locale }) => ({
3  messages: {
4    ...(await import(`../messages/en.json`)).default, // Fallback
5    ...(await import(`../messages/${locale}.json`)).default
6  }
7}));

Issue #4: Middleware Not Detecting Locale

Symptom: Always shows default language, URL locale is ignored

TypeScript
1// ❌ Middleware not running on your routes
2export const config = {
3  matcher: ['/api/:path*']  // Only matches API routes!
4};
5
6// ✅ Correct matcher for i18n
7export const config = {
8  matcher: ['/((?!api|_next|.*\\..*).*)']
9};

Also verify your middleware file location - it must be at middleware.ts in the project root, not inside /app or /src/app.

Check Locale Detection Logic

TypeScript
1// middleware.ts
2import createMiddleware from 'next-intl/middleware';
3
4export default createMiddleware({
5  locales: ['en', 'es', 'de', 'fr'],
6  defaultLocale: 'en',
7  localePrefix: 'always'  // or 'as-needed'
8});

Debug tip: Add console.log to middleware to see what locale is being detected:

TypeScript
1export default function middleware(request: NextRequest) {
2  console.log('Detected locale:', request.nextUrl.pathname);
3  // ... rest of middleware
4}

Issue #5: Hydration Mismatch Errors

Symptom: Console shows "Text content does not match server-rendered HTML"

This happens when server and client render different translations:

Cause 1: Using Client Hook in Server Component

TSX
1// ❌ Server Component using client hook
2// app/[locale]/page.tsx (Server Component by default)
3import { useTranslations } from 'next-intl';  // This is fine actually
4
5export default function Page() {
6  const t = useTranslations('home');
7  return <h1>{t('title')}</h1>;  // Works in next-intl!
8}

Wait, this actually works in next-intl because it detects the context. But with react-i18next:

TSX
1// ❌ With react-i18next in Server Component
2'use server';
3import { useTranslation } from 'react-i18next';  // Won't work
4
5// ✅ Use server-side function
6import { getTranslations } from 'next-intl/server';
7
8export default async function Page() {
9  const t = await getTranslations('home');
10  return <h1>{t('title')}</h1>;
11}

Cause 2: Date/Time Formatting Without Timezone

TSX
1// ❌ Server might be UTC, client is local timezone
2{formatDate(new Date())}
3
4// ✅ Always specify timezone
5import { format } from 'date-fns-tz';
6{format(new Date(), 'PPP', { timeZone: userTimezone })}

Issue #6: Provider Not Wrapping Components

Symptom: "Could not find IntlProvider" or similar context errors

TSX
1// ❌ Missing provider in layout
2// app/[locale]/layout.tsx
3export default function Layout({ children }) {
4  return <html><body>{children}</body></html>;
5}
6
7// ✅ With provider
8import { NextIntlClientProvider } from 'next-intl';
9import { getMessages } from 'next-intl/server';
10
11export default async function Layout({ children, params: { locale } }) {
12  const messages = await getMessages();
13  
14  return (
15    <html lang={locale}>
16      <body>
17        <NextIntlClientProvider messages={messages}>
18          {children}
19        </NextIntlClientProvider>
20      </body>
21    </html>
22  );
23}

Issue #7: Dynamic Keys Not Working

Symptom: t(variableName) returns the variable value, not translation

This is a TypeScript/bundler optimization issue:

TSX
1// ❌ Dynamic key might not be statically analyzable
2const key = `status.${order.status}`;
3t(key);  // Some bundlers can't optimize this
4
5// ✅ Use explicit mapping
6const statusMessages = {
7  pending: t('status.pending'),
8  shipped: t('status.shipped'),
9  delivered: t('status.delivered')
10};
11return statusMessages[order.status];
12
13// ✅ Or use t.raw() for dynamic keys in next-intl
14t(`status.${order.status}`);  // This actually works in next-intl

Issue #8: Pluralization Not Working

Symptom: Shows "{count, plural, one {# item} other {# items}}" literally

You're using ICU format but the library isn't parsing it:

JSON
1// Your JSON
2{
3  "items": "{count, plural, one {# item} other {# items}}"
4}
TSX
1// ❌ Not passing the variable
2t('items');
3
4// ✅ Pass the count variable
5t('items', { count: 5 });  // "5 items"

Check Your Library Supports ICU

  • next-intl: Full ICU support ✅
  • react-i18next: Needs i18next-icu plugin
  • next-translate: Basic pluralization only

Issue #9: Environment/Build Issues

Keys Work in Dev, Break in Production

TypeScript
1// ❌ Dynamic imports might fail at build time
2const messages = await import(`@/messages/${locale}.json`);
3
4// ✅ Ensure all locales are statically known
5import en from '@/messages/en.json';
6import es from '@/messages/es.json';
7
8const messages = { en, es };
9export const getMessages = (locale: string) => messages[locale];

"Module not found" After Adding New Locale

After adding a new locale file, restart your dev server. Next.js caches module resolution.

Terminal
rm -rf .next && npm run dev

Symptom: Clicking internal links resets to default language

TSX
1// ❌ Regular Link loses locale
2import Link from 'next/link';
3<Link href="/about">About</Link>
4
5// ✅ Use navigation from next-intl
6import { Link } from '@/i18n/navigation';  // Your configured navigation
7<Link href="/about">About</Link>  // Preserves locale automatically
8
9// ✅ Or manually include locale
10import { useLocale } from 'next-intl';
11const locale = useLocale();
12<Link href={`/${locale}/about`}>About</Link>

Debugging Checklist

When translations break, run through this checklist:

CheckCommand/Action
JSON syntax valid`cat messages/en.json
Key existsSearch for exact key in JSON file
Locale file existsls messages/
Middleware runningAdd console.log, check server output
Provider in placeCheck layout.tsx for IntlProvider
Correct importServer: getTranslations, Client: useTranslations
Cache clearedrm -rf .next && npm run dev

Common Error Messages Decoded

ErrorMeaningFix
Missing message: "key"Key not in JSONAdd key to all locale files
Unable to find next-intl localeMiddleware not setting localeCheck middleware.ts placement and config
Hydration failedServer/client mismatchUse correct hook for component type
Cannot read property 't' of undefinedMissing providerWrap with IntlClientProvider
ENOENT: no such fileFile path wrongCheck messages folder path in config

Prevention: Avoid These Issues Entirely

1. Use TypeScript for Type-Safe Keys

TypeScript
1// Generate types from your JSON
2// With next-intl, create types file:
3type Messages = typeof import('./messages/en.json');
4declare global {
5  interface IntlMessages extends Messages {}
6}

Now TypeScript will error on invalid keys.

2. Use a Translation Management System

Tools like IntlPull automatically:

  • Validate JSON syntax
  • Track missing translations per locale
  • Prevent key typos with autocomplete
  • Sync keys across all locales

3. Add CI Checks

YAML
1# .github/workflows/i18n-check.yml
2- name: Validate JSON files
3  run: |
4    for f in messages/*.json; do
5      python -m json.tool "$f" > /dev/null || exit 1
6    done
7
8- name: Check for missing keys
9  run: npx intlpull check --config intlpull.config.json

4. Integration Tests for Critical Paths

TypeScript
1// e2e/i18n.spec.ts
2test('checkout page renders in all locales', async ({ page }) => {
3  for (const locale of ['en', 'es', 'de']) {
4    await page.goto(`/${locale}/checkout`);
5    // Should not contain raw translation keys
6    await expect(page.locator('body')).not.toContainText('checkout.');
7  }
8});

When to Use Translation Management

If you're experiencing these issues repeatedly, consider a TMS:

ScenarioDIY JSON FilesTranslation Management
< 50 keys, 1-2 devs✅ Works fineOverkill
> 200 keys, multiple devs❌ Merge conflicts✅ Single source of truth
> 3 languages❌ Hard to sync✅ Missing key detection
External translators❌ JSON handoff nightmares✅ Built-in workflow

IntlPull (shameless plug) is built specifically for this. It integrates with your Git workflow, catches missing translations in CI, and supports OTA updates so you don't need to redeploy for translation fixes.


Frequently Asked Questions

Why is my t() function returning the key instead of the translation?

Your translation key doesn't exist in the JSON file or namespace. Check for typos in the key, verify the JSON structure matches your code's expected path, ensure you're using the correct namespace, and validate that the JSON file is being loaded. This is the most common i18n issue.

Why do my translations work in English but not other languages?

The translation key is missing from other locale files. When you add a new key to en.json, you must also add it to es.json, de.json, etc. Use a TMS like IntlPull to automatically detect missing translations, or set up fallback locales in your config.

How do I fix Next.js i18n hydration mismatch errors?

Server and client are rendering different content. This usually happens with date/time formatting (server is UTC, client is local), using client hooks in Server Components, or locale mismatch between server and client. Specify timezones explicitly, use correct hooks for component type, and ensure locale is passed correctly.

Why doesn't my Next.js middleware detect the locale?

Your middleware matcher pattern isn't matching your routes. Ensure middleware.ts is in the project root (not in /app), your matcher includes the routes you need, and the locales array matches your supported languages. Add console.log to middleware to debug what's being detected.

How do I debug "Could not find IntlProvider" errors?

Your component isn't wrapped with the i18n provider. Check that NextIntlClientProvider (or your library's provider) is in your app/[locale]/layout.tsx and wraps all child components. The provider must receive messages and the current locale.

Why does my JSON file cause "Unexpected token" errors?

Your JSON syntax is invalid. Common issues: trailing commas, single quotes instead of double quotes, unescaped quotes in values, or missing commas. Run your JSON through a validator like python -m json.tool to find the exact error location.

How do I make translations work in Next.js Server Components?

Use server-side translation functions. In next-intl, use getTranslations() from next-intl/server in Server Components. The regular useTranslations() hook works in next-intl due to context detection, but for react-i18next you need explicit server-side functions.

Why do my translations break in production but work in development?

Dynamic imports may fail at build time. Ensure all locale files exist before build, use static imports or verified dynamic imports, and clear the .next cache before building. Production builds optimize imports differently than dev mode.

How do I prevent locale from being lost when navigating?

Use your i18n library's Link component instead of Next.js Link. In next-intl, configure navigation exports in i18n/navigation.ts and import Link from there. Alternatively, manually include locale in href: /${locale}/about.

How do I test that all translations exist?

Add CI checks that validate JSON and detect missing keys. Validate JSON syntax with a linter, compare keys across locale files, and run integration tests that verify pages don't display raw translation keys. IntlPull's CLI provides automated missing translation detection.


Summary

Most Next.js i18n issues fall into predictable categories:

  1. Key typos → Use TypeScript types and autocomplete
  2. Invalid JSON → Validate in CI
  3. Missing locale data → Use a TMS with sync checking
  4. Wrong hook/context → Follow Server vs Client Component rules
  5. Middleware issues → Check file placement and matcher config

The ecosystem has gotten much better. With App Router and modern libraries like next-intl, i18n is significantly more reliable than it was with Pages Router. But it's still code, and code has bugs.

Build defensively: type-safe keys, CI validation, and good tooling will save you hours of debugging.


Need help managing translations at scale? Start free with IntlPull — automatic missing translation detection, AI translation, and seamless next-intl integration.

Tags
nextjs
i18n
troubleshooting
debugging
next-intl
react-i18next
localization
2026
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.