eskatos's thoughts

Customer i18n overlays with GWT

with 7 comments

GWTI’m building a web backoffice for an IT product with GWT for several month now. I had to internationalize all the UI. GWT provides several mechanisms for i18n. I use Constants and Messages because I love the compile time check and the fact that deferred binding permutations provide a download as small as possible to the users.

All this worked well until we wanted to provide customers the ability to change how things are named in our product. In the UI layer of course !

Like said above, GWT is using deferred binding to provide a Constant/Message implementation per locale. I extended this mechanism to provide a Constant/message implementation per locale and a given GWT property.

Enough talk, lets see how this is done.

Classic i18n

Here we will use the Constants approach, this is working with Messages too. For the purpose of this example we will use only one message “businessKey” that is called “Business Key” in our product but that some customers will like to name “DaGlobalUniquePersonnalIdentifierToRuleThemAll” or anything else they want.

Following the GWT documentation about static string internationalization, create an interface that extends Constants :

    public interface MyI18N extends Constants {
        String businessKey();
        String anotherMessage();
    }

In the same package, provide a properties file for each locale you want to support :

MyI18N.properties :
    businessKey=Business Key
    anotherMessage=Another message

MyI18N_fr.properties :
    businessKey=Identifiant
    anotherMessage=Un autre message

All this is not enough to get i18n to work with GWT, you have to modify your gwt.xml file. Read the documentation for more information.

The first i18n overlay

Now we want to add an i18n overlay to change how “businessKey” is named for only one customer.

Create an interface that extends the previous MyI18N interface :
    public interface MyCustomI18N extends MyI18N {}

This interface can be empty as we only want to provide customized existing messages. Now create a properties file for each locale you want to support :

MyCustomI18N.properties :
    businessKey=DaGlobalUniquePersonnalIdentifierToRuleThemAll

MyCustomI18N_fr.properties :
    businessKey=IdentifiantGlobalUniquePersonnelPourLesControlerTous

Here we declare only the message(s) we want to overwrite in this i18n overlay.

Adding a GWT property

We now need a gwt property that will trigger the use of an i18n overlay. We will name it “flavour”. For this you have to modify you host page and you gwt.xml file.

We will have two possible value for this property : “default” and “customer1″.

Add the flavour property in a meta tag inside your host page :
    <meta name="gwt:property" content="flavour=customer1" />

Now you know that with some server side code you can change this property in your host page depending on various configuration details. It’s up to you.

Finally, add theses lines in your GWT module gwt.xml file :

    <define-property name="flavour" values="default,customer1" />
    <property-provider name="flavour">
    <![CDATA[
        var flavour = __gwt_getMetaProperty("flavour");
        if (flavour == null){
            flavour = "default";
        }
        return flavour;
    ]]>
    </property-provider>

This snippet will allow us to use this property’s value for deferred binding.

Writing an i18n factory

Next, we need a way to defer to runtime the choice between My18N and MyCustom18N constants. We need to add a permutation layer on top of the GWT built-in locale permutation. We will do this with a simple factory.

First, write the MyI18NFactory interface :
    public interface MyI18NFactory {
        MyI18N getMyI18N();
    }

Next, write the default implementation, aka MyDefaultI18NFactory :

    public class MyDefaultI18NFactory implements MyI18NFactory {
        private static final MyI18N i18n = (MyI18N) GWT.create(MyI18N.class);
        public MyI18N getMyI18N() {
            return i18n;
        }
    }

And the implementation for customer1, aka MyCustomerOneI18NFactory :

    public class MyCustomerOneI18NFactory implements MyI18NFactory {
        private static final MyI18N i18n = (MyI18N) GWT.create(MyCustomI18N.class);
        public MyI18N getMyI18N() {
            return i18n;
        }
    }

The only difference is the targeted i18n implementation.

Deferred binding

The final step is to add deferred binding for our MyI18NFactory implementations so that depending on the “flavour” property, GWT will instanciate MyDefaultI18NFactory or MyCustomerOneI18NFactory.

Simply add the following to your GWT module gwt.xml file :

    <replace-with class="my.MyDefaultI18NFactory">
        <when-type-is class="my.MyI18NFactory"/>
        <when-property-is name="flavour" value="default"/>
    </replace-with>
    <replace-with class="my.MyCustomerOneI18NFactory">
        <when-type-is class="my.MyI18NFactory"/>
        <when-property-is name="flavour" value="customer1"/>
    </replace-with>

