Skip to content

A JavaScript i18n Library for HTML 5 Applications (Part 2)

This is Part 2 in the series of describing an approach that can be adopted to provide internationalization and localization support to web applications, specifically client-side intensive web applications. The original intent for this series was to look at the subject matter in terms of modern web-applications, but there’s no reason why this can’t be applied to traditional (non-application like) and mobile websites as well.

Translating software can be a lot of work and its process begs to ask several different questions such as:

  1. How do I support multiple languages in my application?
  2. What changes do I require to support multiple languages?
  3. What language should I start with?

This blog entry discusses some basic concepts as well as provides a few examples of the concepts in practice. While it is certainly not my intent to cover every detail, I hope it will give you an idea of what’s involved and get you started along the process with your own work.

HTML5-i18n

Some Basic Terminology

There are a couple of basic things to understand though, before you create a multilingual application. Let’s agree on some basic definitions as these terms are often used interchangeably.

  • Internationalization (i18n) – the process of enabling the application (e.g. backend of a website) to handle different languages, character sets, currencies, submit form data, search capabilities, etc.
  • Localization (L10n) – involves translating the application (usually the front-end) into different languages ensuring all content (text and graphics) is translated in an accurate and culturally correct manner. It also ensures that appropriate locale of your target audience is chosen, for instance the Brazilian Portuguese language in contrast to the continental Portuguese language.
  • Globalization – The combination of Internationalization and Localization
  • Language – For example, English (ISO Code “en”), Spanish (ISO code “es”), etc.
  • Locale – Canada. Note that French in France is not the same as French in Canada, e.g. “fr-FR” vs. “fr-CA”

Using the simple i18n JavaScript Library – continued

The resource files nomenclature

Ideally the generated JSON-based resource file can be used when rendering static messages, dynamic messages, dialogs, etc. within the UI of the web application.

Consider the following example of a JSON-based resource file we introduced in Part 1 of this series:

English Version (strings.en-US.json) Spanish Version (strings.es-AR.json)
[code] {
“btnSubmit__value” : “Hello English”,
“btnSubmit__title” : “Hello English”,
“registrationForm” :
{
“firstName” : “First Name”,
“lastName”  :  “Last Name”,
“register__value”: “Register”,
“register_title” : “Click to Register”
},
“loggedIn”:
{
“welcomeMessage” : “Welcome %s %s!”
}
}
[/code]
[code] {
“btnSubmit__value” : “Hola Inglés”,
“btnSubmit__title” : “Hola Inglés”,
“registrationForm” :
{
“firstName” : “Nombre”,
“lastName”  :  “Apellido”,
“register__value”: “Registrarse”,
“register_title” : “Haga clic para registrarse”
},
“loggedIn”:
{
“welcomeMessage” : “Bienvenido %s %s!”
}
}
[/code]

The following table describes the type of resource strings and how they are used within the i18n library:

