Web dev and more

Aurelia advanced I18N

September 04, 2016 | 8 Minute Read

Recently I had to implement i18n in order to support several languages and users all around the world. I used the official aurelia-18n library and had some difficulties make everything work in Safari (Cordova iOS App) and with Typescript. This post will may cover and extend some steps that are already described on the README.md page of the Github repo.

I used the beta version of the library but shortly afterwards Aurelia 1.0 were released and I updated to aurelia-i18n 1.0 version without any issues.

Using aurelia-i18n with Typescript

Like on the Github page suggested you have to execute

jspm install i18next
jspm install i18next-xhr-backend 

or some other backend service from this list of available backends and install typings

typings install i18next

Since i18next-xhr-backend comes with a d.ts file you can just add

 "filesGlob": [
    "../jspm_packages/**/*.d.ts",
  ]

to your tsconfig.js and the gulp typescript task will find and use the i18next-xhr-backend.d.ts file. If you are up to date with your aurelia packages you shouldn´t get any "Duplicate identifier" error messages. If you have some issues with this you could change the filesGlob path to only match the specific file or follow the steps described under point 8.ii.

Translation with ValueConverter

For converting numbers with the number format value converter or converting dates with the date format value converter you have to pass the wanted locale.

//number format with 'en_US' as locale
 ${ 1234567.890 | nf : undefined : 'en_US'} //undefined is the value for the options parameter 

//date format with a locale string bound to selectedLocale
${ myDate | df : undefined : selectedLocale} 

I didn´t wanted to pass the current selected locale to every view or make it global so I created a wrapper around the nf and df value converters. These wrapper value converters always requesting the current locale first so you don´t have to provide this value in every view or make it global.

import {autoinject} from 'aurelia-framework';
import {I18N} from 'aurelia-i18n';

@autoinject()
export class CnfValueConverter {

  constructor(private i18n: I18N) { }

  toView(value: string) {
    let numberFormatFunc = this.i18n.nf(undefined, this.i18n.getLocale());
    return numberFormatFunc.format(value);
  }
}

This approach was fitting for me because I don´t need the options. You could also extend the custom value converter parameters with a options object. The custom date format value converter would look similar. Then you can use them like this

 ${ 1234567.890 | cnf } 
 ${ myDate | cdf }

Locale selectbox

I had to implement a selectbox to let the user easily change the locale. I wanted to style the selectbox properly and enable the user to search for a specific locale. Thus I choose the Select2 plugin and orientated on this blog post from Dwayne.

Similar to the blog post I created a custom element language-switch but instead of using events I am using a bindable property because I wanted to get the whole selected object and not only the key.

//language-switch.html
<template>
    <require from="select2/css/select2.min.css"></require>

    <select name.bind="name" value.bind="selected" class="custom-selectbox">
        <option repeat.for="option of options" model.bind="option">${option.label & t}</option>
    </select>
</template>
//language-switch.ts
import {bindable, inject, customElement} from 'aurelia-framework';
import * as $ from 'jquery';
import 'select2';

@inject(Element)
export class LanguageSwitch {
  @bindable name: string = null;
  @bindable selected: any = null;
  @bindable options: Array<any> = [];

  constructor(private element) {
    this.element = element;
  }

  attached() {
    $(this.element).find('select')
      .select2()
      .on('change', (event: Event) => {
        this.selected = event.target.selectedOptions[0].model;
    });
  }

  detached() {
    $(this.element).select2('destroy');
  }
}
//language option model
let options = [ {'key': 'de', 'label': 'Germany'}, {'key': 'en', 'label': 'English'}];

Safari und Intl

aurelia-i18n uses i18next a widely known internationalization library which depends on the window.Intl API. Unfortunately the browser support for this API is limited. Thus for supporting Safari and some mobile browsers you have to install the polyfill intl.js

 jspm install npm:intl

and include it somewhere in your project and into your bundles

import 'intl'; //in main.ts or like me in some own translation service

Aurelia-i18n will only take this polyfill if window.Intl is not available, therefore you don´t need to install extra typings for it.

But that fixed not all of my problems and it caused me some headache to figure out how to solve them. I discovered that you have to include a JSON file for every locale you are supporting in order to use numberformat and dateformat. So I created a file where I included all needed locales and imported this file in my project. Next to that you could use this file for determining supported languages for the language-switch.

import 'intl/locale-data/jsonp/de';
import 'intl/locale-data/jsonp/de-DE';
import 'intl/locale-data/jsonp/en';
import 'intl/locale-data/jsonp/en-US';

You can include the whole folder or use the complete.js for importing all locales but because all these files together have a size over 56MB I choose to add them manually. For more optimizations you could load intl.js and all the needed locale files only if window.intl is not available.

Unit Testing

When using the TValueConverter or TBindingBehavior in the view of the custom element I was testing, the compiler was complaining about "No BindingBehavior named 't' was found!" What helped was to include t manually to the test. Given this custom element

import {bindable} from 'aurelia-framework';
export class MyComponent {
  @bindable firstName;
}

and given this unit test you can include the binding behaviour with

 .withResources(['...', 'aurelia-i18n/t']) //the binding behavior is defined in a own file under aurelia-i18n named 't.js'

Here is a complete example extending the custom element testing example from Aurelia HUB page

import {StageComponent} from 'aurelia-testing';
import {bootstrap} from 'aurelia-bootstrapper';

describe('MyComponent', () => {
  let component;

  beforeEach(() => {
    component = StageComponent
      .withResources('src/my-component', 'aurelia-i18n/t')
      .inView('<my-component first-name.bind="firstName"></my-component>')
      .boundTo({ firstName: 'Bob' });
  });

  it('should render first name', done => {
    component.create(bootstrap).then(() => {
      const nameElement = document.querySelector('.firstName');
      expect(nameElement.innerHTML).toBe('Bob');
      done();
    });
  });

  afterEach(() => {
    component.dispose();
  });
});

Hopefully this will help someone using aurelia-i18n or struggling with Safari support. Feel free to write me an Email or create a pull request if I missed something.

Found some typo, want to give feedback or discuss? Feel free to contact me :)