IntlPull
Tutorial
12 min read

Next.js Internationalization: Complete Setup Guide with App Router (2026)

Step-by-step tutorial for adding internationalization to Next.js 14+ applications using the App Router, next-intl, and best practices for 2026.

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

Next.js 14+ with the App Router brings powerful new capabilities for internationalization.

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

  • Next.js 14 or later
  • Basic understanding of the App Router
  • Node.js 18+

Step 1: Install next-intl

Terminal
npm install next-intl

Step 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.js

Step 3: Configure next-intl

Create i18n.ts in your project root:

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

Step 4: Create Middleware

TypeScript
1// middleware.ts
2import createMiddleware from 'next-intl/middleware';
3
4export default createMiddleware({
5  locales: ['en', 'es', 'fr', 'de', 'ja'],
6  defaultLocale: 'en',
7  localePrefix: 'as-needed' // or 'always' for /en/about style
8});
9
10export const config = {
11  matcher: ['/', '/(de|en|es|fr|ja)/:path*']
12};

Step 5: Update next.config.js

JavaScript
1const withNextIntl = require('next-intl/plugin')();
2
3module.exports = withNextIntl({
4  // Your existing Next.js config
5});

Step 6: Create Layout with Locale

TSX
1// app/[locale]/layout.tsx
2import { NextIntlClientProvider } from 'next-intl';
3import { getMessages } from 'next-intl/server';
4
5export default async function LocaleLayout({
6  children,
7  params: { locale }
8}: {
9  children: React.ReactNode;
10  params: { locale: string };
11}) {
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}

Step 7: Create Translation Files

JSON
1// messages/en.json
2{
3  "home": {
4    "title": "Welcome to our app",
5    "description": "The best solution for your needs",
6    "cta": "Get Started"
7  },
8  "navigation": {
9    "home": "Home",
10    "about": "About",
11    "contact": "Contact"
12  }
13}
JSON
1// messages/es.json
2{
3  "home": {
4    "title": "Bienvenido a nuestra aplicación",
5    "description": "La mejor solución para tus necesidades",
6    "cta": "Comenzar"
7  },
8  "navigation": {
9    "home": "Inicio",
10    "about": "Acerca de",
11    "contact": "Contacto"
12  }
13}

Step 8: Use Translations in Pages

Server Components

TSX
1// app/[locale]/page.tsx
2import { getTranslations } from 'next-intl/server';
3
4export default async function HomePage() {
5  const t = await getTranslations('home');
6
7  return (
8    <main>
9      <h1>{t('title')}</h1>
10      <p>{t('description')}</p>
11      <button>{t('cta')}</button>
12    </main>
13  );
14}

Client Components

TSX
1'use client';
2
3import { useTranslations } from 'next-intl';
4
5export function LanguageSwitcher() {
6  const t = useTranslations('navigation');
7  // ... language switching logic
8}

Step 9: Add Language Switcher

TSX
1'use client';
2
3import { useLocale } from 'next-intl';
4import { useRouter, usePathname } from 'next/navigation';
5
6const locales = ['en', 'es', 'fr', 'de', 'ja'];
7
8export function LanguageSwitcher() {
9  const locale = useLocale();
10  const router = useRouter();
11  const pathname = usePathname();
12
13  const switchLocale = (newLocale: string) => {
14    const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
15    router.push(newPath);
16  };
17
18  return (
19    <select value={locale} onChange={(e) => switchLocale(e.target.value)}>
20      {locales.map((loc) => (
21        <option key={loc} value={loc}>
22          {loc.toUpperCase()}
23        </option>
24      ))}
25    </select>
26  );
27}

Step 10: SEO Optimization

Generate Static Params

TSX
1// app/[locale]/page.tsx
2export function generateStaticParams() {
3  return [
4    { locale: 'en' },
5    { locale: 'es' },
6    { locale: 'fr' },
7    { locale: 'de' },
8    { locale: 'ja' },
9  ];
10}

Metadata with Translations

TSX
1import { getTranslations } from 'next-intl/server';
2
3export async function generateMetadata({ params: { locale } }) {
4  const t = await getTranslations({ locale, namespace: 'home' });
5
6  return {
7    title: t('meta.title'),
8    description: t('meta.description'),
9    alternates: {
10      canonical: `https://example.com/${locale}`,
11      languages: {
12        'en': 'https://example.com/en',
13        'es': 'https://example.com/es',
14        'fr': 'https://example.com/fr',
15      },
16    },
17  };
18}

Hreflang Tags

TSX
1// app/[locale]/layout.tsx
2export default function Layout({ children, params: { locale } }) {
3  return (
4    <html lang={locale}>
5      <head>
6        <link rel="alternate" hrefLang="en" href="https://example.com/en" />
7        <link rel="alternate" hrefLang="es" href="https://example.com/es" />
8        <link rel="alternate" hrefLang="x-default" href="https://example.com" />
9      </head>
10      <body>{children}</body>
11    </html>
12  );
13}

Advanced: Pluralization and Formatting

Plurals

JSON
1{
2  "cart": {
3    "items": "{count, plural, =0 {No items} =1 {1 item} other {# items}}"
4  }
5}
TSX
t('cart.items', { count: 5 }) // "5 items"

Rich Text

JSON
{
  "welcome": "Hello, <bold>{name}</bold>!"
}
TSX
1t.rich('welcome', {
2  name: 'John',
3  bold: (chunks) => <strong>{chunks}</strong>
4})

Dates and Numbers

TSX
1import { useFormatter } from 'next-intl';
2
3function PriceDisplay({ amount, date }) {
4  const format = useFormatter();
5
6  return (
7    <div>
8      <p>{format.number(amount, { style: 'currency', currency: 'USD' })}</p>
9      <p>{format.dateTime(date, { dateStyle: 'full' })}</p>
10    </div>
11  );
12}

Managing Translations at Scale

Manually managing JSON files gets painful fast. Here's how IntlPull helps:

Bidirectional Sync

Terminal
# Watch for changes and sync automatically
npx @intlpullhq/cli sync --watch

This keeps your local files in sync with IntlPull in real-time.

Upload & Download

Terminal
1# Pull latest from IntlPull
2npx @intlpullhq/cli download --output ./messages
3
4# Push new strings
5npx @intlpullhq/cli upload --source ./messages/en.json

AI 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.

TypeScript
1// middleware.ts
2export default createMiddleware({
3  localePrefix: 'always', // Prevents mismatches
4});

Issue: Flash of Wrong Language

Solution: Use cookies for persistence:

TypeScript
1export default createMiddleware({
2  localeDetection: true,
3  // Stores preference in cookie
4});

Issue: Missing Translations in Production

Solution: Set up fallback handling:

TypeScript
1getRequestConfig(async ({ locale }) => ({
2  messages: {
3    ...(await import(`./messages/en.json`)).default, // Fallback
4    ...(await import(`./messages/${locale}.json`)).default,
5  },
6}));

Performance Tips

  1. Use static generation where possible
  2. Code-split large translation namespaces
  3. Cache translations with ISR
  4. Lazy load non-critical translations

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.

Tags
nextjs
i18n
next-intl
app-router
tutorial
react
2026
IntlPull Team
IntlPull Team
Engineering

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