Internationalization


A significant amount of software product gets deployed to all corners of the globe. No longer can you get away with developing an application for only your home language. Some localities, such as Quebec, require all software to execute in two or more languages. Success in the global marketplace hinges on the ability to sell your software in many countries using many different languages.

Building software so that it supports different languages and cultures is known as internationalization, or i18n[8] in shorthand. Preparing software for delivery to a single locale is known as localization. A locale is a region. Geography, culture, or politics can define locales.

[8] Count the number of letters between the first and last letters of internationalization

Retrofitting a large, existing application to support multiple languages can be expensive. This might lead you to believe that you must internationalize your application from the outset of development. Perhaps. Extreme adherence to the "no duplication" rule can leave you with a design that supports a rapid transition to internationalized software. Nonetheless, building a foundation for internationalization can make things easier. You can justify these building blocks because they do help eliminate duplication.

Use internationalization mechanisms to help eliminate duplication and position you to localize your application.


Resource Bundles

String literals embedded directly within code cause problems. Most significant, they create duplication. If a literal appears in production code and if you've been testing everything like you should, some test will duplicate that same literal. Also, the meaning of the literal is not always clear. In Agile Java, I had you extract String literals to class constants. Both the test and production code can refer to the same constant. The name of the constant imparts meaning.

Still, having to maintain a bunch of class constants is painful. Consider also the needs of the international application. The ideal solution involves extracting all literals to a common file. When you must deploy the software to a different locale, you provide a new file that contains the translated text.

Instances of the Java class java.util.ResourceBundle manage interaction with locale-specific resource files. To internationalize your software, you'll update all code to ask a ResourceBundle for a localized message String.

Using a ResourceBundle is pretty easy. You obtain a ResourceBundle by calling the creation method getBundle, passing it the name of the bundle. You then work with the ResourceBundle like a mapyou pass it a key, it gives you a localized object in return.

Testing use of a ResourceBundle is another matter. You want to ensure that you can read a given property and its value from a ResourceBundle. But you can't presume that a key and certain value already exists in the resource file. The easiest solution would be to write out a small file with a known key and value. The problem is, you don't want to overwrite the resource file that the rest of your application requires.

 package sis.util; import junit.framework.TestCase; import java.io.IOException; public class BundleTest extends TestCase {    private static final String KEY = "someKey";    private static final String VALUE = "a value";    private static final String TEST_APPEND = "test";    private static final String FILENAME =        "./classes/sis/util/" + Bundle.getName() + "Test.properties";    private String existingBundleName;    protected void setUp() {       TestUtil.delete(FILENAME);       existingBundleName = Bundle.getName();       Bundle.setName(existingBundleName + TEST_APPEND);    }    protected void tearDown() {       Bundle.setName(existingBundleName);       TestUtil.delete(FILENAME);    }    public void testMessage() throws IOException {       writeBundle();       assertEquals(VALUE, Bundle.get(KEY));    }    private void writeBundle() throws IOException {       LineWriter writer = new LineWriter();       String record = String.format("%s=%s", KEY, VALUE);       writer.write(FILENAME, record);    } } 

The test carefully manages a test resource file using setUp and tearDown. It interacts with a class you will create named sis.util.Bundle to retrieve the name of the existing bundle. The test stores the existing resource file base name, then tells Bundle to use a new base name. Only the test will recognize this base name. The test itself (testMessage) writes to the test resource file. Each entry in the resource file is a key value pair separated by an equals sign (=).

The assertion in testMessage calls the get method on the Bundle class to retrieve a localized resource. Finally, the tearDown method resets Bundle to use the original base name.

 package sis.util; import java.util.ResourceBundle; public class Bundle {    private static String baseName = "Messages";    private static ResourceBundle bundle;    static String getName() {       return baseName;    }    static void setName(String name) {       baseName = name;       bundle = null;    }    public static String get(String key) {       if (bundle == null)          loadBundle();       return (String)bundle.getString(key);    }    private static void loadBundle() {       bundle = ResourceBundle.getBundle("sis.util." + getName());    } } 

The method loadBundle obtains a ResourceBundle instance by base name. The base name is similar to a fully qualified class name. Presume that you have a resource file with a fully qualified path name of ./classes/sis/util/Messages.properties. Also presume that the directory ./classes appears on the classpath. In order for getBundle to locate this resource file, the base name must be "sis.util.Messages".

Once you have a ResourceBundle object, you can send it a few different messages to extract localized resources. The message getString returns localized text.

Once you've implemented the Bundle class, you would replace occurrences of literals in your application to use its get method. You will end up with a potentially large file of key-value pairs. If the file size makes management of the resource file unwieldy, consider partitioning it into multiple resource bundles.

