Software localization

How to Localize Apps Using the Aurelia Framework

Simple, concise and easy to use: This tutorial will take you through the process of internationalizing JavaScript applications using the Aurelia framework.
Software localization blog category featured image | Phrase

This tutorial will show you how to localize applications using the Aurelia framework, i.e. how to install and configure the aurelia-i18n plugin, where to store translations and how to load them asynchronously. We'll also discuss how to format your data, add support for HTML translations, switch between locales and observe events. Shall we start?

Creating the App

If you would like to follow along, aurelia-cli will be required, so don't forget to install it:

npm install aurelia-cli -g

Next, create a new application with any name:

au new

I have used a custom setup with the following settings:

  1. RequireJS loader (note that Webpack plays very bad with the I18n plugin at the moment and you might have tough times getting it to work)
  2. Web platform
  3. Babel transpiler
  4. No markup processing
  5. Standard CSS with no pre-processing
  6. No test runners (we are not going to tackle testing in this tutorial, but it is a good idea to do so in the real world)
  7. Atom code editor

Of course, the general approach to localizing Aurelia apps should work with other setups as well, but the code may slightly differ from case to case.

After your dependencies are installed (which may take quite a lot of time), create a new User class. We are going to pretend that our app (for now) works only with an array of some users:

// src/user.js

export class User {

  constructor(name) {

    this.name = name;

  }

}

Next, tweak the src/app.js file to require the newly created class and prepare an array of users:

// src/app.js

import {User} from './user';

export class App {

  constructor() {

    this.users = [

      new User("John Doe"), new User("Ann Smith")

    ];

  }

}

Now we may render them inside the src/app.html:

<template>

  <h1>Users</h1>

  <ul>

    <li repeat.for="user of users">

      <span>

        ${user.name}

      </span>

    </li>

  </ul>

</template>

So far so good: we have some minimalistic app and it is time to proceed to the next section and integrate aurelia-i18n package.

Adding AureliaI18n

Install all the necessary packages by running:

npm install aurelia-i18n i18next i18next-xhr-backend --save

Let me briefly cover what've installed:

  • aurelia-i18n is the main star today. It is a plugin that adds internationalization support and does all the heavy lifting for us.
  • i18next is a core plugin that aurelia-i18n relies on. To put it shortly, I18next is an open source internationalization framework used by numerous JavaScript apps out there. If you would like to learn more about vanilla I18next, you may skim through this article.
  • i18next-xhr-backend is an optional but very useful plugin that allows loading translation files in an asynchronous manner from the server.

If you have the same setup as I've described in the previous section, tweak the aurelia_project/aurelia.json file by including all the installed plugins into the dependencies section, so that it looks like this:

"dependencies": [

{

            "name": "i18next",

            "path": "../node_modules/i18next/dist/umd",

            "main": "i18next"

          },

          {

            "name": "aurelia-i18n",

            "path": "../node_modules/aurelia-i18n/dist/amd",

            "main": "aurelia-i18n"

          },

          {

            "name": "i18next-xhr-backend",

            "path": "../node_modules/i18next-xhr-backend/dist/umd",

            "main": "i18nextXHRBackend"

          }

]

Note that aurelia-i18n supports other setups as well, including JSPM and Webpack (though, as mentioned above, it is pretty buggy).

Configuration

Tweak the src/main.js file to incorporate the newly installed plugin:

// src/main.js

import environment from './environment';

import {I18N, TCustomAttribute} from 'aurelia-i18n'; // <------------ 1

import Backend from 'i18next-xhr-backend'; // <------------ 2

export function configure(aurelia) {

  aurelia.use

  .standardConfiguration()

  .feature('resources');

  aurelia.use.plugin('aurelia-i18n', (instance) => { // <------------ 3

    let aliases = ['t', 'i18n']; // <------------ 4

    TCustomAttribute.configureAliases(aliases);

    instance.i18next.use(Backend); // <------------ 5

    return instance.setup({

      fallbackLng: 'ru', // <------------ 6

      whitelist: ['en', 'ru'],

      preload: ['en', 'ru'], // <------------ 7

      ns: 'global', // <------------ 8

      defaultNS: 'global',

      fallbackNS: false,

      attributes: aliases, // <------------ 9

      lng: 'en', // <------------ 10

      debug: true, // <------------ 11

      backend: {

        loadPath: './locales/{{lng}}/{{ns}}.json',  // <------------ 12

      }

    });

  });

  // rest of the code.......

}

