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);

Freitag, 2. März 2012

Overwrite RestProxy in Sencha Touch 2

Here are the steps how you can override Sencha Touch functionalities. In this simple case I override the buildUrl method in Ext.data.proxy.Rest to customize the URL for my needs. Create a new class. There is no naming convention although I think it is useful to use the naming scheme from the class you are overriding. So in my case I'm using MyApp.data.proxy.CustomRestProxy. Instead of extend you'll then have to use override to specify the class whose functionality you want override.

Ext.define('MyApp.data.proxy.CustomRestProxy', {
 override: 'Ext.data.proxy.Rest',
 
   buildUrl: function(request) {
     //here I simply add an url prefix to all rest calls
         var  me = this, _serviceUrl = globalConf.serviceUrl, url = me.getUrl(request);
         request.setUrl(_serviceUrl + url);

         return me.callParent([request]);
     }
});
Put this class in a folder according to the name.
Mine is located in app/data/proxy/CustomRestProxy.js

 In your app.js make sure to set this class as required.
requires: ['MyApp.data.proxy.CustomRestProxy'] 


When you start your App and Sencha complains it can't find your new class you have to tell Sencha where to look.

Put
Ext.Loader.setPath('MyApp', 'app'); 
Before you call
Ext.application ...

Dienstag, 14. Februar 2012

How to access a remote git repository in your local network?


I don't always want to push my changes to my online repository, just to pull them
an instant later when I switch the machine I'm working with.

So how to access a git repository on a machine in the same network?
(I'm working on Mac OS but this should be applicable for Linux as well.)

First make sure ssh is enabled and the user you log in with, has access to the directory
where the remote repository is located.

Then run

git pull git+ssh://USER@IP_OF_REMOTE_MACHINE/PATH_TO_REPO BRANCH

e.g.

git pull git+ssh://fred@fred-air.local/~/my/repo master

Maybe you'll get following exception:

bash: git-upload-pack: command not found
fatal: The remote end hung up unexpectedly

This stackoverflow thread pointed me into the right direction.
http://stackoverflow.com/questions/225291/git-upload-pack-command-not-found-how-to-fix-this-correctly

In my case I had to edit the .bashrc on the remote machine located
in the home directory of the user I logged in with.

I added git-upload-pack located in /usr/local/git/bin to the PATH and it worked like
a charm.