Disappearing apostrophes in localized messages of Grails application
We have witnessed a strange phenomenon recently. Well, when I say ‘strange’ I mean it looked very strange when the mystery wasn’t resolved, but when you come to understand it, it is not strange at all, and has perfect explanation, but first things first.
What happened?
Initially, we had a controller that would send an e-mail message and then inform the user that the message was sent:
class EmailSendingController {
def sendSomeMail(EmailRequestCommand cmd) {
// .... checking validity of the command's parameters here
sendMail {
to cmd.emailAddress // E-mail address is a request parameter. It is properly validated of course
// .... doing some e-mail sending work here - standard Mail Plugin stuff
}
flash.message = message(code: "success.email.sent.label", default: "E-mail sent")
redirect(mapping: "somewhere")
}
}
Being a developers who adhere to good practices we keep all of our messages internationalization-ready and always define them in messages.properties
file. In this case, we had a message defined like so:
success.email.sent.label=We've sent you an e-mail
# .... some other messages of our application
So far everything was going well, and the message “We've sent you an e-mail” (without the quotes, of course!) was appearing on the redirected page. But at some later stage we decided it would be a good idea to tell the user where the message was sent to (of course, it would be the address they entered, but what if they forgot what they entered? We’d better remind them, we thought). And so the flash.message
line was changed into:
flash.message = message(
code: "success.email.sent.label",
args: [cmd.emailAddress.encodeAsHTML()], // Never forget to encode what is output in HTML page!
default: "E-mail sent to {0}")
And changed message in messages.properties
file now looked like this:
success.email.sent.label=We've sent an e-mail to {0}; read it and rejoice!
But this time something went awry. Now we were receiving this message: “Weve sent an e-mail to {0}; read it and rejoice!” on the redirected page. The e-mail address we entered was not displayed, and the argument placeholder (the {0} part) was appearing clearly in the message for the users to see! At first we thought we misspelled the args
parameter to the message tag/method, or perhaps it should not be args
, but something completely different? But no, in other places it was certainly the args
parameter that was used, and it had worked perfectly in those places, just not in this particular one. As it is always the case with situations when you don’t completely understand something, you start tinkering. So did we. We tried removing the args
parameter, passing empty list, passing more items in the list than needed, explicitly converting the list into an array, changing the index of the placeholder (from {0} to {1}), passing e-mail’s text with and without encodeAsHTML()
call — everything. All of this went to no avail — the actual e-mail address would not appear in the resulting message.
The solution
And then some eagle-eyed chap from our team spotted the absence of an apostrophe on the page. Indeed, in messages.properties
file the apostrophe was present: "We've sent ...
", but not on the resulting HTML page: it definitely said “Weve sent ...” there; the apostrophe was not present even in the HTML source of the page. Apparently there was a problem with the message in the properties file itself, where apostrophes have to be escaped, like so:
success.email.sent.label=We''ve sent an e-mail to {0}; read it and rejoice!
After doing exactly that, the e-mail address did finally appear on the HTML page, voila! (Apostrophe was also back, but that is a minor point for such a result.)
Why was it so?
But why did the apostrophe appear with the first variant of the message (yes, we double-checked, it did appear there, even without escaping; in fact, if escaped, there would be two apostrophes in the final output), and had to be escaped when we later included a placeholder? The answer lies somewhere (not so deep) inside the AbstractMessageSource class of Spring Framework. It turns out, there is an optimization of some sort in place which does not pass the text through MessageFormat, but simply outputs it as is, if there are no arguments specified. So if you do not specify args
parameter, or pass null
or empty array/list, that will count as no arguments, and the text of your message (whatever it is) will be returned without any processing, effectively meaning that any escaping, if present, will not be processed. If, on the other hand, you do specify at least one formatting argument, then the text of your message will be formatted (even if it does not contain any placeholders in it), and for that matter it has to be escaped appropriately (for the rules of escaping refer to the documentation of the MessageFormat
class itself).
This actually poses a problem (and a big one, as it is not documented anywhere, not in official Grails docs at least). Let’s consider the following error message you might have defined:
register.username.maxSize.error=Username shouldn't be longer than 32 characters
You see, here you do not use any placeholders for formatting arguments, but that is irrelevant. Because internally, when the error message with this code is displayed, the formatting arguments are passed to the respective method of the AbstractMessageSource
class, and it will pass the text through message formatter, and because the apostrophe is not escaped, it will disappear.
All right, you might say, simply remember then that error messages must always be properly escaped. And so should any messages using formatting arguments placeholders. But what about the messages (ordinary messages, not the error ones) that come without visible placeholders and do have arguments passed for formatting in your code? This may be due to some compatibility reasons, or simply because it is clearly a bug: you added or removed arguments in one place, but forgot to do the same change in the other. This type of bug could be hard to catch if in the language you are developing in, the message does not have to be escaped, but when later translated by some unsuspecting interpreter, the message in the target language does have to be escaped.
Is there some permanent solution?
Well, actually there is. It is rather less efficient, because it turns off this shortcut of passing text of the message without processing, but at least you will know that all of your messages should be escaped.
You see, the people who developed Spring Framework did put an option in there to always pass messages through MessageFormat
. Unfortunately, when Grails later instantiates the message source in its i18n plugin, it does not set this option. Not even through configuration can this be changed (although this would be very preferable).
So, one of the possible fixes would be writing your own internationalization plugin and then throwing away the original one. And your own plugin would set then that option to true
. Going this way is relatively easy, as the original plugin consists of a single file. Yet, this approach still requires too much hassle for such a minor addition. Especially in the light of availability of another way.
Since the message source defined by the standard i18n plugin (and used for all localization in Grails application) is a Spring bean, you can easily inject it into another class of your application. Even into BootStrap.groovy
. See where this is going?
class BootStrap {
def messageSource // Inject the message source
def init = { servletContext ->
// Turn on the option that tells Spring always to pass messages through MessageFormat class
messageSource.alwaysUseMessageFormat = true
// .... do other initialization stuff for your application
}
def destroy = {
}
}
After doing this, the texts of all of messages in your application will be passed through MessageFormat
class, even when the list of formatting arguments is emtpy. Just, don''t forget to escape all of your messages, all right?
Shameless plug
We develop mobile and web apps, you can hire us to work on your next project.