Posts Tagged ‘i18n’

Easier i18n of Spring error messages

August 4, 2009

When validating fields, Grails provides an easy way to validate complete error messages like “The property Last Name of User cannot be blank”. However, using a default message “The property {0} of {1} cannot be blank”, and providing translations for {0} = Last Name and {1} = User only, is not possible. In this post, we will show you how you can tweak Grails to give you this feature.

Grails i18n introduction

  • With Grails, you can easily put localisable messages in your view. Within a GSP, simply use the <g:message> tag:
    <g:message code="user.create" default="Create User" />

    When Grails encounters this tag, it looks for a code “user.create” in your grails-app/i18n/messages.properties. If Grails cannot find the code, it uses the value of the default argument.

  • By default, there are message.properties files for many languages. If you want to see a page in your application in another language, simply add “?lang=es” to the url. Replace “es” by any ISO language code for which you have provided a translation.
  • The standard scaffolded (and generated) templates of your application are not internationalised. You can replace the standard scaffolding (and generating) templates by your own versions, by simply putting them into src/templates/scaffolding. Writing templates is almost as much fun as writing Lisp macros! But wait, others have already done the work. Simply use the I18n Templates Plugin and you’re done.

Validation messages

  • Grails uses Spring validation for rendering error messages. The messages are looked up in the messages.properties file. There are default messages for all kinds of validation errors. For example:
    default.blank.message=Property [{0}] of class [{1}] cannot be blank
  • Spring automatically substitutes {0} by the property name and {1} by the class name. For example, the user would see the friendly message:
    Property [lastName] of class [com.acme.myfirstgrailsapp.security.User] cannot be blank.
  • It becomes even more user friendly when you translate it. Let’s say in Dutch:
    Attribuut [lastName] van entiteit [com.acme.myfirstgrailsapp.security.User] mag niet leeg zijn.

Translating lastName and com.acme.myfirstgrailsapp.security.User

  • Of course we do not want the user to see texts like “lastName” and “com.acme.myfirstgrailsapp.security.User”. “Last name” and “User” would be better options. Or in Dutch: “Achternaam” and “Gebruiker”.
  • Spring has a mechanism to overwrite the default message for specific classes and properties. If we add
    user.lastName.blank.error=Property last name of user cannot be blank

    Spring will render the above message if the last name of a user is blank. For all other properties that are left blank, Spring will fall back to the default message above.

  • If our application has 20 domain classes with 20 properties, we only have to supply 400 variants of the default message “Property [{0}] of class [{1}] cannot be blank”.
  • But wait, Spring does not only check for blank fields, but also for null fields, minimum values, maximum values, minimum size, maximum size, regex patterns… Each check has its own message. So, we do not only get 400 variants of the “blank” message, but also 400 variants of a null message, 400 variants of a minimum value message, 400 variants of a maximum value message, …

Making it easier

  • Spring enables us to modify and translate the message “Property [{0}] of class [{1} cannot be blank”, but it does not enable us to modify and translate the filled in values for {0} and {1}. Let’s tell Grails to do so.
  • Grails uses the <g:renderErrors> tag to render the above messages. This tag is implemented in the Grails class ValidationTagLib.
  • Filling in the values for {0} and {1} is done in the “message” closure in the ValidationTagLib class. We will provide an alternative implementation of this closure, in our own TagLib. First, generate the tag lib:
    grails create-tag-lib i18n-errors
  • We start by inheriting all default functionality from the ValidationTagLib class, by simply extending it. Also, we will provide a namespace i18m (short for I18n Messages) for the tag lib, so we can type <i18m:renderErrors>. The code of our newly created tag lib is now:
    import org.springframework.web.servlet.support.RequestContextUtils as RCU
    import org.codehaus.groovy.grails.plugins.web.taglib.ValidationTagLib
    import org.springframework.context.NoSuchMessageException
    
    class I18nErrorsTagLib extends ValidationTagLib {
    
      static namespace = "i18m"
      
    }
  • Now insert an adjusted version of the “message” closure. The closure is a copy from the original ValidationTagLib, with a few lines added.
      private String tryResolveMessage(messageSource, String code, locale) {
        try {
            messageSource.getMessage(code, new Object[0], locale) ?: null
        }
        catch (NoSuchMessageException e) {
          null
        }
      }
    
      private String abbreviateFullClassName(String classname) {
        def index = classname.lastIndexOf('.') + 1
        classname.substring(index, index + 1).toLowerCase() + classname.substring(index + 1)
      }
    
      def message = {attrs ->
        def messageSource = grailsAttributes.getApplicationContext().getBean("messageSource")
    
        def locale = RCU.getLocale(request)
        def text
    
        if (attrs['error']) {
          def error = attrs['error']
    
          // We adjusted the original "message" closure here.
          // Instead of just passing the error arguments, we try to resolve the
          // arguments in message.properties.
          def propname = error.arguments[0]
          def classname = abbreviateFullClassName(error.arguments[1].toString())
          error.arguments[0] = tryResolveMessage(messageSource, "${classname}.${propname}", locale) ?: error.arguments[0]
          error.arguments[1] = tryResolveMessage(messageSource, "${classname}", locale) ?: error.arguments[1]
    
          def message = messageSource.getMessage(error, locale)
          if (message) {
            text = message
          }
          else {
            text = error.code
          }
        }
        if (attrs['code']) {
          def code = attrs['code']
          def args = attrs['args']
          def defaultMessage = (attrs['default'] != null ? attrs['default'] : code)
    
          def message = messageSource.getMessage(code,
                  args == null ? null : args.toArray(),
                  defaultMessage,
                  locale)
          if (message != null) {
            text = message
          }
          else {
            text = defaultMessage
          }
        }
        if (text) {
          out << (attrs.encodeAs ? text."encodeAs${attrs.encodeAs}"() : text)
        }
      }

Making it work

  • Replace <g:renderErrors> by <i18m:renderErrors> wherever you wish to use the alternative error rendering.
  • If you placed your own scaffolding templates in src/templates/scaffolding (for example because you’re using the I18n Templates Plugin), you can do the replacement here.
  • Put names for the com.acme.myfirstgrailsapp.security.User class and its lastName property, in messages.properties:
    user=User
    user.lastName=Last Name
  • If you do not put a user.lastName.blank.error in your messages.properties, Grails will use the default message “Property [{0}] of class [{1}] cannot be blank”, with {0} = User and {1} = Last Name.
  • If you do not specify user and user.lastName in messages.properties, Grails will fall back to default behaviour (fully qualified class name and exact property name).
  • Optionally, you can still override specific messages for specific properties of specific classes. But you don’t have to.
    user.lastName.blank.error = Property Last Name of class User cannot be blank,
    unlike the First Name which you can leave out
Advertisements