IntlPull
Tutorial
12 min read

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

Learn how to set up react-intl with Next.js Pages Router. Complete tutorial covering ICU messages, IntlProvider, getStaticProps, and locale switching.

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

Learn how to set up react-intl with Next.js Pages Router. Complete tutorial covering ICU messages, IntlProvider, getStaticProps, and locale switching.

Quick Answer

To use react-intl with Next.js Pages Router: Install react-intl, create ICU message files, wrap _app.tsx with IntlProvider, load messages in getStaticProps/getServerSideProps, and use FormattedMessage or useIntl for translations.


Prerequisites

  • Next.js with Pages Router
  • Node.js 18+
  • Basic understanding of ICU Message Format

Project Setup

Step 1: Install Dependencies

Terminal
npm install react-intl

Step 2: Configure Next.js i18n

next.config.js:

JavaScript
1module.exports = {
2  reactStrictMode: true,
3  i18n: {
4    locales: ['en', 'es', 'fr'],
5    defaultLocale: 'en',
6  },
7};

Directory Structure

├── lang/
│   ├── en.json
│   ├── es.json
│   └── fr.json
├── lib/
│   └── i18n.ts
├── pages/
│   ├── _app.tsx
│   └── index.tsx
└── next.config.js

Step 3: Create Message Files

lang/en.json:

JSON
1{
2  "app.title": "Welcome to My App",
3  "app.greeting": "Hello, {name}!",
4  "app.items": "{count, plural, =0 {No items} one {# item} other {# items}}",
5  "nav.home": "Home",
6  "nav.about": "About"
7}

Step 4: Create i18n Utilities

lib/i18n.ts:

TypeScript
1export const locales = ['en', 'es', 'fr'] as const;
2export type Locale = (typeof locales)[number];
3
4export const localeNames = {
5  en: 'English',
6  es: 'Español',
7  fr: 'Français',
8};
9
10export async function loadMessages(locale: string) {
11  try {
12    return (await import(`../lang/\${locale}.json`)).default;
13  } catch {
14    return (await import('../lang/en.json')).default;
15  }
16}

Step 5: Set Up _app.tsx

pages/_app.tsx:

TSX
1import type { AppProps } from 'next/app';
2import { IntlProvider } from 'react-intl';
3import { useRouter } from 'next/router';
4
5export default function App({ Component, pageProps }: AppProps) {
6  const router = useRouter();
7  const locale = router.locale || 'en';
8
9  return (
10    <IntlProvider
11      locale={locale}
12      messages={pageProps.messages || {}}
13      onError={(err) => {
14        if (err.code === 'MISSING_TRANSLATION') {
15          console.warn(err.message);
16          return;
17        }
18        throw err;
19      }}
20    >
21      <Component {...pageProps} />
22    </IntlProvider>
23  );
24}

Step 6: Create Pages

pages/index.tsx:

TSX
1import { GetStaticProps } from 'next';
2import { FormattedMessage, useIntl } from 'react-intl';
3import { loadMessages } from '../lib/i18n';
4import LanguageSwitcher from '../components/LanguageSwitcher';
5
6export default function Home() {
7  const intl = useIntl();
8
9  return (
10    <main>
11      <h1>
12        <FormattedMessage id="app.title" />
13      </h1>
14
15      <p>
16        <FormattedMessage id="app.greeting" values={{ name: 'World' }} />
17      </p>
18
19      <p>
20        <FormattedMessage id="app.items" values={{ count: 5 }} />
21      </p>
22
23      <LanguageSwitcher />
24    </main>
25  );
26}
27
28export const getStaticProps: GetStaticProps = async ({ locale }) => {
29  const messages = await loadMessages(locale || 'en');
30  return {
31    props: { messages },
32  };
33};

Step 7: Language Switcher

components/LanguageSwitcher.tsx:

TSX
1import Link from 'next/link';
2import { useRouter } from 'next/router';
3import { localeNames, Locale } from '../lib/i18n';
4
5export default function LanguageSwitcher() {
6  const router = useRouter();
7  const { locales, locale: currentLocale, asPath } = router;
8
9  return (
10    <div>
11      {locales?.map((locale) => (
12        <Link
13          key={locale}
14          href={asPath}
15          locale={locale}
16          style={{
17            padding: '0.5rem',
18            border: currentLocale === locale ? '2px solid blue' : '1px solid gray',
19            margin: '0.25rem',
20          }}
21        >
22          {localeNames[locale as Locale]}
23        </Link>
24      ))}
25    </div>
26  );
27}

ICU Message Format Features

Pluralization

JSON
{
  "notifications": "{count, plural, =0 {No notifications} one {# notification} other {# notifications}}"
}

Select

JSON
{
  "status": "{gender, select, male {He} female {She} other {They}} is online"
}

Built-in Formatters

TSX
1import { FormattedNumber, FormattedDate, FormattedRelativeTime } from 'react-intl';
2
3<FormattedNumber value={1234.56} style="currency" currency="EUR" />
4<FormattedDate value={new Date()} dateStyle="full" />
5<FormattedRelativeTime value={-2} unit="day" />

Rich Text with Tags

JSON
{
  "terms": "I agree to the <link>Terms of Service</link>"
}
TSX
1<FormattedMessage
2  id="terms"
3  values={{
4    link: (chunks) => <a href="/terms">{chunks}</a>,
5  }}
6/>

TypeScript Support

types/intl.d.ts:

TypeScript
1import en from '../lang/en.json';
2
3declare global {
4  namespace FormatjsIntl {
5    interface Message {
6      ids: keyof typeof en;
7    }
8  }
9}

Scaling with IntlPull

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

Tags
next.js
react-intl
pages-router
i18n
tutorial
icu
2026
IntlPull Team
IntlPull Team
Engineering

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