Quick Answer
To use react-intl with Next.js App Router: Install react-intl, create message files in ICU format, set up a [locale] dynamic route, wrap your app with IntlProvider in a Client Component wrapper, and use FormattedMessage or useIntl hook for translations.
Prerequisites
- Next.js 14, 15, or 16 with App Router
- Basic understanding of React Server Components
- Node.js 18+
Project Setup
Step 1: Install Dependencies
Terminalnpm install react-intl @formatjs/intl-localematcher negotiator npm install -D @types/negotiator
Directory Structure
app/
├── [locale]/
│ ├── layout.tsx
│ ├── page.tsx
│ └── providers.tsx
├── i18n/
│ ├── config.ts
│ └── get-messages.ts
└── messages/
├── en.json
├── es.json
└── fr.json
Step 2: Create Message Files (ICU Format)
messages/en.json:
JSON1{ 2 "home.title": "Welcome to our application", 3 "home.description": "This is a multilingual Next.js site", 4 "home.greeting": "Hello, {name}!", 5 "home.items": "{count, plural, =0 {No items} one {# item} other {# items}}" 6}
Step 3: Configure i18n Settings
app/i18n/config.ts:
TypeScript1export const locales = ['en', 'es', 'fr'] as const; 2export type Locale = (typeof locales)[number]; 3export const defaultLocale: Locale = 'en'; 4 5export const localeNames = { 6 en: 'English', 7 es: 'Español', 8 fr: 'Français', 9};
app/i18n/get-messages.ts:
TypeScript1import { Locale } from './config'; 2 3const messageImports = { 4 en: () => import('../messages/en.json'), 5 es: () => import('../messages/es.json'), 6 fr: () => import('../messages/fr.json'), 7}; 8 9export async function getMessages(locale: Locale) { 10 const messages = await messageImports[locale](); 11 return messages.default; 12}
Step 4: Create IntlProvider Wrapper
app/[locale]/providers.tsx:
TSX1'use client'; 2 3import { IntlProvider } from 'react-intl'; 4import { ReactNode } from 'react'; 5 6type Props = { 7 locale: string; 8 messages: Record<string, string>; 9 children: ReactNode; 10}; 11 12export function IntlClientProvider({ locale, messages, children }: Props) { 13 return ( 14 <IntlProvider locale={locale} messages={messages} onError={() => null}> 15 {children} 16 </IntlProvider> 17 ); 18}
Step 5: Set Up the Locale Layout
app/[locale]/layout.tsx:
TSX1import { locales, Locale } from '../i18n/config'; 2import { getMessages } from '../i18n/get-messages'; 3import { IntlClientProvider } from './providers'; 4 5export function generateStaticParams() { 6 return locales.map((locale) => ({ locale })); 7} 8 9export default async function LocaleLayout({ 10 children, 11 params: { locale }, 12}) { 13 const messages = await getMessages(locale as Locale); 14 15 return ( 16 <html lang={locale}> 17 <body> 18 <IntlClientProvider locale={locale} messages={messages}> 19 {children} 20 </IntlClientProvider> 21 </body> 22 </html> 23 ); 24}
Step 6: Create the Home Page
app/[locale]/page.tsx:
TSX1import { getMessages } from '../i18n/get-messages'; 2import { Locale } from '../i18n/config'; 3import HomeContent from './components/HomeContent'; 4 5export default async function HomePage({ params: { locale } }) { 6 const messages = await getMessages(locale as Locale); 7 8 return ( 9 <main> 10 <h1>{messages['home.title']}</h1> 11 <HomeContent /> 12 </main> 13 ); 14}
Step 7: Create Client Components
app/[locale]/components/HomeContent.tsx:
TSX1'use client'; 2 3import { useState } from 'react'; 4import { FormattedMessage, useIntl } from 'react-intl'; 5 6export default function HomeContent() { 7 const intl = useIntl(); 8 const [count, setCount] = useState(0); 9 10 return ( 11 <div> 12 <p> 13 <FormattedMessage id="home.greeting" values={{ name: 'World' }} /> 14 </p> 15 <p>{intl.formatMessage({ id: 'home.items' }, { count })}</p> 16 <button onClick={() => setCount(count + 1)}>Add</button> 17 </div> 18 ); 19}
Step 8: Add Middleware
middleware.ts:
TypeScript1import { NextRequest, NextResponse } from 'next/server'; 2import { match } from '@formatjs/intl-localematcher'; 3import Negotiator from 'negotiator'; 4import { locales, defaultLocale } from './app/i18n/config'; 5 6function getLocale(request: NextRequest): string { 7 const headers = { 'accept-language': request.headers.get('accept-language') || '' }; 8 const languages = new Negotiator({ headers }).languages(); 9 return match(languages, [...locales], defaultLocale); 10} 11 12export function middleware(request: NextRequest) { 13 const pathname = request.nextUrl.pathname; 14 15 const pathnameHasLocale = locales.some( 16 (locale) => pathname.startsWith(`/\${locale}/`) || pathname === `/\${locale}` 17 ); 18 19 if (pathnameHasLocale) return; 20 21 const locale = getLocale(request); 22 request.nextUrl.pathname = `/\${locale}\${pathname}`; 23 return NextResponse.redirect(request.nextUrl); 24} 25 26export const config = { 27 matcher: ['/((?!_next|api|favicon.ico).*)'], 28};
Using ICU Features
Pluralization
TSX<FormattedMessage id="home.items" values={{ count: 5 }} />
Number and Date Formatting
TSX1import { FormattedNumber, FormattedDate } from 'react-intl'; 2 3<FormattedNumber value={1234.56} style="currency" currency="USD" /> 4<FormattedDate value={new Date()} dateStyle="full" />
Scaling with IntlPull
Terminal1npx @intlpullhq/cli init 2npx @intlpullhq/cli extract --format icu 3npx @intlpullhq/cli translate --all 4npx @intlpullhq/cli download --output ./app/messages
