IntlPull
Tutorial
12 min read

Next.js App Router with react-intl: Complete i18n Tutorial 2026

Learn how to set up react-intl (FormatJS) in Next.js 14, 15, and 16 App Router. Covers IntlProvider, FormattedMessage, Server Components, and locale switching.

IntlPull Team
IntlPull Team
Feb 1, 2026
On this page
Summary

Learn how to set up react-intl (FormatJS) in Next.js 14, 15, and 16 App Router. Covers IntlProvider, FormattedMessage, Server Components, and locale switching.

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

Terminal
npm 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:

JSON
1{
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:

TypeScript
1export 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:

TypeScript
1import { 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:

TSX
1'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:

TSX
1import { 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:

TSX
1import { 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:

TSX
1'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:

TypeScript
1import { 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

TSX
1import { 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

Terminal
1npx @intlpullhq/cli init
2npx @intlpullhq/cli extract --format icu
3npx @intlpullhq/cli translate --all
4npx @intlpullhq/cli download --output ./app/messages

Tags
next.js
react-intl
app-router
i18n
tutorial
formatjs
2026
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.