All Articles

How to use Internationalization (i18n) in Angular

The consumption of modern web and mobile experiences is a worldwide thing. It isn’t just related to the locals around you and the surrounding culture. Therefore, just as you’d ensure that your design is aestheticly-pleasing and accessible, you should also ensure that your text is localized.

This is the process of ensuring that your application supports multiple languages. This is traditionally done in an Angular application via multiple ways.

You can, for example, use third party libraries such as ngx-translate, or, you can use the built-in i18n functionality.

Funny thing that I have encountered a problem recently that there is no much of a tutorial on how to set up i18n on a project that already running. That is exactly why we’ll specifically be looking at using the built-in i18n inside of this article.

project-running

If you had same or similar task to mine and somehow managed to find this article with the results of all my struggles than my mission accomplished so far.

Glossary

First things first. What do we have here?

What is Internationalization?

Internationalization is the process of designing your app so that it can support various languages. It is also known as i18n.

What is Localization?

Localization is the process of translating your app into different languages. Before you can localize your app, you need to internationalize it.

Why is Internationalization so important?

Nowadays, with so many websites to choose from, making applications available and user-friendly to a worldwide audience is super important. It’s a step forward in the name of user experience.

Let’s make it step-by-step

Step 1: Text marking

Let’s start by marking text that we’d like to translate within our application. I’ll be translating the application into fr with Google Translate providing the translations.

There is an Angular attribute to mark translatable content and it is i18n. You have to place it on every element tag whose fixed text you want to be translated.

Add the i18n directive to all of the text that you’d like to translate:

<section>
  <article>
    <h1 i18n>Hello World!</h1>
    <p i18n>This page is greetings you.</p>
  </article>
</section>

and something like this

<h2 i18n="@@templatesTitle">
  Templates
</h2>

As you can see I’ve added the i18n attribute to h1 tag at the first example, what is pretty much clear. But also, I added a custom unique identifier “@@templatesTitle” for the h2 on the second scratch of code. When you specify a custom id, the extractor tool and compiler (you will see how to use it in Step 2) will generate a translation unit with that custom id like the following:

<trans-unit id="templatesTitle" datatype="html">

Custom Ids are useful because when translating the texts (in generated xlf files) you will know where the text belongs (in this case, it’s the title of the Templates page).

There are other types of identifications or descriptions you can use for your i18n attributes.

Step 2: Create a translation source file

Then I’ll make a script inside package.json that uses the Angular CLI to extract this into a messages.xlf file which contains all of our marked items:

{
  "scripts": {
    "int:extract": "ng xi18n"
  }
}

After adding this, run npm run int:extract inside of your terminal. Then, open up messages.xlf and you’ll see something similar to this:

<?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="48a16ab522feaff81571155668deb1a4304291f4" datatype="html">
        <source>Hello World!</source>
        <context-group purpose="location">
          <context context-type="sourcefile">app/app.component.html</context>
          <context context-type="linenumber">3</context>
        </context-group>
      </trans-unit>
      <trans-unit id="84c778d7a95cb5dc26c9cc9feb5b7abb4d295792" datatype="html">
        <source>This page is greetings you.</source>
        <context-group purpose="location">
          <context context-type="sourcefile">app/app.component.html</context>
          <context context-type="linenumber">4</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>

For each item that needs translating (i.e. has the i18n directive), a trans-unit will be created.

Example above for the code which was just specified by i18n attribute and for the ones with custom attributes and ids:

<trans-unit id="tutorial-button" datatype="html">
    <source>Tutorials</source>
    <context-group purpose="location">
        <context context-type="sourcefile">app/app.component.html</context>
        <context context-type="linenumber">18</context>
    </context-group>
</trans-unit>

But the thing is you have to use this structure to provide more information about the translation. This is useful if you’re getting each message translated by a third party, and you do even if you are super-duper-multi-lang-dev that just how things done in business.

mr-worldwide …into your native language (actually did happen to me)

So let’s get back to unmarked part of the code inside of app.component.html, update the i18n items with a description:

<section>
  <article>
    <h1 i18n="Title for the hello page">Hello World!</h1>
    <p i18n="A description for the hello page">This page is greetings you.</p>
  </article>
</section>

You can further add context to this by using the | seperator and id’s like I did before. This gives an item meaning and each item with the same meaning will have the same translation.

<section>
  <article>
    <h1 i18n="Page Header|Title for the hello page@@helloHeader">Hello World!</h1>
    <p i18n="Page Description|A description for the hello page@@helloDescription">This page is greetings you.</p>
  </article>
</section>

And when you build your translations once again:

$ npm run int:extract

You’ll have your items update with the id, meaning and description:

<body>
  <trans-unit id="helloHeader" datatype="html">
    <source>Hello World!</source>
    <context-group purpose="location">
      <context context-type="sourcefile">app/app.component.html</context>
      <context context-type="linenumber">3</context>
    </context-group>
    <note priority="1" from="description">Title for the under hello page</note>
    <note priority="1" from="meaning">Page Header</note>
  </trans-unit>
  <trans-unit id="helloDescription" datatype="html">
    <source>This page is greetings you.</source>
    <context-group purpose="location">
      <context context-type="sourcefile">app/app.component.html</context>
      <context context-type="linenumber">4</context>
    </context-group>
    <note priority="1" from="description">A description for the hello page</note>
    <note priority="1" from="meaning">Page Description</note>
  </trans-unit>
</body>

Note: Every time a change is made to the html, either adding or modifying a text, this file must be generated again.

