Den Dribbles

Exploring Internationalisation With Nextjs 10 and react-intl

November 08, 2020

In this post, we will be diving into one of Next.js 10’s new advanced features in internationalised routing and how we can use this with react-intl.

Getting started

Create a new Next.js 10 project by running npx create-next-app i18n-example to create a new project name i18n-example.

We will run some other commands to set things up:

# Create new Next.js 10 project "i18n-example"
npx create-next-app i18n-example
cd i18n-example
# A place to pop some internationalisation content
mkdir -p content/locale
# A place for some languages
# French
touch content/locale/fr.js
# Spanish
touch content/locale/es.js
# English
touch content/locale/en.js
# A barrel file
touch content/locale/index.js
# Installing react-intl for i18n within components
npm i react-intl --legacy-peer-deps
# Required for updating config
touch next.config.js

For installing react-intl I’ve used --legacy-peer-deps as there was a peer dependency of react@^16.3.0 and I was running npm v7.

Now that we have some files going, let’s get started with some basic content!

Setting up Next.js i18n

Follow on from the docs on getting started, we need to update next.config.js:

// next.config.js
module.exports = {
  i18n: {
    // These are all the locales you want to support in
    // your application
    locales: ["en", "fr", "es"],
    // This is the default locale you want to be used when visiting
    // a non-locale prefixed path e.g. `/hello`
    defaultLocale: "en",
  },
}

Here we are going with sub-path routing, so the tl;dr is that our-website.com will be the default locale (English), whereas our-website.com/fr and our-website.com/es will direct us to the French and Spanish websites respectively.

Now that we have that out of the way, let’s update the pages/index.js page!

Internationalising our home page

We can use the Next router to grab which locale we are on.

There is a straight forward example from Vercel’s GitHub that we can take for inspiration.

Replace pages/index.js to look like the following:

import { useRouter } from "next/router"

export default function IndexPage(props) {
  const router = useRouter()
  const { locale, locales, defaultLocale } = router

  return (
    <div>
      <h1>Hello, world!</h1>
      <p>Welcome to your internationalised page!</p>
      <br />
      <p>Current locale: {locale}</p>
      <p>Default locale: {defaultLocale}</p>
      <p>Configured locales: {JSON.stringify(locales)}</p>
    </div>
  )
}

With this, we are ready to start our app and see the results.

Run npm run dev to start the server and head to the localhost port-specific (likely http://localhost:3000).

Once you are there, you will see the current locale of English as well as what locales are configured!

Base page

Base page

Given what we mentioned previously about the sub-routing, we can now go to /fr and /es and expect the current locale to change. The below image will be just for the /fr route to show our sub-routing works.

French locale

French locale

Amazing! Now that we are done here, we can get to using this with react-intl.

Switching copy with react-intl

We will run a simple example here with react-intl, but what we need to do first is prep some content that we wish to swap out!

Inside of content/locale/en.js, let’s through in some basic JSON to replace our “Hello, world!” and welcome message:

export const en = {
  "/": {
    hello: "Hello, world!",
    welcomeMessage: "Welcome to your internationalised page!",
  },
  "/alt": {
    hello: "Yo",
  },
}

The structure of these files is up to you, but I am going with a top-level key of the page name for now and identifiers in the string. Places I have worked previous keep this as JSON to upload to places such as Smartling, so you may want to go down an avenue that transforms JSON to the above ES6 format I am using.

Let’s copy-paste that across to our Spanish and French files and use some possibly inaccurate Google translations to help us out.

For the French:

export const fr = {
  "/": {
    hello: "Bonjour le monde!",
    welcomeMessage: "Bienvenue sur votre page internationalisée!",
  },
  "/alt": {
    hello: "Bonjour",
  },
}

For the Spanish:

export const es = {
  "/": {
    hello: "¡Hola Mundo!",
    welcomeMessage: "¡Bienvenido a tu página internacionalizada!",
  },
  "/alt": {
    hello: "¡Hola!",
  },
}

Finally, we want to update our barrel file content/locale/index.js:

export * from "./en"
export * from "./fr"
export * from "./es"

Great! Now that we are there, let’s go back to pages/_app.js to add our required provider.

// pages/_app.js
import { IntlProvider } from "react-intl"
import { useRouter } from "next/router"
// import all locales through barrel file
import * as locales from "../content/locale"
import "../styles/globals.css"

function MyApp({ Component, pageProps }) {
  const router = useRouter()
  const { locale, defaultLocale, pathname } = router
  const localeCopy = locales[locale]
  const messages = localeCopy[pathname]

  return (
    <IntlProvider
      locale={locale}
      defaultLocale={defaultLocale}
      messages={messages}
    >
      <Component {...pageProps} />
    </IntlProvider>
  )
}

export default MyApp

We are doing a number of things here:

  1. Importing all the locale files through the barrel file we created.
  2. Import the IntlProvider from react-intl to use in each of our pages as part of the app.
  3. Using the pathname given by the Next.js router to determine what copy of the locale to use based on the page.

Now let’s go back to pages/index.js and make use of react-intl.

// pages/index.js
import { useRouter } from "next/router"
import { useIntl } from "react-intl"

export default function IndexPage(props) {
  const { formatMessage } = useIntl()
  const f = id => formatMessage({ id })
  const router = useRouter()
  const { locale, locales, defaultLocale } = router

  return (
    <div>
      <h1>{f("hello")}</h1>
      <p>{f("welcomeMessage")}</p>
      <br />
      <p>Current locale: {locale}</p>
      <p>Default locale: {defaultLocale}</p>
      <p>Configured locales: {JSON.stringify(locales)}</p>
    </div>
  )
}

On this page, I am importing the useIntl hook, destructuring formatMessage from that hook, making a helper function f that abstract the need to always pass an object with the id and replace the appropriate code with our key name for the page in the locale content.

Let’s fire up the app with npm run dev and see what happens!

If we check /, /fr and /es we get the following respectively:

English home

English home

French home

French home

Spanish home

Spanish home

Success!

As an added bonus to show how the other locale pages would work with the /alt route key we put in the locale files, we can create a new file pages/alt.js and add something similar:

import { useIntl } from "react-intl"

export default function IndexPage(props) {
  const { formatMessage } = useIntl()
  const f = id => formatMessage({ id })

  return (
    <div>
      <h1>{f("hello")}</h1>
    </div>
  )
}

Going to /fr/alt and /es/alt respectively give us the following:

Alt image - French

Alt image - French

Alt image - Spanish

Alt image - Spanish

Notice that we have re-used the hello key for this page too but we are not getting that clash thanks to how we set up the locales and pages/_app.js page? Very handy. I am unsure if that is the best way to lay it out (there may be issues I am yet to run into at scale) but for this demo, it works rather nicely.

Note: if you are having an error pop up in the terminal about missing Polyfills, refer to the formatjs documentation. The tl;dr is that you need Node v13+ or you can install a package.

Summary

In conclusion, we have explored Next.js internationalisation and used the react-intl package to help make our locales come to life!

See the final project (although lacking aesthetics) here and the final code here.

Resources and further reading

  1. react-intl
  2. Next.js 10 - i18n routing
  3. Completed project
  4. Final code
  5. react-intl runtime requirements
  6. Smartling

Image credit: Andrew Butler

Originally posted on my blog. Follow me on Twitter for more hidden gems @dennisokeeffe92.


A personal blog on all things of interest. Written by Dennis O'Keeffe, Follow me on Twitter