A Next.js Walkthrough Using react-intl for Localization
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:
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:
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:
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:
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!
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:
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:
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:
On the right side of the row, we can open the menu to add pluralization to this string:
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:
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:
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:
Let’s change the count to 5:
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