Back to Blog
Tutorial
Featured

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.

IntlPull Team
IntlPull Team
Engineering
November 28, 202412 min read

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

    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:

    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 nextjs

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

    // 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

  • Use static generation where possible
  • Code-split large translation namespaces
  • Cache translations with ISR
  • 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.

    nextjs
    i18n
    next-intl
    app-router
    tutorial
    react
    Share:

    Ready to simplify your i18n workflow?

    Start managing translations with IntlPull. Free tier included.