Software localization

The Ultimate Guide to Angular Localization

Learn end-to-end Angular localization with the built-in i18n library and explore third-party internationalization alternatives along the way.
Software localization blog category featured image | Phrase

Among the big 3 web UI libraries (React, Vue, Angular), Google's Angular stands out as a mature, batteries-included framework. With its CLI, TypeScript support, modules, dependency injectors, reactive programming, and string conventions, Angular is built for projects that can scale while maintaining structure.

Of course, you’re here for Angular internationalization (i18n), and we’ve got you covered. It's no surprise that Angular has robust built-in i18n support. In this step-by-step tutorial on Angular localization and internationalization, we’ll walk you through how to install, configure, and localize your Angular applications using the first-party @angular/localize package.

This is a hands-on guide, so we’ll hit the ground running with a quick demo app and localize it. Let’s get started 🙂

Library versions used

In this article, we’re using the following NPM packages (versions in parentheses):

  • Angular (13.3)—our UI framework
  • @angular/localize (13.3)—Angular’s first-party i18n library
  • Tailwind CSS (3.0)—used for styling and optional here (but just FYI)

The demo app

Meet azcadea, a fictional e-commerce store that specializes in retro arcade cabinets, desperate for localization.

Our little demo app before localization

🔗 Resource » Get our starter demo app code from GitHub.

Attributions

Shoutouts to the following people for making the assets we’ve used in this demo available for free.

Demo components

Our demo is largely presentational to allow us to focus on the i18n. Its hierarchy looks like the following.

.

└── src/

    └── app/

        │

        │   # Defines routes to / and /about

        ├── app-routing.module.ts

        │

        │   # Root component, with header, footer,

        │   # and <router-outlet>

        ├── app.component.html

        ├── app.component.ts

        │

        │   # Top and bottom bars

        ├── layout/

        │   ├── navbar/

        │   │   └── (html/ts)

        │   └── footer/

        │       └── (html/ts)

        │

        └── pages/

            │

            │   # Renders list of arcade cabinets

            ├── home/

            │   └── (html/ts)

            │

            │   # Simple about page with text

            └── about/

                └── (html/ts)

We’re using Angular’s router to provide two “pages”, Home and About.

Switching between our home and about pages

Let’s take a look at the Home component in a bit more detail.

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

import { Cabinet } from '../../cabinet.model';

@Component({

  selector: 'app-home',

  templateUrl: './home.component.html',

})

export class HomeComponent {

  // Mock data that we would probably fetch from an API

  cabinets: Cabinet[] = [

    {

      imageUrl: '/assets/cabinets/mappy.jpg',

      name: 'Mappy Mini-cabinet',

      description: 'The original cat and mouse game.',

      addedAt: new Date(2021, 12, 22),

      storeCount: 2,

      price: 27.99,

    },

    // …

  ];

  altFor(cabinet: Cabinet): string {

    // Hard-coded in English

    return `Image of ${cabinet.name}`;

  }

}

🗒 Note » In our actual code on GitHub we’re using a little injected service to provide the cabinet data. We’re omitting that code here for brevity.

<!-- Tailwind CSS classes omitted for brevity-->

<div>

  <div *ngFor="let cabinet of cabinets">

    <div>

      <img

        [alt]="altFor(cabinet)"

        [src]="cabinet.imageUrl"

      />

    </div>

    <div>

      <div>

        <h3>{{ cabinet.name }}</h3>

        <!-- Currency symbol is hard-coded -->

        <p>${{ cabinet.price }}</p>

      </div>

      <p>{{ cabinet.description }}</p>

      <div>

        <span>

          <!-- Hard-coded in English -->

          Available at {{ cabinet.storeCount }} outlets

        </span>

        <button>

          <!-- Hard-coded in English -->

          Add to cart

        </button>

      </div>

    </div>

  </div>

</div>

This should all be bread-and-butter Angular for you. Note, however, that all of our UI strings are hard-coded in English. We want our app to be available in multiple languages, so we need to internationalize and localize it.

🔗 Resource » Get all of our starter demo app code from GitHub.

How do I localize my Angular app with the first-party localize package?

The basic recipe is:

  1. Install and configure the @angular/localize package
  2. Mark strings as localizable in your components
  3. Extract localized strings for translation
  4. Translate strings to the locales you want to support
  5. Ensure your dates and numbers are localized
  6. Build your app to merge in your supported locales, and deploy

We’ll go over all these steps in detail in the following sections.

How do I install the Angular localize package?

All we have to do is use the Angular CLI to add the @angular/localize library. From the project root, let’s run the following from the command line.

$ ng add @angular/localize

This will install the @angular/localize NPM package for us, adding it to our project.json.

🗒 Note » The command will also import the $localize template tag in polyfill.ts. We’ll use $localize directly a bit later. For now, just know that Angular needs it to handle strings marked for translation in our templates.

How do I configure the supported locales in my app?

