---
title: i18n setting
tags: Projects
---
:::info
Tech stack: React, Next.js, next-i18next
:::
## Related files
* _app.tsx
* Set the whole app receive i18n propreties
* next-i18next.config.js
* Edit router for different lang
* Edit default lang
* src\i18n\config.ts
* Edit lang switcher options
* src\i18n\localests
* Edit our i18n files in TypeScript
* src\i18n\generateLocales.js
* add script in **package.json**
* Execute this file to convert our i18n ts files to json files
* @types\react-i18next.d.ts

## Set the whole app receive i18n propreties
* add configuration in **_app.tsx**
```typescript=
import { appWithTranslation } from 'next-i18next';
const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />;
export default appWithTranslation(MyApp);
```
## Edit router for different lang, default lang
* next-i18next.config.js
```javascript=1
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['cn', 'ar', 'en', 'ms', 'id', 'vi', 'zh'],
localePath: './src/i18n/locales'
},
react: {
useSuspense: false
}
};
```
## Edit lang switcher options
* src\i18n\config.ts
```typescript=
/** Files to modify locales
* next-i18next.config.js
* */
/** Please use ISO codes to name locales as possible
* https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
* */
import nextI18next from '~/next-i18next.config';
export type Locales = 'cn' | 'ar' | 'en' | 'ms' | 'id' | 'vi' | 'zh';
export const locales: Locales[] = nextI18next.i18n.locales as Locales[];
type localesOptions = {
code: Locales;
value: string;
};
export const localesOptions: localesOptions[] = [
{ code: 'cn', value: '简体' },
{ code: 'ar', value: 'بالعربية' },
{ code: 'en', value: 'English' },
{ code: 'ms', value: 'Malay' },
{ code: 'id', value: 'Indonesia' },
{ code: 'vi', value: 'Việt Nam' },
{ code: 'zh', value: '繁體' }
];
```
* LangSelector.tsx
- See below
## Edit our i18n files in TypeScript
Because we wanna have type intelligence in our project, but ==next-i18next== yet not support TypeScript, so we write our i18n files in TypeScript, and convert files to json ourselves.
* src\i18n\localests
## Execute this file to convert our i18n ts files to json files
I first wrote this in shell script, but shell commands are different in Linux, Windows and IOS, so I wrote JavaScript version then it's compatible in all above OS.
Basically, it coverts all ts files in ==src\i18n\localests== to json files, and puts all files in ==src\i18n\locales==, you can modify the folder name.
* src\i18n\generateLocales.js
```javascript=
const des = 'locales';
const src = 'localests';
const outputFormat = '.json';
const desPath = `./${des}`;
const srcPath = `./${src}`;
const npmScript = 'format';
const {
promises: fs,
readdirSync,
rename,
readFile,
writeFile
} = require('fs-extra');
const { exec } = require('child_process');
/* eslint-disable */
(async () => {
// Remove old folder if exist
await fs.rmdir(desPath, { recursive: true }, (err) => {
if (err) return console.error(err);
});
console.log(`1. Remove ${des} folder if exist`);
// Copy folder exclude '@types'
await copyDir(srcPath, desPath, '@types');
console.log(`2. Copy folder`);
let filenames = readdirSync(desPath);
for (const file of filenames) {
const path = `${desPath}/${file}`;
let jsonFiles = readdirSync(path);
for (const jsonFile of jsonFiles) {
const jsonFilePath = `${path}/${jsonFile}`;
// Read file
readFile(jsonFilePath, (err, fileBuffer) => {
if (err) {
console.error(err.message);
}
const fileContent = fileBuffer.toString();
const contentWeWant = fileContent.match(/(?<= = {)(.*)[^}]*/);
const outputContent = `{${contentWeWant[0]}}`;
if (contentWeWant[0]) {
// Rewrite the file with content we want
writeFile(jsonFilePath, outputContent, (err) => {
if (err) {
return console.log(err);
} else {
const oldPath = jsonFilePath;
const newPath = jsonFilePath.replace(/\.[^.]+$/, outputFormat);
// Rename file from whatever to "outputFormat"
rename(oldPath, newPath, (err) => {
if (err) {
return console.log(err);
}
});
}
});
}
});
}
}
console.log('3. Rewrite/Rename file from whatever to .json');
exec(`npm run ${npmScript}`);
console.log('4. Format json');
})();
const path = require('path');
const copyDir = async (src, dest, escapeFolder = undefined) => {
await fs.mkdir(dest, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (!entry.name.includes(escapeFolder)) {
entry.isDirectory()
? await copyDir(srcPath, destPath)
: await fs.copyFile(srcPath, destPath);
}
}
};
```
* add script in **package.json**
```json=
{
...
"scripts": {
...
"locales": "npx prettier --write ./src/i18n/**/*.json"
}
...
}
```
## LangSelector.tsx
:::warning
The points we should concern
* Store user's selection in LocalStorage
* Browser's function in mobile is different from PC's
* Browser use [ISO code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) as language code
* Because we have both traditional Chinese and simplified chinese for the website, so we use 'zh' for traditional Chinese and 'cn' for simplified chinese.
:::
### Logic explanation
* First time or no data in cookie
* Detect user's browser's language
* Check if the user is using mobile or PC
* If browser's language matches the website's language, use it, if not, use default language
* Have data in cookie
* Check data in cookie, if value matches the website's language, use the value, if not, then use default language
* Codes
```typescript=
import { locales, Locales, localesOptions } from '@/i18n/config';
import { getCookie, isUsingMobile, setCookie } from '@/utils';
import { ChevronDownIcon } from '@chakra-ui/icons';
import {
Button,
Image,
Menu,
MenuButton,
MenuItem,
MenuList,
Text
} from '@chakra-ui/react';
import { i18n } from 'next-i18next';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
const LangSelector: React.FC<{}> = () => {
useEffect(() => {
getCurrentLanguage();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const [language, setLanguage] = useState(i18n?.languages[0]);
const router = useRouter();
const currentLang = router.locale as Locales;
const getCurrentLanguage = () => {
const langInCookie = getCookie('lang') as Locales;
if (
langInCookie &&
locales.includes(langInCookie) &&
langInCookie !== currentLang
) {
handleSetLanguage(langInCookie);
} else {
let lang = i18n?.languages[0] as Locales;
// Check browser language manually
const isCN =
(window as any)?.navigator.languages.includes('zh-CN') ||
(window as any)?.navigator.languages.includes('zh-cn') ||
null;
if (isCN) {
lang = 'cn';
} else if (isUsingMobile()) {
// If it's a mobile, use substr(0, 2) to get lang ISO code, if it matches any of our app locales, set it as language
const matchLang = navigator.languages
.find((x) => x.substr(0, 2) === navigator.language.substr(0, 2))
.substr(0, 2) as Locales;
if (i18n?.languages.includes(matchLang)) {
lang = matchLang;
}
}
// to save locale in cookie
setCookie('lang', lang, 30);
}
};
const handleLanguageChange = (locale: Locales) => {
handleSetLanguage(locale);
};
const handleSetLanguage = (locale: Locales) => {
// to show option in select
setLanguage(locale);
// to change router locale
router.push(router.asPath, router.asPath, { locale: locale });
// to save locale in cookie
setCookie('lang', locale, 30);
};
return (
<Menu>
<MenuButton minW="90px" as={Button} rightIcon={<ChevronDownIcon />}>
<Image src={`../assets/images/${language}.png`} alt={language}></Image>
</MenuButton>
<MenuList minW={{ base: '90px', md: '150px' }} zIndex={3}>
{locales.map((locale: Locales) => (
<MenuItem
key={locale}
onClick={() => handleLanguageChange(locale)}
_hover={{
bgColor: '#CBD5E0'
}}
>
<Image
border="1px"
src={`../assets/images/${locale}.png`}
alt={locale}
></Image>
<Text ml={4} display={{ base: 'none', md: 'block' }}>
{localesOptions.find((x) => x.code === locale)?.value || locale}
</Text>
</MenuItem>
))}
</MenuList>
</Menu>
);
};
export default LangSelector;
```
## src\utils\isUsingMobile.ts
```typescript=1
export const isIOS = () => {
if (process.browser) {
return (
navigator.userAgent.match(/iPhone/i) ||
navigator.userAgent.match(/iPad/i) ||
navigator.userAgent.match(/iPod/i)
);
}
};
export const isUsingMobile = () => {
if (process.browser) {
return (
isIOS() ||
navigator.userAgent.match(/Android/i) ||
navigator.userAgent.match(/webOS/i) ||
navigator.userAgent.match(/BlackBerry/i) ||
navigator.userAgent.match(/Windows Phone/i)
);
}
};
```
## src\utils\cookie.ts
```typescript=1
export function setCookie(cname: string, cvalue: string, exdays = -1) {
const d = new Date();
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
const expires = 'expires=' + d.toUTCString();
document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/';
}
export function getCookie(cname: string) {
const name = cname + '=';
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i += 1) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return '';
}
```
## @types\react-i18next.d.ts
TypeScript magic!!
[srouce](https://medium.com/geekculture/strong-typed-i18n-in-react-c43281de720c)
```typescript=
import IResources from '../src/i18n/localests/@types';
// react-i18next versions higher than 11.11.0
declare module 'react-i18next' {
// eslint-disable-next-line
interface CustomTypeOptions {
defaultNS: 'en';
resources: IResources;
}
}
```