Friday, April 27, 2018

Translating Stimulus Apps With I18Next

Translating Stimulus Apps With I18Next

In my previous article I covered Stimulus—a modest JavaScript framework created by Basecamp. Today I'll talk about internationalizing a Stimulus application, since the framework does not provide any I18n tools out of the box. Internationalization is an important step, especially when your app is used by people from all around the world, so a basic understanding of how to do it may really come in handy.

Of course, it is up to you to decide which internationalization solution to implement, be it jQuery.I18n, Polyglot, or some other. In this tutorial I would like to show you a popular I18n framework called I18next that has lots of cool features and provides many additional third-party plugins to simplify the development process even further. Even with all these features, I18next is not a complex tool, and you don't need to study lots of documentation to get started.

In this article, you will learn how to enable I18n support in Stimulus applications with the help of the I18next library. Specifically, we'll talk about:

  • I18next configuration
  • translation files and loading them asynchronously
  • performing translations and translating the whole page in one go
  • working with plurals and gender information
  • switching between locales and persisting the chosen locale in the GET parameter
  • setting locale based on the user's preferences

The source code is available in the tutorial GitHub repo.

Bootstrapping a Stimulus App

In order to get started, let's clone the Stimulus Starter project and install all the dependencies using the Yarn package manager:

We're going to build a simple web application that loads information about the registered users. For each user, we'll display his/her login and the number of photos he or she has uploaded so far (it does not really matter what these photos are). 

Also, we are going to present a language switcher at the top of the page. When a language is chosen, the interface should be translated right away without page reloads. Moreover, the URL should be appended with a ?locale GET parameter specifying which locale is currently being utilized. Of course, if the page is loaded with this parameter already provided, the proper language should be set automatically.

Okay, let's proceed to rendering our users. Add the following line of code to the public/index.html file:

Here, we are using the users controller and providing a URL from which to load our users. In a real-world application, we would probably have a server-side script that fetches users from the database and responds with JSON. For this tutorial, however, let's simply place all the necessary data into the public/api/users/index.json file:

Now create a new src/controllers/users_controller.js file:

As soon as the controller is connected to the DOM, we are asynchronously loading our users with the help of the loadUsers() method:

This method sends a fetch request to the given URL, grabs the response, and finally renders the users:

renderUsers(), in turn, parses JSON, constructs a new string with all the content, and lastly displays this content on the page (this.element is going to return the actual DOM node that the controller is connected to, which is div in our case).

I18next

Now we are going to proceed to integrating I18next into our app. Add two libraries to our project: I18next itself and a plugin to enable asynchronous loading of translation files from the back end:

We are going to store all I18next-related stuff in a separate src/i18n/config.js file, so create it now:

Let's go from top to bottom to understand what's going on here:

  • use(I18nXHR) enables the i18next-xhr-backend plugin.
  • fallbackLng tells it to use English as a fallback language.
  • whitelist allows only English and Russian languages to be set. Of course, you may choose any other languages.
  • preload instructs translation files to be preloaded from the server, rather than loading them when the corresponding language is selected.
  • ns means "namespace" and accepts either a string or an array. In this example we have only one namespace, but for larger applications you may introduce other namespaces, like admincart, profile, etc. For each namespace, a separate translation file should be created.
  • defaultNS sets users to be the default namespace.
  • fallbackNS disables namespace fallback.
  • debug allows debugging information to be displayed in the browser's console. Specifically, it says which translation files are loaded, which language is selected, etc. You will probably want to disable this setting before deploying the application to production.
  • backend provides configuration for the I18nXHR plugin and specifies where to load translations from. Note that the path should contain the locale's title, whereas the file should be named after the namespace and have the .json extension
  • function(err, t) is the callback to run when I18next is ready (or when an error was raised).

Next, let's craft translation files. Translations for the Russian language should be placed into the public/i18n/ru/users.json file:

login here is the translation key, whereas Логин is the value to display.

English translations, in turn, should go to the public/i18n/en/users.json file:

To make sure that I18next works, you may add the following line of code to the callback inside the i18n/config.js file:

Here, we are using a method called t that means "translate". This method accepts a translation key and returns the corresponding value.

However, we may have many parts of the UI that need to be translated, and doing so by utilizing the t method would be quite tedious. Instead, I suggest that you use another plugin called loc-i18next that allows you to translate multiple elements at once.

Translating in One Go

Install the loc-i18next plugin:

Import it at the top of the src/i18n/config.js file:

Now provide the configuration for the plugin itself:

There are a couple of things to note here:

  • locI18next.init(i18n) creates a new instance of the plugin based on the previously defined instance of I18next.
  • selectorAttr specifies which attribute to use to detect elements that require localization. Basically, loc-i18next is going to search for such elements and use the value of the data-i18n attribute as the translation key.
  • optionsAttr specifies which attribute contains additional translation options.
  • useOptionsAttr instructs the plugin to use the additional options.

