2014 06 03 17 17 47 Sirolo

Monte Conero 1920x512

Monte Conero 1920x512

Setting Up Localization For A React App

User Rating: 5 / 5

Star ActiveStar ActiveStar ActiveStar ActiveStar Active
 

This article shows how to set up a react app for localization using react-i18next and i18next. The information here is mostly taken from the react-i18next site at the Step By Step Guide and Using hooks with react-i18next. The complete code is available at https://github.com/gwynsoft/react-with-localization-base

Starting from a base create-react-app setup (See Setting Up Nvm, Node.js And Create-React-App), first, install react-i18next and i18next:

npm i i18next react-i18next

Change index.js to:

import React from "react";
import ReactDOM from "react-dom";

import reportWebVitals from "./reportWebVitals";

import i18n from "i18next";
import { useTranslation, initReactI18next } from "react-i18next";

import "./index.css";

//import App from "./App";

i18n
  .use(initReactI18next) // passes i18n down to react-i18next
  .init({
    // the translations
    // (tip move them in a JSON file and import them,
    // or even better, manage them via a UI: https://react.i18next.com/guides/multiple-translation-files#manage-your-translations-with-a-management-gui)
    resources: {
      en: {
        translation: {
          "welcome-to-react": "Welcome to React and react-i18next",
        },
      },
      it: {
        translation: {
          "welcome-to-react": "Benvenuti a React e react-i18next",
        },
      },
    },
    lng: "it", // if you're using a language detector, do not define the lng option
    fallbackLng: "en",

    interpolation: {
      escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
    },
  });

function App() {
  const { t } = useTranslation();

  return <h2>{t("welcome-to-react")}</h2>;
}

// append app to dom
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Check that the Italian version works with npm start. Notice the i18n initialization block; we set the translations in resources by language and key, the language we want in lng and the fallback in fallbackLng. We use 2-letter identifiers for each language. The translation itself is performed by the t function from useTranslation. Note also that there is no fallback translation: if there is no language definition for a given key, the key itself is shown, however, it is better to use keys that reflect the module they are translating rather than actual words in a fallback language, both for clarity and organization.

At the moment, we have no way to change the language shown on the site, so lets use the html language tag to set it:

Change lng: 'en', to 'lng: document.querySelector('html').lang' and in public/index.html, change <html lang="en"> to <html lang="it"> and vice-versa: the browser message should change to reflect the language we set.

