Deploying an i18n Angular app with angular-cli

Updated 2017/Apr/24 with information about my Angular Translator application.

I will explain in this article how to create from scratch an internationalized (i18n) Angular app with the use of the angular-cli and how to deploy it on an Apache web server.

The following versions are used:

  • angular/cli: 1.0.0
  • angular: 4.0.0
  • Apache 2.4

The described sample app is available at: https://github.com/feloy/angular-cli-i18n-sample

A fresh i18n app

We first create a fresh Angular app with the help of angular-cli:

$ ng new angular-cli-i18n-sample

We make some changes to add some translatable text, in app.component.html:

<h1 i18n>Hello world!</h1>

We need now to create an xlf file with the translatable strings. We can generate the file src/i18n/messages.xlf with the following command:

$ ng xi18n --output-path src/i18n

We now create translations for different languages, here in english with a fresh file src/i18n/messages.en.xlf copied from src/i18n/messages.xlf:

[...]
<trans-unit id="[...]" datatype="html">
<source>Hello World!</source>
<target>Hello World!</target>
</trans-unit>
[...]

in french with src/i18n/messages.fr.xlf:

[...]
<trans-unit id="[...]" datatype="html">
<source>Hello World!</source>
<target>Salut la foule !</target>
</trans-unit>
[...]

and in spanish with src/i18n/messages.es.xlf:

[...]
<trans-unit id="[...]" datatype="html">
<source>Hello World!</source>
<target>¿hola, qué tal?</target>
</trans-unit>
[...]

It is now possible to make angular-cli serving the app with the language of your choice, here in spanish:

$ ng serve --aot \
--i18n-file=src/i18n/messages.es.xlf \
--locale=es \
--i18n-format=xlf

You can access the app at http://localhost:4200 and you can see that it displays the spanish string!

Prepare the app for production

In production, we would like the app to be accessible in different subdirectories, depending on the language; for example the spanish version would be accessible at http://myapp.com/es/ and the french one at http://myapp.com/fr/. We also would like to be redirected from the base url http://myapp.com/ to the url of our preferred language.

For this, we guess that we need to change the base href to es, en or fr, depending on the target language. angular-cli has a special command-line option for this, --bh which permits to declare the base href at compile time from command line.

Linux/macOS users

Here is the shell command we can use to create the different bundles for the different languages:

$ for lang in es en fr; do \
ng build --output-path=dist/$lang \
--aot \
-prod \
--bh /$lang/ \
--i18n-file=src/i18n/messages.$lang.xlf \
--i18n-format=xlf \
--locale=$lang; \
done

We can create a script definition in package.json for this command and execute it with npm run build-i18n:

{
[...]
"scripts": {
[...]
"build-i18n": "for lang in en es fr; do ng build --output-path=dist/$lang --aot -prod --bh /$lang/ --i18n-file=src/i18n/messages.$lang.xlf --i18n-format=xlf --locale=$lang; done"
}
[...]
}

At this point we get three directories en/, es/ and fr/ into the dist/ directory, containing the different bundles.

Windows users

As a Windows user, you can use these commands to build your different bundles for different languages:

> ng build --output-path=dist/fr --aot -prod --bh /fr/ --i18n-file=src/i18n/messages.fr.xlf --i18n-format=xlf --locale=fr
> ng build --output-path=dist/es --aot -prod --bh /es/ --i18n-file=src/i18n/messages.es.xlf --i18n-format=xlf --locale=es
> ng build --output-path=dist/en --aot -prod --bh /en/ --i18n-file=src/i18n/messages.en.xlf --i18n-format=xlf --locale=en

We can create script definitions in package.json for these commands and a supplementary one to run all these commands at once and execute the last one with npm run build-i18n:

"scripts": {
"build-i18n:fr": "ng build --output-path=dist/fr --aot -prod --bh /fr/ --i18n-file=src/i18n/messages.fr.xlf --i18n-format=xlf --locale=fr",
"build-i18n:es": "ng build --output-path=dist/es --aot -prod --bh /es/ --i18n-file=src/i18n/messages.es.xlf --i18n-format=xlf --locale=es",
"build-i18n:en": "ng build --output-path=dist/en --aot -prod --bh /en/ --i18n-file=src/i18n/messages.en.xlf --i18n-format=xlf --locale=en",
"build-i18n": "npm run build-i18n:en && npm run build-i18n:es && npm run build-i18n:fr"
}