A special i18n section in our project’s angular.json file is reserved for configuring the @angular/localize library. We can use this section to define our app’s supported locales.

{

  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",

  "version": 1,

  "newProjectRoot": "projects",

  "projects": {

    "azcadea": {

        // …

      },

      "root": "",

      "sourceRoot": "src",

      "prefix": "app",

      "i18n": {

        // The locale we use in development, defaults to en-US

        "sourceLocale": "en-CA",

        // All other locales our app supports

        "locales": {

          "ar": {

            "translation": "src/locale/messages.ar.xlf"

          }

        }

      },

      "architect": {

        // …

      }

    }

  },

  "defaultProject": "azcadea"

}

By default, Angular will assume that we use the United States English locale (en-US) when developing. This is the sourceLocale, where we translate messages from. Each locale we want to translate to goes under the locales key.

I changed the sourceLocale to Canadian English (en-CA) above and added Arabic (ar) as a supported locale. Feel free to use any locales you want here. We need to include the path to a translation file for each locale other than the source (more on translation files a bit later).

🗒 Note » To identify a locale in Angular, use a BCP 47 language tag (like en for English) for the language, followed by an optional country code (like -US for the United States). This CLDR list of locales might help.

What is the end-to-end translation workflow?

Now that we’ve installed and configured the @angular/localize package, let’s run through an example of how we would localize a string of text in our app.

Let’s start with our app name in the navbar.

<nav>

  <div>

    <img alt="Logo" src="assets/logo.svg" />

    <h1>azcadea</h1>

  </div>

  <!-- … -->

</nav>

We'll add a special i18n attribute to the <h1> element, which marks its text for translation.

<nav>

  <div>

    <img alt="Logo" src="assets/logo.svg" />

    <h1 i18n>azcadea</h1>

  </div>

  <!-- … -->

</nav>

Next, let’s run the extract-i18n CLI command from our project root to pull this marked string into a translation file.

$ ng extract-i18n --output-path src/locale

🗒 Note » For better organization, we add the optional --output-path argument to the command, ensuring that all of our translation files go under the src/locale directory.

A new file should have appeared in our project:

<?xml version="1.0" encoding="UTF-8" ?>

<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">

  <file source-language="en-CA" datatype="plaintext" original="ng2.template">

    <body>

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

        <source>azcadea</source>

        <context-group purpose="location">

          <context context-type="sourcefile">src/app/layout/navbar/navbar.component.html</context>

          <context context-type="linenumber">11</context>

        </context-group>

      </trans-unit>

    </body>

  </file>

</xliff>

The ng extract-i18n command combed through our app, pulled out the string we marked with i18n, and put it in a <trans-unit> (translation unit) in the new messages.xlf file.

🗒 Note » XLIFF stands for XML Localization Interchange File Format and is a common standard for translation files. Read more about the XLIFF format on Wikipedia.

🗒 Note » You can use formats other than XLIFF if you want.

To translate our string into Arabic, let’s copy the generated messages.xlf file to a new file in the same directory and name it messages.ar.xlf. We can name the copy anything we want as long as it matches the file path we specified in angular.json above. Now to translate:

<?xml version="1.0" encoding="UTF-8" ?>

<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">

  <file source-language="en-CA" datatype="plaintext" original="ng2.template">

    <body>

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

        <source>azcadea</source>

        <!-- Add translation within <target> tags -->

        <target>أزكيديا</target>

        <!-- //… -->

      </trans-unit>

    </body>

  </file>

</xliff>

Translation in place, the final step is to merge our new translation into a build. Providing a --localize argument to the ng build CLI command should do just the trick.

$ ng build --localize

Each of our configured locales will generate a unique copy of our app with that locale baked in. Take a look under the dist directory and you should see something like the following.

.

└── dist/

    └── azcadea/

        ├── ar/

        │   ├── assets/

        │   ├── index.html

        │   └── …

        └── en-CA/

            ├── assets/

            ├── index.html

            └── …

We can use a simple HTTP server to test these builds. I’ll use the popular http-server here.

# From the project root

$ npx http-server dist/azcadea

Now if we open http://localhost:8080/en-CA in our browser, we’ll see our app exactly as it was in development. And if we open http://localhost:8080/ar we’ll see that our brand name has been translated into Arabic.

Our app's name in Arabic

Heads up » If you’re working along with us from the starter demo, you'll notice that images aren’t showing up in the build. Localized builds will have each have their own assets subdirectory, e.g. /en-CA/assets, /ar/assets. So any URLs in our app that having a leading forward slash, like /assets/foo.jpg, will break in built versions. A quick fix is to remove the leading forward slash in such URLs, so /assets/foo.jpg becomes assets/foo.jpg.

How do I configure my production server for a multilingual app?

