Tuesday, 21 June 2011

Madura Rules Part 5

I've been working on, and posting on, other topics but I did say I would mention I18n issues and how Madura Rules handles them. In fact the heavy lifting is done in Madura Objects, when it isn't done by standard Java features. If you don't know about I18n at all then go here.

The specific problems we need to solve with this are:
  • Labels for fields should be in the selected language
  • Drop down lists should be populated in the correct language
The first thing to notice is that in a multi-user application, like any web app, we have a problem. Web requests arrive with a locale specified, which is great, but we need it 15 call levels into the application, which is not. Either we pass the locale in a lot of methods or we do something smarter. We could use Locale.setDefault() but that is not specific to this request. Other requests will be forced to use our setting, or we will be forced to use theirs. The setting is JVM wide. Not what we need.

We have a similar issue with the .properties file. That is a little different because we can use Spring to inject the file where we want it, this will automatically pick the right version of the file according to our locale. But it is too limiting to have everything in .properties files and there are lots of dynamically instantiated classes that I need to inject it into, so I can't use Spring there.

We can solve all these issues with a small factory class called MessageTranslator. While I almost always use Spring injection this time I'm using a factory because it gives me a simple way to get a class that is local to the current thread and hence to the local request. I know Spring provides various scopes for beans but this is simpler.

So, MessageTranslator allows me to construct an instance of the class and store it on a ThreadLocal. The class holds the Locale and the MessageResource. The MessageResource is Spring's nice way to handle those .properties files. But we will come back to that.

Early in the processing of the request, ie when I still have easy access to the locale, I create a MessageTranslator for this thread with the locale and the MessageResource. Thereafter I can get a locale based message like this:
MessageTranslator.getMessageTranslator().getMessage(String code)
The getMessage() method is overridden to allow me to pass arguments and a default message if I need it. But the point is I do not need to pass the locale or event the MessageResource. Nor do I need to inject the MessageResource to use it.

The code is simple enough and you can find it in Madura Objects at
nz.co.senanque.localemanagement.MessageTranslator

So what do I use this for exactly?

First, whenever Madura Objects fetches a field label it calls the MessageTranslator assuming the name being used is actually a key to the resource. So if you have a field labelled 'amount' then it will look up your properties file for that key and return what it finds. For the English properties file you would have amount=amount but for the French file you would perhaps use amount=quantite. Also, if you failed to put a label on a field then the field name will be used.

To make this work you create a bean in the Spring file like this:
<bean id="messageSource"
 class="org.springframework.context.support.ResourceBundleMessageSource">
 <property name="basenames">
 <list>
  <value>Messages</value>
 </list>
 </property>
</bean>
and then inject that somewhere convenient for you to create the MessageTranslator for this request. This variant shows that you can have a list of properties files, though we only used one here. This is Messages.properties. The French variant would be Messages_fr_FR.properties but you don't need to specify it in your Spring file, it will be found automatically.

So far so good. We can deliver the right labels for the language. But what about the values in the drop down?

Here it gets a little tricky because, while we can just create a properties file for everything, we may be loading values from a database or other external source. Those values might change and we won't want to rebuild our application with a new properties file every time they do. We also want to be able to maintain the alternate language data in a similar way to the primary data. That means if it the primary data is stored in a database we want the translated names stored there too, not in a properties file we forget to update.

Fortunately Spring helps us out here. We can just adjust our messageSource bean so that it looks like this:
<bean id="messageSource" class="nz.co.senanque.i18n.XMLMessageSource">
 <property name="resource" value="classpath:/Messages.xml"/>
 <property name="parentMessageSource">
 <bean class="org.springframework.context.support.ResourceBundleMessageSource">
  <property name="basenames">
  <list>
   <value>Messages</value>
  </list>
  </property>
 </bean>
 </property>
</bean>
I'm using a custom class (XMLMessageSource) that extends org.springframework.context.support.AbstractMessageSource. AbstractMessageSource allows you to inject a parent source and in this case I am using my Messages.properties file. If XMLMessageSource fails to find an entry it will delegate to the parent and look there.
My XMLMessageSource is fairly simple, it looks up an XML document for the right entry and it handles multiple locales which may be specified in the document. I use this for testing and illustration rather than production. You can replace this with a class that looks up a database or whatever. The net effect is that any lookups go to the first class and then to the second (and possibly to more places if you can use that).

When an application is building a drop down list it calls nz.co.senanque.validationengine.FieldMetadata.getChoiceList() which gives a list of ChoiceBase objects. Each of these has a key and a description. The key is the real value, the value we want to store in a database etc and the value we would compare with in a rule. The description is the display value. If we call ChoiceBase.toString() we get the translated description. So the description becomes the lookup code and you can store the codes and the translation for each language in the same place.

It depends on your UI architecture how well this actually works, but it works very nicely with Vaadin. Vaadin supports POJOs as updateable UI objects and Madura Objects are exactly that, though they have extra metadata as well which we want to expose to the UI. But more about UI next time.

There is one other I18n issue to cover and that is the messages generated from rules when there is an error. Recall that we can have a rule that look like this:
constraint: Customer "check the count: {0}" [invoiceCount]
{
 invoiceCount < 10L; }

This says we only allow up to 9 invoices on a customer (just an example, not something you would do in the real world). If this constraint is violated the message 'check the count...'
will be generated and the argument(s) listed in brackets will be applied. Now, what if you want this message in French?

When the rules are generated a properties file is generated along with the Java called messages.properties and all of these messages are placed in it with appropriate labels. The MessageTranslator is used to resolve the message code so this messages.properties file must be
included in your messageResource bean somewhere for it to work.

You can, then, supply alternate messages.properties files of your own, eg one in French. As long as the locale MessageTranslator is initialised properly the right things will happen.

There is another properties file that is used by the validation engine in Madura Objects. This is ValidationMessages.properties and it holds all of the error messages generated by the validation. As usual it must be part of your messageResource bean. You can supply your own translations of these as required. So your messageResource bean will look something like this:
<bean id="messageSource" class="nz.co.senanque.i18n.XMLMessageSource">
 <property name="resource" value="classpath:/Messages.xml"/>
 <property name="parentMessageSource">
 <bean class="org.springframework.context.support.ResourceBundleMessageSource">
  <property name="basenames">
  <list>
   <value>Messages</value>
   <value>ValidationMessages</value>
   <value>com.mydomain.rules.messages</value>
  </list>
  </property>
 </bean>
 </property>
</bean>

Next time I will post about using all this in a Vaadin application. There is, it turns out, very little to do.
Post a Comment