Internationalizing your app can make software development a painful experience, especially if you don’t start doing it from the very beginning or you take a willy-nilly approach toward it.
Modern apps, where the front-end and the back-end are distinctly separate from one another, can be even trickier to deal with when it comes to internationalization. Suddenly you no longer have access to the plethora of time-tested tools that once helped with internationalizing your traditional server-side page generated web apps.
Accordingly, an AngularJS app requires on-demand delivery of internationalization (i18n) and localization (l10n) data to be delivered to the client to render itself in the appropriate locale. Unlike traditional server-side rendered apps, you can no longer rely on the server to deliver pages that are already localized. You can learn about building a multilingual PHP application here
In this article, you will learn how you can internationalize your AngularJS app, and will learn about tools that you can use to ease the process. Making your AngularJS app multilingual can pose some interesting challenges, but certain approaches can make it easier to work around most of those challenges.
A Simple i18n Capable AngularJS App
To allow the client to change the language and locale on the fly based on user preferences, you will need to make a number of key design decisions:
- How do you design your app to be language and locale-agnostic from the start?
- How do you structure i18n and l10n data?
- How do you deliver this data efficiently to clients?
- How do you abstract away as much of the low-level implementation details to simplify the developer workflow?
Answering these questions as early as possible can help avoid hindrances in the development process down the line. Each of these challenges will be addressed in this article; some through robust AngularJS libraries, others through certain strategies and approaches.
Internationalization Libraries for AngularJS
There are a number of JavaScript libraries that are built specifically for internationalizing AngularJS apps.
angular-translate
is an AngularJS module that provides filters and directives, along with the ability to load i18n data asynchronously. It supports pluralization through MessageFormat
, and is designed to be highly extensible and configurable.
If you are using
angular-translate
in your project, you may find some of the following packages super useful:angular-sanitize
: can be used to guard against XSS attacks in translations.angular-translate-interpolation-messageformat
: pluralization with support for gender-sensitive text formatting.angular-translate-loader-partial
: used to deliver translated strings to clients.
For a truly dynamic experience, you can add
angular-dynamic-locale
to the bunch. This library allows you to change the locale dynamically—and that includes the way dates, numbers, currencies, etc. are all formatted.
Getting Started: Installing Relevant Packages
Assuming you already have your AngularJS boilerplate ready, you can use NPM to install the internationalization packages:
npm i -S angular-translate angular-translate-interpolation-messageformat angular-translate-loader-partial angular-sanitize messageformat
Once the packages are installed, do not forget to add the modules as your app’s dependencies:
// /src/app/core/core.module.js
app.module('app.core', ['pascalprecht.translate', ...]);
Note that the name of the module is different from the name of the package.
Translating Your First String
Suppose your app has a toolbar with some text and a field with some placeholder text:
<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>
The above view has two bits of text that you can internationalize: “Hello” and “Search”. In terms of HTML, one appears as the innertext of an anchor tag, while the other appears as a value of an attribute.
To internationalize them, you will have to replace both string literals with tokens that AngularJS can then replace with the actual translated strings, based on the user’s preference, while rendering the page.
AngularJS can do this by using your tokens to perform a lookup in translation tables that you provide. The module angular-translate expects these translation tables to be provided as plain JavaScript objects or as JSON objects (if loading remotely).
Here’s an example of what these translation tables would generally look like:
// /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 view from above, you need to replace the string literals with tokens that AngularJS can use to lookup in the translation table:
<!-- /src/app/toolbar/toolbar.html -->
<a class="navbar-brand" href="#" translate="TOOLBAR.HELLO"></a>
<!-- or -->
<a class="navbar-brand" href="#">{{'TOOLBAR.HELLO' | translate}}</a>
Notice how, for inner text, you can either use the translate directive or the translate filter. (You can learn more about the translate directive here and about translate filters here.)
With these changes, when the view is rendered, angular-translate will automatically insert the appropriate translation corresponding to TOOLBAR.HELLO into the DOM based on the current language.
To tokenize string literals that appear as attribute values, you can use the following approach:
<!-- /src/app/toolbar/toolbar.html -->
<input type="text"
class="form-control"
ng-model="vm.query"
translate
translate-attr-placeholder="TOOLBAR.SEARCH">
Now, what if your tokenized strings contained variables?
To handle cases like “Hello, {{name}}.”, you can perform variable replacement using the same interpolator syntax that AngularJS already supports:
Translation table:
// /src/app/toolbar/i18n/en.json
{
"TOOLBAR": {
"HELLO": "Hello, {{name}}."
}
}
You can then define the variable in a number of ways. Here are a few:
<!-- /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>
Dealing with Pluralization and Gender
Pluralization is a pretty hard topic when it comes to i18n and l10n. Different languages and cultures have different rules for how a language handles pluralization in various situations.
Because of these challenges, software developers will sometimes simply not address the problem (or at least won’t address it adequately), resulting in software that produces silly sentences like these:
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 is a standard for how to handle this, and a JavaScript implementation of the standard is available as MessageFormat.
With MessageFormat, you can replace the above poorly structured sentences with the following:
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 the following:
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(' ');
You can build a formatter with the above array, and use it to generate strings:
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.'
How can you use MessageFormat with angular-translate to take advantage of its full functionality within your apps?
In your app config, you simply tell angular-translate that message format interpolation is available as follows:
/src/app/core/core.config.js
app.config(function ($translateProvider) {
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
});
Here is how an entry in the translation table might then look:
// /src/app/main/social/i18n/en.json
{
"SHARED": "{GENDER, select, male{He} female{She} other{They}} shared this."
}
And in the view:
<!-- /src/app/main/social/social.html -->
<div translate="SHARED"
translate-values="{ GENDER: 'male' }"
translate-interpolation="messageformat"></div>
<div>
{{ 'SHARED' | translate:"{ GENDER: 'male' }":'messageformat' }}
</div>
Here you must explicitly indicate that the message format interpolator should be used instead of the default interpolator in AngularJS. This is because the two interpolators differ slightly in their syntax. You can read more about this here.
Providing Translation Tables to Your App
Now that you know how AngularJS can lookup translations for your tokens from translation tables, how does your app know about the translation tables in the first place? How do you tell your app which locale/language should be used?
This is where you learn about $translateProvider.
You can provide the translation tables for each locale that you want to support directly in your app’s core.config.js file as follows:
// /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 you are providing translation tables as JavaScript objects for English (en) and Turkish (tr), while declaring the current language to be English (en). If the user wishes to change the language, you can do so with the $translate service:
// /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...
};
});
There’s still the question of which language should be used by default. Hard-coding the initial language of our app may not always be acceptable. In such cases, an alternative is to attempt to determine the language automatically using $translateProvider:
// /src/app/core/core.config.js
app.config(function ($translateProvider) {
...
$translateProvider.determinePreferredLanguage();
});
determinePreferredLanguage searches for values in window.navigator and selects an intelligent default until a clear signal is provided by the user.
Lazy-loading Translation Tables
The previous section showed how you can provide translation tables directly in the source code as JavaScript objects. This may be acceptable for small applications, but the approach is not scalable, which is why translation tables are often downloaded as JSON files from a remote server.
Maintaining translation tables this way reduces the initial payload size delivered to the client but introduces additional complexity. Now you are faced with the design challenge of delivering i18n data to the client. If this is not handled carefully, your application’s performance can suffer needlessly.
Why is it so complex? AngularJS applications are organized into modules. In a complex application, there may be many modules, each with its own distinct i18n data. A naive approach, such as loading and providing i18n data all at once, should therefore be avoided.
What you need is a way to organize your i18n data by module. This will enable you to load just what you need when you need it, and to cache what’s previously been loaded to avoid reloading the same data (at least until the cache is invalid).
This is where partialLoader comes into play.
Let’s say your application’s translation tables are structured like this:
/src/app/main/i18n/en.json /src/app/main/i18n/tr.json /src/app/toolbar/i18n/en.json /src/app/toolbar/i18n/tr.json
You can configure $translateProvider to use partialLoader with a URL pattern that matches this structure:
// /src/app/core/core.config.js
app.config(function ($translateProvider) {
...
$translateProvider.useLoader('$translatePartialLoader', {
urlTemplate: '/src/app/{part}/i18n/{lang}.json'
});
});
As one would expect, “lang” is replaced with the language code at runtime (e.g. “en” or “tr”). What about “part”? How does $translateProvider know which “part” to load?
You can provide this information inside controllers with $translatePartialLoader:
// /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, and the i18n data for a given view is loaded when its controller is first executed, which is exactly what you want.
Caching: Reducing Load Times
What about caching?
You can enable the standard cache in the app config with $translateProvider:
// /src/app/core/core.config.js
app.config(function ($translateProvider) {
...
$translateProvider.useLoaderCache(true); // default is false
});
If you need to bust the cache for a given language, you can use $translate:
$translate.refresh(languageKey); // omit languageKey to refresh all
With these pieces in place, your application is fully internationalized and supports multiple languages.
Localizing Numbers, Currencies, and Dates
In this section, you will learn how you can use angular-dynamic-locale to support formatting of UI elements such as numbers, currencies, dates, and the like, in an AngularJS application.
You will need to install two more packages for this:
npm i -S angular-dynamic-locale angular-i18n
Once the packages are installed, you can add the module to your app’s dependencies:
// /src/app/core/core.module.js
app.module('app.core', ['tmh.dynamicLocale', ...]);
Locale Rules
Locale rules are simple JavaScript files that provide specifications for how dates, numbers, currencies, and the like should be formatted by components that depend on the $locale service.
The list of currently supported locales is available here.
Here is a snippet from angular-locale_en-us.js illustrating month and date formatting:
... "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 global to the application, requiring the rules for a given locale to be loaded all at once.
By default, angular-dynamic-locale expects locale rules files to be located in angular/i18n/angular-locale_{{locale}}.js. If they are located elsewhere, tmhDynamicLocaleProvider must be used to override the default:
// /src/app/core/core.config.js
app.config(function (tmhDynamicLocaleProvider) {
tmhDynamicLocaleProvider.localeLocationPattern(
'/node_modules/angular-i18n/angular-locale_{{locale}}.js');
});
Caching is automatically handled by the tmhDynamicLocaleCache service.
Invalidating the cache is less of a concern here, since locale rules are less likely to change than string translations.
To switch between locales, angular-dynamic-locale provides the tmhDynamicLocale service:
// /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 with Automatic Translation
Locale rules are shipped with the angular-i18n package, so all you have to do is make the package contents available to your application as needed. But how do you generate the JSON files for your translation tables? There isn’t exactly a package you could download and plug into our application.
One option is to use programmatic translation APIs, especially if the strings in your application are simple literals without variables or pluralized expressions.
With Gulp and a couple of extra packages, requesting programmatic translations for your application is a breeze:
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 ...
The script first reads all English translation tables, asynchronously requests translations for their string resources, and then replaces the English strings with the translated strings to produce a translation table in a new language.
Finally, the new translation table is written as a sibling to the English translation table, yielding:
/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 ...
Implementation of getTranslation is also straightforward:
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 });
}
Here, we’re using Microsoft Translate, but one could just easily use another provider such as Google Translate or Yandex Translate.
While programmatic translations are convenient, there are several drawbacks, including:
- Robot translations are good for short strings, but even then, there could be pitfalls with words that have different meanings in different contexts (e.g., “pool” can mean swimming or grouping).
- APIs may not be able to handle strings with variables or strings that rely on message format.
In these cases and others, human translations may be required; however, that’s a topic for another blog post.
Internationalizing Front-ends Only Looks Daunting
In this article, you learned how to use these packages to internationalize and localize AngularJS applications.
angular-translate, angular-dynamic-locale, and gulp are powerful tools for internationalizing an AngularJS application that encapsulate painful low-level implementation details.
For a demo app that illustrates the ideas discussed in this post, check out this GitHub repository.
About the author
Mehmet Bajin
Source: toptal.com
0 comments:
Post a Comment