Dealing with localized applications in Flex 2

When talking about application localization, we should distinct two kinds of data :

1. Remote data that come, say, from a database, is dynamic by nature, and should then be localized via your application server logic.
2. User Interface data, however, is very different. Most of the time, it is set statically during development. It is spread all over the application, making it quite difficult to treat as normal "business" data.

Flex 2 deals with application UI localisation via static BundleRessources. There are several limits to this approach.

First, the nature of the translation data : ressource bundle files are (as far as I know) the way java deals with application localization. While this is good, as it so can be considered as a standard, it's definitely not the unique standard.

There are many other options when choosing translation file formats, the first of which being XLIFF.

http://docs.oasis-open.org/xliff/xliff-core/xliff-core.html

All in all, it would be better if Flex application developers could have the choice over what kind of file format to use.

The second limit is the fact that Flex 2 bundle ressources are intended to be static. It's supposed to be compiled with your application, hence forbidding you to dynamically load them at run time.

We should mention that there are some workaround to this :
http://mannu.livejournal.com/300260.html

At its best, a localisation strategy should allow you to create only one application that would be able to not only load the desired localized data at start up, but also to change the current language at runtime.

Please note that Flex 3 has partially adressed this problem. Eric Feminella has created an API, which you can check here:
http://www.ericfeminella.com/blog/2007/09/01/flex-3-resourceinspector/

While there is definetly no "best way" to achieve this, we can imagine what a simple implementation could look like.

In this example, all translation data will be stored in a single XML file. Yet, we could easily change this and use multiple translation files (ie one per language).

This is what my data looks like :

<?xml version="1.0" encoding="utf-8"?>
<lang>
	<main>	
		<item id="language">			
			<en>english</en>
			<fr>french</fr>
		</item>				
	</main>
 
	<navigation>
		<item id="saveBtn">
			<fr>Enregistrer</fr>
			<en>Save</en>
		</item>
 
		<item id="prodTitle">
			<fr>Produit</fr>
			<en>Product</en>
		</item>
 
	</navigation>
 
</lang>

What we'll need then is an object that is capable of setting the current language, storing the translated data and spread this data all over our application.

This object will have a method that is able to return a localized string when provided with the adequate item ID.

Since we want all localizable strings to be set dynamically at runtime, we're going to use dataBinding for each and every one of them. This means that our views will have to store a reference to the Localizer object.

But what if our translation file format were to change later in our development process? We would have to re-write the Localizer so that its getString() method can perform the requested parsing. But all our views have a reference to the Localizer, and we definetly don't want to have to re-code every single view in our application.

In a word, to provide greater flexibility to our application, we need to de-couple the Localizer and its users. The best way to achieve this is to use an Interface that our Localizer will have to implement.

package com.dehats.localize
{
	import flash.events.IEventDispatcher;
 
	/*
		Interface to implement
		so that the views can access localized data
	*/
 
 
	[Event(name="change")]
	public interface ILocalizer extends IEventDispatcher
	{
		function get language():String;
		function set language(pLang:String):void;
 
		function get langXML():XML
		function set langXML(pXML:XML):void
 
		[Bindable("change")]
		function getString(pKey:String):String;
 
	}
}

This way, our views will maintain a reference to the Localizer via a property which type will be that of the interface, not the Localizer.

The easiest way to have an object accessible from anywhere in our application, and in the meantime make sure that there is only one instance of this class in our application, is to create a Singleton.

This is what my Localizer class looks like :

package com.dehats.localize
{
	import flash.events.EventDispatcher;
	import flash.events.Event 
 
	/*
		Implementation of ILocalizer.
 
		Each translation file format should have its own Localizer, 
		implementing ILocalizer.
 
		In this example, we'll get the translated data by searching
		the XML nodes by an attribute named id, which is our key,
		using E4X.
 
	*/
 
	[Event(name="change")]	
 
	[Bindable]
 
	public class Localizer extends EventDispatcher implements ILocalizer
	{
 
		private var _language:String;
		private var _langXML:XML;
 
 
		private static var instance:Localizer;
 
		public function Localizer()
		{
			if(instance!=null) throw new Error("Localizer is a Singleton");
		}
 
		public static function getInstance():Localizer
		{
 
			if(instance==null) instance=new Localizer();
			return instance ;
		}
 
		/*
			string search method
		*/
		[Bindable("change")]
        public function getString(pKey:String):String
        {
		// string to display while waiting for data to load       	
        	if(langXML==null) return "...";
 
        	var s:String = langXML..item.(@id==pKey)[_language];
 
        	// If there is no translation available, return the key
        	if(s=="") return pKey ;	        	
 
        	return s;
        }		
 
	   	public function get langXML():XML
	   	{
	   		return _langXML;
	   	}
 
	   	public function set langXML(pXML:XML):void
	   	{
	   		_langXML = pXML;
	   		dispatchEvent(new Event("change"));
	   	}
 
 
	   	public function get language():String
	   	{
	   		return _language;
	   	}
 
	   	public function set language(pLg:String):void
	   	{
	   		_language = pLg;
	   		dispatchEvent(new Event("change"));
	   	}
 
	}
}

