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:
- Middleware for locale detection and redirection
- Dynamic route segments (e.g.,
[locale]) for sub-path routing - Server Components that can load translations on the server
- Third-party libraries like
next-intlfor 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-Languageheader - 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/aboutexample.com/fr/aboutexample.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
langattribute on the<html>tag for each locale - Add hreflang tags pointing to all language versions of each page
- Include an
x-defaulthreflang 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:
- Using the Pages Router i18n config with App Router. It simply does not work. You must use middleware and dynamic segments instead.
- Forgetting to validate locale params. Always check if the locale in the URL is valid and return a 404 if not.
- Hardcoding strings in components. Even for a “small” project, extract all strings into translation files from day one. Retrofitting i18n later is painful.
- Missing hreflang tags. Without them, Google may show the wrong language version in search results.
- Not handling the default locale redirect. Decide whether
example.comshould redirect toexample.com/enor serve English content without a prefix. Be consistent. - 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.
