Mittwoch, 24. Oktober 2012

Sencha Touch 2 - Simple Localization (i10n)

When I started writing my Sencha Touch 2 App I didn't find a suitable localization solution build into it. Also I didn't want to include another JS library for doing this job. I wanted something integrated directly into the ST2 life cycle. So let me share my simple solution which works fairly well for my case. My solution is split up in two parts. We have Translation.js. A sencha class containing all my translations. And Localization.js doing the actual translation. "Translation.js" has a very simple structure. Basically it acts as a key-value map. All data is marked as static.
Ext.define('MyApp.util.Translations',{
 statics: {
  //list all translated languages
  available: ["EN", "DE"],
  data: {
  "login.label.notamember" : {
   "DE" :  "Kein Mitglied?",
   "EN" :  "Not a member?"
  },
  "checkin.error.nickname" : {
   "DE" :  "Der Spitzname muss zwischen {0} und {1} Zeichen lang sein.",
   "EN" :  "Alias must be between {0} and {1} characters."
  },
  
  //and so on ...
 }
});
Here comes the interesting part "Localization.js". Let me show you the code first and then I'll provide some explanation. Also ist is more or less self documenting.
Ext.define('MyApp.util.Localization', {
 //used as a shorthand
 alternateClassName: ['i10n'],
 requires: ['Ext.String','MyApp.util.Constants', 'MyApp.util.Translations', 'MyApp.util.Configuration'],
 singleton: true,
 config: {
  lang: null  
 },

 constructor: function() {
  //get browser/system locale 
  this.setLang(this.getLanguage());  
 },

 getTranslations: function() {
  return MyApp.util.Translations.data || {};
 },


 /**
 * @private
  * returns the browser language 
  * e.g. DE, EN
  */
 getLanguage: function() {
  var lang;
  //http://stackoverflow.com/questions/10642737/detecting-and-applying-current-system-language-on-html-5-app-on-android
     if (navigator && navigator.userAgent && (lang = navigator.userAgent.match(/android.*\W(\w\w)-(\w\w)\W/i))) {
         lang = lang[1];
     }

     if (!lang && navigator) {
         if (navigator.language) {
             lang = navigator.language;
         } else if (navigator.browserLanguage) {
             lang = navigator.browserLanguage;
         } else if (navigator.systemLanguage) {
             lang = navigator.systemLanguage;
         } else if (navigator.userLanguage) {
             lang = navigator.userLanguage;
         }
         lang = lang.substr(0, 2);
     }

     lang = lang.toUpperCase();

  console.log('browser language: '+lang);

  //check if this language is configured
  if(Ext.Array.indexOf(MyApp.util.Translations.available, lang) == -1) {
   console.log(lang + " not available using default " + appConfig.defaultLanguage);
   lang = appConfig.defaultLanguage;
  } else if(lang === 'undefined'|| lang.length == 0) {
   //use default language
   lang = appConfig.defaultLanguage;
  }

  //set language in configuration
  appConfig.language = lang;

  return lang;
 },

 /**
  * Translates the given key into the corresponding value in selected language.
  * @param key
  *   The key used to find a specific translation.
  *    if the translated string contains placeholders in form of {0}, {1} ... eiter
  *    1. additional parameters with replacing values 
  *    OR
  *    2. an array containing placeholders
  *    can be submited
  * @returns
  *   Translation or key if none was found
  */
  translate: function(key) {
   //alternativ with custom object and no sencha store
  var value = "",
   translations = this.getTranslations();
   if (key && translations[key] && translations[key][this.getLang()] && translations[key][this.getLang()] !== '') {
    value = translations[key][this.getLang()];
    if(arguments.length > 1) {
     //this is a string with placeholders
     //replace key with retrieved value and the call Ext.String.format     
     var _array;
     
     if(Object.prototype.toString.call(arguments[1]) === '[object Array]') {
      _array = new Array();
      _array[0] = value;
      for(var i = 0; i < arguments[1].length; i++) {
       _array[i+1] = arguments[1][i];
      }
     } else {
      arguments[0] = value;
      _array = arguments;
     }                     
     //Documentation for Ext.String.format http://docs.sencha.com/touch/2-0/#!/api/Ext.String-method-format
     //we need apply because we don't know the number of arguments
     value = Ext.String.format.apply(this, _array);
    }
   }
   return (value == "") ? key : value;
  },
});
We mark the class as a singleton and give it an alternate class name so we can easily call it from everywhere.
In the constructor we extract the system language and set in the lang config of our sencha app. If the system language is not translated we fall back to default language. To mark which languages have been translated simply add it to the available property in Translation.js.

The real "magic" ;) happen in translate.
 Translate takes a key and looks it up in your Translation.js. If no translation is found simply the value of key is returned so you can easily see parts of your application which are not translated.
Your translation can contain placeholders in form of {0}, {1}. To supply the placeholders you can pass them either as an array or as normal arguments. I'll rely on a build in Ext Method for the formatting of the string.

And you're done.

Now translating something in your app is as simple as:
  i10n.translate("checkin.error.nickname", 3, 25);