This is a quick tutorial on how to make it easy to support multiple languages in your Java programs.

Internationalization in Java is accomplished through the use of the ResourceBundle object, and most often specifically the PropertyResourceBundle.

A Quick Example

Let's start with the classic of computer science examples:

static public void main(String args[]) {
  System.out.println("Hello World!");
}

The problem here, as you likely know since you're reading this tutorial, is that we've just hard-coded the string "Hello World" into our code, and when we export our program to another country where they don't speak English, we'll have a hard time changing our program.

Instead, we create a "properties" file. Typically this properties file would go inside the jar file, in a subdirectory called "resources".

/resources/MyApplicationStrings.properties
# Strings for our program
helloMessage=Hello World!

The properties file lists a series of keys, and associates them with messages. In this case, we have a single key "helloMessage", associated with the message "Hello World!".

Then in our code:

static public void main(String args[]) {
  Locale locale = new Locale("en");
  ResourceBundle messages = ResourceBundle.getBundle(
    "resources/MyApplicationStrings", locale);
  String helloMessage = messages.getString("helloMessage");
  System.out.println(helloMessage);
}

So, what have we done here? First, we create a Locale object:

  Locale locale = new Locale("en");

A Locale represents information first about the language we display information in (in this case "en" for "English"), and second it can represent information about the country to display the information in. The reason for specifying a language is obvious; we want our program to be displayed in different languages. We also specify a country so that we can make the messages we display specific to the location; the formats of dates and numbers might be different in different countries. Even a language might be different from country to country; for example British and Canadian English have some spelling differences from US English. To specify a Locale of English in the United States, for example, you would use:

  Locale locale = new Locale("en", "US");

We then get the resource bundle specific to the locale we're using. Since we only have one properties file, that's the properties file we'll get. We'll see more about how to create property files in other languages in a moment.

  ResourceBundle messages = ResourceBundle.getBundle(
    "resources/MyApplicationStrings", locale);

Note that we don't pass "resources/MyApplicationStrings.properties" to the getBundle() method. We leave off the ".properties" because we are not passing a single properties file, but the name of the bundle. As we'll see, a bundle may consist of many properties files.

Finally, we get the string which corresponds to the key "helloMessage" out of the resource bundle.

  String helloMessage = messages.getString("helloMessage");

Creating .properties Files for Different Languages

Let's create another properties file:

/resources/MyApplicationStrings_fr.properties
# French properties file.
helloMessage=Bonjour Monde!

Note that this properties file's name ends in "_fr". We now have a two different properties files: a "base" properties file and the French language properties file, both of which are in the "resources/MyApplicationStrings" bundle. We then modify our Java program to use a French locale. Since both properties files are part of the same bundle, the call to getBundle() doesn't change. Only the locale changes:

static public void main(String args[]) {
  Locale locale = new Locale("fr");
  ResourceBundle messages = ResourceBundle.getBundle(
    "resources/MyApplicationStrings", locale);
  String helloMessage = messages.getString("helloMessage");
  System.out.println(helloMessage);
}

Now our program outputs a message in French.

If we specify a locale with a language that doesn't exist, Java will fall back on the base properties file. Note that even if the properties file for a specific language does exist, if a message does not exist in the language specific properties file, then Java will try to find the message in the base properties file. So, for example, if someone adds a new "goodbye=Goodbye!" to the base properties file, but forgets to add it to the French language file, and then tries to use the "goodbye" key in the program with a French local, "Goodbye!" will still be displayed.

Creating .properties Files for Different Countries

Let's create two more properties files:

/resources/MyApplicationStrings_en_US.properties
# US English properties file.
helloMessage=Hello World!
/resources/MyApplicationStrings_en_CA.properties
# Canadian English properties file.
helloMessage=Hello World, eh?

Note that these filenames have not only a language code ("en" for English), but also a country code ("US" for the United States, and "CA" for Canada). Again, these files are still part of the same bundle. Now, if we set our locale to "English, "Canadian":

  Locale locale = new Locale("en", "CA");

we will see a different message than for "English", "United States".

The order in which ResourceBundle's "getBundle()" method looks for property files is somewhat complicated, but if you're interested, have a look at the JavaDoc for the getBundle() method. Essentially, though, getBundle() will first look for a file that matches both the language and the locale. If one cannot be found, it will instead try to find one which matches the language (even if it is in a different locale). Failing that it will look for one which matches the locale (even if it is in a different language). Finally, failing to find country or locale, it will resort to the base bundle.

Messages with Arguments

Not all the messages we display consist of pre-defined static Strings. For example, after a user logs in, we might want to welcome them.

  System.out.println("Hello " + username + "! Welcome to " + programName + "!");

You might be tempted to make the "Hello" and "Welcome to" parts of this string into two different entries in the properties file, but there's a better way. In our properties file, we add a new line:

/resources/MyApplicationStrings.properties
# Strings for our program
helloMessage=Hello World!
welcome=Hello {0}! Welcome to {1}!

The line in the properties file contains special markers which indicate where our arguments should go. The {0} refers to the 0th argument in the argument array. Then, in our code, we construct an array of arguments we want to put into the message, and use a MessageFormat object to do the replacement:

public static void main(String[] args) {
  Locale locale = new Locale("en", "US");
  String username = "Jason";
  String programName = "The I18n Tutorial";

  ResourceBundle messages = ResourceBundle.getBundle(
    "resources/MyApplicationStrings", locale, ClassLoader.getSystemClassLoader());
  String welcomeMessage = messages.getString("welcome");
  Object [] messageArguments = new Object [] {username, programName};
  MessageFormat formatter = new MessageFormat(welcomeMessage, locale);
  String output = formatter.format(messageArguments);
  System.out.println(output);
}

Note that we pass the locale to the MessageFormat constructor. In this particular example, the MessageFormat doesn't care about locale, but by setting the locale, we can get MessageFormat to format dates and numbers in a location specific way. The correct symbol will be chose for currencies, for example.

The format for the markers in the properties file is different for dates and numbers. Here are a few examples:

/resources/DatesAndNumbers.properties
# Display the current date and time. The first argument in the arguments
# array should be an instance of java.util.Date. Note that we're using one
# Date object for both the date and time parts of the message. This
# might display something like:
#
# The time is now 1:43 PM and the date is February 10, 2008.
#
# but remember this might change, depending on the locale.</p>

dateAndTime = The time is now {0,time,short} and the date is {0,date,long}.

# Display a currency in a location specific way. The first argument
# in the arguments array should be an instance of Number, such
# as java.lang.Float. This might display something like "Balance: $20.45".

balance = Balance: {0,number,currency}.

For more information, see the JavaDoc for MessageFormat.

Further Reading

Sun has an excellent trail about internationalization on their web site, which goes into greater detail than this tutorial.