Apache2 configuration

Here is a virtual host configuration which will serve your different bundles from the /var/www directory: you will have to copy in this directory the three directories en/, es/ and fr/ previously generated.

With this configuration, the url http://www.myapp.com is redirected to the subdirectory of the preferred language defined in your browser configuration (or en if your preferred language is not found) and you still have access to the other languages by accessing the other subdirectories.

<VirtualHost *:80>
ServerName www.myapp.com
DocumentRoot /var/www
<Directory "/var/www">
RewriteEngine on
RewriteBase /
    RewriteRule ^../index\.html$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule (..) $1/index.html [L]
    RewriteCond %{HTTP:Accept-Language} ^fr [NC]
RewriteRule ^$ /fr/ [R]
    RewriteCond %{HTTP:Accept-Language} ^es [NC]
RewriteRule ^$ /es/ [R]
    RewriteCond %{HTTP:Accept-Language} !^es [NC]
RewriteCond %{HTTP:Accept-Language} !^fr [NC]
RewriteRule ^$ /en/ [R]
</Directory>
</VirtualHost>

Bonus: add links to the different languages

It would be interesting to have some links in the app so the user can navigate to another languages by clicking these links. The links will point to /en/, /es/ and /fr/.

One trick to know, the current language is available in the LOCALE_ID token.

Here is how you can get the LOCALE_ID value and display the list of languages, differentiating the current language:

// app.component.ts
import { Component, LOCALE_ID, Inject } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
  languages = [
{ code: 'en', label: 'English'},
{ code: 'es', label: 'Español'},
{ code: 'fr', label: 'Français'}
];
  constructor(@Inject(LOCALE_ID) protected localeId: string) {}
}
<!-- app.component.html -->
<h1 i18n>Hello World!</h1>
<template ngFor let-lang [ngForOf]="languages">
<span *ngIf="lang.code !== localeId">
<a href="/{{lang.code}}/">{{lang.label}}</a> </span>
<span *ngIf="lang.code === localeId">{{lang.label}} </span>
</template>

Bonus 2: i18n after ejection

It is possible from angular-cli to eject a webpack configuration for a particular language. Here is the command to eject the webpack configuration for the english language — note that the command is very similar to the previously used ng build command:

ng eject \
--output-path=dist/en \
--aot \
-prod \
--bh /en/ \
--i18n-file=src/i18n/messages.en.xlf \
--i18n-format=xlf \
--locale=en

This command will create for you a webpack.config.js that will be used to build your english version when you run the command npm run build.

You can now copy this webpack.config.js file to three files for your three different languages, for example webpack.en.config.js, webpack.es.config.js and webpack.fr.config.js , then adapt these specific files for each specific language. Here is the result of a diff after the required changes for the french language:

--- webpack.config.js
+++ webpack.fr.config.js
@@ -44,7 +44,7 @@
]
},
"output": {
- "path": path.join(process.cwd(), "dist/en"),
+ "path": path.join(process.cwd(), "dist/fr"),
"filename": "[name].[chunkhash:20].bundle.js",
"chunkFilename": "[id].[chunkhash:20].chunk.js"
},
@@ -220,7 +220,7 @@
}
}),
new BaseHrefWebpackPlugin({
- "baseHref": "/en/"
+ "baseHref": "/fr/"
}),
new CommonsChunkPlugin({
"name": "inline",
@@ -283,9 +283,9 @@
new AotPlugin({
"tsConfigPath": "src/tsconfig.json",
"mainPath": "main.ts",
- "i18nFile": "src/i18n/messages.en.xlf",
+ "i18nFile": "src/i18n/messages.fr.xlf",
"i18nFormat": "xlf",
- "locale": "en",
+ "locale": "fr",
"hostReplacementPaths": {
"environments/environment.ts": "environments/environment.prod.ts"
},

You also need to modify you package.json file for the build command to execute webpack for each configuration:

{
[...]
"scripts": {
[...]
"build": "webpack --config webpack.fr.config.js && webpack --config webpack.en.config.js && webpack --config webpack.es.config.js"
}
[...]
}

You can now run npm run build which will build your three bundles in dist/enn, dist/es and dist/fr.

Bonus 3: Angular Translator application

This weekend for the 2017 AngularAttack, I’ve created an application that can definitely help you translate your Angular applications. It is still in development, feedbacks are welcome: http://angular-translator.elol.fr/.

Good translations!