Internationalize your software, Part 2

news
Jan 9, 199927 mins

Learn how to develop software for the global marketplace

Last month, I introduced a three-part series that explores the topic of developing Java-based software for an international audience. After a quick introduction to the concepts of internationalization and localization, Part 1 of this series explored character sets and standards for defining characters, introduced the concept of a locale along with Java’s Locale class; and introduced the concept of a resource bundle along with Java’s ResourceBundle, PropertyResourceBundle and ListResourceBundle classes.

Read the whole “Internationalize Your Software” series:

Part 2 expands upon the material presented last month, including:

  • Internationalization and localization, part 2
  • Characters and character definition standards, part 2
  • Locales, part 2
  • Resource bundles, part 2

A complete list of Java’s internationalization and localization classes is presented along with the concept of an “umbrella class.” We examine how to use Java classes for determining character properties, sorting characters, and detecting textual boundaries — from an international perspective. We also look at how to use the host computer’s operating system (using Windows 95 as an example) to set the default locale.

Finally, Part 2 concludes by expanding on last month’s discussion of resource bundles. More specifically, we look at how to access objects that are contained within a resource bundle. We find out how to use property resource bundles with applets — without running into security violations. And we explore how to use a list resource bundle to hold nontextual data — such as locale-specific images.

Some notes on this part of the series: In last month’s segment, I mentioned that formatters would be covered in the second part of this series. However, due to reorganization, coverage of formatters will be deferred to Part 3.

Java applets are used to illustrate Java’s internationalization and localization features. These applets were compiled with the JDK 1.1.6 compiler and tested with the JDK 1.1.6 appletviewer and Netscape Navigator 4.06 programs. Netscape was running version 1.1.5 of the Java runtime environment during testing.

Internationalization and localization, part 2

Last month, we defined internationalization and localization. A checklist of elements, that should be verified when localizing software, was presented. This month, we’re going to examine a complete list of classes (as of JDK version 1.1.6) that composes Java’s internationalization and localization resources. We’ll also examine umbrella classes.

Internationalization and localization classes

We’ve already been introduced to Java’s ListResourceBundle, Locale, PropertyResourceBundle, and ResourceBundle classes. However, these classes are just the tip of the iceberg. There are many more classes that are part of Java’s internationalization and localization framework.

The code below lists all of the internationalization and localization classes that are part of JDK 1.1.6. Classes are listed with full package names. Base classes are listed above and to the left of child classes. Abstract classes are listed in italics.

java.lang.Object
     java.text.BreakIterator
          java.text.SimpleTextBoundary
     java.util.Calendar
          java.util.GregorianCalendar
     java.lang.Character
     java.text.CollationElementIterator
     java.text.CollationKey
     java.text.Collator
          java.text.RuleBasedCollator
     java.text.CollationRules
     java.text.CompactByteArray
     java.text.CompactCharArray
     java.text.CompactIntArray
     java.text.CompactShortArray
     java.text.CompactStringArray
     java.util.Date
     java.text.DateFormatSymbols
     java.text.DecimalFormatSymbols
     java.text.DecompositionIterator
     java.text.DigitList
     java.text.EntryPair
     java.text.FieldPosition
     java.text.Format
          java.text.DateFormat
               java.text.SimpleDateFormat
          java.text.MessageFormat
          java.text.NumberFormat
               java.text.ChoiceFormat
               java.text.DecimalFormat
     java.util.Locale
     java.text.MergeCollation
     java.text.ParsePosition
     java.text.PatternEntry
     java.io.Reader
          java.io.InputStreamReader
     java.util.ResourceBundle
          java.util.ListResourceBundle
          java.util.PropertyResourceBundle
     java.text.SpecialMapping
     java.text.StringCharacterIterator (implements java.text.CharacterIterator)
     java.text.TextBoundaryData
          java.text.CharacterBreakData
          java.text.LineBreakData
          java.text.SentenceBreakData
          java.text.WordBoundaryData
     java.util.TimeZone
          java.util.SimpleTimeZone
     java.lang.Throwable
          java.lang.Exception
               java.text.ParseException
               java.lang.RunTimeException
                    java.lang.MissingResourceException
     java.text.UnicodeClassMapping
     java.text.Utility
     java.text.WordBreakTable
     java.io.Writer
          java.io.OutputStreamWriter

