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:

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

npm install next-intl

2. Set Up the File Structure

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

3. Create Your Locale Files

app/i18n/en.json

{
"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:

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

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

await 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:

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

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

9. SEO Best Practices for i18n

Make sure to update:

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

Done in the layout with:

<html lang={locale}>

2. Use the Metadata API to localize meta tags:

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

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

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

{
"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.