Server-side Java: Internationalize JSP-based Websites

Build Websites that speak in tongues

Most Websites are customized for a specific locale, with the locale defined as a geographical region or political entity that shares a common language, culture, and customs. While this may not be a significant issue to most people, it may pose some interesting challenges to those conducting business on the Web. By its very nature, ecommerce is all about facilitating commerce across national, linguistic, and cultural boundaries. While the Web may have opened up businesses to a truly international clientele, Web-based firms have to contend with the all-too-probable scenario of non-English-speaking users struggling to understand their sites’ content. As many businesses have come to realize, every Web surfer who turns away because of the site’s English-centric nature is a potential customer lost.

So, is there an easy, manageable solution to this problem that keeps you from having to run multiple versions of the site? Well, if the dynamic content for the Website is generated through Java technology, there sure is! That’s because the Java platform intrinsically supports the development of locale-independent applications through various classes supporting internationalization found within the java.util, java.io, and java.text packages. Internationalization itself is the name given to the design process wherein an application’s country, language, and culture-specific information is isolated and typically encapsulated within some external files or objects. Consequently, making your application compatible with an entirely new language or country does not have to involve a major rewrite of your presentation logic; rather, it now merely involves creating an additional version, specific to the new locale, of the external files. The process of creating these locale-specific entities (be they files or objects), along with the associated translation of text, is called localization.

Websites based on JavaServer Pages (JSPs) lend themselves more easily to internationalization than those developed using servlets. This is because the technology facilitates the separation of presentation from application logic. Now, let us examine this premise in detail by developing a global version of the Music Without Borders online store that was demonstrated in my earlier article on JSP architecture (see the Resources section below for a link to this story). The online store is based on the Model-View-Controller (MVC) architecture to maximize the separation of presentation from content.

Although internationalizing a JSP-based Web application is not particularly difficult, it is a task best initiated at the design stage. Toward this end, all locale-specific textual data that constitute an application’s user interface, such as page headers and trailers, user messages, form button labels, dates, currencies, and so forth, are first identified and isolated from the application within external files. These entities, which hold different values under different locales, are also known as resources; a grouping of resources within an external class or property file is known as a resource bundle or, simply, a bundle.

The Java 2 SDK provides the abstract base class java.util.ResourceBundle for handling resources, along with its subclasses ListResourceBundle and PropertyResourceBundle. The bundle for a supported locale can be defined as either a classfile (by using ListResourceBundle) or as a property file (by using PropertyResourceBundle). Both these mechanisms then allow your application to access the values for resources by name, just as you would with a Hashtable object. Although you can store objects of any type as values for resources using classfiles, most Web applications use property files, since they almost always customize only string types. Consequently, I will focus my discussion on the use of property files.

Developing global Web applications

In this example, the Music Without Borders online store has been localized to support users not only in the US, but also in Germany, Sweden, and France. Listings 1 and 2 show the property files supporting the locales for the US and France. (You can obtain the property files for Germany and Sweden when you download the source code for the example in Resources.) As you can see, the property files essentially isolate all of the GUI elements from the JSP pages. The property files for bundles must follow some standard naming conventions. The suffix must always be properties. Also, since a locale is typically defined in Java via a country code and/or a language code and an optional variant, the name of the bundle has to correlate with the locale it serves. For instance, all of the following bundle prefixes are valid:

BundleName + "_" + localeLanguage + "_" + localeCountry + "_" + localeVariant
BundleName + "_" + localeLanguage + "_" + localeCountry
BundleName + "_" + localeLanguage
BundleName

Observe that the property file serving as the bundle for France (shown in Listing 2) was named Message_fr_FR.properties. Bundles pertaining to a specific country need to have both the language code and country code appear within the prefix. (I could also have used the optional application-specific variant and further specialized the file as Message_fr_FR_MAC.properties — the bundle for all French-speaking Mac users from France. But I shall resist the temptation!) Similarly, the bundle containing the German labels is named Message_de_DE.properties, and so on. Resources contains links for obtaining valid country and language codes. For the overly curious, the following code shows the source you can use to create a classfile, serving the same purpose as the property file shown in Listing 2:

