React and i18n - translate by adding the locale in the URL

IseNgaRt picture IseNgaRt · Jul 12, 2019 · Viewed 7.6k times · Source

Hello I'm building a demo application just to learn React and I'm kinda stuck with the translation proccess. What I'm trying to do is have a multi-language website with default language the "Greek" and secondary "English". When Greek are enabled the URL shouldn't contain any locale in the URL but when English are, the URLS should be rewritten with /en/.

i18n Config

import translationEn from './locales/en/translation';
import translationEl from './locales/el/translation';
import Constants from './Utility/Constants';

i18n
    .use(Backend)
    .use(LanguageDetector)
    .use(initReactI18next)
    .init({
        fallbackLng: 'el',
        debug: true,
        ns: ['translations'],
        defaultNS: 'translations',
        detection: {
            order: ['path'],
            lookupFromPathIndex: 0,

        },
        resources: {

            el: {
                translations: translationEl
            },
            en: {
                translations: translationEn

            }
        },

        interpolation: {
            escapeValue: false, // not needed for react as it escapes by default

        }
    }, () => {

        // Why the fuck this doesnt work automatically with fallbackLng ??

        if (!Constants.allowed_locales.includes(i18n.language)) {

            i18n.changeLanguage(Constants.default_locale);
        }
        return;


    });

i18n.on('languageChanged', function (lng) {

    // if somehow it get injected

    if (!Constants.allowed_locales.includes(i18n.language)) {
        i18n.changeLanguage(Constants.default_locale);
    }


    // if the language we switched to is the default language we need to remove the /en from URL
    if (Constants.default_locale === lng) {

        Constants.allowed_locales.map((item) => {
            if (window.location.pathname.includes("/" + item)) {
                let newUrl = window.location.pathname.replace("/" + item, "");
                window.location.replace(newUrl);

            }
        })


    } else { // Add the /en in the URL

        // @todo: add elseif for more than 2 langs because this works only for default + 1 more language

        let newUrl = "/" + lng + window.location.pathname;
        window.location.replace(newUrl);


    }

});


export default i18n;

I used the Language Detector plugin with detection from path so it can parse the locale from URL. Now without the callback function I added at the initialization, the LanguageDetector would set correctly the language if the url was www.example.com/en/ or www.example.com/el or www.example.com/en/company. BUT if I directly accessed the www.example.com/company (before visiting first the home so the locale would be set) i18n would set the locale/language to "company" !!!

