Basics of internationalization in Java

Alonso Del Arte
6 min readJan 18, 2025

--

Photo by Rocker Sta on Unsplash

Java is supposed to be “write once, run anywhere.” You can write and compile a Java program on, for example, a MacBook, then take that compiled program to a Dell and have it run without re-compiling, like you would have to do with a C++ program.

Whether or not the program will be usable by someone else somewhere else in the world is a separate issue. If all of the program’s user-facing text components are in English, people who don’t know English might not be able to use the program.

It would be nice if there was some mechanism by which the user-facing text could automatically be changed to a different language based on the user’s locale. Java provides such a mechanism.

You know the Java philosophy: why write just one class when you can write twenty classes and three interfaces?

That’s why so many Java programmers use indiscriminate wildcard imports. Like, if you’re doing something with Java Swing, maybe you prefer to do a wildcard import for the whole javax.swing package rather than the dozen specific classes you actually need for your project.

We can get a lot done on internationalization importing just two classes from the Java Development Kit: Locale and ResourceBundle, both from the java.util package.

I’m not actually going to use Java Swing to demonstrate these, just a simple console Hello World.

But the basic idea is pretty much the same: instead of “hard-coding” a bit of text for a button or for a console prompt, our program has a clearinghouse object that retrieves the appropriate text for the locale, or a default text if locale-specific text is not available.

Normally, the purpose of Hello World is simply to check that you can compile and run a program. The message doesn’t have to be “Hello, world!” It can be any other text that is not an error message.

For this tutorial, though, we want a Hello World that gives a greeting in as many languages as we can think to provide for, but defaults to English if the user is in a locale for which we have not provided a translation.

Here’s just one way we could go about that. This program only uses Locale, we won’t use ResourceBundle just yet. In order to demonstrate the concept, the program tries selecting a locale based on the command line parameters, but otherwise uses the default locale.

package org.example;

import java.util.Locale;

public class HelloWorld {

static String greeting(Locale locale) {
String langTag = locale.toLanguageTag().substring(0, 2);
return switch (langTag) {
case "de" -> "Hallo Welt!";
case "es" -> "¡Hola, mundo!";
case "fr" -> "Bonjour le monde!";
case "ja" -> "こんにちは世界!";
default -> "Hello, world!";
};
}

public static void main(String[] args) {
Locale locale = Locale.getDefault();
if (args.length > 0) {
Locale propLocale = Locale.forLanguageTag(args[0]);
if (!propLocale.toLanguageTag().equals("und")) {
locale = propLocale;
}
}
System.out.println(greeting(locale));
}

}

Run this without any command line parameters. Then try running it with parameters like “fr-CA” (Canadian French).

al@MacBook-Pro-167 BasicExercises % java -cp . org.example.HelloWorld ja-JP
こんにちは世界!
al@MacBook-Pro-167 BasicExercises % java -cp . org.example.HelloWorld es-US
¡Hola, mundo!

Problem with this approach is that if we want to include more languages, we would have to edit the Java source file. A translator who doesn’t know Java might be hesitant to edit the Java source file. But he or she might be more willing to edit a file with a much simpler format.

This is where resource bundles come in. A resource bundle is a group of files with a common base name that we specify, optional predefined modifiers and the *.properties file extension.

We create these files somewhere in Source Packages and they should become available at runtime through the ResourceBundle class.

I decided to make the subpackage i18n (short for “internationalization”) in the org.example package. In the src/org/example/i18n folder, make a file called Messages.properties. This will contain the defaults.

greeting = Hello, world!
timeNotice = The time is

In a real world program, we would need a lot more messages than these. In this tutorial, we really only need the greeting.

Now, in the same package, make a file called Messages_es.properties. This will contain the Spanish messages.

greeting = ¡Hola, mundo!
timeNotice = La hora es

We can get more specific than this. We could do, for example, one for Mexican Spanish, another one for Puerto Rican Spanish, etc. I read somewhere that we can also specify platform (e.g., Windows, macOS, etc.), but I have not been able to corroborate this yet.

We need not limit ourselves to a single base name. We could have one bundle for menu headers with the default file being MenuHeaders.properties, another one for button labels with the default file being ButtonLabels.properties, etc.

So we could have, for example with the Messages resource bundle,

  • Messages.properties
  • Messages_fr.properties
  • Messages_fr_CA.properties
  • Messages_fr_CA_UNIX.properties

Theoretically, then, on a UNIX system in Quebec, the Java runtime would look for Messages_fr_CA_UNIX.properties. If that’s not available, it would then look for Messages_fr_CA.properties. If that’s not available either, it would look for Messages_fr.properties.

And if that’s not available either, it would either load Messages.properties or throw a MissingResourceException.

These files can include comments, but the comment indicator must start the line.

# This is a proper comment
greeting = Hello, world! # This is not a proper comment

The text “This is a proper comment” won’t show up in any user-facing text for this program, but the text “This is not a proper comment” will show up whenever the greeting is retrieved from this *.properties file. I have verified this when compiled for Java 8 and run on a Java 21 runtime.

Once you have *.properties files for all the languages you want to include, edit HelloWorld.greeting() thus:

    static String greeting(Locale locale) {
ResourceBundle bundle = ResourceBundle
.getBundle("org/example/i18n/Messages", locale);
return bundle.getString("greeting");
}

Note that the path to the *.properties files needs to be specified from the level below src regardless of where those are at in relation to the Java source file in which they’re referenced.

To add more languages, we simply add more Messages_*.properties files in src/org/example/i18n. Adding more languages does not require us to edit HelloWorld.java any further.

Your integrated development environment (IDE) should copy these *.properties files to the corresponding build folder (such as build/classes for NetBeans projects). These files should also be compressed into a JAR when applicable.

As long as we have provided a default *.properties file with the specified name and the specified key, we can guarantee that ResourceBundle will always at least provide a default message for every locale.

There is one more wrinkle we need to be aware of even at this elementary level. If the language we’re providing translations for uses characters beyond those in Unicode Basic Latin and the Latin 1 Supplement, the text could become garbled.

For example, given Messages_zh.properties,

greeting = 你好世界!
timeNotice = 時間是

our Hello World program might give us

C:\Users\AL\NetBeansProjects\Examples>
java -cp build/classes org.example.HelloWorld zh-TW
?????

This is actually not the best demonstration, since the installation of the Windows 10 command prompt that I’m using simply lacks the necessary characters to display Chinese text.

The macOS Terminal does have access to the necessary characters, but they might get lost in a trip through ResourceBundle’s processing anyway.

I read somewhere that ResourceBundle in Java 8 and earlier was limited to ISO 8859–1. But I have been able to reproduce this problem where I did not expect it (Java 21 runtime in the macOS Terminal) and not been able to reproduce it where I expected it (NetBeans 11.2 Output pane using Java 8).

For languages like Chinese and Japanese we would have to use Unicode escape sequences to ensure the text does not get garbled. It would look something like this:

greeting = \u4F60\u597D\u4E16\u754C\uFF01
timeNotice = 時間是

It’s quite a nuisance for just one line, imagine having to do it for a full Java Swing application with four or five menus and a couple of submenus in each menu.

Fortunately, the Java Development Kit provides us with a little tool to take care of this problem, the Native2ASCII program. For this next demonstration, bash and zsh users should know that the Type command in DOS is equivalent to the Cat command.

C:\Users\AL\Documents\NetBeansProjects\Examples\src\org\example\i18n>
type Messages_zh.properties
greeting = 你好世界!
timeNotice = µÖéΘûôµÿ»

C:\Users\AL\Documents\NetBeansProjects\Examples\src\org\example\i18n>
native2ascii -encoding UTF-8 Messages_zh.properties
Messages_zh_CHANGED_OVER.properties

C:\Users\AL\Documents\NetBeansProjects\Examples\src\org\example\i18n>
type Messages_zh_CHANGED_OVER.properties
greeting = \u4f60\u597d\u4e16\u754c\uff01
timeNotice = \u6642\u9593\u662f

Having verified the conversion happened correctly, I would probably move the original file to a folder outside version control, and then rename the converted file.

Actually, it turns out NetBeans 11.2 automatically takes care of this while still displaying human-readable characters to you, but IntelliJ IDEA Community Edition doesn’t. Regardless, it’s always a good idea to check that all of your program’s user-facing text displays as you expect.

--

--

Alonso Del Arte
Alonso Del Arte

Written by Alonso Del Arte

is a Java and Scala developer from Detroit, Michigan. AWS Cloud Practitioner Foundational certified

No responses yet