You might find this class list handy when exploring the JDK. However, you should not use any class that isn’t listed in the JDK documentation (some of these classes are for internal use only).

As a point of interest, these classes were originally coded by Taligent (an IBM subsidiary) and have been licensed to Sun. Although Taligent and IBM hold the original copyright to these classes, Sun also holds copyright to portions of this code.

Umbrella classes

An umbrella class is an abstract base class that describes either a common entity (such as a calendar) or a common operation (such as sorting). This class shields code from the many concrete (as opposed to abstract) subclass implementation differences by providing a common interface to these implementations. Umbrella classes contain static factory methods, giving code the ability to instantiate objects from concrete subclasses without needing to know any details about those subclasses.

When writing international code, it’s important to be as locale-independent as possible. Therefore, it’s a good idea to work directly with the umbrella classes, instead of instantiating objects from their concrete subclasses.

Java’s internationalization and localization classes include the following umbrella classes:

  • BreakIterator
  • Calendar
  • Collator
  • DateFormat
  • NumberFormat
  • ResourceBundle
  • TimeZone

With the exception of TimeZone, the following code fragment shows how to call the static factory methods for these classes (I’ll show you how to call TimeZone‘s factory methods in the third part of this series, where I’ll discuss time zones in more detail).

// Obtain a Chinese break iterator for sentences.

BreakIterator bi = BreakIterator.getSentenceInstance (Locale.CHINESE);

// Obtain a German calendar.

Calendar cal = Calendar.getInstance (Locale.GERMAN);

// Obtain an American English collator.

Collator col = Collator.getInstance (new Locale ("en", "US"));

// Obtain an Italian date formatter.

DateFormat df = DateFormat.getDateInstance (DateFormat.SHORT, Locale.ITALIAN);

// Obtain a currency formatter for France.

NumberFormat nf = NumberFormat.getCurrencyInstance (Locale.FRANCE);

// Obtain an Arabic images resource bundle.

ResourceBundle rb = ResourceBundle.getBundle ("images", new Locale ("ar", ""));

The preceding code shows that each factory method takes a Locale object argument. This argument is either a directly instantiated Locale object or is an indirectly instantiated Locale object — a language constant such as Locale.GERMAN. However, each umbrella class also contains one or more factory methods that don’t take Locale object arguments. These other factory methods work with the default locale.

With the exceptions of ResourceBundle and TimeZone, all of the umbrella classes contain the getAvailableLocales () method, which returns an array of Locale objects for those locales that the umbrella class supports. Instead of assuming that you can pass any Locale object to an umbrella class, you should get into the habit of writing code that calls the getAvailableLocales () method and iterates through its list of supported locales, to see if a specific locale is supported. The following code fragment shows how to do this:

Locale l [] = Calendar.getAvailableLocales ();

Locale target = Locale.ITALIAN;

int i;

for (i = 0; i < l.length; i++) if (l [i].equals (target)) break;

if (i == l.length) { // target locale not found

... } else { // target locale found

... }

There may be times when you need access to functionality that is only found in a concrete subclass. For example, it’s possible to create more flexible date formatters by working directly with DateFormat‘s SimpleDateFormat subclass. The following code fragment shows how to create a SimpleDateFormat object:

SimpleDateFormat formatter = new SimpleDateFormat ("yyyy.mm.dd e");

The argument being passed to SimpleDateFormat‘s constructor identifies a new date format. Check with the JDK documentation for more detailed information about date formats.

The umbrella classes provide the maximum degree of portability when writing international software because they hide the specifics of concrete subclasses. On the other hand, concrete subclasses contain useful functionality (as shown by SimpleDateFormat) which is not normally accessible through the umbrella classes — there are limitations to portability.

Is it a good idea to focus exclusively on the umbrella classes? Yes and no. Yes, because future versions of Java may deprecate some of the concrete subclasses and it is doubtful that the umbrella classes would be deprecated. No, because you may need functionality that you cannot get through an umbrella class.

