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:
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
sh
1npm install next-i18next
Create a next-i18next.config.js and update next.config.js
This is an async function that you need to include on your page-level components, via either getStaticProps or getServerSideProps.
pages/index.jsx
123456789101112import{ serverSideTranslations }from"next-i18next/serverSideTranslations";// export default function Home...exportasyncfunctiongetStaticProps({ locale }){return{props:{...(awaitserverSideTranslations(locale,["common","home"])),// Will be passed to the page component as props},};}
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:
pages/index.jsx
123456789101112// other importsimport{ useTranslation }from"next-i18next";exportdefaultfunctionHome(){// We want to get the translations from `home.json`const{ t }=useTranslation("home");// Get the translation for `greeting` keyreturn<main>{t("greeting")}</main>;}// export async function getStaticProps...
Let's also translate the links in the Header component.
components/Header.jsx
12345678910111213141516171819202122232425// other importsimport{ useTranslation }from"next-i18next";constHeader=()=>{// ...// If no argument is passed, it will use `common.json`const{ t }=useTranslation();return(<header><nav><Linkhref="/"><aclassName={router.asPath==="/"?"active":""}>{t("home")}</a></Link><Linkhref="/about"><aclassName={router.asPath==="/about"?"active":""}>{t("about")}</a></Link></nav>{/* Other code */}</header>);}
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.
pages/about.jsx
123456789101112// other importsimport{ serverSideTranslations }from"next-i18next/serverSideTranslations";// export default function About...exportasyncfunctiongetStaticProps({ locale }){return{props:{...(awaitserverSideTranslations(locale,["common"])),},};}
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.
locales/en/newsletter.json
123456789101112{"title":"Stay up to date","subtitle":"Subscribe to my newsletter","form":{"firstName":"First name","email":"E-mail","action":{"signUp":"Sign Up","cancel":"Cancel"}}}
We can omit some translation keys if we want it to use the default locale value(en in our case).
Let's create a component which use the translations above.
components/SubscribeForm.jsx
1234567891011121314151617181920212223242526272829303132333435import{ useTranslation }from"next-i18next";importReactfrom"react";constSubscribeForm=()=>{const{ t }=useTranslation("newsletter");return(<section><h3>{t("title")}</h3><h4>{t("subtitle")}</h4><form><inputplaceholder={t("form.firstName")}/><inputplaceholder={t("form.email")}/><button>{t("form.action.signUp")}</button><button>{t("form.action.cancel")}</button></form>{/* For styling only */}<stylejsx>{` form { max-width: 300px; display: flex; flex-direction: column; } input { margin-bottom: 0.5rem; } `}</style></section>);};exportdefaultSubscribeForm;
Render the form in pages/index.jsx and add newsletter in serverSideTranslations.
pages/index.jsx
123456789101112131415161718192021222324252627import{ serverSideTranslations }from"next-i18next/serverSideTranslations";import{ useTranslation }from"next-i18next";importSubscribeFormfrom"../components/SubscribeForm";exportdefaultfunctionHome(){const{ t }=useTranslation("home");return(<main><div>{t("greeting")}</div>{/* Render the form here */}<SubscribeForm/></main>);}exportasyncfunctiongetStaticProps({ locale }){return{props:{...(awaitserverSideTranslations(locale,["common","home","newsletter",// Add newsletter translations])),},};}
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.
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.
pages/api/about.js
1234567891011exportdefaultfunctionhandler(req, res){const lang = req.query.lang||"en";if(lang ==="sv"){return res.status(200).json({message:"Jag är Code Gino"});}elseif(lang ==="zh-CN"){return res.status(200).json({message:"我是代码吉诺"});}else{return res.status(200).json({message:"I am Code Gino"});}}
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.
pages/about.jsx
123456789101112131415161718192021// This import is not related to fetching translations from backend.import{ serverSideTranslations }from"next-i18next/serverSideTranslations";exportdefaultfunctionAbout({ message }){return<h1>{message}</h1>;}exportasyncfunctiongetStaticProps({ locale }){const{ message }=awaitfetch(// forward the locale value to the server via query params`https://next-i18n-example-cg.vercel.app/api/about?lang=${locale}`).then((res)=> res.json());return{props:{ message,// The code below is not related to fetching translations from backend....(awaitserverSideTranslations(locale,["common"])),},};}
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.