Localization ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-10-17

Localization. Practical Tasks

You saved title=Bücher in a UTF-8 file messages_de.properties, ran the program on Java 8, and the console shows “Bücher” instead of “Bücher”. Before Java 9, ResourceBundle read properties files as ISO-8859-1, so any non-ASCII text saved in UTF-8 turned into garbage. Since Java 9 (JEP 226) such files are read as UTF-8 by default, but this is only one of the surprises you meet the first time you localize an application.

This page collects 3 Java localization exercises with hints: properties files with texts in two languages, a report whose title changes with the locale, and prices in the currency of the current locale for the online store practice project. You will need the classes Locale, ResourceBundle and NumberFormat; the general theory is in the lesson Internationalization (i18n) and Localization (l10n) in Java.

Java Topics in Two Languages (ResourceBundle)

  1. Create properties files in two languages, for example English and French, containing the names of the Java topics you have studied.
  2. Write a method that takes a locale as a parameter and prints all the information from the properties file.

Hint

To go through all the keys, use ResourceBundle.keySet(); the value for a key is returned by getString(key). Put the files on the classpath, for example src/main/resources/topics_en.properties and topics_fr.properties, and load them with ResourceBundle.getBundle("topics", locale).

FullReport Class: a Report Titled in the Locale Language

Employee Report · part 2 of 2 · ← part 1

  1. Create a FullReport class that prints the same information as the Report class (Employee Salary Report on the Java String Exercises page), but the report title must change depending on the selected locale (use the ResourceBundle class).
  2. Use the NumberFormat class to format the numbers.

Where to start

Move the report titles to report_en.properties and report_fr.properties, and format the numbers with NumberFormat.getNumberInstance(locale) or getCurrencyInstance(locale). Pass the locale as a parameter instead of calling Locale.getDefault() inside the method: then the report is easy to check for both locales in one run.

Receipt with Prices in the Current Locale Currency

Online Store · part 4 of 4 · ← part 3

  1. When a customer makes a purchase, print a report of the purchased products to the console. The currency must match the current locale (use the NumberFormat class). For example, for the locale en_US:
    Product        Price
    --------------------
    Jeans      $1,500.78
    Skirt      $1,000.56
    Tie          $500.78
    --------------------
    Total:     $3,002.12
    
  2. The report must be in the language of the current locale — use ResourceBundle.

A note on money

Store prices in BigDecimal rather than double, otherwise the total may differ from the sum of the lines by a cent. For some locales (for example fr_FR) the thousands separator in the NumberFormat output is a non-breaking space, so do not compare the result with a hand-typed string.

Solution on Patreon →

Common Localization Mistakes and How to Fix Them

In localization exercises the program usually breaks not because of the logic but because of the environment: a properties file is not on the classpath, a key is missing from one of the files, or a formatted amount contains a non-breaking space instead of a regular one. The table lists the symptom, the cause and the fix.

Mistake What happens How to fix it
The file is not on the classpath, or the base name is written without the package MissingResourceException: Can't find bundle for base name messages, locale en_US Put the files in src/main/resources (Maven/Gradle); for a file in the i18n folder call getBundle("i18n.messages") (file naming)
A key exists in messages_en.properties but is missing from messages_fr.properties The value is silently taken from the base file messages.properties; if the key is missing there too, you get MissingResourceException: Can't find resource for bundle ..., key ... Keep the same set of keys in all files and use the base file without a suffix as the fallback (missing keys)
The result of format() is compared with a hand-typed string For Locale.FRANCE, "1 500,78 €".equals(...) returns false: the thousands separator is a non-breaking space (U+202F on Java 13+, U+00A0 on older versions), and the space before € is non-breaking too Compare with the result of the same NumberFormat, or turn grouping off: setGroupingUsed(false)
Prices are stored in double 0.1 + 0.2 gives 0.30000000000000004, and the receipt total differs from the sum of its lines by a cent Store amounts in BigDecimal; NumberFormat.format() accepts it as a Number
A locale is created with new Locale("en", "US") Since Java 19 the Locale constructors are marked @Deprecated, and the compiler prints a warning Locale.of("en", "US") (Java 19+) or Locale.forLanguageTag("en-US") (creating a Locale)

What Does This Code Print: the Fallback Locale

If there is no properties file for the requested locale, ResourceBundle.getBundle() first looks for the file of the default locale (Locale.getDefault()) and only then falls back to the base file without a suffix. That is why, on a computer with a French default locale, a “German” report is unexpectedly printed in French rather than in the language of the base file (search order).

// src/main/resources/messages.properties:    title=Report
// src/main/resources/messages_fr.properties: title=Rapport
Locale.setDefault(Locale.of("fr", "FR"));   // before Java 19: new Locale("fr", "FR")

ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.GERMANY);
System.out.println(bundle.getString("title"));
System.out.println(bundle.getLocale());
// Output:
// Rapport
// fr

There is no messages_de_DE.properties file, so the search moves on to the default locale fr_FR and finds messages_fr.properties. To always get the base file when a translation is missing, switch this step off with ResourceBundle.Control:

ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.GERMANY,
        ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT));
System.out.println(bundle.getString("title"));
System.out.println(bundle.getLocale().equals(Locale.ROOT));
// Output:
// Report
// true

The Control variant works in a regular (non-modular) project; calling it from a named module on Java 9+ throws UnsupportedOperationException.

Check your code

Before you compare your work with a solution, go through the code style checklist: meaningful class and method names, no “magic” strings instead of keys from the properties file, and the locale passed as a parameter rather than hard-coded inside a method.

Frequently Asked Questions

Do I need to escape non-ASCII characters in properties files with \uXXXX?

For ResourceBundle on Java 9+ you do not: the files are read as UTF-8, and if a file cannot be decoded as UTF-8 it is read again as ISO-8859-1. Escaping is needed on Java 8 and when you load a file with Properties.load(InputStream), which still uses ISO-8859-1. In the second case pass a Reader instead: properties.load(new InputStreamReader(in, StandardCharsets.UTF_8)).

How do I keep the receipt columns aligned when the texts are translated?

Format the amount into a string first and then pad it as text: System.out.printf("%-12s%14s%n", name, currency.format(price)). Take the column widths from the longest translated label and the longest formatted amount instead of hard-coding them: “Total:” and “Gesamtsumme:” differ in length, and so do $1,500.78 and 1.500,78 €.

How do I print a price in euros if the computer uses an English locale?

Pass a locale with a country explicitly: NumberFormat.getCurrencyInstance(Locale.GERMANY) prints 1.500,78 €. Calling setCurrency(Currency.getInstance("EUR")) on a formatter of an English locale changes only the currency, while the separators and the symbol position stay English: €1,500.78. More in NumberFormat: currency and in the section Common Localization Mistakes and How to Fix Them.

How do I test a program for another locale without changing the OS settings?

The current locale is returned by Locale.getDefault(). You can change it with the JVM options -Duser.language=en -Duser.country=US or with a call to Locale.setDefault(...) at the start of main. The most reliable way is to pass the locale to methods as a parameter: then the report can be checked for both locales in a single run.

Comments

Please log in or register to have a possibility to add comment.