---
# System prepended metadata

title: i18n setting
tags: [Projects]

---

---
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;
  }
}
```