--- 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 ![](https://i.imgur.com/2XxYBR2.jpg) ## 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; } } ```