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
03 Feb 2026, 11:44 AM [PST]
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.