DEV Community

Cover image for Deploying an i18n Angular app with angular-cli
Philippe MARTIN for Angular

Posted on

Deploying an i18n Angular app with angular-cli

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 or NGINX web server.

The following versions are used:

  • angular-cli: 8.0.4
  • angular: 8.0.4
  • Apache 2.4
  • NGINX 1.17

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 the Angular CLI:

$ ng new angular-cli-i18n-sample
Enter fullscreen mode Exit fullscreen mode

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

<h1 i18n>Hello world!</h1>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
[...]
Enter fullscreen mode Exit fullscreen mode

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

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

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>
[...]
Enter fullscreen mode Exit fullscreen mode

It is now possible to make Angular CLI build the app with the language of your choice, here in spanish:

$ ng build --aot \
           --i18n-file=src/i18n/messages.es.xlf \
           --i18n-locale=es \
           --i18n-format=xlf
Enter fullscreen mode Exit fullscreen mode

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, --base-href 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 \
             --base-href /$lang/ \
             --i18n-file=src/i18n/messages.$lang.xlf \
             --i18n-format=xlf \
             --i18n-locale=$lang; \
  done
Enter fullscreen mode Exit fullscreen mode

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 --base-href /$lang/ --i18n-file=src/i18n/messages.$lang.xlf --i18n-format=xlf --i18n-locale=$lang; done"
  }
  [...]
}
Enter fullscreen mode Exit fullscreen mode

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 --base-href /fr/ --i18n-file=src/i18n/messages.fr.xlf --i18n-format=xlf --i18n-locale=fr

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

> ng build --output-path=dist/en --aot --prod --base-href /en/ --i18n-file=src/i18n/messages.en.xlf --i18n-format=xlf --i18n-locale=en
Enter fullscreen mode Exit fullscreen mode

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 --base-href /fr/ --i18n-file=src/i18n/messages.fr.xlf --i18n-format=xlf --i18n-locale=fr",
    "build-i18n:es": "ng build --output-path=dist/es --aot --prod --base-href /es/ --i18n-file=src/i18n/messages.es.xlf --i18n-format=xlf --i18n-locale=es",
    "build-i18n:en": "ng build --output-path=dist/en --aot --prod --base-href /en/ --i18n-file=src/i18n/messages.en.xlf --i18n-format=xlf --i18n-locale=en",
    "build-i18n": "npm run build-i18n:en && npm run build-i18n:es && npm run build-i18n:fr"
  }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

NGINX configuration

Here is an NGINX configuration that will give you the same behaviour: an access to http://www.myapp.com will redirect to the preferred language defined in the browser (or en if your preferred language is not found) and the other languages are still accessible.