Entry Resource String Type Usage
btnSubmit__value Attribute [code] i18n._(“btnSubmit__value”) [/code]
registrationForm Group [code] None. Exists for grouping related items. [/code]
firstName String [code] i18n._(“registrationForm.firstName”) [/code]
welcomeMessage Formatted String [code] var name = $(‘#txtLastName’).val() + ‘, ‘ + $(‘#firstName’).val();
i18n._(“loggedIn.welcomeMessage”, name); [/code]
Attribute Resource Type

In your web application user interface (UI), there are many components which will contain text (which require translation) that when once loaded remains unchanged (i.e. static). The attribute resource type provides a way to help load your translated text items (e.g. button text, labels, column headings, etc.) at application start-up time.

From your client-side mark-up you can simply add the attribute “data-res” with the appropriate resource entry as the key and the library will automatically load the translated text as the inner html of the DOM element.

However, if the resource key entry adheres to the following naming convention:

[code] <resource-entry-name>__<dom-attribute-name> [/code]

The library will load the translated text into the attribute name rather than using the default “innerHTML” property.

Group Resource Type

This type of entry is used to group items belonging to a similar context together.  This allows us to avoid collisions of similar “key” references being made. For example, your web application may have several uses for a given string (e.g. key-> “all”). Although in English there is a single word for this key, in another language there may be multiple options available given the context in which the word is being use (e.g. In Spanish “todos”, “todas”).

Just as maleness or femaleness is an inherent characteristic of human beings and most animals, so is gender an inherent characteristic of nouns in some languages. Depending on the context in which a word is being used, it may be more linguistically accurate to use the male form versus the female form or vice versa.

As such, we can use the Group resource type to create context and re-use the same key in each of the different contexts. That way, we get the appropriate translations for each of the different languages our application supports.

String / Formatted String Resource Type

This JavaScript i18n library automatically loads localization packages (i.e. bundles) based on the user’s language preference. Before you can do any begin rendering multiple language versions you have to initialize the library with a ‘dictionary’ (basically a property list mapping keys to their translations).

Once the initialization process has completed, you can ‘lookup’ string resources using their key names as shown below:

[code] var fName = i18n._(“registrationForm.firstName”); var lName = i18n._(“registrationForm.lastName”); [/code]

String resources are static in nature and will always produce the same result of the current active language.

On the other hands, Formatted String resources, are dynamic and allow you to include parameters at run-time which in effect allows you to create custom string values. Formatted String resources are used just like regular String resources except that you include parameters in the method invocation. See example below:

[code] var fullName = i18n._(“loggedIn.welcomeMessage”, fName, lName); [/code]

This version replaces any format items in a target specified string with the string representation of three specified objects. The target specific string is represented by the result of looking up the key “loggedIn.welcomeMessage”. This key produces the result “Welcome %s %s!”. The “%s” represent format items which will be replaced by the values of the parameters “fName” and “lName”.

A simple i18n JavaScript Library Implementation – Updated

[code] var i18n = (function () {
var defaultPackage = ‘default’,
currentCountry, currentLanguage, resources;

function resetStaticElements() {
$(‘[data-res]’).each(function() {
var $el = $(this);
var resKey = $el.attr(‘data-res’);
var localizedData = getString(resKey);

if (resKey.indexOf(‘__’) == -1) {
$el.html(resValue);
} else {
var attrKey = key.substring(key.indexOf(‘__’) + 2);
$el.attr(attrKey, resValue);
}
});
}

function loadResources(localeID) {
var resourcePath = ‘i18n-resources/strings.’ + localeID + ‘.json?r=’ + (new Date().getTime());
return $.getJSON(resourcePath).promise();
}

function setupLocalizedResources(languageResources, newlanguageResources) {
for (var property in languageResources) {
if (newlanguageResources.hasOwnProperty(property)) {
if (!languageResources[property]) {
languageResources[property] = newlanguageResources[property];
} else {
if (typeof languageResources[property] == ‘object’) {
setupLocalizedResources(languageResources[property], newDefaults[property]);
}
}
}
}
}

function printf(input, args) {
if (!args) return input;

var ret = ”;
var searchRegex = /%(\d+)\$s/g;

var matches = searchRegex.exec(input);
while (matches) {
var index = parseInt(matches[1], 10) – 1;
input = input.replace(‘%’ + matches[1] + ‘\$s’, (args[index]));
matches = searchRegex.exec(input);
}
var parts = input.split(‘%s’);

if (parts.length > 1) {
for (var i = 0; i < args.length; i++) { if (parts[i].length > 0 && parts[i].lastIndexOf(‘%’) == (parts[i].length – 1)) {
parts[i] += ‘s’ + parts.splice(i + 1, 1)[0];
}
ret += parts[i] + args[i];
}
}

return ret + parts[parts.length – 1];
}

function getString(key) {
var keyItems = key.split(‘.’);
var obj = resources;
for (var i = 0; i < keyItems.length; i++) { obj = obj[keyItems[i]]; if (!obj) { break; } } if (typeof obj === ‘string’) { return obj; } else { return ”; } } function getFormattedString(key) { var val = getString(key); if (typeof val === ‘string’) { if (arguments.length > 1) {
var formatArgs = Array.prototype.slice.call(arguments, 1);
return printf(val, formatArgs);
} else {
return val;
}
} else {
return ”;
}
}

function setCurrentLocale(language, country, successCallback) {
currentCountry = country || ‘unknown’;
currentLanguage = language || ‘unknown’;

// TODO: provide better support for default fallback languages
if (currentCounrty === ‘unknown’ || currentLanguage === ‘unknown’) {
currentCountry = ‘US’;
currentLanguage = ‘en’;
}

var dfd1 = loadResources(currentLanguage + ‘-‘ + currentCountry),
dfd2 = loadResources(currentLanguage),
dfd3 = loadResources(defaultPackage);

$.when(dfd1, dfd2, dfd3)
.then(function (json1, json2, json3) {
resources = {};
setupLocalizedResources(resources, json1);
setupLocalizedResources(resources, json2);
setupLocalizedResources(resources, json3);

resetStaticElements();

if (callback) {
callback();
}

}, function () {
// TODO: implement error handling for loading of resources
console.log(‘Error loading resources…’);
});
}

return {

/**
* Replaces each format item in a specified localized string with the
* text equivalent of a corresponding arguments (after key).
* e.g. key = ‘helloFirstNameLastName’
localised value of key = “Hello %s %s!”
* _(‘helloFirstNameLastName’, ‘John’, ‘Smith’);
* returns “Hello John Smith!”
*
* @method _
* @param {key} The unique identifier for the resource name (using object notation).
* @return {String} Returns the localized value based on the provided key and optional arguments.
*/
_: getFormattedString,

/**
* Sets the current language/locale and does any application initialization after loading language.
*
* @method load
* @param {language} The two-letter ISO language code.
* @param {country} The two-letter ISO conuntry code.
* @param {successCallback} The function to be called when the language/locale has been loaded.
*/
load: setCurrentLocale,

getCurrentCountry: function() {
return currentCountry;
},

getCurrentLanguage: function() {
return currentLanguage;
}
};
}());
[/code]