Loading...

Monday, November 14, 2011

Grails Goodness: Internationalize Javascript Messages with JAWR Plugin

Grails has great builtin support for internationalization (i18n). The underlying Spring support for i18n is used. We can easily change for example text on views based on the user's locale. But this only applies for the server side of our code. So we can generate the correct messages and labels based on the user's locale on the server, but not in our Javascript code. What if we want to display a localized message in a bit of Javascript code, that is not created on the server? Why do I add this extra information 'not created on the server'? Because we can still generate Javascript code in a view or use the gsp-resources plugin to create Javascript on the server. This code can contain the output of a localized message and can be used in Javascript. But that is not what we want for this blog post. Here we are going to reference our i18n messages from plain, non-generated Javascript code.

We can achieve this with the JAWR plugin. The plugin provides roughly the same functionality as the resources plugin for bundling resources efficiently in a Grails application. We are not interested in that part, but the JAWR library used by the plugin also has a i18n messages generator. And we are going to use that in our Grails application to get localized Javascript messages.

First we must install the JAWR plugin: $ grails install-plugin jawr. Next we can configure the plugin. We open our grails-app/conf/Config.groovy file and add:

// File: grails-app/conf/Config.groovy
...

jawr {
    js {
        // Specific mapping to disable resource handling by plugin.
        mapping = '/jawr/'

        bundle {
            lib {
                // Bundle id is used in views.
                id = '/i18n/messages.js'

                // Tell which messages need to localized in Javascript.
                mappings = 'messages:grails-app.i18n.messages'
            }
        }
    }
    locale {
        // Define resolver so ?lang= Grails functionality works with controllers.
        resolver = 'net.jawr.web.resource.bundle.locale.SpringLocaleResolver'
    }
}

...

At line 6 we define a mapping. If we don't define a mapping the JAWR plugin will also act as a resource and bundling plugin, but for this example we only want to use the i18n messages generator.

Line 14 defines which resource in the classpath contains the messages that need to be accessible in Javascript. For our Grails application we want the messages from messages.properties (and the locale specific versions) so we define grails-app.i18n.messages.

With Grails it is easy to switch to a specific user locale by adding the request parameter lang to a request. At line 20 we add a resolver that will use the Grails locale resolver to determine a user's locale.

It it time to see our Javascript in action. First we create two new message labels in messages.properties. One without variables and one with a variable placeholder to show how the JAWR plugin supports this:

// File: grails-app/i18n/messages.properties
js.sample.hello.message=Hello
js.sample.hello.user.message=Hello {0}

Let's add a Dutch version of these messages in grails-app/i18n/messages_nl.properties:

// File: grails-app/i18n/messages_nl.properties
js.sample.hello.message=Hallo
js.sample.hello.user.message=Hallo {0}

Now it is time to create a GSP view with a simple controller (only request through a controller will be able to use the locale resolver we defined in our configuration).

// File: grails-app/controller/grails/js/i18n/SampleController.groovy
package grails.js.i18n

class SampleController {
    def index = {
        // render 'sample/index.gsp'
    }
}
%{-- File: grails-app/views/sample/index.gsp --}%
<html>
    <head>
        <meta name="layout" content="main"/>
        <jawr:script src="/i18n/messages.js"/>
        <g:javascript library="application"/>
    </head>
    <body>
        <h1>Simple message</h1>

        <input type="button" onclick="showAlertHello();" value="Hello"/>

        <hr/>

        <h1>Message with variable placeholder</h1>

        Username: <input type="text" id="username" size="30"/>

        <input type="button" onclick="showAlertUsername();" value="Hello"/>

    </body>
</html>

At line 4 we inlude the Javascript i18n messages generated by the JAWR plugin. And at line 5 we include an external Javascript file that will use the generated messages:

// File: web-app/js/application.js

function showAlertHello() {
    var alertMessage = messages.js.sample.hello.message();
    alert(alertMessage);
}

function showAlertUsername() {
    var usernameValue = document.getElementById("username").value;
    var alertMessage = messages.js.sample.hello.username.message(usernameValue);
    alert(alertMessage);
}

Notice how we can access the i18n messages in Javascript. The plugin will convert the messages to Javascript functions to return the message. And even variable substitution is supported (see line 9).

The following screenshots show the alert messages for the default locale and for a request with the Dutch locale:






With the current configuration of the JAWR plugin all messages in the messages.properties (and locale versions) will be exported to Javascript messages. But maybe this is too much and we only want to include a subset of the messages in the generated Javascript. In the configuration we can define a prefix for the messages to be exported or we can even define a separate properties file with only messages necessary for Javascript:

// File: grails-app/conf/Config.groovy
...
// Only filter messages starting with js.
jawr.js.bundle.lib.mappings=messages:grails-app.i18n.messages[js]
...
// File: grails-app/conf/Config.groovy
...
// Use a different properties file: jsmessages.properties (jsmessages_nl.properties, ...).
jawr.js.bundle.lib.mappings=messages:grails-app.i18n.jsmessages
...

In our Javascript we reference the messages by prefixing messages. to the message properties. We can change this as well in our JAWR plugin configuration. If for example we want to use i18n we must define our plugin as follows:

// File: grails-app/conf/Config.groovy
...
// Define custom namespace for reference in Javascript.
jawr.js.bundle.lib.mappings=messages:grails-app.i18n.messages(i18n)
...

With the use of the JAWR plugin and the i18n messages generator we can easily use localized messages in our Javascript code.

10 comments:

Iván said...

Hello,