There is an option for fallbackLng that I thought that would set the language to what you config it if the LanguageDetector dont detect it, but seems that there isnt an option to set available languages or default language to i18n ( or I'm an idiot and couldnt find it ) so LanguageDetector set whatever he finds in the URL. To fix this I added a Constants file and the callback function above.

Contants.js

const Constants = {
    "allowed_locales": ['el','en'],
    "default_locale": 'el'
}

export default Constants;

Also I added an event Handler that fires on LanguageChange so it will rewrite the URL with /en/ if English is active or remove the /el/ if Greek is.

index.js


ReactDOM.render(
    <BrowserRouter>
        <I18nextProvider i18n={i18n}>
            <App/>
        </I18nextProvider>
    </BrowserRouter>
    ,
    document.getElementById('root')
);

App.js


class App extends React.Component {


    render() {

        return (

            <div className="App">
                <Suspense fallback="loading">
                    <Header {...this.props}/>
                    <Routes {...this.props} />
                </Suspense>
            </div>

        );
    }
}

export default withTranslation('translations')(App);

Nothing special for index.js and App.js

Header Component


class Header extends React.Component {


    linkGenerator(link) {
        // if the current language is the default language dont add the lang prefix
        const languageLocale = this.props.i18n.options.fallbackLng[0] === this.props.i18n.language ? null : this.props.i18n.language;
        return languageLocale ? "/" + languageLocale + link : link;
    }

    render() {

        return (


            <div className="header">

                <Navbar bg="light" expand="lg">
                    <Container>
                        <Navbar.Brand className="logo" href="/"> <Image src="/assets/logo.png" rounded/>
                        </Navbar.Brand>
                        {/*Used For Mobile Navigation*/}
                        <Navbar.Toggle aria-controls="basic-navbar-nav"/>
                        <Navbar.Collapse id="basic-navbar-nav" className="float-right">
                            <Nav className="ml-auto">
                                <Nav.Link as={NavLink} exact to={this.linkGenerator("/")}>{this.props.t('menu.home')}</Nav.Link>
                                <Nav.Link as={NavLink} to={this.linkGenerator("/company")}>{this.props.t('menu.company')}</Nav.Link>

                            </Nav>
                            <Nav className="mr-auto">
                                {this.props.i18n.language !== "el" ? <button onClick={() => this.props.i18n.changeLanguage('el')}>gr</button>
                                    : null}
                                {this.props.i18n.language !== "en" ? <button onClick={() => this.props.i18n.changeLanguage('en')}>en</button>
                                    : null}
                            </Nav>

                        </Navbar.Collapse>

                    </Container>
                </Navbar>
            </div>


        )

    }
}

export default Header

In order to create the urls of the Menu with the locale, I created the linkGenerator function

And Finally in my Routes Component which handle all the routing, I added a constant before the actual url so it will work for all of theese /page , /el/page , /en/page

Routes Component

import React from 'react';
import {Switch, Route} from 'react-router-dom';

import CompanyPage from './Pages/CompanyPage';
import HomePage from './Pages/HomePage';
import NotFound from './Pages/NotFound';


class Routes extends React.Component {


    render() {
        const localesString = "/:locale(el|en)?";

        return (

            <Switch>
                <Route exact path={localesString + "/"} component={HomePage}/>
                <Route path={localesString + "/company"} component={CompanyPage}/>
                <Route component={NotFound}/>
            </Switch>
        );
    }


}

export default Routes

The code somehow works but is full of hacks like :

  1. Extra config file ( constants.js )

  2. Callback function to change the language from "company" to default locale. ( this triggers 2 page reloads)

  3. functions to handle the locale in the menu and routes

etc..

Isnt there any "build-in" functionality or a better approach in order to achieve the same thing without the above hacks?

Answer

Cristiano Santos picture Cristiano Santos · Dec 8, 2019

I needed the same thing and I found out that you can set the whitelistproperty of i18n.init options and specify the supported languages. After that, if you set checkWhitelist: true inside your detection options, the LanguageDetector will only match the language if it exists on the whitelist array.

Anyway, you still need to define the languageChanged event in order to redirect the page when matching the default language but, you no longer need to redirect if it is another supported language (at least I don't need).

Last thing that I did differently is that I defined the languageChanged event first and only then called the i18n.init, so that it would trigger the event already for the first time that it sets the language.

Here's my code:

i18n.js

import i18n from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'

i18n.on('languageChanged', function (lng) {
  // if the language we switched to is the default language we need to remove the /en from URL
  if (lng === i18n.options.fallbackLng[0]) {
    if (window.location.pathname.includes('/' + i18n.options.fallbackLng[0])) {
      const newUrl = window.location.pathname.replace('/' + i18n.options.fallbackLng[0], '')
      window.location.replace(newUrl)
    }
  }
})

i18n
  .use(LanguageDetector)
  .init({
    resources: {
      en: {
        translation: require('./translations/en.js').default
      },
      pt: {
        translation: require('./translations/pt.js').default
      }
    },
    whitelist: ['en', 'pt'],
    fallbackLng: ['en'],
    detection: {
      order: ['path'],
      lookupFromPathIndex: 0,
      checkWhitelist: true
    },
    interpolation: {
      escapeValue: false,
      formatSeparator: '.'
    }
  })

export default i18n

App.js

import { Route, Switch } from "react-router-dom";

import AboutPage from "./AboutPage";
import HomePage from "./Homepage/HomePage";
import NotFoundPage from "./NotFoundPage";
import PropTypes from "prop-types";
import React from "react";
import { hot } from "react-hot-loader";
import {
  Collapse,
  Navbar,
  NavbarToggler,
  NavbarBrand,
  Nav,
  NavItem,
  NavLink } from 'reactstrap';
import i18n from "../i18n";

const baseRouteUrl = "/:locale(pt|en)?";
export const baseUrl = i18n.language === 'en' ? '' : '/'+i18n.language;

class App extends React.Component {
  state = {
    isOpen: false
  }

  render() {
    return (
      <div>
        <div>
          <Navbar color="grey" expand="md">
            <NavbarBrand href="/">Testing</NavbarBrand>
            <Nav className="ml-auto" navbar>
              <NavItem>
                <NavLink href={baseUrl + "/"}>Home</NavLink>
              </NavItem>
              <NavItem>
                <NavLink href={baseUrl + "/about/"}>About</NavLink>
              </NavItem>
            </Nav>
          </Navbar>
        </div>
        <Switch>
          <Route exact path={baseRouteUrl + "/"} component={HomePage} />
          <Route path={baseRouteUrl + "/about"} component={AboutPage} />
          <Route component={NotFoundPage} />
        </Switch>
      </div>
    );
  }
}

App.propTypes = {
  children: PropTypes.element
};

export default hot(module)(App);

In my case, when I need to translate something, I import my i18n.js and call the respective key like this:

<div>{i18n.t('home.bannerStart')}</div>