There are two main considerations when configuring a production server for our localized Angular app.

  1. Serve the correct locale to the visitor. We could redirect the root route / to our default locale, /en-CA in our case. Alternatively, we could try to detect the browser’s preferred language and redirect to the closest locale (more on that a bit later).
  2. Serve the index.html whenever any localized URI is requested. So if we get a request for /ar/about, we need to send back the /ar/about/index.html file. Our index.html file contains our single-page Angular app, which will handle the /about route itself.

🗒 Note » Angular builds an entire copy of our app for each locale we support. So switching locales essentially means that when a user wants the Arabic version of our app, we serve them the entire Arabic copy. In our case this means that we redirect them to /ar, which is the subdirectory (and URI) where our Arabic version lives.

Of course, the implementation details will depend both on your production server and the needs of your app. Here’s a quick and dirty implementation in Express that we can use for testing our production builds in development.

/*******************************************************

 * ⚠️ This server is for testing production builds in a

 * development environment. It has not been checked for

 * security. Please do not use in production!

 *****************************************************/

const path = require('path');

const express = require('express');

const port = 8080;

const rootDir = path.join(__dirname, 'dist/azcadea');

const locales = ['en-CA', 'ar'];

const defaultLocale = 'en-CA';

const server = express();

// Serve static files (HTML, CSS, etc.)

server.use(express.static(rootDir));

// Always serve the index.html file in a locale

// build's directory e.g. /en-CA/foo will return

// /en-CA/index.html. This allows Angular to handle

// the /foo route itself.

locales.forEach((locale) => {

  server.get(`/${locale}/*`, (req, res) => {

    res.sendFile(

      path.resolve(rootDir, locale, 'index.html')

    );

  });

});

// Redirect / to /en-CA

server.get('/', (req, res) =>

  res.redirect(`/${defaultLocale}`)

);

server.listen(port, () =>

  console.log(`App running at port ${port}…`)

);

Now can build our app and run our production test server.

$ ng build --localize --configuration production

$ node serve-prod-test.js

If we visit http://localhost:8080 in our browser, we should be redirected to http://localhost:8080/en-CA and see the English version of our app. And if we enter localhost/ar/about directly in our browser’s address bar, we should see the Arabic version of our about page. All routes accounted for 🙂

🗒 Note » Depending on your configuration, you might want to adjust each locale’s <base href> to control relative links. Angular has a handy baseHref option when configuring a locale that takes care of this. Read about it in the official docs.

🔗 Resource » The Angular documentation has listings for Nginx and Apache server configurations for production Angular apps.

How do I speed up my localization development workflow?

It can be a pain to build our app every time we want to test a change to one of our localizations. One way to alleviate this is to add a development server configuration for each of our locales. This allows us to run our dev server, with hot reloading as usual, while serving a locale-specific version of our app. Let’s add an Arabic version by modifying our angular.json file.

{

  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",

  "version": 1,

  "newProjectRoot": "projects",

  "projects": {

    "azcadea": {

      // …

      "architect": {

        "build": {

           // …

          },

          "configurations": {

            "production": {

              // …

            },

            "development": {

              "buildOptimizer": false,

              "optimization": false,

              "vendorChunk": true,

              "extractLicenses": false,

              "sourceMap": true,

              "namedChunks": true,

              // Localizes default dev build to Canadian English

              "localize": ["en-CA"]

            },

            // New build configuration with Arabic localization

            "ar": {

              // Options optimize for dev environment

              "buildOptimizer": false,

              "optimization": false,

              "vendorChunk": true,

              "extractLicenses": false,

              "sourceMap": true,

              "namedChunks": true,

              // This is the real magic

              "localize": ["ar"]

            }

          },

          "defaultConfiguration": "production"

        },

        "serve": {

          "builder": "@angular-devkit/build-angular:dev-server",

          "configurations": {

            "production": {

              "browserTarget": "azcadea:build:production"

            },

            "development": {

              "browserTarget": "azcadea:build:development"

            },

            // New dev server configuration that targets

            // Arabic build we added above

            "ar": {

              "browserTarget": "azcadea:build:ar"

            }

          },

          "defaultConfiguration": "development"

        },

        "extract-i18n": {

          // …

        },

        "test": {

          // …

        }

      }

    }

  },

  "defaultProject": "azcadea"

}

Now we can run the following command line at the project root to work with an Arabic development version of our app.

$ ng serve --configuration ar

🗒 Note » Running ng serve without the --configuration argument will start the dev server with the English version of our app.

We can shorten this a bit by adding it as an NPM script to our package.json.

{

  "name": "azcadea",

  "version": "0.0.0",

  "scripts": {

    "ng": "ng",

    "start": "ng serve",

    // Our new script

    "start:ar": "ng serve --configuration ar",

    "build": "ng build",

    "watch": "ng build --watch --configuration development",

    "test": "ng test",

  },

  "private": true,

  // …

}

Now we can run npm run start:ar from the command line to run our app in development with Arabic localization.

How can I detect a visitor’s locale?

