Skip to main content

Internationalization

We make use of react-i18next, a powerful internationalization framework for React / React Native which is based on i18next. In certain projects we also use:

In order to understand how this repository implements i18n, it is important to understand how these libraries work. The remainder of this document assumes a working knowledge of these libraries.

Adding i18n to your project​

  1. add i18next.config.js to project root.
const i18nextConfigBase = require('../../i18next.config.base') // this file is located in the workspace root

module.exports = {
...i18nextConfigBase,
extract: {
...i18nextConfigBase.extract,
defaultNS: 'PROJECT_NAMESPACE', // e.g. 'apps-journeys' or 'libs-journeys-ui'
input: ['src/**/*.{js,jsx,ts,tsx}']
}
}
  1. add extract-translations command to targets object and implicitDependencies to base object in project.json. You must replace PROJECT_PATH with your project path e.g. apps/journeys.
{
...
"implicitDependencies": ["locales"],
"targets": {
"extract-translations": {
"executor": "@nx/workspace:run-commands",
"options": {
"commands": [
{
"command": "pnpm exec i18next-cli extract --config PROJECT_PATH/i18next.config.js"
}
]
}
}
...

}
}
  1. update your components to make use of useTranslation hooks, appWithTranslation HOCs, Trans components etc in order to indicate to the parser that there is a string available for translation.

It is very important that you include a namespace. This namespace should be a dashed version of your project path. For example apps/journeys would become apps-journeys or libs/journeys/ui would become libs-journeys-ui.

import { useTranslation } from 'next-i18next'

function Component() {
const { t } = useTranslation('libs-journeys-ui') // must include namespace
return <div>{t('hello world')}</div>
}
  1. run nx command extract-translations for the project you are working on. This should pull all of the strings requiring translations into a namespaced file for example locales/en/libs-journeys-ui.json ready for translation. This needs to be committed and kept up to date. This is tested in CI so will fail if not correctly updated.

Next.js configuration​

  1. update input in i18next.config.js located in the project root
const i18nextConfigBase = require('../../i18next.config.base')

module.exports = {
...i18nextConfigBase,
extract: {
...i18nextConfigBase.extract,
input: ['src/**/*.{js,jsx,ts,tsx}', 'pages/**/*.{js,jsx,ts,tsx}']
}
}
  1. add next-i18next.config.js to the project root
const path = require('path')

/**
* @type {import('next-i18next').UserConfig}
**/
const i18nConfig = {
i18n: {
defaultLocale: 'en',
locales: ['en'],
localePath: path.resolve('./libs/locales')
}
}

module.exports = i18nConfig
  1. update next.config.js
const { i18n } = require('./next-i18next.config')

const nextConfig = {
i18n,
...
}
  1. update pages/_app.tsx to use appWithTranslation HOC
import { appWithTranslation } from 'next-i18next'
import i18nConfig from '../next-i18next.config'

// your _app component
function CustomApp({ Component, pageProps }) {
return <Component {...pageProps} />
}

export default appWithTranslation(JourneysApp, i18nConfig)
  1. add async function on your page-level components, via either getStaticProps or getServerSideProps (depending on your use case)
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import i18nConfig from '../next-i18next.config' // project root

export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(
context.locale ?? 'en',
['apps-watch'], // namespaces your components make use of
i18nConfig
))
},
};
}
  1. (Optional) Add a middleware to detect user's preferred language by creating a middleware.ts file in the project root. This middleware will:
  • Detect and manage user language preferences using browser settings and cookies
  • Handle redirects for language-specific routes
  • Use cookie fingerprinting to version language preferences

Basic implementation example:

import { NextRequest, NextResponse } from 'next/server'

const DEFAULT_LOCALE = 'en'
const SUPPORTED_LOCALES = ['en', 'es', 'fr'] // Add your supported locales
// Fingerprint is used to version cookies and may be referenced by other components
// (e.g., LanguageSwitcher)
const COOKIE_FINGERPRINT = '00001' // Increment when updating cookie logic

export function middleware(req: NextRequest): NextResponse | undefined {
// Skip internal paths and static files
if (req.nextUrl.pathname.startsWith('/_next') || req.nextUrl.pathname.includes('/api/') || /\.(.*)$/.test(req.nextUrl.pathname)) {
return
}

// Get stored language preference with fingerprint
const localeCookie = req.cookies.get('NEXT_LOCALE')?.value
const [fingerprint, storedLocale] = localeCookie?.split('---') ?? []

// If no valid fingerprinted cookie exists, get browser preference
const preferredLanguage = (fingerprint === COOKIE_FINGERPRINT ? storedLocale : null) ?? req.headers.get('accept-language')?.split(',')[0] ?? DEFAULT_LOCALE

// Redirect to appropriate language path if needed
if (preferredLanguage !== req.nextUrl.locale) {
const response = NextResponse.redirect(new URL(`/${preferredLanguage}${req.nextUrl.pathname}${req.nextUrl.search}`, req.url))
response.cookies.set('NEXT_LOCALE', `${COOKIE_FINGERPRINT}---${preferredLanguage}`)
return response
}

return NextResponse.next()
}

Important Notes:

  • Update SUPPORTED_LOCALES with your project's supported languages
  • Increment the COOKIE_FINGERPRINT when updating cookie logic to invalidate old preferences
    • Note that this fingerprint may be referenced in other components like LanguageSwitcher
    • For more information, read about Cookie Versioning
  • Adjust the locale detection logic based on your needs
  • Consider adding language fallbacks for regional variants (e.g., 'en-US' → 'en')