Localization

Suppose you must deploy your application to Mexico. You would send your resource file to a translator for localization. The translator's job is to return a new file of key-value pairs, replacing the base value (perhaps English) with a Spanish translation.

(In reality, the translator will probably need to interact with you or someone else who understands the application. The translator will often require contextual information because of multiple word meanings. For example, your translator may need to know whether "File" is a verb or a noun.)

You must name the file that the translator returns to you according to the specific Locale. A locale is a language, a country (which is optional), and a variant (also optional; it is rarely used). For deployment to Mexico, the language is Spanish, indicated by "es" (español). The country is "MX" (Mexico). To come up with a resource file name, you append these bits of locale information to the base name, using underscores ('_') as separators. The Mexican--localized file name for the base name "Messages" is "Messages_es_MX.properties".

You can request a complete list of locales supported on your platform using the Locale class method getAvailableLocales. Code and execute this ugly bit of code:

 for (Locale locale: Locale.getAvailableLocales())   System.out.println(String.format("%s %s: use '_%s%s'",     locale.getDisplayLanguage(), locale.getDisplayCountry(),     locale.getLanguage(),     (locale.getCountry().equals("") ? "" : "_" + locale.getCountry()))); 

The output from executing this code snippet will give you appropriate extensions to the base name.

Here is BundleTest, modified to include a new test for Mexican localization:

 package sis.util; import junit.framework.TestCase; import java.io.IOException; import java.util.Locale; public class BundleTest extends TestCase {    private static final String KEY = "someKey";    private static final String TEST_APPEND = "test";    private String filename;    private String existingBundleName;    private void prepare() {       TestUtil.delete(filename);       existingBundleName = Bundle.getName();       Bundle.setName(existingBundleName + TEST_APPEND);    }    protected void tearDown() {       Bundle.setName(existingBundleName);       TestUtil.delete(filename);    }    public void testMessage() throws IOException {       filename = getFilename();       prepare();       final String value = "open the door";       writeBundle(value);       assertEquals(value, Bundle.get(KEY));    }    public void testLocalizedMessage() throws IOException {       final String language = "es";       final String country = "MX";       filename = getFilename(language, country);       prepare();       Locale mexican = new Locale(language, country);       Locale current = Locale.getDefault();       try {          Locale.setDefault(mexican);          final String value = "abre la puerta";          writeBundle(value);          assertEquals(value, Bundle.get(KEY));       }       finally {          Locale.setDefault(current);       }    }    private void writeBundle(String value) throws IOException {       LineWriter writer = new LineWriter();       String record = String.format("%s=%s", KEY, value);       writer.write(getFilename(), record);    }    private String getFilename(String language, String country) {       StringBuilder builder = new StringBuilder();       builder.append("./classes/sis/util/");       builder.append(Bundle.DEFAULT_BASE_NAME);       builder.append("Test");       if (language.length() > 0)          builder.append("_" + language);       if (country.length() > 0)          builder.append("_" + country);       builder.append(".properties");       return builder.toString();    }    private String getFilename() {       return getFilename("", "");    } } 

BundleTest requires refactoring to support the changes to the test resource filename format. You cannot use the setUp method to delete the test resource file, since its name must change. I renamed setUp to prepare (an adequate name at best). The prepare method assumes that the field filename is populated with the appropriate test resource filename.

The new test, testLocalizedMessage, writes test data to a file using a name encoded with the language and country. The test creates a Locale object using the same language and country. It then obtains the current (default) Locale so that the test can reset the Locale to this default when it completes. As the final preparation before testing the Bundle.get method, the test sets the default locale to the newly created Mexican Locale object.

Setting a Locale is a global change. Any code that might need to be internationalized can request the current Locale so that it knows what to do. Most of the time, this is handled for you. In the case of ResourceBundle, you don't need to do anything. To load a bundle, you supply only the base name. Code in ResourceBundle retrieves the default Locale and combines information from it with the base name to create a complete resource file name.

Formatted Messages

Entries in resource files may contain substitution placeholders. For example:

 dependentsMessage=You have {0} dependents, {1}. Is this correct? 

The value for dependentsMessage contains two format elements: {0} and {1}. Code that loads this string via a ResourceBundle would use a Message Format object to replace the format elements with appropriate values. The following language test demonstrates:

 public void testMessageFormat() {    String message = "You have {0}dependents, {1}. Is this correct?";    MessageFormat formatter = new MessageFormat(message);    assertEquals(       "You have 5 dependents, Señor Wences. Is this correct?",       formatter.format(message, 5, "Señor Wences")); } 