People can set their preferred locales in their browsers, and these locales are sent as an Accept-Language HTTP header to servers along with normal GET requests. We can use the Accept-Language header to resolve the initial locale on the server for our Angular apps. Let’s update our production test Express server to add locale detection and serve a version of our app matching the closest supported locale.

/*******************************************************

 * ⚠️ This server is for testing production builds in a

 * development environment. It has not been checked for

 * security. Please do not use in production!

 *****************************************************/

const path = require('path');

const express = require('express');

const matchSupportedLocales = require('./match-supported-locales');

// …

const locales = ['en-CA', 'ar'];

const defaultLocale = 'en-CA';

const server = express();

// …

server.get('/', (req, res) => {

  const closestSupportedLocale = matchSupportedLocales(

    req.acceptsLanguages(),

    locales,

    defaultLocale

  );

  return res.redirect(`/${closestSupportedLocale}`);

});

// …

When a visitor hits our root route (/), we try to match one of our supported locales to one of their accepted locales, retrieved from Express’ helpful req.acceptsLanguages(). The function returns the locales in the incoming Accept-Language header as an array. Our new matchSupportedLocales() function uses this array, along with our supported locales and our default locale, to make the best match.

function matchSupportedLocale(

  acceptsLocales,

  supportedLocales,

  defaultLocale

) {

  return (

    firstExactMatch(acceptsLocales, supportedLocales) ||

    firstLanguageMatch(acceptsLocales, supportedLocales) ||

    defaultLocale

  );

}

function firstExactMatch(acceptsLocales, supportedLocales) {

  return acceptsLocales.find((al) =>

    supportedLocales.includes(al)

  );

}

function firstLanguageMatch(

  acceptsLocales,

  supportedLocales

) {

  for (acceptedLang of languagesFor(acceptsLocales)) {

    const match = supportedLocales.find(

      (sl) => languageFor(sl) === acceptedLang

    );

    if (match) {

      return match;

    }

  }

}

function languagesFor(locales) {

  return locales.map((loc) => languageFor(loc));

}

function languageFor(locale) {

  return locale.split('-')[0];

}

module.exports = matchSupportedLocale;

Here’s the basic algorithm:

  1. Find the first exact match between the visitor’s accepted locales and our app’s supported locales. If a visitor to our app has en-CA in her accepted locales, we will serve the en-CA version of our site.
  2. If no exact match is found, find the first language match between the visitor’s accepted locales and our app’s supported locales. If a visitor to our app has ar-SA (Arabic, Saudi Arabia) in his accepted locales, we will serve the ar version of our site.
  3. If no language match is found, we will serve the default locale version of our site (en-CA in our case).

And with that, we can serve the best possible experience to match our visitors’ preferred locales.

How do I build a language switcher for my app?

Even with browser locale detection we often need to provide our visitors a way to manually select their locale of choice. Luckily, we can cook up a little locale switcher without too much effort.

import {

  Inject,

  Component,

  LOCALE_ID,

} from '@angular/core';

@Component({

  selector: 'app-locale-switcher',

  templateUrl: './locale-switcher.component.html',

})

export class LocaleSwitcherComponent {

  locales = [

    { code: 'en-CA', name: 'English' },

    { code: 'ar', name: 'عربي (Arabic)' },

  ];

  constructor(

    @Inject(LOCALE_ID) public activeLocale: string

  ) {}

  onChange() {

    // When the visitor selects Arabic, we redirect

    // to `/ar`

    window.location.href = `/${this.activeLocale}`;

  }

}

The special injection token, LOCALE_ID, is used to retrieve the active locale from Angular. When the English version of our site is loaded LOCALE_ID will be en-CA, for example. We store this value in an activeLocale field, which we bind to the switcher’s value in our template.

<select

  (change)="onChange()"

  [(ngModel)]="activeLocale"

>

  <option

    *ngFor="let locale of locales"

    [value]="locale.code"

  >

    {{ locale.name }}

  </option>

</select>

So with very little code we've given our visitors a nice UI to switch locales.

Our language switcher in action

Heads up » When we run ng serve or npm start our app will run in development mode with our source locale, and our locale switcher won’t work. If we build our app and run the production test server we wrote above, however, the switcher should switch with no hitch.

How do I translate strings in my Angular app?

Now that we have our i18n infrastructure set up, let’s go deeper into Angular string translation.

How do I translate strings in my component templates?

We’ve gone through this one, but it bears repeating: we just use the handy i18n attribute to mark a string for translation.

<h1 i18n>azcadea</h1>

When we run ng extract-i18n, our marked string will be pulled into messages.xlf.

<?xml version="1.0" encoding="UTF-8" ?>

<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">

  <file source-language="en-CA" datatype="plaintext" original="ng2.template">

    <body>

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

        <source>azcadea</source>

        <context-group purpose="location">

          <context context-type="sourcefile">src/app/navbar/navbar.component.html</context>

          <context context-type="linenumber">14</context>

        </context-group>

      </trans-unit>

      <!-- ... -->

    </body>

  </file>

