Angular Translation: A Closer Look at Angular 8

Phrase
Software Localization Tutorials
14 min readMay 23, 2019
Angular’s release cycle provides for the launch of a new major version every six months. In this tutorial, we’ll go through the process of translating Angular 8 apps and take a closer look at how Phrase integration can simplify translation file management.

Update: Angular released a new version of its framework so make sure you take a look at our latest Angular 9 tutorial on internationalization!

When it comes to Angular, we’ve already covered some popular internationalization solutions for Angular and seen how to localize Angular apps with the help of I18next framework. Today, I’d like to go a step further and talk about Angular translation in its latest (eighth) version by using the built-in I18n module.

The release of Angular 8 was announced at the beginning of 2019 and should go live quite soon. As you probably know, there are multiple solutions supporting Angular i18n. Nonetheless, I’d now recommend sticking to the built-in internationalization module. I18next is a great framework, but it’s quite “heavy” and can often be perceived as overkill. Ngx-translate is quite a solid tool as well, but it was considered to be a temporary solution from the very beginning. Now that Angular has its very own I18n module, I wouldn’t start a new project with ngx-translate as it seems to have a whole lot of bugs. Some users even report it didn’t play nicely with latest versions of the framework.

Considering all of this, what we’ll focus on in this tutorial will be:

  • Upgrading your app to Angular 8
  • Performing translations with the help of the built-in I18n module
  • Translating attributes
  • Performing pluralization
  • Setting up an AOT compiler and introducing multilingual support
  • Deploying the app to Firebase
  • Integrating with Phrase to simplify translation files management

The working demo can be found at https://ngdemo8.firebaseapp.com/en/.

Getting Ready to Start with Angular Translation

We’ll start off by creating a sample Angular 8 application. Please note that at the time of writing this article, Angular 8 has only a release candidate version (a stable version should go live at the end of May or beginning of June 2019). Therefore, to get started with Angular 8, you first need to take a couple of simple steps.

To begin with, make sure you have Node.js 10 installed.

Next, install the latest stable version Angular by running:

npm install -g @angular/cli

Subsequently, create a new demo application:

ng new NGDemo8

Navigate to the application’s directory and update to Angular 8:

ng update @angular/cli @angular/core codelyzer --next

This operation may take a couple of minutes, and then you are good to go!

Performing Translations

In order to mark translatable content, you should use an i18nattribute. In the simplest case, this attribute doesn’t accept any value at all. It just says: “This node should be translated properly”. However, you can provide meaning of and description for the translation. Open the src/app/app.component.html file and replace its content with the following markup:

<h1 i18n="main header|Friendly welcoming message">Welcome!</h1>

“Main header” stands here for the meaning. “Friendly welcoming message” is the description; note that it’s separated by a pipe (|). This information is very helpful for translators – having context at their disposal enables them to deliver more accurate translations.

Where Translations Dwell…

The next question is how do we actually translate our header?. Translation messages live in separate files that may have one of the following formats:

If you got used to working with JSON or YAML formats, XLIFF may seem a bit complex at first. But, fear not: There’s nothing we can’t handle!

You may create translation files manually, but it’s much simpler to use the built-in xi18n tool:

ng xi18n --i18n-locale ru --out-file locale/messages.ru.xlf

We are creating a messages.ru.xlf file inside the src/locale folder which will contain translations for the Russian locale. xi18n won’t just create this file but rather search for all translatable content and extract it properly. Therefore, open the messages.ru.xlf file and provide the translation for our welcome message (I’ve pinpointed it with a comment):

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="ru" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="1eaf722c330fc9bf1ab4255345fc2f6127e9556a" datatype="html">
<source>Welcome!</source>
<target>Добро пожаловать!</target> <!-- translation -->
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">9</context>
</context-group>
<note priority="1" from="description">Friendly welcoming message</note>
<note priority="1" from="meaning">main header</note>
</trans-unit>
</body>
</file>
</xliff>

Every translation is wrapped with a trans-unit tag. The source tag contains the translation key, whereas the target hosts translation value. You can also see where this translation resides and what are the description and its meaning.

Translation ID

Now take a look at the id attribute of the trans-unit tag. You should not alter this ID directly because it’s used to provide translation for the proper node. The ID has a unique value which is generated using the text inside the node and its meaning (not description). Suppose we have two different nodes with different a description but similar meaning and text:

<h1 i18n="main header|Friendly welcoming message">Welcome!</h1>

<h2 i18n="main header|Another message">Welcome!</h2>

They’ll have the same ID and, effectively, the same translation, even though the description differs.

However, in many cases, this is not very convenient, especially if you want multiple nodes to have the same translation. To overcome this problem, you may assign custom IDs inside the i18n attribute. Custom IDs should be prefixed with @@ symbols:

