Internationalizing a React Application using Polyglot
Internationalizing an application is always simple at first glance. It only means applying a translate
function on strings to be translated, right? This function is generally a mapping function, linking an input key (the string to be translated) to the returned translated string.
Sounds simple? What about handling plural forms? It doesn't consist of just adding a s
at the end of the word (see mouse
and mice
for instance). And, what about some Slavic languages where there are several plural forms? For instance, in Russian, there is the singular form for a single element, the dual form for
between 2 and 4 elements, and the plural form for 5 or more elements.
It already becomes quite complex, yet we just talked about strings, not even date or currency formats. Fortunately, we can rely on some open-source i18n libraries. The most famous in React ecosystem are probably node-polyglot and react-i18next.
I won't cover the differences between these two different libraries, as I don't know enough react-i18next
. Indeed, Polyglot was already present on the project we maintain and does the job perfectly. Why switch to another lib if everything works well and is simple to develop? Just be pragmatic!
Discovering Polyglot
First, we need to grab the Polyglot dependency:
npm install node-polyglot
Be careful: the Polyglot we retrieve here is the Airbnb one, called node-polyglot
. There is also a polyglot
package, but it is not the one covered by this post.
A First Translation using Polyglot
Reading the official documentation, we can achieve our first translation very easily, using a code similar to:
import Polyglot from 'node-polyglot';
const locale = 'fr';
const phrases = {
'actions.fullscreen': 'Voir en plein écran',
};
const polyglot = new Polyglot({ locale, phrases });
console.log(polyglot.t('actions.fullscreen');
// Voir en plein écran
We instantiate a Polyglot
instance passing it two properties:
- locale: current locale, used only for pluralizations,
- phrases: a list of translations.
Then, translating a string is as simple as calling the t
function and passing it the string to be translated.
String Interpolation
Let's imagine we need to display a customized welcome message to our users. We would need a username
variable in our string. That would be especially useful to handle punctuation properly. Indeed, assuming we want to display Welcome Jonathan! once logged in, we may use a code like:
const phrases = {
"home.welcome": "Welcome",
};
const polyglot = new Polyglot({ locale, phrases });
const login = "Jonathan";
// Welcome Jonathan!
console.log(polyglot.t("home.welcome") + login + "!");
It works for English. But not in French, where there is always a space before exclamation points. So, we need to embed the login into our translation. Polyglot supports string interpolation, easing our job:
const phrases = {
"home.welcome": "Welcome %{login}!",
};
const polyglot = new Polyglot({ locale, phrases });
const login = "Jonathan";
// Welcome Jonathan!
console.log(polyglot.t("home.welcome", { login }));
Polyglot replaces all instances of %{variableName}
by the value of variableName
, allowing to embed
our punctuation directly into our translated strings. In French, it would have been Bienvenue %{login} !
.
Handling Plural Forms
As explained above, handling plural forms is not as trivial as it may sound. Fortunately, Polyglot handles it natively:
const phrases = {
numberChildren: "%{smart_count} child |||| %{smart_count} children",
};
const polyglot = new Polyglot({ locale: "fr", phrases });
// 1 child
console.log(polyglot.t("numberChildren", { smart_count: 1 }));
// 3 children
console.log(polyglot.t("numberChildren", { smart_count: 3 }));
Note the ||||
symbol which is the plural form separator. As Polyglot is configured in French (via
the locale
attribute), it splits the translation string on this symbol. Polyglot considers the first part as the singular form, and the last one as the plural form. If we need to support Russian for instance, we can have several ||||
in the same translated string.
The number of items is retrieved from the special smart_count
variable.
So, that was the getting started of Polyglot. Yet, how do we use it in a real-world application, where you need it in several files?
Using Polyglot in a Real World Application
There is generally a huge gap between the straightforward getting started tutorial and the integration into a real-world application.
Polyglot is not an exception to the rule. Just following the tutorial, how can we use it in all our React components without instantiating it several times? The quick and dirty solution would be to declare it globally via a global.translate
property. Yet, not fully satisfying, as we are inevitably going to face some troubles using global variables.
Explaining how to implement Polyglot on a real-world application requires a real-world application. I bootstrapped a really basic video list application. You can grab the whole source code on GitHub, each commit bringing improvement compared to the previous one.
Our sample application contains three components: a <VideoList />
displaying a list of <Video />
, each one having a <Metas />
component (for duration and number of views). The code is basic React and I'll assume you have a basic knowledge of this awesome lib. That's why I won't cover the setup part and focus on the internationalization part.
The Naive Solution
The first naive solution would be to declare a Polyglot instance once, in our top level script, and then to pass it manually via props to all children. For instance, we may write the following code:
import React from "react";
import ReactDOM from "react-dom";
import Polyglot from "node-polyglot";
import videos from "./data";
import messages from "./messages";
import VideoList from "./VideoList";
const locale = window.localStorage.getItem("locale") || "fr";
const polyglot = new Polyglot({
locale,
phrases: messages[locale],
});
const translate = polyglot.t.bind(polyglot);
ReactDOM.render(
<VideoList videos={videos} translate={translate} />,
document.getElementById("root")
);
export const VideoList = ({ translate, videos }) => (
<div className="videos-list">
{videos.map(video => (
<Video key={video.title} video={video} translate={translate} />
))}
</div>
);
export const Video = ({ video, translate }) => (
<div className="video">
<img src={video.picture} alt={video.title} />
<div className="infos">
<h2 className="title">{video.title}</h2>
<Metas metas={video.metas} translate={translate} />
</div>
</div>
);
export const Metas = ({ metas, translate }) => (
<div className="video-metas">
<div className="duration">
{translate("minutes", { smart_count: metas.duration })}
</div>
<div className="views">
{translate("views", { smart_count: metas.views })}
</div>
</div>
);
This naive implementation works perfectly, but is incredibly cumbersome and thus error-prone, due to the numerous translate
prop transfers. This basic sample handles only three levels of components. What about a more complex application with sometimes a dozen depth levels? We need a better solution. Especially as only the <Metas>
component needs the translate
prop.
What About Using Context?
To Use or Not To Use Context?
Fortunately, React provides a solution. Reading the official documentation:
In some cases, you want to pass data through the component tree without having to pass the props down manually at every level. You can do this directly in React with the powerful "context" API.
That's exactly what we need. Yet, a few lines later, we can read, still on the same page:
If you want your application to be stable, don't use context. It is an experimental API and it is likely to break in future releases of React.
Not really encouraging. So, it perfectly fits our need but is not recommended. What should we do? When facing such questions, the best solution is to refer to the developer collective intelligence. Dan Abramov, a high-skilled developer you need to follow if you are interested in the React ecosystem, shared a code snippet on Twitter:
function shouldIUseReactContextFeature() {
if (amIALibraryAuthor() && doINeedToPassSomethingDownDeeply()) {
// A custom <Option> component might want to talk to its <Select>.
// This is OK but note that context is experimental API and doesn't update
// correctly in some cases so you might want to roll your own subscriptions.
return amIFineWith(API_CHANGES && BUGGY_UPDATES);
}
if (myUseCase === "theming" || myUseCase === "localization") {
// In apps, context can be used for "global" variables that rarely change.
// If you insist on using it, provide a higher order component.
// This way when we change the API, you will only need to update one place.
return iPromiseToWriteHOCInsteadOfUsingItDirectly();
}
if (libraryAsksMeToUseContext()) {
// Ask them to provide a higher order component!
throw new Error("File an issue with this library.");
}
// Good luck.
return yolo();
}
So, using context for localization is fine, but only with a Higher Order Component (HOC). Let's focus on how to use context for now.
Using context consists in creating a Provider
component, which should fulfill three different requirements:
- Declare data structure of context passed data,
- Fill context data,
- Render
children
components.
Writing our First Provider
In our case, we are going to pass the translate
function to the context. But also the locale
as a string. Indeed, we may need it if we want to localize dates using moment for instance. So, let's declare our context new data types:
import { Children, Component, PropTypes } from "react";
class I18nProvider extends Component {}
I18nProvider.childContextTypes = {
locale: PropTypes.string.isRequired,
translate: PropTypes.func.isRequired,
};
export default I18nProvider;
Now, we need to fill the context new attributes, via the getChildContext
method. That's where we need to instantiate Polyglot:
// [...]
class I18nProvider extends Component {
getChildContext() {
const { locale } = this.props;
const polyglot = new Polyglot({
locale,
phrases: messages[locale],
});
const translate = polyglot.t.bind(polyglot);
return { locale, translate };
}
}
// [...]
We use the locale
prop passed to the Provider
instead of retrieving it directly via the local storage. Indeed, this logic should not be embedded in such a "dumb" provider.
We can now use our Provider
in our application. So, let's change our index.js
render script:
const locale = window.localStorage.getItem("locale") || "fr";
ReactDOM.render(
<I18nProvider locale={locale}>
<VideoList videos={videos} />
</I18nProvider>,
document.getElementById("root")
);
We wrapped the <VideoList>
component in <I18nProvider>
, adding it a locale
property. That's the only change required. All the instantiation logic is moved to the provider, keeping our code really readable.
The <I18nProvider>
component has no render
method yet. This method should just act as a proxy and render its child component.
import { Children } from "react";
// [...]
class I18nProvider extends Component {
render() {
return Children.only(this.props.children);
}
}
// [...]
But We Promised Dan to Write an HOC...
If we remember correctly Dan's previous tweet, we promised him to write a Higher Order Component (aka HOC). But what is it?
A higher-order component (HOC) is a function that takes a component and returns a new component.
In our case, an HOC is useful as it allows to map our base component with our new context attributes. It would return a new component passing translate
and locale
as props. Such an HOC code would be:
import React, { PropTypes } from "react";
export const translate = BaseComponent => {
const TranslatedComponent = (props, context) => (
<BaseComponent
translate={context.translate}
locale={context.locale}
{...props}
/>
);
TranslatedComponent.contextTypes = {
translate: PropTypes.func.isRequired,
locale: PropTypes.string.isRequired,
};
return TranslatedComponent;
};
export default translate;
We map our component to the context thanks to the contextTypes
property. It takes the same data structure as our I18nProvider.childContextTypes
. Then, we can retrieve it thanks to the second argument of our functional component.
Now that we have an HOC, we just need to remove all former translate
props from components, and just call the translate
function on the <Metas>
component:
import translate from './translate';
export const Metas = ({ metas, translate }) => (
// [...]
);
export default translate(Metas);
Our internationalization is now quite straightforward, just requiring a function call to our translate
component to access Polyglot. And we also learned how to create a Provider. Level up!
The final code is available on GitHub as the penultimate commit. HEAD
of the repository uses recompose to simplify code slightly.