Step 3: Translations

Now that we have a messages.xlf file that contains all of the items we want to translate, we can drag this into a src/locales folder and make the respective messages.de.xlf and messages.fr.xlf files.

At this stage, we can also update our int:extract script to handle this:

"int:extract": "ng xi18n --output-path src/locale"

French

Starting with messages.fr.xlf, let’s look to translate the messages by using target and source.

Firstly, copy everything from messages.xlf into messages.fr.xlf. We can then add a target attribute for each item, which is equal to the translation in that language.

<body>
  <trans-unit id="helloHeader" datatype="html">
    <source>Hello World!</source>
    <target>Bonjour le monde!</target>
    <context-group purpose="location">
      <context context-type="sourcefile">app/app.component.html</context>
      <context context-type="linenumber">3</context>
    </context-group>
    <note priority="1" from="description">Title for the under hello page</note>
    <note priority="1" from="meaning">Page Header</note>
  </trans-unit>
  <trans-unit id="helloDescription" datatype="html">
    <source>This page is greetings you.</source>
    <target>Cette page vous souhaite la bienvenue.</target>
    <context-group purpose="location">
      <context context-type="sourcefile">app/app.component.html</context>
      <context context-type="linenumber">4</context>
    </context-group>
    <note priority="1" from="description">A description for the hello page</note>
    <note priority="1" from="meaning">Page Description</note>
  </trans-unit>
</body>

You (or third party of yours) have to translate all the trans-unit nodes in the same way. I know it may sound too complicated but it’s something you will do just once, so don’t panic!

For examples about translating plural and select expressions visit this link documentation is quite helpful sometimes.

Step 4: Locale builds

Great! We’ve now got versions of our application that are translated based on locale.

We can use the Angular CLI to generate specific builds for each locale that we want to support.

Head over to angular.json and inside of the build settings add the configurations. I’ve omitted most of the boilerplate that already exists.

{
  "projects": {
    "AngularInt": {
      "architect": {
        "build": {
          "configurations": {
            "fr": {
              "aot": true,
              "outputPath": "dist/hello-page-fr/",
              "baseHref": "/fr/",
              "i18nFile": "src/locale/messages.fr.xlf",
              "i18nFormat": "xlf",
              "i18nLocale": "fr",
              "i18nMissingTranslation": "error"
            }
          }
        }
      }
    }
  }
}

We can also update configurations inside of serve to allow us to serve the fr folder. Once again, I’ve kept this brief:

{
  "serve": {
    "configurations": {
      "production": {
        "browserTarget": "AngularInt:build:production"
      },
      "fr": {
        "browserTarget": "AngularInt:build:fr"
      }
    }
  }
}

Note that you need to set the following configurations:

  • "outputPath": "dist/hello-page-fr/": the output folder for your French project
  • "baseHref": "/fr/": the base url param for your spanish version of your app
  • "i18nFile": "src/locale/messages.fr.xlf": the path to the translation file.
  • "i18nFormat": "xlf": the format of the translation file.
  • "i18nLocale": "fr": the locale id

Step 5: i18n multi language on flight

In a production environment, you most probably would like your angular app to be accessible in different subdirectories, depending on the language; for example the french version would be accessible at https://yourwebsite.com/fr/ and the english one at https://yourwebsite.com/en/. We also would like to be redirected from the base url https://yourwebsite.com to the url of our preferred language.

To achieve this, in the previous section I changed the base href to fr or en depending on the target language. I did this in the angular.json file.

I’ll use a simple Express server for this. Long story short go to your server.ts file and add some lines of code to achieve the multi language capacity we want. Specifically, we will change this method:

app.get('*', (req, res) => {
  res.render('index', { req });
});

I will add some logic before rendering the requested page to check if the requested url has a correct format with the style ’https://yourwebsite.com/any-locale/’ and if it matches any of the locales we support in our angular app (fr or en). If not, we will render the default locale: english.

app.get('*', (req, res) => {

  //this is for i18n
  const supportedLocales = ['fr', 'es'];
  const defaultLocale = 'en';
  const matches = req.url.match(/^\/([a-z]{2}(?:-[A-Z]{2})?)\//);
  //check if the requested url has a correct format '/locale' and matches any of the supportedLocales
  const locale = (matches && supportedLocales.indexOf(matches[1]) !== -1) ? matches[1] : defaultLocale;

  res.render("${locale}/index", {req});
});

One important thing to explain from the code above is the:

res.render("${locale}/index", { req });

Note: that I added ${locale}, and this is because depending on the requested locale, server will serve the code from a different folder.

Scripts to compile and run i18n angular app

{
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "start:fr": "ng serve --configuration=fr",
    "build": "ng build",
    "build:fr": "ng build --configuration=fr",
    "build:client-and-server-bundles-i18n": "ng run i18n-demo:build:production-fr && ng run i18n-demo:build:production-en && ng run i18n-demo:server:production",
	"build:client-and-server-bundles": "ng build --prod && ng run i18n-demo:server:production",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "int:extract": "ng xi18n --output-path src/locale"
  }
}

To build your app for Production with i18n multi language run:

npm run build:i18n-ssr && npm run serve:ssr

It compiles your application and spins up a Node Express to serve your Universal application.

joker Et Voilà!

Would you like to know more? Read what I read


Switching to Angular - Third Edition: Align with Angular version 5 and Google's long-term vision for Angular Pro Angular 6 3rd ed. Edition Angular 6 for Enterprise-Ready Web Applications: Deliver production-ready and cloud-scale Angular web apps