Best practice javascript and multilanguage

When I've built multi-lingual sites before (not very large ones, so this might not scale too well), I keep a series of "language" files:

  • lang.en.js
  • lang.it.js
  • lang.fr.js

Each of the files declares an object which is basically just a map from key word to language phrase:

// lang.en.js
lang = {
    greeting : "Hello"
};

// lang.fr.js
lang = {
    greeting : "Bonjour"
};

Dynamically load one of those files and then all you need to do is reference the key from your map:

document.onload = function() {
    alert(lang.greeting);
};

There are, of course, many other ways to do this, and many ways to do this style but better: encapsulating it all into a function so that a missing phrase from your "dictionary" can be handled gracefully, or even do the whole thing using OOP, and let it manage the dynamic including of the files, it could perhaps even draw language selectors for you, etc.

var l = new Language('en');
l.get('greeting');

There are a few things you need to keep in mind when designing multilanguage support:

1 - Separate code from data (i.e. don't hard-code strings right into your functions)

2 - create a formatting hook function to deal with localization differences. Allowing formattable strings ("{0}") is better than concatenating ("Welcome to" + value), for a lot of reasons:

  • in some languages, a number is formatted like 1.234.678,00 instead of 1,234,567.00
  • pluralization is often not as simple as appending an "s" at the end of the singular
  • grammar rules are different and can affect the order of things so you should allow dynamic data to be appended after the translation hook: for example, "Welcome to {0}" turns into "{0} he youkoso" in japanese (this happens in pretty much every language, mind you).

3 - Make sure that you can actually format strings after the translation hook runs, so you can reuse keys.

4 - Do not, under any circunstance, hook database outputs to the translator utility. If you have multilingual data, create separate tables / rows in your database. I've seen people get this no-brainer wrong fairly often (usually for countries and states/provinces in forms).

5 - Create explicit coding practices rules for creating keys. The formatter utility function (which will look something like translate("hello world") will take a key as a parameter, and keys with slight variations make maintainance very annoying. For instance, you might end up with three keys in the following example: "enter you name", "enter your name:", "enter your name: ". Choose one format (e.g. no colon, trimmed) and catch discrepancies in code reviews. Don't do this filtering programmatically, as it can trigger false positives.

6 - Be mindful that HTML markup could potentially be needed in the translation table (e.g. if you need to bold a word in a sentence, or have footnote medical references). Test for this extensively.

7 - There are several ways of importing language strings. Ideally, you should have multiple versions of a language.lang.js file, switch between them with server side code, and reference the file from the bottom of the HTML file. Pulling the file via AJAX is also an alternative, but could introduce delays. Merging language.js into your main code file is not advisable, since you lose the benefits of file caching.

8 - Test with your target languages. This sounds silly, but I've seen a serious bug once because the programmer didn't bother to check for the existence of "é" in the key.


function Language(lang)
{
    var __construct = function() {
        if (eval('typeof ' + lang) == 'undefined')
        {
            lang = "en";
        }
        return;
    }()

    this.getStr = function(str, defaultStr) {
        var retStr = eval('eval(lang).' + str);
        if (typeof retStr != 'undefined')
        {
            return retStr;
        } else {
            if (typeof defaultStr != 'undefined')
            {
                return defaultStr;
            } else {
                return eval('en.' + str);
            }
        }
    }
}

After adding this to your page, you can work with it like this:

var en = {
    SelPlace:"Select this place?",
    Save:"Saved."
};

var tr = {
    SelPlace:"Burayı seçmek istiyor musunuz?"
};

var translator = new Language("en");
alert(translator.getStr("SelPlace")); // result: Select this place?
alert(translator.getStr("Save")); // result: Saved.
alert(translator.getStr("DFKASFASDFJK", "Default string for non-existent string")); // result: Default string for non-existent string

var translator = new Language("tr");
alert(translator.getStr("SelPlace")); // result: Burayı seçmek istiyor musunuz?
alert(translator.getStr("Save")); // result: Saved. (because it doesn't exist in this language, borrowed from english as default)
alert(translator.getStr("DFKASFASDFJK", "Default string for non-existent string")); // result: Default string for non-existent string

If you call the class with a language that you haven't defined, English(en) will be selected.


Just found a nice article about i18n in javascript:
http://24ways.org/2007/javascript-internationalisation

Although a simple google search with i18n + javascript reveals plenty of alternatives.

In the end, it depends on how deep you want it to be. For a couple of languages, a single file is enough.

You could use a framework like Jquery, use a span to identify the text (with a class) and then use the id of each span to find the corresponding text in the chosen language.
1 Line of Jquery, done.


After reading the great answers by nickf and Leo, I created the following CommonJS style language.js to manage all my strings (and optionally, Mustache to format them):

var Mustache = require('mustache');

var LANGUAGE = {
    general: {
        welcome: "Welcome {{name}}!"
    }
};

function _get_string(key) {
    var parts = key.split('.');
    var result = LANGUAGE, i;
    for (i = 0; i < parts.length; ++i) {
        result = result[parts[i]];
    }
    return result;
}

module.exports = function(key, params) {
    var str = _get_string(key);
    if (!params || _.isEmpty(params)) {
        return str;
    }
    return Mustache.render(str, params);
};

And this is how I get a string:

var L = require('language');
var the_string = L('general.welcome', {name='Joe'});