12.1 Locales and Resource Bundles
Key Takeaways
- A Locale pairs a lowercase ISO 639 language code with an optional uppercase ISO 3166 country code; Java 19 deprecated the Locale constructors in favour of Locale.of, Locale.forLanguageTag, and Locale.Builder.
- Locale.getDefault(Category.DISPLAY) and Locale.getDefault(Category.FORMAT) are independent, letting an application show its interface in one language while formatting numbers and dates for another region.
- getBundle tries the requested locale from most specific to least specific, then repeats that narrowing for the default locale, and finally the base name alone before throwing MissingResourceException.
- Once a bundle is selected, missing keys are resolved through that bundle's parent chain down to the base bundle only; default-locale bundles that were merely candidates are never parents.
- getString throws the unchecked MissingResourceException when a key is absent anywhere in the chain and ClassCastException when a ListResourceBundle stores a non-String value.
Locales and Resource Bundles
Implementing Localization is one of the ten top-level topics on the 1Z0-830 exam, and it is the topic candidates most often skip. The whole objective reads: "Implement localization using locales and resource bundles. Parse and format messages, dates, times, and numbers, including currency and percentage values." This section covers the first sentence; section 12.2 covers the second.
Localization in Java rests on two types. A Locale identifies who the user is - a language, and optionally a region. A ResourceBundle supplies what to show them - the translated text keyed by a stable identifier. Neither one translates anything for you; they select and load the right values.
1. Constructing a Locale
A locale is composed of a language code (lowercase, ISO 639, e.g. fr), an optional country/region code (uppercase, ISO 3166, e.g. CA), and an optional variant. The exam expects you to spot a swapped case pattern instantly: Locale.of("FR", "ca") is legal Java but semantically wrong.
Java 19 deprecated the Locale constructors, so Java 21 code should use one of three modern routes:
// 1. Static factory (the direct replacement for the deprecated constructors)
Locale enUS = Locale.of("en", "US");
Locale french = Locale.of("fr");
// 2. BCP 47 language tag - note the HYPHEN, not an underscore
Locale frCA = Locale.forLanguageTag("fr-CA");
System.out.println(frCA.toLanguageTag()); // fr-CA
// 3. Builder, for full BCP 47 control
Locale deDE = new Locale.Builder()
.setLanguage("de")
.setRegion("DE")
.build();
Predefined constants come in two flavours that are easy to confuse:
| Constant | Language | Country | toString() |
|---|---|---|---|
Locale.GERMAN | de | (none) | de |
Locale.GERMANY | de | DE | de_DE |
Locale.FRENCH | fr | (none) | fr |
Locale.CANADA_FRENCH | fr | CA | fr_CA |
Note that toString() uses underscores while toLanguageTag() uses hyphens - the same distinction that separates a resource bundle file name from a language tag.
Default Locales Come in Two Categories
Locale.getDefault(); // convenience: the FORMAT default
Locale.getDefault(Locale.Category.DISPLAY); // menus, labels, display names
Locale.getDefault(Locale.Category.FORMAT); // numbers, currency, dates
Locale.setDefault(Locale.of("es", "MX")); // sets BOTH categories
Splitting the two lets an application show its user interface in one language while formatting numbers and dates according to a different regional convention.
getDisplayLanguage() and getDisplayCountry() are themselves localizable, which is a favourite exam nuance:
Locale target = Locale.of("de", "DE");
System.out.println(target.getDisplayLanguage(Locale.US)); // German
System.out.println(target.getDisplayLanguage(Locale.FRANCE)); // allemand
System.out.println(target.getLanguage()); // de (never localized)
2. The Two Kinds of ResourceBundle
ResourceBundle is an abstract class with two concrete forms in the standard library.
PropertyResourceBundle is created automatically from a .properties file. You never name the class:
# Messages_fr.properties
greeting = Bonjour
farewell = Au revoir
Since Java 9 these files are read as UTF-8 by default (with a fallback to ISO-8859-1 for legacy files), so accented characters no longer need \\uXXXX escapes.
ListResourceBundle is a Java class, which lets values be any object rather than only strings:
public class Messages_ja extends ListResourceBundle {
@Override
protected Object[][] getContents() {
return new Object[][] {
{ "greeting", "こんにちは" },
{ "maxItems", 25 } // an Integer, not a String
};
}
}
Loading and reading them is identical either way:
ResourceBundle rb = ResourceBundle.getBundle("Messages", Locale.of("fr", "CA"));
String greeting = rb.getString("greeting");
getString(key)throwsMissingResourceException(unchecked) when the key is nowhere in the chain, andClassCastExceptionwhen the stored value is not aString.- Use
getObject(key)for non-string values andgetStringArray(key)forString[]. keySet()returns every key visible through the bundle and its parents;containsKey(key)is the safe pre-check.
3. Bundle Selection: The Highest-Yield Rule in This Topic
getBundle(baseName, requestedLocale) builds a list of candidate names from most specific to least specific, and takes the first one that exists. At each candidate name it looks for a class first, then a .properties file.
For base name Messages, requested locale fr_CA, and default locale en_US, the search order is:
1. Messages_fr_CA (requested: language + country)
2. Messages_fr (requested: language only)
3. Messages_en_US (default locale: language + country)
4. Messages_en (default locale: language only)
5. Messages (base bundle, no suffix)
-> otherwise MissingResourceException
Key Lookup Uses the Parent Chain, Not the Search List
Once a bundle is chosen, missing keys are resolved by walking that bundle's parent chain, which strips one locale component at a time down to the base bundle. The default-locale bundles are not in the chain unless the match itself came from the default-locale pass.
Given these files:
| File | Keys |
|---|---|
Messages_fr.properties | greeting |
Messages_en.properties | greeting, hint |
Messages.properties | greeting, farewell |
and a request for fr_CA with default en_US:
ResourceBundle rb = ResourceBundle.getBundle("Messages", Locale.of("fr", "CA"));
rb.getString("greeting"); // from Messages_fr - the selected bundle
rb.getString("farewell"); // from Messages - via the parent chain
rb.getString("hint"); // MissingResourceException - Messages_en is NOT a parent
That last line is the single most commonly tested trap in the localization objective. The en bundle was a candidate during selection, but selection stopped at Messages_fr; the chain is Messages_fr then Messages, full stop.
4. Practical Rules and Module Considerations
- Bundle files live on the classpath (or module path) at the package matching the base name:
getBundle("com.app.Messages", ...)resolvescom/app/Messages_fr.properties. - In a named module, the module that reads the bundle needs access to those resources; either keep the bundles in the same module, or
opensthe package holding them. - Keys must be identical across every translation. A key present only in
Messages_frand absent fromMessageswill fail for every other locale. - Bundles are cached, so repeated
getBundlecalls with the same base name, locale, and loader return the same instance. - Always provide a base bundle with no locale suffix. Without it, any request whose language matches nothing at all fails with
MissingResourceExceptionrather than degrading gracefully.
An application calls ResourceBundle.getBundle("Labels", Locale.of("fr", "CA")) while the JVM default locale is en_US. In what order does getBundle look for candidate bundles?
Only three files exist: Messages_fr.properties (key greeting), Messages_en.properties (keys greeting and hint), and Messages.properties (keys greeting and farewell). With the default locale set to en_US, what happens?
ResourceBundle rb = ResourceBundle.getBundle("Messages", Locale.of("fr", "CA"));
System.out.println(rb.getString("farewell"));
System.out.println(rb.getString("hint"));
Which statement about obtaining a Locale in Java SE 21 is correct?
A ListResourceBundle stores the entry { "maxItems", 25 } where 25 is an Integer. What happens when client code calls bundle.getString("maxItems")?