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 :
Extra config file ( constants.js )
Callback function to change the language from "company" to default locale. ( this triggers 2 page reloads)
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?
I needed the same thing and I found out that you can set the whitelist
property 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>