We want to set the current language automatically; we can do this with i18next-language-detector (See https://github.com/i18next/i18next-browser-languageDetector):

npm i i18next-browser-languagedetector

Add the following in index.js:

import React from "react";
import ReactDOM from "react-dom";
import LanguageDetector from 'i18next-browser-languagedetector';
import reportWebVitals from "./reportWebVitals"; import i18n from "i18next"; import { useTranslation, initReactI18next } from "react-i18next"; import "./index.css"; //import App from "./App"; i18n .use(initReactI18next) // passes i18n down to react-i18next
.use(LanguageDetector) .init({ // the translations // (tip move them in a JSON file and import them, // or even better, manage them via a UI: https://react.i18next.com/guides/multiple-translation-files#manage-your-translations-with-a-management-gui) resources: { en: { translation: { "welcome-to-react": "Welcome to React and react-i18next", }, }, it: { translation: { "welcome-to-react": "Benvenuti a React e react-i18next", }, }, }, lng:document.querySelector("html").lang, // if you're using a language detector, do not define the lng option fallbackLng: "en", detection: { // order and from where user language should be detected order: ["htmlTag", "cookie", "localStorage", "path", "subdomain"], }, interpolation: { escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape }, }); function App() { const { t } = useTranslation(); return <h2>{t("welcome-to-react")}</h2>; } // append app to dom ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();

and check functionality by changing the language in the index.html file as above.

The language detector detects the language to set from a variety of sources such as cookies, local storage, and the html language attribute in the order set by detection: order.

If all goes well, add ' caches: ['cookie'],' after the 'order' key in .init. This automatically sets the user's 'i18next' cookie to the browser language. we can change the name of the cookie by adding lookupCookie: "my-cookie-name" under the caches: line, where my-cookie-name is the name of the cookie we want to set/read. Change the order: key so that cookie comes first; now we can change the page language by changing the value of the cookie and reloading the page.

As the number of options we add increases, the index.js file starts to become unwieldy so the next thing to do is to move our initialization out to another file. Create a file i18n.js in /src containing the following:

/src/i18n.js

import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';

i18n
  // load translation using http -> see /public/locales
  // learn more: https://github.com/i18next/i18next-http-backend
  .use(Backend)
  // detect user language
  // learn more: https://github.com/i18next/i18next-browser-languageDetector
  .use(LanguageDetector)
  // pass the i18n instance to react-i18next.
  .use(initReactI18next)
  // init i18next
  // for all options read: https://www.i18next.com/overview/configuration-options
  .init({
    fallbackLng: 'en',
    debug: true,
detection: { // order and from where user language should be detected order: ["path", "cookie", "htmlTag", "localStorage", "subdomain"], caches: ["cookie"],
lookupCookie: "myapp-lang", // Change this to the name of the cookie you want }, interpolation: { escapeValue: false, // not needed for react as it escapes by default }, }); export default i18n;

We don't want to keep translations in our i18n.js file but load them dynamically so we will use http-backend for this (See https://github.com/i18next/i18next-http-backend) :

npm install i18next-http-backend

Now transfer the translations in index.js to individual files under public/assets/locales/[lang]/translation.json eg for English:

/public/locales/en/translation.json:

{
"welcome-to-react": "Welcome to React and react-i18next"
}

then remove the unneeded lines from .init in index.js to end up with this:

/src/index.js

import React from "react";
import ReactDOM from "react-dom";

import reportWebVitals from "./reportWebVitals";

import "./i18n";
import { useTranslation } from "react-i18next";

import "./index.css";

function App() {
  const { t } = useTranslation();

  return <h2>{t("welcome-to-react")}</h2>;
}

// append app to dom
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

By default backend loads the translation files from /public/locales/{{lng}}/translation.json. If you want to keep them in another location, just add the key backend: { loadPath: "path-to-locales/{{lng}}/translation.json" }, to .init .

If you run npm start at this point you will get a loading error. For now, to avoid it add the key react: { useSuspense: false}, after detection.

Finally, change the detection key as follows:

    detection: {
      // order and from where user language should be detected
      order: ["cookie", "htmlTag", "localStorage", "path", "subdomain"],
      caches: ["cookie"],
    },

Create A Language Switcher

So far we can have our page in the user's browser's preferred language, but we need a way to allow the user to change language at will and to persist the choice in some way. To do this we will need to set up an app skeleton with a language switcher.

In index.js remove the App function and add import App from './App' . We can also remove useTranslation from the imports. Now add supportedLngs: ['en', 'it'], as first line under .init. in /src/i18n.js

For the App component we will need to use cookies so first install them along with some flag icons:

npm i js-cookie flag-icon-css

Substitute the contents of App.js with the following:

import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import i18next from "i18next";

import "flag-icon-css/css/flag-icon.min.css";
import "./App.scss";
const languages = [ { code: "it", name: "Italiano", country_code: "it", }, { code: "en", name: "English", country_code: "gb", }, ]; function Navbar() { const { t, i18n } = useTranslation(); const currentLanguageCode = i18n.language; return ( <div className="navbar"> <div className="switcher"> {languages.map(({ code, name, country_code }) => ( <button style={{ opacity: currentLanguageCode === code ? 0.3 : 1, }} className="switcher__btn" key={country_code} onClick={() => { i18next.changeLanguage(code); }} > <span className={`flag-icon flag-icon-${country_code}`}></span> {name} </button> ))} </div> </div> ); } function App() { const { t, i18n } = useTranslation(); const currentLanguage = i18n.language; useEffect(() => { document.title = t("app.title"); }, [currentLanguage, t]); return ( <div> <Navbar /> <div className="container"> <h1>{t("app.welcome")}</h1> </div> </div> ); } export default App;

Now change /src/App.css to /src/App.scss and fill it with the following code:

/src/App.scss

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

.navbar {
  display: flex;
  justify-content: flex-end;
  padding: 0.5rem 2rem;
  background-color: #444;
}

.switcher, navigation {
  display: flex;
  justify-content: space-evenly;

  &__btn {
    margin: 0 0.2rem;
    border: none;
    background-color: transparent;
    color: #ddd;
    font-family: inherit;
font-size: 1.2rem; padding: 0; cursor: pointer; .flag-icon { margin-right: 0.2rem; } } } .container { text-align: center; }

Try out the switcher.

To add a couple of alternate pages we will need to add routing to our app with react-router-dom. This will allow us to change pages without the browser reloading.

npm i react-router-dom

Change /src/App.js as follows:

/src/App.js

import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import i18next from "i18next";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";

import "flag-icon-css/css/flag-icon.min.css";
import "./App.scss";
const languages = [
  {
    code: "it",
    name: "Italiano",
    country_code: "it",
  },
  {
    code: "en",
    name: "English",
    country_code: "gb",
  },
];

function Navbar() {
  const { t, i18n } = useTranslation();

  const currentLanguageCode = i18n.language;

  return (
    <div className="navbar">
      <div className="navigation">
        <button className="switcher__btn">
          <Link to="/">{t("navbar.home")}</Link>
        </button>
        <button className="switcher__btn">
          <Link to="/about-us">{t("navbar.about")}</Link>
        </button>
      </div>
      <div className="switcher">
        {languages.map(({ code, name, country_code }) => (
          <button
            style={{
              opacity: currentLanguageCode === code ? 0.3 : 1,
            }}
            className="switcher__btn"
            key={country_code}
            onClick={() => {
              i18next.changeLanguage(code);
            }}
          >
            <span className={`flag-icon flag-icon-${country_code}`}></span>
            {name}
          </button>
        ))}
      </div>
    </div>
  );
}

function Home() {
  const { t } = useTranslation(); // Originally in App function
  return (
    <div className="container">
      <h1>{t("home.message")}</h1>
    </div>
  );
}

function AboutUs() {
  const { t } = useTranslation();
  return (
    <div className="container">
      <h1>{t("about.message")}</h1>
    </div>
  );
}

function App() {
  const { t, i18n } = useTranslation();
  const currentLanguage = i18n.language; //cookies.get("i18next") || "en";
  //const currentLanguage = languages.find((l) => l.code === currentLanguageCode);

  useEffect(() => {
    document.title = t("app.title");
  }, [currentLanguage, t]);

  return (
    <Router>
      <div>
        <Navbar />
        <Switch>
          <Route path="/" exact>
            <Home></Home>
          </Route>
          <Route path="/about-us" exact>
            <AboutUs></AboutUs>
          </Route>
          <div className="container">
            <h1>{t("app.welcome")}</h1>
          </div>
        </Switch>
      </div>
    </Router>
  );
}

export default App;

 Don't forget to update the translation.json files to include the new keys:

/public/locales/en/translation.json

{
  "app.welcome": "Welcome to React and react-i18next",
  "app.title": "App Title",

  "navbar.home": "Home",
  "navbar.about": "About Us",

  "home.message": "Home Page",
  "about.message": "About Us"
}

/public/locales/it/translation.json

{
  "app.welcome": "Benvenuti a React e react-i18next",
  "app.title": "Titolo del App",

  "navbar.home": "Home",
  "navbar.about": "Chi Siamo",

  "home.message": "Pagina Home",
  "about.message": "Chi Siamo"
}

We should also bring "path" to the front of the order array in /src/i18n.js:

...

detection: { // order and from where user language should be detected order: ["path", "cookie", "htmlTag", "localStorage", "subdomain"], caches: ["cookie"],
lookupCookie: "myapp-lang", // Change this to the name of the cookie you want },
...

Add SEO to localization

At this point, clicking on either of the language buttons will change the page to the appropriate language and set the 'i18next' cookie to that language's code. On a first visit, without a cookie, the app will check for a preferred language and, if it is in our array of supported languages, will set it and the cookie, falling back to our default language ('en') if it isn't. Unfortunately, the html lang attribute does not change to reflect the language of the page, which might be a drawback for SEO. We can change this by adding code to the Navbar and App components:

...
function Navbar() { const currentLanguageCode = cookies.get("i18next") || "en"; return ( <div className="navbar"> <div className="navigation"> <button className="switcher__btn"> <Link to="/">{t("navbar.home")}</Link> </button> <button className="switcher__btn"> <Link to="/about-us">{t("navbar.about")}</Link> </button> </div> <div className="switcher"> {languages.map(({ code, name, country_code }) => ( <button style={{ opacity: currentLanguageCode === code ? 0.3 : 1, }} className="switcher__btn" key={country_code} onClick={() => { i18next.changeLanguage(code); document.querySelector("html").setAttribute("lang", code); // Added to change language code in html lang property
let here = window.location.pathname; here = here.slice(3); var there = "/" + code + here; window.location = there; }} > ...
function App() { const { t, i18n } = useTranslation(); const currentLanguage = i18n.language; useEffect(() => { document.title = t("app_title"); }, [currentLanguage, t]);

document.querySelector("html").setAttribute("lang", currentLanguage); // Added to change language code in html lang property return ( ...

If we want our page url and language to be updated in the address bar we can use Redirect.

/src/App.js

...
<Switch> <Route path="/" exact> <Redirect to={`/${currentLanguage}`} /> </Route> <Route path="/:lang" exact> <Home /> </Route> <Route path="/:lang/about-us"> <AboutUs /> </Route> </Switch>
...

Make Apache Work with React

If we want to serve our finished app from apache we must add some lines to the <Directory > group in our .conf file:

    RewriteEngine on
    # Don't rewrite files or directories
    RewriteCond %{REQUEST_FILENAME} -f [OR]
    RewriteCond %{REQUEST_FILENAME} -d
    RewriteRule ^ - [L]
    # Rewrite everything else to index.html to allow html5 state links
    RewriteRule ^ index.html [L]

 {jcomments on}