So what do you do? Try to isolate the code that works with concrete subclasses. Perhaps you could create a class library. If you ever need to change this code, this approach would minimize your work.

Characters and character definition standards, part 2

Last month, we defined character and character set. We also examined the EBCDIC, ASCII, and Unicode character definition standards. This month, we’re going to take a look at character properties, along with Java’s Character class. We’ll see how Java makes it possible to sort characters by using collators. Finally, we’ll examine boundary detection — an important operation to certain classes of software (such as word processors).

Character properties

A character property is some attribute of a character that sets it apart from others. For example, the character 6 is known as a digit and is used in the formation of numbers. On the other hand, the character U is known as an uppercase letter and is used in the formation of words. Writing code to examine character properties is something that many of us have done at one time or another. The following code fragment provides an illustration.

String s = ...;

for (int i = 0; i < s.length; i++) { char ch = s.charAt (i);

if (ch >= 'a' && ch <= 'z') { ... } else if (ch >= '0' && ch <= '9') { ... } else { ... } }

The preceding code is not internationalized. Although the English language defines a lowercase letter as lying between a and z, other languages have lowercase letters (with various accents) that do not lie in this range — French and German are good examples of such languages. However, the lowercase letters in these languages are just as valid.

And what about digits? The following table lists some Unicode number ranges for digits that are used in a variety of cultures. These number ranges are shown using Hexadecimal notation.

Unicode number rangeCulture description
0x0030 - 0x0039ISO-LATIN-1 digits (0 through 9)
0x0660 - 0x0669Arabic-Indic digits
0x06F0 - 0x06F9Extended Arabic-Indic digits
0x0BE7 - 0x0BEFTamil digits
0x0E50 - 0x0E59Thai digits
0x0ED0 - 0x0ED9Lao digits
0x0F20 - 0x0F29Tibetan digits

The preceding code fragment handles only those digits that are ISO-LATIN-1 compatible (as seen in the first row of the preceding table).

How can we correct this problem? Take a look at the following code fragment:

String s = ...;

for (int i = 0; i < s.length; i++) { char ch = s.charAt (i);

if (Character.isLowerCase (ch)) { ... } else if (Character.isDigit (ch)) { ... } else { ... } }

The preceding code fragment solves the problem. It calls methods that are defined by Java’s Character class to determine if a character is a lowercase letter or a digit. It is better to examine a char variable’s character properties by calling Character‘s methods than to attempt to examine these properties by comparing the variable against literal characters because different languages have different definitions of what is a lowercase letter, what is a digit, etc. And this information has been encapsulated within the Character class. Detailed information about Character is available in the following class reference, located at Sun’s Java Web site: https://java.sun.com/products/jdk/1.1/docs/api/java.lang.Character.html.

Some of Character‘s more useful methods include:

  • isDigit (char)
  • isLetter (char)
  • isLetterOrDigit (char)
  • isLowerCase (char)
  • isSpaceChar (char)
  • isUpperCase (char)

The getType (char) method is useful in examining a character’s properties. The following code fragment calls this method to see if the English + character is a math symbol (as specified by the MATH_SYMBOL property constant) and to see if the German letter ¸ is a lowercase letter (as specified by the LOWERCASE_LETTER property constant).

if (Character.getType ('+') == Character.MATH_SYMBOL) ...

if (Character.getType ('¸') == Character.LOWERCASE_LETTER) ...

The Character class defines several property constants, that should be used with getType (char) when examining character properties.

Comparing strings

Software often organizes lists of strings into ascending or descending order. This “organizing” is handled by sorting algorithms, which rely on comparison logic to decide on how to order pairs of strings. Strings are compared on a character by character basis. The first character of the first string is compared with the first character of the second string. If they match, then the second character of the first string is compared with the second character of the second string. This process continues until either both strings match or a pair of characters is found that doesn’t match (which is also the case if one string is longer than or shorter than the other).

Because computers work with binary numbers, comparison logic is really comparing binary numbers. If one character’s binary value (associated with that character’s symbol) is smaller than the other character’s binary value, the symbol associated with this smaller binary value is assumed to precede the other symbol. Therefore, it’s important to match symbols to appropriate binary numbers — to reflect the natural ordering of a language’s symbols.

