How to Implement Internationalization (i18n) in a Next.js App with App Router

How to Implement Internationalization (i18n) in a Next.js App with App Router

by | May 14, 2026 | Uncategorized | 0 comments

Why Internationalization in Next.js App Router Matters in 2026

If you are building a website or a SaaS product that serves users in multiple countries, internationalization (i18n) is not optional. It is a requirement. The Next.js App Router, introduced as the default paradigm since Next.js 13 and refined through versions 14, 15, and now 16, provides a powerful foundation for building multilingual applications. But the official docs only scratch the surface.

In this guide, we go deep. We will walk you through every step of setting up internationalization in a Next.js App Router project, from locale detection and routing strategies to translation file management and SEO optimization. Whether you are starting a new project or retrofitting an existing one, this article will give you everything you need.

What You Will Learn

  • How the Next.js App Router handles internationalization differently from the Pages Router
  • How to structure your project for multi-language support
  • How to implement locale detection using middleware
  • Two routing strategies: sub-path and domain-based
  • How to manage translation files at scale
  • How to integrate the popular next-intl library
  • SEO best practices for multilingual Next.js sites

App Router vs. Pages Router: What Changed for i18n?

If you previously used next-i18next with the Pages Router, you will notice significant differences with the App Router. The built-in i18n configuration in next.config.js that worked with the Pages Router does not work with the App Router. This is a critical distinction that trips up many developers.

With the App Router, internationalization is handled through:

  1. Middleware for locale detection and redirection
  2. Dynamic route segments (e.g., [locale]) for sub-path routing
  3. Server Components that can load translations on the server
  4. Third-party libraries like next-intl for a complete solution
Feature Pages Router App Router
Built-in i18n config Yes No
Locale detection Automatic via config Custom middleware
Recommended library next-i18next next-intl
Server Components support No Yes
Routing strategy Sub-path or domain Sub-path or domain (manual setup)

Step 1: Project Setup and Folder Structure

Start by creating a new Next.js project or opening your existing one. We will use Next.js 15 or 16 for this guide.

npx create-next-app@latest my-i18n-app
cd my-i18n-app

Your folder structure for internationalization should look like this:

my-i18n-app/
├── messages/
│   ├── en.json
│   ├── fr.json
│   └── de.json
├── src/
│   ├── app/
│   │   └── [locale]/
│   │       ├── layout.tsx
│   │       ├── page.tsx
│   │       └── about/
│   │           └── page.tsx
│   ├── middleware.ts
│   └── i18n/
│       ├── request.ts
│       └── routing.ts
├── next.config.js
└── package.json

The key element here is the [locale] dynamic segment inside the app directory. Every route in your application will be nested under this segment, which allows Next.js to serve content based on the user’s selected language.

Step 2: Define Your Supported Locales

Create a configuration file to define your supported locales and the default locale. This single source of truth will be referenced throughout your application.

// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['en', 'fr', 'de', 'es'],
  defaultLocale: 'en'
});

You can also create a simpler version without next-intl if you prefer a manual approach:

// src/i18n/config.ts
export const locales = ['en', 'fr', 'de', 'es'] as const;
export const defaultLocale = 'en';
export type Locale = (typeof locales)[number];

Step 3: Set Up Middleware for Locale Detection

Middleware is the backbone of internationalization in the Next.js App Router. It runs before every request and is responsible for:

  • Detecting the user’s preferred language from the Accept-Language header
  • Redirecting users to the correct locale sub-path
  • Handling cases where no locale is present in the URL

Option A: Using next-intl middleware (recommended)

// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  matcher: ['/', '/(fr|de|en|es)/:path*']
};

Option B: Custom middleware without a library

// src/middleware.ts
import { NextRequest, NextResponse } from 'next/server';

const locales = ['en', 'fr', 'de', 'es'];
const defaultLocale = 'en';

function getLocale(request: NextRequest): string {
  const acceptLanguage = request.headers.get('accept-language');
  if (!acceptLanguage) return defaultLocale;

  const preferred = acceptLanguage
    .split(',')
    .map(lang => lang.split(';')[0].trim().substring(0, 2));

  for (const lang of preferred) {
    if (locales.includes(lang)) return lang;
  }
  return defaultLocale;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  const pathnameHasLocale = locales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (pathnameHasLocale) return;

  const locale = getLocale(request);
  request.nextUrl.pathname = `/${locale}${pathname}`;
  return NextResponse.redirect(request.nextUrl);
}

export const config = {
  matcher: ['/((?!_next|api|favicon.ico).*)'],
};

The custom middleware approach gives you full control but requires more maintenance. For most projects, we recommend next-intl as it handles edge cases you might not think of initially.

Step 4: Create Translation Files

Translation files are simple JSON files stored in a messages directory at the root of your project. Each file corresponds to one locale.

