Angular 2 Localization with an ASP.NET Core MVC Service

This article shows how localization can be implemented in Angular 2 for static UI translations and also for localized data requested from a MVC service. The MVC service is implemented using ASP.NET Core. This post is the first of a 3 part series. The following posts will implement the service to use a database and also implement an Angular 2 form to add dynamic data which can be used in the localized views.

Code: https://github.com/damienbod/Angular2LocalizationAspNetCore

2016.11.01: Updated to Angular 2.1.1, angular2localization 1.1.0, Webpack 2, AoT, treeshaking
2016.10.01: Updated to Angular 2.0.1, angular2localization 1.0.3, typings
2016.09.18: Updated to Angular 2 release, ASP.NET Core 1.0.1, angular2localization 1.0.1
2016.09.04: Updated to Angular 2 rc.6 and angular2localization 0.10.0
2016.08.14: Updated to Angular 2 rc.5 and angular2localization 0.8.10
2016.07.06: Updated to Angular 2 rc.4 and angular2localization 0.8.6
2016.06.28: Updated to Angular 2 rc.3, angular2localization 0.8.5 and dotnet RTM
2016.06.17: Updated to Angular 2 rc.2 and angular2localization 0.8.4
2016.05.31: Using Webpack for Angular 2 app, angular2localization version 0.8.1, updated by Roberto Simonetti.
2016.05.16: Updated to ASP.NET Core RC2 dotnet
2016.05.13: Updated to angular2localization version 0.7.1
2016.05.07: Updated by Roberto Simonetti to Angular 2 rc.1, thanks

Posts in this series

Creating the Angular 2 app and adding angular2localization

The project is setup using Visual Studio using ASP.NET Core MVC. The npm package.json file is configured to include the required frontend dependencies. angular2localization from Roberto Simonetti is used for the Angular 2 localization.

package.json

{
  "version": "1.0.0",
  "description": "",
  "main": "wwwroot/index.html",
  "author": "",
  "license": "ISC",
  "scripts": {
    "ngc": "ngc -p ./tsconfig-aot.json",
    "start": "webpack-dev-server --inline --progress --port 8080",
    "webpack-dev": "set NODE_ENV=development&& webpack",
    "webpack-prod": "set NODE_ENV=production&& webpack",
    "build": "npm run webpack-dev",
    "buildProduction": "npm run ngc && npm run webpack-prod"
  },
    "dependencies": {
        "@angular/common": "~2.1.1",
        "@angular/compiler": "~2.1.1",
        "@angular/core": "~2.1.1",
        "@angular/forms": "~2.1.1",
        "@angular/http": "~2.1.1",
        "@angular/platform-browser": "~2.1.1",
        "@angular/platform-browser-dynamic": "~2.1.1",
        "@angular/router": "~3.1.1",
        "@angular/upgrade": "~2.1.1",
        "angular-in-memory-web-api": "~0.1.13",
        "core-js": "^2.4.1",
        "reflect-metadata": "^0.1.8",
        "rxjs": "5.0.0-beta.12",
        "systemjs": "0.19.39",
        "zone.js": "^0.6.25",
        "@angular/compiler-cli": "~2.1.2",
        "@angular/platform-server": "~2.1.0",
        "bootstrap": "^3.3.7",
        "ie-shim": "^0.1.0",
        "angular2localization": "1.1.1"
    },
  "devDependencies": {
    "@types/node": "^6.0.45",
    "awesome-typescript-loader": "^2.2.4",
    "angular2-template-loader": "^0.5.0",
    "source-map-loader": "^0.1.5",
    "clean-webpack-plugin": "^0.1.9",
    "copy-webpack-plugin": "^2.1.3",
    "html-webpack-plugin": "^2.8.1",
    "css-loader": "^0.23.0",
    "file-loader": "^0.8.4",
    "jquery": "^2.2.0",
    "json-loader": "^0.5.3",
    "node-sass": "^3.10.1",
    "sass-loader": "^4.0.2",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.5.2",
    "style-loader": "^0.13.0",
    "ts-helpers": "^1.1.1",
    "typescript": "2.0.3",
    "url-loader": "^0.5.6",
    "webpack": "2.1.0-beta.25",
    "webpack-dev-server": "^1.16.2"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": true,
    "noImplicitAny": true,
    "skipLibCheck": true,
    "lib": [
      "es2015",
      "dom"
    ],
    "types": [
      "node"
    ]
  },
  "files": [
    "angular2App/app/app.module.ts",
    "angular2App/main.ts"
  ],
  "awesomeTypescriptLoaderOptions": {
    "useWebpackText": true
  },
  "compileOnSave": false,
  "buildOnSave": false
}