Java’s String class contains a compareTo (String) method that compares a pair of strings on a Unicode character by Unicode character basis. Why don’t we use this method? Unfortunately, we can’t. This method contains hardcoded logic that compares strings according to the English language’s notion of alphabetical order. This is not acceptable to anyone who does not have an English background. However, Java comes to the rescue with an innovative solution.

Java provides a Collator class that gives your code the ability to compare strings in a language-independent fashion. Detailed information about Collator is available in the following class reference, located at Sun’s Java Web site: https://java.sun.com/products/jdk/1.1/docs/api/java.text.Collator.html.

We previously noted that Collator is an abstract class and that we need to call one of its static factory methods to instantiate an object from one of its subclasses. Let’s take a look at an applet that sorts a list of French and Spanish words using the French, Spanish, and United States collators. This applet is shown below. The source code to this applet is located in example5.java.

You need a Java-enabled browser to view this applet.

Below is a code fragment, taken from the preceding applet, that shows how to create a collator.

// Obtain a locale-specific collator object.

if (locale.equals ("Spanish")) { // Define the traditional Spanish sort rules.

String upperNTilde = new String ("u00D1"); String lowerNTilde = new String ("u00F1");

String spanishRules = "< a,A < b,B < c,C < ch, cH, Ch, CH < d,D < e,E < f,F < g,G " + "< h,H < i,I < j,J < k,K < l,L < ll, lL, Ll, LL < m,M < n,N " + "< " + lowerNTilde + "," + upperNTilde + " " + "< o,O < p,P < q,Q < r,R < s,S < t,T < u,U < v,V < w,W < x,X " + "< y,Y < z,Z";

// Attempt to create a new RuleBasedCollator object. If there is a // syntax error in the rules String then a ParseException object is // thrown.

try { c = new RuleBasedCollator (spanishRules); }

catch (ParseException ex) { ta.setText ("ParseException thrown.");

return; } } else c = Collator.getInstance (target);

The Collator umbrella class does not properly order the Spanish ch and ll letters in the traditional manner. Therefore, we need to establish a new set of ordering rules for the Spanish language. These rules are specified, using a special syntax, as a Java String and this String is passed, as an argument, to the constructor when a RuleBasedCollator object is instantiated.

Notice that, unlike the static Collator.getInstance (Locale) factory method, RuleBasedCollator‘s constructor does not take a Locale object argument. The reason for this is that Collator.getInstance (Locale) uses the Locale object argument to locate the appropriate set of ordering rules for that locale. Since we are working with a specific locale’s ordering rules, we don’t need this argument. Detailed information about RuleBasedCollator is available in the following class reference, located at Sun’s Java Web site: https://java.sun.com/products/jdk/1.1/docs/api/java.text.RuleBasedCollator.html.

Sorting strings can take a lot of time. Much of this time is spent in the comparison process. However, there’s a way to speed up this activity — by taking advantage of Java’s CollationKey class.

Objects that are instantiated from CollationKey represent a sort key for a given String object and a given Collator object. It’s faster to compare two CollationKey objects than to compare two String objects because CollationKey objects are nothing more than integers — and it’s faster to compare a pair of integers than a pair of strings. However, it does take time to generate CollationKey integers so you wouldn’t want to use these objects if you’re only going to compare a few String objects. Those of you who are interested in learning more about this class should take a look at the detailed information about CollationKey in the following class reference, located at Sun’s Java Web site: https://java.sun.com/products/jdk/1.1/docs/api/java.text.CollationKey.html.

Detecting boundaries

Certain types of software need to detect boundaries — character, line, sentence, and word boundaries — when processing strings. For example, word processors need to detect boundaries when cutting and pasting characters, wrapping words at the end of a line, moving to the next or previous sentence, and highlighting words. While this is conceptually easy from an English viewpoint, detecting these boundaries in more pictorial languages — such as Chinese, Arabic, or Hebrew — is not so easy. Fortunately, Java contains a class, called

BreakIterator