<h1 i18n="main header|Friendly welcoming message@@welcome">Welcome!</h1>

<h2 i18n="main header|Another message@@welcome">Welcome!</h2>

Now re-run the xi18n tool and take a look at the messages.ru.xlf file:

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="ru" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="welcome" datatype="html">
<source>Welcome!</source>
<target>Добро пожаловать!</target> <!-- add translation -->
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
<note priority="1" from="description">Friendly welcoming message</note>
<note priority="1" from="meaning">main header</note>
</trans-unit>
</body>
</file>
</xliff>

Now the ID is much more user-friendly. Also note that this ID was found on lines 9 and 11, but the translation will be the same in both cases.

One thing to remember when assigning custom IDs is that they should be unique. If two nodes have the same IDs, they will always have the same translation!

Translation Use-Cases

When Attributes Are Translated

Interestingly, Angular I18n can translate any attribute of the given tag. Take a look at the following example:

<abbr i18n i18n-title title="European Organization for Nuclear Research">CERN</abbr>

This abbreviation explains what CERN is. However, I would like to translate both the abbreviation and the title. To do that, I’ve added an i18n-title attribute saying that the title should have its translation too. Run the xi18n tool once again and open the messages.ru.xlf file:

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="ru" datatype="plaintext" original="ng2.template">
<body>
<!-- other translations -->
<trans-unit id="bd360d5d5f7b6fae01a8bff987de71b45596279d" datatype="html">
<source>CERN</source>
<target>ЦЕРН</target> <!-- translate abbreviation -->
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="6b2377079997e96c6a53564c5008c4cdaae0856d" datatype="html">
<source>European Organization for Nuclear Research</source>
<target>Европейская организация по ядерным исследованиям</target> <!-- translate title -->
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

We’ve got two separate trans-unit tags now, and therefore, we can translate both the abbreviation and the title! Note that translatable attributes may also have meaning, description, and ID. To add any of these, simply assign a proper value to the i18n-title attribute as shown in the previous section.

To Pluralize Or Not To Pluralize

One typical task every developer faces sooner or later is the need to pluralize a given string. Let’s suppose I’d like to display how many unread messages a user has got. First of all, let’s simply hard-code this number in our app component:

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

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
newMessages = 5; // <---
}

Now, add a paragraph with the following content:

<p i18n>{newMessages, plural, =0 {no messages} =1 {one message} few {{{newMessages}} messages} other {{{newMessages}} messages}}</p>

This syntax is written according to the ICU message format. Yes, it does look quite strange. In reality, things are much simpler. newMessages is the variable we have defined in the component. plural means the translation should be properly pluralized based on the newMessagesvalue. Then, we provide translations for different cases, according to the CLDR plural rules that Angular I18n relies on. There are four cases (in Russian, pluralization rules are more complex than in English).

Note » Be aware of the potential problem that I’ve encountered! If you provide p on one line and its pluralized contents on another line, xi18n creates two separate translation units with different values but the same key. This seems like a bug which was also present in Angular 6.

Now, as always, run the xi18n tool and open the messages.ru.xlf file:

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="ru" datatype="plaintext" original="ng2.template">
<body>
<!-- other translations... -->
<trans-unit id="5f57663c051c1451f0b6302315ac6a24bfc0df7c" datatype="html">
<source>{VAR_PLURAL, plural, =0 {no messages} =1 {one message} few {<x id="INTERPOLATION" equiv-text="{{newMessages}}"/> messages} other {<x id="INTERPOLATION" equiv-text="{{newMessages}}"/> messages} }</source>
<!-- Our translation -->
<target>{VAR_PLURAL, plural, =0 {нет сообщений} =1 {одно сообщение} few {<x id="INTERPOLATION" equiv-text="{{newMessages}}"/> сообщения} other {<x id="INTERPOLATION" equiv-text="{{newMessages}}"/> сообщений} }</target>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

We provide translations for cases when there is one message or there are a few, many, or no messages. In case there are only a few or many messages, we also need to interpolate the actual number, therefore we use an x tag with a special INTERPOLATION ID and equiv-text with the newMessages value.

If you are unsure of how many cases should be provided for some language, use this table as a reference.

We Need No Element!

Sometimes, you might want to translate some text without wrapping it in any tag. To do that, use a special ng-container tag with a i18nattribute:

<ng-container i18n>No element will be created, only a text!</ng-container>

The compiler will perform translation and then remove the ng-container. As a result, you will see a plain text on your page without any wrapping element. Sweet!

Creating a Multilingual App

It can often be the case that you want to create a multilingual app with the ability to switch between the languages. In this section, we will see how to achieve that and configure the app properly.

Language Switcher

First things first, we need to add a language switcher to the page. I’d like to have support for two languages, English and Russian, therefore tweak the component in the following way:

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

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
newMessages = 5;