// messages/en.json
{
  "HomePage": {
    "title": "Welcome to our platform",
    "description": "Build amazing products with our tools.",
    "cta": "Get Started"
  },
  "Navigation": {
    "home": "Home",
    "about": "About",
    "pricing": "Pricing",
    "contact": "Contact"
  }
}
// messages/fr.json
{
  "HomePage": {
    "title": "Bienvenue sur notre plateforme",
    "description": "Créez des produits incroyables avec nos outils.",
    "cta": "Commencer"
  },
  "Navigation": {
    "home": "Accueil",
    "about": "À propos",
    "pricing": "Tarifs",
    "contact": "Contact"
  }
}

Translation File Management Tips

  • Namespace your translations by page or component to keep files organized
  • Use nested keys instead of flat keys for better readability
  • Keep a consistent structure across all locale files
  • Consider using a translation management system (TMS) like Crowdin or Phrase if you have more than 5 languages
  • Validate your JSON files in CI/CD to catch missing keys early

Step 5: Load Translations in Server Components

One of the biggest advantages of the App Router is the ability to load translations on the server, which means zero JavaScript shipped to the client for static content.

With next-intl

First, install the library:

npm install next-intl

Create the request configuration:

// src/i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;

  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale;
  }

  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default
  };
});

Update your next.config.js:

// next.config.js
import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin();

const nextConfig = {};

export default withNextIntl(nextConfig);

The Layout File

// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';