server {
    listen       80;
    server_name  localhost;

    location /en/ {
        alias   /usr/share/nginx/html/en/;
        try_files $uri$args $uri$args/ /en/index.html;
    }
    location /es/ {
        alias   /usr/share/nginx/html/es/;
        try_files $uri$args $uri$args/ /es/index.html;
    }
    location /fr/ {
        alias   /usr/share/nginx/html/fr/;
        try_files $uri$args $uri$args/ /fr/index.html;
    }

    set $first_language $http_accept_language;
    if ($http_accept_language ~* '^(.+?),') {
        set $first_language $1;
    }

    set $language_suffix 'en';
    if ($first_language ~* 'es') {
        set $language_suffix 'es';
    }
    if ($first_language ~* 'fr') {
        set $language_suffix 'fr';
    }

    location / {
        rewrite ^/$ http://localhost:4200/$language_suffix/index.html permanent;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
<ng-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>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

Bonus 2: Angular Translator application

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!

Top comments (14)

Collapse
 
boban100janovski profile image
Boban Stojanovski • Edited

Hi

I started with 3 languages and all seemed ok, for a while.
But now my app has 6 and in a few months it should have 9 languages.

It's becoming unbearable to wait for all those builds to finish, it's a burden to maintain.

Any solution for this in the near future from the Angular team or maybe someone can suggest an alternative solution (ngx-translate?).

Collapse
 
guilhermeurnau profile image
Guilherme Santos

Take a look at npm-run-all

Collapse
 
boban100janovski profile image
Boban Stojanovski

Still slow and i still need to upload 9 zipped version to production in 9 different folders

Collapse
 
mintplayer profile image
MintPlayer • Edited

Hi,

I see that, to switch languages, you're using the <a href> tag. I was wondering if it's possible to use the <a [routerLink]> instead so that the app can switch languages without reloading the page.

From the angular docs I read that we actually have to build the app for each language we want to serve. I don't really like this since it would probably imply that the user cannot switch languages without reloading the page. Do you know a proper way around this?

Collapse
 
mkubdev profile image
Maxime Kubik • Edited

Hi

Nice one, but what if i want to reverse_proxy my API calls ? I mean, on the /fr/ version, my api call is translated to /fr/api/ instead of /api/, how can i handle this with nginx ?

Before i18n, my rule was :

location /api/ { 
 proxy_pass http://myapi.fr;
}
Enter fullscreen mode Exit fullscreen mode

So i get no items from my api since the basehref is added before the request

Collapse
 
santhony7 profile image
Tony Smith

I don't suppose you have an updated solution using the new CLI? i18n-file, --i18n-format, and --i18n-locale are all now deprecated. I have tried many different ways using the new --localize but none seem to really work how this used to.

Collapse
 
xxxlogiatxxx profile image
xxxLOGIATxxx

Hey! Thank you for brilliant post. Using latest Angular and first bonus sounds really good to me but it's showing me some errors so i can't build my app:

ERROR in src/app/app.component.html(24,13): Property 'localeId' is protected and only accessible within class 'AppComponent' and its subclasses.
src/app/app.component.html(26,13): Property 'localeId' is protected and only accessible within class 'AppComponent' and its subclasses.
src/app/app.component.html(26,74): Property 'localeId' is protected and only accessible within class 'AppComponent' and its subclasses.
src/app/app.component.html(12,51): Property 'localeId' is protected and only accessible within class 'AppComponent' and its subclasses.

It's ok if i change protected constructor(@Inject(LOCALE_ID) protected localeId: string) {} to public?

And it's returned me en-US, not en locale.

Collapse
 
mrreyesa profile image
MrReyesA

hey @xxxlogiatxxx
Did you found a Solution???

Collapse
 
ronnyek profile image
Weston

I think the new stuff in @angular/localize is showing promise, but still lacking in a lot of places.

I like the features of transloco, and use some of Netanel's other libraries (author of transloco), and have been very pleased.

I just wish we could h ave some "blend" of whats in i18n / @angular/localize and whats in transloco. There is benefit to having some stuff be pre-compiled (@angular/localize), but seems like it leaves quite a bit to be desired at this point.

Collapse
 
skb0110 profile image
Santosh Biswakarma

Hey Martin,

Thanks for sharing such great information. I was following the same for one of my project, But i am struggling with routing and navigation.
myproject.com/travel, /submittion, /final, so in such scenario how i would handle the base-href. I am using angular 8.
I mean how can implement like myproject.com/travel/en, /submittion/en,
/final/en

Kindly guide me.

Thanks.

Collapse
 
graemeq profile image
Graeme Q

I can't thank you enough for this. I'm using angular 9 with 5 different languages.

I couldn't for the life of me work out the Nginx config. I knew I had it wrong but I couldn't work out how to correct it. Alias's was the answer.

Thank you!

Collapse
 
wilsonyoung profile image
Wilson

Greate tutorial,
how about mix it with angular universal ?

Collapse
 
sanketmaru profile image
Sanket Maru

Thanks for step by step explanation. This really helps understanding serving multiple locales.
Is --base-href supported for angular 6 vesion ?