Saturday, May 26, 2012

Localizing an HTML5 application using JavaScript resource files

In my previous post I have been writing about server-side localization in ASP.NET MVC. In one of my HTML5 applications, that is cached for offline use, localization resources are also accessed client-side, from JavaScript. In this post I will propose a simple client-side localization system that I have created for that purpose. The localization resources will be defined in JavaScript (JSON) files, one for each language that we want to support. Depending on a language variable the correct file is dynamically loaded at the application start. To refer to a specific resource from within HTML elements I have borrowed a method from ASP.NET that I have mentioned at the beginning of my previous post. A resource key attribute is added to each HTML element that I want to localize. HTML5 allows for custom element attributes whose names start with the 'data-' prefix, so I have named the resource key attribute 'data-res'. Here is an example of a localized HTML form button:
<input type="submit" value="Submit" title="Send your message"
    data-res="btnSubmit" />
The 'data-res' attribute refers to a key in a JSON resource file. A French resource file may look like this:
var resources = {
    "btnSubmit.value": "Envoyer",
    "btnSubmit.title": "Envoyez votre message",
    "ConfirmText": "Etes vous sûr ?"
}
Using a single key ('btnSubmit' in the example) we can translate multiple properties on a single HTML element, e.g. the value and title of a button. Note the key 'ConfirmText', which is an example resource that does not apply to an HTML element but is used by some JavaScript application code directly. I will illustrate this later.
The resource strings for the default language (English in our case) should already be included in the HTML page. Those are applied by the browser automatically if we do not override them in a resource file. This makes the HTML page much better readable and testable, since no resource file interpreter is in place yet.
The resource file that matches the user's current language is loaded at the application start. We assume that there is some init JavaScript method that is called when the window 'load' event fires. We add the following code to that method:

// Try to get language info from the browser.
var lang = window.navigator.userLanguage || window.navigator.language;
if (lang) {
    if (lang.length > 2) {
        // Convert e.g. 'en-GB' to 'en'. We do not support
        // resources for specific cultures at the moment.
        lang = lang.substring(0, 2);
    }
    // Include the languages that we want to support
    // in the following condition.
    // If we do not support the current navigator language,
    // default to english.
    if (lang != 'en' && lang != 'fr' && lang != 'nl') {
        lang = 'en';
    }
}
else {
    // Default to english if we did not succeed in getting
    // a language from the browser.
    lang = 'en';
}
// Construct a language-specific resource path.
var resourcePath = 'resources/strings.' + lang + '.js';
// Get a reference to the HEAD element of the HTML page.
var head = document.head || document.getElementsByTagName('head')[0];
// Dynamically add the resourcePath to the HEAD element
// to start loading the resources.
var scriptEl = document.createElement('script');
scriptEl.type = 'text/javascript';
scriptEl.src = resourcePath;
head.appendChild(scriptEl);

After this code has been run, a global JavaScript variable resources will exist that our resource interpreter can parse. Here is how that interpreter looks like:

// Get all HTML elements that have a resource key.
var resElms = document.querySelectorAll('[data-res]');
for (var n = 0; n < resElms.length; n++) {
    var resEl = resElms[n];
    // Get the resource key from the element.
    var resKey = resEl.getAttribute('data-res');
    if (resKey) {
        // Get all the resources that start with the key.
        for (var key in resources) {
            if (key.indexOf(resKey) == 0) {
                var resValue = resources[key];
                if (key.indexOf('.') == -1) {
                    // No dot notation in resource key,
                    // assign the resource value to the element's
                    // innerHTML.
                    resEl.innerHTML = resValue;
                }
                else {
                    // Dot notation in resource key, assign the
                    // resource value to the element's property
                    // whose name corresponds to the substring
                    // after the dot.
                    var attrKey = key.substring(key.indexOf('.') + 1);
                    resEl[attrKey] = resValue;
                }
            }
        }
    }
}

The interpreter loops through all HTML elements that must be localized. A resource value is assigned to an element property that is specified after a dot (e.g. 'btnSubmit.value' or 'btnSubmit.title'), or to the element's innerHTML if there is no dot in the key.

The 'ConfirmText' example that I have mentioned earlier may be used as follows:
var confirmText = resources.ConfirmText;
    // or resources['ConfirmText'] if you prefer.
if (confirm(confirmText )) {
    // Do something that needs confirmation.
}
The method described in this post makes localizing an HTML app or page pretty easy. I think the method is limited (for instance, we do not take specific cultures like 'en-GB' into account) but effective.

1 comment:

  1. Nice job...this helped a lot. Thank you. One way to simplify the method setting the values:

    var resElms = document.querySelectorAll('[data-res]');
    for (var n = 0; n < resElms.length; n++) {
    var elem = resElms[n];
    var resKey = elem.getAttribute('data-res');
    if(resKey)
    elem.innerHTML = resources[resKey];
    }

    ReplyDelete