The webpack.config.js file defines the webpack build which can be used from the Visual Studio Webpack task runner, or directly in the command line. This build has a production, development switch.

/// <binding ProjectOpened='Run - Development' />

var environment = (process.env.NODE_ENV || "development").trim();

if (environment === "development") {
    module.exports = require('./webpack.dev.js');
} else {
    module.exports = require('./webpack.prod.js');
}

The UI localization resource files MUST be saved in UTF-8, otherwise the translations will not be displayed correctly, and IE 11 will also throw exceptions. Here is an example of the locale-de.json file. The path definitions are defined in the AppComponent typescript file.

{
    "HELLO": "Hallo",
    "NAV_MENU_HOME": "Aktuell",
    "NAV_MENU_SHOP": "Online-Shop"
}

The index HTML file adds all the Javascript dependencies directly and not using the system loader. These can all be found in the libs folder of the wwwroot. The files are deployed to the libs folder from the node_modules using gulp.

<!DOCTYPE html>
<html>
<head>
    <base href="/">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Angular 2 ASP.NET Core api</title>

    <meta http-equiv="content-type" content="text/html; charset=utf-8" />

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <link rel="stylesheet" href="css/bootstrap.css">
</head>
<body>

    <div class="container body-content">
        <my-app>Loading...</my-app>

        <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en-US,Intl.~locale.de-CH,Intl.~locale.it-CH,Intl.~locale.fr-CH"></script>

        <!--loads the application-->
        <script src="dist/app.bundle.js"></script>
    </div>
    
</body>
</html>

The app.module adds the localization service so that it could be used inside module.

import { NgModule } from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { Configuration } from './app.constants';
import { routing } from './app.routes';
import { HttpModule, JsonpModule } from '@angular/http';

import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ShopComponent } from './shop/shop.component';
import { ShopAdminComponent } from './shop-admin/shop-admin.component';

import { ProductService } from './services/ProductService';

import { LocaleModule, LocalizationModule } from 'angular2localization';

@NgModule({
    imports: [
        BrowserModule,
        CommonModule,
        FormsModule,
        routing,
        HttpModule,
        JsonpModule,
        LocaleModule, // LocaleService is singleton.
        LocalizationModule.forChild() // New instance of LocalizationService.
    ],
    declarations: [
        AppComponent,
        ShopComponent,
        HomeComponent,
        ShopAdminComponent
    ],
    providers: [
        ProductService,
        Configuration
    ],
    bootstrap:    [AppComponent],
})

export class AppModule {}

The AppComponent loads and uses the Angular 2 localization npm package. The languages, country and currency are defined in this component. For this app, de-CH, fr-CH, it-CH and en-US are used, and CHF or EUR can be used as a currency. The ChangeCulture function is used to set the required values.

import { Component, OnInit} from '@angular/core';

// Services.
import { Locale, LocaleService, LocalizationService } from 'angular2localization';
import { ProductService } from './services/ProductService';

// AoT compilation doesn't support 'require'.
import './app.component.scss';
import '../style/app.scss';

@Component({
    selector: 'my-app',
    templateUrl: 'app.component.html'
})


export class AppComponent extends Locale {

