Localization in Model-Glue - Part I
Published April 17th, 2008 in ColdFusion, ColdSpring Framework, Databases, Model-Glue MVC Framework, Reactor Framework, Software DevelopmentBefore continuing to read, please note that there is a Part II to this post which addresses a performance issue in the following approach. However, please do continue reading because Part II assumes you have read this entire post and only makes minor changes to the code herein…
I have seen a couple of approaches to localization in Model-Glue. Personally I like the idea of having a single view page, regardless of how many languages you will support, and store the localized labels and messages in langauge files. I even extend this to the Validators that Reactor generates (more on this later).
A different approach than that of mine can be found here: Localization in Model-Glue 2
That solution involves creating pages for each locale. This is in contrast to my solution, where I have one page, but many language files.
In developing my solution, I borrowed heavily from the use of ResourceBundle here: Multi-lingual site with Model-Glue and Coldspring
However, I took it a step further to allow for a user to dynamically change the locale. I followed Andy Jarret’s approach and decided that I would inject a default locale into the main controller using the autowiring approach. So, you can take it as read that I added the same code to ColdSpring.xml and Controller.cfc as Andy did.
The next step is to give us a nice way of changing this locale. Now, when we inject the intial resource bundle into the Controller, this obviously gets saved into the Application scope so that we don’t need to keep accessing the filesystem on each page request. Every user then has that default locale. What we need now is a Controller method to change the locale. I added the following to the Controller.cfc:
<cffunction name="changeLocale" access="public" returnType="void" hint="I change the locale resource bundle">
<cfargument name="event" type="any" />
<cftry>
<cfset session.resourceBundle = CreateObject("component","mgshop.model.resourceBundle") />
<cfset session.resourceBundle.init(rbFile=variables._genConfig.getConfig().localedir & arguments.event.getValue("locale"),
rbLocale=arguments.event.getValue("locale")) />
<cfset arguments.event.addResult("success") />
<cfcatch>
<cfset arguments.event.addResult("failure") />
</cfcatch>
</cftry>
</cffunction>
The try / catch block is just in case the file is missing and such other unforesee events. As you can see, I use the session scope directly. Some people like to use facades and the like, but I have no issue using the session like this. I store the locale directory (e.g. /myapp/model/locale_data) in a simpleconfigbean and the locale is passed in using a url or form variable.
There is the small matter of actually making the active resource bundle available. This adds to Andy Jarret’s piece of code in onQueueComplete:
<cfif isDefined("session.resourceBundle")>
<cfset arguments.event.setValue("rb", session.Resourcebundle) />
<cfelse>
<cfset arguments.event.setValue("rb", variables.Resourcebundle) />
</cfelse></cfif>
In modelglue.xml (or whatever xml file you include into it) add the fourth message-listener into an appropriate controller to listen out for our change locale message:
<controllers>
<controller name="DefaultController" type="mgshop.controller.Controller">
<message -listener message="OnRequestStart" function="OnRequestStart" />
<message -listener message="OnQueueComplete" function="OnQueueComplete" />
<message -listener message="OnRequestEnd" function="OnRequestEnd" />
<message -listener message="changeLocale" function="changeLocale" />
</controller>
</controllers>
Next we need an event handler that we can invoke to change the locale:
<event -handler name="Locale.change" access="public">
<broadcasts>
<message name="changeLocale" />
</broadcasts>
<views>
</views>
<results>
<result name="success" do="User.admin" redirect="true" />
<result name="failure" do="User.admin" redirect="false" />
</results>
</event>
I won’t bother going into detail on the view. This event could be called from a user’s preferences page and could list locales available in a table. You could submit a form with a locale variable (perhaps from a drop-down list), or simply have a url variable in a link.
The important thing is to mirror the language files exactly in terms of the property keys. For example:
File en_GB.properties:
product.singular=Product
product.plural=Products
product.weight=Weight
File fr.properties:
product.singular=Produit
product.plural=Produits
product.weight=Taille
Now, if you are using the Reactor integration (I do not know how this holds for transfer, but it may be similar), you may be familiar with the Validators that Reactor generates. The validators use the generated dictionaries which contain error messages for length and type, where appropriate. For example:
<product>
<productid>
<label>ProductId</label>
<maxlength>0</maxlength>
<scale>0</scale>
<notprovided>The ProductId field is required but was not provided.</notprovided>
<invalidtype>The ProductId field does not contain valid data. This field must be a numeric value.</invalidtype>
</productid>
etc...
</product>
Of course, there are no multi-lingual versions of these generated files. There is nothing for it, I’m afraid, other than either editing these files, or get stuck into reactor to change these messages to language property keys (i.e. entries in the .properties files). For example:
<product>
<productid>
<label>ProductId</label>
<maxlength>0</maxlength>
<scale>0</scale>
<notprovided>error.product.id.notprovided</notprovided>
<invalidtype>error.product.id.invalidtype</invalidtype>
</productid>
</product>
At some point I will investigate how to customise Reactor and cut down on this work.
Then in the properties file:
error.product.id.notprovided=The ProductId field is required but was not provided.
and so on…
These messages are passed back to the caller for display on the view screen. There can be multiple messages, even for the same column, e.g. value was too long and of the wrong type. Model-Glue comes with a custom-tag (/Model-Glue/customtages/validationErrors.cfm) which is called by cfmodule in forms generated by the scaffolding feature in model-glue. However, if you use that, you will simply display the language property key on screen, which may be decipherable, but is not what we want. What we must do is to create a new custom tag of our own, which is similar, but uses the resourceBundle to translate the message for us.
<cfif thisTag.executionMode eq "start">
<cfsilent>
<cfparam name="attributes.field" type="string" />
<cfparam name="attributes.messages" type="struct" />
<cfparam name="attributes.rb" type="any" />
</cfsilent>
</cfif><cfif structKeyExists(attributes.messages, attributes.field)
and isArray(attributes.messages[attributes.field])>
<cfset msgs = attributes.messages[attributes.field] />
<div class="error">
<ul>
<cfoutput>
<cfloop from="1" to="#arrayLen(msgs)#" index="i">
<li>#attributes.rb.getResource(msgs[i])#</li>
</cfloop>
</cfoutput>
</ul>
</div>
</cfif>
As you can see, it is very similar to the Model-Glue custom-tag, though I did take the liberty of renaming a few things to my liking. The main changes are that we now accept the view’s resource bundle as a parameter and output messages from the language properties file. A sample custom tag invocation:
<cfmodule template="/mgshop/customtags/writeMessages.cfm" field="Weight" messages="#validation#" rb="#rb#" />
This approach works very nicely. When the application first loads, a single default resource bundle is loaded into memory in the application scope via coldspring injecting it into the controller. Each user initially will use this until they either select one or have it assigned as they log in, thus firing the Locale.change event. This then loads their resource bundle into their session.
This approach works fine, but it may have an issue with memory usage depending on the number of users, how many of them deviate from the default resource bundle stored in the application scope, and how big the resource bundle files are. There are a couple of suggestions that might improve the situation:
- Store all the resource bundles up-front by injecting all of them into the controller and thus there will be one copy of each in the application scope. Then all that would be stored in the session is the locale id, e.g. the string “en_GB”. Then the appropriate resource bundle can be put into the view state in onQueueComplete. It probably would be fairly easy to build and store a structure of resource bundles in the application scope and access it using the locale from the url/form variable as a key.
- Load the resource bundles into the application scope as they are requested. Have a caching mechanism to time them out if they are large files.
- Split the files up if possible, e.g. one for front-end and another for back-end admin, etc.
Finally I will very briefly address the localisation of data. The previous paragraphs have dealt with the localization of labels, error messages, button text, page headings, etc, but not the data stored in the database. For example, you have a product table with a name column, but then get asked to make the site multi-lingual. You suddenly need to be able to add new language versions of the data on the fly, so you can’t have loads of columns like name_english, name_french, name_german, name_spanish, and so on. In this case you should never hold any descriptive data (names, titles, descriptions, page content, etc) on the main object / table. For example, a product table would contain things like weight, price, date added, etc. But not the product’s name or description. Instead there would be a language table for each object / table that requires names and descriptions. So, for the product table, there would also be a product_language or product_description table that would store each language version of names and descriptions.
A point to remember is that you may need to keep the locale of admin users separate from front-end users. Your web-site may have a backroom staff in 3 countries requiring 3 different languages, so you would have 3 language properties files for the labels and messages in the admin application. However, your front-end site may sell globally and require dozens of languages. The backroom staff will not likely add a language requirement too often, but as the sales and marketing team drive into new markets, they may need to add front-end language on the fly. The lessonis to keep the languages and locales separate, e.g. have a language table and a locale table.
I’ve rambled on too much already to go into the multi-lingual side of things in the database and how to handle this in views and add / update forms. I may come back to it in a future post.
I hope this tutorial is of use to someone. If you did find it useful, please add a trackback or a comment, whether you found it useful or not. It’s nice to know if anyone out there is listening ![]()
Technorati Tags: model-glue, modelglue, coldspring, reactor, localization, coldfusion
3 Responses to “Localization in Model-Glue - Part I”
- 1 Pingback on Apr 18th, 2008 at 7:37 pm
I have implemented suggestion 1 above. Now I have a wrapper component for the resourceBundle component called resourceBundleCollection, which is now a structure containing the locale as a key to retrieve the resource bundle for the locale. I use coldspring to inject the directory path and a comma-delimited list of locales to load. Works a charm. Now I have just the locale string, e.g. “en_GB” stored in the session and I only retrieve the resource bundle each time in the request. This will have a lower memory footprint the more users there are.
I will post up the code solution for this soon. Most of what is above still holds true with the modifications I will post up.
I appreciate you cross-linking to my article, “Localization in Model-Glue 2″. But honestly - you missed something. I mentioned at the bottom of my posting that localized text tokens are for another article. The article provides a way to have localized views - and a fallback method when no localized view is present. Combined with your approach - a full localization internationalization system can easily be modeled.