VercelVercel

How to internationalise error pages in Next.js App Router

Learn how to show translated error messages in Next.js App Router when using the app/[lang]/ pattern for internationalisation.

5 min read
Last updated January 21, 2026

Error pages like error.tsx and not-found.tsx don't inherit the locale from your app/[lang]/ pattern. Users see English error messages regardless of their language setting.

This guide shows you how to localise both files using the correct patterns for each.

Different error files have different constraints:

FileComponent typeHow to access locale
error.tsxClient Component (required)useParams() hook
not-found.tsxServer Componentheaders() referer

error.tsx must be a Client Component because React error boundaries require client-side JavaScript. If your main dictionary loader uses async/await and runs only on the server, it won't work in a Client Component.

For not-found.tsx, you can use a Server Component, but it doesn't receive the params prop directly. You need to extract the locale from the request headers.

In Next.js 16, dynamic route params are typed as string because their values aren't known until runtime. Users can type any URL into the address bar. The Next.js docs recommend using runtime validation to handle invalid params:

i18n-config.ts
import { notFound } from "next/navigation";
export const i18n = {
defaultLocale: "en",
locales: ["en", "de", "cs"],
} as const;
export type Locale = (typeof i18n)["locales"][number];
export function isValidLocale(value: string | undefined): value is Locale {
return value !== undefined && i18n.locales.includes(value as Locale);
}
export function assertValidLocale(value: string): asserts value is Locale {
if (!isValidLocale(value)) notFound();
}

Use assertValidLocale() early in your layout and pages. Invalid locales trigger a 404 instead of crashing your app:

app/[lang]/layout.tsx
export default async function Root(props: {
children: React.ReactNode;
params: Promise<{ lang: string }>;
}) {
const { lang } = await props.params;
assertValidLocale(lang);
return (
<html lang={lang}>
<body>{props.children}</body>
</html>
);
}

Most dictionary loaders are async and server-only. That won't work in a Client Component. Create a separate sync loader with just the error strings:

get-error-dictionary.ts
const errorDictionaries = {
en: { title: "Something went wrong!", "try-again": "Try again" },
de: { title: "Etwas ist schief gelaufen!", "try-again": "Erneut versuchen" },
cs: { title: "Něco se pokazilo!", "try-again": "Zkusit znovu" },
};
export type ErrorDictionary = (typeof errorDictionaries)["en"];
export function getErrorDictionary(locale: string): ErrorDictionary {
return (
errorDictionaries[locale as keyof typeof errorDictionaries] ??
errorDictionaries.en
);
}

Keep this dictionary minimal. Error pages should load fast, and since this dictionary is bundled into the client, every byte counts.

The error.tsx file handles runtime errors. It must be a Client Component:

app/[lang]/error.tsx
"use client";
import { useEffect } from "react";
import { useParams } from "next/navigation";
import { getErrorDictionary } from "../../get-error-dictionary";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const params = useParams<{ lang: string }>();
const dictionary = getErrorDictionary(params?.lang ?? "en");
useEffect(() => {
console.error(error);
}, [error]);
return (
<div>
<h2>{dictionary.title}</h2>
<button onClick={() => reset()}>{dictionary["try-again"]}</button>
</div>
);
}

The params?.lang ?? "en" fallback: useParams() can return null in error boundaries, so always provide a default.

Skip assertValidLocale() here because error.tsx is a fallback. The dictionary loader already handles unknown locales by falling back to English.

The not-found.tsx file can be a Server Component, so you can use your existing async dictionary loader. Add the not-found key to your JSON dictionaries:

dictionaries/en.json
{
...
"not-found": {
"title": "Page Not Found",
"description": "Could not find the requested resource.",
"back-home": "Return Home"
}
}
dictionaries/de.json
{
...
"not-found": {
"title": "Seite nicht gefunden",
"description": "Die angeforderte Ressource konnte nicht gefunden werden.",
"back-home": "Zurück zur Startseite"
}
}

First, add a helper function to extract the locale from URLs:

i18n-config.ts
export function getFirstPathSegment(url: string): string | undefined {
try {
return new URL(url).pathname.split("/")[1] || undefined;
} catch {
return undefined;
}
}

Then use it in your not-found page to extract the locale from the referer header:

app/[lang]/not-found.tsx
import Link from "next/link";
import { headers } from "next/headers";
import { getDictionary } from "../../get-dictionary";
import { i18n, isValidLocale, getFirstPathSegment } from "../../i18n-config";
export default async function NotFound() {
const headersList = await headers();
const referer = headersList.get("referer") || "";
const urlLocale = getFirstPathSegment(referer);
const locale = isValidLocale(urlLocale) ? urlLocale : i18n.defaultLocale;
const dictionary = (await getDictionary(locale))["not-found"];
return (
<div>
<h2>{dictionary.title}</h2>
<p>{dictionary.description}</p>
<Link href={`/${locale}`}>{dictionary["back-home"]}</Link>
</div>
);
}

The locale detection from the referer header works because the user navigated from a page within your app. If the referer is empty or contains an invalid locale, it falls back to the default.

To trigger your localised 404 for unknown paths within a locale, add a catch-all route:

app/[lang]/[...slug]/page.tsx
import { notFound } from "next/navigation";
export default function CatchAllPage() {
notFound();
}

This ensures /en/nonexistent shows your localised not-found page instead of a generic 404. Without this catch-all, Next.js might render a default error page that bypasses your translations.

This catch-all only triggers when no other route matches. If you have routes like app/[lang]/blog/[slug]/page.tsx, they match first. The catch-all handles everything else.

Verify everything works:

  • Visit /en/nonexistent → English "Page Not Found"
  • Visit /de/nonexistent → German "Seite nicht gefunden"
  • Visit /invalid/anything → Default locale 404 (via assertValidLocale)
  • Trigger a runtime error → Localised error message with retry button

global-error.tsx handles errors in the root layout. It completely replaces your layout tree, including app/[lang]/layout.tsx. That means it has no access to the locale param.

For most applications, you don't need global-error.tsx. The segment-level error.tsx inside app/[lang]/ handles errors within your localised routes, which covers the majority of cases. global-error.tsx only triggers when the root layout itself throws an error.

Files to create:

  • app/[lang]/error.tsx - Client Component with useParams()
  • app/[lang]/not-found.tsx - Server Component with headers() referer
  • app/[lang]/[...slug]/page.tsx - Catch-all to trigger localised 404
  • get-error-dictionary.ts - Sync dictionary loader for Client Components

Key patterns:

  • Runtime validation with assertValidLocale() to narrow string to Locale
  • Separate sync dictionary for Client Components
  • Referer header extraction for locale detection in not-found pages
  • Null-safe useParams() access with fallback

Was this helpful?

supported.

Read related documentation

No related documentation available.

Explore more guides

No related guides available.