Struts Applications and Multilanguage Support
In a world that is getting smaller every day, the software developer's job is changing. Just a few months or years ago, you wrote software for your company that covered only this company's particular location or region. Now that you switched from native applications to a wider range, supporting more than one region and/or country, some of you are switching to J2EE, Web Applications, and (in search of a powerful framework) getting in touch with Struts.
This article introduces you to Struts and shows you how to build an application that can support multiple languages, preserve the MVC pattern, and help you to easily maintain your code. First, I discuss the principles of building multiple language support, including the different choices you have and showing you the enhanced possibilities that you can have with Struts.
Options for Multilanguage Support
There are many opportunities to deliver multiple language support to an application, and each one has positives and negatives.
User's Choice
One of the simplest ways to support more than one language is to provide a set of static HTML pages for each language you want to support. Just consider one HTML page as your starting point for an application and have a link for each language (for example, "Click here to view this site in English").
After this point, you can have separate page sets, but multiple times with the same application logic. If you think about it, it's not very handy for a user because he has to click on a link to only start this particular application. And it's not very friendly for your content team or you as the developer. You would run into the worst-case scenario: maintaining code that is equal in multiple sets of pages or servlets.
Server's Choice
The next step would be: "Hey, the user's browser is adjusted to a particular region; let me think of it. The browser tells me which region and language the user might like to see with every request to my server..."
Let's have a look at the request, sort it out, and redirect the user to the correct set of pages.
One of the neat things that HttpServletRequest provides is a simple way to retrieve the locale that is selected in the user's browser:
java.util.Locale currentLocale = request.getLocale();
Now that you can get the locale, you can let the server automatically redirect the user to the correct set of pages and servlets. But one problem is still present, however: You have to maintain multiple sets of your pages and servlets. Consider the pain that this may cause to a content publisher.
Struts
There is an answer to this problem: Struts and its powerful application framework. With Struts, you can support multiple languages at a glance. The Struts framework separates your logic from the presented content, later referred to as messages. You can easily manage your code, develop in a safe way, and stick with the model view controller (MVC) pattern.
Experience Struts
A simple way to let Struts manage your multilanguage needs is to use the standard Tag libraries, which are located in your Struts distribution. There, you can find the <bean:message/> tag. You have to utilize the parameter "key", which tells Struts which message it has to lookup.
To define the keys and the proper messages, you need an ApplicationResourceBundle. For every language you want to support, a single ApplicationResources_xx_XX.properties file is required (where "xx_XX" stands for the specific locale; for example, en_US).
Don't forget to adjust struts-config.xml:
<message-resources parameter="strutsmessagewas4.resources.ApplicationResources"/>
The ApplicationResources.properties itself can be used for messages that are the same for all languages. Make sure that your Resource Bundles are available in the application's class path. A simple example follows:
Include the standard Struts Tag libraries:
Start the output of your JSP with the following tag:
Use bean Tag Library to let the framework generate messages on the JSP:
Close your JSP appropriately:
Don't forget to customize the Resource Bundle mentioned above.
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<html:html locale="true">
Make sure that you pass the attribute locale; otherwise, Struts does not look for the locale that the user's browser passes to the server via its request.
<bean:message key="view.label.hello"/>
This code tells Struts to look up a message identified by "view.label.hello" for the locale stored in the current user's session.
</html:html>
That was pretty easy, and a lot of our problems are solved for the moment. We have a solid code base and we don't need to recompile or change the presentation logic when a change to content has to be made. But do we want content publishers to be able to edit files on an application server? Especially when mostly every Web Application accesses database systems? That brings us to the next step: customizing Struts message handling.
Extending MessageResources
Because we cannot tell the default message implementation to use a database for looking up messages, we have to provide our own MessageResources implementation. The minimum you have to do is to create two classes: the MessageResources implementation itself and a factory class that is called to instantiate this.
Let's start with the factory class:
package com.dbt.strutsmessage.model; public class MessageResourcesFactory extends org.apache.struts.util.MessageResourcesFactory { /** * Factory to provide access to MessageResource implementation */ public org.apache.struts.util.MessageResources createResources(String configuration) { return new MessageResources(this, configuration); }
}
All this class does is expose a factory method that returns an instance of your MessageResources implementation. Ensure that both classes are located in the same package. I did not import org.apache.struts.util.MessageResourcesFactory in order to avoid problems with the class loader.
The second step is to create the MessageResources class. For the smallest implementation, inherit org.apache.struts.util.MessageResources, implement java.io.Serializable, and overwrite public String getMessage(Locale currentLocale, String askedKey) {}.
This is the method that is called by the Struts framework to retrieve a message. Here is where you ask your backend logic for the message, identified by the given key and locale. A good suggestion for separating your business logic and database-specific objects from the Struts presentation layer is to use a singleton ApplicationModel. (You can see download the war-file here.)
The last step is telling Struts to use this implementation instead of the default classes that come with the framework. This can be achieved by two parameters, which are passed to the Struts instance via your web.xml.
Find the section in which the Struts Action servlet is published and add the following:
<init-param> <param-name>application</param-name> <param-value>com.dbt.strutsmessage.model.MessageResources</param-value> </init-param> <init-param> <param-name>factory</param-name> <param-value>com.dbt.strutsmessage.model.MessageResourcesFactory</param-value> </init-param>
You have now created your own message handling logic.
Adding a Cache
Every developer knows the good advice for architectures: they should be bullet-proof, reliable, and fast in their execution.
"Only call the datalayer if you can't have it in your cache!"
In this case, we have to call the database every time a page makes use of <bean:message/>. Thinking of the type of data, we can assume that this information is fully cacheable:
private synchronized void cacheLocale(Locale locale) { // has some thread already cached, while we are waiting for // for this method? if(messageLocales.contains(locale)) { return; } String askedLocale = super.localeKey(locale); // ask Application for Messages associated to this locale Vector Messages = ApplicationModel. getApplicationModel(). getMessagesByLanguage(askedLocale); // store messages in cache // ... }
For me, the best practice is to check the presence of a set of messages for the current asked locale, at the moment getMessage() is called. If that fails, the application calls cacheLocale(); otherwise, it does not return either the message for this locale, or (if it is not present) the message for the parent of the asked locale.
The interesting thing about cacheLocale() is that you have to be aware that more than one thread may want to call this method because every call to the MessageFactory from within your JSP results in multiple threads in this class. So we should recheck the cache for presence of the asked locale because it's more than obvious that a thread before has cached the locale for us.