, that simplifies this task. Detailed information about

BreakIterator

is available in the following class reference, located at Sun’s Java Web site:

https://java.sun.com/products/jdk/1.1/docs/api/java.text.BreakIterator.html

.

BreakIterator contains four factory methods for obtaining iterator objects — objects that contain logic for moving between characters, lines, sentences and words. These methods include:

  • getCharacterInstance ()
  • getLineInstance ()
  • getSentenceInstance ()
  • getWordInstance ()

Each iterator object detects one type of boundary (character, line, sentence, or word). You need to instantiate more than one iterator object if you want to detect multiple boundaries.

Iterator objects contain the notion of an imaginary cursor. This cursor points to the current boundary within a string. The BreakIterator class defines several abstract methods for moving the cursor between boundaries while concrete subclasses provide the actual definitions for these methods. The following cursor movement methods are defined by BreakIterator.

  • current ()
  • first ()
  • last ()
  • next ()
  • next (int)
  • previous ()

These methods return a zero-based integer that represents a boundary index. The current () method returns the boundary index of the most recent call to the other cursor movement methods. The first () method moves the cursor to the first boundary while the last () method moves the cursor to the last boundary. The previous () method moves the cursor to the previous boundary. Finally, the next (int) method moves the cursor to the nth boundary — identified by the int argument — from the current boundary. Passing a value of 0 for this argument results in no action being taken.

So how does cursor movement work? Suppose we want to move the cursor between the words that make up the following sentence: The day turned out to be colder than expected. The first task is to note word boundaries (Think of a word boundary as the location of an imaginary cursor that is used when moving from one word to another). The following code shows the word boundaries for each word making up our example sentence.

Boundary  0 precedes: The 
Boundary  4 precedes: day 
Boundary  8 precedes: turned 
Boundary 15 precedes: out 
Boundary 19 precedes: to
Boundary 22 precedes: be
Boundary 25 precedes: colder
Boundary 32 precedes: than
Boundary 37 precedes: expected.
Boundary 46 follows : .

Prior to moving our imaginary cursor, the current () method returns the number 0. This number represents the word boundary that precedes the word The. If we call next (), this method would return 4 — the boundary that precedes the word day. The number 46 would be returned if we called the last () method. This number is the index of the boundary that follows the period (.) character.

How do we instantiate iterator objects? The following code fragment shows how to instantiate objects that represent character, line, sentence and word iterators.

// Obtain a character iterator for the German locale.

BreakIterator ci = BreakIterator.getCharacterInstance (Locale.GERMAN);

// Obtain a line iterator for the United States locale.

BreakIterator li = BreakIterator.getLineInstance (new Locale ("en", "US"));

// Obtain a sentence iterator for the France locale.

BreakIterator si = BreakIterator.getSentenceInstance (Locale.FRANCE);

// Obtain a word iterator for the China locale.

BreakIterator wi = BreakIterator.getWordInstance (new Locale ("zh", "CN"));

Sun provides a really good tutorial on Java called (you guessed it) — The Java Tutorial. It contains a section on internationalization. Boundary detection is one of topics covered under this section and includes some really good examples of iterators. Check out Resources for a link to The Java Tutorial.

Locales, part 2

Last month, we defined locale. We learned that a locale is nothing more than an identifier that combines language and region codes. We saw that locales are objects that are instantiated from Java’s Locale class. We learned that there is no such thing as a global locale, making it possible to write software that simultaneously works with multiple locales. This month we’re going to learn how to change the default locale.

Changing the default locale

The default locale is the locale that is immediately available to a Java program when that program starts running. This locale is based on the regional settings of the computer that is hosting the Java virtual machine and can be obtained by calling Locale.getDefault ().

You might want to change the default locale. Why? Suppose you’ve finished localizing a program and are starting to test it to see how this program reacts to different locales. If there are any locale-related bugs, you need to know about them before your software ships. You can call Locale.setDefault (Locale) to change the default locale.

The following code fragment calls setDefault (Locale) to change the default locale and getDefault () to obtain the default locale.

// Create a new locale with English as the language and Ireland as the region.

Locale l = new Locale ("en", "IE");