the plugin works great but I've a problem with spanish messages. My messages_es.properties is encoded as UTF-8 and if me message is (note the u with the accent):
js.test.message = Ningún

When get the message in the js and write it I get:
Ningún

Is there any way to indicate the encoding?.

Thanks and regards, Iván.

Iván said...

After some google search I reach: http://stackoverflow.com/questions/7042121/extjs-with-jawr-appears-to-be-messing-with-encoding-settings/7069549#7069549

If I execute "grails run-app" it doesn't work, but if I run: "grails run-war" it works properly :-).

Regards, Iván.

Hubert Klein Ikkink said...

@Iván: wow, thank you for finding the answer for the encoding problem. Very useful.

Jackie Bolinsky said...

Hello...

If I understand it right, you're recommending to translate the i18n message bundle into a javascript message bundle. Translation would be done dynamically based on the current visitor's chosen language, right? That does make sense and is probably a good idea to get the text fragments available to JavaScript.

Jackie.
Javascript Refresh Page

Erli Quintana said...

Awesome, this is sooo useful. Thanks for sharing :)

Neil Grover said...

Thanks for this post. It helped me a lot.

I built upon this by adding a javascript function to retrieve text. I felt that referencing properties as javascript functions is a bit fragile and error prone. For example, if you have javascript code that references an i18 property that no longer exists, then a javascript error is thrown. My code below handles that. If a property doesn't exist then you wind up with the property key being displayed in you're web-page (which is easy to notice and therefore you can fix it).




window.Text = function () {

return {
get:function (prop) {
var handler = window.messages;
var tokens = prop.split('.');
for (var i = 0; i < tokens.length; i++) {
handler = handler[tokens[i]];
}
var substitutes = Array.prototype.slice.call(arguments).slice(1);
return typeof handler === 'function' ? handler.apply(this, substitutes) : '{'+prop+'}';
}
}
}();




Assuming you have a key defined as:
Then you can use the code above as followed:

Text.get('default.not.found.messagesd', 'Something', 'blah');

That will return: "Something not found with id blah"

If the key is not defined, it will return: "{default.not.found.message}"

JDuf said...

Have you had any success using this plugin with Grails v2.0.x? I've been struggling with it for a couple of days. The root issue seems to be that the "/script" URI can't be found - which leads me to believe that the jawr servlet isn't starting up properly (although I get no error messages during startup).

Thanks.

Niko Mäkelä said...

Thanks to Neil Grover for his idea, I made his function to act like g.message -tag, so you can use code, default and args attributes.

Call function in js:
----------------
i18n.message({"code":"js.translation.code.message", "default": "Default text", "args": [$("#elementId").val()]});


i18n-localization function:
-----------------------
window.i18n = function() {

return {

"message": function(message) {

var handler = window.messages; // Messages made by JAWR plugin
var tokens = message.code.split('.'); // Split code

for (var i = 0; i < tokens.length; i++) {
if (tokens.hasOwnProperty(i) && handler.hasOwnProperty(tokens[i])){
handler = handler[tokens[i]];
}
}

var substitutes = message.hasOwnProperty("args") ? message["args"] : [];
var defaultText = message.hasOwnProperty("default") ? message["default"] : "";

var returnText = typeof handler == 'function' ? handler.apply(this, substitutes) : defaultText;

return returnText;
}
};
}();

Anonymous said...

Error processing GroovyPageView: Error executing tag : null

Anonymous said...

Thanks for this great example!
It works perfect in development mode but in production i get following error:

###
FATAL servlet.JawrGrailsServlet - Enable to find the resource bundle : grails-app.i18n.messages[js]
net.jawr.web.exception.BundlingProcessException: Enable to find the resource bundle : grails-app.i18n.messages[js]
at net.jawr.web.resource.bundle.locale.ResourceBundleMessagesGenerator.getAvailableVariants(ResourceBundleMessagesGenerator.java:124)
at net.jawr.web.resource.bundle.generator.GeneratorRegistry.getAvailableVariants(GeneratorRegistry.java:552)
at net.jawr.web.resource.bundle.factory.PropertiesBasedBundlesHandlerFactory.buildCustomBundleDefinition(PropertiesBasedBundlesHandlerFactory.java:305)
at net.jawr.web.resource.bundle.factory.PropertiesBasedBundlesHandlerFactory.(PropertiesBasedBundlesHandlerFactory.java:155)
at net.jawr.web.servlet.JawrRequestHandler.initializeJawrConfig(JawrRequestHandler.java:507)
at net.jawr.web.servlet.JawrRequestHandler.initializeJawrContext(JawrRequestHandler.java:312)
at net.jawr.web.servlet.JawrRequestHandler.initRequestHandler(JawrRequestHandler.java:272)
at net.jawr.web.servlet.JawrRequestHandler.(JawrRequestHandler.java:227)
at net.jawr.web.servlet.JawrGrailsServlet.init(JawrGrailsServlet.java:68)
at javax.servlet.GenericServlet.init(GenericServlet.java:160)
at org.apache.catalina.core.StandardWrapper.initServlet(StandardWrapper.java:1266)
at org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1185)
at org.apache.catalina.core.StandardWrapper.load(StandardWrapper.java:1080)
at org.apache.catalina.core.StandardContext.loadOnStartup(StandardContext.java:5015)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5302)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:895)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:871)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:615)
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:958)
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1599)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)
at java.util.concurrent.FutureTask.run(FutureTask.java:166)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1146)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:701)
###

I am using jawr 3.5.1 and grails 2.3.6.
Can you help, please?
Thanks,
Paul

Post a Comment