| [ Team LiB ] |
|
14.2 Generating Localized OutputNow that you have an understanding of the type of internationalization support Java provides, let's look at a concrete example. However, instead of using the internationalization classes directly in the pages, we'll use the JSTL I18N actions based on these classes. The example application, briefly described in the introduction to this chapter, lets visitors voice their opinion by selecting one of the answers to a question, as well as seeing how others have answered. The text, numbers, and dates are available in three different languages. Figure 14-1 shows all the pages used in this application and how they are related. Figure 14-1. Localized poll application pages![]() The first page the user sees is the poll.jsp page, shown in Figure 14-2. The language that displays the contents the first time this page is requested is based on the Accept-Language request header value. The top part of the page contains radio buttons for the three supported languages and a Submit button. If the user wants the application to be presented in another language, he selects the corresponding radio button and clicks Submit, causing the page to be requested again, this time with a language parameter included in the request. The value of the language parameter is then used to select the corresponding locale and display the page in the selected locale's language. Information about the selected locale is saved as session data, so it's available to all the other application pages as well. Figure 14-2. The language selection and question page![]() The poll.jsp page also includes the question, linked to a page with background information for the question, and a group of radio buttons representing the different answers, as well as a Submit button. Clicking on the Submit button invokes the calculate.jsp page, in which the choice is first validated. If it's valid, it's added to the global poll result. The request is then forwarded to the result.jsp page, which displays the poll statistics with all numbers formatted according to the selected locale. If it's not valid, the request is forwarded back to the poll.jsp page. Both the poll.jsp page and the result.jsp page are designed to show text, numbers, and dates according to the selected locale using the JSTL I18N actions. This approach is perfect when the amount of text is small; only one set of pages has to be maintained. But if a page needs to contain a great deal of text, typing it into a properties file and escaping all line breaks may not be the best approach. Some pages also need to use different layouts, colors, images, and general appearance based on the locale. In this case, it's easier to use a separate page per locale. The pages providing more detailed information about the question in this example illustrate this approach. The link on the poll.jsp page leads to different JSP pages depending on the selected language, named according to the same naming convention as ResourceBundle properties files: details_en.jsp, details_de.jsp, and details_sv.jsp for the English, German, and Swedish pages, respectively. Let's look at the one-page and the multipage approaches separately. 14.2.1 Using One Page for Multiple LocalesExample 14-1 shows the poll.jsp page. That's where the magic of locale selection happens, and the selection is then used to produce text in the corresponding language throughout the page. Example 14-1. Language selection and vote page (poll.jsp)<%@ page contentType="text/html" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%-- Set the locale to the selected one, if any. Otherwise, let the <fmt:setBundle> action pick the best one based on the Accept-Language header. --%> <c:if test="${param.language == 'en'}"> <fmt:setLocale value="en" scope="session" /> </c:if> <c:if test="${param.language == 'sv'}"> <fmt:setLocale value="sv" scope="session" /> </c:if> <c:if test="${param.language == 'de'}"> <fmt:setLocale value="de" scope="session" /> </c:if> <fmt:setBundle basename="pages" var="pagesBundle" /> <fmt:setBundle basename="labels" scope="session" /> <html> <head> <title> <fmt:message key="title" /> </title> </head> <body bgcolor="white"> <h1> <fmt:message key="title" /> </h1> <fmt:message key="select_language" />: <form action="poll.jsp"> <p> <c:set var="currLang" value="${pagesBundle.locale.language}" /> <input type="radio" name="language" value="en" ${currLang == 'en' ? 'checked' : ''}> <fmt:message key="english" /><br> <input type="radio" name="language" value="sv" ${currLang == 'sv' ? 'checked' : ''}> <fmt:message key="swedish" /><br> <input type="radio" name="language" value="de" ${currLang == 'de' ? 'checked' : ''}> <fmt:message key="german" /><br> <p> <input type="submit" value="<fmt:message key="new_language" />"> </form> <a href="<fmt:message key="details_page" bundle="${pagesBundle}" />"> <fmt:message key="question" /> </a> <form action="calculate.jsp" method="post"> <input type="radio" name="answerId" value="1" checked> <fmt:message key="answer1" /> <br> <input type="radio" name="answerId" value="2"> <fmt:message key="answer2" /> <br> <input type="radio" name="answerId" value="3"> <fmt:message key="answer3" /> <p> <input type="submit" value="<fmt:message key="submit" />"> </form> </body> </html> At the top of the page, the taglib directives identify the JSTL libraries. Besides the JSTL core library used in previous chapters, this page also declares the JSTL I18N and formatting library with the prefix fmt. After the tag library declarations follows a section that determines if the page is invoked with the language parameter, to request a specific language to be used. If it is, and the requested language is one supported by this application, the body of the matching <c:if> action element is processed to set the corresponding locale explicitly. This is where we encounter the first I18N action: <fmt:setLocale>, described in Table 14-3.
This action creates an instance of the Locale class corresponding to the value and variant attribute values and saves it in the specified scope. The value attribute must include a language code. It may also include a country code, separated from the language code with a hyphen (-) or an underscore (_). The variant attribute is rarely used, but the I18N Java classes used behind the scene support it, so the JSTL action supports it as well. It can specify a locale that applies to a specific platform in addition to a language and a country. One example is if you use a locale to select help texts, you may want to provide one set of descriptions for Internet Explorer and another for Netscape browsers. In Example 14-1, only the language code is specified, and the locale setting is saved in the session scope to apply it to all pages requested by the same user. The variable that the <fmt:setLocale> sets is a configuration variable—in other words a variable with an implementation-dependent name used to set the default for a specific scope, as described in Chapter 12. It's backed by a context parameter that can set a global default: <web-app>
...
<context-param>
<param-name>
javax.servlet.jsp.jstl.fmt.locale
</param-name>
<param-value>
en-US
</param-value>
</context-param>
...
</web-app>
The first time the page is invoked, no language is specified. Hence, the language parameter is not received with the request so none of the <c:if> conditions is matched. In this case, the supported locale that is the closest match to the language preferences defined by the user through the browser settings should be used. The <fmt:setBundle> action, described in Table 14-4, takes care of this task.
In Example 14-1, I use this action to load two separate resource bundles: one that contains the names of language-specific pages and one that contains all localized text. The basename attribute is mandatory and identifies a specific resource bundle. Properties files for all locales supported by the sample application, named according to the pattern basename_locale.properties, represent each bundle: labels_en.properties; labels_de.properties and labels_sv.properties; and pages_en.properties, pages_de.properties, and pages_sv.properties. All properties files are located in the WEB-INF/classes directory for the application, making them part of the classpath the I18N classes use to locate resource bundles. If a default locale has been established using the <fmt:setLocale> action or the locale configuration setting, the <fmt:setBundle> action simply loads the resource bundle corresponding to the base name for the selected locale. In Example 14-1, this is the case when a language parameter with a value matching one of the supported locales is sent with the request. But what happens if no language parameter is sent or when its value doesn't match a supported locale? In this case, the <fmt:setBundle> action has to decide which of the supported locales most closely matches the user's preferences. Recall that the user's language preferences are sent with the request in the Accept-Language header. The <fmt:setBundle> action compares this list to the set of available locale-specific bundles for the base name and picks the best one. For each locale in the list, it first tries to find a locale that matches all parts specified for the preferred locale: language, country, and variant. If it doesn't find a perfect match, it drops the variant and tries again. If it still can't find a match, it drops the country. As soon as it finds a bundle for one of the locales using this algorithm, it uses it and ignores the other locales. This means that with English, German, and Swedish as the available locales and an Accept-Language header containing the value "sv, en-US", the Swedish locale is selected (it's listed first, so it has higher priority). With an Accept-Language header such as "fr, en-US", the English locale is selected, since the highest priority locale (fr) is not available, and the closest match for the en-US locale is the en locale. If the application has bundles for both the en file and the en-US locale, the en-US locale is used because it's an exact match for the user's preferences. An interesting case is what happens if none of the locales in the Accept-Language header matches an available locale. The best the <fmt:setBundle> action could do on its own would be to randomly pick one of the available locales, but that's no good. Instead you need to tell it which locale to use in this case. It's called the fallback locale in the JSTL specification, and it's defined as a configuration setting. You can set its context parameter like this in the deployment descriptor: <web-app>
...
<context-param>
<param-name>
javax.servlet.jsp.jstl.fmt.fallbackLocale
</param-name>
<param-value>
en
</param-value>
</context-param>
...
</web-app>
Here I set the en locale as the fallback, so if none of the preferred locales match, the <fmt:setBundle> action tries to locate a bundle with the en locale. If it still can't find a matching bundle, it falls back to the ultimate backup: the so-called root bundle. The root bundle is represented by a bundle without any locale information, for instance a file named labels.properties in the example application. The <fmt:setBundle> action also supports the var and the scope attributes. They let you specify the name of a variable for the bundle and the scope where it should be stored. The type of the variable is javax.servlet.jsp.jstl.fmt.LocaleContext. It's a simple class that holds an instance of the java.util.ResourceBundle that was selected as well as a java.util.Locale instance for the locale that matched it. This variable can be used as an attribute value for other I18N actions to tell them which bundle to use. That's what I do for the bundle with page names in Example 14-1. If you omit the var attribute, the action saves the localization context in a configuration variable for the scope instead. It is then used as the default for all JSTL I18N actions that use a bundle. I use this approach for the bundle that contains all text, and I specify the session scope so it's available to all pages requested by the same user. As for all configuration setting, you can also define the base name for a resource bundle to use by default as a context parameter: <web-app>
...
<context-param>
<param-name>
javax.servlet.jsp.jstl.fmt.localizationContext
</param-name>
<param-value>
labels
</param-value>
</context-param>
...
</web-app>
The context parameter value is used by the I18N actions that need a localization context if it's not established in another way, for instance by a <fmt:setBundle> action. To extract the localized text from a resource bundle and add it to the response, you use the <fmt:message> action, described in Table 14-5.
The <fmt:message> action looks up the message identified by the key attribute value in the bundle specified by the bundle attribute and adds it to the response. If it can't find a message for the key, it adds the key enclosed in question marks instead. The var attribute can be used to save the localized message in the named variable instead. The variable is saved in the page scope, unless another scope is specified by the scope attribute. The bundle attribute can be omitted if a localization context configuration setting is used to establish a default bundle, as is the case in Example 14-1. It can also be omitted if the <fmt:message> action is nested within the body of a <fmt:bundle> action element, described in Table 14-6.
When you use the <fmt:bundle> action to establish the localization context for the nested actions, you can specify a key prefix as well. The prefix attribute is a convenience feature that comes in handy if your resource bundle keys have very long names. For instance, if all keys start with com.mycompany.labels, you can specify this as the prefix and use only the last part of the key name for the <fmt:message> actions that pull messages from the bundle. If you look carefully at Example 14-1, you'll notice that the only static content in the page consists of HTML elements; all text is added by the <fmt:message> action, using localized messages from the resource bundle matching the selected locale. Here's how the labels resource bundle file for the English locale, labels_en.properties file looks: title=Industry Trends
select_language=Select your preferred language
new_language=New Language
english=English
swedish=Swedish
german=German
question=What's the longest development time you dare to plan with?
answer1=One year
answer2=Six months
answer3=Less than six months
result1=One year {0, number, integer}% ({1, number, integer})
result2=Six months {0, number, integer}% ({1, number, integer})
result3=Less than six months {0, number, integer}% ({1, number, integer})
submit=Vote
number_of_votes=Total number of votes
result=Poll result
The value of the title key, used by the first two <fmt:message> actions, is set to "Industry Trends"; that's what appears as the title and header of the page when the English locale is selected. If the Swedish locale is selected, the text "Industri Trender" (the value specified for the title key in the lables_sv.properties file) is used instead. To let the user pick another language than the one selected based on the Accept-Language header, the page contains a form with a set of radio buttons and a Submit button. Every time the page is displayed, the radio button group must reflect the currently selected language. The only sure way to find out which language is selected is to ask the LocalizationContext created by the <fmt:setBundle> action. The LocalizationContext class can be used as a bean with properties named locale and resourceBundle. The locale property contains a Locale instance that represents the locale used to pick the resource bundle. The Locale class can also be used as a bean, with a property named language. Armed with this knowledge, it's easy to write an EL expression that gets the current language: ...
<c:set var="currLang" value="${pagesBundle.locale.language}" />
<input type="radio" name="language" value="en"
${currLang == 'en' ? 'checked' : ''}>
...
The <c:set> action gets the current language using an EL expression that first gets the Locale from the LocalizationContext stored in the pagesBundle variable and then the language from the Locale. The result is saved in a variable named currLang, which is then used in the EL expressions for each radio button to set the checked attribute for the one that matches the current language. All radio button elements have the name language, which means that they form a group in which only one can be selected. When the user clicks on the Submit button, the same page is requested with the value of the selected radio button included as a request parameter named language. As described earlier, this parameter is then used to switch to the selected language. Next comes another form with radio buttons representing the three alternative answers to the poll question. All radio buttons are named answerId. The texts for the question, the answers, and the Submit button are displayed in the current language using the <fmt:message> action. When the user selects an answer and clicks on the Submit button, the calculate.jsp page, shown in Example 14-2, is invoked. Example 14-2. Validation and calculation of votes (calculate.jsp)<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<jsp:useBean id="pollResult" scope="application"
class="com.ora.jsp.beans.poll.PollBean" />
<jsp:useBean id="answer" class="com.ora.jsp.beans.poll.AnswerBean" >
<jsp:setProperty name="answer" property="*" />
</jsp:useBean>
<c:choose>
<c:when test="${answer.valid}" >
<c:set target="${pollResult}" property="answer"
value="${answer}" />
<jsp:forward page="result.jsp" />
</c:when>
<c:otherwise>
<jsp:forward page="poll.jsp" />
</c:otherwise>
</c:choose>
As with all pure logic pages, this page contains only action elements; no response text is generated. A PollBean in the application scope keeps track of the answers from all visitors, and an AnswerBean in the page scope captures and validates a single answer. The AnswerBean has a property named answerId, set to the value of the corresponding request parameter using the <jsp:setProperty> action. It also has a valid property, used in the <c:when> action to test if the answer is valid or not. In this example, it returns true if the answer ID is valid (1, 2, or 3). However, in a real application, you may want to include other validation rules. For instance, if the poll information is stored in a database, you can use cookies or a username to make sure each user answers only once. If the answer is valid, a <c:set> action sets the answer property of the PollBean to the valid answer, and the request is forwarded to the result.jsp page to display the poll statistics. Figure 14-3 shows a sample of the results page with the Swedish locale. Figure 14-3. The result page using the Swedish locale![]() The result.jsp page, shown in Example 14-3, uses a couple of JSTL I18N actions we haven't talked about so far to display the localized date and numbers. Example 14-3. Showing the result (result.jsp)<%@ page contentType="text/html" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<title>
<fmt:message key="title" />
</title>
</head>
<body bgcolor="white">
<jsp:useBean id="pollResult" scope="application"
class="com.ora.jsp.beans.poll.PollBean" />
<jsp:useBean id="now" class="java.util.Date" />
<h1>
<fmt:message key="result" />:
<fmt:formatDate value="${now}" />
</h1>
<fmt:message key="question" />
<p>
<fmt:message key="number_of_votes" />:
<fmt:formatNumber value="${pollResult.total}" />
<table width="70%">
<tr>
<td width="30%">
<fmt:message key="result1">
<fmt:param value="${pollResult.answer1Percent}" />
<fmt:param value="${pollResult.answer1}" />
</fmt:message>
</td>
<td>
<table
width="<fmt:formatNumber
value="${pollResult.answer1Percent}"/>%"
bgcolor="lightgreen">
<tr>
<td> </td>
</tr>
</table>
</td>
</tr>
<tr>
<td width="30%">
<fmt:message key="result2">
<fmt:param value="${pollResult.answer2Percent}" />
<fmt:param value="${pollResult.answer2}" />
</fmt:message>
</td>
<td>
<table
width="<fmt:formatNumber
value="${pollResult.answer2Percent}"/>%"
bgcolor="lightblue">
<tr>
<td> </td>
</tr>
</table>
</td>
</tr>
<tr>
<td width="30%">
<fmt:message key="result3">
<fmt:param value="${pollResult.answer3Percent}" />
<fmt:param value="${pollResult.answer3}" />
</fmt:message>
</td>
<td>
<table
width="<fmt:formatNumber
value="${pollResult.answer3Percent}"/>%"
bgcolor="orange">
<tr>
<td> </td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
This page uses the <fmt:message> action to add the localized text, just like the poll.jsp page. A <jsp:useBean> action creates a variable that represents the current date and time, and this value is then added to the response with the <fmt:formatDate> action, described in Table 14-7. When you play around with this application, you see how the date format changes depending on the language you select.
The <fmt:formatDate> action supports many attributes, but all except value are optional. The var and scope attributes are used as in all other JSTL actions: to specify the name of a variable to hold the result and optionally in which scope to store it. All the other attributes deal with how the value should be formatted. The type attribute specifies if the result should contain just the date, just the time, or both. The dateStyle and timeStyle attributes allow you to specify predefined patterns for the date and the time part. The patterns vary between locales. For the English locale, the following chart shows the result of applying the predefined patterns for the date and time parts:
To use a custom pattern instead of one of the predefined patterns, you can use the pattern attribute. The pattern uses a number of symbols to define which parts should be included, and in which form (i.e., as a number or a name). A complete description is included in Appendix B, but the following chart shows a few examples using the English locale:
Finally, you can use the timeZone attribute to adjust the value to a specific time zone. Internally, Java handles date and time values as coordinated universal time (UTC), a time zone-neutral format, if you will. But when you create a text representation of a date, it must be displayed based on a specific time zone. If you do not specify a time zone, the value is formatted based on the current time-zone settings (more about this shortly). The value of the timeZone attribute can be a standard abbreviation (e.g., "PST," "GMT"), a full name (e.g., "Europe/Stockholm"), or a GMT offset (e.g., "GMT+1"), specified as a static text value or a String variable. Instead of specifying a time zone for each action that needs it, you can use the <fmt:setTimeZone> action, described in Table 14-8, to change the default in the same way as for the locale and localization context. It can also be set by a context parameter named javax.servlet.jsp.jstl.fmt.timeZone.
Yet another alternative is the <fmt:timeZone> action described in Table 14-9. It establishes the time zone for all actions in its body.
Back to Example 14-3. Another new action used in this page is the <fmt:formatNumber> action, used to format numbers according to the rules for the selected locale. It's described in Table 14-10.
The <fmt:formatNumber> action is the companion to the <fmt:formatDate> action, so it offers similar features. The use of the value, var, and scope attributes should be familiar by now. The value can be specified as static text, a String variable, or a Number variable. The other attributes let you specify various formatting rules. The type attribute specifies if the number should be formatted as a regular number, as currency or as a percent value. The currencyCode or currencySymbol attribute can specify the currency symbol when type is set to currency, as an ISO-4217 code (e.g., "USD") or as the actual symbol (e.g., "$"), respectively. This can be useful when you need to show prices expressed in a fixed currency, but you want the amount to be formatted according to the selected locale. You can use the remaining attributes to adjust the default formatting rules defined by the type: groupingUsed, maxIntegerDigits, minIntegerDigits, maxFractionDigits, and minFractionDigits. The attribute names should be self-explanatory. The pattern attribute specifies a custom pattern, the same as with the <fmt:formatDate> action. Appendix B contains a complete reference of the symbols you can use, but here are some examples for the English locale to give you an idea of what a pattern looks like when it's applied to 10000:
When the pattern attribute is specified, it overrides the type attribute and all the format adjustment attributes. The first occurrence of the <fmt:formatNumber> action in Example 14-3 is used to display the total number of votes, just before the table that shows the distribution of the votes. The table with details about the distribution comes next. Here I have used a trick with nested tables to generate a simple bar chart. I also use the <fmt:message> action described earlier in a new way: ...
<table width="70%">
<tr>
<td width="30%">
<fmt:message key="result1">
<fmt:param value="${pollResult.answer1Percent}" />
<fmt:param value="${pollResult.answer1}" />
</fmt:message>
</td>
<td>
<table
width="<fmt:formatNumber
value="${pollResult.answer1Percent}"/>%"
bgcolor="lightgreen">
<tr>
<td> </td>
</tr>
</table>
</td>
</tr>
...
The main table contains a row with two cells for each poll answer. The first cell is just a regular cell, with the answer text, the percentage of votes with this answer, and the absolute number of votes with this answer. The value is inserted by a <fmt:message> action with nested <fmt:param> actions. This is a technique you can use when the localized message contains dynamic values—in this case, the percentage and absolute number of votes. A message of this type looks like this: result1=One year {0, number, integer}% ({1, number, integer})
The message contains placeholders for dynamic values within curly braces. A number associates each placeholder with a <fmt:param> action, starting with 0 for the first one. Optionally, the value type can be specified with one or more comma-separated keywords, as in this example. Appendix B describes all options. The next cell is also interesting. It contains a nested table, and the width of the table is set to the same percentage value as the percentage of votes with this answer. By specifying a required space (using the HTML code) as the value of the single cell and a unique background color, the result is a simple dynamic bar chart. As the percentage values of the answers change, the width of each nested table changes as well, as shown in Figure 14-3. Pretty neat! 14.2.2 Using a Separate Page per LocaleThe JSTL I18N actions make it easy to use the same page for all locales. But as described earlier, sometimes it's better to use a separate page per locale. The poll example uses this approach for the detailed description of the question. As shown in Example 14-1, the poll.jsp page uses a resource bundle with the base name pages to hold the name of the details page for each locale. Here's how the pages_sv.properties looks: details_page=details_sv.jsp This makes it possible to use the <fmt:message> action to dynamically generate a link to a separate page for each locale: <a href="<fmt:message key="details_page" bundle="${pagesBundle}" />">
<fmt:message key="question" />
</a>
Here, I specify the bundle for the <fmt:message> action explicitly since the pages bundle is not the default bundle. All that remains is to create a page per supported locale. Example 14-4 shows the Swedish page. Example 14-4. Swedish details page (details_sv.jsp)<%@ page contentType="text/html" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<title>
<fmt:message key="title" />
</title>
</head>
<body bgcolor="yellow">
<h1>
<font color="blue">
<fmt:message key="question" />
</font>
</h1>
<font color="blue">
Idag introduceras nya teknologier och affärsideer mycket
snabbt. Produkter som såg ut som givna vinstbringare
igår är idag så vanliga att det inte går att tjäna
pengar på dem, med flera versioner tillgängliga gratis
som Open Source. En affärsplan baserad på inkomst från
annonser på en populär web site, eller att lägga till
".com" till företagsnamnet, väcker inte samma intresse
hos investerare idag som det gjorde för bara några månader
sedan.
<p>
I en industri som rör sig så här snabbt, hur lång tid
törs du allokera till utveckling av en ny produkt eller
tjänst, utan att riskera att den är ointressant när den
väl är färdig?
</font>
</body>
</html>
As you can see, most of this page consists of Swedish text. The colors of the Swedish flag (yellow and blue) are also used as the background, header, and text colors. The detail pages for the other locales follow the same pattern. When the amount of text is large and other details of the page differ, like the colors in this example, it's often convenient to use a separate page for each locale instead of the one-page approach described earlier. |
| [ Team LiB ] |
|