At some point, Flash and Flex developers encounter the issues of modifying a Flex/Flash rich internet application (RIA) to support another language and accomodate software changes for specific cultural preferences. This article focuses on localization of Flash solutions implemented with Flex 3 or Flex 4. Both Flash CS4 and the Flex frameworks provide localization tools and programmatic support features for Flash applications and widgets. Unfortunately the support for localization differs significantly between the toolsets and frameworks offered.
Internationalization (i18n)[1] is the process of generalizing a product so that it can handle multiple languages and cultural conventions without the need for redesign. i18n processes usually take place at the level of program design and documentation development. Localization (l10n) is the process of adapting products and services (web sites, documents, data files, videos, manuals, and software interfaces). Software localization is the adaptation of static text, skins, images, sytles, fonts [etc.] in accordance to linguistic, cultural, technical, and other locale-specific requirements of the target market.
With cloud-services, market globalization, and radical growths in both online and mobile applications, Flash i18n solutions will play critical roles in product differentiation and market acquisition. The importance and growth of the localization market is illustrated in the chart below.

As mentioned earlier, localization is the process of including assets to support multiple locales. A locale is the combination of a language and a country code. This is required for country differences in language syntatics and culture. For example, an application in the en_US (USA) locale might spell the word "color", whereas the word would be "colour" in the en_GB (Great Britain) locale. Localization goes beyond just translating Strings used in your application. It can also include any type of asset such as audio files, images, and videos. Because the meanings of colors and images may vary based on the culture, you should change the styles used by your application – in addition to the language used – based on the locale.
In order to localize (l10n) Flex 4 applications, you may use built-in support for accessing compiled resource bundles or create your own solution to access localized assets stored in remote database tables[2]. To use the built-in Flex mx framework solutions, you first
Flex allows the developer to localize strings, skinning code/classes, and embedding graphics. These allows Flex apps to localize for cultural differences also (stylesheets, colors, images, etc.) Note that many Flex applications load dynamic xml content; content that may also need to be localized. Using "best practices" approaches[6] to localization will provide for easy maintenance and on-demand additions of localized resource bundles. As noted above, Java ANT scripts are usually employed to compile the localized files and assets into run-time loadable bundles.
Flash IDE tools do not support the Flex solution for resource bundles and compiled resource swfs. Instead a Locale class is provided that supports loading locale-based XML files and provides support for string look-ups by locale and ID codes. While localized content can by dynamically loaded at runtime, the GUI controls must be programmatically updated [after the localized text or XML data has loaded]. Note that the Locale class does not support loading of localized skins and images; only string-based content is supported.
Adobe and the developer community offer a plethora of articles and videos for developers to self-train and implement Flex l10n solutions. Recently, two Flex entrepreneurs created an open-source tool called ResourceManager and a Flex/AIR solution called Lupo Manager;both focused on providing tools to easily manage properties files and modify source code to accommodate l10n features.
Those tools are a step in the right direction and I give a big shout out to David Deraedt for his efforts. In fact, Adobe has provided significant documentation and recommended solutions to be used as an industry standard for localizing Flex apps. The Flex community has myriad blogs and snippets that discuss the issues. But…
Now before you get really offended, realize that I deliberately used crude expletives to (a) grab your attention, (b) highlight my dismay and (c) set the stage for a better solution. Let me show you what I mean. Consider a typical registration [shown below] that has validation, remembers last login, supports english error messages, and more. Note that this version of the application does not yet support l10n:
To appreciate the impacts of l10n support, let’s first look at the code used in the demo. In particular let’s look at the MXML BEFORE it has l10n-enabling code modifications:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!-- Component: Sign-in Form has "registration" and "already registered child components. --> <?xml version="1.0" encoding="utf-8"?> <mx:Canvas id="registerArea" x="20" top="20" bottom="20" cacheAsBitmap="true" horizontalScrollPolicy="off" verticalScrollPolicy="off" styleName="registrationForm" > <forms:RegistrationForm id="frmRegister" account="{visitor}" x="25" top="33" width="386" height="290" mouseDown="frmSignin.clearErrors();" /> <forms:AlreadyRegisteredForm id="frmSignin" email="{visitor.email}" x="25" top="343" width="386" height="111" mouseDown="frmRegister.clearErrors();" /> </mx:Canvas> |
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 | <!-- Component: New Account Registration --> <?xml version="1.0" encoding="utf-8"?> <mx:Canvas width="386" height="290" clipContent="false" xmlns:mx="http://www.adobe.com/2006/mxml" > <mx:Script> <![CDATA[ /** Removed while displaying in blog... */ ]]> </mx:Script> <mx:Array id="registrationValidators"> <mx:StringValidator source="{txtFirstName}" property="text" required="true" minLength="2" requiredFieldError="First name is required!"/> <mx:StringValidator source="{txtLastName}" property="text" required="true" minLength="2" requiredFieldError="Last name is required!"/> <mx:EmailValidator source="{txtEmail}" property="text" required="true" requiredFieldError="A valid email is required!"/> <mx:StringValidator source="{txtCompany}" property="text" required="true" minLength="2" requiredFieldError="A company name is required!"/> <mx:StringValidator source="{txtPostal}" property="text" required="true" minLength="2" requiredFieldError="What is your Postal Code?"/> <mx:PhoneNumberValidator source="{txtPhone}" property="text" required="true" minDigits="10" requiredFieldError="A valid phone # is required; e.g (515.221.3216)"/> <mx:NumberValidator source="{cmbCountry}" property="selectedIndex" required="true" minValue="0" lowerThanMinError="Please select your Country of residence" /> <mx:NumberValidator source="{cmbState}" property="selectedIndex" required="true" minValue="0" lowerThanMinError="Please select a US State of residence" enabled="{cmbCountry.selectedItem == Countries.USA}"/> </mx:Array> <mx:Label id="lblTitle" text="Register Now" styleName="siHeader" y="0"/> <mx:Label id="lblSubTitle" text="It only takes a few seconds" styleName="siText" x="2" y="35"/> <mx:Label id="lblMsg" text="(All fields are required)" styleName="siText" fontStyle="italic" textAlign="right" y="37" x="232" fontSize="10"/> <mx:Label id="lblFirstName" text="First Name:" styleName="siFieldLabel" y="65" x="-2"/> <mx:TextInput id="txtFirstName" text="{_account.firstName}" minWidth="168" focusOut="onCheckValidation(event);" x="0" y="81" tabIndex="1"/> <mx:Label id="lblLastName" text="Last Name:" x="181" minWidth="70" y="65" styleName="siFieldLabel" /> <mx:TextInput id="txtLastName" text="{_account.lastName}" width="168" x="183" focusOut="onCheckValidation(event);" y="81" tabIndex="2"/> <mx:Label id="lblEmail" text="eMail:" width="70" x="-3" y="111" styleName="siFieldLabel"/> <mx:TextInput id="txtEmail" text="{_account.email}" width="168" y="127" focusOut="onCheckValidation(event);" tabIndex="3"/> <mx:Label id="lblCompany" text="Company:" x="181" width="70" y="111" styleName="siFieldLabel" /> <mx:TextInput id="txtCompany" text="{_account.company}" width="168" y="127" x="183" focusOut="onCheckValidation(event);" tabIndex="4"/> <mx:Label id="lblZip" text="Postal" width="70" y="159" x="240" styleName="siFieldLabel"/> <mx:TextInput id="txtPostal" text="{_account.postal}" width="109" x="242" focusOut="onCheckValidation(event);" y="175" tabIndex="6"/> <mx:Label id="lblCountry" text="Country" x="-2" width="70" y="159" styleName="siFieldLabel" /> <mx:ComboBox id="cmbCountry" dataProvider="{_countries}" width="230" x="0" y="176" change="onCountryChanged();" prompt="Please select your Country..." tabIndex="5"/> <mx:Label id="lblState" text="State:" width="70" x="-1" y="205" styleName="siFieldLabel" alpha="{(cmbCountry.selectedItem == Countries.USA) ? 1 : .3}"/> <mx:ComboBox id="cmbState" dataProvider="{_states}" width="230" x="0" y="222" change="onStateChanged();" selectedIndex="-1" prompt="Please select a State..." tabIndex="7" /> <mx:Label id="lblPhone" text="Phone:" width="70" x="240" y="205" styleName="siFieldLabel"/> <mx:TextInput id="txtPhone" text="{_account.phone}" width="109" x="242" y="222" restrict="0-9,.-" focusOut="onCheckValidation(event);" tabIndex="8"/> <mx:Image id="btnRegister" click="onSaveProfile()" x="2" y="258" tabIndex="9" source="@Embed('/assets/images/btn_register.png')" useHandCursor="true" buttonMode="true" focusEnabled="true"/> </mx:Canvas> |
A design-mode view from FlashBuilder shows nothing surprising… just your standard WYSIWYG renderering shown in Figure 2. Now let’s apply Adobe-recommended code changes to support localization (l10n) for English and Spanish. Figure 3 shows the design-view impacts as a result of source code modifications; necessary to databind to the resourceManager.
Are here is the MXML code associated with the Figure 3 design-view snapshot:
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 | <?xml version="1.0" encoding="utf-8"?> <mx:Canvas width="386" height="290" clipContent="false" xmlns:frm="com.thunderbay.i18nDemo.views.forms.*" xmlns:comp="com.thunderbay.i18nDemo.control.*" xmlns:mx="http://www.adobe.com/2006/mxml" > <mx:Metadata> [ResourceBundle("registration")]</mx:Metadata> <mx:Script> <![CDATA[ /** Removed while displaying in blog... */ ]]> </mx:Script> <mx:Array id="registrationValidators"> <mx:StringValidator source="{txtFirstName}" property="text" required="true" minLength="2" requiredFieldError="{resourceManager.getString('registration','register.validationError.firstName')}"/> <mx:StringValidator source="{txtLastName}" property="text" required="true" minLength="2" requiredFieldError="{resourceManager.getString('registration','register.validationError.lastName')}"/> <mx:EmailValidator source="{txtEmail}" property="text" required="true" requiredFieldError="{resourceManager.getString('registration','register.validationError.email')}"/> <mx:StringValidator source="{txtCompany}" property="text" required="true" minLength="2" requiredFieldError="{resourceManager.getString('registration','register.validationError.company')}"/> <mx:StringValidator source="{txtPostal}" property="text" required="true" minLength="2" requiredFieldError="{resourceManager.getString('registration','register.validationError.postal')}"/> <mx:PhoneNumberValidator source="{txtPhone}" property="text" required="true" minDigits="10" requiredFieldError="{resourceManager.getString('registration','register.validationError.phone')}"/> <mx:NumberValidator source="{cmbCountry}" property="selectedIndex" required="true" minValue="0" lowerThanMinError="{resourceManager.getString('registration','register.validationError.country')}" /> <mx:NumberValidator source="{cmbState}" property="selectedIndex" required="true" minValue="0" lowerThanMinError="{resourceManager.getString('registration','register.validationError.state')}" </mx:Array> <mx:Label id="lblTitle" text="{resourceManager.getString('registration','register.title')}" styleName="siHeader" y="0" width="351"/> <mx:Label id="lblSubTitle" text="{resourceManager.getString('registration','register.subtitle')}" styleName="siText" x="2" y="35" width="199"/> <mx:Label id="lblMsg" text="{resourceManager.getString('registration','register.allRequired')}" styleName="siText" fontStyle="italic" textAlign="right" y="37" x="232" fontSize="10"/> <!-- Each text input is horizontally aligned on column markers each at 184 pixels wide --> <mx:Label id="lblFirstName" text="{resourceManager.getString('registration','register.firstName')}" styleName="siFieldLabel" y="65" x="-2" width="170"/> <mx:TextInput id="txtFirstName" text="{_account.firstName}" width="168" focusOut="onCheckValidation(event);" x="0" y="81" tabIndex="1"/> <mx:Label id="lblLastName" text="{resourceManager.getString('registration','register.lastName')}" x="181" width="170" y="65" styleName="siFieldLabel" /> <mx:TextInput id="txtLastName" text="{_account.lastName}" width="168" x="183" focusOut="onCheckValidation(event);" y="81" tabIndex="2"/> <mx:Label id="lblEmail" text="{resourceManager.getString('registration','register.email')}" width="171" x="-3" y="111" styleName="siFieldLabel"/> <mx:TextInput id="txtEmail" text="{_account.email}" width="168" y="127" focusOut="onCheckValidation(event);" tabIndex="3"/> <mx:Label id="lblCompany" text="{resourceManager.getString('registration','register.company')}" x="181" width="170" y="111" styleName="siFieldLabel" /> <mx:TextInput id="txtCompany" text="{_account.company}" width="168" y="127" x="183" focusOut="onCheckValidation(event);" tabIndex="4"/> <mx:Label id="lblZip" text="{resourceManager.getString('registration','register.postal')}" width="111" y="159" x="240" styleName="siFieldLabel"/> <mx:TextInput id="txtPostal" text="{_account.postal}" width="109" x="242" focusOut="onCheckValidation(event);" y="175" tabIndex="6"/> <mx:Label id="lblCountry" text="{resourceManager.getString('registration','register.country')}" x="-2" width="234" y="159" styleName="siFieldLabel" /> <mx:ComboBox id="cmbCountry" dataProvider="{_countries}" width="230" x="0" y="176" change="onCountryChanged();" prompt="{resourceManager.getString('registration','register.prompt.country')}" tabIndex="5"/> <mx:Label id="lblState" text="{resourceManager.getString('registration','register.state')}" width="233" x="-1" y="205" styleName="siFieldLabel" alpha="{(cmbCountry.selectedItem == Countries.USA) ? 1 : .3}"/> <mx:ComboBox id="cmbState" dataProvider="{_states}" width="230" x="0" y="222" change="onStateChanged();" selectedIndex="-1" prompt="{resourceManager.getString('registration','register.prompt.state')}" tabIndex="7" /> <mx:Label id="lblPhone" text="{resourceManager.getString('registration','register.phone')}" width="111" x="240" y="205" styleName="siFieldLabel"/> <mx:TextInput id="txtPhone" text="{_account.phone}" width="109" x="242" y="222" restrict="0-9,.-" focusOut="onCheckValidation(event);" tabIndex="8"/> <mx:Image id="btnSubmit" click="onSaveProfile()" x="2" y="258" tabIndex="9" source="{resourceManager.getClass('registration','register.btnRegister'}" useHandCursor="true" buttonMode="true" /> </mx:Canvas> |
The changes for l10n results in a maintenance nightmare and is – IMHO – completely unusable! And what are the biggest issues with these industry l10n code techniques/solutions?
Now realize that I would not condemn an industry standard without presenting a viable – and superior – solution.
For those familiar with my previous presentations, talks, and blogs on Flex Behavior Injection, let’s consider a new approach to i18n. So instead of modifying your source code with resourceManager invocations, “inject” the results of the locale changes from “outside” your components. Now your views and components no longer know anything about locale changes and requirements. Now your design views render properly.[8]
This feature is very similar to that used in the SWIZ, Mate, and other IOC frameworks. So now you, as the developer, never need to embed resourceManager calls in any of your view components. That is really COOL!
So how does this new l10n injection work? What changes are needed to your source code? Below is the application MXML code for the demo. This code (and demo shown above) does NOT support l10n.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?xml version="1.0" encoding="utf-8"?> <mx:Application pageTitle="i18n Demo without l10n" preloader="com.thunderbay.i18nDemo.views.preloader.Preloader" creationComplete="signIn.visible = true;" backgroundColor="#777777" backgroundGradientColors="[#777777,#BABABA]" layout="absolute" horizontalScrollPolicy="off" verticalScrollPolicy="off" xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:Style source="locale/en_US/assets/css/styles.css" /> <!-- Classic Cairngorm MVC structures --> <model:Model id="_model" xmlns:model="com.thunderbay.i18nDemo.model.*"/> <mvc:i18nController xmlns:mvc="com.thunderbay.i18nDemo.control.*"/> <views:SignInPage id="signIn" xmlns:views="com.thunderbay.i18nDemo.views.*" visitor="{_model.account}" visible="false" width="854" height="526" horizontalCenter="0" verticalCenter="0" /> </mx:Application> |
And below is the same MXML application code with 1 line change to support l10n for the entire 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 | <?xml version="1.0" encoding="utf-8"?> <mx:Application pageTitle="i18n Demo with LocalizationMaps" preloader="com.thunderbay.i18nDemo.views.preloader.Preloader" creationComplete="signIn.visible = true;" backgroundColor="#777777" backgroundGradientColors="[#777777,#BABABA]" layout="absolute" horizontalScrollPolicy="off" verticalScrollPolicy="off" xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:Style source="locale/en_US/assets/css/styles.css" /> <!-- Classic Cairngorm MVC structures --> <model:Model id="_model" xmlns:model="com.thunderbay.i18nDemo.model.*"/> <mvc:i18nController xmlns:mvc="com.thunderbay.i18nDemo.control.*"/> <views:SignInPage id="signIn" xmlns:views="com.thunderbay.i18nDemo.views.*" visitor="{_model.account}" visible="false" width="854" height="526" horizontalCenter="0" verticalCenter="0" /> <!-- Single tag used to provide l10n features for the entire application and all sub views --> <local:LocalizationMap account="{_model.account}" xmlns:local="*" /> </mx:Application> |
Now, whenever the locale changes (at app startup detection, or based on user profile, or user interaction) ALL your views and application l10n features are auto-magically updated. How is this achieved? In the code above, we used a LocalizationMap to configure or “map” injectors to “inject” localized values into your runtime components and models.
Here is the LocalizationMap source for the demo presented in this blog:
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 | <?xml version="1.0" encoding="utf-8"?> <map:SmartLocaleMap targets="{[i18nDemo]}" targetReady="onUpdateTarget(event);" xmlns="com.asfusion.mate.injectors.*" xmlns:map="com.asfusion.mate.maps.*" xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:Script> <![CDATA[ import com.asfusion.mate.maps.LocaleMapEvent; import com.thunderbay.i18nDemo.views.SignInPage; import com.thunderbay.i18nDemo.views.forms.RequestNotification; import com.thunderbay.i18nDemo.vo.AccountVO; [Bindable] public var account : AccountVO = null; /** * EventHandler when the target class has been instantiated. * We then use the new instance to update ResourceProxies and fire locale updates */ private function onUpdateTarget(event:LocaleMapEvent):void { var who : Object = event.targetInst as Object; if (who is i18nDemo) { signInPage = (who as i18nDemo).signIn; } } [Bindable] private var signInPage : SignInPage = null; ]]> </mx:Script> <!-- ****************************************************************************************************** This solution leverages compile-time type checking but suffers from: 1) Have to use the onUpdateTarget() to get a reference [after 'creationComplete'] 2) Only supports a single unique instance of the target class But it does support property chain references (e.g. signInPage.frmSignin.validator) ****************************************************************************************************** --> <ResourceInjector bundleName="registration"> <ResourceProxy target="{signInPage.frmSignin}" property="alertTitle" key="signing.invalid" /> <ResourceProxy target="{signInPage.frmSignin.validator}" property="requiredFieldError" key="signin.validationError.email" /> <ResourceProxy target="{signInPage.frmSignin.lblTitle}" property="text" key="signin.title" /> <ResourceProxy target="{signInPage.frmSignin.lblEmail}" property="text" key="signin.email" /> <ResourceProxy target="{signInPage.frmRegister}" property="alertTitle" key="register.invalidProfile" /> <ResourceProxy target="{signInPage.frmRegister.registrationValidators[0]}" property="requiredFieldError" key="register.validationError.firstName" /> <ResourceProxy target="{signInPage.frmRegister.registrationValidators[1]}" property="requiredFieldError" key="register.validationError.lastName" /> <ResourceProxy target="{signInPage.frmRegister.registrationValidators[2]}" property="requiredFieldError" key="register.validationError.email" /> <ResourceProxy target="{signInPage.frmRegister.registrationValidators[3]}" property="requiredFieldError" key="register.validationError.company" /> <ResourceProxy target="{signInPage.frmRegister.registrationValidators[4]}" property="requiredFieldError" key="register.validationError.postal" /> <ResourceProxy target="{signInPage.frmRegister.registrationValidators[5]}" property="requiredFieldError" key="register.validationError.phone" /> <ResourceProxy target="{signInPage.frmRegister.registrationValidators[6]}" property="lowerThanMinError" key="register.validationError.country" /> <ResourceProxy target="{signInPage.frmRegister.registrationValidators[7]}" property="lowerThanMinError" key="register.validationError.state" /> <ResourceProxy target="{signInPage.frmRegister.lblTitle}" property="text" key="register.title" /> <ResourceProxy target="{signInPage.frmRegister.lblSubTitle}" property="text" key="register.subtitle" /> <ResourceProxy target="{signInPage.frmRegister.lblMsg}" property="text" key="register.allRequired" /> <ResourceProxy target="{signInPage.frmRegister.lblFirstName}" property="text" key="register.firstName" /> <ResourceProxy target="{signInPage.frmRegister.lblLastName}" property="text" key="register.lastName" /> <ResourceProxy target="{signInPage.frmRegister.lblEmail}" property="text" key="register.email" /> <ResourceProxy target="{signInPage.frmRegister.lblPhone}" property="text" key="register.phone" /> <ResourceProxy target="{signInPage.frmRegister.lblCompany}" property="text" key="register.company" /> <ResourceProxy target="{signInPage.frmRegister.lblZip}" property="text" key="register.postal" /> <ResourceProxy target="{signInPage.frmRegister.lblState}" property="text" key="register.state" /> <ResourceProxy target="{signInPage.frmRegister.cmbState}" property="prompt" key="register.prompt.state" /> <ResourceProxy target="{signInPage.frmRegister.lblCountry}" property="text" key="register.country" /> <ResourceProxy target="{signInPage.frmRegister.cmbCountry}" property="prompt" key="register.prompt.country" /> </ResourceInjector> <!-- ****************************************************************************************************** --> <!-- SmartInjectors for targeted class instantiations; where the class will have multiple instances created --> <!-- ****************************************************************************************************** --> <SmartResourceInjector target="{RequestNotification}" bundleName="registration"> <ResourceProxy property="alertTitle" key="register.invalidProfile" /> <ResourceProxy property="registrationValidators[0].requiredFieldError" key="register.validationError.firstName" /> <ResourceProxy property="registrationValidators[1].requiredFieldError" key="register.validationError.lastName" /> <ResourceProxy property="registrationValidators[2].requiredFieldError" key="register.validationError.email" /> <ResourceProxy property="registrationValidators[3].requiredFieldError" key="register.validationError.company" /> <ResourceProxy property="lblTitle.text" key="request.title" /> <ResourceProxy property="lblSubTitle.text" key="request.subtitle" /> <ResourceProxy property="lblMsg.text" key="register.allRequired" /> <ResourceProxy property="lblFirstName.text" key="register.firstName" /> <ResourceProxy property="lblLastName.text" key="register.lastName" /> <ResourceProxy property="lblEmail.text" key="register.email" /> <ResourceProxy property="lblCompany.text" key="register.company" /> <!-- Inject localized text with parameterized, current values of account details e.g. "Thanks for registering {0}!" ==> "Thanks for registering Thomas Burleson!" --> <ResourceProxy property="lblThanks.htmlText" key="request.thanks.title" parameters="{[account.fullName]}" /> <ResourceProxy property="lblThanksMessage.htmlText" key="request.thanks.text" parameters="{[account.email]}"/> </SmartResourceInjector> </map:SmartLocaleMap> |
Here are some key points regarding the amazing benefits of using a LocalizationMap:
Notice that LocalizationMap extends SmartLocaleMap; which is a parent class available in a soon-to-be-released extension of the open-source Mate framework. Let’s pause while I give a major shout-out to Nahuel Foronda and Laura Arguello![9]
Best of all, is the fact that the SmartLocaleMap can be used outside of Mate with a MVC framework of your choice… notice that the demo here uses the Cairngorm MVC framework. You can use this easily with Cairngorm, Swiz, Mate, PureMVC, etc.
Within the next week, I will publish two important additions within this blog:
Consider the implications of this blog on your product, projects, and processes. Perhaps you can optimize your ROI and engage ThunderBay Software to help with your product deliveries and development improvements?
Thanks for reading my blogs. Don’t forget to rate this blog please!
And contact me to discuss IT resources and assistance we can provide to help you with internet solutions, architecture, mentoring, training, or product delivery.
Be sure to read Part 2: LocalizationMaps – A Video Tutorial and Source Code
Hi Thomas,
I used your localization framework and it works quite good except one thing I found. I’m not able to inject an array of strings, i.e. for a calendar component. Is it possible from the LocalizationMap at all? Nowadays I use the resourceManager.getStringArray() method.
Thanks for your suggestion.
Checkout ResourceInjector.as (line 404). The getStringArray() is supported but you must tell the ResourceProxy instance to that the localized bundle data is array; this is done by setting the “type” attribute of the ResourceProxy (not that the default type value is string).
For example, let’s consider a signup form (id == frmSignup) with a Calendar control. We want to inject localized holidays (as array of dates) into the Calendar::holidays property. That information is in the schedule.properties file with the key scheduler.officialHolidays. So we use the following:
<ResourceProxy target=”{frmSignup.myCalendar}” property=”holidays” key=”scheduler.officialHolidays” type=”array” />
- Attention Developers -
All source code and sample apps are now available on GitHub:
http://github.com/ThomasBurleson/l10n_Injection
http://github.com/ThomasBurleson/l10nInjection_Samples
Hat off to you mate, this is exactly what I was looking for, thanks heaps for the post!
I’m using your localization framework with Mate and it works great for most Flex components except Renderers like AdvancedDataGrid Renderers because the grid instantiates a new Renderer per row and you don’t have a handle to the created object. Any thoughts or suggestions on how this would or could work with that?
Thanks.
Al V.
Al,
Lists and Grids use custom item renderer instances to render row information.
Consider the renderer “ADGRenderer1″ with contains a button “Edit” and some text [defined based on the set data(src:Object)]
Say your requirements need to localize the Edit button using l10n injection; the text is based on dynamic data that is not dependent on localized bundles.
You can use the SmartResourceInjector that “listens” for a creationComplete event from instances of the registered UIComponent class.
All instances are automatically cached by the SRI and current locale information is injected as defined in the ResourceProxies.
And any localization changes are immediately used to re-inject into those cached instances. E.g.
Hope that clarifies some advanced features of the l10n IOC
Sorry to trouble you again. I’m interested in this project but it can not work with flex 4 very well. If you use ResourceProxy on a NavigatorContent’s label attribute in flex 4, an error “can not convert from NavigatorContent to mx.core.Container” will occur. Maybe this is a bug?
Liang,
Can you provide a test app (.zip archive). I will debug and resolve quickly for you.
Thx,
ThomasB
Cheng,
I just tested your sample app. The Flex 4 error you encountered is a Flex 4 source code BUG in the Accordian.as class (line 2475).
This bug has nothing to do with the l10Injection solution.
Be sure to read Part 2: LocalizationMaps – A Video Tutorial and Source Code
Hi! I’m new to this post and I tried to added this swc file into my own project with mate framwork. But the file I wrote for translating is not work well. The strings need for translating is disapeared so I know it works but no translated strings added. Therefore it is comletly empty. I don’t know why… Please give me a hand. Thank you.
Liang,
1) Confirm that the ResourceManager loaded bundle.
2) Confirm that your fonts are available (if embedded fonts, these must already be loaded)
3) Confirm that your bundles compiled properly. Did you compile with unicode ranges? Are you using Chinese glyphs or English fonts?
As you can tell by my questions, these issues can be complex. I am available as a consultant if your company wishes to engage me to resolve these with you.
Best of luck,
ThomasB
Thanks for your help! I thought the ResourceBundle will be set automatically because I have set the field “bundleName” on ResourceInjector, so I didn’t add the Metadata as usual. When I added this everything is OK.
You’ve done a good job! Thank you!
Sorry, I have another question. I used this extension in Flex 4. It has the new state syntax. For example the label has different text attribute: text.main=”MAIN” and text.sub=”SUB”. How can I do this in the extension? I tried property=”text.main” and it seemed this can not work well. Maybe I had something wrong again? Thank you for answering me.
Liang,
Flex3 and Flex4 state changes are not yet fully supported in LocalizationMaps:
1) The ResourceInjector must listen for EnterState events dispatched from targets and target parents.
2) When the state changes, all ResourceInjectors (for that target state) need to re-inject the localized resources.
I will release an updated lib soon that will support State changes in Flex4.
Liang,
Thank you for the sample/demo app that illustrates the view state “bug”.
View states are NOW supported!
I have fixed the l10n Injection framework and your sample app.
Here is a link to an archive with the enhanced l10nInjection.swc, the framework source code, and your sample app. Please review the readme for details on specific changes.
Thanks for the feedback. Viewstates rock.
Stay tuned… soon I will write another blog entry that demonstrates the power features of l10n Injection and view states!
Liang,
The ResourceInjectors (and LocalizationMaps) do not need ResourceBundle tags in view code; but you must insure that the localized bundles are properly loaded. And dynamically loaded bundles must still be manually loaded upon demand.
Setting the MetaData [ResourceBundle()] tag only tells the compiler to link the compiled bundles into your application; you should not need to use the MetaData ResourceBundle tags.
Tom,
I’m getting error: unable to open ‘\l10nInjection\bin\l10nInjection.swc’
Is that one is missing in the zip sources?
Thanks.
I’m also seeing some dependencies on universalmind sources:
@ i18nController.as
import com.universalmind.cairngorm.control.FrontController
@ LoadAccountEvent.as
import com.universalmind.cairngorm.events.UMEvent
@ SaveAccountEvent.as
import com.universalmind.cairngorm.events.UMEvent
Please advise…
Dmitry,
I updated the LocalizationMaps.zip file to include the libs directory and a Readme.txt.
The libs dir now contains the l10Injection.swc, the CGX.swc (Cairngorm Extensions), and the Degrafa.swc… all used in the RegsitrationDemo app. The error with the UMEvent dependency is due to the fact that you need the Cairngorm Extensions library; which is now contained in the libs directory. The ReadMe.txt (in /l10n-src/*) also describes how you can rebuild the l10nIjection.swc; if needed.
Hope this helps. Thanks for the feedback regarding the source.
[...] Amazing i18n IOC solutions for Flex [...]
Awesome post Thomas! I am really looking forward to being able to use the same technique, as i am developing several projects with Mate
Do you have any updates or info on when the SmartLocaleMap will be made “public”?
Best regards,
Paw Suddergaard
Thanks Paw.
On Monday, I plan to publish “Using IOC for Flex l10n – Part 2″. Part 2 will contain both (a) a video on why and how to use the LocaleMaps for l10n IOC and (b) a live demo of a registration form with support for both English and Spanish. I will also publish the source code to the demo and the source code to the LocaleMap module. So stay tuned…
BTW, LocaleMaps can be used with Cairngorm, Swiz, Mate, or your own custom architecture.
Hi Thomas – Please put me on your “white paper requests” list – Thanks, Douglas
I see you’re loading the localizations from bundles. I misread the code snippets.
Tim,
You are correct.
The ResourceManager still uses runtime loaded (and compiled) resource bundles. And when the ResourceManager announces that the locale has changed, then the LocalizationMap does its magic.
The nice part about this approach is that if you added code to load locale strings from a remote database and integrated that “data load” with the ResourceManager, then the LocalizationMap would STILL work and the views would be updated again. This approach completely decouples the view updates from the mechanics of loading the resource strings/bundles.
Another great reason for LocalizationMaps is that the view updates occur upon any of 3 [independent] triggers:
1) when the ResourceManager announces locale changes
2) when new views instances are created… or
3) when parameterized values change; such as user fullname, etc.
This flexibility significantly reduces l10n complexities in your RIAs.
Social comments and analytics for this post…
This post was mentioned on Twitter by adobelove: Amazing i18n IOC solutions for Flex: Flex allows the developer to localize strings, skinning code/classes, and .. http://bit.ly/40rnzv...
Hi Thomas,
Thanks for article and for mentioning Lupo Manager & Lupo Translator.
I think your article does a great job at highlighting the various pitfalls of the “standard / Adobe documentation”(ResourceBundle metadata + ResourceManager reference in all your views) Flex i18n technique.
I’d just like to mention one or two things here.
First, the standard “Adobe documentation” technique is not the only way to work with Lupo, and in fact it’s not even recommended. You might have been confused because of my video presentation which demonstrates this (bad) technique. I did hesitate a lot before showing this technique in the video, but I thought doing otherwise might confuse people. I now realize that I was probably wrong.
I personnaly never work this way for my own applications or for my clients. That’s the reason why Lupo allows the user to work in a completely different way which is the way I recommend : using presentation pattern.
The use of a presentation pattern, like the Presentation Model for instance, allows the developer to externalize the i18n logic in a dedicated class that can be used through composition inside the presentation layer. In my own experience, that’s the best way to deal with i18n. Lupo was made to implement this technique, not the “Adobe documentation” technique.
Now, I realize that your approach also has significant advantages. Most are shared with the presentation pattern based techniques (like the fact that we keep our views “clean” of all the i18n garbage), but some are not.
One drawback I couldn’t help noticing in your approach is that it looks like you have to give an id to all your localizable views. Another is that your seem to have one i18nController instead of sharing it throughout all your views. Also, I’d like to see exactly how your ResourceProxy was implemented.
All in all, I think that is a very interesting approach, and I can’t wait to learn more about it.
David,
.
Thank you for the great feedback. While I lament a current “standard”, at least Adobe Flex offers a better solution than the Flash IDE
I agree that the presentation pattern is superior to the Adobe i18n techniques. More than likely your views use a databinding “pull” technique to self-update when the presentation model announces changes. This is a standard Flex technique.
I have found the IOC approaches, however, to be much more satisfying and robust… hence the LocalizationMap solution. With IOC, the views truly do not “know” about outside datafeeds. With IOC, views simply render data and state and announce user interactions. The data is somehow magically assigned
.
Two comments:
1) The i18nController is part of the Cairngorm MVC framework; not part of the Localization solution.
2) You do not need view IDs for the LocalizationMap to work. See line 79 (above) in the LocalizationMap source code.
On that line we see:
We this syntax, we auto-magically capture and remember all instantiations of the RequestNotification class. Whenever the locale or data models change, the SmartResourceInject re-injects into all those instances. A major benefit of this mechanism, is that it allows you to inject into instance attributes regardless of the instance’s relationship/nesting in the application’s view hierarchy.
Stay tuned for my video, where I will discuss all of this. I am sure you will love the SmartLocaleMap IOC approach for Lupo.
Best regards,
ThomasB