Integrating Intl with Ionic

Before you continue, a quick warning. This article discusses how to localize numbers and formats for an Ionic 2 app using the Intl spec. Based on my reading, this should actually be baked into Angular 2 itself. In my testing, this was not the case. I could be wrong. I could be right and the feature is just bugged. Just know that what follows may not be technically necessary. I'm sharing it anyway as it was fun to write and gave me the opportunity to play with pipes.

Over two years ago I first wrote about the Intl spec: Working with Intl. This is an incredibly cool, and actually fairly simple, way to localize dates and numbers for your web applications. Because it was incredibly useful, of course Apple dragged their heels in supporting it, but as of iOS 10, it is finally baked in:

Intl support

With support at nearly 80%, and with it being easy to fall back when not supported, I thought I'd take a look at adding it to a simple Ionic 2 application.

I started off building a 2-page master detail application. The first page is a list of cats. For each cat, I want to output the time of their last rating.

Demo

Clicking on a cat loads a detail.

Demo

In both of the above screen shots, you can see dates and numbers printed as is - with no special formatting. While the code isn't that complex, let's take a look at it as a baseline. (Note, in the repo URL I'll share at the end of this post, you can find the original code in the src_v1 folder.)

First, here is the cat data provider. This one isn't going to change as we progress through the various versions:


import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import 'rxjs/add/operator/map';

@Injectable()
export class CatProvider {

 public cats:Array<Object> = [
   {name:'Luna', lastRating:new Date(2016, 12, 2 ,9, 30), numRatings:338324, avgRating:3.142},
   {name:'Pig', lastRating:new Date(2016, 12, 12, 16,57), numRatings:9128271, avgRating:4.842},
   {name:'Cracker', lastRating:new Date(2016, 11, 29, 13, 1), numRatings:190129, avgRating:2.734},
   {name:'Robin', lastRating:new Date(2016, 12, 19, 5, 42), numRatings:642850, avgRating:4.1},
   {name:'Simba', lastRating:new Date(2016, 12, 18, 18, 18), numRatings:80213, avgRating:1.9999}
   ];


  constructor() {
    console.log('Hello CatProvider Provider');
  }

  load() {
    console.log('called load');
    return Observable.from(this.cats);
  }
}

Now let's look at the view for the home page:


<ion-header>
  <ion-navbar>
    <ion-title>
      Intl Demo
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>

  <ion-list inset>
    <ion-item *ngFor="let cat of cats" (click)="loadCat(cat)" detail-push>
      {{cat.name}} 
      <ion-note item-right>Last rated: {{cat.lastRating}}</ion-note>
    </ion-item>
  </ion-list>

</ion-content>

And the code behind it:


import { Component } from '@angular/core';
import { CatProvider } from '../../providers/cat-provider';
import { NavController } from 'ionic-angular';
import { DetailPage } from '../detail/detail';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html',
  providers:[CatProvider]
})
export class HomePage {

 public cats:Array<Object> = [];

  constructor(public navCtrl: NavController, public catProvider:CatProvider) {
    catProvider.load().subscribe((catData) => {      
      this.cats.push(catData);
    });
  }

  loadCat(cat) {
    this.navCtrl.push(DetailPage, {selectedCat:cat});
  }

}

Next up is the detail view:


<ion-header>

  <ion-navbar>
    <ion-title>{{cat.name}}</ion-title>
  </ion-navbar>

</ion-header>


<ion-content padding>

  <ion-card>
    <ion-card-header>
      Details
    </ion-card-header>

    <ion-card-content>
      The cat {{cat.name}} has gotten {{cat.numRatings}} ratings with an 
      average of {{cat.avgRating}}.
    </ion-card-content>

  </ion-card>
</ion-content>

And its code:


import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';

/*
  Generated class for the DetailPage page.

  See http://ionicframework.com/docs/v2/components/#navigation for more info on
  Ionic pages and navigation.
*/
@Component({
  selector: 'page-detail',
  templateUrl: 'detail.html'
})
export class DetailPage {

  public cat:Object;

  constructor(public navCtrl: NavController, public navParams: NavParams) {
    console.log(navParams);
    this.cat = navParams.data.selectedCat;
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad DetailPage');
  }

}

Round One

The first change (found in src_v2) I did was to employ Angular's built in pipes for date and number formatting. In the home view, I used this:


<ion-note item-right>Last rated: {{cat.lastRating | date:'shortDate'}}</ion-note>

And in the detail I used this:


<ion-card-content>
    The cat {{cat.name}} has gotten {{cat.numRatings | number}} ratings with an 
    average of {{cat.avgRating | number:'1.0-2'}}.
</ion-card-content>

Nice and simple and we're done, right? Well as I said above, this worked for me, but only in the English locale. Switching to French did nothing to change the output. To be clear, maybe I was doing it wrong. But again, this simply didn't do it for me. On to round two!

Round Two

So I gave up on the pipes (and removed them from the view!) and switched to using Intl (this may be found in src_v3). I began by adding code to modify the result from the provider in home.ts. To be clear, this felt wrong, but was my first draft with adding Intl:


import { Component } from '@angular/core';
import { CatProvider } from '../../providers/cat-provider';
import { NavController } from 'ionic-angular';
import { DetailPage } from '../detail/detail';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html',
  providers:[CatProvider]
})
export class HomePage {

 public cats:Array<Object> = [];

  dtFormat(d) {
    if(Intl) {
      return new Intl.DateTimeFormat().format(d) + ' ' + new Intl.DateTimeFormat(navigator.language, {hour:'numeric',minute:'2-digit'}).format(d);
    } else {
      return d;
    }
  }

  constructor(public navCtrl: NavController, public catProvider:CatProvider) {
    catProvider.load().subscribe((catData:any) => {

      catData.lastRating = this.dtFormat(catData["lastRating"]);
      this.cats.push(catData);

    });
  }

  loadCat(cat) {
    this.navCtrl.push(DetailPage, {selectedCat:cat});
  }

}

My function dtFormat sniffs for Intl. If it exists, I format both a date and time string. Notice that in order to do this, I format twice with options. The second call is more complex because I have to pass in a value for locale if I want to pass options (a mistake in the API imo).

I do something similar for the numbers:


import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';

/*
  Generated class for the DetailPage page.

  See http://ionicframework.com/docs/v2/components/#navigation for more info on
  Ionic pages and navigation.
*/
@Component({
  selector: 'page-detail',
  templateUrl: 'detail.html'
})
export class DetailPage {

  public cat:Object;

  numberFormat(d) {
    if(Intl) {
      return new Intl.NumberFormat().format(d);
    } else {
      return d;
    }
  }

  constructor(public navCtrl: NavController, public navParams: NavParams) {
    navParams.data.selectedCat.numRatings = this.numberFormat(navParams.data.selectedCat.numRatings);
    navParams.data.selectedCat.avgRating = this.numberFormat(navParams.data.selectedCat.avgRating);
    this.cat = navParams.data.selectedCat;
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad DetailPage');
  }

}

And this worked! But I didn't like modifying the data like that. I knew I could write my own pipes, so I did so in the next version.

Round Three

I began by adding a pipes folder to my src (this version of the app is in the src and src_v4 folders). Here is my date pipe:


import { Pipe, PipeTransform } from '@angular/core';

@Pipe({name: 'dtFormat'})
export class dtFormatPipe implements PipeTransform {
  transform(value: Date): string {

    if(Intl) {
      return new Intl.DateTimeFormat().format(value) + ' ' + new Intl.DateTimeFormat(navigator.language, {hour:'numeric',minute:'2-digit'}).format(value);
    } else {
      return value.toString();
    }

  }
}

And here is my number pipe:


import { Pipe, PipeTransform } from '@angular/core';

@Pipe({name: 'numberFormat'})
export class numberFormatPipe implements PipeTransform {
  transform(value: string): string {

    if(Intl) {
      return new Intl.NumberFormat(navigator.language, {maximumFractionDigits:2}).format(Number(value));
    } else {
      return value;
    }

  }
}

Notice the addition of maximumFractionDigits. This will cut off decimals to 2 places. In theory I could build my pipe so that was an argument passed in by the view, but I kept it simple. I removed my code from the page controllers, and then simply added my new pipes in to the views. (Note - you also have to add in your pipes to app.module.ts. I forget this in nearly every Ionic 2 app I build.)

First the home page:


<ion-note item-right>Last rated: {{cat.lastRating | dtFormat}}</ion-note>

Then the detail:


<ion-card-content>
    The cat {{cat.name}} has gotten {{cat.numRatings | numberFormat }} ratings with an 
    average of {{cat.avgRating | numberFormat}}.
</ion-card-content>

Here is the result with French set as my language:

Demo

Demo

For comparison, here is the home page with English set as my language:

Demo

You can find the full source here: https://github.com/cfjedimaster/Cordova-Examples/tree/master/ionic_intl

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

Want to read more like this?