// Set the default locale to this locale.

Locale.setDefault (l);

// Display the default locale (should be English/Irish).

System.out.println (Locale.getDefault ());

Although the preceding code works with Java applications, you’ll run into a security violation if you attempt to change the default locale from within an applet. This is due to security restrictions that Java’s security manager places on applets in order to protect a user’s machine from either malicious or accidental damage.

There is a second technique that you can use to change the default locale for both applications and applets. This technique does not raise any security violations, but it is platform-specific. Basically, you need to change the regional settings on your computer prior to running your Java applet or application. I’ll show you how to adjust these settings under Windows 95. If you have a different operating system, you’ll need to review your operating system documentation to find out how to adjust its regional settings.

Figure 1 shows the Windows 95 control panel. You can open the control panel by selecting Settings/Control Panel from the Start menu (located in the left-hand corner of the Windows 95 task bar). Double-click the Regional Settings icon.

Once you’ve double-clicked this icon, a Regional Settings Property Sheet dialog box will appear. This dialog box, as shown in Figure 2, provides a tabbed control that lets you select your region of interest and fine-tune this region by making individual adjustments to the number, currency, time and date formats.

Once you’ve made your adjustments, click on either the OK or Apply buttons. You’ll be prompted to restart your machine before the new settings take effect. After restarting your machine, try running your Java applet or application. The default locale should reflect the new regional settings.

Resource bundles, part 2

Last month, we defined resource bundle. We learned that a resource bundle is a container that holds a program’s locale-specific elements once that program has been internationalized. We examined property resource bundles and list resource bundles and learned how to obtain a bundle by calling ResourceBundle.getBundle (String, Locale). We also saw that resource bundles are organized into families that share a common family name but differ by locale. Finally, we looked at the search algorithm that is used to locate a specific bundle.

This month we’ll take a closer look at how to access a bundle’s objects. We’ll correct a mistake that was made in last month’s search algorithm and look at a concrete example of how search works. We’ll examine how to use property resource bundles in applets and how to use list resource bundles with nontextual data.

Accessing a bundle’s objects

Every ResourceBundle subclass contains methods for accessing the locale-sensitive objects that are stored within the bundle. The following method is the most basic:

public final Object getObject (String);

The getObject (String) method returns a reference to an object that is associated with the String argument. This argument is known as a key. This value may need to be cast to a more appropriate object before it can be used. For example, the following code fragment shows how to obtain a Button object that is associated with a key called OK:

ResourceBundle rb = ResourceBundle.getBundle ("buttons");   // buttons + language code + region code is a ...
Button b = (Button) rb.getObject ("OK");                    // ... subclass of ListResourceBundle

The same technique can be used for retrieving Strings. However, there are more convenient methods that can be used with Strings. These methods are shown in the following code fragment:

public final String getString (String);
public final String [] getStringArray (String);

We saw an example of calling getString (String) in last month’s installment of this series.

Searching for resource bundles

In last month’s discussion, I described the process used to search for a resource bundle. I mentioned that this process is based on a specified locale and that a MissingResourceException object is thrown if the resource bundle cannot be found. This is not quite correct.

If the search (using a specified locale) fails, the search is repeated using the default locale. However, this search does not attempt to locate a resource bundle by family name only, because this has already been done. If this search fails, then a MissingResourceException object is thrown.

Let’s look at a concrete search example. Suppose I’ve created a resource bundle family that contains binary data for flag images. The family name for these resource bundles is “flag”. Now suppose that my default locale is Australia, and I’m attempting to obtain a resource bundle for the China locale. I do some research and discover that the language code for Chinese is “zh” and the country code for China is “CN”. Furthermore, I know that the language spoken in Australia is English, so its language code is “en”. I also know that the country code for Australia is “AU”.

How does the search work? A search is attempted using the specified locale (China).

  1. Look for bundle composed of family name, language code, and region code (flag_zh_CN). Quit if found.

  2. Look for bundle composed of family name and region code (flag_zh). Quit if found.

  3. Look for bundle composed of family name (flag). Quit if found.

