I’ve always thought of resource bundles as one of those things I would implement when I needed them (I’ve never needed to port an application I’ve built to another language). That said, there’s another great benefit to bundles which sometimes gets lost in the crowd. They allow non-developers to change the text displayed in the application. I can’t tell you how many hours\days\weeks I’ve spent making minuscule text changes (only to have them changed back later).
With this new appreciation of the power of resource bundles I’ve started to implement them in the latest application I’m working on. The problem I’ve run into is that they add a tremendous amount of bloat. In this post I’d like to show the technique I’m using to eliminate a lot of the extra code.
Here’s an example of the standard way to use resource bundles (this is a piece of component called MyForm.mxml)
<mx:Metadata> [ResourceBundle("MyForm")] </mx:Metadata> <mx:Script> <![CDATA[ import mx.resources.ResourceManager; ]]> </mx:Script> <mx:Form> <mx:FormItem label="{ resourceManager.getString( 'MyForm', 'firstNameTextInput' ) }"> <mx:TextInput id="firstNameTextInput"/> </mx:FormItem> <mx:FormItem label="{ resourceManager.getString( 'MyForm', 'lastNameTextInput' ) }"> <mx:TextInput id="lastNameTextInput"/> </mx:FormItem> ... </mx:Form>
The main thing that bothers me with the above code is that in most cases the parameters passed to the resourceManager shouldn’t need to be specified. The first parameter, the bundle name, is most likely going to be the same for all of the fields on the page. Additionally, the key for the field can (if you use consistent naming conventions) match the id of the input component.
The heart of this solution is a helper utility class called ResourceUtils.
Here’s our first pass at it.
package { import mx.resources.ResourceManager; public class ResourceUtils { private var _bundleName:String; public function ResourceUtils( bundleName:String ) { _bundleName = bundleName; } public function getString( key:String ):String { return ResourceManager.getInstance().getString( _bundleName, key ); } } }
To use this we’ll simply create an instance of it at the top of our component
<mx:Metadata> [ResourceBundle("MyForm")] </mx:Metadata> <mx:Script> <![CDATA[ import ResourceUtils; [Bindable] private var _rb:ResourceUtils = new ResourceUtils( "MyForm" ); ]]> </mx:Script> <mx:Form> <mx:FormItem label="{ _rb.getString( 'firstNameTextInput' ) }"> <mx:TextInput id="firstNameTextInput"/> </mx:FormItem> <mx:FormItem label="{ _rb.getString( 'lastNameTextInput' ) }"> <mx:TextInput id="lastNameTextInput"/> </mx:FormItem> ... </mx:Form>
By using this class we’ve eliminated the need to specify the bundle name every time we want to look up a key. That’s pretty cool but I think we can do even better. The second part of this solution is going to enable us to auto-magically map the key values to the input components based on the component ids.
Here’s the final version of the ResourceUtils class
package { import mx.containers.FormItem; import mx.core.UIComponent; import mx.resources.ResourceManager; public class ResourceUtils { private var _bundleName:String; public function ResourceUtils( bundleName:String ) { _bundleName = bundleName; } public function getString( key:String ):String { return ResourceManager.getInstance().getString( _bundleName, key ); } public static function setLabels( container:UIComponent, bundleName:String = null ):void { if (!bundleName) { bundleName = container.className; } setChildLabels( container, bundleName ); } private static function setChildLabels( container:UIComponent, bundleName:String ):void { for (var x:uint; x < container.numChildren; x++) { var child:Object = container.getChildAt( x ); if (child is UIComponent) { ResourceUtils.setChildLabels( child as UIComponent, bundleName ); var label:String = ResourceManager.getInstance().getString( bundleName, child.id ); if (!label) { continue; } if (UIComponent( child ).parent is FormItem) { var formItem:FormItem = UIComponent( child ).parent as FormItem; formItem.label = label; } else if (child.hasOwnProperty( "prompt" )) { child.prompt = label; } else if (child.hasOwnProperty( "label" )) { child.label = label; } else if (child.hasOwnProperty( "text" )) { child.text = label; } } } } } } [/sourcecode] In the final version of the class we've added a new public static function called setLabels. This will recursively loop through all of the forms children and check for any components for which a key has been set in our resource bundle file. If we find a value we'll check if it looks like something we can set the label for. Here's an example of how we'd use this class [sourcecode language='jscript'] <mx:Metadata> [ResourceBundle("MyForm")] </mx:Metadata> <mx:Script> <![CDATA[ import ResourceUtils; // call this function in the initializer handler for the component private function init():void { ResourceUtils.setLabels( this ); } ]]> </mx:Script> <mx:Form> <mx:FormItem> <mx:TextInput id="firstNameTextInput"/> </mx:FormItem> <mx:FormItem> <mx:TextInput id="lastNameTextInput"/> </mx:FormItem> ... </mx:Form>
If you scroll back to the top of the page you’ll see that we’ve removed the need to reference not just the ResourceManager but the labels as well.
In practice you’d probably want to use a combination of the static setLabels function as well instantiating an instance of ResourceUtils depending on whether it makes sense for the labels to be auto-magically mapped. Also, be careful to only use the setLabels functions on your lower level child components. If you used it in your main application class (and you have a large application) it’s going to recurse it’s way through every component in your application which probably won’t be too efficient.
Hope you find this approach useful
Best,
Hillel
nice! this is definitely helpful ^_^
You write very well.
hi hellel,
Its pretty gud.
Im using the resourceManager.loadResourceModule(resourceModuleURL);
in my main application .In the modules Iam not using this below tag
[ResourceBundle(“messages”)]
But still my application is loading correctly the messages from the messages.properties file.If so Whats the purpose of this metadata tag.
10x in deed. Can u please make clear that.
Selva,
I guess using the resource manager to load the module eliminates the need for the metadata. In my example I’m calling the getString method without loading a resource module which requires using the metadata tag.
hi hillel,
thanx a lot hillel.
In that case which approach is better one.
Also my application will have some 6 or 7 languages .
Also im using modules() in my application.
So Can u plz suggest which approach is better.
cheers
selva
Selva,
If you’ll be using multiple locals resource modules seem like a goo choice
http://livedocs.adobe.com/flex/3/html/help.html?content=l10n_5.html
Did somebody test if your approach works with changing the language while the app runs?
I’m trying an similar approach, but the GUI only changes all the labels when I use a direct binding with resourcemManager.getString(…), not with any other combination I used.
Any Idea what I’m doing wrong?
Thx Karfau
You need to override resourcesChanged() and call “setLabels” again in it:
// Automatically called when language is changed
override protected function resourcesChanged():void {
super.resourcesChanged();
ResourceUtils.setLabels( this );
}
If you cannot override resourcesChanged() (you are not in a UIComponent context), you can listen to the “change” event on the “resourceManager” singleton to know when the function should be called again.
i found a little problem in this solution,
if i want to use 2 components with the same text i´ll have to duplicate the resource keys, otherwise i would have two components with the same id, an off course that´s impossible.
Nice code. But I have a error while trying
Unable to resolve resource bundle “it_TA” for locale “en_US”
it_TA is my property file name. It consists tamil(one indian language) language values
Gobi,
I’d sugget trying to use the resource bundle w/o my class to see where the problem is.