1
\$\begingroup\$

I had been trying to find a suitable solution to implement localization in my Phonegap/Cordova application. My search proved that any localization through seperate .xml or .json files was a nightmare (mostly due to cross-access-origin).

Because of this, I decided to create a (very) simple, lightweight localization solution.

Here is what I've written:

var locale = {
    getString:function(required){
        var language = localStorage.getItem("language");
        if(language === null){
            language = "nl";
        }

        return window['locale']['translation'][language][required];
    },

    translation: {
        en: {
            play:"Play",
            faq:"FAQ",
            highscore:"Highscore",
            gameStart:"Game starting in ",
        },
        nl: {
            play:"Speel",
            faq:"FAQ",
            highscore:"Highscore",
            gameStart:"Spel start in ",
        }
    }
}

I am really interested in any comments you have. Would there be a better solution or any better methods to use in my implementation?

\$\endgroup\$
  • \$\begingroup\$ "My search proved that any localization through seperate .xml or .json files was a nightmare (mostly due to cross-access-origin)." - is storing json files on the same domain not possible? \$\endgroup\$ – Dan Pantry Mar 28 '15 at 17:14
  • \$\begingroup\$ Storing is possible, but loading them appears to be a nightmare. I might be better with the HTML5 Api, but because of support I had to use a plain XMLHttpRequest. Chrome blocks any XHR cross-domain requests out of the box~ which I assumed also happens on Android. \$\endgroup\$ – Matthijs Mar 28 '15 at 17:17
  • \$\begingroup\$ I don't use either Cordova or Phonegap, so forgive me, but do you have the ability to set headers on the .json files? CORS is not the end of the world as long as you set the appropriate HTTP header on the response. \$\endgroup\$ – Dan Pantry Mar 28 '15 at 17:22
  • \$\begingroup\$ I had actually set the header. During testing in a Chrome browser, it resulted in the following: "Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https, chrome-extension-resource.". I could manually add a parameter for this on my end, but I was unsure about how this would behave on Android. Uncertainty lead to me deciding on the above solution. \$\endgroup\$ – Matthijs Mar 28 '15 at 17:50
  • \$\begingroup\$ Also, it will be slow to access localStorage too frequently \$\endgroup\$ – megawac Mar 29 '15 at 2:46
1
\$\begingroup\$

AFAIK, accessing local files using XMLHttpRequest won't be a problem once you have your page up on a web server.

Anyway, if you decide to go with the solution above, here are some general tips:

  1. getString() method:

    • Use this.translation rather than window['locale']['translation']. There no point traversing to window and back again when you can use the this pointer.

    • Try to be more descriptive when naming variables and methods. For example, the required argument. If a third party user is using your API, how would they pssobile know what to pass? A string? An object?

  2. translation object:

    • Your language objects have some indentical values. To be less repetitive, create a default object and merge with each new language. I will use jQuery.extend() in my example.
  3. Misc:

    • Create an object that keeps track of your constants and reuse it throughout your module.

The code could look something like this:

var locale = {

    constants: {
        language: {
            localStorageKey: 'language',
            default: 'nl'
        }
    },

    getTranslationByLanguage:function(languageProperty){

        if(!languageProperty) 
            throw new ReferenceError('languageProperty not defined');

        var language = localStorage.getItem(this.constants.language.localStorageKey) 
                       || this.constants.language.default;

        var translation = this.translation.getTranslationByLanguageKey(language);

        return translation[languageProperty];
    },

    translation: {

        defaults: {
            faq: 'FAQ',
            highscore: 'highscore'
        },

        en: {
            play:'Play',
            gameStart:'Game starting in '
        },

        nl: {
            play:'Speel',
            gameStart:'Spel start in '
        },

        getTranslationByLanguageKey: function(languageKey){
            return $.extend({}, this.defaults, this[languageKey] || {});
        }

    }
};
\$\endgroup\$
1
\$\begingroup\$

Some thoughts:

  1. Cache your locale, rather than determining it each time through. This will improve performance a bit, but also gives you an extra benefit: if you expose the property, you can change it programmatically in order to test.

  2. Don't use [] when . will work. That is window['locale']['translation'][language][required] should be written window.locale.translation[language][required]. Of course, as @Johan mentioned, you should really use this.translation[language][required] instead.

  3. You may need to consider language & region -- for example, even though Spanish is spoken in many regions, it isn't always spoken the same. Idioms often differ, but so does the way many words are used. The same is, of course, true for English -- UK and US English can be very different.

  4. Your gameStart key is going to produce some difficulties -- some grammars may require your number (or whatever you're using) to be in a different location. I like to use string interpolation for this (for example: gameStart: "Game starts in {{1}}" or the like).

  5. If a locale has no translation matrix, your code will error. Catch this and provide a helpful reason why the failure occurred (probably due to a typo or a translation matrix that failed to load). Alternatively, I tend to fall back to a default locale in these cases, so that at least something will get rendered.

  6. If a key isn't found in the translation matrix, your code won't error, but will return undefined. This might be very confusing. In the past I've returned the key (which becomes obvious when you run the app, because I tend to use SNAKE_CASE keys), but throwing an error might be the better option depending on your needs.

I think I'd do something like this in ES5 (this is similar to what I've used in the past, though I've rethought it quite a bit along the way, so my past translation codes don't match this exactly). In ES6, I may do something very different, but I haven't worked through quite how I want that to work yet using quasi-literals (if that's even wise).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.