JavaTM Programming Language Basics, Part 2
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 JavaTM 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"));
}
}
The applicable error text is accessed by calling the
getString
method on the ResourceBundle
,
and passing it the keyword that maps to the applicable error text.
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"));
The next thing the constructor does is use the args
parameter to look up the remote server object. If there are any
errors in this process, the catch
statements
get the applicable error text from the ResourceBundle
and print it to the command line.
User interface objects that display text, such as
JLabel
and JButton
, are
created in the same way:
//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;
}
The actionPerformed
method calculates item and
cost totals, translates them to the correct format for
the language currently in use, and displays them in the
user interface.
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 JavaTM 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 Java1 virtual machine (VM) instead
of the web browser's default Java VM.
To use applet viewer, type the following where
rmiFrench.html
is the HTML
file
for the French applet.
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]