Usage

To get the right i18n Constants implementation in your code you now can use the following statement :

    ((MyI18NFactory) GWT.create(MyI18NFactory.class)).getMyI18N().businessKey();

Of course you would like to cache the MyI18N instance as instanciation is an expensive task.

I don’t know if this is the simplest or the beautifulest way to do this. If someone has a better pattern I’ll be glad to learn ! :)

About these ads

Written by Paul

July 19, 2008 at 6:38 pm

Posted in Code, HOWTOs

Tagged with

7 Responses

Subscribe to comments with RSS.

  1. Er, I might have missed something (I mostly overlooked at the article) but why do you need an I18NFactory? Why cannot the GWT.create(MyI18N.class) vs. GWT.create(MyCustomI18N.class) be used directly? AFAIUI, generate-with/replace-with are processed in reverse declaration order; so replacing my.I18N with my.CustomI18N when property flavour is customer1 should occur before the generate-with rule from the inherited com.google.gwt.i18n.I18N module.

    Could you confirm? (please CC me by mail)

    Thomas Broyer

    July 21, 2008 at 4:18 pm

  2. @Thomas :
    From what I understand, if I do get the property in my client code and depending on its value I use GWT.create(MyI18N.class) or GWT.create(MyCustomI18N.class) the two i18n implementation would be downloaded to the browser. This is why I’m using the i18n “factory” as a code step allowing me to use the deferred binding mechanism.

    Next, about the order of generate-with/replace-with processing, are you telling me that I shall put the replace-with directive above the gwt I18N module inherit. ? Do you mean that it’s good practice to put inherits directives at the bottom of a gwt.xml file ?

    eskatos

    July 21, 2008 at 6:55 pm

  3. What I said was:

    From what I understood reading the GWTCompiler’s code, in the permutations where flavour==customer1, MyI18N is replaced with MyCustomI18N and then the generator is applied, taking into account your MyCustomI18N.properties.
    In the permutations where flavour != customer1, MyI18N is used as-is by the generator.
    …and good practice would be to put the inherits at the *top* of the gwt.xml files.

    Thomas Broyer

    August 8, 2008 at 12:21 pm

  4. Grrr!
    With the markup (hoping it’ll pass this time):
    <module>
    <inherits name=”com.google.gwt.i18n.I18N” />

    <replace-with class=”my.MyCustomI18N”>
    <when-type-is class=”my.MyI18N” />
    <when-property-is name=”flavour” value=”customer1″>
    </replace-with>
    </module>

    Oh, and I would rather make different gwt.xml for each customer, using <set-property> to force the flavour property to “customer1″ or “default” and calling the GWTCompiler on each gwt.xml to generate customer-specific javascript…
    That way, when a customer wants/needs to change some message/constant value, you only have to recompile and deploy that customer’s module.

    Thomas Broyer

    August 8, 2008 at 12:30 pm

  5. Hello Thomas,

    Thanks for your comment and for pointing that I over complicated the deferred binding with my factories. I’ll post a blog correcting this.

    And about the gwt.xml for each customer, I understand your point of view but I’m not in the process of handling several modules yet. I’ll think about it.

    Regards

    Paul

    eskatos

    August 8, 2008 at 2:50 pm

  6. OK, it seems I misread the GWTCompiler code: the first matching rule applies and then rebinding stops (note that rules are evaluated in reverse parse order: in my example code, if the replace-with rule matches, the generate-with rule from the com.google.gwt.i18N.I18N module won’t be evaluated; so the my.I18N interface is effectively replaced with the my.CustomI18N one, but then the GWTCompiler tries to use it as-is –and will fail, as it’s not a class–, it doesn’t call the generator).

    Thomas Broyer

    September 11, 2008 at 2:41 pm

  7. Hey Thomas,

    It’s nice to see you coming back here :)

    I didn’t came back on this subject because what is explained in this blog is working and in production. So, if I follow you I had to put my “factories” between the i18N base mechanism and the deferred binding between my i18n classes.

    Paul

    eskatos

    September 11, 2008 at 3:34 pm


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: