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