next-intl guide for Next.js: Practical i18n architecture with SimpleLocalize

If you are building with Next.js App Router, next-intl is one of the strongest options for internationalization.
This guide covers current setup patterns from next-intl docs, production trade-offs, and a concrete translation workflow with SimpleLocalize.
For the broader architecture, key strategy, and localization workflows across frameworks, see the complete technical guide to internationalization and software localization.
If you are choosing between libraries, read react-i18next vs next-intl: Which should you use for React localization?.
What next-intl gives you
next-intl is designed for Next.js and supports:
- App Router and Server Components
- ICU messages for plurals, interpolation, and rich text
- Locale-aware routing (path prefix and domain-based)
- Navigation wrappers (
Link,useRouter,redirect) that include locale context - Optional TypeScript augmentation for typed locales, keys, and formats
For up-to-date reference, use the official docs:
When next-intl is a great fit
next-intl works very well when:
- Your app is Next.js App Router first
- You want locale-aware URLs for indexing and sharing (
/en/pricing,/de/pricing) - You need server-side translations in layouts, metadata, and server components
- You want one i18n system for UI labels plus numbers, dates, and lists
- Your team prefers message files in Git plus CI-based translation sync

Typical use cases
- SEO-focused marketing pages with localized pathnames
- SaaS dashboards with locale switchers and saved user locale preference
- E-commerce checkout flows with price/date formatting and plural rules
- Product sites that need domain-based locale routing by market
When another approach can be better
next-intl can be a weaker fit in these cases:
- You are not on Next.js
- You need one i18n library across web, React Native, Electron, and non-Next.js apps
- You require runtime translation delivery as your default model across many frontends
Also note static export constraints. If your Next.js app uses output: 'export', proxy/middleware does not run. next-intl still works, but with limits documented by the project:
- Locale prefix is required
- Server locale negotiation is unavailable
- Pathname localization config is unavailable
If your product depends heavily on runtime message updates without redeploys, compare file-based delivery with CDN-based delivery patterns and choose per route type.
Current setup pattern (App Router + locale routing)
The setup below follows next-intl docs for locale-based routing.
1. Define routing
// src/i18n/routing.ts
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'de', 'fr', 'pl'],
defaultLocale: 'en'
});
2. Add proxy (Next.js 16+) or middleware (Next.js 15 and lower)
// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: ['/((?!api|trpc|_next|_vercel|.*\\..*).*)']
};
3. Create localized navigation wrappers
// src/i18n/navigation.ts
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';
export const {Link, redirect, usePathname, useRouter, getPathname} = createNavigation(routing);
4. Configure request-scoped locale and messages
// src/i18n/request.ts
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';
export default getRequestConfig(async ({requestLocale}) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
5. Use [locale] segment and enable static params
// src/app/[locale]/layout.tsx
import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { setRequestLocale, getMessages } from 'next-intl/server'; // added getMessages
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
// ... (generateStaticParams)
export default async function LocaleLayout({
children,
params
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
setRequestLocale(locale);
const messages = await getMessages(); // Fetch messages for client components
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Translation usage examples
Client component
'use client';
import {useTranslations} from 'next-intl';
export function UpgradeBanner({plan}: {plan: 'free' | 'pro'}) {
const t = useTranslations('UpgradeBanner');
return (
<p>
{t('cta', {plan})}
</p>
);
}
{
"UpgradeBanner": {
"cta": "Move to {plan, select, free {Free} pro {Pro} other {Free}} today"
}
}
Server component
import {getTranslations} from 'next-intl/server';
export default async function BillingSummary({
total
}: {
total: number;
}) {
const t = await getTranslations('Billing');
return <h2>{t('summary', {total})}</h2>;
}
{
"Billing": {
"summary": "Total due: {total, number, ::currency/USD}"
}
}
Plurals and time formatting
{
"Inbox": {
"messages": "{count, plural, =0 {No messages} one {# message} other {# messages}}",
"lastSync": "Last sync: {updatedAt, date, medium}"
}
}
import {useTranslations} from 'next-intl';
export function InboxHeader({count, updatedAt}: {count: number; updatedAt: Date}) {
const t = useTranslations('Inbox');
return (
<>
<h1>{t('messages', {count})}</h1>
<small>{t('lastSync', {updatedAt})}</small>
</>
);
}
Architecture flow for next-intl + SimpleLocalize
Here is the practical flow many Next.js teams use:
- Git Repo (source of truth for messages and reviewable changes)
- SimpleLocalize CLI (
uploadanddownloadin local dev and CI) - SimpleLocalize Translation Platform (translators, reviewers, AI suggestions)
- CI build (translations bundled and production-ready)

Implementation with SimpleLocalize
Most teams using next-intl keep message files in Git and sync them with a translation platform. This gives clear version history, predictable builds, and easier rollback.
Why this model performs so well in Next.js
For Next.js Server Components, CLI-based JSON sync is often a better default than dynamic translation API fetching per request:
- Messages are bundled at build time or read from local disk, so rendering does not wait on an external translation API call.
- Lower request-time I/O helps keep TTFB fast.
- App Router rendering stays stable under traffic spikes because message loading is local and predictable.
- Builds are reproducible. You know exactly which translation set went to production.
Dynamic loading still has its place for selected routes that need immediate content changes, but for most UI strings in Server Components, file-based sync is the best baseline.
1. Store source messages
/messages
en.json
de.json
fr.json
pl.json
2. Configure SimpleLocalize CLI
# simplelocalize.yml
apiKey: YOUR_PROJECT_API_KEY
uploadFormat: single-language-json
uploadLanguageKey: en
uploadPath: ./messages/en.json
uploadOptions:
- REPLACE_TRANSLATION_IF_FOUND
downloadFormat: single-language-json
downloadLanguageKey: ['de', 'fr', 'pl']
downloadPath: ./messages/{lang}.json
Omit
downloadLanguageKeyto pull all languages. UsedownloadPathwith{lang}placeholder to save each language in its own file.
3. Add scripts
{
"scripts": {
"i18n:upload": "simplelocalize upload",
"i18n:download": "simplelocalize download"
}
}
4. Use CI to pull latest translations
# Example GitHub Actions step
- name: Download translations
run: npx simplelocalize download
env:
SIMPLELOCALIZE_API_KEY: ${{ secrets.SIMPLELOCALIZE_API_KEY }}

5. Optional runtime loading pattern
If you want message updates without redeploy, load messages remotely in i18n/request.ts and apply caching rules. This can be useful for selected content-heavy sections, while core UI keeps file-based messages.
SimpleLocalize docs:
Common implementation mistakes
Some common mistakes to avoid:
- Mixing non-localized imports (
next/link) with localized navigation wrappers in locale-aware routes - Keeping hardcoded user-facing strings in server actions or metadata
- Skipping locale validation and rendering invalid locale segments
- Missing matcher coverage for routes with dots in path segments
- Adding many tiny message namespaces too early and making extraction harder
Decision guide
Pick next-intl if your architecture is Next.js-centric and you want strong App Router integration.
Pick react-i18next if you need one shared i18n runtime across multiple React environments.
For many teams, the best process is simple:
- Keep message source files in repo
- Sync with SimpleLocalize in CI
- Add runtime message fetching only where deployment frequency is a real issue
This gives predictable releases and still leaves room for faster content updates where needed.
Related posts
- The complete technical guide to Internationalization (i18n) & Software localization
- react-i18next vs next-intl: Which should you use for React localization?
- Locale detection strategies: URL, Cookie, or Header?
- How to handle pluralization across languages
- URLs in Localization: How to structure and optimize for multilingual websites