</xliff>

We can copy messages.xlf to messages.ar.xlf to translate our string to Arabic (and again for each of our app’s supported locales).

<?xml version="1.0" encoding="UTF-8" ?>

<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">

  <file source-language="en-CA" datatype="plaintext" original="ng2.template">

    <body>

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

        <source>azcadea</source>

        <target>أزكيديا</target>

      </trans-unit>

      <!-- ... -->

    </body>

  </file>

</xliff>

Now we can build our app for production with ng build --localize to bake our translation into the Arabic version of our app.

🗒 Note » By default, Angular will mark translated strings using an auto-generated ID in the translation files <trans-unit>s. You can set custom IDs for your translation strings if you want, however.

🗒 Note » You can use an <ng-container> element if you want to mark an inline string for translation without wrapping it in an otherwise superfluous HTML element. <ng-container> renders its contents without adding a wrapper.

How do I translate strings in HTML attributes?

Let’s mark our logo’s <img alt="…"> attribute for translation: We simply use i18n-{attribute}.

<nav>

  <div>

    <div>

      <img

        i18n-alt

        alt="azcadea logo"

        src="assets/logo.svg"

      />

      <h1 i18n>azcadea</h1>

    </div>

    <!-- … -->

  </div>

  <!-- … -->

</nav>

Just like i18n for inner text, i18n-{attribute} will be caught by ng extract-i18n and cause the string to be added as a <trans-unit> to our messages.xlf.

How do I translate strings in my component TypeScript code?

When we mark our HTML tags with i18n, the Angular compiler generates code that calls the $localize template tag underneath the hood. Like any other JavaScript template tag, the function can be called like $localize`Text in source locale`. Let’s use $localize to mark our navbar link strings for translation.

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

@Component({

  selector: 'app-navbar',

  templateUrl: './navbar.component.html',

})

export class NavbarComponent {

  home: string = $localize`Home`;

  about: string = $localize`About`;

}

Strings in our component code marked with $localize will be pulled by ng extract-i18n into messages.xlf as usual.

<!-- … -->

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

        <source>Home</source>

        <context-group purpose="location">

          <context context-type="sourcefile">src/app/navbar/navbar.component.ts</context>

          <context context-type="linenumber">8</context>

        </context-group>

      </trans-unit>

<!-- … -->

To complete the translation we of course copy the <trans-unit> to messages.ar.xlf, add a <target> with our Arabic translation, and run ng build --localize.

English and Arabic versions of our app's navbar

How do I work with dynamic values in my translated strings?

Injecting strings that can change at runtime into translations is quite common, and works seamlessly with $localize. Our arcade cabinet images currently have English-only alt text that can serve to demonstrate.

// …

@Component({

  selector: 'app-home',

  templateUrl: './home.component.html',

})

export class HomeComponent implements OnInit {

  // …

  altFor(cabinet: Cabinet): string {

    return `Image of ${cabinet.name}`;

  }

}

All we need to do is stick $localize in front of our template string, and interpolate cabinet.name as usual.

// …

@Component({

  selector: 'app-home',

  templateUrl: './home.component.html',

})

export class HomeComponent implements OnInit {

  // …

  altFor(cabinet: Cabinet): string {

    return $localize`Image of ${cabinet.name}`;

  }

}

Angular is smart enough to know that interpolated values need special placeholders in translation strings, and it adds them in for us when we run ng extract-i18n.

<!-- … -->

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

        <source>Image of <x id="PH" equiv-text="cabinet.name"/></source>

        <context-group purpose="location">

          <context context-type="sourcefile">src/app/home/home.component.ts</context>

          <context context-type="linenumber">29</context>

        </context-group>

      </trans-unit>

<!-- … -->

Note the <x …> placeholder tag added above. We need to use this same tag in our translations to let Angular know where we want to inject the runtime value.

<!-- … -->

      <trans-unit id="6223458566712133696">

        <source>Image of <x id="PH" equiv-text="cabinet.name"/></source>

        <target>صورة <x id="PH" equiv-text="cabinet.name"/></target>

      </trans-unit>

<!-- … -->

That’s all it takes. Now we can ng build --localize and run our test production server. When we do, opening our browser’s dev tools reveals that our alt tags are localized with proper interpolation in our builds.

Localized interpolation displayed in our English and Arabic image alt tags

How do I work with plurals in my translations?

Angular implements the ICU standard for its localized plurals. ICU is a bit outside the scope of this guide, but you can read more about it in The Missing Guide to the ICU Message Format. Still, the ICU syntax is intuitive enough that we don’t need a comprehensive understanding of the standard to use it. An ICU plural message looks like the following.

{todoCount, plural,

  one { You have one to-do remaining! }

  other { You have {{todoCount}} to-dos left. Keep going 🙂 }

}