languageList = [ // <--- add this
{ code: 'en', label: 'English' },
{ code: 'ru', label: 'Русский' }
];
}

This is the list of supported languages that we will render on the page. Next, tweak the template:

<ul>
<li *ngFor="let language of languageList">
<a href="/{{language.code}}/">
{{language.label}}
</a>
</li>
</ul>

<!-- other tags -->

Effectively, this will display an unordered list with links leading to /enand /ru paths.

Before proceeding to the next section, generate a translation file for the English locale:

ng xi18n --i18n-locale en --out-file locale/messages.en.xlf

Here is the full contents for the messages.en.xlf file:

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="welcome" datatype="html">
<source>Welcome!</source>
<target>Welcome!</target>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
<note priority="1" from="description">Friendly welcoming message</note>
<note priority="1" from="meaning">main header</note>
</trans-unit>
<trans-unit id="bd360d5d5f7b6fae01a8bff987de71b45596279d" datatype="html">
<source>CERN</source>
<target>CERN</target>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="6b2377079997e96c6a53564c5008c4cdaae0856d" datatype="html">
<source>European Organization for Nuclear Research</source>
<target>European Organization for Nuclear Research</target>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="5f57663c051c1451f0b6302315ac6a24bfc0df7c" datatype="html">
<source>{VAR_PLURAL, plural, =0 {no messages} =1 {one message} few {<x id="INTERPOLATION" equiv-text="{{newMessages}}"/>
messages} other {<x id="INTERPOLATION" equiv-text="{{newMessages}}"/>
messages} }</source>
<target>
{VAR_PLURAL, plural, =0 {no messages} =1 {one message} other {<x id="INTERPOLATION" equiv-text="{{newMessages}}"/>
messages} }
</target>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

Lastly, make sure you have translated everything properly inside the messages.ru.xlf file:

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="ru" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="welcome" datatype="html">
<source>Welcome!</source>
<target>Добро пожаловать!</target>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
<note priority="1" from="description">Friendly welcoming message</note>
<note priority="1" from="meaning">main header</note>
</trans-unit>
<trans-unit id="bd360d5d5f7b6fae01a8bff987de71b45596279d" datatype="html">
<source>CERN</source>
<target>ЦЕРН</target>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="6b2377079997e96c6a53564c5008c4cdaae0856d" datatype="html">
<source>European Organization for Nuclear Research</source>
<target>Европейская организация по ядерным исследованиям</target>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="5f57663c051c1451f0b6302315ac6a24bfc0df7c" datatype="html">
<source>{VAR_PLURAL, plural, =0 {no messages} =1 {one message} few {<x id="INTERPOLATION" equiv-text="{{newMessages}}"/>
messages} other {<x id="INTERPOLATION" equiv-text="{{newMessages}}"/>
messages} }</source>
<target>{VAR_PLURAL, plural, =0 {нет сообщений} =1 {одно сообщение} few {<x id="INTERPOLATION" equiv-text="{{newMessages}}"/>
сообщения} other {<x id="INTERPOLATION" equiv-text="{{newMessages}}"/>
сообщений} }</target>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">15</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

A Tale of Two Compilers

Next, we need to decide which compiler we’d like to use for production. Angular has two types of compilers available:

  • AOT (Ahead of Time) compiler — converts Angular HTML and TypeScript (or JavaScript) code into the optimized code during the build phase before the browser actually downloads the code. This is the recommended type of compilation for production since it provides great performance and loading time
  • JIT (Just in Time) compiler — converts Angular code into a code understandable by the browser during runtime. This is the default type of compilation and it’s the recommended option for the development environment. Nevertheless, in terms of production, JIT is strongly discouraged in favor of AOT.

While Angular I18n does support the JIT compiler, we’ll stick to AOT which is recommended for the production environment. The idea is to generate two separate packages of the app translated into English and Russian, respectively. When the user changes the language of the app, they’ll effectively switch between the packages of our application.

Setting Up the AOT Compiler

To set up the AOT compiler, open the angular.json file and find the configurations section under the build key. Add ru and en keys inside configurations (I skipped other options for brevity):

{
"projects": {
"NGDemo8": {
"architect": {
"build": {
"configurations": {
"ru": {
"aot": true,
"outputPath": "dist/ru/",
"i18nFile": "src/locale/messages.ru.xlf",
"i18nFormat": "xlf",
"i18nLocale": "ru",
"i18nMissingTranslation": "error",
"baseHref": "/ru/"
},
"en": {
"aot": true,
"outputPath": "dist/en/",
"i18nFile": "src/locale/messages.en.xlf",
"i18nFormat": "xlf",
"i18nLocale": "en",
"i18nMissingTranslation": "error",
"baseHref": "/en/"
}
}
}
}
}
}
}
  • We enable the AOT compiler for both configurations in that we set aot to true
  • The packages should reside inside the dist/ru and dist/enfolders, respectively (the outputPath option)
  • i18nFile instructs where the translation file resides
  • i18nLocale sets the locale for the package
  • i18nMissingTranslation instructs what to do if some key does not have a translation; the default action is to signal a warning, while other supported values are error and ignore
  • baseHref sets the base URL portion for the given package