Our users are being loaded asynchronously, so we have to wait until this operation is done and only perform localization after that. For now, let's simply set a timer that should wait for two seconds before calling the localize() method—that's a temporary hack, of course.

Code the localize() method itself:

As you see, we only need to pass a selector to the loc-i18next plugin. All elements inside (that have the data-i18n attribute set) will be localized automatically.

Now tweak the renderUsers method. For now, let's only translate the "Login" word:

Nice! Reload the page, wait for two seconds, and make sure that the "Login" word appears for each user.

Plurals and Gender

We have localized part of the interface, which is really cool. Still, each user has two more fields: the number of uploaded photos and gender. Since we can't predict how many photos each user is going to have, the "photo" word should be pluralized properly based on the given count. In order to do this, we'll require a data-i18n-options attribute configured previously. To provide the count, data-i18n-options should be assigned with the following object: { "count": YOUR_COUNT }.

Gender information should be taken into consideration as well. The word "uploaded" in English can be applied to both male and female, but in Russian it becomes either "загрузил" or "загрузила", so we need data-i18n-options again, which has { "context": "GENDER" } as a value. Note, by the way, that you can employ this context to achieve other tasks, not only to provide gender information.

Now update the English translations:

Nothing complex here. Since for English we don't care about the gender information (which is the context), the translation key should be simply uploaded. To provide properly pluralized translations, we are using the photos and photos_plural keys. The part is interpolation and will be replaced with the actual number.

As for the Russian language, things are more complex:

First of all, note that we have both uploaded_male and uploaded_female keys for two possible contexts. Next, pluralization rules are also more complex in Russian than in English, so we have to provide not two, but three possible phrases. I18next supports many languages out of the box, and this small tool can help you to understand which pluralization keys should be specified for a given language.

Switching Locale

We are done with translating our application, but users should be able to switch between locales. Therefore, add a new "language switcher" component to the public/index.html file:

Craft the corresponding controller inside the src/controllers/languages_controller.js file:

Here we are using the initialize() callback to display a list of supported languages. Each li has a data-action attribute which specifies what method (switchLanguage, in this case) should be triggered when the element is clicked on.

Now add the switchLanguage() method:

It simply takes the target of the event and grabs the value of the data-lang attribute.

I would also like to add a getter and setter for the currentLang attribute:

The getter is very simple—we fetch the value of the currently used language and return it.

The setter is more complex. First of all, we use the changeLanguage method if the currently set language is not equal to the selected one. Also, we are storing the newly selected locale under the data-current-lang attribute (which is accessed in the getter), localizing the body of the HTML page using the loc-i18next plugin, and lastly highlighting the currently used locale.

Let's code the highlightCurrentLang():

Here we are iterating over an array of locale switchers and comparing the values of their data-lang attributes to the value of the currently used locale. If the values match, the switcher is assigned with a current CSS class, otherwise this class is removed.

To make the this.switcherTargets construct work, we need to define Stimulus targets in the following way:

Also, add data-target attributes with values of switcher for the lis:

Another important thing to consider is that translation files may take some time to load, and we must wait for this operation to complete before allowing the locale to be switched. Therefore, let's take advantage of the loaded callback:

Lastly, don't forget to remove setTimeout from the loadUsers() method:

Persisting Locale in the URL

After the locale is switched, I would like to add a ?lang GET parameter to the URL containing the code of the chosen language. Appending a GET param without reloading the page can be easily done with the help of the History API:

Detecting Locale

The last thing we are going to implement today is the ability to set the locale based on the user's preferences. A plugin called LanguageDetector can help us to solve this task. Add a new Yarn package:

Import LanguageDetector inside the i18n/config.js file:

Now tweak the configuration:

The order option lists all the techniques (sorted by their importance) that the plugin should try in order to "guess" the preferred locale:

  • querystring means checking a GET param containing the locale's code.
  • lookupQuerystring sets the name of the GET param to use, which is lang in our case.
  • navigator means getting locale data from the user's request.
  • htmlTag involves fetching the preferred locale from the lang attribute of the html tag.

Conclusion

In this article we have taken a look at I18next—a popular solution to translate JavaScript applications with ease. You have learned how to integrate I18next with the Stimulus framework, configure it, and load translation files in an asynchronous manner. Also, you've seen how to switch between locales and set the default language based on the user's preferences.

I18next has some additional configuration options and many plugins, so be sure to browse its official documentation to learn more. Also note that Stimulus does not force you to use a specific localization solution, so you may also try using something like jQuery.I18n or Polyglot

That's all for today! Thank you for reading along, and until the next time.


No comments:

Post a Comment