public class Message_fr_FR extends ListResourceBundle {
        public Object[][] getContents() {
                return contents;
        }
        static final Object[][] contents = {
                {"main.title", "Musique sans frontières"},
                {"main.subhead", "Sons du village global"},
            {"main.addLabel","Ajouter"},
             . . .
            {"cd.quantityLabel","Quantité"}
        }
}

You might wonder why the property file containing the English labels (shown in Listing 1) does not contain a country or language code in its prefix. Although I could have named the bundle Message_en_US.properties, I chose to simply call it Message.properties. Any bundle that contains neither country nor language code is treated as the default bundle and serves a specific purpose: it is picked up as a last resort if there is no other matching bundle for the requested locale. Observe that the literals used to identify resources remain the same within the bundles for all locales; only the values change for the corresponding locale. Now that I have isolated the GUI elements and localized them within the property files, I’ll show how you can use them within a JSP page.

Listing 1: Message.properties

main.title=Music Without Borders
main.subhead=Sounds from the Global Village
main.addLabel=Add
main.qtyLabel=Quantity
main.bgcolor=#33CCFF
cart.bgcolor=#FFFFFF
cart.delLabel=Delete
cart.checkoutLabel=Checkout
checkout.bgcolor=#33CCFF
checkout.title=Music Without Borders Checkout
checkout.subhead=Thanks for your order!
checkout.totalLabel=Total
checkout.returnLabel=Shop some more!
dollar.exchRate=1.00
cd.albumLabel=Album
cd.artistLabel=Artist
cd.countryLabel=Country
cd.priceLabel=Price
cd.quantityLabel=Quantity

Listing 2: Message_fr_FR.properties

main.title=Musique sans frontières
main.subhead=Sons du village global
main.addLabel=Ajouter
main.qtyLabel=Quantité
main.bgcolor=#33CCFF
cart.bgcolor=#FFFFFF
cart.delLabel=Supprimer
cart.checkoutLabel=Passez à la caisse
checkout.bgcolor=#33CCFF
checkout.title= Caisse pour Musique sans frontières
checkout.subhead=Merci pour votre commande!
checkout.totalLabel=Total
checkout.returnLabel=Faites d'autres commandes!
dollar.exchRate=6.48
cd.albumLabel=Album
cd.artistLabel=Artiste
cd.countryLabel=Pays
cd.priceLabel=Prix
cd.quantityLabel=Quantité

The JSP page i18nDemo.jsp (shown in Listing 3) acts as the gateway to the online store. Its main task is to enable the user to select an appropriate language for viewing the site, as shown in Figure 1.

Figure 1. Gateway to the internationalized Web application

In Java, the focal point of all internationalization activity is the java.util.Locale class, which serves to encapsulate the country code, language code, and the optional variant code. The Locale class also provides a number of convenient constants representing the most commonly used locales. Consider the following snippet, which establishes the locale for the Web application:

Locale locale=null;
if (lang.equals("German")) {
  locale=Locale.GERMANY;
} else if (lang.equals("French")) {
  locale=Locale.FRANCE;
} else if (lang.equals("Swedish")) {
  locale=new Locale("sv","SE");
} else {
  locale=Locale.US;
}

While the Locale instance is typically obtained from a predefined constant within the Locale class, it can also be instantiated using the class’s constructor. You can do this by specifying the language code and country code (there is also another version of the Locale constructor that additionally accepts an application-specific variant code). The language code is two lowercase letters, and the country code is always two uppercase letters. (As mentioned earlier, you can find a link to lists of valid country and language code in Resources.)

Since I do not have a predefined constant for Sweden, observe that its Locale instance is instantiated by explicitly passing the language and country code as:

locale=new Locale("sv","SE");

Once the locale has been established, you can retrieve its corresponding bundle:

ResourceBundle bundle =
  ResourceBundle.getBundle("Message", locale);