The example above is an English translation, of course, so we provide the language’s two plural forms, one and other. todoCount (which can be named anything) is an interpolated integer counter that determines which form is rendered at runtime. We can inject the value of todoCount into our plural form messages using Angular’s usual {{variable}} syntax.

Let’s use this syntax to localize the string that indicates how many store outlets provide a given cabinet in azcadea.

<div>

  <div *ngFor="let cabinet of cabinets">

    <!-- … -->

    <div>

      <!-- … -->

      <div>

        <span i18n>

          {cabinet.storeCount, plural,

             =0 {Sold out}

             =1 {Available at 1 outlet}

             other {Available at {{ cabinet.storeCount }} outlets} }

        </span>

        <!-- … -->

      </div>

    </div>

  </div>

</div>

Note that we’ve used =0 and =1, plural form selectors that override the language’s formal selectors (one, few, other, etc.). This allows us to have a “zero” case in our English translation when English normally uses its other form for zero.

Heads up » In general, the other case is always required.

You may have guessed what’s next at this point: We run ng extract-i18n to get the message into messages.xlf.

<!-- … -->

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

        <source> <x id="ICU" equiv-text="{cabinet.storeCount, plural, =0 {Sold out} =1

          {Available at 1 outlet} other {Available at

          {{ cabinet.storeCount }} outlets} }" xid="4465788077291796430"/> </source>

        <!-- … -->

      </trans-unit>

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

        <source>{VAR_PLURAL, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at

          <x id="INTERPOLATION"/> outlets}}</source>

        <!-- … -->

      </trans-unit>

<!-- … -->

We get two <trans-unit>s this time, which can seem a bit weird. Angular pulls the ICU nested expression bit out into its own separate <trans-unit>. This is because sometimes the ICU expression is only part of a bigger translation string. In this case, the ICU expression takes up the entire string, so both <trans-units> are virtually identical. Let’s copy both <trans-units> into our Arabic translation file and translate them.

<!-- … -->

      <trans-unit id="8856905278208146821">

        <source> <x id="ICU" equiv-text="{cabinet.storeCount, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at {{ cabinet.storeCount }} outlets} }" xid="4465788077291796430"/> </source>

        <target> <x id="ICU" equiv-text="{cabinet.storeCount, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at           {{ cabinet.storeCount }} outlets} }" xid="4465788077291796430"/> </target>

      </trans-unit>

      <trans-unit id="7185053985224017078">

        <source>{VAR_PLURAL, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at

          <x id="INTERPOLATION"/> outlets}}</source>

        <target>{VAR_PLURAL, plural,

          =0 {غير متوفر}

          =1 {متوفر عند فرع <x id="INTERPOLATION"/>}

          two {متوفر عند فرعين}

          few {متوفر عند <x id="INTERPOLATION"/> أفرع}

          many {متوفر عند <x id="INTERPOLATION"/> فرع}

          other {متوفر عند <x id="INTERPOLATION"/> فرع}

        }</target>

      </trans-unit>

<!-- … -->

The outer string is copied as-is in translation and is only included in this case to avoid a warning that the Angular build tools will output if we don’t include it. The important <trans-unit> for us is the second, nested expression. Note that, unlike English, Arabic has six plural forms that need to be added for proper translation.

🔗 Resource » The CLDR Language Plural Rules document lists plural forms for all languages.

If we build our app for production and test it, we see that our plural forms are showing correctly for our supported locales.

Our demo app showing plural forms for Arabic and English translations

How do I localize numbers in my Angular app?

The simplest way to localize numbers is to use Angular’s built-in formatting pipes in our component templates. These pipes will use the active locale for formatting by default.

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

@Component({

  selector: 'app-number-pipes',

  template: `

    <p>number: {{ myNumber | number }}</p>

    <p>currency: {{ myNumber | currency: 'CAD' }}</p>

    <p>percent: {{ myPercent | percent }}</p>

  `,

})

export class NumberPipesComponent {

  myNumber: number = 9999.99;

  myPercent: number = 0.21;

}

<!-- When active locale is en-CA, renders: -->

<p>number: 9,999.99</p>

<p>currency: $9,999.99</p>

<p>percent: 21%</p>

<!-- When active locale is ar, renders: -->

<p>number: 9,999.99</p>

<p>currency: CA$ 9,999.99</p>

<p>percent: 21‎%‎</p>

You can notice a difference in the currency formatting above. Unfortunately, Angular always seems to use Western Arabic (1, 2, 3) digits regardless of the active locale. While not ideal, this is often adequate, since most people are familiar enough with Western Arabic digits.

🤿 Go deeper » The decimal, currency, and percent pipes have a lot of formatting options through arguments. They also have function equivalents we can use in our TypeScript: formatNumber(), formatCurrency(), and formatPercent().

Let’s update our Home component to localize our arcade cabinet prices.

