A Next.js Walkthrough Using react-intl for Localization

Next.js Logo

The complete guide for Next.js localization using react-intl

(7 minute read)

Prefer a different library?

Not sure which library is right for you? Learn the differences

Before We Start...

Are you using the Next 13+ App Router?

For a tutorial on setting up internationalization with the App Router, you will want to use this tutorial instead.

The following tutorial uses react-intl with the classic Pages Router.

Next.js Internationalization with react-intl

To get started, I’ve created a new Next.js project using create-next-app:

$ npx create-next-app@latest

The only dependency we need to add is react-intl:

$ npm install react-intl

Let's go!

Configuration

The first thing we need to do is update our next.config.js to use Next’s built-in internationalized routing functionality.

To do this, we need to add the i18n property to our config. This will define the languages we want our app to support (locales) and our app’s default language (defaultLocale):

next.config.js
const nextConfig = {
  reactStrictMode: true,
  i18n: {
    locales: ['en', 'ja', 'de'],
    defaultLocale: 'en'
  }
};

module.exports = nextConfig;

I’m going to support English, Japanese, and German. Add as many languages as you would like!

Managing Translations

Historically, developers would store all of their strings in JSON files at the root of their project. They would update their native language’s JSON file as needed and later email the file to professional translators for translation. But, lucky for us, things have gotten a lot easier 🙂

We’re going to use i18nexus to manage and machine translate all of our translations for us. Later, when we’re ready to hire professional translators, we can simply invite them to our i18nexus project to edit and confirm the machine translations instead of emailing them JSON files.

Let's do it!

If you haven’t used i18nexus before, sign up for a free account. After naming our project we’ll be directed to our language dashboard:

The first language tile is your base language. This should be your native language. Let's click Add Language to add each of the languages we previously added in our next.config.js. In my case, I’ll add Japanese and German:

i18nexus Language Dashboard

After you’ve added your languages, click Open Project in the top right corner to go to the Strings Management page.

Let’s click Add String to add a new string to our project:

Adding a string in i18nexus

The key is what is used to reference this string in our code. It will be the same in all languages.

The value is the text that will be rendered on screen to our users.

The details field is optional. It is used to provide any extra information about the context of your string for when you’re ready to bring in professional translators. You can also add an image here for more context.

After saving the string, you can use the expand icon on the left side of the row to view its translations which have been generated with Google Translate:

Viewing machine translations generated for the string

Sweet!

One last thing we’ll want to take notice of is the namespaces dropdown in the top right corner. Your project should already have one namespace named “default” which is where we’ve just added our string. Let’s rename this namespace to “home”:

With Next.js, it is recommended to have one namespace per page. Namespaces help us to both organize our strings and limit how many strings Next has to load at once. It doesn’t make sense for our app to load all the strings for all the pages in our app at once, especially as our app grows.

Let’s jump back to our code and integrate these translations!

Loading Translations

At the moment I only have one page in my app, index.js, which is my Home page. I’ve taken out a lot of the boilerplate, so my page now looks like this:

pages/index.js
import Head from 'next/head';
import styles from '@/styles/Home.module.css';

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
        <h1>Welcome to my website!</h1>
      </main>
    </>
  );
}

Let’s replace the static “Welcome to my website” text with our translations using react-intl.

The first thing we need to do is get the current user’s preferred language. Next.js automatically detects the user’s preferred language for us using the browser’s request headers. We can then access this locale in getStaticProps where we can pass it to our page:

pages/index.js
import Head from 'next/head';
import styles from '@/styles/Home.module.css';

export async function getStaticProps({ locale }) {
  return {
    props: {
      locale: locale
    }
  };
}

export default function Home(props) {
  console.log(props.locale);

  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
        <h1>Welcome to my website!</h1>
      </main>
    </>
  );
}

In getStaticProps, we also need to load our strings to pass into our page as props.

To do this, we’re going to fetch our strings from the i18nexus API. There is no need to worry about load speed in production because this fetching will happen in getStaticProps which only runs at build time.

Note: If you would prefer to download and store your translations as JSON files in your project, check out the i18nexus-cli.

Since we’re going to need to load translations in every future page we create, let’s create a simple helper function that we can reuse for loading translations. I’m going to create a folder called utils at the root of my project and create a file called i18n.js.

The react-intl library refers to strings as “messages”, so let’s name the function getMessages in the i18n.js file:

utils/i18n.js
export const getMessages = async (locale, namespace) => {
  const apiKey = process.env.I18NEXUS_API_KEY;

  const response = await fetch(
    `https://api.i18nexus.com/project_resources/translations/${locale}/${namespace}?api_key=${apiKey}`
  );

  return response.json();
};

This function takes a locale and a namespace as arguments and then fetches our translations from i18nexus. You can find your API key in the Export page of your i18nexus project.

Now back in getStaticProps, we can call this function with our current locale and namespace to load our messages as props:

pages/index.js
import Head from 'next/head';
import styles from '@/styles/Home.module.css';
import { getMessages } from '@/utils/i18n';

export async function getStaticProps({ locale }) {
  return {
    props: {
      locale: locale,
      messages: await getMessages(locale, 'home')
    }
  };
}
...

Awesome!

Rendering translations with react-intl

Now that we have our locale and messages as props in our page component, we can start using react-intl to render our messages.

Let’s import IntlProvider and FormattedMessage from react-intl:

import { IntlProvider, FormattedMessage } from 'react-intl';

