Next.js Internationalization: next-intl, Locale Routing, Pluralization, and SEO
Implement Next.js internationalization with next-intl: locale-based routing with App Router, server-side translations in RSC, pluralization, date/number formatting, hreflang SEO, and locale detection middleware.
Internationalizing a Next.js App Router application is more involved than the old Pages Router approach — there's no built-in i18n routing in App Router, but next-intl fills that gap cleanly. It handles locale-based URL routing (/en/about, /de/uber-uns), server-side translations in React Server Components without prop drilling, pluralization, and locale-aware date/number formatting.
This post covers the full setup: directory structure, middleware for locale detection and routing, translations in RSC and Client Components, pluralization rules, locale-aware formatting, and hreflang tags for SEO.
1. Project Structure
src/
app/
[locale]/ ← All pages inside locale segment
layout.tsx
page.tsx
about/
page.tsx
blog/
[slug]/
page.tsx
i18n/
request.ts ← next-intl server config
routing.ts ← locale definitions
messages/
en.json
de.json
fr.json
es.json
middleware.ts
2. Installation and Configuration
npm install next-intl
// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'de', 'fr', 'es'],
defaultLocale: 'en',
// Locale prefix strategy
localePrefix: 'as-needed', // /en/about vs /about (default locale no prefix)
// Alternatives:
// 'always' → /en/about, /de/uber-uns (explicit for all locales)
// 'never' → /about, /about (use Accept-Language header only — harder for SEO)
// Optional: custom locale paths (slug translation)
pathnames: {
'/about': {
en: '/about',
de: '/uber-uns',
fr: '/a-propos',
es: '/acerca-de',
},
'/blog/[slug]': {
en: '/blog/[slug]',
de: '/blog/[slug]',
fr: '/blog/[slug]',
es: '/blog/[slug]',
},
},
});
// src/i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
// Validate locale — fall back to default if invalid
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
// Time zone: use user preference or server default
timeZone: 'UTC',
// Number formatting defaults
formats: {
number: {
currency: { style: 'currency', minimumFractionDigits: 2 },
},
},
};
});
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 1000+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
3. Middleware for Locale Routing
// middleware.ts (project root)
import createMiddleware from 'next-intl/middleware';
import { routing } from './src/i18n/routing';
import { type NextRequest, NextResponse } from 'next/server';
const intlMiddleware = createMiddleware(routing);
export default function middleware(req: NextRequest): NextResponse {
// Run next-intl middleware for locale routing
return intlMiddleware(req);
}
export const config = {
// Match all pathnames except static files and Next.js internals
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|images|fonts).*)'],
};
4. Translation Messages
// messages/en.json
{
"common": {
"loading": "Loading...",
"error": "Something went wrong",
"tryAgain": "Try again",
"save": "Save changes",
"cancel": "Cancel",
"delete": "Delete",
"confirm": "Confirm"
},
"nav": {
"home": "Home",
"about": "About",
"blog": "Blog",
"pricing": "Pricing",
"contact": "Contact us"
},
"pricing": {
"title": "Simple, transparent pricing",
"subtitle": "Choose the plan that works for your team",
"perMonth": "per month",
"startTrial": "Start free trial",
"plans": {
"starter": {
"name": "Starter",
"description": "Perfect for small teams getting started"
},
"pro": {
"name": "Pro",
"description": "For growing teams that need more power"
}
},
"seats": "{count, plural, one {# seat} other {# seats}}",
"trialDays": "{days, plural, one {# day free trial} other {# days free trial}}"
},
"blog": {
"readMore": "Read more",
"publishedOn": "Published on {date}",
"minuteRead": "{minutes, plural, one {# minute read} other {# minute read}}",
"relatedPosts": "Related posts"
},
"errors": {
"notFound": "Page not found",
"notFoundDescription": "The page you're looking for doesn't exist.",
"serverError": "Server error",
"serverErrorDescription": "Something went wrong on our end. Please try again."
}
}
// messages/de.json
{
"common": {
"loading": "Wird geladen...",
"error": "Etwas ist schiefgelaufen",
"tryAgain": "Erneut versuchen",
"save": "Änderungen speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"confirm": "Bestätigen"
},
"nav": {
"home": "Startseite",
"about": "Über uns",
"blog": "Blog",
"pricing": "Preise",
"contact": "Kontakt"
},
"pricing": {
"title": "Einfache, transparente Preise",
"subtitle": "Wählen Sie den Plan, der zu Ihrem Team passt",
"perMonth": "pro Monat",
"startTrial": "Kostenlos testen",
"seats": "{count, plural, one {# Platz} other {# Plätze}}",
"trialDays": "{days, plural, one {# Tag kostenlos} other {# Tage kostenlos}}"
},
"blog": {
"readMore": "Weiterlesen",
"publishedOn": "Veröffentlicht am {date}",
"minuteRead": "{minutes, plural, one {# Minute Lesezeit} other {# Minuten Lesezeit}}",
"relatedPosts": "Verwandte Beiträge"
},
"errors": {
"notFound": "Seite nicht gefunden",
"notFoundDescription": "Die gesuchte Seite existiert nicht.",
"serverError": "Serverfehler",
"serverErrorDescription": "Auf unserer Seite ist etwas schiefgelaufen. Bitte versuchen Sie es erneut."
}
}

🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Recommended Reading
5. Using Translations in Server Components
// src/app/[locale]/pricing/page.tsx
import { useTranslations } from 'next-intl';
import { getTranslations, getLocale } from 'next-intl/server';
import { setRequestLocale } from 'next-intl/server';
import type { Metadata } from 'next';
// Generate metadata with locale-aware title/description
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'pricing' });
return {
title: t('title'),
description: t('subtitle'),
alternates: {
// hreflang tags for SEO
languages: {
en: '/en/pricing',
de: '/de/preise',
fr: '/fr/tarifs',
es: '/es/precios',
'x-default': '/pricing',
},
},
};
}
// Server Component — useTranslations works server-side with next-intl
export default async function PricingPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// Required in Next.js 15+ for static rendering with i18n
setRequestLocale(locale);
const t = useTranslations('pricing');
return (
<div className="mx-auto max-w-4xl px-4 py-16">
<h1 className="text-4xl font-bold text-center text-gray-900">{t('title')}</h1>
<p className="mt-4 text-xl text-center text-gray-500">{t('subtitle')}</p>
<div className="mt-12 grid grid-cols-2 gap-8">
<PlanCard
name={t('plans.starter.name')}
description={t('plans.starter.description')}
price={29}
seats={5}
trialDays={14}
/>
<PlanCard
name={t('plans.pro.name')}
description={t('plans.pro.description')}
price={99}
seats={25}
trialDays={14}
/>
</div>
</div>
);
}
function PlanCard({
name, description, price, seats, trialDays,
}: {
name: string; description: string; price: number; seats: number; trialDays: number;
}) {
const t = useTranslations('pricing');
const locale = useLocale();
return (
<div className="rounded-xl border border-gray-200 p-6">
<h2 className="text-xl font-semibold">{name}</h2>
<p className="mt-1 text-gray-500 text-sm">{description}</p>
<p className="mt-4 text-3xl font-bold">
{/* Locale-aware number formatting */}
{new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }).format(price)}
<span className="text-base font-normal text-gray-400"> {t('perMonth')}</span>
</p>
{/* ICU message format pluralization */}
<p className="mt-2 text-sm text-gray-600">{t('seats', { count: seats })}</p>
<p className="text-sm text-gray-600">{t('trialDays', { days: trialDays })}</p>
<button className="mt-6 w-full rounded-lg bg-blue-600 py-2 text-sm font-medium text-white">
{t('startTrial')}
</button>
</div>
);
}
6. Translations in Client Components
// src/components/LocaleSwitcher.tsx
'use client';
import { useLocale, useTranslations } from 'next-intl';
import { useRouter, usePathname } from 'next-intl/client';
import { routing } from '../../i18n/routing';
export function LocaleSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const localeNames: Record<string, string> = {
en: 'English',
de: 'Deutsch',
fr: 'Français',
es: 'Español',
};
function handleChange(newLocale: string) {
// Switches locale while keeping the current pathname
router.replace(pathname, { locale: newLocale });
}
return (
<select
value={locale}
onChange={(e) => handleChange(e.target.value)}
className="rounded border border-gray-300 px-2 py-1 text-sm"
aria-label="Select language"
>
{routing.locales.map((loc) => (
<option key={loc} value={loc}>
{localeNames[loc] ?? loc}
</option>
))}
</select>
);
}
// Client component using translations
'use client';
import { useTranslations, useFormatter } from 'next-intl';
export function BlogCard({ post }: { post: Post }) {
const t = useTranslations('blog');
const format = useFormatter();
return (
<article className="rounded-lg border p-4">
<h3 className="font-semibold">{post.title}</h3>
<p className="text-sm text-gray-500">
{t('publishedOn', {
date: format.dateTime(new Date(post.publishedAt), {
year: 'numeric', month: 'long', day: 'numeric',
}),
})}
</p>
<p className="text-xs text-gray-400">
{t('minuteRead', { minutes: post.readTime })}
</p>
</article>
);
}
7. Layout with Locale Provider
// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '../../i18n/routing';
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// Validate locale
if (!routing.locales.includes(locale as any)) {
notFound();
}
setRequestLocale(locale);
// Pass messages to Client Components
const messages = await getMessages();
return (
<html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Cost Reference
| Translation approach | Cost | Scale |
|---|---|---|
| JSON files in repo | $0 | < 1,000 keys |
| Crowdin / Phrase | $50–200/mo | Team translation workflow |
| DeepL Auto-translate | $5.49/mo + $25/M chars | AI-assisted first pass |
| Locize (next-intl native) | $10–100/mo | Real-time translation updates |
You Might Also Like
- Next.js App Router: Server Components, Streaming, and Server Actions
- Next.js Middleware: Edge Auth Guards and A/B Testing
- Next.js Image Optimization: AVIF/WebP and CDN Delivery
- React Server Actions: useActionState and Optimistic UI
- Next.js Testing Strategy: Unit, Integration, and E2E with Playwright
Why Clients Trust Viprasol
Adding multiple languages to an existing Next.js App Router application, or starting a new project that needs to serve English, German, French, and Spanish from day one? We implement next-intl with locale-based routing, server-side RSC translations, ICU pluralization, locale-aware formatting, and hreflang SEO — so your app is globally ready without a full rewrite.
External Resources
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.