You can store the bundles anywhere in the classpath, as understood by your application server or JSP engine. The getBundle() invocation initiates the search for the bundle after extracting the language code (xx, say) and the country code (YY, say) from the locale object passed as a parameter. (You can also invoke getBundle() without passing an instance of Locale, in which case the system default locale is used.) You conduct the search first by looking for the presence of Message_xx_YY.class within the classpath. If this is absent, the search tries to locate Message_xx.class and then Message.class. If there is no matching classfile, the search proceeds to locate a matching property file instead, starting with Message_xx_YY.properties, moving to Message_xx.properties, and finally, Message.properties. If the lookup fails without a match, getBundle() throws a MissingResourceException.

Listing 3: i18nDemo.jsp





Music Without Borders




Music Without Borders


Please select a language:

English
Deutsch
Français

locale=new Locale(“sv”,”SE”);
} else {
locale=Locale.US;
}
session.putValue(“myLocale”,locale);
ResourceBundle bundle =
ResourceBundle.getBundle(“Message”,locale);
for (Enumeration e = bundle.getKeys();e.hasMoreElements();) {
String key = (String)e.nextElement();
String s = bundle.getString(key);
session.putValue(key,s);
}
%>

After retrieving the bundle, you obtain the resources for the locale along with their associated values and place them into the session as:

for (Enumeration e = bundle.getKeys();e.hasMoreElements();) {
  String key = (String)e.nextElement();
  String s = bundle.getString(key);
  session.putValue(key,s);
}

Once you have initialized the session with all the necessary localized information, the request is forwarded to eshop.jsp (shown in Listing 4), which facilitates the main view for the online store.

As every good JSP page should, eshop.jsp deals almost exclusively with presenting the application’s UI to the client. In fact, the only processing it performs is when it formats the price field according to the selected locale. Take a look at the method computePrice() declared within the page, which helps handle the display of currencies and formats them in a manner appropriate to the locale. The real work is done by the java.text.NumberFormat object for the specified locale obtained from the getCurrencyInstance() invocation. When the format() method is invoked on this instance, it returns a String that includes the correctly formatted number, along with the appropriate currency sign. Observe that I also make use of the resource dollar.exchRate to convert the input dollar amount to the equivalent currency of the locale. Otherwise, the CDs would be a major bargain in Sweden, for instance, where a dollar is worth about 8.40 Swedish Krona!

You will see that most GUI elements, including header messages, button labels, and so forth, are locale sensitive. They are set at runtime by interrogating the session for the appropriate resource:

<title>
<%=session.getValue("main.title")%>
</title>

Listing 4: eshop.jsp




<title>
<%=session.getValue("main.title")%>
</title>











CD: :

<jsp:include page="cart.jsp" flush="true" />

Also, notice that another JSP page, cart.jsp (shown in Listing 5), is included within eshop.jsp via the following directive:

<jsp:include page="cart.jsp" flush="true" />

Here, cart.jsp handles the presentation of the session-based shopping cart, which is implemented by a Vector object. Observe the scriptlet at the beginning of cart.jsp:

<%
  Vector buylist = (Vector) session.getValue("shopping.shoppingcart");
  if (buylist != null && (buylist.size() > 0)) {
%>

Basically, the scriptlet extracts the shopping cart from the session. If the cart is empty or not yet created, it displays nothing; thus, the first time users access the application, they are presented with the view shown in Figure 2, assuming the users had opted to localize the site for French.

Figure 2. Main view localized for French

Listing 5: cart.jsp

<%@ page import="java.util.*, shopping.i18n.CD"  %>
<%
 Vector buylist = (Vector) session.getValue("shopping.shoppingcart");
 if (buylist != null && (buylist.size() >0)) {
%>
   <center>
   <table class="legacyTable" border=0 cellpadding=0 width=100%
bgcolor="<%=session.getValue("cart.bgcolor")%>">
   <tr>
        <td><b><%=session.getValue("cd.albumLabel")%></b></td>
        <td><b><%=session.getValue("cd.artistLabel")%></b></td>
        <td><b><%=session.getValue("cd.countryLabel")%></b></td>
        <td><b><%=session.getValue("cd.priceLabel")%></b></td>
      <td><b><%=session.getValue("cd.quantityLabel")%></b></td>
        <td></td>
  </tr>
  <%
     for (int index=0; index< buylist.size();index++) {
        CD anOrder = (CD) buylist.elementAt(index);
  %>
         <tr>
                <td><b><%=anOrder.getAlbum()%></b></td>
                <td><b><%=anOrder.getArtist()%></b></td>
                <td><b><%=anOrder.getCountry()%></b></td>
                <td><b><%=anOrder.getPrice()%></b></td>
                <td><b><%=anOrder.getQuantity()%></b></td>
                <td>
                <form name="deleteForm" action="main.jsp" method="post">
                  <input type="submit"
                  value="<%=session.getValue("cart.delLabel")%>">
                    <input type="hidden" name=delindex value="<%=index%>">
                    <input type="hidden" name="action" value="DELETE">
                 </form>
                </td>
        </tr>
  <% } %>
   </table>
   

<form name="checkoutForm" action="main.jsp" method="post"> <input type="hidden" name="action" value="CHECKOUT"> <input type="submit" name="Checkout" value="<%=session.getValue("cart.checkoutLabel")%>"> </form> </center> <% } %>

If the shopping cart is not empty, the selected CDs are extracted from the cart one at a time, as demonstrated by the following scriptlet:

<%
  for (int index=0; index < buylist.size(); index++) {
    CD anOrder = (CD) buylist.elementAt(index);
%>

Once the variable describing an item has been created, it is simply inserted into the static HTML template using JSP expressions. You can avoid having an excessive amount of script code when iterating beans with indexed properties by implementing the looping via custom tags. Figure 3 shows the Swedish view after the user has placed some items into the shopping cart. Contrast it with the English view for the same procedure, as shown in Figure 4. Observe how seamlessly the localization is performed while using the same underlying application logic.

Figure 3. Shopping cart view localized for Swedish
Figure 4. Shopping cart view localized for English

The important thing to observe here is that the processing for all actions carried out within either eshop.jsp or cart.jsp is handled by the controller JSP page, main.jsp, shown in Listing 6.

Listing 6: main.jsp

<%@ page import="java.util.*, java.text.*, shopping.i18n.CD" %>
<%
  String action = request.getParameter("action");
  if (action == null) {
%>
    <jsp:forward page="i18nDemo.jsp" />
<%
  } else  {
    Vector buylist=
              (Vector)session.getValue("shopping.shoppingcart");
    if (!action.equals("CHECKOUT")) {
        if (action.equals("DELETE")) {
          String del = request.getParameter("delindex");
          int delIndex = (new Integer(del)).intValue();
          buylist.removeElementAt(delIndex);
        } else if (action.equals("ADD")) {
            //any previous buys of same cd?
          boolean match=false;
%>
        <jsp:useBean id="aCD" class="shopping.i18n.CD" scope="page">
             <% aCD.setCDProperties(request); %>
        </jsp:useBean>
<%
          if (buylist==null) {
            //add first cd to the cart
            buylist = new Vector(); //first order
            buylist.addElement(aCD);
       } else { // not first buy
            for (int i=0; i< buylist.size();i++) {
              CD cd = (CD) buylist.elementAt(i);
                if (cd.getAlbum().equals(aCD.getAlbum())) {
                   int tmpQty =
                  (new Integer(cd.getQuantity())).intValue() +
                        (new Integer(aCD.getQuantity())).intValue();
                   cd.setQuantity(new Integer(tmpQty).toString());
                   buylist.setElementAt(cd,i);
                   match = true;
              } //end of if name matches
            } // end of for
            if (!match) buylist.addElement(aCD);
         } //end of not first buy
} //end of action==ADD
        session.putValue("shopping.shoppingcart", buylist);
%>
        <jsp:forward page="eshop.jsp" />
<%
} else { // if checkout
            NumberFormat nFormat = NumberFormat.getCurrencyInstance(
                                           (Locale)session.getValue("myLocale"));
            float total =0;
            for (int i=0; i< buylist.size();i++) {
              CD anOrder = (CD) buylist.elementAt(i);
                Number n = nFormat.parse(anOrder.getPrice().trim());
              float price= n.floatValue();
              int qty = (new Integer(anOrder.getQuantity())).intValue();
              total += (price * qty);
           }
           String amountDue=nFormat.format(total);
           request.setAttribute("amountDue",amountDue);
%>
           <jsp:forward page="checkout.jsp" />
<%
        }
}
%>

If the user tries to add or delete an item, or checks out, the request is posted to the controller, main.jsp. This controller page handles addition requests initiated from eshop.jsp, as well as deletion and checkout requests triggered from cart.jsp. If the request is an addition, for instance, the controller processes the request parameters for the item to be added, and then instantiates a new CD bean (shown in Listing 7) representing the selection. The updated shopping-cart object is then placed back within the session. The controller is also enabled with enough smarts to understand that if a previously added CD is reselected, the controller should simply increase the count for that CD bean within the shopping cart. Changes affecting the state of the shopping cart, such as an addition or deletion, cause the controller to forward the request after processing to eshop.jsp. This, in turn, redisplays the main view, along with the updated contents of the shopping cart. If the user decides to check out, the request is forwarded after processing to checkout.jsp, shown in Listing 8.

A big advantage of having a separate controller page such as this one is that you can always exercise complete control over the resources that should be invoked in response to specific actions. In fact, you can further isolate this by having the controller initialize itself via a special property file containing target resources for specific user actions. That way, you can completely externalize the maintenance of your Web application and gain maximum flexibility.

Listing 7: CD.java

package shopping.i18n;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
public class CD {
        String album;
        String artist;
        String country;
        String price;
        String quantity;
        public CD() {
                album="";
                artist="";
                country="";
                price="";
                quantity="";
        }
        public void setCDProperties(HttpServletRequest req) {
                String myCd = req.getParameter("CD");
                StringTokenizer t = new StringTokenizer(myCd,"|");
                this.album= t.nextToken();
                this.artist = t.nextToken();
                this.country = t.nextToken();
                this.price = t.nextToken().trim();
                this.quantity = req.getParameter("qty");
        }
        public void setAlbum(String title) {
                album=title;
        }
        public String getAlbum() {
                return album;
        }
        public void setArtist(String group) {
                artist=group;
        }
        public String getArtist() {
                return artist;
        }
        public void setCountry(String cty) {
                country=cty;
        }
        public String getCountry() {
                return country;
        }
        public void setPrice(String p) {
                price=p;
        }
        public String getPrice() {
                return price;
        }
        public void setQuantity(String q) {
                quantity=q;
        }
        public String getQuantity() {
                return quantity;
        }
 }

The checkout.jsp page simply extracts the shopping cart from the session and the total amount for the request, and then displays the selected items and their total cost. Figure 5 shows the client view upon checkout, localized for German.

Figure 5. Checkout view localized for German

Once the user goes to the checkout counter, it is equally important to get rid of the session object. That is taken care of by having a session.invalidate() invocation at the end of the page. This process is necessary for two reasons. First, if the session is not invalidated, the user’s shopping cart is not reinitialized; if the user then attempts to commence another round of shopping upon checkout, his shopping cart will continue to hold items that he has already purchased. The second reason is that if the user simply left the site upon checkout, the session object will not be garbage collected and will continue to take up valuable system resources until its lease period expires. Since the default session-lease period is about 30 minutes, in a high-volume system this can quickly lead to the system’s running out of memory.

Listing 8: checkout.jsp

<%@ page import="java.util.*, shopping.i18n.CD" %>
<html>
<head>
<title>
<%=session.getValue("checkout.title")%>
</title>
</head>
<body bgcolor="<%=session.getValue("checkout.bgcolor")%>">
<font face="Times New Roman,Times" size=+3>
<%=session.getValue("checkout.title")%>
</font>

<hr> <%=session.getValue("checkout.subhead")%>

<center> <table class="legacyTable" border="0" cellpadding="0" width="100%" bgcolor="<%=session.getValue("cart.bgcolor")%>"> <tr> <td><b><%=session.getValue("cd.albumLabel")%></b></td> <td><b><%=session.getValue("cd.artistLabel")%></b></td> <td><b><%=session.getValue("cd.countryLabel")%></b></td> <td><b><%=session.getValue("cd.priceLabel")%></b></td> <td><b><%=session.getValue("cd.quantityLabel")%></b></td> <td></td> </tr> <% Vector buylist=(Vector)session.getValue("shopping.shoppingcart"); for (int i=0; i< buylist.size();i++) { CD anOrder = (CD) buylist.elementAt(i); %> <tr> <td><b><%=anOrder.getAlbum()%></b></td> <td><b><%=anOrder.getArtist()%></b></td> <td><b><%=anOrder.getCountry()%></b></td> <td><b><%=anOrder.getPrice()%></b></td> <td><b><%=anOrder.getQuantity()%></b></td> </tr> <% } %> <tr> <td> </td> <td> </td> <td><b><%=session.getValue("checkout.totalLabel")%></b></td> <td><b><%=request.getAttribute("amountDue")%></b></td> <td> </td> </tr> </table>

<a href=i18nDemo.jsp><%=session.getValue("checkout.returnLabel")%<>/a> </center> <% session.invalidate(); %> </body> </html>

Deploying the internationalized Web application

I will assume you are using the latest version of JavaServer Web Development Kit (JSWDK) from Sun for running the example. If not, see Resources to find out where to get it. Assuming the server is installed in jswdk-1.0.1, its default location in Microsoft Windows, deploy the Music Without Borders application files as follows:

  • Create a i18n directory under jswdk-1.0.1examplesjsp
  • Copy i18nDemo.jsp to jswdk-1.0.1examplesjspi18n
  • Copy eshop.jsp to jswdk-1.0.1examplesjspi18n
  • Copy cart.jsp to jswdk-1.0.1examplesjspi18n
  • Copy main.jsp to jswdk-1.0.1examplesjspi18n
  • Copy checkout.jsp to jswdk-1.0.1examplesjspi18n
  • Compile CD.java by typing javac CD.java
  • Create a shoppingi18n directory under jswdk-1.0.1examplesWeb-Infjspbeans
  • Copy CD.class to jswdk-1.0.1examplesWeb-Infjspbeansshoppingi18n

Once your server has been started, you should be able to access the application using as the URL.

Speaking of characters

Throughout this example, I made use of the default 8-bit ISO Latin-1 character set (aka ISO-8859-1), since it supports the 200 or so characters common to most Western European languages. But to develop truly international applications, you would need to use a character set capable of supporting languages such as Chinese, Arabic, Japanese, and so forth, whose range of characters far exceeds the 256-character limit of ISO Latin-1. You can obtain a list of character encodings supported by Java from Resources. If you are using something other than the ISO-8859-1 character set, you must communicate this to the browser using the contentType attribute of the page tag, as:

<%@ page contentType="text/html; charset=charset_name" %>

For example, if you generated your dynamic content in Cyrillic (for a page in Russian or Bulgarian, for example), your JSP would have to communicate that fact to the browser as:

<%@ page contentType="text/html; charset=ISO-8859-5" %>

Of course, in this case, your users will also need a browser that supports the new character set. Toward this end, you must ensure that the browsers are HTML 4.0 compliant, since HTML 4.0 supports Basic Multilingual Plane, a standardized 16-bit character set that supports most of the world’s languages. Additionally, it is important that the browser is also enabled with the requisite font to display the characters of your target language.

Conclusion

In this article, I have examined the development of multilocale JSP pages. Websites based on JSP technology lend themselves more naturally to internationalization because of the ease with which the presentation view can be separated from application logic. Developing a truly global site is, however, a challenging task, due to the complexities introduced by diverse character sets and erratic browser support.

The authors of this month’s server-side Java computing articles will be holding a free online seminar on March 9 at 10:00 a.m. PST. Register to join at https://seminars.jguru.com.

Govind Seshadri is an enterprise Java guru for
jGuru.com, and the author of Enterprise Java Computing —
Applications and Architecture from Cambridge University Press
(1999). Learn more about Govind at jGuru.com.
JavaWorld and jGuru have formed a partnership to help the
community better understand server-side Java technology. Together,
JavaWorld and jGuru are jointly producing articles and
free educational Web events.

Source: www.infoworld.com