Next.js Internationalization: Complete Setup Guide with App Router
Step-by-step tutorial for adding internationalization to Next.js 14+ applications using the App Router, next-intl, and best practices.
Introduction
Next.js 14+ with the App Router brings powerful new capabilities for internationalization. This guide shows you how to set up a fully internationalized Next.js application with proper routing, SEO, and translation management.
Prerequisites
Step 1: Install next-intl
npm install next-intlStep 2: Project Structure
/app
/[locale]
/layout.tsx
/page.tsx
/about
/page.tsx
/messages
/en.json
/es.json
/fr.json
/i18n.ts
/middleware.ts
/next.config.jsStep 3: Configure next-intl
Create i18n.ts in your project root:
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`./messages/${locale}.json`)).default
}));Step 4: Create Middleware
// middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
locales: ['en', 'es', 'fr', 'de', 'ja'],
defaultLocale: 'en',
localePrefix: 'as-needed' // or 'always' for /en/about style
});
export const config = {
matcher: ['/', '/(de|en|es|fr|ja)/:path*']
};Step 5: Update next.config.js
const withNextIntl = require('next-intl/plugin')();
module.exports = withNextIntl({
// Your existing Next.js config
});Step 6: Create Layout with Locale
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
export default async function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}Step 7: Create Translation Files
// messages/en.json
{
"home": {
"title": "Welcome to our app",
"description": "The best solution for your needs",
"cta": "Get Started"
},
"navigation": {
"home": "Home",
"about": "About",
"contact": "Contact"
}
}// messages/es.json
{
"home": {
"title": "Bienvenido a nuestra aplicación",
"description": "La mejor solución para tus necesidades",
"cta": "Comenzar"
},
"navigation": {
"home": "Inicio",
"about": "Acerca de",
"contact": "Contacto"
}
}Step 8: Use Translations in Pages
Server Components
// app/[locale]/page.tsx
import { useTranslations } from 'next-intl';
export default function HomePage() {
const t = useTranslations('home');
return (
<main>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
<button>{t('cta')}</button>
</main>
);
}Client Components
'use client';
import { useTranslations } from 'next-intl';
export function LanguageSwitcher() {
const t = useTranslations('navigation');
// ... language switching logic
}Step 9: Add Language Switcher
'use client';
import { useLocale } from 'next-intl';
import { useRouter, usePathname } from 'next/navigation';
const locales = ['en', 'es', 'fr', 'de', 'ja'];
export function LanguageSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const switchLocale = (newLocale: string) => {
const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
router.push(newPath);
};
return (
<select value={locale} onChange={(e) => switchLocale(e.target.value)}>
{locales.map((loc) => (
<option key={loc} value={loc}>
{loc.toUpperCase()}
</option>
))}
</select>
);
}Step 10: SEO Optimization
Generate Static Params
// app/[locale]/page.tsx
export function generateStaticParams() {
return [
{ locale: 'en' },
{ locale: 'es' },
{ locale: 'fr' },
{ locale: 'de' },
{ locale: 'ja' },
];
}Metadata with Translations
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({ params: { locale } }) {
const t = await getTranslations({ locale, namespace: 'home' });
return {
title: t('meta.title'),
description: t('meta.description'),
alternates: {
canonical: `https://example.com/${locale}`,
languages: {
'en': 'https://example.com/en',
'es': 'https://example.com/es',
'fr': 'https://example.com/fr',
},
},
};
}Hreflang Tags
// app/[locale]/layout.tsx
export default function Layout({ children, params: { locale } }) {
return (
<html lang={locale}>
<head>
<link rel="alternate" hrefLang="en" href="https://example.com/en" />
<link rel="alternate" hrefLang="es" href="https://example.com/es" />
<link rel="alternate" hrefLang="x-default" href="https://example.com" />
</head>
<body>{children}</body>
</html>
);
}Advanced: Pluralization and Formatting
Plurals
{
"cart": {
"items": "{count, plural, =0 {No items} =1 {1 item} other {# items}}"
}
}t('cart.items', { count: 5 }) // "5 items"Rich Text
{
"welcome": "Hello, <bold>{name}</bold>!"
}t.rich('welcome', {
name: 'John',
bold: (chunks) => <strong>{chunks}</strong>
})Dates and Numbers
import { useFormatter } from 'next-intl';
function PriceDisplay({ amount, date }) {
const format = useFormatter();
return (
<div>
<p>{format.number(amount, { style: 'currency', currency: 'USD' })}</p>
<p>{format.dateTime(date, { dateStyle: 'full' })}</p>
</div>
);
}Managing Translations at Scale
Manually managing JSON files gets painful fast. Here's how IntlPull helps:
Automatic String Extraction
# Scan your codebase for hardcoded strings
npx intlpull scan --framework nextjsThis automatically finds and extracts strings like:
// Before
<h1>Welcome to our app</h1>
// After
<h1>{t('home.title')}</h1>Sync Translations
# Pull latest from IntlPull
npx intlpull pull --output ./messages
# Push new strings
npx intlpull push --source ./messages/en.jsonAI Translation
When you add a new string, IntlPull automatically translates it to all your configured languages using context-aware AI.
Common Issues and Solutions
Issue: Hydration Mismatch
Solution: Ensure your middleware and locale detection are consistent.
// middleware.ts
export default createMiddleware({
localePrefix: 'always', // Prevents mismatches
});Issue: Flash of Wrong Language
Solution: Use cookies for persistence:
export default createMiddleware({
localeDetection: true,
// Stores preference in cookie
});Issue: Missing Translations in Production
Solution: Set up fallback handling:
getRequestConfig(async ({ locale }) => ({
messages: {
...(await import(`./messages/en.json`)).default, // Fallback
...(await import(`./messages/${locale}.json`)).default,
},
}));Performance Tips
Conclusion
Next.js with next-intl provides a powerful, type-safe way to build internationalized applications. Combined with a translation management system like IntlPull, you can ship localized features faster than ever.
Ready to simplify your Next.js i18n? Try IntlPull free and automate your translation workflow.