In older code using MessageFormat, you might see the arguments wrapped in an Object array:

 formatter.format(new Object[] {new Integer(5), "Señor Wences"}) 

Why not just use the java.util.Formatter class? The reason is that the MessageFormat scheme predates the Formatter class, which Sun introduced in Java 5.0.

A more interesting reason for using the MessageFormat scheme is to take advantage of the ChoiceFormat class. This can eliminate the need to code additional if/else logic in order to supply format element values. The classic example shows how to manage formatting of messages that refer to numbers. We want to present a friendlier message to Señor Wences, and telling him that he has "one dependents" is unacceptable.[9]

[9] No doubt he would have responded indignantly, "Tell it to the hand."

Using ChoiceFormat is a bit of a bear. You first create a mapping of numeric ranges to corresponding text. You refer to the ranges, expressed using an array of doubles, as limits. You express the corresponding text, or formats, in an array of strings.

 double[] dependentLimits = {0, 1, 2 }; String[] dependentFormats =     {"no dependents", "one dependent", "{0}dependents" }; 

The numbers in the limits array are ranges. The final element in the limits array represents the low end of the rangetwo dependents and up, in this case. For two dependents, the ChoiceFormat would apply the corresponding format string "{0}dependents", substituting the actual number of dependents for {0}.

Using these limits and formats, you create a ChoiceFormat object:

 ChoiceFormat formatter =     new ChoiceFormat(dependentLimits, dependentFormats); 

A message string might have multiple format elements. Only some of the format elements might require a ChoiceFormat. You must create an array of Format objects (Format is the superclass of ChoiceFormat), substituting null where you have no need for a formatter.

 Format[] formats = {formatter, null }; 

You can then create a MessageFormat object and set the array of formats into it:

 MessageFormat messageFormatter = new MessageFormat(message); messageFormatter.setFormats(formats); 

You're now able to use the MessageFormat as before. The entire process is shown in the language test testChoiceFormat.

 public void testChoiceFormat() {    String message = "You have {0}, {1}. Is this correct?";    double[] dependentLimits = {0, 1, 2 };    String[] dependentFormats =        {"no dependents", "one dependent", "{0}dependents" };    ChoiceFormat formatter =        new ChoiceFormat(dependentLimits, dependentFormats);    Format[] formats = {formatter, null };    MessageFormat messageFormatter = new MessageFormat(message);    messageFormatter.setFormats(formats);    assertEquals(       "You have one dependent, Señor Wences. Is this correct?",       messageFormatter.format(new Object[] {1, "Señor Wences" }));    assertEquals(       "You have 10 dependents, Señor Wences. Is this correct?",       messageFormatter.format(new Object[] {10, "Señor Wences" })); } 

Interaction with ChoiceFormat can be even more complex. Refer to the Java API documentation for the class.

Note that the format strings should come from your ResourceBundle.

Other Areas to Internationalize

The formatting of numbers, dates, and currencies can vary depending upon the locale. Display numbers using separators every three digits in order to make them more readable. The separators are commas (,) in the United States, while some other localities use periods (.). Dates in the United States often appear in month-day-year order, separated by slashes or hyphens. Dates elsewhere may appear in a different order, such as year-month-day.

The classes java.text.NumberFormat, java.util.Calendar, and java.text.DateFormat and their subclasses, support the use of Locale objects. Your code that works with these classes should request instances using factory methods. Your code should not directly instantiate these classes, otherwise you will not get the default Locale support. For example, to obtain a SimpleDateFormat instance with support for the default locale:

 DateFormat formatter = SimpleDateFormat.getDateInstance(); 

The Calendar class also allows you to manage time zones.

Various layout managers in Java supply support for different axis orientation. Some non-Western cultures orient text so that it reads from top to bottom instead of left to right. For example, the BoxLayout class allows you to organize components within a container around a LINE_AXIS. Each Locale contains a java.awt.ComponentOrientation object; this object defines whether text reads from right to left or left to right (Western) and whether lines are horizontal (Western) or vertical. At runtime, then, the BoxLayout obtains the ComponentOrientation in order to determine whether LINE_AXIS corresponds to the x axis or the y axis.

Other languages require the use of different character sets. Since Java uses Unicode, you generally don't have to concern yourself with the different character sets. However, if you need to sort text elements, you will have to work with collation sequences. The class java.text.RuleBasedCollator provides the basis for this support. The API documentation for this class contains a good overview of collation in Java.



Agile Java. Crafting Code with Test-Driven Development
Agile Javaв„ў: Crafting Code with Test-Driven Development
ISBN: 0131482394
EAN: 2147483647
Year: 2003
Pages: 391
Authors: Jeff Langr

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net