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.

Concept diagram for 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...

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
2import { IntlProvider } from 'react-intl';
3import { ReactNode } from 'react';
4
5type Props = {
6  locale: string;
7  messages: Record<string, string>;
8  children: ReactNode;
9};
10
11export function IntlClientProvider({ locale, messages, children }: Props) {
12  return (
13    <IntlProvider locale={locale} messages={messages} onError={() => null}>
14      {children}
15    </IntlProvider>
16  );
17}

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

Frequently Asked Questions

How do I use react-intl with the Next.js App Router?

Install react-intl, store messages in ICU JSON, add a [locale] dynamic route, and wrap the tree in a Client Component that renders IntlProvider. Server Components read messages directly, while client UI uses FormattedMessage or the useIntl hook. Follow the eight steps above in order to wire up the provider, layout, middleware, and locale switching.

Does react-intl work with React Server Components in Next.js?

Yes, but IntlProvider, FormattedMessage, and useIntl only run in Client Components. Load messages in a Server Component (layout or page), pass them as props into a 'use client' provider wrapper, and render translated UI inside that boundary. Static strings can also be read straight from the messages object in a Server Component without the provider.

Is react-intl the same as FormatJS?

react-intl is the React binding within the FormatJS family of i18n libraries. FormatJS provides the underlying ICU MessageFormat engine and tooling, and react-intl exposes it through React components and hooks. Searches for "formatjs next.js" and "next.js react-intl" point to this same setup.

Should I use react-intl or next-intl for the App Router?

Use react-intl when you want the standard FormatJS ICU API and portability across React frameworks; use next-intl when you want App Router-native routing helpers and server-first APIs out of the box. Both support ICU messages and pluralization. This tutorial uses react-intl, but the message files and translation workflow transfer cleanly if you migrate.

How do I scale react-intl translations across many languages?

Extract your ICU messages to JSON, then automate translation and sync with a tool like IntlPull instead of hand-editing locale files. Run extract --format icu, translate, and download back into ./app/messages so new keys stay in sync across every locale. This keeps the [locale] route message files consistent as the app grows.


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.