Internationalization and wro4j - a messages.properties processor

272 views
Skip to first unread message

Ed

unread,
Aug 16, 2012, 8:57:06 AM8/16/12
to wr...@googlegroups.com
I am just looking into how my project might handle the i18n concens of Dust templates and JS generated text. It seems we've landed upon the librbary i18next. Regardless of the library choice however, I'm guessing most client side message resolution is going to require a JSON object containing message keys to message values (in a particular language).

I'm thinking wro4j could help us out here quite a bit - we could make use of its caching and neat framework. So the need for a MessagePropertiesProcessor seems to be emerging. 

What I imagine my wro.xml file might look like:

...
<group name="i18n.myTable">
 
<js>/i18n/messages.properties</js>
</
group>
...

Then my messages_en.properties:

welcomeMsg = Welcome to my site!
userStatus
= Logged in as {user}

And a compiled JavaScript snippet (for English) looking something like this:

if (!i18n)
{
  i18n
= {};
}

i18n
.myTable = {  
 
'welcomeMsg' : 'Welcome to my site!'
 
'userStatus' : 'Logged in as {user}'
}

My MessagePropertiesProcessor might look roughly like this:

...
Locale requestedLocale = // Hmmmm??
ResourceBundle bundle = getBundle(requestedLocale);
...

Now the actual reading of a Java properties file and converting to a JSON object is the simple bit. The bit that I'm looking for advice over is the way in which we might decide which language properties file to read...do I look at the request and check for a GET param 'lang'? I figure this isn't going to work if the resource is cached and then the parameter changes from 'en' to 'fr'; we'd presumably still see the JSON for English, not French.

My current thought is to take a look at the GroupExtractor (and its implementations). Possibly when requesting groups it could be done either by requesting a particular localised version of the resources (which for most resources would be disregarded) or just the default locale's version of the resource.

For example I would request a table.js group in any of the following ways, where all would be identical assuming the group contains no message.properties files:

/wro/table.js
/wro/en/table.js
/wro/fr/table.js

And then my localised text files for specific locales like so:

/wro/i18n.myTable.js // Returns English since it's my default locale
/wro/en/i18n.myTable.js // Identical to above
/wro/fr/i18n.myTable.js // French version

I don't think I've got all of the design figured out yet though - caching is still of concern.The easiest way might be to maintain copies of each resource in the cache as many times as they are requesting in different locales, regardless of whether their content differs.

Is there a better way? Maybe this would be quite neat..having a new PreProcessor interface:

public interface LocaleAwareResourcePreProcessor extends ResourcePreProcessor {
 
void process(final Locale locale, final Resource resource, final Reader reader, final Writer writer)
                     
throws IOException;
}

Thoughts and suggestions..?
  


Alex Objelean

unread,
Aug 16, 2012, 9:19:28 AM8/16/12
to wr...@googlegroups.com
Hi Ed,

Very interesting suggestion.

Here are my first thoughts:
1) There is no need to create new PreProcessor interface, since you can easily access the HttpServletRequest object using Context.get().getRequest() inside processor implementation. Given that, you can easily extract locale using your preferred method (from uri, path or request headers). 
2) The caching might be a little problem, since it is performed on per group name basis. The challenge is to cache group based on locale. There is an open issue which will make it possible in the future. Until then, the following options are available:
  • Each locale sensitive group should have a dedicated group in the model (copy+paste or using group-ref which is not very convenient)
  • Create a ModelTransformer which will clone locale sensitive groups with locale encoded in its name (example: for group table, the following group will be created: table-en, tagle-fr, table-es, etc).
  • Create a custom GroupExtractor implementation and change the way groupName is retrieved based on provided request. Thus, you can check if the url contains a /wro/{locale}/group.js pattern, return group-{locale} as group name. 
Your use-case proves that the cachingStrategy should use more than groupName as a cache key. This requires few (possibly big) changes which might break backward compatibility of the clients which extended CachingStrategy interface. 
I'll add new comments when I'll have new ideas. Also, I'm curious to hear other ideas. 

I will create a new issue to track this use-case.

Thanks,
Alex

Ivar

unread,
Aug 16, 2012, 10:33:33 AM8/16/12
to wr...@googlegroups.com
I think this sounds like a very interesting suggestion. 

Another way to structure the javascript is to use AMD. With AMD you can write custom plugins to also make it possible to define "message bundles" as dependencies. We did this on a project to handle client side localization. 

Ivar



--
You received this message because you are subscribed to the Google Groups "wro4j" group.
To view this discussion on the web visit https://groups.google.com/d/msg/wro4j/-/8FhzIvi47kQJ.

To post to this group, send email to wr...@googlegroups.com.
To unsubscribe from this group, send email to wro4j+un...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/wro4j?hl=en.

Alex Objelean

unread,
Aug 16, 2012, 10:39:30 AM8/16/12
to wr...@googlegroups.com
Hi Ivar,
I would be curious to see a proof of the concept for AMD approach. That would help to find a way to define requirements for future features to be supported by wro4j. 

Btw, another approach for the problem is to generate a single js containing JSON representation of translation for all languages. Example:

i18n = {
  "en": {
     "key1": "value1EN"
     "key2": "value2EN"
  },
  "fr": {
     "key1": "value1FR"
     "key2": "value2FR"
  },
  /* and so on... */
}

The CONS of this approach is that the resulted js can be large and very probably most of the keys won't be ever used. 
The PRO is that it is cached along with other resources and doesn't require a new version if user changes preferred locale.

Cheers,
Alex

Ivar

unread,
Aug 16, 2012, 12:36:38 PM8/16/12
to wr...@googlegroups.com
Actually dojo has also done this [1]. The point of using AMD is that AMD allows you lazy-load scripts until they are needed (and you get rid of all the globals). But this to require that you use AMD in your application [2]. The difference between you module-approach and AMD is just how you define the javascript. In our project the server side approach was tied to customer limitations and we had to do a lot of custom plumbing to retrieve all the messages from a CMS-resource. 

Either way, AMD or not, there must be some server side logic to build up and deliver the messages. It is pretty stright forward to convert a messages.properties to a proper javascript-format. But why can't you just have the messages as regular javascript-files? With the messages.properties approach you still need an additional configuration to specify which message that should be bundled together.

I think the client should specify which locale he/she wants the message bundle in. In the dojo's i18n AMD plugin they have done this by always using the plugin to retrieve the message-modules. An example of this is:
- myApp/nls/de/myResources.js
- myApp/nls/en/myResources.js

AMD allows for better modularization of the javascript and you can just host the the message-bundles in common folder-structures. This way you can just have your application server to handle the caching, as all you are delivering is just static javascript resources. 

It is also common to bundle AMD-modules together in to application layers to reduce the HTTP-request. In our case we found that we really didn't want this for the message-bundles for multiple reasons:
- The messages could be changed at any time, out of our control
- The size of each bundle was small and did not add any noticeable overhead (compressed and gzipped ofcourse) 
- They was cached in the client and at the server

Unfortunately wro4j does not have any support for layering AMD modules yet. There is a wro4j feature request to add support for requirejs, a common implementation of AMD [3]. I hope we can start looking on how to support this soon. 

To view this discussion on the web visit https://groups.google.com/d/msg/wro4j/-/Gy2dQR98PW8J.

Alex Objelean

unread,
Aug 16, 2012, 1:17:02 PM8/16/12
to wr...@googlegroups.com
Ivar,

I have created a dedicated issue for AMD support: http://code.google.com/p/wro4j/issues/detail?id=527
It would be nice to discuss about how this can be implemented. Any ideas or suggestions can be discussed here: https://github.com/alexo/wro4j/issues/60 (I prefer github issues for discussions).

Thanks,
Alex

Ivar

unread,
Aug 17, 2012, 2:21:14 AM8/17/12
to wr...@googlegroups.com
cool ;)

To view this discussion on the web visit https://groups.google.com/d/msg/wro4j/-/C97_L26xwuIJ.

Ed

unread,
Aug 17, 2012, 6:02:45 AM8/17/12
to wr...@googlegroups.com
Thanks for all of those suggestions - I decided to go with option 2; using a ModelTransformer seemed like a nice solution with the code as it currently stands. I have got it working to a point now although what I've noticed is that caching is still a problem. The issue I have is caused by the names of my groups (that are generated by my model transformer). I had chosen to name my groups {locale-code}/{original-group-name}. The issue with this is that the DefaultGroupExtractor#getGroupName method just returns the original group name. What are your thoughts on me changing the behaviour of the getGroupName method? 

Thanks,
Ed

Alex Objelean

unread,
Aug 17, 2012, 6:16:24 AM8/17/12
to wr...@googlegroups.com
One solution could be the way group name are generated by ModelTransformer. If you would use the following naming pattern: group, group-en, gpoup-fr, etc... than everything would work out of the box.
Otherwise, you have to extend GroupsProcessor and inspect the requestPath (check if {locale-code} is set). 

Cheers,
Alex

--
You received this message because you are subscribed to the Google Groups "wro4j" group.
To view this discussion on the web visit https://groups.google.com/d/msg/wro4j/-/AJMxSASBVc4J.

Ludovic Dussart

unread,
Nov 26, 2014, 10:17:07 AM11/26/14
to wr...@googlegroups.com
Hello Alex and Ed,


Do you developped this custom processor ?
 I have the same need so i would like to know if you do anything for this ?

Thanks in advance.

Alex Objelean

unread,
Nov 27, 2014, 5:47:19 PM11/27/14
to wr...@googlegroups.com
Hi Ludovic,

sorry for late reply.

There is an example which might be useful for you. If you have any questions, don't hesitate to ask.

Cheers,
Alex



--
You received this message because you are subscribed to the Google Groups "wro4j" group.
To unsubscribe from this group and stop receiving emails from it, send an email to wro4j+un...@googlegroups.com.

To post to this group, send email to wr...@googlegroups.com.
Visit this group at http://groups.google.com/group/wro4j.
For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages