TL;DR
This is for NextJS i18n which uses the
pagedirectory. Here is the blog for doing i18n in the new app directory.Check the demo here
Check the source code here
Introduction
Internationalization (i18n) is the process of preparing software so that it can support local languages and cultural settings. An internationalized product supports the requirements of local markets around the world, functioning more appropriately based on local norms and better meeting in-country user expectations. Copy-pasted from here
In my early days of development, I find i18n to be a tedious task. However, in NextJS, it is relatively simple to create such as challenging feature.
Project Setup
Initialize a NextJS project
Let's start by creating a new NextJS project. The simplest way is to use these commands:
1npx create-next-app@latest2# or3yarn create next-appFor more information, check this Create Next App docs
Remove boilerplate code
Let's simplify the project by removing unused code.
1export default function Home() {2 return <main>Hello world</main>;3}Check the changes here
Create another route/page
This step is not related to i18n. It is mainly for easy demonstration.
Update the Home page to display the current locale.
1import { useRouter } from "next/router";2
3export default function Home() {4 const { locale } = useRouter();5
6 return <main>Hello world: {locale}</main>;7}Let's create an About page with the same content as the Home page.
1import { useRouter } from "next/router";2
3export default function About() {4 const { locale } = useRouter();5
6 return <main>About page: {locale}</main>;7}Without any configuration changes, the pages will be rendered as:
As you can see, localhost:3000 shows Hello world: . This is because useRouter is not aware of the value of locale.
localhost:3000/zh-CN and localhost:3000/sv obviously will not exist because we have not created pages/zh-CN.jsx and pages/sv.jsx
Internationalized Routing
Built-in NextJS i18n routing
Let's add this simple i18n configuration to our next.config.js file and see what happens.
1const nextConfig = {2 // other stuff3 i18n: {4 defaultLocale: "en",5 locales: ["en", "sv", "zh-CN"],6 },7};With the configuration above, we automagically get the locale value and the following routes:
Home page
About page
Not defined locale
If you try to access localhost:3000/fr, you will still get a 404 error. This is because we did not add fr to our locale values
Create a header component
To further simplify our demo, let's create a header component that can:
- Navigate to home and about pages
- Change the locale values using a dropdown
1import React from "react";2import Link from "next/link";3import { useRouter } from "next/router";4
5const Header = () => {6 const router = useRouter();7
8 const handleLocaleChange = (event) => {9 const value = event.target.value;10
11 router.push(router.route, router.asPath, {12 locale: value,13 });14 };15
16 return (17 <header>18 <nav>19 <Link href="/">20 <a className={router.asPath === "/" ? "active" : ""}>Home</a>21 </Link>22 <Link href="/about">23 <a className={router.asPath === "/about" ? "active" : ""}>About</a>24 </Link>25 </nav>26
27 <select onChange={handleLocaleChange} value={router.locale}>28 <option value="en">🇺🇸 English</option>29 <option value="zh-CN">🇨🇳 中文</option>30 <option value="sv">🇸🇪 Swedish</option>31 </select>32
33 <style jsx>{`34 a {35 margin-right: 0.5rem;36 }37
38 a.active {39 color: blue;40 }41
42 nav {43 margin-bottom: 0.5rem;44 }45 `}</style>46 </header>47 );48};49
50export default Header;Let's add the Header component to our pages/_app.js file.
1import Header from "../components/Header";2import "../styles/globals.css";3
4function MyApp({ Component, pageProps }) {5 return (6 <>7 <Header />8 <Component {...pageProps} />9 </>10 );11}12
13export default MyApp;Now we can see clearly the power of NextJS built-in i18n support. We can now access the locale value in our useRouter hook, and the URL is updated based on the locale.
To learn more about NextJS i18n routing, check this link.
Content translation
Unfortunately, there is no NextJS built-in support for content translation so we need to do it on our own.
However, there are a libraries that can help to not reinvent the wheel. In this blog post, we will use next-i18next.
Let's support content translation by setting up next-i18next in our app.
Install next-i18next
1npm install next-i18nextCreate a next-i18next.config.js and update next.config.js
1module.exports = {2 i18n: {3 defaultLocale: "en",4 locales: ["en", "sv", "zh-CN"],5 localePath: "./locales",6 },7};localePath is optional and will default to ./public/locales.
1const { i18n } = require("./next-i18next.config");2
3const nextConfig = {4 // other stuff5 i18n,6};7
8module.exports = nextConfig;Create translation files
.└── locales ├── en | └── common.json | └── home.json └── zh-CN | └── common.json | └── home.json └── se └── common.json └── home.jsonEnglish Translations
1{2 "greeting": "Hello world!"3}1{2 "home": "Home",3 "about": "About"4}Please forgive me for any translation mistakes. I only used Google Translate to translate the content. 🤣
Chinese translations
1{2 "greeting": "世界您好"3}1{2 "home": "主页",3 "about": "关于页面"4}Swedish translations
1{2 "greeting": "Hej världen!"3}1{2 "home": "Hem",3 "about": "Om"4}There are three functions that
next-i18nextexports, which you will need to use to translate your project:
appWithTranslation
This is a HOC which wraps your _app. This HOC is primarily responsible for adding a I18nextProvider.
1import { appWithTranslation } from "next-i18next";2import Header from "../components/Header";3import "../styles/globals.css";4
5function MyApp({ Component, pageProps }) {6 return (7 <>8 <Header />9 <Component {...pageProps} />10 </>11 );12}13
14export default appWithTranslation(MyApp);serverSideTranslations
This is an async function that you need to include on your page-level components, via either getStaticProps or getServerSideProps.
1import { serverSideTranslations } from "next-i18next/serverSideTranslations";2
3// export default function Home...4
5export async function getStaticProps({ locale }) {6 return {7 props: {8 ...(await serverSideTranslations(locale, ["common", "home"])),9 // Will be passed to the page component as props10 },11 };12}useTranslation
This is the hook which you'll actually use to do the translation itself. The useTranslation hook comes from react-i18next, but can be imported from next-i18next directly:
1// other imports2import { useTranslation } from "next-i18next";3
4export default function Home() {5 // We want to get the translations from `home.json`6 const { t } = useTranslation("home");7
8 // Get the translation for `greeting` key9 return <main>{t("greeting")}</main>;10}11
12// export async function getStaticProps...Let's also translate the links in the Header component.
1// other imports2import { useTranslation } from "next-i18next";3
4const Header = () => {5 // ...6
7 // If no argument is passed, it will use `common.json`8 const { t } = useTranslation();9
10 return (11 <header>12 <nav>13 <Link href="/">14 <a className={router.asPath === "/" ? "active" : ""}>{t("home")}</a>15 </Link>16 <Link href="/about">17 <a className={router.asPath === "/about" ? "active" : ""}>18 {t("about")}19 </a>20 </Link>21 </nav>22 {/* Other code */}23 </header>24 );25}The changes above will yield the following output:
The home page is translated properly; however, the about page is not. It is because we need to use serverSideTranslations in every route.
1// other imports2import { serverSideTranslations } from "next-i18next/serverSideTranslations";3
4// export default function About...5
6export async function getStaticProps({ locale }) {7 return {8 props: {9 ...(await serverSideTranslations(locale, ["common"])),10 },11 };12}Now both routes are translated
We only specified common in the serverSideTranslations because we don't plan on using anything in home.json in the About page.
I will fetch the translations of the About page's content from the backend. But before that, let's first check some cool stuff we can do with our translation library.
Nested translation keys and default translation
We are not limited to a flat JSON structure.
1{2 "title": "Stay up to date",3 "subtitle": "Subscribe to my newsletter",4 "form": {5 "firstName": "First name",6 "email": "E-mail",7 "action": {8 "signUp": "Sign Up",9 "cancel": "Cancel"10 }11 }12}We can omit some translation keys if we want it to use the default locale value(en in our case).
1{2 "title": "保持最新状态",3 "form": {4 "email": "电子邮箱",5 "action": {6 "cancel": "取消"7 }8 }9}Let's create a component which use the translations above.
1import { useTranslation } from "next-i18next";2import React from "react";3
4const SubscribeForm = () => {5 const { t } = useTranslation("newsletter");6
7 return (8 <section>9 <h3>{t("title")}</h3>10 <h4>{t("subtitle")}</h4>11
12 <form>13 <input placeholder={t("form.firstName")} />14 <input placeholder={t("form.email")} />15 <button>{t("form.action.signUp")}</button>16 <button>{t("form.action.cancel")}</button>17 </form>18
19 {/* For styling only */}20 <style jsx>{`21 form {22 max-width: 300px;23 display: flex;24 flex-direction: column;25 }26
27 input {28 margin-bottom: 0.5rem;29 }30 `}</style>31 </section>32 );33};34
35export default SubscribeForm;Render the form in pages/index.jsx and add newsletter in serverSideTranslations.
1import { serverSideTranslations } from "next-i18next/serverSideTranslations";2import { useTranslation } from "next-i18next";3import SubscribeForm from "../components/SubscribeForm";4
5export default function Home() {6 const { t } = useTranslation("home");7
8 return (9 <main>10 <div>{t("greeting")}</div>11 {/* Render the form here */}12 <SubscribeForm />13 </main>14 );15}16
17export async function getStaticProps({ locale }) {18 return {19 props: {20 ...(await serverSideTranslations(locale, [21 "common",22 "home",23 "newsletter", // Add newsletter translations24 ])),25 },26 };27}And now, we have this!
Built-in Formatting
It is very easy to format most of our data since next-i18next is using i18next under the hood.
Let's use the translation files below to showcase the formatting features.
1{2 "number": "Number: {{val, number}}",3 "currency": "Currency: {{val, currency}}",4 "dateTime": "Date/Time: {{val, datetime}}",5 "relativeTime": "Relative Time: {{val, relativetime}}",6 "list": "List: {{val, list}}",7 "weekdays": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]8}1{2 "number": "数: {{val, number}}",3 "currency": "货币: {{val, currency}}",4 "dateTime": "日期/时间: {{val, datetime}}",5 "relativeTime": "相对时间: {{val, relativetime}}",6 "list": "列表: {{val, list}}",7 "weekdays": ["星期一", "星期二", "星期三", "星期四", "星期五"]8}1{2 "number": "Nummer: {{val, number}}",3 "currency": "Valuta: {{val, currency}}",4 "dateTime": "Datum/tid: {{val, datetime}}",5 "relativeTime": "Relativ tid: {{val, relativetime}}",6 "list": "Lista: {{val, list}}",7 "weekdays": ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag"]8}Let's create a component which use the translations above.
1import { useTranslation } from "next-i18next";2import React from "react";3
4const BuiltInFormatsDemo = () => {5 const { t } = useTranslation("built-in-demo");6
7 return (8 <div>9 <p>10 {/* "number": "Number: {{val, number}}", */}11 {t("number", {12 val: 123456789.0123,13 })}14 </p>15 <p>16 {/* "currency": "Currency: {{val, currency}}", */}17 {t("currency", {18 val: 123456789.0123,19 style: "currency",20 currency: "USD",21 })}22 </p>23
24 <p>25 {/* "dateTime": "Date/Time: {{val, datetime}}", */}26 {t("dateTime", {27 val: new Date(1234567890123),28 formatParams: {29 val: {30 weekday: "long",31 year: "numeric",32 month: "long",33 day: "numeric",34 },35 },36 })}37 </p>38
39 <p>40 {/* "relativeTime": "Relative Time: {{val, relativetime}}", */}41 {t("relativeTime", {42 val: 12,43 style: "long",44 })}45 </p>46
47 <p>48 {/* "list": "List: {{val, list}}", */}49 {t("list", {50 // https://www.i18next.com/translation-function/objects-and-arrays#objects51 // Check the link for more details on `returnObjects`52 val: t("weekdays", { returnObjects: true }),53 })}54 </p>55 </div>56 );57};58
59export default BuiltInFormatsDemo;The more you look, the more you'll be amazed
Other translation functions to check
Fetching translations from backend
The work here is mainly done on the backend side or your CMS. On the frontend, we simply fetch the translations and pass a parameter to distinguish the language we want.
I created a simple endpoint to fetch the content of the about page. The result will change based on query param lang value.
1export default function handler(req, res) {2 const lang = req.query.lang || "en";3
4 if (lang === "sv") {5 return res.status(200).json({ message: "Jag är Code Gino" });6 } else if (lang === "zh-CN") {7 return res.status(200).json({ message: "我是代码吉诺" });8 } else {9 return res.status(200).json({ message: "I am Code Gino" });10 }11}Sample usage
/api/about: English/api/about?lang=zh-CN: Simplified Chinese/api/about?lang=sv: Svenska/api/about?lang=invalid: English
We can consume the API as usual (e.g. inside getServerSideProps, getStaticProps, useEffect, etc.).
In this example, let's fetch the translation inside the getStaticProps. We can get the locale value from the context, then append ?lang=${locale} to our request URL.
1// This import is not related to fetching translations from backend.2import { serverSideTranslations } from "next-i18next/serverSideTranslations";3
4export default function About({ message }) {5 return <h1>{message}</h1>;6}7
8export async function getStaticProps({ locale }) {9 const { message } = await fetch(10 // forward the locale value to the server via query params11 `https://next-i18n-example-cg.vercel.app/api/about?lang=${locale}`12 ).then((res) => res.json());13
14 return {15 props: {16 message,17 // The code below is not related to fetching translations from backend.18 ...(await serverSideTranslations(locale, ["common"])),19 },20 };21}The code above will yield the following result:
Conclusion
Internationalization is a complex requirement simplified in Next.js due to the built-in i18n routing support and the easy integration of next-i18next. And because next-i18next is using i18next, we can perform better translations with less code.
