Quantcast
Channel: ASP.NET Core – Software Engineering
Viewing all articles
Browse latest Browse all 269

Angular 2 Localization with an ASP.NET Core MVC Service

$
0
0

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

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.

{
    "version": "1.0.0",
    "description": "",
    "main": "wwwroot/index.html",
    "author": "",
    "license": "ISC",
    "scripts": {
        "tsc": "tsc",
        "tsc:w": "tsc -w",
        "typings": "typings",
        "postinstall": "typings install"
    },
    "dependencies": {
        "angular2": "2.0.0-beta.17",
        "systemjs": "0.19.26",
        "es6-shim": "^0.35.0",
        "reflect-metadata": "0.1.2",
        "rxjs": "5.0.0-beta.6",
        "zone.js": "0.6.12",
        "es6-promise": "3.1.2",
        "bootstrap": "^3.3.6",
        "gulp": "^3.9.0",
        "angular2localization": "0.5.2"
    },
    "devDependencies": {
        "jquery": "^2.2.0",
        "typescript": "1.8.10",
        "typings": "0.8.1"
    }
}

The tsconfig.json is configured to use the module system so that we can debug in Visual Studio.

{
    "compilerOptions": {
        "target": "es5",
        "module": "system",
        "moduleResolution":  "node",
        "removeComments": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "noEmitHelpers": false,
        "sourceMap": true
    },
    "exclude": [
        "node_modules",
        "typings/main",
        "typings/main.d.ts"
    ],
    "compileOnSave": false,
    "buildOnSave": false
}

The typings is configured as shown in the following code block. If the npm packages are updated, the typings definitions in the solution folder sometimes need to be deleted manually, because the existing files are not always removed.

{
    "ambientDependencies": {
        "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654",
        "jasmine": "registry:dt/jasmine#2.2.0+20160412134438"
    }
}

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.

@{
    ViewData["Title"] = "Angular 2 Localization";
}

<my-app>Loading...</my-app>

<!-- IE required polyfills, in this exact order -->
<script src="libs/es6-shim.min.js"></script>
<script src="libs/system-polyfills.js"></script>
<script src="libs/shims_for_ie.js"></script>
<script src="libs/angular2-polyfills.js"></script>
<script src="libs/system.js"></script>
<script src="libs/Rx.js"></script>
<script src="libs/angular2.dev.js"></script>
<script src="libs/jquery.min.js"></script>
<script src="js/bootstrap.js"></script>
<script src="libs/router.dev.js"></script>
<script src="libs/http.dev.js"></script>
<script src="libs/angular2localization.min.js"></script>
<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>

<script>
    System.config({
        meta: {
            '*.js' : {
                scriptLoad: true
            }
        },
        packages: {
            app: {
                format: 'register',
                defaultExtension: 'js'
            }
        }
    });
    System.import('app/boot')
        .then(null, console.error.bind(console));
</script>

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} from 'angular2/core';
import {NgClass} from 'angular2/common';
import {Location} from 'angular2/platform/common';

import {LocaleService, LocalizationService } from 'angular2localization/angular2localization';
// Pipes.
import {TranslatePipe} from 'angular2localization/angular2localization';
// Components.

import { RouteConfig, AsyncRoute, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
import { HomeComponent } from './home/home.component';
import { ShopComponent } from './shop/shop.component';
import { ProductService } from './services/ProductService';

@Component({
    selector: 'my-app',
    templateUrl: 'app/app.component.html',
    styleUrls: ['app/app.component.css'],
    directives: [ROUTER_DIRECTIVES],
    providers: [ROUTER_PROVIDERS, LocaleService, LocalizationService, ProductService], // Inherited by all descendants.
    pipes: [TranslatePipe]
})

@RouteConfig([
        { path: '/home', name: 'Home', component: HomeComponent, useAsDefault: true },
        { path: '/shop', name: 'Shop', component: ShopComponent },
])

export class AppComponent {

    constructor(
        public locale: LocaleService,
        public localization: LocalizationService,
        public location: Location,
        private _productService: ProductService
    ) {
        // 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.
    }

    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);
    }
}

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 }}</a></li>
                <li><a [routerLink]="['/Shop']">{{ 'NAV_MENU_SHOP' | translate }}</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 'angular2/core';
import { Http, Response, Headers } from 'angular2/http';
import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';
import { Product } from './Product';
import { LocaleService } from 'angular2localization/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 tranformations.

import { Component, OnInit } from 'angular2/core';
import { CORE_DIRECTIVES } from 'angular2/common';
import { Observable } from 'rxjs/Observable';
import { Router, RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
import { Http } from 'angular2/http';
import { Product } from '../services/Product';
import { LocaleService } from 'angular2localization/angular2localization';
import { ProductService } from '../services/ProductService';
import {TranslatePipe} from 'angular2localization/angular2localization';

@Component({
    selector: 'shopcomponent',
    templateUrl: 'app/shop/shop.component.html',
    directives: [CORE_DIRECTIVES, ROUTER_DIRECTIVES],
    pipes: [TranslatePipe]
})

export class ShopComponent implements OnInit {

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

    constructor(
        public _locale: LocaleService,
        private _productService: ProductService,
        private _router: Router) {
        this.message = "shop.component";

        this._locale.countryCodeChanged.subscribe(item => this.onCountryChangedDataRecieved(item));
        this._locale.currencyCodeChanged.subscribe(currency => this.onChangedCurrencyRecieved(currency));

    }

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

        this.Currency = this._locale.getCurrentCurrency();
    }

    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 onCountryChangedDataRecieved(item) {
        this.GetProducts();
        console.log("onProductDataRecieved");
        console.log(item);
    }

    private onChangedCurrencyRecieved(currency) {
        this.Currency = currency;
        console.log("onChangedCurrencyRecieved");
        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.AspNet.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.AspNet.Mvc.Localization;

namespace Angular2LocalizationAspNetCore.Providers
{
    public class ProductProvider : IProductProvider
    {
        private IHtmlLocalizer<ShopResource> _htmlLocalizer;

        public ProductProvider(IHtmlLocalizer<ShopResource> localizer)
        {
            _htmlLocalizer = 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 = _htmlLocalizer[t.Description],
                    Name = _htmlLocalizer[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;
        }
    }
}

At present the service is implemented using ASP.NET Core RC1, so due to all the Visual Studio localization tooling bugs, this needs to be run from the console to localize properly. 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
Links

https://github.com/robisim74/angular2localization

https://angular.io

http://docs.asp.net/en/1.0.0-rc2/fundamentals/localization.html



Viewing all articles
Browse latest Browse all 269

Trending Articles