Tips for Making Your AngularJS App International-friendly

Making your software globally accessible can be quite a challenge during development, particularly if internationalization isn’t considered from the outset or if a haphazard approach is taken.

This challenge is amplified with modern apps that decouple the front-end and back-end. Traditional tools for internationalizing server-side rendered web apps become less effective.

Consequently, an AngularJS app needs i18n and l10n data delivered directly to the client for proper locale rendering. Unlike server-side rendered apps, you can’t rely on pre-localized pages from the server. For insights on building multilingual PHP applications, refer to this resource.

This article guides you through internationalizing your AngularJS app, introducing tools to streamline the process. While going multilingual with AngularJS presents unique hurdles, strategic approaches can mitigate most of these.

Building a Multilingual AngularJS App

To empower users to switch languages and locales seamlessly, several key design decisions need to be addressed:

  • How to design your app from the ground up to be language and locale-agnostic?
  • How to structure i18n and l10n data effectively?
  • How to deliver this data to clients efficiently?
  • How to abstract low-level implementation details for a simplified developer experience?

Answering these early on prevents development roadblocks down the line. This article tackles each of these, some through robust AngularJS libraries and others through strategic approaches.

Internationalization Libraries for AngularJS

Various JavaScript libraries are purpose-built for internationalizing AngularJS apps.

angular-translate is one such AngularJS module, offering filters, directives, and asynchronous i18n data loading. It supports pluralization through MessageFormat and boasts high extensibility and configurability.

When using angular-translate, these packages can prove beneficial:

For a truly dynamic experience, incorporate angular-dynamic-locale. This library allows dynamic locale switching, impacting the formatting of dates, numbers, currencies, and more.

Getting Started: Installing Necessary Packages

Assuming you have your AngularJS boilerplate ready, install the internationalization packages using NPM:

1
2
3
4
5
6
npm i -S
 angular-translate
 angular-translate-interpolation-messageformat
 angular-translate-loader-partial
 angular-sanitize
 messageformat

After installation, remember to add these modules as dependencies in your app:

1
2
// /src/app/core/core.module.js
app.module('app.core', ['pascalprecht.translate', ...]);

Note the distinction between the module name and the package name.

Translating Your First String

Imagine your app has a toolbar with some text and a search field with placeholder text:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<nav class="navbar navbar-default">
 <div class="container-fluid">
   <div class="navbar-header">
     <a class="navbar-brand" href="#">Hello</a>
   </div>
   <div class="collapse navbar-collapse">
     <form class="navbar-form navbar-left">
       <div class="form-group">
         <input type="text"
                class="form-control"
                ng-model="vm.query"
                placeholder="Search">
       </div>
     ...
   </div>
 </div>
</nav>

This view has two translatable text elements: “Hello” and “Search.” In HTML terms, one is within an anchor tag, while the other is an attribute value.

Internationalization requires replacing these literals with tokens that AngularJS can dynamically replace with the appropriate translations based on user preference during rendering.

AngularJS achieves this by using tokens to look up translations in provided tables. The angular-translate module expects these tables as plain JavaScript objects or JSON objects (for remote loading).

A typical translation table structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// /src/app/toolbar/i18n/en.json
{
 "TOOLBAR": {
   "HELLO": "Hello",
   "SEARCH": "Search"
 }
}
// /src/app/toolbar/i18n/tr.json
{
 "TOOLBAR": {
   "HELLO": "Merhaba",
   "SEARCH": "Ara"
 }
}

To internationalize the toolbar, replace literals with AngularJS-interpretable tokens:

1
2
3
4
5
6
<!-- /src/app/toolbar/toolbar.html -->
<a class="navbar-brand" href="#" translate="TOOLBAR.HELLO"></a>

<!-- or -->

<a class="navbar-brand" href="#">{{'TOOLBAR.HELLO' | translate}}</a>

Observe how the translate directive or filter can be used for inner text. (Explore more about the translate directive here and translate filters here.)

With these changes, angular-translate dynamically inserts the appropriate TOOLBAR.HELLO translation based on the current language.

For tokenizing literals within attribute values:

1
2
3
4
5
6
<!-- /src/app/toolbar/toolbar.html -->
<input type="text"
      class="form-control"
      ng-model="vm.query"
      translate
      translate-attr-placeholder="TOOLBAR.SEARCH">

Now, how about tokenized strings with variables?

For instances like “Hello, {{name}}.”, use AngularJS’s interpolator syntax for variable replacement:

Translation table:

1
2
3
4
5
6
// /src/app/toolbar/i18n/en.json
{
 "TOOLBAR": {
    "HELLO": "Hello, {{name}}."
 }
}

Define the variable in various ways, such as:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!-- /src/app/toolbar/toolbar.html -->
<a ...
  translate="TOOLBAR.HELLO"
  translate-values='{ name: vm.user.name }'></a>

<!-- or -->

<a ...
  translate="TOOLBAR.HELLO"
  translate-value-name='{{vm.user.name}}'></a>

<!-- or -->

<a ...>{{'TOOLBAR.HELLO | translate:'{ name: vm.user.name }'}}</a>

Handling Pluralization and Gender

Pluralization is a complex aspect of i18n and l10n. Different languages have distinct pluralization rules across contexts.

Often, developers might overlook this (or not address it comprehensively), leading to awkward sentences like:

1
2
3
He saw 1 person(s) on floor 1.
She saw 1 person(s) on floor 3.
Number of people seen on floor 2: 2.

Fortunately, there’s a standard to address this, with a JavaScript implementation available as MessageFormat.

Using MessageFormat, restructure the previous sentences:

1
2
3
He saw 1 person on the 2nd floor.
She saw 1 person on the 3rd floor.
They saw 2 people on the 5th floor.

MessageFormat accepts expressions like:

1
2
3
4
5
6
7
8
var message = [
 '{GENDER, select, male{He} female{She} other{They}}',
 'saw',
 '{COUNT, plural, =0{no one} one{1 person} other{# people}}',
 'on the',
 '{FLOOR, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}',
 'floor.'
].join(' ');

Build a formatter with such an array and generate strings:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var messageFormatter = new MessageFormat('en').compile(message);

messageFormatter({ GENDER: 'male', COUNT: 1, FLOOR: 2 })
// 'He saw 1 person on the 2nd floor.'

messageFormatter({ GENDER: 'female', COUNT: 1, FLOOR: 3 })
// 'She saw 1 person on the 3rd floor.'

messageFormatter({ COUNT: 2, FLOOR: 5 })
// 'They saw 2 people on the 5th floor.'

To leverage the full potential of MessageFormat within your apps in conjunction with angular-translate:

In your app config, inform angular-translate about message format interpolation:

1
2
3
4
/src/app/core/core.config.js
app.config(function ($translateProvider) {
 $translateProvider.addInterpolation('$translateMessageFormatInterpolation');
});

A translation table entry might then look like:

1
2
3
4
// /src/app/main/social/i18n/en.json
{
 "SHARED": "{GENDER, select, male{He} female{She} other{They}} shared this."
}

And in the view:

1
2
3
4
5
6
7
<!-- /src/app/main/social/social.html -->
<div translate="SHARED"
    translate-values="{ GENDER: 'male' }"
    translate-interpolation="messageformat"></div>
<div>
 {{ 'SHARED' | translate:"{ GENDER: 'male' }":'messageformat' }}
</div>

Explicitly specifying the message format interpolator is necessary due to slight syntax variations with the default AngularJS interpolator. More details can be found here.

Providing Translation Tables

Now that AngularJS knows how to look up translations, how does it know about the tables themselves? And how do you set the default language?

This is where $translateProvider comes in.

Provide translation tables for each supported locale directly in your app’s core.config.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// /src/app/core/core.config.js
app.config(function ($translateProvider) {
 $translateProvider.addInterpolation('$translateMessageFormatInterpolation');

 $translateProvider.translations('en', {
   TOOLBAR: {
     HELLO: 'Hello, {{name}}.'
   }
 });

 $translateProvider.translations('tr', {
   TOOLBAR: {
     HELLO: 'Merhaba, {{name}}.'
   }
 });

 $translateProvider.preferredLanguage('en');
});

Here, English (en) and Turkish (tr) tables are provided as JavaScript objects, with English set as the default. To switch languages based on user preference, use $translate service:

1
2
3
4
5
6
7
// /src/app/toolbar/toolbar.controller.js
app.controller('ToolbarCtrl', function ($scope, $translate) {
 $scope.changeLanguage = function (languageKey) {
   $translate.use(languageKey);
   // Persist selection in cookie/local-storage/database/etc...
 };
});

For dynamic default language selection without hardcoding, use $translateProvider:

1
2
3
4
5
// /src/app/core/core.config.js
app.config(function ($translateProvider) {
 ...
 $translateProvider.determinePreferredLanguage();
});

determinePreferredLanguage analyzes window.navigator for an intelligent default until a clear user preference is available.

Lazy-Loading Translation Tables

The previous section demonstrated embedding translation tables directly in the source code. While feasible for small applications, this lacks scalability. Often, translation tables are fetched as JSON files from a server.

This reduces the initial payload but adds complexity to delivering i18n data, potentially impacting performance if not handled carefully.

The challenge stems from the modular structure of AngularJS applications. A large application might have numerous modules, each with its own i18n data. Avoid loading all i18n data upfront.

The solution lies in organizing i18n data by module. This allows loading only what’s necessary, caching previously loaded data to prevent redundant requests (until cache invalidation).

This is where partialLoader proves useful.

Suppose your application’s translation tables follow this structure:

1
2
3
4
/src/app/main/i18n/en.json
/src/app/main/i18n/tr.json
/src/app/toolbar/i18n/en.json
/src/app/toolbar/i18n/tr.json

Configure $translateProvider to use partialLoader with a matching URL pattern:

1
2
3
4
5
6
7
// /src/app/core/core.config.js
app.config(function ($translateProvider) {
 ...
 $translateProvider.useLoader('$translatePartialLoader', {
   urlTemplate: '/src/app/{part}/i18n/{lang}.json'
 });
});

“lang” is dynamically replaced with the language code (e.g., “en” or “tr”). But how about “part”? How does $translateProvider determine which “part” to load?

Provide this context within controllers using $translatePartialLoader:

1
2
3
4
5
6
7
8
9
// /src/app/main/main.controller.js
app.controller('MainCtrl', function ($translatePartialLoader) {
 $translatePartialLoader.addPart('main');
});

// /src/app/toolbar/toolbar.config.js
app.controller('ToolbarCtrl', function ($translatePartialLoader) {
 $translatePartialLoader.addPart('toolbar');
});

The pattern is now complete, loading the relevant i18n data only when its controller is first executed.

Caching for Faster Loading

Enable caching in the app config using $translateProvider:

1
2
3
4
5
// /src/app/core/core.config.js
app.config(function ($translateProvider) {
 ...
 $translateProvider.useLoaderCache(true); // default is false
});

To clear the cache for a specific language, use $translate:

1
$translate.refresh(languageKey); // omit languageKey to refresh all

With these in place, your application is fully internationalized with multilingual support.

Localizing Numbers, Currencies, and Dates

This section explores using angular-dynamic-locale to format UI elements like numbers, currencies, and dates in an AngularJS app.

Install two additional packages:

1
2
3
npm i -S
 angular-dynamic-locale
 angular-i18n

After installation, add the module to your app’s dependencies:

1
2
// /src/app/core/core.module.js
app.module('app.core', ['tmh.dynamicLocale', ...]);

Locale Rules

Locale rules are JavaScript files specifying formatting rules for dates, numbers, currencies, etc., for components relying on the $locale service.

A list of supported locales is available here.

Here’s a snippet from angular-locale_en-us.js showing month and date formatting:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
   "MONTH": [
     "January",
     "February",
     "March",
     "April",
     "May",
     "June",
     "July",
     "August",
     "September",
     "October",
     "November",
     "December"
   ],
   "SHORTDAY": [
     "Sun",
     "Mon",
     "Tue",
     "Wed",
     "Thu",
     "Fri",
     "Sat"
   ],
...

Unlike i18n data, locale rules are application-global, requiring the entire rule set for a locale to be loaded at once.

By default, angular-dynamic-locale expects rule files at angular/i18n/angular-locale_{{locale}}.js. Override this using tmhDynamicLocaleProvider:

1
2
3
4
5
// /src/app/core/core.config.js
app.config(function (tmhDynamicLocaleProvider) {
 tmhDynamicLocaleProvider.localeLocationPattern(
   '/node_modules/angular-i18n/angular-locale_{{locale}}.js');
});

The tmhDynamicLocaleCache service handles caching automatically.

Cache invalidation is less critical for locale rules as they change less frequently than string translations.

Switch between locales using the tmhDynamicLocale service:

1
2
3
4
5
6
7
// /src/app/toolbar/toolbar.controller.js
app.controller('ToolbarCtrl', function ($scope, tmhDynamicLocale) {
 $scope.changeLocale = function (localeKey) {
   tmhDynamicLocale.set(localeKey);
   // Persist selection in cookie/local-storage/database/etc...
 };
});

Generating Translation Tables: Automatic Translation

While locale rules come bundled with angular-i18n, generating JSON files for translation tables requires a different approach. There isn’t a readily available package for this.

One option is leveraging programmatic translation APIs, particularly for simple literal strings without variables or pluralization.

With Gulp and a few extra packages, request programmatic translations effortlessly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import gulp from 'gulp';
import map from 'map-stream';
import rename from 'gulp-rename';
import traverse from 'traverse';
import transform from 'vinyl-transform';
import jsonFormat from 'gulp-json-format';

function translateTable(to) {
 return transform(() => {
   return map((data, done) => {
     const table = JSON.parse(data);

     const strings = [];
     traverse(table).forEach(function (value) {
       if (typeof value !== 'object') {
         strings.push(value);
       }
     });

     Promise.all(strings.map((s) => getTranslation(s, to)))
       .then((translations) => {
         let index = 0;
         const translated = traverse(table).forEach(function (value) {
           if (typeof value !== 'object') {
             this.update(translations[index++]);
           }
         });
         done(null, JSON.stringify(translated));
       })
       .catch(done);
   });
 });
}

function translate(to) {
 return gulp.src('src/app/**/i18n/en.json')
   .pipe(translateTable(to))
   .pipe(jsonFormat(2))
   .pipe(rename({ basename: to }))
   .pipe(gulp.dest('src/app'));
}

gulp.task('translate:tr', () => translate('tr'));

This task assumes the following folder structure:

/src/app/main/i18n/en.json
/src/app/toolbar/i18n/en.json
/src/app/navigation/i18n/en.json
...

This script reads all English translation tables, fetches translations for their strings asynchronously, and replaces the English strings with the translated counterparts, creating a new language table.

The new table is saved alongside the English one, resulting in:

1
2
3
4
5
6
7
/src/app/main/i18n/en.json
/src/app/main/i18n/tr.json
/src/app/toolbar/i18n/en.json
/src/app/toolbar/i18n/tr.json
/src/app/navigation/i18n/en.json
/src/app/navigation/i18n/tr.json
...

Implementing getTranslation is straightforward:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import bluebird from 'bluebird';
import MicrosoftTranslator from 'mstranslator';

bluebird.promisifyAll(MicrosoftTranslator.prototype);

const Translator = new MicrosoftTranslator({
 client_id: process.env.MICROSOFT_TRANSLATOR_CLIENT_ID,
 client_secret: process.env.MICROSOFT_TRANSLATOR_CLIENT_SECRET
}, true);

function getTranslation(string, to) {
 const text = string;
 const from = 'en';

 return Translator.translateAsync({ text, from, to });
}

This example uses Microsoft Translate, but other providers like Google Translate or Yandex Translate can be used interchangeably.

While convenient, programmatic translations have limitations:

  • Robot translations work well for short strings but can stumble with context-dependent words (e.g., “pool” for swimming or grouping).
  • APIs might struggle with variables or message format strings.

Human translation becomes necessary in such cases, but that’s a topic for another discussion.

Internationalizing Front-ends: Less Daunting Than It Seems

This article explored using various packages for internationalizing and localizing AngularJS applications.

angular-translate, angular-dynamic-locale, and gulp are powerful tools that abstract away the complexities of internationalization.

A demo showcasing these concepts can be found GitHub repository.

Licensed under CC BY-NC-SA 4.0