Adding multilanguage support to your React project

How to take your project to another level

Matheus Monteiro
Level Up Coding

--

Photo by Kyle Glenn on Unsplash

Interaction with users and services from different countries has become increasingly common, and the tendency is to transcend the concepts of borders within the I.T. community. Thus, there is a growing urge to develop platforms with support for different countries, accordingly adapting to the local language.

An interesting exercise that can help us to understand the importance of this is trying to imagine the following scenarios:

. Facebook, Twitter, or other social media that you use regularly with support only for Korean, French, or any language that you do not have a domain;

. E-commerce sites using only the local currency to represent monetary costs (How much would your cell phone cost in Russian Rubles?);

. Online diaries using date and time formats from other countries (
Do you know how represent your birthday in Taiwanese?);

Upon reflection on the topics listed above, do you think you would have the same experience in those scenarios, if compared to what you currently have as a user?

If the answer is no, then we are both aligned on the importance of internationalization, and why we should be concerned with that when building our applications.

I present to you React Intl

This library, made for React, provides us with components and API’s for the formatting of different types of data, with support for more than 150 languages.

If you want to know more about this library, you can directly access its repository on GitHub by clicking here!

In this post, I will focus only on translating messages from our app to different languages. If you are interested in the subject and would like more content commenting on how to format dates, monetary values, among other data types, leave your suggestion in the comments!

Defining the messages

The react-intl Library uses the Message Descriptor concept, which contains the necessary data to define how messages will be formatted.

Thus, each message has the following properties:

. id (required): Defines a unique identification for each message.

. description (optional): A description of the message’s content.

. defaultMessage (optional): A standard message, used as a fallback when there is an error in the handling of the main message, such as in case no translation was found. It can be used directly as your main language text.

Using the same library, we can use another function called defineMessages to return a list of Message Descriptors, and thus use them in our components.

Using the defineMessages function to define our messages.

Note that in the id of each message, I used a scope, followed by the message key.

In large projects, a large number of messages are defined throughout the application. Thinking about scalability and to ensure that each identification is unique to each message, we can use some conventions to write the message id.

In my projects, I usually use the path of each file where the messages are found as a scope, followed by the message key, so that we have the id of each message defined as:

{pathToFile}.{messageKey}

The choice of which convention to use may vary, but the recommendation is that once the standard identification is defined, it should be maintained for all project messages.

Formatting Messages

After defining the messages, we will use FormattedMessages to format the messages in our component. To do this, just call this function in our component, passing it the MessageDescriptor object with the previously defined message and, if necessary, a property called values, which contains an object with values that will replace possible variables contained in the message.

Using FormattedMessage to format messages in our components

By definition, all components that use React Intl in some way, must use a component called IntlProvider, which is responsible for setting the context of the i18n (used as an abbreviation for Internationalization, with 18 letters between “I” and “n ”).

Using IntlProvider

And… done! We are already using messages formatted by React Intl.

Implementing other languages ​​in our App

First, let’s think about how we are going to arrange our translation files in our project…

It is a good programming practice to store the translation files in a location close to where they will be used.

We will follow this concept, however, creating also a location apart from the components, which will import the translation of all locations, centralizing all of them in a single file.

Folder architecture

Accordingly to the model I created for i18n, we should implement the following steps:

Step 1: Create a messages.js file in the same folder as the component’s index, which will use the defineMessages function, but only by defining the scope and id of the messages.

Example message file

Note: We won’t even write a defaultMessage here, I’ll explain the reason later. If you want to add a description next to the id, feel free.

Step 2: For each component translation file, import the scope of the message file and define for each id, the respective message translated into the language named by the file.

en.js
pt.js

Do this for all messages in your components.

Step 3: Centralize all translations into a single file

In the translation files that we define outside the components and containers, we import all the translations that we define in our code.

Here, if necessary, we can use intermediate files to avoid a massive amount of imports in a single file.

For each general translation file, we would have something like:

Centralizing all translations into a single file

Finally, we export an object containing as key all languages ​​that will be supported by our application.

We now have access to an object containing all translations of our project.

Step 4: Inject the messages passing the current location

Using messages in the Intl Provider

That way, when we change the location, all of our texts in the app will be translated to the chosen location. Cool, isn’t it?

Defining standard messages

Suppose you are newly hired by a Brazilian company that is growing in the renewable energy market in the region where it operates. As much as we intend to write the app in several languages, in this case, we will always have Portuguese as the default language, since the app will probably be used initially in Brazil, and later, when the company expands internationally, to be used by users of other nationalities.

MessageDescriptor predicts this, and provides us with the defaultMessage, where we pass a string with a default language to our application, and if react-intl fails to try to translate any message, it will return the value of defaultMessage, according to its strategy fallback.

In that case, it would be enough to write a defaultMessage in our messages.js files, along with the id, to prevent any translation errors from happening, right?

Yes, it is a way to solve this problem. But let’s dig deeper and imagine the following scenario:

Your company has grown a lot, is a leader in its sector, and caught the attention of a multinational based in Canada that decided to buy it to take over its business. But the multinational’s headquarters is not in Brazil, and it wants to make its old company migrate to Canada, and have a new headquarters there, now with clients that mostly do not speak Portuguese.

It makes sense that the company wants to change the default language of its newly acquired service to the local language, right?

But imagine the work it will have to translate each existing message, whose defaultMessage is in Portuguese, spread across the various messages.js files throughout the code. What if for some reason she needs to change the default language again?

This is not scalable at all.

The react-intl library, unfortunately, does not accept any properties related to default messages in its IntlProvider to deal with this problem, as it expects to receive this via MessageDescriptor.

However, there is a super simple solution for this, using spread operators for Javascript objects.

It’s very simple. Instead of sending only the translations of the selected language, as message prop, we will create an auxiliary object mergedMessages, which receives all ID’s with the translation of the language set as the default, and soon afterward we replace them with the current language, using the ID as a comparison key.

That way, if react-intl cannot find the translation of any message in the selected language, it will always use the default language translation.

Using merged Messages
Using merged Messages

Cool, the solution was good and everything, but I still have a question…

Will I need to manually change the location to have my messages translated?

Answer: With what we have so far, yes. Fortunately, there is a way to simplify things and automate our work.

Using Redux to manage location

We need something that can change the value of the location, at runtime, and dynamically. In this context, the Redux library fits perfectly, and this is what we will use to complement our solutions.

The modification to be made here is to pass the IntlProvider to a container, which receives the location via the store’s status. Thus, if there is a need to change the location (user has indicated that he wants to change the language of the texts on screen, for example), just trigger an action, replacing the state of the store, so the new location will be passed to IntlProvider.

Using Redux to manage location

Finally, just use the exported component instead of IntlProvider, in our root index.

Using LanguageProvider

Next steps

This solution considers that all translations are done manually.

We could use, for example, the babel-plugin-react-intl library, which extracts all messages from the modules with react-intl and generates a JSON with all of them.

From there, we could use a separate translation service to generate the translations automatically, which would save a lot of development time, even if that way the application would be dependent on an external service and automatically generated translations. If it makes sense for your purpose, I think it would be a more than justifiable change.

Due to the current length of this post, I leave this as an exercise for readers who are interested in the subject.

I reinforce that the solution presented above represents only one of the several possible existing solutions.

Feel free to leave any doubts, critics or even suggestions for improvement in the comments.

--

--

An electrical and software engineer, always learning how to play with code.