<div>

  <div *ngFor="let cabinet of cabinets">

      <!-- … -->

        <p>

          {{ cabinet.price | currency: 'USD' }}

        </p>

	  <!-- … -->

  </div>

</div>

How do I localize dates in my Angular app?

Similar to numbers, the simplest way to localize a date is to use the date pipe in our component templates.

<div>

  <div *ngFor="let cabinet of cabinets">

      <!-- … -->

        <p>

          <!-- cabinet.addedAt is a Date object -->

          {{ cabinet.addedAt | date }}

        </p>

	  <!-- … -->

  </div>

</div>

Angular will automatically use the active locale when rendering the date.

<!-- When active locale is en-CA, and cabinet.addedAt is

     2022-01-22, the above will render: -->

<p>Jan. 22, 2022</p>

<!-- When active locale is ar, and cabinet.addedAt is

     2022-01-22, the above will render: -->

<p>2022/01/22</p>

We can specify a date format for the pipe to control its output.

<div>

  <div *ngFor="let cabinet of cabinets">

      <!-- … -->

        <p>

          {{ cabinet.addedAt | date: 'd MMM YYYY' }}

        </p>

	  <!-- … -->

  </div>

</div>

🤿 Go deeper » Check out of all the formatting options for the date pipe, as well as its function equivalent, formatDate().

And with that, our demo app is pretty well localized 😊.

Complete English version of our demo app
Complete Arabic version of our demo app

🔗 Resource » Get the entire code of our fully localized app from GitHub.

How do I use Phrase to localize my Angular app?

You may have noticed that managing translation files with Angular’s localization library is a bit clunky. We have to copy the source locale for each of our other supported locales, and we have to keep all these files in sync as we make changes within our app.

There’s a better way: the Phrase Localization Suite can take care of all this juggling for us and keep us focused on the code we love. Let me show you how to use Phrase to take all that file syncing headache out of your localization workflow.

🗒 Note » To follow along here, you’ll need to sign up for Phrase.

With the Phrase CLI installed, we can run the following from the command line in our Angular project root.

$ phrase init

We’ll be asked for an access token at this point, and we can get one by logging into Phrase, clicking our name near the top-right of the page, then clicking Profile SettingsAccess tokens.

The account dropdown on the main Phrase account page

The token tab in profile settings

From there we can click the Generate Token button, enter the required note, and click Save. Next, let’s copy the API token we’re given and paste it where the CLI command is waiting for it.

Entering API token into Phrase CLI

At this point, we’ll be given a few prompts to complete initialization:

  • Select an existing Phrase project or create a new one. We can create a new one here and name it azcadea.
  • Select the translation file format. We can select the XLIFF (not XLIFF 2) format since that’s what we’re using here.
  • Enter the path to the file we want to upload. This is equivalent to our source locale, so we can enter ./src/locale/messages.xlf.
  • Enter the path to the files we want to download. We can use the special <locale_name> placeholder here and enter ./src/locale/messages.<locale_name>.xlf. This will allow us to add any number of locales to our Phrase project.

At this point, we’ll be asked if we want to upload our locales for the first time. We can enter y to start the upload.

Believe it or not, that’s it from a developer’s end. At this point, our English source translations are available in our Phrase project. Our translators can add Arabic (ar) as a project language, then use the Phrase web console to get their translation work done.

The Phrase translation console

🗒 Note » It’s probably a good idea to go to Project SettingsPlaceholders and selecting the OASIS XLIFF placeholders to match the <x …> placeholders Angular uses in our translation files. This will make the placeholders easy to spot and use for our translators.

Once translations are ready, we just need to run the following from the command line at the root of our project.

$ phrase pull

This downloads the latest Arabic messages to our codebase.

<?xml version="1.0" encoding="UTF-8"?>

<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">

  <file original="ng2.template" datatype="plaintext" source-language="en-CA" target-language="ar">

    <body>

      <trans-unit id="1726363342938046830">

        <source xml:lang="en-CA">About</source>

        <target xml:lang="ar">نبذة عنا</target>

      </trans-unit>

      <trans-unit id="7185053985224017078">

        <source xml:lang="en-CA">{VAR_PLURAL, plural, =0 {Sold out} =1 {Available at 1 outlet} other {Available at

          <x id="INTERPOLATION"/> outlets}}</source>

        <target xml:lang="ar">{VAR_PLURAL, plural,

          =0 {غير متوفر}

          =1 {متوفر عند فرع <x id="INTERPOLATION"/>}

          two {متوفر عند فرعين}

          few {متوفر عند <x id="INTERPOLATION"/> أفرع}

          many {متوفر عند <x id="INTERPOLATION"/> فرع}

          other {متوفر عند <x id="INTERPOLATION"/> فرع}

        }</target>

      </trans-unit>

      <!-- … -->

    </body>

  </file>

</xliff>

We didn’t have to sync translation files ourselves. Imagine how much easier our whole workflow would be if we had ten locales we had to support, or fifty!

