For over 5 years, I have been a Cairngorm proponent and evangelist. During the last 6 months I have discovered a new framework passion called “Swiz.” Inversion of control, metadata processors, dependency injection… these are just some of the amazing features available in the Swiz framework.
With the release of Swiz 1.0rc1, several developers asked questions regarding the best way to use the BabelFx Framework (aka l10nInjection) to add multi-language features to Swiz RIA applications. So here is the modified version of the Swiz Flex 4 CafeTownsend application… now with extra features for Logging and BabelFx.
Using BabelFx, the Swiz-version of CafeTownsend demo now supports both Spanish and English locales. And when you look at the code, you will see that none of the view code has been changed… yet click on the map buttons and the locale changes magically work. If you run the application from in debug mode, the trace console will show a full log output of the application activity.
Add-in the BabelFx framework with custom Swiz Logging and… wow! you will be amazed. With Swiz and “inversion of control”, the code for my own applications has become so clean and robust. Can’t you tell that I love Swiz?
The CafeTownsend application leverages Swiz v1.0rc1 and the [Inject] metadata tag functionality to inject models into controllers and views. Now do you recall that the BabelFx library also listens for “creationComplete” of GUI instances and injects localized resources into those target instances?
Did you know, however, that the BabelFx can also inject into data models and controllers? With Swiz it is almost trivial to accomplish that. Simply add the BabelFx LocalizationMap to the beanProvider list and then let Swiz Inject model instance references into your custom LocalizationMap. Below is a live demo of the Swiz, multi-language CafeTownsend application; with attached source code. Simply right-click to browse or download the source.
In order to add l10n features to the FlexStore, I needed to perform three (3) tasks in order to add multi-language features to the CafeTownsend application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | <?xml version="1.0" encoding="utf-8"?> <l10n:LocaleMap enableLog="true" xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:l10n="library://ns.mindspace.com/l10n/flex/" > <mx:Metadata> <!-- References to bundles so embedding process works; if runtime loaded, then disable this section --> [ResourceBundle("login")] [ResourceBundle("employees")] [ResourceBundle("language")] </mx:Metadata> <mx:Script> <![CDATA[ import com.cafetownsend.domain.Employee; import com.cafetownsend.model.AppModel; import com.cafetownsend.model.Constants; import com.cafetownsend.presentation.LoginPresentationModel; import com.cafetownsend.view.EmployeeDetail; import com.cafetownsend.view.EmployeeDetailNavigation; import com.cafetownsend.view.EmployeeListNavigation; import com.cafetownsend.view.LoginView; import com.cafetownsend.view.MainView; import l10n.views.LanguageBar; import mx.containers.Form; import mx.resources.IResourceManager; import mx.resources.ResourceManager; // ******************************************************************************** // Public Mutators and Properties (injected by Swiz) // ******************************************************************************** /** * To support l10n injection into non-displayObjects (e.g. data models) * the LocaleMap must have instance references. So we use Swiz to inject * a reference to each model bean instance that will be used or modified * when locales change. * */ [Bindable] [Inject("employeeModel.selectedEmployee")] public function get selectedEmployee():Employee { return _selectedEmployee; } public function set selectedEmployee(value:Employee):void { _selectedEmployee = value; Constants.EMPLOYEE_CONFIRM_DELETE = buildConfirmMessage(); } [Bindable] [Inject] public var authenticated : AppModel = null; [Inject] public var model : LoginPresentationModel = null; // ******************************************************************************** // Private Methods // ******************************************************************************** /** * Manual injections into static variables stored in the Constants class. * */ private function onLocaleChanged(event:Event):void { var sri : SmartResourceInjector = event.target as SmartResourceInjector; var cntr : LoginView = sri.targetInstances[0] as LoginView; if(cntr != null) { /** * BabelFx or l10nInjection supports injection into both GUI and non-GUI instances (data models, * controllers, etc). But static variables cannot be directly modifed with injection. * This is manual process; shown below. */ Constants.LOGIN_FAILED_MESSAGE = _rMngr.getString("login","login.error.noMatch"); Constants.LOGIN_FAILED_TITLE = _rMngr.getString("login","login.error.title"); Constants.LOGIN_INVALID_USERNAME = _rMngr.getString("login","login.error.usernameRequired"); Constants.LOGIN_INVALID_PASSWORD = _rMngr.getString("login","login.error.passwordRequired"); Constants.EMPLOYEE_LOAD_ERROR = _rMngr.getString("employees","users.unableToLoad"); Constants.EMPLOYEE_INVALID_FIRSTNAME= _rMngr.getString("employees","editor.invalid.firstName"); Constants.EMPLOYEE_INVALID_LASTNAME= _rMngr.getString("employees","editor.invalid.lastName") ; Constants.EMPLOYEE_CONFIRM_DELETE = buildConfirmMessage(); cntr.loginErrorTxt.text = ""; // Manually clear the error text... cntr.loginBtn.skin.invalidateProperties(); // Force refresh on custom skin part if (model != null) { model.usernameError = ""; // Clear all validation errors... model.passwordError = ""; } } else { var main : MainView = sri.targetInstances[0] as MainView; if (main != null) { main.btnLogout.skin.invalidateProperties(); // Force refresh on custom skin part } } } private function buildConfirmMessage():String { var params : Array = selectedEmployee ? [selectedEmployee.firstName, selectedEmployee.lastName] : ["",""]; return _rMngr.getString("employees","users.confirmDelete",params); } // ******************************************************************************** // Private Attributes // ******************************************************************************** private var _rMngr : IResourceManager = ResourceManager.getInstance(); private var _selectedEmployee : Employee = null; ]]> </mx:Script> <l10n:SmartResourceInjector bundleName="login" target="{MainView}" localeChange="onLocaleChanged(event);" > <l10n:ResourceProxy property="btnLogout.label" key="login.signOut" /> <l10n:ResourceProxy property="txtWelcomeUser.text" key="login.welcome" parameters="{[authenticated.user.username]}" /> <l10n:ResourceProxy property="imgHeader.source" key="login.header" type="class" /> <!-- Would be better to inject a new styleName or a new styleSheet Note: The login and logout buttons have custom skins with states. Because we are modifying the colors of skin part manually (instead of different styleName) we must manually invalidate to force immediate redraws. (see lines 83/89). --> <l10n:ResourceProxy property="contentContainer.backgroundColor" key="content.background.color" /> <l10n:ResourceProxy property="contentFrame.color" key="content.frame.color" /> <l10n:ResourceProxy property="btnLogout.backgroundColor" key="content.frame.color" /> </l10n:SmartResourceInjector> <l10n:SmartResourceInjector bundleName="login" target="{LoginView}" localeChange="onLocaleChanged(event);" > <l10n:ResourceProxy property="fiUserName.label" key="login.form.fiUserName" /> <l10n:ResourceProxy property="fiPassword.label" key="login.form.fiPassword" /> <l10n:ResourceProxy property="loginBtn.label" key="login.submit" /> <l10n:ResourceProxy property="lblHint.text" key="login.tip" /> <l10n:ResourceProxy property="loginBtn.backgroundColor" key="content.frame.color" /> </l10n:SmartResourceInjector> <l10n:SmartResourceInjector bundleName="employees" target="{EmployeeListNavigation}" > <l10n:ResourceProxy property="btnCreate.label" key="users.buttons.create" /> <l10n:ResourceProxy property="btnEdit.label" key="users.buttons.edit" /> <l10n:ResourceProxy property="btnDelete.label" key="users.buttons.delete" /> </l10n:SmartResourceInjector> <l10n:SmartResourceInjector bundleName="employees" target="{EmployeeDetailNavigation}" > <l10n:ResourceProxy property="btnBack.label" key="users.buttons.back" /> </l10n:SmartResourceInjector> <l10n:SmartResourceInjector bundleName="employees" target="{EmployeeDetail}" > <l10n:ResourceProxy property="fiFirstName.label" key="form.fiFirstName" /> <l10n:ResourceProxy property="fiLastName.label" key="form.fiLastName" /> <l10n:ResourceProxy property="fiStartDate.label" key="form.fiStartDate" /> <l10n:ResourceProxy property="fiEmail.label" key="form.fiEmail" /> <l10n:ResourceProxy property="btnSubmit.label" key="form.buttons.submit" /> <l10n:ResourceProxy property="btnDelete.label" key="form.buttons.delete" /> <l10n:ResourceProxy property="scaleX" key="form.scaling" /> <l10n:ResourceProxy property="scaleY" key="form.scaling" /> </l10n:SmartResourceInjector> <l10n:SmartResourceInjector bundleName="language" target="{LanguageBar}"> <l10n:ResourceProxy property="lblBarHelp.text" key="languagebar.title" parameters="{['target.selectedLocale']}" /> <!-- See LocaleAssets for sort order on Flags --> <l10n:ResourceProxy property="flags[0].toolTip" key="flag.toolTip.en_US" /> <l10n:ResourceProxy property="flags[1].toolTip" key="flag.toolTip.es_ES" /> </l10n:SmartResourceInjector> </l10n:LocaleMap> |
The BabelFx framework works with Flex 3 & 4, and with Cairngorm, Mate, Swiz, RobotLegs, and other MVC/IoC solutions. But it seems to work “best of all” (easiest to integrate) with Swiz. Swiz leverages dependency injection and listens for GUI instantiations. And BabelFx leverages resource injection and listens for GUI instantiations (independent of Swiz). These two frameworks are a “marriage made in heaven.”
An interesting challenge with the CafeTownsend application was its implementation of color borders with Flex 4 skins. In order to change the skin colors during locale changes, the skins need to redraw via an updateDisplayList phase. See LocalizationMap [line 92] for the easy resolution to invalidating the skin colors during locale changes. Alternate solutions are possible but I wanted to demonstrate that l10Injection even adapts to allow changes of color settings in container skins.
Another important change to the CafeTownsend demo are the new logging features. With Swiz you can log (or trace to the output console) activity within the Swiz engine and within your own custom application code. And, remember that the Localization Framework (l10nInjection) has its own logger to ouput its injection activity. While the l10nInjection injection has its own logging automatically enabled within the CafeTownsend application (see bottom), the application code now uses a new metadatatag [Log] to inject a logger instance and enable super-easy log functionality.
Note that the [Log] tag is enabled simply by adding a LogProcessor instance to your swiz setup and then calling log.debug() from your controllers, models, and views. Developers should also note that the log.debug() methods do not have to specify times, dates, or even the classname… yet they all show up in the trace console. That feature is inherited from the LogProcessor.
Below is the Swiz setup class which activates the Swiz LogProcessor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <?xml version="1.0" encoding="utf-8"?> <sw:Swiz xmlns:sw="http://swiz.swizframework.org" xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:mx="library://ns.adobe.com/flex/mx" > <fx:Script> <![CDATA[ import mx.controls.Alert; import mx.rpc.events.FaultEvent; private function genericFault( fe : FaultEvent ) : void { var message : String = fe.fault.faultDetail; var code : String = fe.fault.faultCode; Alert.show(message , code); } ]]> </fx:Script> <sw:beanProviders> <config:Beans xmlns:config="com.cafetownsend.config.*" /> </sw:beanProviders> <sw:config> <sw:SwizConfig id="appConfig" strict="true" eventPackages="com.cafetownsend.event.*" viewPackages="com.cafetownsend.view.*" defaultFaultHandler="{genericFault}"/> </sw:config> <sw:customProcessors> <sw:LogProcessor /> </sw:customProcessors> </sw:Swiz> |
Now that we have configured Swiz to use the custom LogProcessor, we can use the [Log] metadatatag in our custom application code. The [Log] tag tells Swiz to inject a custom instance of an ILogger. That instance has been pre-configured to auto-output the time, date and className for the class in which it has been injected. Below is a snippet of the EmployeeController class which illustrates how you configure the “almost magical” ILogger injection using the [Log] tag and how you use the log.debug() method. Notice how the developer only had to add the method name and the method argument as additional custom information that they wish to log.
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class EmployeeController { [Log] public var log : ILogger = null; [Mediate(event="LoginEvent.COMPLETE")] public function loadEmployees(department:String=""):void { log.debug("loadEmployees( '{0}' )",department); var agent : AsyncInterceptor = new AsyncInterceptor(parseEmployeesData); serviceRequestUtil.executeServiceCall(agent.intercept(delegate.loadEmployees()), onResults_loadEmployees); } } |
Below is a trace console output sample from the CafeTownsend Swiz demo; output generated when using the custom Swiz LogProcessor and ILogger::debug() calls:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 6/6/2010 07:38:23.169 [DEBUG] com.cafetownsend.controller::AppController login(Flex) 6/6/2010 07:38:23.968 [DEBUG] com.cafetownsend.controller::AppController onResults_login(Flex) 6/6/2010 07:38:23.974 [DEBUG] com.cafetownsend.controller::EmployeeController loadEmployees('accounting') 6/6/2010 07:38:24.009 [DEBUG] com.cafetownsend.controller::NavigationController logInCompleteHandler() 6/6/2010 07:38:24.009 [DEBUG] com.cafetownsend.presentation::MainViewPresentationModel currentState==loggedIn 6/6/2010 07:38:24.198 [DEBUG] com.cafetownsend.presentation::EmployeeListPresentationModel set employees(); len=0 6/6/2010 07:38:24.200 [DEBUG] com.cafetownsend.presentation::EmployeeListPresentationModel selectedEmployeeIndex == -1 6/6/2010 07:38:24.201 [DEBUG] com.cafetownsend.presentation::EmployeeListPresentationModel selectedEmployeeIndex == -1 6/6/2010 07:38:24.476 [DEBUG] com.cafetownsend.controller::EmployeeController parseEmployeesData() 6/6/2010 07:38:24.477 [DEBUG] com.cafetownsend.controller::EmployeeController EmployeeUtil.getEmployeesFromXML() 6/6/2010 07:38:24.478 [DEBUG] com.cafetownsend.controller::EmployeeController onResults_loadEmployees(): employees=4 6/6/2010 07:38:24.481 [DEBUG] com.cafetownsend.presentation::EmployeeListPresentationModel set employees(); len=4 6/6/2010 07:38:24.481 [DEBUG] com.cafetownsend.presentation::EmployeeListPresentationModel set employees(); len=4 6/6/2010 07:38:25.071 [DEBUG] com.cafetownsend.presentation::EmployeeListPresentationModel set employees(); len=4 6/6/2010 07:38:25.072 [DEBUG] com.cafetownsend.presentation::EmployeeListPresentationModel selectedEmployeeIndex == -1 6/6/2010 07:38:25.073 [DEBUG] com.cafetownsend.presentation::EmployeeListPresentationModel selectedEmployeeIndex == -1 |
And since the Flex Localization (l10nInjection) framework has logging enabled by default, the l10nInjection logging generates output that looks something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 07:40:02.599 [DEBUG] LocalizationMap register(com.cafetownsend.view::MainView) 07:40:02.600 [DEBUG] LocalizationMap register(com.cafetownsend.view::LoginView) 07:40:02.600 [DEBUG] LocalizationMap register(com.cafetownsend.view::EmployeeListNavigation) 07:40:02.601 [DEBUG] LocalizationMap register(com.cafetownsend.view::EmployeeDetailNavigation) 07:40:02.602 [DEBUG] LocalizationMap register(com.cafetownsend.view::EmployeeDetail) 07:40:02.603 [DEBUG] LocalizationMap register(l10n.views::LanguageBar) 07:40:02.604 [DEBUG] LocalizationMap addListenerProxy() Attaching listener for all 'creationComplete' event 07:40:02.859 [DEBUG] LocalizationMap onLoadLocale() announce 'changing' locale 07:40:02.861 [DEBUG] LocaleCommand loadLocale(en_US) - embedded. 07:40:02.961 [DEBUG] LocalizationMap onCreationComplete_Target() for 'l10n.views::LanguageBar' 07:40:02.962 [DEBUG] SmartResourceInjector onInstanceCreationComplete(l10n.views::LanguageBar) 07:40:02.965 [DEBUG] SmartResourceInjector inject 'Switch from English to locale:' into 'lblBarHelp.text' from resource language::languagebar.title 07:40:02.966 [DEBUG] SmartResourceInjector inject 'English (USA)' into 'flags[0].toolTip' from resource language::flag.toolTip.en_US 07:40:02.967 [DEBUG] SmartResourceInjector inject 'Spanish' into 'flags[1].toolTip' from resource language::flag.toolTip.es_ES 07:40:02.968 [DEBUG] LocalizationMap onCreationComplete_Target() for 'com.cafetownsend.view::MainView' 07:40:02.968 [DEBUG] SmartResourceInjector onInstanceCreationComplete(com.cafetownsend.view::MainView) 07:40:02.968 [DEBUG] SmartResourceInjector inject 'logout' into 'btnLogout.label' from resource login::login.signOut 07:40:02.969 [DEBUG] SmartResourceInjector inject 'Hello {0}!' into 'txtWelcomeUser.text' from resource login::login.welcome 07:40:02.970 [DEBUG] SmartResourceInjector inject '[class en_US$login_properties__embed_properties___assets_images_header_png_321309068]' into 'imgHeader.source' from resource login::login.header 07:40:02.970 [DEBUG] SmartResourceInjector inject '0xFFFFFF' into 'contentContainer.backgroundColor' from resource login::content.background.color 07:40:02.971 [DEBUG] SmartResourceInjector inject '0x9C0000' into 'contentFrame.color' from resource login::content.frame.color 07:40:02.971 [DEBUG] SmartResourceInjector inject '0x9C0000' into 'btnLogout.backgroundColor' from resource login::content.frame.color 07:40:03.399 [DEBUG] LocalizationMap onCreationComplete_Target() for 'loginView' 07:40:03.400 [DEBUG] SmartResourceInjector onInstanceCreationComplete(com.cafetownsend.view::LoginView) 07:40:03.400 [DEBUG] SmartResourceInjector inject 'Username:' into 'fiUserName.label' from resource login::login.form.fiUserName 07:40:03.401 [DEBUG] SmartResourceInjector inject 'Password:' into 'fiPassword.label' from resource login::login.form.fiPassword 07:40:03.401 [DEBUG] SmartResourceInjector inject 'Login' into 'loginBtn.label' from resource login::login.submit 07:40:03.402 [DEBUG] SmartResourceInjector inject '( Username: Flex Password: Swiz )' into 'lblHint.text' from resource login::login.tip 07:40:03.402 [DEBUG] SmartResourceInjector inject '0x9C0000' into 'loginBtn.backgroundColor' from resource login::content.frame.color |
As you can see, the logging can be a very powerful output and monitoring tool for developers. The custom Swiz LogProcessor is available on my GitHub repository. And the l10nInjection logger is built-into the l10nInjection framework.
Damn, so many code… Localization is a simple thing, just create
[Bindable("stringsChange")]
public function getString(stringId:String):* {}
function in your model and that’s it. Why having so many code?
@Maxim,
Have you read my other blogs regarding Localization? Have you reviewed my video tutorial and slides? Looked at the online samples?
After deploying four (4) commercial Flex applications that support multiple languages, I seriously doubt you will convince me that another solution is better. I have, however, a proposal for you:
Study the BabelFx solution and samples.
Download the source for the Swiz CafeTownsend application.
Use your own solution or approach to add localization to the Swiz app. (Do not use the BabelFx framework.)
Compare your solution with mine.
Show me the better solution
If your solution is better – and I will be a fair judge – I will pay you $50 USD for your efforts.
I have been using cairngorm for 18 months and I hated it but It works, I like how simple swiz seems to be I like this example demo the best, thanks again many times. I still need to attach 10nInjection framework to see it work behind the scenes. what I would like to know is it possible to query Google translate with the titles and text and have changed with a select language button. Yes not all translators know everything you are talking about but the 8 majors work pretty well.
BabelFx (aka l10nInjection) provides a sophisticated set of mechanisms to “inject” localized resources into RIA targets. And it even provides support to use externally-loaded resourcesbundles (pre-compiled as SWFs). But BabelFx does not do anything [yet] to auto-convert your properties files to alternate locales. Effort is currently underway to provide an online resource that internally uses Google Translate to 1-click translate a specified .properties file to an alternate localization. That tool is currently under development.
Localization Map is framework is very good and cool feature , i have some questions regrading it:
1) How to set the default Locale? I want to set default localization based on browser language. Is there any attribute or field to set as default localization?
2) How to load runtime Locale (example : en_US.swf ) using commandFactory , can you provide API or details about command Factory (if possible any you provide simple example)? Or can I use ResourceManager.getInstance().loadResourceModule for loading runtime and for loading complete i ill dispatch LocaleEvent.LOAD_LOCALE , is this approach is correct?
@Vijay,
Check out the GitHub samples and source code.. The l10nInjection has support for both (a) detecting locale changes and to (b) switching locales. To request a localeChange, you must dispatch a com.mindspace.l10n.events.LocaleEvent; e.g.
dispatchEvent(new LocaleEvent(LocaleEvent.LOAD_LOCALE,”en_US”)
If the resourceBundles are embedded, the locale will change and your LocalizationMap will re-inject new resources. If the resourceBundle (for the locale specified) is not embedded, you must load that bundle externally. That loading can be done manually with your own code or you can use the l10nInjection ExternalLocaleCommand to load the bundle, switch to that new locale, and re-inject localized resources into your application.
Now to load external resource bundles, it is SUPER easy. Of course, you must have the bundles already compiled and deployed to a url for FlashPlayer loading. The l10nInjection has an ExternalLocaleCommand that can be used in the LocalizationMap. In fact the l10nInjection has a super simple way to configure and plugin that command… see lines 74-80 in the FlexStore. LocalizationMap for a sample.
The samples use a LanguageBar to show locale Flags; when clicked each flag dispatches a LocaleEvent. The FlexStore has a Flex4 LanguageBar and the RegistrationDemo has a Flex3 LanguageBar. The FlexStore LanguageBar also has code to auto-detect the current OS language setttings and auto-switch the Locale upon startup see lines 98-100.
Hi thomasb
I got answer for every Question ThankQ so much for such a Awesome Framework
One Last Question
Do you have any idea about getting browser language and setting it as default. I tryed javascript but its not working
if you provide any suggestion/solution
Thanks
@Vijay,
Did you read my previous reply on 2010/07/12? Read the docs and the source code for the samples and LanguageBar. Those resources will answer your questions.
Thanks For Help ,All my query’s are solved.