This episode covers internationalization in the App Router using next-intl, localized routing with language prefixes and locale detection via middleware, content translation, and SEO and metadata for multi-language applications.

An application that reaches many countries needs more than just translation: you have to handle routing based on language, date and number formatting, text direction, and SEO-friendly URLs for each language. This is the world of internationalization.
Episode 11 covers i18n support in the App Router using next-intl, localized routing and language detection via middleware, managing content translations, and SEO and metadata for multi-language applications.
The App Router doesn't provide a built-in one-size-fits-all i18n solution; the recommended approach is segment-based routing: create a dynamic [locale] segment at the root of the application, then a library like next-intl or next-i18next manages messages and formatting. next-intl is the most popular choice for the App Router because it supports server components.
First, install the dependency and create the folder structure:
npm install next-intlThe application structure becomes app/[locale]/layout.tsx and app/[locale]/page.tsx. Each language has a message file in the messages folder, for example messages/id.json and messages/en.json.
Use getRequestConfig to load messages based on the locale:
import { getRequestConfig } from "next-intl/server"
export default getRequestConfig(async ({ requestLocale }) => {
const locale = await requestLocale
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
}
})The configuration above loads the message file matching the active locale. This function runs per request, so messages are always consistent with the language the user is currently viewing.
In a component, call the useTranslations function to access messages:
import { useTranslations } from "next-intl"
export default function HomePage() {
const t = useTranslations("Home")
return <h1>{t("title")}</h1>
}t("title") reads the Home.title key from the active language's message file. When the user switches languages, the text changes without a full page reload.
Middleware in middleware.ts picks a locale based on browser preference or URL, then redirects to the language segment:
import createMiddleware from "next-intl/middleware"
export default createMiddleware({
locales: ["id", "en"],
defaultLocale: "id",
})
export const config = {
matcher: ["/", "/(id|en)/:path*"],
}The createMiddleware middleware above routes users arriving at / to /id or /en based on their language preference. The locales and defaultLocale configuration sets the available languages.
Message files are organized per namespace:
{
"Home": {
"title": "Selamat datang",
"description": "Aplikasi multi-bahasa"
}
}The Home namespace above separates messages per page or feature. Consistent keys across language files prevent missing translations — the library will throw a warning when a key isn't found.
For multi-language SEO, every page needs hreflang tags that tell Google how language versions relate. next-intl provides the getAlternateLinks and getLocalizedPathnames functions to generate alternate links automatically. Complete it with localized metadata:
export async function generateMetadata({ params }) {
const { locale } = await params
return {
title: locale === "id" ? "Beranda" : "Home",
alternates: {
languages: { "id-ID": "/id", "en-US": "/en" },
},
}
}The alternates.languages object generates hreflang tags linking all language versions. Each language also has its own URL — this lets Google index each version correctly, without duplicate content.
Here's what to take away:
In the next episode, episode 12, we'll discuss authentication and authorization — authentication patterns in Next.js, integrating NextAuth.js or Auth.js, protecting routes with middleware and session management, and role-based access control and secure redirects. The access security of your application starts to take shape.