I've pinpointed the lines that should be added to the file. Let's move step-by-step here:

  1. Import the necessary modules. TCustomAttribute is not really required, but I've added it for demonstration purposes.
  2. Import the module to load translations asynchronously.
  3. Hook up the plugin. Be warned that with some setups (for instance, when using Webpack) the plugin's name should be written as PLATFORM.moduleName('aurelia-i18n'), not just aurelia-i18n as suggested in the docs.
  4. This is an array of aliases that we would like to utilize in order to perform translations. t is the default one, whereas i18n is a custom one.
  5. We specify which backend to use (i18next-xhr-backend in this case).
  6. Our application is going to have support for two languages: English and Russian. On this line, we specify that if translations for the English locale cannot be found, use Russian as a fallback language. Basically, all the settings here are described in the I18next docs.
  7. Here we provide an array of locales to preload translations for. It is very convenient because the corresponding files are loaded in an asynchronous manner and later we don't need to spend any time loading them once a user changes the site's language.
  8. That's a namespace. In general, I18next may support multiple namespaces, but for this demo, a single one will do. We'll see how this namespace is utilized in a moment.
  9. Here we provide the aliases defined at step #4.
  10. The default language.
  11. Provide debugging information in the browser's console. Disable this for production apps.
  12. The path where to load translation files from. {{lng}}, of course, means "language" (ru or en), whereas {{ns}} is a namespace. The files must be in JSON format.

Next, what you need to do is create a locales folder in the root of the project (because we've specified the ./locales/{{lng}}/{{ns}}.json path in our settings above). Inside that folder create two nested directories named after the locales of your choice (en and ru in this demo). Inside these folders, in turn, create thethe global.json file that is going to host all the translations for the given language. "global" is the name of our namespace defined above. For larger apps, it is quite common to have multiple namespaces and, in turn, multiple files with translations for different sections of the site.

Here  is the content of the en/global.json file:

{

  "user": {

    "name": "Name"

  },

  "users": "Users"

}

And

ru/translation.json

file:
{

  "user": {

    "name": "Имя"

  },

  "users": "Пользователи"

}

Note here that translation keys can be nested.

Great! The next step is to actually utilize these translations, so proceed to the next section.

Performing Translations in a Simple Way

The simplest way to perform translations is by using an HTML attribute called t (or i18n which is an alias according to our settings). Tweak the src/app.html file like this:

<template>

  <h1 i18n="users"></h1>

  <ul>

    <li repeat.for="user of users">

      <b t="user.name"></b>:

      <span>

        ${user.name}

      </span>

    </li>

  </ul>

</template>

Now run the application using the following command:

au run --watch

Note that the --watch flag must be provided in Windows environment to avoid a pretty nasty bug, preventing the application from opening at all. Next, navigate to http://localhost:9000 and open the browser console. Among other debug messages you should see:

i18next::backendConnector: loaded namespace global for language en

i18next::backendConnector: loaded namespace global for language ru

i18next: languageChanged en

i18next: initialized

It means that I18next has successfully initialized and loaded all translation files for us. You will also see that both the "Users" and "Name" text is shown on the page which shows that the translations were performed properly.

Controlling Translations Behaviour

By default, translations do not support HTML markup, so if you say something like:

{"users": "<em>Users</em>"}

Then the em tag won't be processed and instead will be printed out in a raw format. To overcome this problem, prepend the translation key with [html] prefix:

<h1 i18n="[html]users"></h1>

Moreover, you may even control how the translation should be added to the given tag. By default, it replaces any text inside, but there are two other prefixes available:

  • [prepend]
  • [append]

Note that these two prefixes implicitly allow HTML content as well.

Formatting the Output

It is possible to easily format numbers and dates with aurelia-i18n. For starters, let's provide additional information about our users: salary and their birthdate:

export class User {

  constructor(name, salary, birthdate) {

    this.name = name;

    this.salary = salary;

    this.birthdate = birthdate;

  }

}

Set it in the following way:

    this.users = [

      new User("John Doe", 20000, new Date(1989, 2, 3, 0,0,1)),

      new User("Ann Smith", 25000, new Date(1985, 3, 1, 0,0,1))

    ];

And now I would like to format these new values properly. Start with the salary that has to be displayed in a currency format:

      <span>

        ${ user.salary | nf : { style: 'currency', currency: 'EUR' }}

      </span>

nf is a special value converted and here we are saying to format the given number as a currency, prefixed with a euro symbol.

Formatting dates can be done in a similar manner:

      <span>

        ${ user.birthdate | df : { year: 'numeric', month: 'numeric', day: 'numeric' }}

      </span>

You may find more examples of using the plugin in the official docs.

Now let's proceed to the next feature and add the ability to switch between locales.

Switching Locales

In order to introduce this new feature, I propose creating a separate Locales class. We will need to inject I18N into it:

// src/locales.js

import {I18N} from 'aurelia-i18n';

export class Locales {

  static inject = [I18N];

  constructor(i18n) {

    this.i18n = i18n;

  }

}

Also, let's provide an array of supported locales and store the currently set language:

export class Locales {

  static inject = [I18N];

  constructor(i18n) {

    this.i18n = i18n;

    this.locales = [

      {

        title: "English",

        code: "en"

      },

      {

        title: "Русский",

        code: "ru"

      }

    ]

    this.currentLocale = this.i18n.getLocale();

  }

}

Now display locales on the page and bind a click event handler to them:

<template>

  <ul>

    <li repeat.for="locale of locales"

    css="text-decoration: ${locale.code === currentLocale ? 'underline' : 'none'}"

    click.trigger="setLocale(locale)">

      ${locale.title}

    </li>

  </ul>

</template>

Note that we are also providing a basic styling for a currently selected language using the css attribute. The next step is to process the click event inside the setLocale method:

export class Locales {

  static inject = [I18N];

  constructor(i18n) {

    // ....

  }

  setLocale(locale) {

    let code = locale.code

    if(this.currentLocale !== code) {

      this.i18n.setLocale(code);

      this.currentLocale = code;

    }

  }

}

The idea is simple: we grab the locale's code and check that it is not the same as the currently set one. If not — use the setLocale method of the i18n object to update locale. Note, by the way, that the i18n object can be used to perform translations programmatically. For example:

this.i18n.tr('users'); // => Users

The last step is to require these newly created files. First, the template inside the src/app.html file:

<template>

  <require from="./locales"></require> <!-- 1 -->

  <locales></locales> <!-- 2 -->

  <h1 i18n="users"></h1>

  <ul>

    <li repeat.for="user of users">

      <b t="user.name"></b>:

      <span>

        ${user.name}

      </span>

    </li>

  </ul>

</template>

Then the module inside the src/app.js:

import {Locales} from  './locales';

//... all other code here

That's it! Now try switching between locales — the text on the page should be translated accordingly.

Observing Locale Changes

Sometimes it is desirable to listen for a "locale changed" event and perform specific actions. Let me demonstrate how to do that:

// src/app.js

import {BaseI18N, I18N} from 'aurelia-i18n'; // <--------- 1

import {EventAggregator} from 'aurelia-event-aggregator'; // <--------- 2

import {User} from './user';

import {Locales} from  './locales';

export class App {

  static inject = [I18N,Element,EventAggregator]; // <--------- 3

  constructor(i18n,element,ea) {

    this.users = [

      new User("John Doe", "male"),

      new User("Ann Smith", "female")

    ];

    this.i18n = i18n;

    this.element = element;

    this.currentLocale = this.i18n.getLocale();

    ea.subscribe('i18n:locale:changed', payload => { // <--------- 4

      this.i18n.updateTranslations(this.element);

    });

  }

  attached(){ // <--------- 5

    this.i18n.updateTranslations(this.element);

  }

}

Here are the key points:

  1. We load the BaseI18N module
  2. Also load the EventAggregator
  3. Perform injection of all the necessary modules
  4. Subscribe to a "locale change" event and update all translations inside the current element and all nested elements.
  5. Also update translations as soon as the current class is attached to an element

As you see, nothing complex here!

Bundling Translation Files

What's interesting, your translation files can be easily packed into the Aurelia bundle. All you need to do is add the .json extension to the extensions section inside the aurelia_project/aurelia.json file:

    "loader": {

      "type": "require",

      "configTarget": "vendor-bundle.js",

      "includeBundleMetadataInConfig": "auto",

      "plugins": [

        {

          "name": "text",

          "extensions": [

            ".html",

            ".css",

            ".json"

          ],

          "stub": true

        }

      ]

    }

Learn more here.

Phrase and Translation Files

Working with translation files is hard, especially when the app is big and supports many languages. You might easily miss some translations for a specific language which will lead to confusion among users. And so Phrase can make your life easier!

Grab your 14-day trial. Phrase supports many different languages and frameworks, including JavaScript of course. It allows you to easily import and export translation data. What’s cool, you can quickly understand which translation keys are missing because it’s easy to lose track when working with many languages in big applications.

On top of that, you can collaborate with translators as it’s much better to have professionally done localization for your website.

Conclusion

In this article, we have seen how to introduce internationalization into Aurelia applications. We have seen what plugins are required to do that, how to install and setup them. Also, you've learned how and where to store the actual translations, how to utilize them and use various formatters. On top of that, we have added the ability to switch between locales and observe the "locale changed" event. Note that aurelia-i18n plugin has other features. Some of them can be found in the docs, whereas others are documented at the i18next.com website because, after all, Aurelia I18n is powered by this plugin.

Hopefully, you found this article useful! As always, I thank you for staying with me and until the next time.