Fork me on GitHub
Showing posts with label i18n. Show all posts
Showing posts with label i18n. Show all posts

2 Aug 2011

Avoiding accidental i18n in Grails

We’re developing an app that’s exclusively for a UK audience so i18n really isn’t an issue for us. However recently we got bitten by some i18n creeping in where we didn’t want it. Specifically, when using Grails’ g:dateFormat tag the default behaviour is to format the date according to the Locale specified in the user’s Accept-Language header. Even though we are explicitly specifying a format pattern for the date Java is aware of localized day names for some languages so the output can vary. The result is that on a page full of English text there suddenly appears a Spanish or Swedish day name. What makes things worse is that as we use server-side content caching and a CDN if a user with a non-English Accept-Language header is the first to see a particular page or bit of dynamically retrieved content then the cache is primed and until it expires everyone will see the non-English day name text.
The solution in a Grails app is as simple as replacing Spring’s standard localeResolver bean with an instance of FixedLocaleResolver. Just add the following to grails-app/conf/spring/resources.groovy:
localResolver(org.springframework.web.servlet.i18n.FixedLocaleResolver, Locale.UK)
This changes the way Spring works out the request locale and any locale-aware tags should just fall into place.

24 Sept 2010

Stubbing access to the g:message tag in unit tests

Grails controllers and tag libs can access any tag as though it were a method. The most common use for this is probably accessing i18n messages via the g:message tag. However, because tag access is magic wired up by Grails it's not available in unit tests without some effort.

This is a perennial nuisance. Not exactly difficult to solve and yet something I find myself solving over and over in different tests and different projects.

I've come up with what I think is a pretty good and re-usable solution. It allows you to specify messages if you want to, or just use the message code if it's not something you care about in a particular test. As an aside, at a unit test level, I think testing that the correct message code is being used is probably the right thing to do; I'd leave testing actual message text to end-to-end tests.

Here's an example. Imagine we're testing the following controller that displays a simple greeting:

Here's a spec that shows the behaviour both when a message is specified and when it isn't:

A few things to note:
  1. The stubbed message tag returns the code if there is no message defined
  2. message arguments are ignored unless there is a message defined

Although in the example I've used Spock, this technique would work equally well with JUnit tests extending ControllerUnitTestCase. It will also work just as well for tag libs tests extending TagLibUnitTestCase or TagLibSpec.

10 Mar 2009

Internationalizing Domain Classes and Enums

It's common to provide a toString implementation on a domain object that may well end up being used in the view. However, this doesn't allow for internationalization in the view. A good solution that I've used a number of times is to have classes implement Spring's MessageSourceResolvable interface. Consider this domain class that represents an image file:

grails-app/domain/Image.groovy

class Image {
    String name
    String path
    User uploadedBy
    org.joda.time.DateTime dateCreated

    static transients = ['file']
    File getFile() {
        new File(ConfigurationHolder.config.image.base.dir, path)
    }

    String toString() {
        "$name uploaded by $uploadedBy.username on $dateCreated - ${file.size()} bytes"
    }
}

The toString implementation is all well and good if we're targeting an English-speaking audience but with some simple changes we can make it i18n compliant:

grails-app/domain/Image.groovy

class Image implements org.springframework.context.MessageSourceResolvable {

    // properties as above

    static transients = ["codes", "arguments", "defaultMessage"]

    Object[] getArguments() {
        [name, uploadedBy.username, dateCreated.toDate(), file.size()] as Object[]
    }

    String[] getCodes() {
        ['image.info'] as String[]
    }

    String getDefaultMessage() {
        "$name uploaded by $uploadedBy.username on $dateCreated - ${file.size()} bytes"
    }
}

In our message properties file we can add:

grails-app/i18n/messages.properties

image.info={0} uploaded by {1} on {2,date} - ${3,number,integer} bytes

In the view we can display our object like this:

<g:message error="${imageInstance}"/>

Yes, that is the error attribute we're passing to the message tag! Grails intends the attribute to be used for outputting validation errors but the underlying mechanism is the same - Spring's ObjectError implements MessageSourceResolvable and that's how Grails' message tag resolves the displayed error message. Rather than passing separate code, args and default attributes to the tag we can pass the single MessageSourceResolvable instance and its implementation will take care of supplying those values.

Note: I added a message attribute to the message tag to avoid the confusion caused by using error. This is in Grails from version 1.2-M1 onwards.

We can now add translations of our object description. For example:

grails-app/i18n/messages_af.properties

image.info={0} opgelaai deur {1} op {2,date} - ${3,number,integer} grepe

It's worth noting that the format of the dateCreated property will be automatically determined by the request locale so the value will be formatted correctly for the user.

I've found this technique can also be very useful on enum classes. For example:

src/groovy/com/mycompany/Season.groovy

package com.mycompany

enum Season implements org.springframework.context.MessageSourceResolvable {
    SPRING, SUMMER, AUTUMN, WINTER

    Object[] getArguments() { [] as Object[] }

    String[] getCodes() {
        ["${getClass().name}.${name()}"] as String[]
    }

    String getDefaultMessage() { name() }
}

grails-app/i18n/messages.properties

com.mycompany.Season.SPRING=Spring
com.mycompany.Season.SUMMER=Summer
com.mycompany.Season.AUTUMN=Autumn
com.mycompany.Season.WINTER=Winter

grails-app/i18n/messages_en_US.properties

com.mycompany.Season.AUTUMN=Fall