This article shows how localization can be implemented in Angular 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 form to add dynamic data which can be used in the localized views.
Code: https://github.com/damienbod/AngularLocalizationAspNetCore
2018-08-01: ASP.NET Core 2.1, Localization.SqlLocalizer 2.0.2 , Angular 6.1.0, angular-l10n 5.0.0
2017-08-20: ASP.NET Core 2.0, Localization.SqlLocalizer 2.0.0 , Angular 4.3.5, angular-l10n 3.5.0
2017-05-14: Localization.SqlLocalizer 1.0.10 , Angular 4.1.0, latest dotnet core packages
2017-02-03: Updated to angular-l10n 2.0.0, VS2017 RC3 msbuild3, angular 2.4.5 and webpack 2.2.1
2017-01-07: Updated to ASP.NET Core 1.1 and VS2017 with csproj, angular 2.4.1 and webpack 2.2.0-rc.3
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
- Angular Localization with an ASP.NET Core MVC Service
- Creating and requesting SQL localized data in ASP.NET Core
- Adding SQL localization data using an Angular form and ASP.NET Core
Creating the Angular app and adding angular-l10n
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. angular-l10n from Roberto Simonetti is used for the Angular localization.
package.json
{ "name": "angular-webpack-visualstudio", "version": "5.0.3", "description": "An Angular VS template", "main": "wwwroot/index.html", "author": "", "license": "ISC", "repository": { "type": "git", "url": "https://github.com/damienbod/Angular2WebpackVisualStudio.git" }, "scripts": { "start": "concurrently \"webpack-dev-server --env=dev --open --hot --inline --port 8080\" \"dotnet run\" ", "webpack-dev": "webpack --env=dev", "webpack-production": "webpack --env=prod", "build-dev": "npm run webpack-dev", "build-production": "npm run webpack-production", "watch-webpack-dev": "webpack --env=dev --watch --color", "watch-webpack-production": "npm run build-production --watch --color", "publish-for-iis": "npm run build-production && dotnet publish -c Release", "test": "karma start", "test-ci": "karma start --single-run --browsers ChromeHeadless", "lint": "tslint ./angularApp" }, "dependencies": { "angular-l10n": "5.0.0", "@angular/animations": "6.1.0", "@angular/common": "6.1.0", "@angular/compiler": "6.1.0", "@angular/compiler-cli": "6.1.0", "@angular/core": "6.1.0", "@angular/forms": "6.1.0", "@angular/http": "6.1.0", "@angular/platform-browser": "6.1.0", "@angular/platform-browser-dynamic": "6.1.0", "@angular/platform-server": "6.1.0", "@angular/router": "6.1.0", "@angular/upgrade": "6.1.0", "@angular-devkit/core": "0.7.1", "bootstrap": "4.1.3", "core-js": "2.5.7", "ie-shim": "0.1.0", "rxjs": "6.2.2", "rxjs-compat": "6.2.2", "zone.js": "0.8.26" }, "devDependencies": { "@ngtools/webpack": "^6.1.1", "@types/jasmine": "^2.8.8", "@types/node": "^10.5.5", "angular-router-loader": "0.8.5", "angular2-template-loader": "^0.6.2", "awesome-typescript-loader": "^5.2.0", "clean-webpack-plugin": "^0.1.19", "codelyzer": "^4.4.2", "concurrently": "^3.6.1", "copy-webpack-plugin": "^4.5.2", "css-loader": "^1.0.0", "file-loader": "^1.1.11", "html-webpack-plugin": "^3.2.0", "jasmine-core": "3.1.0", "jquery": "^3.3.1", "json-loader": "^0.5.7", "karma": "^2.0.5", "karma-chrome-launcher": "^2.2.0", "karma-jasmine": "^1.1.2", "karma-jasmine-html-reporter": "^1.2.0", "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "^0.0.32", "karma-webpack": "^3.0.0", "node-sass": "^4.9.2", "raw-loader": "^0.5.1", "rimraf": "^2.6.2", "sass-loader": "^7.0.3", "source-map-loader": "^0.2.3", "style-loader": "^0.21.0", "tslint": "^5.11.0", "tslint-loader": "^3.6.0", "typescript": "2.7.2", "uglifyjs-webpack-plugin": "^1.2.7", "url-loader": "^1.0.1", "webpack": "^4.16.3", "webpack-bundle-analyzer": "^2.13.1", "webpack-cli": "3.1.0", "webpack-dev-server": "^3.1.5" }, "-vs-binding": { "ProjectOpened": [ "watch-webpack-dev" ] } }
tsconfig.json
{ "compilerOptions": { "target": "es5", "module": "es2015", "moduleResolution": "node", "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "removeComments": true, "noImplicitAny": true, "skipLibCheck": true, "lib": [ "es2015", "dom" ], "typeRoots": [ "./node_modules/@types/" ] }, "exclude": [ "node_modules", "angularApp/main-aot.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' /> module.exports = function(env) { return require(`./config/webpack.${env}.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 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 { Configuration } from './app.constants'; import { AppComponent } from './app.component'; import { routing } from './app.routes'; import { HttpClientModule } from '@angular/common/http'; 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 { L10nConfig, L10nLoader, TranslationModule, StorageStrategy, ProviderType } from 'angular-l10n'; const l10nConfig: L10nConfig = { locale: { languages: [ { code: 'en', dir: 'ltr' }, { code: 'it', dir: 'ltr' }, { code: 'fr', dir: 'ltr' }, { code: 'de', dir: 'ltr' } ], language: 'en', storage: StorageStrategy.Cookie }, translation: { providers: [ { type: ProviderType.Static, prefix: './i18n/locale-' } ], caching: true, missingValue: 'No key' } }; @NgModule({ imports: [ BrowserModule, CommonModule, FormsModule, routing, HttpClientModule, TranslationModule.forRoot(l10nConfig), ], declarations: [ AppComponent, ShopComponent, HomeComponent, ShopAdminComponent ], providers: [ ProductService, Configuration ], bootstrap: [AppComponent], }) export class AppModule { constructor(public l10nLoader: L10nLoader) { this.l10nLoader.load(); } }
The AppComponent loads and uses the Angular 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 '@angular/core'; import { LocaleService, TranslationService, Language } from 'angular-l10n'; import './app.component.scss'; import '../styles/app.scss'; @Component({ selector: 'app-component', templateUrl: 'app.component.html' }) export class AppComponent { @Language() lang = ''; constructor(public locale: LocaleService, public translation: TranslationService ) { this.locale.defaultLocaleChanged.subscribe((item: string) => { this.onLanguageCodeChangedDataRecieved(item); }); } public ChangeCulture(language: string, country: string, currency: string) { this.locale.setDefaultLocale(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 { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Configuration } from '../app.constants'; import { Product } from './Product'; import { ProductCreateEdit } from './ProductCreateEdit'; import { LocaleService } from 'angular-l10n'; @Injectable() export class ProductService { private actionUrl: string; private actionUrlShopAdmin: string; private headers: HttpHeaders; private isoCode = ''; constructor(public locale: LocaleService, private http: HttpClient, configuration: Configuration ) { this.actionUrl = `${configuration.Server}api/Shop/`; this.actionUrlShopAdmin = `${configuration.Server}api/ShopAdmin/`; this.headers = new HttpHeaders(); this.headers = this.headers.set('Content-Type', 'application/json'); this.headers = this.headers.set('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()}`; return this.http.get<Product[]>(`${this.actionUrl}AvailableProducts?culture=${this.isoCode}`, { headers: this.headers }); } public CreateProduct(product: ProductCreateEdit): Observable<ProductCreateEdit> { const item = JSON.stringify(product); return this.http.post<ProductCreateEdit>(this.actionUrlShopAdmin, item, { headers: this.headers }); } }
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 { LocaleService, TranslationService, Language } from 'angular-l10n'; @Component({ selector: 'app-shop-component', templateUrl: 'shop.component.html' }) export class ShopComponent implements OnInit { @Language() lang = ''; public message: string; public Products: Product[] = []; public Currency = ''; public Price = ''; constructor( public _locale: LocaleService, public localization: TranslationService, private _productService: ProductService ) { this.message = 'shop.component'; this._locale.defaultLocaleChanged.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 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
Application in fr-CH with currency EUR
Links
https://github.com/robisim74/angular-l10n
https://docs.asp.net/en/latest/fundamentals/localization.html
[…] 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. […]
[…] the full article, click here. @jeremylikness: “#angular2 localization with an #aspnet Core #Mvc app #ng2 […]
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!
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
I did the same process but couldn’t fix it. Looks like VS 2015 intellisense issue. I’ll still researching about it.
Thanks Damien!
OK, let me know what you find.
Greetings Damien
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?
[…] Angular 2 Localization with an ASP.NET Core MVC Service […]
very nice article.