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
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.
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).
Navigating between locales & local detection
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
:
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:
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.
{
"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:
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.
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
:
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:
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:
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
- Using our CLI or GitHub Action or built a custom integration with our REST API for example
- 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