The initial search fails, so a new search is attempted using the default locale (Australia).

  1. Look for bundle composed of family name, language code, and region codes (flag_en_AU). Quit if found.

  2. Look for bundle composed of family name and region code (flag_en). Quit if found.

  3. Throw MissingResourceException object.

Using property resource bundles in applets

Remember last month’s Example3 applet — the applet that works with property resource bundles? I mentioned that this applet would not run under Netscape because it tries to access the user’s machine to read the underlying property file, and this activity results in a security violation. This security violation causes the Java runtime code to throw a MissingResourceException object. So how can we get past this problem?

The solution is to place Example3’s class and property files in a jar file, and to include a reference to this jar file in the <APPLET> tag. The following code fragment uses the jar command (under Windows 95) to create a jar file called example3.jar. This command places example3.class and all files that end with a .properties extension into this jar file:

jar -cvf example3.jar example3.class *.properties

The following code fragment shows the <APPLET> tag with the new ARCHIVE attribute. This attribute is used to identify the jar file.

<APPLET ARCHIVE="example3.jar" 
    CODEBASE="/javaworld/jw-01-1999/internationalize" 
    CODE="example3.class" 
    WIDTH=200 
    HEIGHT=220>
</APPLET>

And now for the grand finale! The following PropertyResourceBundle example shows the same Example3 applet we were introduced to in last month’s installment. The source code and class files are the same. The only differences are that the Example3 class and properties files have been placed into a jar file, and that the <APPLET> tag has an ARCHIVE attribute.

You need a Java-enabled browser to view this applet.

Using list resource bundles with nontextual data

Last month’s Example4 applet was used to illustrate the ListResourceBundle class. This class makes it possible for a program to contain nontextual localized data in subclasses. The Example4 applet instantiated objects from ListResourceBundle subclasses that contain text. However, these subclasses can contain more than textual data.

To prove this, let’s construct a new applet that uses list resource bundles to hold flag images. This applet is shown below and its source code is located in example6.java.

You need a Java-enabled browser to view this applet.

I’ll bet you’re wondering how this works. The following listing shows the source code to a list resource bundle that contains image data for the Australian flag.

// ex6_en_AU.java

// Resource bundle for Australia locale (language = English, region = Australia)

import java.awt.*; import java.util.*;

public class ex6_en_AU extends ListResourceBundle { private byte image [] = { (byte) 71,

...

(byte) 255 };

private Object [][] contents = { { "flag", Toolkit.getDefaultToolkit ().createImage (image) } };

public Object [][] getContents () {

return contents; } }

Notice the reference to the createImage (byte []) method. This method is defined by the Toolkit class to create an image based on the contents of a byte array. The contents of this array must conform to a valid image file format. Currently, only GIF, JPEG, and XBM are valid.

What happens after we’ve created this image? Here is a code fragment that contains an answer:

try

{ // Attempt to obtain resource bundle associated with the // locale argument.

ResourceBundle rb = ResourceBundle.getBundle ("ex6", l);

if (rb == null) return;

// Get flag image resource from bundle and display in window.

ic.displayImage ((Image) rb.getObject ("flag")); } catch (MissingResourceException e) { System.out.println (e);}

After obtaining an appropriate bundle for the locale, specified by l, the code tries to read an object from this bundle by calling getObject (String) with flag as the key. Java’s runtime calls the getContents () method that’s defined inside the subclass. This method returns a reference to the contents object array. The runtime iterates through this array until it finds an entry whose key matches the flag argument. Once found, a reference to the appropriate object is returned to our code. What could be simpler?

Conclusion

We’ve covered a lot of ground — from listing Java’s internationalization and localization classes to working with list resource bundles that hold nontextual data. Please make use of the resources that appear in the following Resources section. They contain valuable information that can be used when you decide to internationalize your software. Next month, this series concludes. We’ll finally cover formatters and examine the two-sided coin of formatting and parsing. We’ll also spend some time looking at dates, calendars, and time zones (pun intended). See you next month.

Jeff is a consultant working with various technologies including C++, digital signatures/encryption, Java, smart cards and Win32. He has worked for a number of technology-related consulting firms including EDS (Electronic Data Systems).