Multilanguage site with Nextjs

リン (linh)
Goalist Blog
Published in
4 min readSep 4, 2022

I. What do you need

- create Next app as normal
- install Typescript package
(trust me, this makes life easier)
- Exceljs
(we’re going to add translation in excel file so this library will help to read and process the excel file)

II. The cons and the idea

Imagine having a 3-language site. Here are the cons:

- we need 3 separate files to declare the translation.
- everytime we need to add a translation, we go to each site and add them with the same key.
- if something went wrong, go back and forth between 3 files to debug is not fun.

SO, the idea is, to have 1 excel file to keep those translations, we write the key only once, it’s easier to track and debug.

III. Implementation

1. Create the excel file
The file format would be as any format you like, mine is like below.
When i read the file, i can export the key as object key and the language as value. This article will handle 2 languages only, but the process is same for more languages.

2. Use Exceljs
Without this library, the idea would have never been existed.
With the library’s help, we can process the file and export the translation.

First, we get the excel file.
Create a js file to do the logic.
There are other way that you can use to get the file.
Here, i know my file has only 1 sheet, so the sheet i want to read will be the first one. You can also get the sheet by it’s name.

//in the js file
const fs = require(‘fs’);
const path = require(‘path’);
const trim = require(‘lodash.trim’);
const Excel = require(‘exceljs’);
const FILE_PATH = “./src/utils/langs.xlsx”;const translate = () => {
const langFile = new Excel.Workbook();
langFile.xlsx.readFile(FILE_PATH).then(() => {
const sheet = langFile.worksheets[0];

}

translate();

Second, loop through each row of the sheet (except the first 1, because it is the title row that we dont want to add to our translation file). Then, for English, we’d want to read 1st cell of the row as object key and 2nd cell of row as value, and for Japanese, read 3rd cell of row as value. I’m not implementing empty row check but you can also add logic to do that, if needed.

let languages = [
{
lang: “en”,
translation: {},
},
{
lang: “ja”,
translation: {},
},
];
//row start from 2 because 1st row is the title
for(let r = 2; r <= sheet.rowCount; r++) {
const row = sheet.getRow(r);
languages[0].translation[row.getCell(1).value] = row.getCell(2).value;
languages[1].translation[row.getCell(1).value] = row.getCell(3).value;
}
// we then save the translation file as JSON
languages.forEach((language, index) => {
fs.writeFileSync(path.join(__dirname, `locales/${language.lang}/${language.lang}.json`),
JSON.stringify(language.translation)
);
});

Third, we generate the typescript file. This is optional but would be smart to have this. Otherwise, everytime you want to access the translation file, you either have to remember the field name or constantly check it.

let keys = sheet
.getColumn(“A”)
.values.filter((v, i) => i > 1)
.map((key) => trim(key));
// export langType.ts
let langTypeData = [];
keys.forEach((key, index) => {
langTypeData.push(`${key} = ‘${key}’,`);
})
let langType = `${langTypeData.join(‘\n’)}`;
console.log(`Exported to lang.ts`);
fs.writeFileSync(path.join(__dirname, “./langType.ts”), langType);

Because i want to generate enum type but haven’t figured out how to do it with node (feel free to help me, please), so after the langType.ts was created, i need to add export enum LANG { // generated content } to wrap generated content.

Finally, we can add a script to our package.json to generate everything again whenever we update excel translation file.

In the end, we’ll have 3 files like this.

// langType.ts
export enum LANG {
Weight = ‘Weight’,
Height = ‘Height’,
Ability = ‘Ability’, …
}
// en.json
{
“Weight”:”Weight”,
“Height”:”Height”,
“Ability”:”Ability”,

}
// ja.json
{
“Weight”:”体重”,
“Height”:”身長”,
“Ability”:”能力”,

}

3. Create useLocale()
Why? Yes, you don’t have to care about this if you prefer writting the same piece of code everywhere you need to use translation file.

import { useRouter } from ‘next/router’;
import en from ‘./locales/en/en.json’;
import ja from ‘./locales/ja/ja.json’;
export const useLocale = () => {
const { locale, asPath } = useRouter();
const t =
locale === ‘ja’ ? ja : en;
return { locale, t, asPath };
};

So, this help you not to repeat line 1,2,3,6,8 everywhere. Instead, wherever you need a translation, just do this const {t} = useLocale(). Voila!

4. Usage
- To switch the language, use the Link from nextjs

<Link href={{ pathname, query }} as={asPath} locale={locale === ‘ja’ ? ‘en’ : ‘ja’}>
<a>{locale.toLocaleUpperCase()}</a>
</Link>

- Replace fixed text with the translation field

<u>Weight:</u> → <u>{t[LANG.Weight]}:</u>

IV. Conclusion

I believe each person will have their own technique to handle multilanguage site. I personally like this way, it would be an option, in case you want a change.
Here’s the repo and demo site for the article for your overview. Thank you and see you in another blog 😄

Github: https://github.com/ellenle26/demo-i18n-nextjs
Demo: http://pokedex-l3.netlify.app

--

--

リン (linh)
Goalist Blog

A career-changed-non-tech-background point of view.