Skip to content

Internationalization (i18n)

i18next logo

To demonstrate how to support multiple languages and locales we will support english and german in the Tozo app. To do this I like to use React-i18next due to it's nice hook support.

React-i18next is installed via npm and requires i18next,

Run this command in frontend/

npm install --save i18next react-i18next

We can also install i18next-browser-languagedetector to detect the browser's language and use it as the default,

Run this command in frontend/

npm install --save i18next-browser-languagedetector

With these installed we can setup internationalization by adding the following to frontend/src/i18n.ts,

import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";

const resources = {
  de: { translation: {} },
  en: { translation: {} },
};

i18n
  .use(initReactI18next)
  .use(LanguageDetector)
  .init({
    debug: process.env.NODE_ENV === "development",
    detection: { order: ["navigator"] },
    fallbackLng: "en",
    interpolation: {
      escapeValue: false
    },
    keySeparator: false,
    load: "languageOnly",
    resources,
  });

export default i18n;

Note that we have set english as the fallback language if the user's prefered language is one we don't support. We have also ignored regional aspects and focused on the language only i.e. en rather than en-GB.

Finally we need to import this file from frontend/src/index.tsx to complete the setup.

Translation keys

The translation associate arrays are keyed by a translation key. To help identify where each key is used I like to construct the key as the ComponentName.field, for example LanguageSelector.change.

Language picker

The language detector may not pick the right language for the user so we should offer the ability to choose the language from within the app. We can do this via a LanguageSelector component by placing the following in frontend/src/components/LanguageSelector.tsx,

import Button from "@material-ui/core/Button";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
import Tooltip from "@material-ui/core/Tooltip";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import LanguageIcon from "@material-ui/icons/Translate";
import React from "react";
import { useTranslation } from "react-i18next";
import { useUID } from "react-uid";

const LanguageSelector = () => {
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const { t, i18n } = useTranslation();
  const menuID = useUID();

  const onMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };

  const onMenuClose = () => {
    setAnchorEl(null);
  };

  const changeLanguage = (language: string) => () => {
    i18n.changeLanguage(language);
    onMenuClose();
  };

  return (
    <>
      <Tooltip enterDelay={300} title={t("LanguageSelector.change") as string}>
        <Button
          aria-haspopup="true"
          aria-owns={menuID}
          color="inherit"
          endIcon={<ExpandMoreIcon fontSize="small" />}
          onClick={onMenuOpen}
          startIcon={<LanguageIcon />}
        >
          {t("LanguageSelector.change")}
        </Button>
      </Tooltip>
      <Menu
        anchorEl={anchorEl}
        anchorOrigin={{
          horizontal: "right",
          vertical: "top",
        }}
        id={menuID}
        keepMounted
        onClose={onMenuClose}
        open={Boolean(anchorEl)}
        transformOrigin={{
          horizontal: "right",
          vertical: "top",
        }}
      >
        {/* eslint-disable i18next/no-literal-string */}
        <MenuItem onClick={changeLanguage("en")}>English</MenuItem>
        <MenuItem onClick={changeLanguage("de")}>Deutsch</MenuItem>
        {/* eslint-enable */}
      </Menu>
    </>
  );
};

export default LanguageSelector;

The translation entries for this component are,

de: {
  translation: {
    "LanguageSelector.change": "Deutsch",
  }
},
en: {
  translation: {
    "LanguageSelector.change": "English",
  }
}