Internationalizing Next.js Apps with next-intl
Locale routing, server-rendered translations, RTL layouts and hreflang SEO without client-side flash.
Internationalization is a foundational requirement for applications that serve global audiences. In the Next.js App Router era, libraries like next-intl have emerged as the de facto standard for managing translations, locale routing, and SEO across multiple languages. This guide walks through proven patterns for building multilingual React applications that perform well, rank in search engines, and respect cultural conventions including right-to-left layouts.
Choosing an i18n Strategy for Next.js
The App Router changed how Next.js handles internationalization. The built-in i18n configuration from the Pages Router no longer applies, and teams must choose a library that integrates with the new routing model. next-intl is the most widely adopted option because it provides type-safe message formatting, locale-aware navigation, and middleware-based locale detection out of the box.
When evaluating your approach, consider these architectural decisions upfront:
- URL structure — Prefix-based (
/en/about,/de/about) versus domain-based (en.example.com,de.example.com). - Default locale handling — Whether the default language appears in the URL or is inferred from headers.
- Translation storage — JSON files, CMS integration, or a translation management platform.
- Fallback behavior — What happens when a translation key is missing in a secondary locale.
Prefix-based routing is the recommended default for most applications. It keeps deployment simple, works with static hosting, and gives search engines clear signals about language variants.
Setting Up Locale Routing
next-intl uses Next.js middleware to detect the user's preferred locale and rewrite requests to the appropriate locale segment. The middleware runs on the edge, adding negligible latency while ensuring every page serves content in the correct language.
// middleware.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware(routing);
export const config = {
matcher: ["/", "/(de|en|fr|ar)/:path*"],
};
// i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["en", "de", "fr", "ar"],
defaultLocale: "en",
localePrefix: "always",
});
Your app directory structure mirrors the locale configuration. Each locale gets its own segment under app/[locale]/, and shared layouts wrap translated pages with consistent navigation and footer content.
Locale-Aware Navigation
Never hardcode paths in links. Use the navigation helpers provided by next-intl to generate locale-prefixed URLs automatically. This prevents broken links when you add or remove locales and ensures active locale context propagates through client-side navigation.
import { Link } from "@/i18n/navigation";
export function NavBar() {
return (
<nav>
<Link href="/about">About</Link>
<Link href="/pricing">Pricing</Link>
</nav>
);
}
Organizing Translation Messages
Structure your message files to scale with your application. A flat JSON file works for small projects, but nested namespaces prevent merge conflicts and improve discoverability as your translation count grows into the thousands.
- Group messages by feature or page —
home.json,checkout.json,errors.json. - Use descriptive key names —
checkout.payment.submitButtoninstead ofbtn1. - Keep ICU message format syntax consistent for pluralization and interpolation.
- Establish a review workflow with native speakers before deploying translations.
Load only the namespaces a page needs to avoid shipping unused translations. next-intl supports lazy loading message modules, which keeps initial bundle sizes manageable even with dozens of locales.
Right-to-Left Layout Support
Supporting RTL languages like Arabic and Hebrew requires more than translating strings. Layout direction, text alignment, icon mirroring, and animation direction all need to adapt when the locale switches to an RTL language.
Set the dir attribute on your root HTML element based on the active locale:
// app/[locale]/layout.tsx
import { getLocale, getMessages } from "next-intl/server";
const rtlLocales = ["ar", "he", "fa"];
export default async function LocaleLayout({ children, params }) {
const locale = await getLocale();
const direction = rtlLocales.includes(locale) ? "rtl" : "ltr";
return (
<html lang={locale} dir={direction}>
<body>{children}</body>
</html>
);
}
Use CSS logical properties throughout your stylesheet. Replace margin-left with margin-inline-start, padding-right with padding-inline-end, and text-align: left with text-align: start. Tailwind CSS v4 includes logical property utilities (ms-4, pe-2) that handle this automatically.
SEO for Multilingual Sites
Search engines need explicit signals to understand language relationships between pages. Without proper hreflang tags and canonical URLs, you risk duplicate content penalties and incorrect locale indexing.
Implement these SEO essentials:
- hreflang link tags — Declare every language variant of each page in the document head.
- Canonical URLs — Each locale page should canonicalize to itself, not to the default locale.
- Translated metadata — Title, description, and Open Graph tags must be localized, not copied from the default locale.
- XML sitemaps — Generate per-locale sitemaps or a unified sitemap with hreflang annotations.
- Structured data — Localize JSON-LD schema markup for rich search results.
next-intl integrates with Next.js metadata API, allowing you to define locale-specific titles and descriptions in each page's generateMetadata function. Combine this with a sitemap generator that iterates over your locales and routes to produce comprehensive search engine coverage.
Testing and Quality Assurance
Internationalization bugs are insidious because they often appear only in specific locale combinations. A missing translation key might render as a raw key string in production. An RTL layout bug might break navigation only in Arabic. Build automated checks into your CI pipeline to catch these issues early.
- Validate that all locales contain the same set of translation keys.
- Run visual regression tests against RTL locales to catch layout breaks.
- Test locale detection with various Accept-Language header combinations.
- Verify that date, number, and currency formatting respects locale conventions.
- Check page weight — ensure loading all locale messages does not inflate bundles.
Internationalization is an ongoing commitment, not a one-time setup task. As your application grows, new features introduce new strings, and maintaining translation parity across locales requires discipline and tooling. By adopting next-intl with structured locale routing, RTL support, and SEO best practices from the start, you build a foundation that scales gracefully as your audience expands across languages and regions.