    constructor(
        public locale: LocaleService,
        public localization: LocalizationService,
        private _productService: ProductService
    ) {
        super(null, localization);
        // Adds a new language (ISO 639 two-letter code).
        this.locale.addLanguage('de');
        this.locale.addLanguage('fr');
        this.locale.addLanguage('it');
        this.locale.addLanguage('en');

        this.locale.definePreferredLocale('en', 'US', 30);

        this.localization.translationProvider('./i18n/locale-'); // Required: initializes the translation provider with the given path prefix.
        this.localization.updateTranslation(); // Need to update the translation.

        this.locale.languageCodeChanged.subscribe(
            (item: string) => { this.onLanguageCodeChangedDataRecieved(item) }
        );

    }

    public ChangeCulture(language: string, country: string, currency: string) {
        this.locale.setCurrentLocale(language, country);
        this.locale.setCurrentCurrency(currency);
    }

    public ChangeCurrency(currency: string) {
        this.locale.setCurrentCurrency(currency);
    }

    private onLanguageCodeChangedDataRecieved(item: string) {
        console.log("onLanguageCodeChangedDataRecieved App");
        console.log(item);
    }
}

The HTML template uses bootstrap and defines the input links and the routing links which are used in the application. The translate pipe is used to display the text in the correct language.

<div class="container" style="margin-top: 15px;">
   
    <nav class="navbar navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <a [routerLink]="['/home']" class="navbar-brand"><img src="images/damienbod.jpg" height="40" style="margin-top:-10px;" /></a>
            </div>
            <ul class="nav navbar-nav">
                <li><a [routerLink]="['/home']">{{ 'NAV_MENU_HOME' | translate:lang }}</a></li>
                <li><a [routerLink]="['/shop']">{{ 'NAV_MENU_SHOP' | translate:lang }}</a></li>
            </ul>

            <ul class="nav navbar-nav navbar-right">
                <li><a (click)="ChangeCulture('de','CH', 'CHF')">de</a></li>
                <li><a (click)="ChangeCulture('fr','CH', 'CHF')">fr</a></li>
                <li><a (click)="ChangeCulture('it','CH', 'CHF')">it</a></li>
                <li><a (click)="ChangeCulture('en','US', 'CHF')">en</a></li>
            </ul>

            <ul class="nav navbar-nav navbar-right">
                <li>
                    <div class="navbar" style="margin-bottom:0;">
                        <form class="navbar-form pull-left">
                            <select (change)="ChangeCurrency($event.target.value)" class="form-control">
                                <option *ngFor="let currency of ['CHF', 'EUR']">{{currency}}</option>
                            </select>
                        </form>
                    </div>
                </li>             
            </ul>
        </div>
    </nav>

    <router-outlet></router-outlet>

</div>

Implementing the ProductService

The ProductService can be used to access the localized data from the ASP.NET Core MVC service. This service uses the LocaleService to get the current language and the current country and sends this in a HTTP GET request to the server service api. The data is then returned as required. The localization can be set by adding ?culture=de-CH to the URL.

import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';
import { Product } from './Product';
import { LocaleService } from 'angular2localization';

@Injectable()
export class ProductService {
    private actionUrl: string;
    private headers: Headers;
    private isoCode: string;

    constructor(private _http: Http, private _configuration: Configuration, public _locale: LocaleService) {
        this.actionUrl = `${_configuration.Server}api/Shop/`;       
    }

    private setHeaders() {
        this.headers = new Headers();
        this.headers.append('Content-Type', 'application/json');
        this.headers.append('Accept', 'application/json');
    }

    // http://localhost:5000/api/Shop/AvailableProducts?culture=de-CH
    // http://localhost:5000/api/Shop/AvailableProducts?culture=it-CH
    // http://localhost:5000/api/Shop/AvailableProducts?culture=fr-CH
    // http://localhost:5000/api/Shop/AvailableProducts?culture=en-US
    public GetAvailableProducts = (): Observable<Product[]> => {
        console.log(this._locale.getCurrentLanguage());
        console.log(this._locale.getCurrentCountry());
        this.isoCode = `${this._locale.getCurrentLanguage()}-${this._locale.getCurrentCountry()}`; 

        this.setHeaders();
        return this._http.get(`${this.actionUrl}AvailableProducts?culture=${this.isoCode}`, {
            headers: this.headers
        }).map(res => res.json());
    }   
}