export default async function LocaleLayout({
  children,
  params
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  const { locale } = await params;

  if (!routing.locales.includes(locale as any)) {
    notFound();
  }

  const messages = await getMessages();

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

Using Translations in a Page

// src/app/[locale]/page.tsx
import { useTranslations } from 'next-intl';

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

  return (
    <main>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
      <button>{t('cta')}</button>
    </main>
  );
}

Step 6: Routing Strategies for Multilingual Next.js Apps

There are two primary routing strategies for internationalization in Next.js. Your choice depends on your business requirements and target audience.

Sub-path Routing

This is the most common approach. Each locale gets its own URL prefix:

  • example.com/en/about
  • example.com/fr/about
  • example.com/de/about

Pros: Easy to set up, works with a single domain, great for SEO with hreflang tags.

Cons: URLs are slightly longer, and some users may not expect a language prefix.

Domain-based Routing

Each locale is served from a different domain or subdomain:

  • example.com (English)
  • example.fr (French)
  • example.de (German)

Pros: Clean URLs, can improve local SEO in specific regions.

Cons: More complex DNS and hosting setup, higher cost for multiple domains.

Criteria Sub-path Routing Domain-based Routing
Setup complexity Low High
SEO control Good Excellent
Hosting cost Single deployment Multiple domains/certs
Best for SaaS, documentation sites Regional e-commerce, large brands

For most SaaS products and standard websites, sub-path routing is the recommended approach. It is simpler, cheaper, and perfectly effective for SEO when combined with proper hreflang tags.

Step 7: Add a Language Switcher

A language switcher allows users to manually change their locale. Here is a clean implementation:

// src/components/LanguageSwitcher.tsx
'use client';

import { usePathname, useRouter } from 'next/navigation';
import { useLocale } from 'next-intl';

const languages = [
  { code: 'en', label: 'English' },
  { code: 'fr', label: 'Français' },
  { code: 'de', label: 'Deutsch' },
  { code: 'es', label: 'Español' },
];

export default function LanguageSwitcher() {
  const locale = useLocale();
  const router = useRouter();
  const pathname = usePathname();

  const handleChange = (newLocale: string) => {
    const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
    router.push(newPath);
  };

  return (
    <select
      value={locale}
      onChange={(e) => handleChange(e.target.value)}
    >
      {languages.map((lang) => (
        <option key={lang.code} value={lang.code}>
          {lang.label}
        </option>
      ))}
    </select>
  );
}

Step 8: SEO Optimization for Multilingual Pages

Search engines need clear signals about which language version of a page to show to which users. Here is what you need to implement:

Hreflang Tags

Add hreflang tags to the <head> of every page. This tells Google which URL corresponds to which language.

// src/app/[locale]/layout.tsx (inside the head)
export async function generateMetadata({ params }: { params: { locale: string } }) {
  const { locale } = await params;

  return {
    alternates: {
      canonical: `https://example.com/${locale}`,
      languages: {
        en: 'https://example.com/en',
        fr: 'https://example.com/fr',
        de: 'https://example.com/de',
        es: 'https://example.com/es',
      },
    },
  };
}

SEO Checklist for Multilingual Next.js Apps

  • Set the lang attribute on the <html> tag for each locale
  • Add hreflang tags pointing to all language versions of each page
  • Include an x-default hreflang for the fallback language
  • Use translated URLs (slugs) when possible for better local relevance
  • Generate a multilingual sitemap with all locale variations
  • Translate meta titles and meta descriptions for each locale

Step 9: Handle Dynamic Content and Dates

Internationalization is not just about translating text. You also need to format dates, numbers, and currencies according to the user’s locale.

// Formatting dates
import { useFormatter } from 'next-intl';

export default function PriceDisplay() {
  const format = useFormatter();

  const formattedDate = format.dateTime(new Date(), {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });

  const formattedPrice = format.number(29.99, {
    style: 'currency',
    currency: 'EUR'
  });

  return (
    <div>
      <p>{formattedDate}</p>
      <p>{formattedPrice}</p>
    </div>
  );
}

Step 10: Static Generation with Multiple Locales

If you want to statically generate pages for all locales at build time, use generateStaticParams:

// src/app/[locale]/page.tsx
export function generateStaticParams() {
  return [
    { locale: 'en' },
    { locale: 'fr' },
    { locale: 'de' },
    { locale: 'es' },
  ];
}

This ensures that all locale versions of a page are pre-rendered during the build process, resulting in fast load times and better SEO performance.

Common Pitfalls to Avoid

Based on our experience building multilingual Next.js applications at Box Software, here are the mistakes we see most often:

  1. Using the Pages Router i18n config with App Router. It simply does not work. You must use middleware and dynamic segments instead.
  2. Forgetting to validate locale params. Always check if the locale in the URL is valid and return a 404 if not.
  3. Hardcoding strings in components. Even for a “small” project, extract all strings into translation files from day one. Retrofitting i18n later is painful.
  4. Missing hreflang tags. Without them, Google may show the wrong language version in search results.
  5. Not handling the default locale redirect. Decide whether example.com should redirect to example.com/en or serve English content without a prefix. Be consistent.
  6. Loading all translations on every page. Split translations by namespace and only load what each page needs to keep bundle sizes small.

Library Comparison: next-intl vs. Manual Implementation

Feature next-intl Manual (DIY)
Middleware setup One-liner Custom code required
Server Component support Built-in Manual loading
Date/number formatting Included Use Intl API directly
Type safety Excellent Manual typing
Community and maintenance Active, well-maintained On you
Bundle size impact Minimal Minimal

For the vast majority of projects, next-intl is the best choice for internationalization in the Next.js App Router. It is lightweight, well-documented, and specifically designed for the App Router paradigm.

Complete Project Structure Recap

Here is the final structure of a well-organized multilingual Next.js project:

my-i18n-app/
├── messages/
│   ├── en.json
│   ├── fr.json
│   ├── de.json
│   └── es.json
├── src/
│   ├── app/
│   │   └── [locale]/
│   │       ├── layout.tsx       # Root layout with lang attribute
│   │       ├── page.tsx         # Home page
│   │       ├── about/
│   │       │   └── page.tsx
│   │       └── pricing/
│   │           └── page.tsx
│   ├── components/
│   │   └── LanguageSwitcher.tsx
│   ├── i18n/
│   │   ├── request.ts           # next-intl request config
│   │   └── routing.ts           # Locale definitions
│   └── middleware.ts            # Locale detection and redirect
├── next.config.js               # With next-intl plugin
└── package.json

Wrapping Up

Implementing internationalization in a Next.js App Router project is more manual than the old Pages Router approach, but it is also more flexible and more powerful. With Server Components, you can load translations without adding client-side JavaScript. With middleware, you get full control over locale detection and routing. And with libraries like next-intl, the developer experience is smooth and productive.

At Box Software, we build multilingual web applications for clients across Europe and beyond. If you need help setting up internationalization in your Next.js project or want us to build your multilingual SaaS product from scratch, get in touch with our team.

Frequently Asked Questions

Does the Next.js App Router have built-in i18n support?

No. Unlike the Pages Router, the App Router does not have a built-in i18n configuration in next.config.js. You need to implement internationalization using middleware, dynamic route segments, and optionally a library like next-intl.

What is the best library for internationalization in Next.js App Router?

next-intl is currently the most popular and well-maintained library for internationalization in the Next.js App Router. It supports Server Components, provides type safety, and includes utilities for formatting dates and numbers.

Can I use next-i18next with the App Router?

No. next-i18next was designed for the Pages Router and does not support the App Router. The recommended migration path is to switch to next-intl or implement a custom solution.

How do I handle SEO for a multilingual Next.js site?

Add hreflang tags to every page, set the correct lang attribute on the HTML element, create a multilingual sitemap, and translate your meta titles and descriptions. Using the generateMetadata function in the App Router makes this straightforward.

Should I use sub-path routing or domain-based routing?

For most projects, sub-path routing (e.g., /en/, /fr/) is the better choice. It is simpler to set up, cheaper to maintain, and works well for SEO. Domain-based routing is better suited for large brands targeting specific regional markets.

How do I handle right-to-left (RTL) languages like Arabic or Hebrew?

Set the dir attribute on the <html> tag based on the current locale. You will also need to use CSS logical properties (e.g., margin-inline-start instead of margin-left) or a CSS framework that supports RTL out of the box.

Can I statically generate pages for all locales?

Yes. Use the generateStaticParams function in your page files to return all locale variants. Next.js will pre-render each version at build time, giving you fast performance and strong SEO.