What alternative localization libraries are available for Angular?

The first-party @angular/localize package is a robust i18n solution for our Angular apps. It is missing some features, however, like support for localized numeral systems. And, if you find Angular creating a production version of your app for each locale a little excessive, you might want to try some third-party Angular libraries for internationalization.

ngx-translate

At the time of writing no third-party i18n libraries for Angular are anywhere near as popular as ngx-translate. A small library that does things a bit differently from @angular/localize, ngx-translate has a few things on offer.

Features of ngx-translate

  • Uses simple JSON files for translations.
  • Allows locale switching at runtime (one app build for all locales).
  • Supports lazy-loading of translation files, which means only the translation our visitor needs is delivered to their browser, reducing load times.

Heads up » ngx-translate does not have any support for date and time formatting, however, so you’ll need additional libraries to handle that if you go with ngx-translate.

Here’s an example of a translated string in an Angular app using ngx-translate:

{

  "welcome": "Hello there!"

}

{

  "welcome": "أهلاً بك!"

}

<!-- In a template -->

<p>{{ "welcome" | translate }}</p>

<!-- When runtime locale is en, renders: -->

<p>Hello there!</p>

<!-- When runtime locale is ar, renders: -->

<p>أهلاً بك!</p>

🤿 Go deeper » With ngx-translate we can translate strings using directives or in TypeScript, handle plurals using a plugin, and much more. Read A Deep Dive on Angular I18n with ngx-translate to get all the details.

Transloco

While not as popular as ngx-translate, the Transloco i18n library for Angular shares the former’s basic design philosophy of loading JSON translation files into your Angular app—no need to rebuild the app for different locales. Well-documented and fully featured via first-party plugins, Transloco is definitely worth a look.

Features of Transloco

Much like ngx-translate, Transloco:

  • Uses simple JSON files for translations.
  • Allows locale switching at runtime.
  • Supports lazy loading of translation files.

However, Transloco also:

  • Has excellent documentation.
  • Supports robust fallback options for missing translations.
  • Supports native numeral systems for localized date and number formatting using the first-party Locale L10N plugin.
A shopping cart line item shown in different locales with their native numeral systems
A shopping cart line item shown in different locales with their native numeral systems

🔗 Resource » Read our Concise Guide to Number Localization if you’re interested in how different locales use different numeral systems.

Here’s what a translated string looks like when working with Transloco:

{

  "greeting": "Hello there!"

}

{

  "greeting": "أهلاً بك!"

}

<!-- In a template -->

<ng-container *transloco="let t">

  <p>{{ t('greeting') }}</p>

</ng-container>

<!-- When runtime locale is en, renders: -->

<p>Hello there!</p>

<!-- When runtime locale is ar, renders: -->

<p>أهلاً بك!</p>

🤿 Go deeper » The structural transloco directive is just one way to translate strings in templates: we can also use a translation pipe or attribute directive. Translating within TypeScript is also available, and much more. Read the Angular Tutorial on Localizing with Transloco for a more detailed look.

i18next

At the time of writing i18next is one of the most popular i18n libraries in the world outside of the Angular ecosystem. If you’re coming to Angular from another UI library or framework, you might already be familiar with i18next. After all, i18next seems to support every library and framework under the sun. Angular support for i18next comes in via the third-party angular-i18next package.

Features of i18next

Just like ngx-translate and Transloco, i18next:

  • Uses simple JSON files for translation by default.
  • Allows locale-switching at runtime.
  • Supports lazy-loading of translation files.

In addition, i18next boasts unique features:

  • Supports a plethora of libraries and frameworks both client- and server-side.
  • Almost any i18n feature is either built-in or supported via a plugin.
  • A flexible architecture means you can swap in your own solutions for any major part of the library.

Here’s what a string translated with i18next looks like in an Angular app:

{

  "hello": "Hello there!"

}

{

  "hello": "أهلاً بك!"

}

<!-- In a template -->

<p>{{ 'hello' | i18next }}</p>

<!-- When runtime locale is en, renders: -->

<p>Hello there!</p>

<!-- When runtime locale is ar, renders: -->

<p>أهلاً بك!</p>

🤿 Go deeper » We’ve just scratched the surface of i18next here. Check out Angular L10n with I18next to get more details on setting up and using i18next in your Angular apps.

Wrapping up our Angular l10n tutorial

We hope that our Angular i18n guide has served you well. No matter which i18n library you choose to go with, Phrase can take your localization process to the next level.

The Phrase Localization Suite supports all the translation file formats we’ve covered here and many more. With its CLI and Bitbucket, GitHub, and GitLab sync, your i18n can be on autopilot. The fully-featured Phrase web console, with machine learning and smart suggestions, is a joy for translators to use. Once translations are ready, they can sync back to your project automatically. You set it and forget it, leaving you to focus on the code you love.

Check out all Phrase features for developers and see for yourself how it can help you take your apps global.