The IntlProvider will wrap our page to let us use our messages in all child components.

The FormattedMessage component will take the message key as a prop and render the value.

Let’s integrate these components:

pages/index.js
import Head from 'next/head';
import styles from '@/styles/Home.module.css';
import { getMessages } from '@/utils/i18n';
import { FormattedMessage, IntlProvider } from 'react-intl';

export async function getStaticProps({ locale }) {
  return {
    props: {
      locale,
      messages: await getMessages(locale, 'home')
    }
  };
}

export default function Home(props) {
  const { locale, messages } = props;

  return (
    <IntlProvider messages={messages} locale={locale}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
        <h1>
          <FormattedMessage id="banner" />
        </h1>
      </main>
    </IntlProvider>
  );
}

And now let’s take a look at what is rendered to the screen:

The 'banner' English translation rendered to screen

And thanks to the i18nexus auto-translations, we can immediately see what our app looks like in other languages. All we need to do is put /es or /ja at the end of the URL:

http://localhost:3000/ja

And here’s our app in Japanese!

The 'banner' Japanese translation rendered to screen

Next’s internationalized routing will use the locale code in the URL path to set the current language. By default, Next will load whatever language our user’s browser requests. It will use the defaultLocale if it is a language we do not support.

We can let our users change the locale by using Next’s Link component:

<Link href="/" locale="ja">Japanese</Link>

To create a menu to allow your users to change their language, you can simply list out Links for each language formatted like above.

You do not need to include the locale prop on every Link in your app. If the locale prop is omitted, Next will just retain the current locale. You only need to include the locale when explicitly changing languages.

Interpolation

Now that we’ve rendered a simple message to our page, let’s get into some more complex real-world examples.

First let’s take a look at interpolation. Interpolation is used when you need to render a dynamic value in a message. For example:

"{name} has sent you a friend request!"

To interpolate a dynamic value, we simply wrap a variable name with curly braces. This syntax is based on ICU Message Format which is used by react-intl.

Let’s add this string to i18nexus:

Adding a new string to i18nexus that uses interpolation

And then let’s render it in our home page:

pages/index.js
...
  return (
    <IntlProvider messages={messages} locale={locale}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
        <h1>
          <FormattedMessage id="banner" />
        </h1>
        <h2>
          <FormattedMessage id="friend_request" values={{ name: 'Steve' }} />
        </h2>
      </main>
    </IntlProvider>
  );
...

The only difference in this FormattedMessage is that we need to include the values prop to pass in the dynamic value.

Let’s take a look at our app in English:

Rendering interpolated string on screen

Perfect! As we can see, {name} was replaced with "Steve".

Pluralization

Oftentimes we will need to format a string that will need to have plural forms. Take the following example:

"You have have {count} unread comments on your post."

If the count is 1, we don’t want to say "1 unread comments". We need to have a singular and plural form based on the count.

The react-intl library handles this for us, but we need a specially formatted message:

"{count, plural, one { You have have one unread comment on your post. } other { You have have # unread comments on your post. } }"

Yuck!

On top of having to write this inconvenient syntax, some languages have more than 2 plural forms that we have to worry about! And some languages, like Japanese, only have one form.

Fortunately, i18nexus handles all of this for us. We don’t need to worry about writing this plural syntax or worry about knowing what plural forms are needed for each language!

Phew!

Let’s add the above plural message to i18nexus:

Adding a new string to i18nexus

On the right side of the row, we can open the menu to add pluralization to this string:

Clicking "Add Plurals" in the string's dropdown menu

This will split the string into 2 rows. One for the singular form (one) and one for the plural form (other). Let’s update the singular form to be grammatically correct:

Updating the values of the singular and plural strings to be grammatically correct

Nice!

A language can have a maximum of 6 plural forms: “zero”, “one”, “two”, “few”, “many”, and “other”. An example of a language that has all 6 is Arabic.

i18nexus auto-magically creates only the necessary translations for each language in your project. For example, you’ll notice that there is no Japanese translation for the “one” form:

Expanding the translations on the "one" form to show that Japanese doesn't have a translation

This is because Japanese only has one form, other. When you get to the stage of inviting professional translators to edit and confirm translations, i18nexus will show your German translator 2 forms to translate and your Japanese translator only one.

Imagine doing this without i18nexus… 🫠

Let’s go ahead and render our new plural message in our page:

pages/index.js
...
  return (
    <IntlProvider messages={messages} locale={locale}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
        <h1>
          <FormattedMessage id="banner" />
        </h1>
        <h2>
          <FormattedMessage id="friend_request" values={{ name: 'Steve' }} />
        </h2>
        <h2>
          <FormattedMessage id="unread_comments" values={{ count: 1 }} />
        </h2>
      </main>
    </IntlProvider>
  );
...

This is what is rendered to the screen:

Rendering the singular translation to the screen

Let’s change the count to 5:

Rendering the plural translation to the screen

Amazing!

Wrapping it up

You now have a fully internationalized Next.js app using react-intl! Not only that, but we’ve learned how to integrate automatic machine translation with the i18nexus API and how to interpolate and pluralize messages. We’re pros!

To learn more about other react-intl features such as formatting times and dates, I recommend heading over to the react-intl docs. Do not hesitate to reach out to us in the in-app chat in the bottom right corner of this page if you need any help!

Level up your localization

It only takes a few minutes to streamline your translations forever.

Get started