Friday, April 16, 2010

Reading i18n messages from the database with Grails

In a recent consulting engagement, a client wanted to know how to go about reading i18n messages from the database rather than static properties files (the default in Grails). Considering how easy this is to do I was surprised when I Googled it that there was no information on how this is achieved.

Anyway it's dead simple. Just create a domain class that models a message:

class Message {

String code

Locale locale

String text

}


Then implement a class that extends the org.springframework.context.support.AbstractMessageSource class. In the example below I am using simple GORM finders to lookup a message using the code and locale

class DatabaseMessageSource extends AbstractMessageSource {


protected MessageFormat resolveCode(String code, Locale locale) {

Message msg = Message.findByCodeAndLocale(code, locale)

def format

if(msg) {

format = new MessageFormat(msg.text, msg.locale)

}

else {

format = new MessageFormat(code, locale )

}

return format;

}

}


Then wire it in using Spring by configuring a "messageSource" bean in the grails-app/conf/spring/resources.groovy file:

beans = {

messageSource(DatabaseMessageSource)

}


And that's it. Now you're serving messages from the database. Of course this is a terrible inefficient implementation since we're hitting the database for ever message code used in the application. However, it's pretty easy to introduce caching. Just create a cache key:

@Immutable

class MessageKey implements Serializable {

String code

Locale locale

}


Then configure an appropriate cache bean (I'm using Ehcache) in Spring and wire it into your MessageSource:

beans = {

messageCache(EhCacheFactoryBean) {

timeToLive = 500

// other cache properties

}

messageSource(DatabaseMessageSource) {

messageCache = messageCache

}

}


Finally, update your implementation to use caching:

class DatabaseMessageSource extends AbstractMessageSource {


Ehcache messageCache

@Override

protected MessageFormat resolveCode(String code, Locale locale) {

def key = new MessageKey(code,locale)

def format = messageCache.get(key)?.value

if(!format) {

Message msg = Message.findByCodeAndLocale(code, locale)

if(msg) {

format = new MessageFormat(msg.text, msg.locale)

}

else {

format = new MessageFormat(code, locale)

}

messageCache.put new Element(key, format)

return format

}

return format;

}

}




5 comments:

Peter Ledbrook said...

Thanks for posting the info on this. Might be good to get it into the documentation.

On another note, wouldn't this be a good use case for a Hibernate query cache? The message table is unlikely to change much and there aren't any joins. It would save having to manage your own cache.

Graeme Rocher said...

You're right, this would be better done using the query cache. So basically this is the Message class:

static mapping = {
cache true
}

And you're done.

Unknown said...

Hi Graeme,

I've used it using Peter's suggestion and works very nicely.

Thank you both for this info!

Best regards

hus said...

I used to try very hard to do it
because google return nothing.
By the way! Thanks for the caching
example because my implementation
uses Map(not efficient memory usage).

j pimmel said...

Its a timely post since am doing some caching anyway, and the approach shown is useful for other applications :) Cheers!