
Phrase and beyond
Software localization
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?
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:
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.
Install all the necessary packages by running:
npm install aurelia-i18n i18next i18next-xhr-backend --save
Let me briefly cover what've installed:
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).
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:
TCustomAttribute
is not really required, but I've added it for demonstration purposes.PLATFORM.moduleName('aurelia-i18n')
, not just aurelia-i18n
as suggested in the docs.t
is the default one, whereas i18n
is a custom one.i18next-xhr-backend
in this case).{{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.
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.
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.
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.
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.
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:
BaseI18N
moduleEventAggregator
As you see, nothing complex here!
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.
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 user’s confusion. 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 to easily import and export translations 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.
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.
Last updated on October 27, 2022.