5 guests
online

elmasse

Is CSS content Suitable for i18n?

Blog
Posts
Written by elmasse
Dec 13 2010
Internationalization (or i18n) is one of the worst features we have today on Web Application. I'm a big fan of Sencha and I have been reviewing their solution about i18n. I have also created my own solution a few years ago but, I came across both solutions have limitations and problems. Using a different js file with, let's call'em keys, you have to overwrite, for example, a Month name as Month.January so when you want to translate that into Spanish you just have to overwrite that key with
 
    Month.January = 'Enero'

The approach is excellent if you plan to keep that key as a pure variable, or you plan to reload the page in case you want to change the language. Now, what if I just want to change my language 'on the fly'. Ok, you could do that just adding those js files onto your document body or header, as scriptTagProxy does for example to load a new script. At this point everything will work ok. A common practice is to assign a key to somewhere or some property that is not used as a simple var. Let me explain this briefly. Assume we have a TabPanel with a few items on it, each one with its title as a key:
 
setupDemo = function(){
    //Bundle is declared global for this example
    Bundle = {title1: 'title One', title2: 'title Two'}; 

    new Ext.TabPanel({
      items:[
        {title: Bundle.title1, html: '1'},{title: Bundle.title2, html: '2'},
      ]
    });
}

Now, in the way i18n works, we should just add a file on top that overwrites Bundle variable and recall setupDemo function. Check http://www.extjs.com/deploy/dev/examples/locale/multi-lang.html On the example we have a re-rendering of the components but not a reload of the page. Let's forget for a second about re-rendering components. Let's assume that our components will be rendered every time they're displayed. With that approach we don't need to worry about re-rendering and we can change the language just adding a new file on top. This is not completely true. One of the biggest problems with i18n are Strings. If you want to compose a message with more than one key, usually you do
 

   {title: Bundle.key1 + '-' + Bundle.key2}

In the example above we have a problem since now, title is not just a reference to a variable but instead it is a new String composed of two, in fact three, strings. So, do we have a clear and nice way to work with i18n in the way we don't need to reload the page nor re render the components? The answer I found is yes and it's called CSS content. Maybe this is not the clever solution for this problem, but I found that it worked nicely. So, let's go for it! Basically, as I said, I will use CSS content property. We can create a component that will put span tags instead of just plain strings on innerHTML or just HTML. Those tags will be empty and you can simply add a class name, in my case, it is named by an Application Bundle name and the message key.
 

Ext.setup({
	onReady: function(){
	
		globalBundle = 'Some Text';

		var bundle = new Ext.ux.i18n.ContentBundle();
		var tb = new Ext.Toolbar({
			dock: 'top',
			items: [bundle.buildSwitcher()]
		});
		
		var bPanel1 = new Ext.Panel({
			html: bundle.getMsg('a-msg-goes-here') + bundle.getMsg('another-here')
		});
		
		var panel = new Ext.Panel({
			fullscreen: true,
			dockedItems: [tb],
			defaults: {flex: 1},
			items:[bPanel1, {html: globalBundle}, {html: 'HardCoded'}]
		});
		
	}
});

And this is what Ext.ux.i18n.ContentBundle does: The getMsg method will just create a span tag with a class name with application bundle and bundle key.
Ext.ns('Ext.ux.i18n');

Ext.ux.i18n.ContentBundle = Ext.extend(Ext.util.Observable, {
	defaultBundle: 'app-bundle',
	defaultLang: 'en',
	
	path: 'i18n',
	bundleFile: 'app-i18n',
	
	constructor: function(config){
		Ext.ux.i18n.ContentBundle.superclass.constructor.call(this, config);
		this.loadBundle(this.defaultLang);
	},
	
	getMsg: function(msg, bundle){
		bundle = bundle || this.defaultBundle;
		return '';
	},
	
	
	
	buildSwitcher: function(){
		this.select = new Ext.form.Select({
		    options: [
		        {text: 'English',  value: 'en'},
		        {text: 'Spanish', value: 'es'}
		    ],
		    listeners: {
		    	change: this.onLangChange,
		    	scope: this
		    }
		});
		return this.select;
	},
	
	loadBundle: function(lang){
		var url, el, css;
		
		url = this.buildUrl(lang);
		
		el = Ext.get(url);
		if(el) el.remove();
		
		css = document.createElement("link");
		css.setAttribute("href", url);
		css.setAttribute("type", "text/css");
		css.setAttribute("rel", "stylesheet");
		css.setAttribute("id", url);
		
		Ext.getHead().appendChild(css);
	},
	
	buildUrl: function(lang){
		return this.path + '/' + this.bundleFile + '-' + (lang ?  lang : 'def') + '.css';
	},
	
	onLangChange: function(sel, val){
		this.loadBundle(val);
	}
});

And finally, the css files that in this case are located at i18n folder:

app-i18n-en.css

.app-bundle-a-msg-goes-here::after {
	content: 'Message Goes Here.'
}

.app-bundle-another-here::after {
	content: 'Even more Here'
}
and app-i18n-es.css
.app-bundle-a-msg-goes-here::after {
	content: 'El Mensaje va aqui.'
}

This code is built on top of SenchaTouch 1.0.1. The main idea is to have a simple panel, that can be easily changed based on the selected language. The language selector will throw an event that causes the css file to be loaded on top based on the selected language.
This is just an experiment. I'm trying to find a better way to work with i18n and despite CSS shouldn't be aware of content I think that maybe it is useful for this kind of cases.

elmasse

5 guests
online
*/ ?>