Skip to main content

Setting up internationalization (i18n) in a Next.js app

· 8 min read
Grégoire Mielle

As your Next.js app grows & gains in usage, you might want to make it available in multiple languages. Next.js has built-in support for i18n routing since version 10.0.

However, it does not provide a default solution for translating content. Let's see how you can integrate the i18next ecosystem in your Next.js application with Typescript.

Setting up i18n routing with Next.js

tip

The code used for demonstration in this post is available in this GitHub repository.

If you don't already have a Next.js app set up, run the following command:

yarn create next-app --typescript

To get started, update your next.config.js file with an i18n object describing the languages you want to support, including the default one.

next.config.js
module.exports = {
i18n: {
// Can be a language (eg. `en`) or language + region (eg. `en-GB`)
locales: ['en', 'fr'],
defaultLocale: 'en',
},
}

You should now be able to browse your pages with both locales prefix (eg. /en/hello-world & /fr/hello-world if you have a hello-world.tsx file in the pages folder).

When your app receives a request, Next.js trie to detect the user's language using the Accept-Language header. If the user's locale is available, they will be automatically redirected to it.

It's still possible for your users to navigate to a specific locale. Using the Link component, you can point to a specific localized version of a page:

import Link from 'next/link'

export const HelpCenterButton = () => {
return (
<Link href="/help-center" locale="en">
Contact our English help center
</Link>
)
}

The current locale & available locales can be accessed from the useRouter hook. This hook is also used to imperatively navigate to a new route.

import { useRouter } from 'next/router'

export const LanguagesSwitch = () => {
const router = useRouter()

return (
<div>
<ul>
{router.locales.map(locale => (
<li key={locale}>
<button
type="button"
onClick={() => {
router.push('/', '/', { locale })
}}
>
{locale.toUpperCase()}
</button>
</li>
))}
</ul>
</div>
)
}

i18next with Next.js

Now that your app can be browsed in multiple locales, let's see how you can make its content available in multiple languages.

Installation

In your Next.js project, run the following command to install i18next:

yarn add i18next next-i18next react-i18next

Configuration

To use i18next in a Next.js project, you use the next-i18next package. It's in charge of initializing i18next with your translation files in order to use translations in pages & components.

First, create a next-i18next.config.js file to configure next-i18next:

next-i18next.config.js
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
defaultNS: 'default',
localePath: './public/locales',
localeExtension: 'json',
localeStructure: '{{lng}}/{{ns}}',
/** To avoid issues when deploying to some paas (vercel...) */
localePath:
typeof window === 'undefined'
? require('path').resolve('./public/locales')
: '/locales',
},
}

As some information are duplicated across Next & Next i18next config files, let's update the next.config.js file:

next-i18next.config.js
const { i18n } = require('./next-i18next.config')

const nextConfig = {
reactStrictMode: true,
i18n: {
locales: i18n.locales,
defaultLocale: i18n.defaultLocale,
},
}

module.exports = nextConfig

The public/locales folder contains a folder for each language you support. In each folder, add a JSON file named default.json. New JSON files can be added in the future if you want to support multiple i18next namespaces.

These folders can be easily synced with a translation management tool like Recontent.app.

To make translations available to use in your code, populate JSON files for each language with keys. Keys can be nested in objects or have a flat structure. We'll see in the following section how to reference them in your code.

public/locales/en/default.json
{
"homepage": {
"title": "Welcome to the homepage"
}
}

Finally, update your _app.tsx file to let next-i18next initialize i18next & automatically add a React context provider for translations:

src/pages/_app.tsx
import { appWithTranslation } from 'next-i18next'
import type { AppProps } from 'next/app'

const App = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />
}

export default appWithTranslation(App)

Now that your app is setup to use i18n, let's see how to actually reference translations in your code or have proper autocompletion & typesafety with Typescript.

How to reference translations in your code

In order to use translations in Next.js pages & children components, translations need to be injected in every page.

This is done by using the serverSideTranslations function in getStaticProps. Based on the requested locale, only relevant translations are loaded.

The useTranslation hook can then be used to replace hardcoded text with dynamic references to your translations based on current language.

src/pages/index.tsx
import { useTranslation } from 'next-i18next'
import { GetStaticProps } from 'next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'

export default function Home() {
const { t } = useTranslation()

return (
<main>
<h1>{t('homepage.title')}</h1>
</main>
)
}

export const getStaticProps: GetStaticProps = async ({ locale }) => {
const translationsProps = await serverSideTranslations(locale ?? 'en', [
// `i18next` namespace, matches translations file names
// & `defaultNS` in `next-i18next.config.js`
'default',
])

return {
props: {
// These props are used by `appWithTranslation` in `_app.tsx`
// to set up a React context which holds translations
...translationsProps,
},
}
}

Each child component in the React tree can use the useTranslation hook to consume translations initialized at the page level.

All i18next features in classic React.js apps like plurals or component interpolation are available.

import { useTranslation } from 'next-i18next'

export const Banner = () => {
const { t } = useTranslation()
const project_count = 2

/**
* {
* "success_notification": "Project created, you can access it from your dashboard."
* "success_notification_plural": "Projects created, find all of them on your dashboard."
* }
*/
return (
<div>
<h1>{t('success_notification', { count: project_count })}</h1>
</div>
)
}

How to test components with i18n

As any component in your Next.js codebase, components using i18n keys can be tested with Jest & react-testing-library. You can either choose to initialize a react-i18next Provider in your tests & make sure translated texts are rendered correctly or rely on keys.

If you choose to rely on keys and not initialize react-i18next in your tests, make sure to mock next-i18next:

setupTests.ts
import '@testing-library/jest-dom'

jest.mock('next-i18next', () => ({
useTranslation: () => {
return {
t: (key: string) => key,
}
},
}))

A test for your component using react-testing-library can look like this:

__tests__/index.spec.tsx
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'
import Home from '../src/pages/index'

describe('Home', () => {
it('renders without crashing', () => {
render(<Home />)

expect(screen.getByText('homepage.title')).toBeInTheDocument()
})
})

Using Typescript for autocomplete & typesafety

If you already use Typescript in your Next.js codebase, you might want to extend the typesafety you have in your code to i18n keys. Indeed, translations are referenced using strings in your code. It's easy to make a typo & release a broken UI to your users.

Moreover, with proper typing, your IDE will be able to autocomplete your keys for better developer experience.

Create a i18next.d.ts in the src folder and add the following content:

src/i18next.d.ts
import en from '../public/locales/en/default.json'

declare module 'i18next' {
interface CustomTypeOptions {
returnNull: false
defaultNS: 'default'
resources: {
default: typeof en
}
}
}

The CustomTypeOptions is augmented to add types to i18next default resources. Typescript should now only allow existing keys within the public/locales/en/default.json file.

Using Recontent to manage the content & translation process

With this setup, your codebase is more maintainable: translations are stored is identified places and updates to them can be made without touching the source code. If you want to deploy your app to new regions, supporting a new language is possible by adding a new [locale]/default.json file.

However, within the product cycle, there are still many issues:

  • Only developers can access & edit translations
  • Content-wise, it's hard to review whole parts of the app without digging in GitHub
  • Everything is done manually, from adding a new language, translating keys, etc.

With Recontent.app, you get a friendly interface for designers, developers & product managers to collaborate.

  • Only take care of the technical setup & stop merging PRs to fix typos
  • Let designers import content from Figma, the team translate it & product managers review it
  • Use revisions like GitHub branches & only deploy when ready
  • Use our CDN or AWS S3/GCP Cloud Storage integrations to store your translations