Using the localization to display data

The ShopComponent uses the localized data from the server. This service uses the @Output countryCodeChanged and currencyCodeChanged event properties from the LocaleService so that when the UI culture is changed, the data is got from the server and displayed as required. The TranslatePipe is used in the HTML to display the frontend static localization transformations.

import { Component, OnInit } from '@angular/core';
import { Product } from '../services/Product';
import { ProductService } from '../services/ProductService';
import { Locale, LocaleService, LocalizationService } from 'angular2localization';

@Component({
    selector: 'shop-component',
    templateUrl: 'shop.component.html'
})

export class ShopComponent extends Locale implements OnInit {

    public message: string;
    public Products: Product[];
    public Currency: string;
    public Price: string;

    constructor(
        public _locale: LocaleService,
        public localization: LocalizationService,
        private _productService: ProductService
    ) {
        super(null, localization);
        this.message = "shop.component";
        this._locale.languageCodeChanged.subscribe(
            (item: string) => { this.onLanguageCodeChangedDataRecieved(item) }
        );
        this._locale.currencyCodeChanged.subscribe(
            (currency: string) => {
                this.onChangedCurrencyRecieved(currency)
            }
        );
    }

    ngOnInit() {
        console.log("ngOnInit ShopComponent");  
        this.GetProducts();

        this.Currency = this._locale.getCurrentCurrency();
        if (!(this.Currency === "CHF" || this.Currency === "EUR")) {
            this.Currency = "CHF";
        } 
    }

    public GetProducts() {
        console.log('ShopComponent:GetProducts starting...');
        this._productService.GetAvailableProducts()
            .subscribe((data) => {
                this.Products = data;
            },
            error => console.log(error),
            () => {
                console.log('ProductService:GetProducts completed');
            }
            );
    } 

    private onLanguageCodeChangedDataRecieved(item: string) {
        this.GetProducts();
        console.log("onCountryChangedDataRecieved Shop");
        console.log(item);
    }

    private onChangedCurrencyRecieved(currency: string) {
        this.Currency = currency;
        console.log("onLanguageCodeChangedDataRecieved Shop");
        console.log(currency);
    }
}

The Shop component HTML template displays the localized data.

<div class="panel-group" >

    <div class="panel-group" *ngIf="Products">

        <div class="mcbutton col-md-4" style="margin-left: -15px; margin-bottom: 10px;" *ngFor="let product of Products">
            <div class="panel panel-default" >
                <div class="panel-heading" style="color: #9d9d9d;background-color: #222;">
                    {{product.name}}
                    <span style="float:right;" *ngIf="Currency === 'CHF'">{{product.priceCHF}} {{Currency}}</span>
                    <span style="float:right;" *ngIf="Currency === 'EUR'">{{product.priceEUR}} {{Currency}}</span>
                </div>
                <div class="panel-body" style="height: 200px;">
                    <!--<img src="images/mc1.jpg" style="width: 100%;margin-top: 20px;" />-->
                    {{product.description}}
                </div>
            </div>
        </div>
    </div>

</div>

ASP.NET Core MVC service

The ASP.NET Core MVC service uses the ShopController to provide the data for the Angular 2 application. This just returns a list of Projects using a HTTP GET request.

The IProductProvider interface is used to get the data. This is added to the controller using construction injection and needs to be registered in the Startup class.

using Angular2LocalizationAspNetCore.Providers;
using Microsoft.AspNetCore.Mvc;

namespace Angular2LocalizationAspNetCore.Controllers
{
    [Route("api/[controller]")]
    public class ShopController : Controller
    {
        private readonly IProductProvider _productProvider;

        public ShopController(IProductProvider productProvider)
        {
            _productProvider = productProvider;
        }

        // http://localhost:5000/api/shop/AvailableProducts
        [HttpGet("AvailableProducts")]
        public IActionResult GetAvailableProducts()
        {
            return Ok(_productProvider.GetAvailableProducts());
        }
    }
}

The ProductDto is used in the GetAvailableProducts to return the localized data.

