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.
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).
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>
);
};
Now that your app can be browsed in multiple locales, let’s see how you can make its content available in multiple languages.
In your Next.js project, run the following command to install i18next
:
yarn add i18next next-i18next react-i18next
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.
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>
);
};
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();
});
});
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.
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:
With Recontent.app, you get a friendly interface for designers, developers & product managers to collaborate.