Feel free to further tweak these configurations to adapt them to your own needs.

Now, we also need to tweak the serve section inside the angular.jsonfile (I’ve omitted other options for brevity):

{
"projects": {
"NGDemo8": {
"architect": {
"serve": {
"configurations": {
"ru": {
"browserTarget": "NGDemo8:build:ru"
},
"en": {
"browserTarget": "NGDemo8:build:en"
}
}
}
}
}
}
}

Note that the NGDemo8:build:ru effectively means that the build configuration named ru should be executed. Therefore, if you have named your build configuration differently in the previous step, you should provide the proper name here as well. Also, don’t forget to replace NgDemo8 with the name of your own app.

Having this configuration in place, you may run the following commands:

  • ng serve --configuration=ru and ng serve --configuration=en to serve the application locally with the given language. Note that the URLs are http://localhost:4200/ru and http://localhost:4200/en as we have set baseHref earlier
  • ng build --configuration=ru and ng build --configuration=en to build Russian and English packages of the app. Run these commands now and make sure they are working properly

It is also possible to provide options directly to the serve and buildcommands, for example:

ng build --prod --i18n-file src/locale/messages.ru.xlf --i18n-format xlf --i18n-locale ru

Our application is now ready for production, and we can deploy it!

Deploying to Firebase

In this section, I will show you how to deploy your multilingual app to Firebase. The deployment process is as straightforward as it can be:

  1. Create a Firebase account if you don’t have one
  2. Navigate to the Firebase console and create a new project (I’ve named mine ngdemo8)
  3. Install Firebase tools globally by running npm install -g firebase-tools
  4. Login via your account using the firebase login command
  5. Initialize Firebase in the root directory of your project by issuing firebase init
  6. Follow the wizard to configure the app. Note that the requests should not be re-routed to index.html. It’s because of this that the answer is “no” when the wizard asks you this question. Make also sure you provide the proper path to the application packages (in our case, they reside in the dist folder)
  7. Last but not least, run firebase deploy to publish your application!

Here is my firebase.json config file that you can use as a reference:

{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}

When you need to update your application, run the build task again and then perform firebase deploy.

You may find the working demo by visiting these links:

Great job!

Use Phrase To Manage Translation Files

As you can see, XLIFF files are quite complex and it is pretty tedious to edit them by hand. Things get even worse when translators need to work with these files, as translators aren’t tech-savvy. Luckily, Phrase can greatly simplify things for you. It provides a convenient online editor that allows multiple translators to collaborate with ease and get translations done without worrying about the underlying file format. Phrase also has other great features like webhook integrations, assignable jobs, activity tracking, and many others.

Configuring the Phrase CLI Tool

Let me guide you through the process of integrating Phrase into your translation workflow.

  1. First of all, grab your free trial if you don’t have an account yet
  2. Create a new project and choose the XLIFF translation file format
  3. Download the command line interface tool for your OS; make sure that the phraseapp executable is available in your PATH
  4. Open the Access Tokens page and generate a new API token with a read-and-write scope (we’ll use it to communicate with Phrase)
  5. Run phraseapp init in the root of your project; this command will boot a setup wizard
  6. Paste the token you’ve generated in step 4
  7. Choose a project you’ve already created
  8. If you’ve selected the XLIFF file format in step 2, simply choose the default format; otherwise, explicitly set XLIFF
  9. Next, the wizard will ask for the upload and download paths; answer ./src/locale/messages.<locale_name>.xlf to both questions (note that the <locale_name> should be typed as it is)
  10. Lastly, answer y to upload your files to Phrase

If you’ve done everything right, you should see two fully translated locales in your Phrase project.

Here is the sample .phraseapp.yml config file:

phraseapp:
access_token: TOKEN
project_id: PROJECT_ID
push:
sources:
- file: ./src/locale/messages.<locale_name>.xlf
params:
file_format: xlf
pull:
targets:
- file: ./src/locale/messages.<locale_name>.xlf
params:
file_format: xlf

Now you may run phraseapp pull to download your translation files and phraseapp push to upload all changes from your app to Phrase.

Conclusion

In this article, we discussed how to translate Angular applications with the help of the built-in I18n module. We took a closer look at its usage and how to create a multilingual app with an AOT compiler. On top of that, we deployed our application to Firebase and integrated it with Phrase to simplify translation file management (if you’re still looking for a solid translation management solution, give Phrase a try). Quite good for one single article, isn’t it?

Originally published on The Phrase Blog.

--

--