Internationalization (i18n) in Next.js with App Router

Table of Contents

  1. Introduction
  2. Why Internationalization Matters
  3. Built-in i18n Support in Next.js
  4. Step-by-Step Setup for i18n in App Router
  5. Routing and Locale Detection
  6. Handling Translations with next-intl
  7. Dynamic Locale Loading
  8. Nested Layouts and Localized Content
  9. SEO Best Practices for i18n
  10. Handling Forms, Errors, and Dates in Locales
  11. Tips for Real-World Multilingual Projects
  12. Conclusion

1. Introduction

Internationalization (i18n) is crucial for applications with a global user base. With Next.js App Router, i18n capabilities are more seamless and powerful, allowing for:

  • Per-locale routing (e.g., /en, /fr, /de)
  • Dynamic translation loading
  • Localized content components
  • SEO enhancements with localized metadata

In this guide, we’ll walk through a practical setup and cover how to build a robust multilingual app.


2. Why Internationalization Matters

Here are some key reasons to invest in i18n:

  • User experience: Visitors are more likely to engage with content in their native language.
  • SEO: Localized URLs and meta tags help rank content better in regional search engines.
  • Market reach: Expand your app’s reach across geographies.

3. Built-in i18n Support in Next.js

Next.js offers built-in i18n support in the configuration via next.config.js:

jsCopyEdit// next.config.js
module.exports = {
  i18n: {
    locales: ['en', 'fr', 'de'],
    defaultLocale: 'en',
    localeDetection: true,
  },
};

This does not handle translations — it only provides locale-based routing and detection. For managing translations, we’ll use the next-intl package.


4. Step-by-Step Setup for i18n in App Router

1. Install Dependencies

bashCopyEditnpm install next-intl

2. Set Up the File Structure

pgsqlCopyEditapp/
  [locale]/
    layout.tsx
    page.tsx
  i18n/
    en.json
    fr.json
    de.json

3. Create Your Locale Files

app/i18n/en.json

jsonCopyEdit{
  "greeting": "Hello",
  "login": "Login",
  "welcome": "Welcome to our app!"
}

Repeat for fr.json, de.json, etc.


5. Routing and Locale Detection

With the App Router, you can use dynamic segments to scope content per locale:

tsxCopyEdit// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { notFound } from 'next/navigation';

export default async function LocaleLayout({ children, params: { locale } }) {
  let messages;
  try {
    messages = (await import(`../../i18n/${locale}.json`)).default;
  } catch (error) {
    notFound();
  }

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider locale={locale} messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

And use localized URLs like:

  • /en
  • /fr
  • /de

6. Handling Translations with next-intl

To use a translation string:

tsxCopyEdit// app/[locale]/page.tsx
'use client';

import { useTranslations } from 'next-intl';

export default function HomePage() {
  const t = useTranslations();

  return <h1>{t('welcome')}</h1>;
}

You get full type-safety and fallback handling with next-intl.


7. Dynamic Locale Loading

The locale-specific layout already loads the necessary JSON dynamically using:

tsCopyEditawait import(`../../i18n/${locale}.json`);

This makes the app scalable and optimized for only loading relevant translation payloads.

You can extend this logic to also load:

  • Date formatting rules
  • Time zone preferences
  • Pluralization rules

8. Nested Layouts and Localized Content

Since layouts can be nested per route, you can have a consistent layout while swapping out translated content:

tsxCopyEdit// app/[locale]/dashboard/page.tsx
'use client';

import { useTranslations } from 'next-intl';

export default function Dashboard() {
  const t = useTranslations('dashboard');

  return <h2>{t('title')}</h2>;
}

Your translation file:

jsonCopyEdit{
  "dashboard": {
    "title": "Your Dashboard"
  }
}

9. SEO Best Practices for i18n

Make sure to update:

1. <html lang="..."> dynamically:

Done in the layout with:

tsxCopyEdit<html lang={locale}>

2. Use the Metadata API to localize meta tags:

tsCopyEdit// app/[locale]/page.tsx
export async function generateMetadata({ params }) {
  const messages = (await import(`../../i18n/${params.locale}.json`)).default;
  return {
    title: messages.meta.title,
    description: messages.meta.description
  };
}

3. Use <link rel="alternate" hreflang="..."> for alternate locales.

This helps Google understand which page is for which language.


10. Handling Forms, Errors, and Dates in Locales

Use Intl.DateTimeFormat for date/time formatting:

tsCopyEditconst formattedDate = new Intl.DateTimeFormat(locale, {
  dateStyle: 'long'
}).format(new Date());

Translate error messages, buttons, and form placeholders by using structured keys:

jsonCopyEdit{
  "form": {
    "username": "Username",
    "password": "Password",
    "submit": "Submit",
    "errors": {
      "required": "This field is required",
      "invalid": "Invalid value"
    }
  }
}

11. Tips for Real-World Multilingual Projects

  • Use consistent translation keys with nested objects.
  • Maintain one master language file and extract others from it using tools like i18next-parser.
  • Add fallbacks for missing translations.
  • Use static analysis or linters to detect untranslated keys.
  • Centralize translation logic in a utility or wrapper if needed.

12. Conclusion

Internationalization with Next.js and the App Router is powerful, flexible, and scalable. With the help of next-intl and dynamic routing, you can deliver localized experiences across the world — with full SEO benefits and optimal performance.

Multilingual support isn’t just a “nice-to-have” anymore. It’s a crucial step for user growth, global trust, and long-term success.