Now we can create our views. In this example, I have created a separate view (mxml component) so that we can see our loose coupling between it and the Localizer.

This is my main application file. It's the only view that has a reference to the Localizer class. It will pass it to its views by databinding.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:view="view.*" creationComplete="start()" width="691">
 
	<mx:Script>
		<![CDATA[		
			import com.dehats.localize.Localizer;
 
			[Bindable]
			public var myLoc:Localizer = Localizer.getInstance();
 
			private function start():void
			{
				myLoc.langXML = this.langData ;
				myLoc.language = "fr";
 
			}
 
		]]>
	</mx:Script>
 
	<mx:XML id="langData" source="locale/lang.xml" />
 
	<view:LPanel x="10" y="10"
		loc="{myLoc}">
	</view:LPanel>
	<mx:Button click="myLoc.language='fr'" x="10" y="318" label="French"/>
	<mx:Button click="myLoc.language='es'" x="10" y="378" label="Spanish (not translated)"/>
	<mx:Button click="myLoc.language='en'" x="10" y="348" label="English"/>
 
 
</mx:Application>

And finally, my localizable view :

<?xml version="1.0" encoding="utf-8"?>
<mx:Panel title="{loc.getString('prodTitle')}" xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="400" height="300">
 
	<mx:Script>
		<![CDATA[
			import mx.utils.StringUtil;
			import flash.utils.getQualifiedClassName;
			import flash.utils.describeType;
 
 
			import com.dehats.localize.*;
 
			[Bindable]
			public var loc:ILocalizer ;
 
 
		]]>
	</mx:Script>
 
	<mx:Button x="10" y="10" label="{loc.getString('saveBtn')}"/>
 
</mx:Panel>

I should probably say that another approach could have been to have public properties in my views for each and every string to be translated. But, in my humble advice, it wouldn't be a great choice since we could end up with dozens of properties, hence polluting the rest of our code.

Again, I'm not saying that this is the only solution to the localization issue, nor the best. I just think it's a convenient way to put this.

Please feel free to tell me what you think about it.

Oh and here is the archive file. ;)
http://www.dehats.com/drupal/files/Localisation.zip

AttachmentSize
Localisation.zip374.08 KB

Anonymous on April 14th 2008

Hi, Thanks a lot for the great tutorial. I have an issue though, and I'd like to know what u think about it. I have a login event, with a Data Transfer Object which stores my user's information (like userid, language, ...). So my InitApp() check if the user is logged in, and then when the user logs in, I call a function that send the myLoc properties. But when my panel shows up, the title is ... I figured out that the panel was created before the langData file is send to the Localizer, but even if I create a fonction that set those properties after the login event, nothing changes. But if I create a button, calling the same function, it works fine. So, to be clear: #THE MAIN APP# <?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:forms="forms.*" initialize="initApp()" xmlns:views="views.*"> <mx:Script> <![CDATA[ import com.dehats.localize.Localizer; import mx.controls.Alert; import dto.User; import mx.rpc.events.ResultEvent; [Bindable] private var currentUser:User = new User(); private function loginResultHandler(event:ResultEvent):void { currentUser = event.result as User; if(currentUser.loggedIn) { createUI(); currentState = ''; } else { Alert.show("There has been a login error","Login Error!"); } } private function initApp():void { if(currentUser.loggedIn) { currentState = ''; } else { currentState = 'loginState'; } } [Bindable] private var myLoc:Localizer = Localizer.getInstance(); private function createUI():void { myLoc.langXML = this.langData; myLoc.language = currentUser.language; } ]]> </mx:Script> <mx:XML id="langData" source="locale/lang.xml" /> <mx:RemoteObject id="cfService" source="cfc.flexdev.SecureService" destination="ColdFusion"> <mx:method name="loginUser" result="loginResultHandler(event)" fault="Alert.show(event.fault.message)"/> </mx:RemoteObject> <mx:states> <mx:State name="loginState"> <mx:RemoveChild target="{myPanel}"/> <mx:AddChild> <forms:LoginForm verticalCenter="0" horizontalCenter="0" login="cfService.loginUser(event.loginObject.username, event.loginObject.password)"/> </mx:AddChild> </mx:State> </mx:states> <views:myPanel id="myPanel" horizontalCenter="0" verticalCenter="0" loc="{myLoc}"> </views:myPanel> <mx:Button click="createUI()" label="French" y="485" horizontalCenter="0"/> </mx:Application> My User info are: currentUser.language=en currentUser.loggedIn = true (when login has succed) And the panel is exactly the same as yours. Any ideas? Jeremie jeremie@eremiya.com

Vladimir (not verified) on March 09th 2009

I found out with debug halp that on SWF loading getString for key saveBtn calls several times ! . And it depends on form complexity. If we have several nested containers before this Button calling count is more than in simple case (like in this example). How you can explain it and what is the solution to avoid it ?