namespace Angular2LocalizationAspNetCore.ViewModels
{
    public class ProductDto
    {
        public long Id { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }

        public string ImagePath { get; set; }

        public double PriceEUR { get; set; }

        public double PriceCHF { get; set; }
    }
}

The ProductProvider which implements the IProductProvider interface returns a list of localized products using resource files and a in memory list. This is just a dummy implementation to simulate data responses with localized data.

using System.Collections.Generic;
using Angular2LocalizationAspNetCore.Models;
using Angular2LocalizationAspNetCore.Resources;
using Angular2LocalizationAspNetCore.ViewModels;
using Microsoft.Extensions.Localization;

namespace Angular2LocalizationAspNetCore.Providers
{
    public class ProductProvider : IProductProvider
    {
        private IStringLocalizer<ShopResource> _stringLocalizer;

        public ProductProvider(IStringLocalizer<ShopResource> localizer)
        {
            _stringLocalizer = localizer;
        }

        public List<ProductDto> GetAvailableProducts()
        {
            var dataSimi = InitDummyData();
            List<ProductDto> data = new List<ProductDto>();
            foreach(var t in dataSimi)
            {
                data.Add(new ProductDto() {
                    Id = t.Id,
                    Description = _stringLocalizer[t.Description],
                    Name = _stringLocalizer[t.Name],
                    ImagePath = t.ImagePath,
                    PriceCHF = t.PriceCHF,
                    PriceEUR = t.PriceEUR
                });
            }

            return data;
        }

        private List<Product> InitDummyData()
        {
            List<Product> data = new List<Product>();
            data.Add(new Product() { Id = 1, Description = "Mini HTML for content", Name="HTML wiz", ImagePath="", PriceCHF = 2.40, PriceEUR= 2.20  });
            data.Add(new Product() { Id = 2, Description = "R editor for data anaylsis", Name = "R editor", ImagePath = "", PriceCHF = 45.00, PriceEUR = 40 });
            return data;
        }
    }
}

In the second part of this series, this ProductProvider will be re-implemented to use SQL localization and use only data from a database.

When the application is opened, the language, country and currency can be changed as required.

Application in de-CH with currency CHF
angula2Localization_01

Application in fr-CH with currency EUR
angula2Localization_02

Notes

Angular 2 loads slowly, need to use Webpack or some other tool.

Links

https://github.com/robisim74/angular2localization

https://angular.io

https://docs.asp.net/en/latest/fundamentals/localization.html

8 comments

  1. […] Angular 2 Localization with an ASP.NET Core MVC Service – Damien Bowden takes a look at performing localisation of an Angular based application using a service implemented in ASP.NET Core MVC to return the localisation data. […]

  2. […] the full article, click here. @jeremylikness: “#angular2 localization with an #aspnet Core #Mvc app #ng2 […]

  3. Federico · · Reply

    Thanks for the post was really useful. I download your code and after build i got this error. “Property ‘map’ does not exist on type ‘Observable'”
    I have the same issue in my solution and i found only that no solved question:

    http://stackoverflow.com/questions/37030963/angular-2-2-0-0-rc-1-property-map-does-not-exist-on-type-observableresponse

    Any idea?

    Thanks!

    1. Hi Federico
      Thanks for your comment. I assume its a problem in the VS build. I don’t have this problem. Can you try deleting the your node_modules and also the libs inside the wwwroot from hand. Then do a npm install from the command line, then execute gulp copy tasks, and then a rebuild.

      And also try updating the nodejs and typescript to the latest versions.

      Greetings Damien

  4. Federico · · Reply

    I did the same process but couldn’t fix it. Looks like VS 2015 intellisense issue. I’ll still researching about it.

    Thanks Damien!

    1. OK, let me know what you find.

      Greetings Damien

  5. Vladimir · · Reply

    Hi Damien

    First of all I want to say thank you for this good article!
    I have a problem with localization inside of ts files.
    Can you please provide an example of how to localize string in js and assign it to variable?

  6. […] Angular 2 Localization with an ASP.NET Core MVC Service […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: