Lesson 6: Internationalization

   

Lesson 6: Internationalization

[ <<BACK] [ CONTENTS] [ NEXT>>]

More and more companies, large and small, are doing business around the world using many different languages. Effective communication is always good business, so it follows that adapting an application to a local language adds to profitability through better communication and increased satisfaction.

The Java 2 platform provides internationalization features that let you separate culturally dependent data from the application (internationalization) and adapt it to as many cultures as needed (localization).

This lesson takes the two client programs from Part 2, Lesson 5: Collections, internationalizes them and adapts the text to France, Germany, and the United States.

Identify Culturally Dependent Data

 

The first thing you need to do is identify the culturally dependent data in your application. Culturally-dependent data is any data that varies from one culture or country to another. Text is the most obvious and pervasive example of culturally dependent data, but other things like number formats, sounds, times, and dates must be considered too.

The RMIClient1.java and RMIClient2.java classes have the following culturally-dependent data visible to the end user:

  • Titles and labels (window titles, column heads, and left column labels)
  • Buttons (Purchase, Reset, View)
  • Numbers (values for item and cost totals)
  • Error messages

Although the application has a server program, the server program is not being internationalized and localized. The only visible culturally-dependent data in the server program is the error message text.

The server program runs in one place and the assumption is that it is not seen by anyone other than the system administrator who understands the language in which the error messages is hard coded. In this example, it is English.

All error messages in RMIClient1 and RMIClient2 are handled in try and catch blocks, as demonstrated by the print method below. This way you have access to the error text No data available for translation into another language.


  public void print(){
    if(s!=null){
      Iterator it = s.iterator();
      while(it.hasNext()){
        try{
          String customer = (String)it.next();
          System.out.println(customer);
        }catch (java.util.NoSuchElementException e){
          System.out.println("No data available");
        }
      }
    }else{
      System.out.println("No customer IDs available");
    }
  }

The print method could have been coded to declare the exception in its throws clause as shown below, but this way you cannot access the error message text thrown when the method tries to access unavailable data in the set.

In this case, the system-provided text for this error message is sent to the command line regardless of the locale in use for the application. The point here is it is always better to use try and catch blocks wherever possible if there is any chance the application will be internationalized so you can localize the error message text.

  public void print() 
        throws java.util.NoSuchElementException{
    if(s!=null){
      Iterator it = s.iterator();
      while(it.hasNext()){
        String customer = (String)it.next();
        System.out.println(customer);
      }
    }else{
      System.out.println("No customer IDs available");
    }
  }

Here is a list of the title, label, button, number, and error text visible to the user, and therefore, subject to internationalization and localization. This data was taken from both RMIClient1.java and RMIClient2.java.

  • Labels: Apples, Peaches, Pears, Total Items, Total Cost, Credit Card, Customer ID
  • Titles: Fruit $1.25 Each, Select Items, Specify Quantity
  • Buttons: Reset, View, Purchase
  • Number Values: Value for total items, Value for total cost
  • Errors: Invalid Value, Cannot send data to server, Cannot look up remote server object, No data available, No customer IDs available, Cannot access data in server

Create Keyword and Value Pair Files

 

Because all text visible to the user will be moved out of the application and translated, your application needs a way to access the translated text during execution. This is done with keyword and value pair files, where this is a file for each language. The keywords are referenced from the application instead of the hard-coded text and used to load the appropriate text from the file for the language in use.

For example, you can map the keyword purchase to Kaufen in the German file, Achetez in the French file, and Purchase in the United States English file. In your application, you reference the keyword purchase and indicate the language to use.

Keyword and value pairs are stored in files called properties files because they store information about the programs properties or characteristics. Property files are plain-text format, and you need one file for each language you intend to use.

In this example, there are three properties files, one each for the English, French, and German translations. Because this application currently uses hard-coded English text, the easiest way to begin the internationalization process is to use the hard-coded text to set up the key and value pairs for the English properties file.

The properties files follow a naming convention so the application can locate and load the correct file at run time. The naming convention uses language and country codes which you should make part of the file name. The language and country are both included because the same language can vary between countries. For example, United States English and Australian English are a little different, and Swiss German and Austrian German both differ from each other and from the German spoken in Germany.

These are the names of the properties files for the German ( de_DE), French ( fr_FR), and American English ( en_US) translations where de, fr, and en indicate the German (Deutsche), French, and English lanuages; and DE, FR, and US indicate Germany (Deutschland), France, and the United States:

  • MessagesBundle_de_DE.properties
  • MessagesBundle_en_US.properties
  • MessagesBundle_fr_FR.properties

Here is the English language properties file. Keywords appear to the left of the equals (=) sign, and text values appear to the right.

MessagesBundle_en_US.properties

apples=Apples:
peaches=Peaches:
pears=Pears:
items=Total Items:
cost=Total Cost:
card=Credit Card:
customer=Customer ID:

title=Fruit 1.25 Each
1col=Select Items
2col=Specify Quantity

reset=Reset
view=View
purchase=Purchase

invalid=Invalid Value
send=Cannot send data to server
nolookup=Cannot look up remote server object

nodata=No data available
noID=No customer IDs available
noserver=Cannot access data in server

With this file complete, you can hand it off to your French and German translators and ask them to provide the French and German equivalents for the text to the right of th equals (=) sign. Keep a copy for yourself because you will need the keywords to internationalize your application text.

The properites file with the German translations produces this user interface for the fruit order client:

The properties file with the French translations produces this user interface for the fruit order client:

Internationalize Application Text

This section walks through internationalizing the RMIClient1.java program. The RMIClient2.java code is almost identical so you can apply the same steps to that program on your own.

Instance Variables

In addition to adding an import statement for the java.util.* package where the internationalization classes are, this program needs the following instance variable declarations for the internationalization process:

//Initialized in main method
  static String language, country;
  Locale currentLocale;
  static ResourceBundle messages;

//Initialized in actionPerformed method
  NumberFormat numFormat;

main Method

The program is designed so the user specifies the language to use at the command line. So, the first change to the main method is to add the code to check the command line parameters. Specifying the language at the command line means once the application is internationalized, you can easily change the language without any recompilation.

The String[] args parameter to the main method contains arguments passed to the program from the command line. This code expects 3 command line arguments when the end user wants a language other than English. The first argument is the name of the machine on which the program is running. This value is passed to the program when it starts and is needed because this is a networked program using the Remote Method Invocation (RMI) API.

The other two arguments specify the language and country codes. If the program is invoked with 1 command line argument (the machine name only), the country and language are assumed to be United States English.

As an example, here is how the program is started with command line arguments to specify the machine name and German language (de DE). Everything goes on one line.

  java -Djava.rmi.server.codebase=
        http://kq6py/~zelda/classes/
        -Djava.security.policy=java.policy 
        RMIClient1 kq6py.eng.sun.com de DE 

And here is the main method code. The currentLocale instance variable is initialized from the language and country information passed in at the command line, and the messages instance variable is initialized from the currentLocale.

The messages object provides access to the translated text for the language in use. It takes two parameters: the first parameter "MessagesBundle" is the prefix of the family of translation files this aplication uses, and the second parameter is the Locale object that tells the ResourceBundle which translation to use.

 

Note: This style of programming makes it possible for the same user to run the program in different languages, but in most cases, the program will use one language and not rely on command-line arguments to set the country and language.

If the application is invoked with de DE command line parameters, this code creates a ResourceBundle variable to access the MessagesBundle_de_DE.properties file.

  public static void main(String[] args){
//Check for language and country codes
    if(args.length != 3) {
      language = new String("en");
      country = new String ("US");
      System.out.println("English");
    }else{
      language = new String(args[1]);
      country = new String(args[2]);
      System.out.println(language + country);
    }

//Create locale and resource bundle
    currentLocale = new Locale(language, country);
    messages = ResourceBundle.getBundle("MessagesBundle", 
        currentLocale);

    WindowListener l = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    };

//Create the RMIClient1 object
    RMIClient1 frame = new RMIClient1();

    frame.addWindowListener(l);
    frame.pack();
    frame.setVisible(true);

    if(System.getSecurityManager() == null) {
      System.setSecurityManager(
               new RMISecurityManager());
    }

    try {
      String name = "//" + args[0] + "/Send";
      send = ((Send) Naming.lookup(name));
    } catch (java.rmi.NotBoundException e) {
      System.out.println(messages.getString(
                                    "nolookup"));
    } catch(java.rmi.RemoteException e){
      System.out.println(messages.getString(
                                    "nolookup"));
    } catch(java.net.MalformedURLException e) {
      System.out.println(messages.getString(
                                    "nolookup"));
    }
  }

getStringResourceBundle
     try {
       String name = "//" + args[0] + "/Send";
       send = ((Send) Naming.lookup(name));
     } catch (java.rmi.NotBoundException e) {
       System.out.println(messages.getString(
                                     "nolookup"));
     } catch(java.rmi.RemoteException e){
       System.out.println(messages.getString(
                                     "nolookup"));
     } catch(java.net.MalformedURLException e) {
       System.out.println(messages.getString(
                                     "nolookup"));
     }

Constructor

 

The window title is set by calling the getString method on the ResourceBundle, and passing it the keyword that maps to the title text. You must pass the keyword exactly as it appears in the translation file, or you will get a runtime error indicating the resource is unavailable.

   RMIClient1(){ 

//Set window title
     setTitle(messages.getString("title"));
argscatchResourceBundleJLabelJButton
//Create left and right column labels
     col1 = new JLabel(messages.getString("1col"));
     col2 = new JLabel(messages.getString("2col"));
...
//Create buttons and make action listeners
     purchase = new JButton(messages.getString(
                                       "purchase"));
     purchase.addActionListener(this);

     reset = new JButton(messages.getString("reset"));
     reset.addActionListener(this);

actionPerformed Method

In the actionPerformed method, the Invalid Value error is caught and translated:

  if(order.apples.length() > 0){
//Catch invalid number error
    try{
      applesNo = Integer.valueOf(order.apples);
      order.itotal += applesNo.intValue();
    }catch(java.lang.NumberFormatException e){
      appleqnt.setText(messages.getString("invalid"));
    }
  } else {
    order.itotal += 0;
  }

actionPerformed

Internationalize Numbers

A NumberFormat object is used to translate numbers to the correct format for the language currently in use. To do this, a NumberFormat object is created from the currentLocale. The information in the currentLocale tells the NumberFormat object what number format to use.

Once you have a NumberFormat object, all you do is pass in the value you want translated, and you receive a String that contains the number in the correct format. The value can be passed in as any data type used for numbers such as int, Integer, double, or Double. No code such as to convert an Integer to an int and back again is needed.

//Create number formatter
  numFormat = NumberFormat.getNumberInstance(
        currentLocale);

//Display running total
  text = numFormat.format(order.itotal);
  this.items.setText(text);

//Calculate and display running cost
  order.icost = (order.itotal * 1.25);
  text2 = numFormat.format(order.icost);
  this.cost.setText(text2);

  try{
     send.sendOrder(order);
   } catch (java.rmi.RemoteException e) {
     System.out.println(messages.getString("send"));
   }

Compile and Run the Application

Here are the summarized steps for compiling and running the example program. The important thing to note is that when you start the client programs, you need to include language and country codes if you want a language other than United States English.

Compile

These instructions assume development is in the zelda home directory.

                    Unix:
cd /home/zelda/classes
javac Send.java
javac RemoteServer.java
javac RMIClient2.java
javac RMIClient1.java
rmic -d . RemoteServer
cp RemoteServer*.class /home/zelda/public_html/classes
cp Send.class /home/zelda/public_html/classes
cp DataOrder.class /home/zelda/public_html/classes


                    Win32:
cd \home\zelda\classes
javac Send.java
javac RemoteServer.java
javac RMIClient2.java
javac RMIClient1.java
rmic -d . RemoteServer
copy RemoteServer*.class 
                \home\zelda\public_html\classes
copy Send.class \home\zelda\public_html\classes
copy DataOrder.class \home\zelda\public_html\classes
                

Start rmi Registry

 

Unix:

cd /home/zelda/public_html/classes
unsetenv CLASSPATH
rmiregistry &

Win32:

cd \home\zelda\public_html\classes
set CLASSPATH=
start rmiregistry 

Start the Server

 

Unix:

  cd /home/zelda/public_html/classes
  java -Djava.rmi.server.codebase=
        http://kq6py/~zelda/classes
  -Dtava.rmi.server.hostname=kq6py.eng.sun.com
  -Djava.security.policy=java.policy RemoteServer


                    Win32:
  cd \home\zelda\public_html\classes
  java -Djava.rmi.server.codebase=
        file:c:\home\zelda\public_html\classes
  -Djava.rmi.server.hostname=kq6py.eng.sun.com
  -Djava.security.policy=java.policy RemoteServer
                

Start RMIClient1 in German

Note the addition of de DE for the German language and country at the end of the line.

                    Unix:
  cd /home/zelda/classes

  java -Djava.rmi.server.codebase=
        http://kq6py/~zelda/classes/
  -Djava.security.policy=java.policy 
        RMIClient1 kq6py.eng.sun.com de DE


                    Win32:
cd \home\zelda\classes

  java -Djava.rmi.server.codebase=
        file:c:\home\zelda\classes\
  -Djava.security.policy=java.policy RMIClient1 
        kq6py.eng.sun.com de DE
                

Start RMIClient2 in French

Note the addition of fr FR for the French language and country at the end of the line.

                    Unix:
  cd /home/zelda/classes

  java -Djava.rmi.server.codebase=
        http://kq6py/~zelda/classes
  -Djava.rmi.server.hostname=kq6py.eng.sun.com
  -Djava.security.policy=java.policy 
        RMIClient2 kq6py.eng.sun.com fr FR


                    Win32:
  cd \home\zelda\classes

  java -Djava.rmi.server.codebase=
        file:c:\home\zelda\public_html\classes
  -Djava.rmi.server.hostname=kq6py.eng.sun.com
  -Djava.security.policy=java.policy RMIClient2 
        kq6py.eng.sun.com/home/zelda/public_html fr FR
                

Program Improvements

A real-world scenario for an ordering application like this might be that RMIClient1 is an applet embedded in a web page. When orders are submitted, order processing staff run RMIClient2 as applications from their local machines.

So, an interesting exercise is to convert RMIClient1.java to its applet equivalent. The translation files would be loaded by the applet from the same directory from which the browser loads the applet class.

One way is to have a separate applet for each language with the language and country codes hard coded. Your web page can let them choose the language by clicking a link that launches the appropriate applet. Here are the source code files for the English, French, and German applets.

Here is the HTML code to load the French applet on a Web page.

<HTML>
<BODY>
<APPLET CODE=RMIFrenchApp.class WIDTH=300 HEIGHT=300>
</APPLET>
</BODY>
</HTML>

Note: To run an applet written with Java 2 APIs in a browser, the browser must be enabled for the Java 2 Platform. If your browser is not enabled for the Java 2 Platform, you have to use appletviewer to run the applet or install Java Plug-in. Java Plug-in lets you run applets on web pages under the 1.2 version of the Java 1 virtual machine (VM) instead of the web browser's default Java VM.
rmiFrench.htmlHTML
  appletviewer rmiFrench.html

Another improvement to the program as it currently stands would be enhancing the error message text. You can locate the errors in the Java API docs and use the information there to make the error message text more user friendly by providing more specific information.

You might also want to adapt the client programs to catch and handle the error thrown when an incorrect keyword is used. Here are the error and stack trace provided by the system when this type of error occurs:

Exception in thread "main" 
  java.util.MissingResourceException: 
Can't find resource
  at java.util.ResourceBundle.getObject(Compiled Code)
  at java.util.ResourceBundle.getString(Compiled Code)
  at RMIClient1.<init>(Compiled Code)
  at RMIClient1.main(Compiled Code)

More Information

You can find more information on Internationalization in the Internationalization trail in The Java Tutorial.

You can find more informationon applets in the Writing Applets trail in The Java Tutorial.

_______
1 As used on this web site, the terms "Java virtual machine" or "JVM" mean a virtual machine for the Java platform

[ TOP]

Left Curve
Java SDKs and Tools
Right Curve
Left Curve
Java Resources
Right Curve
JavaOne Banner Java 8 banner (182)