Integrating AngularJS and i18n-js, Switching Locales at Runtime
AngularJS has really nice built-in support for number, currency and date formatting. Unfortunately it lacks two significant features:
- Performing string lookup / interpolation via externalized message bundles, and
- Allowing the application to change the current locale at runtime.
A colleague of mine recently recommended the i18n-js library for internationalization. i18n-js is a lightweight library (~700 lines) which implements string lookup and formatting and is a suitable replacement for Angular’s localization libraries.
I’ve written a proof-of-concept showing how the two can be integrated and made the source available at Github. Below is an explanation of some of the key files. Some of the gists have been trimmed slightly to ease formatting on the page, so refer to Github for the full code.
(function () {
"use strict";
var I18nAdapter = function I18nAdapter(locales, defaultLocale) {
this.locales = this._formatLocales(locales);
this.defaultLocale = this._formatLocale(defaultLocale);
I18n.locale = I18n.defaultLocale = this.defaultLocale;
};
/**
* Change the current locale and load the specified resource files.
* @param {String} locale
* @param {Array.<String>} urls
* @return {Promise}
*/
I18nAdapter.prototype.setLocale = function (locale, urls) {
var promise,
promises = [],
i,
url;
locale = I18nAdapter.prototype._formatLocale(locale);
if (!I18n.translations[locale]) {
for (i = 0; i < urls.length; i++) {
url = urls[i];
promise = this._loadBundle(locale, url);
promises.push(promise);
}
// when all bundles have loaded, update the locale
promise = $.when.apply(this, promises);
return promise.then(function () {
console.log("set locale to: " + locale);
I18n.locale = locale;
});
}
I18n.locale = locale;
return $.Deferred().resolve().promise();
};
/**
* Load a resource bundle into the specified locale
* @param locale
* @param url
* @return {Promise}
*/
I18nAdapter.prototype._loadBundle = function (locale, url) {
var config = {
dataType: "json",
};
locale = this._formatLocale(locale);
I18n.translations[locale] = {};
return $.ajax(url, config).then(function (data) {
_.extend(I18n.translations[locale], data);
});
};
/**
* Get translation for key.
* @param {String} key
* @param {Object} options
* @return {String}
*/
I18nAdapter.prototype.translate = function (key, options) {
return I18n.t(key, options);
};
/**
* Format number.
* @param value
* @param options
* @return {String}
*/
I18nAdapter.prototype.formatNumber = function (value, options) {
return I18n.toNumber(value, options);
};
/**
* Format currency.
* @param value
* @param options
* @return {String}
*/
I18nAdapter.prototype.formatCurrency = function (value, options) {
return I18n.toCurrency(value, options);
};
/**
* Format date.
* @param date
* @param format
* @return {String}
*/
I18nAdapter.prototype.formatDate = function (date, format) {
return I18n.strftime(date, format);
};
/**
* Convert locale name to hyphenated / all lowercase format.
* @param {String} locale
* @return {String}
* @private
*/
I18nAdapter.prototype._formatLocale = function (locale) {
return locale.replace("_", "-").toLowerCase();
};
/**
* Convert array of locale names to hyphenated / all lowercase format.
* @param {Array.<String>} locales
* @return {Array.<String>}
* @private
*/
I18nAdapter.prototype._formatLocales = function (locales) {
var i;
for (i = 0; i < locales.length; i++) {
locales[i] = this._formatLocale(locales[i]);
}
return locales;
};
window.I18nAdapter = I18nAdapter;
})();
The above adapter wraps the core i18n-js APIs and makes it easy to load message bundles for a given locale. I wanted this functionality to stand on its own, so note that there is no dependency on AngularJS itself in this file.
function initAngular() {
angular.element(document).ready(function () {
angular.bootstrap(document, ["ng-i18n-js"]);
});
}
function initI18n() {
var i18n = new I18nAdapter(["en_US", "de_DE"], "en_US");
i18n.setLocale("en-us", ["i18n/en-us/messages.json", "i18n/en-us/messages2.json"]).then(initAngular);
window.i18nAdapter = i18n;
}
The code above goes in the root HTML page and is used to load the message bundles for the default locale and then bootstrap the Angular application. This ensures all bundles are finished loading before the Angular application inits. This can be important if some application modules require the translations or formatters to be ready during initialization.
(function () {
"use strict";
var module = angular.module("I18nAngular", []);
module.filter("translate", function () {
return function (key) {
return i18nAdapter.translate(key);
};
});
module.filter("number", function () {
return function (value) {
return i18nAdapter.formatNumber(value);
};
});
module.filter("currency", function () {
return function (value) {
return i18nAdapter.formatCurrency(value);
};
});
module.filter("date", function () {
return function (value, format) {
return i18nAdapter.formatDate(value, format);
};
});
})();
The above snippet exposes the i18n-js functionality via a few straightforward AngularJS filters. If other application components require access ot the I18nAdapter itself, it would be easy to add a service or factory here to make the adapter injectable.
If you clone the Github repo and run it under a web server, you can see a very simple AngularJS app making use of this code. Enjoy!