11.4 Internationalization and Localization: `Locale`, `ResourceBundle`, and Formatters
Key Takeaways
Locale combines a lowercase language code (ISO 639) with an optional uppercase country code (ISO 3166); Java 17 code builds locales with constants such as Locale.US, new Locale("fr", "CA"), Locale.forLanguageTag(), or Locale.Builder, because Locale.of() arrived only in Java 19.
ResourceBundle loading follows a strict deterministic fallback order: requested language + country, requested language, default locale language + country, default language, and finally the base bundle.
Class-based ResourceBundles take precedence over property-based bundles with identical names and can store arbitrary non-String Java objects.
Individual key lookups within a matched ResourceBundle traverse only that bundle's parent hierarchy to the base bundle; key lookups never fall back to the system default locale bundle.
DateTimeFormatter is immutable and thread-safe; attempting to format a temporal type missing required fields (such as formatting a LocalDate with a time formatter) throws an UnsupportedTemporalTypeException.
11.4 Internationalization and Localization: Locale, ResourceBundle, and Formatters
Internationalization (I18N) is the process of designing software so that it can adapt to various languages and regions without engineering changes. Localization (L10N) is the adaptation of that software for a specific geographic or cultural market by translating text and formatting numbers, currencies, dates, and times according to regional conventions. Java SE 17 provides comprehensive localization support through java.util.Locale, java.util.ResourceBundle, java.text.NumberFormat, and java.time.format.DateTimeFormatter.
The Locale Class and Construction
A java.util.Locale object represents a specific geographic, political, or cultural region. It is composed of:
- Language Code: Mandatory, 2 or 3 lowercase letters conforming to ISO 639 (e.g.,
enfor English,frfor French,defor German,jafor Japanese). - Country / Region Code: Optional, 2 uppercase letters conforming to ISO 3166 or 3 digits conforming to UN M.49 (e.g.,
USfor United States,CAfor Canada,DEfor Germany,GBfor Great Britain). - Variant / Script: Optional additional markers (e.g., script
Latnor variantPOSIX).
Instantiation Approaches in Java SE 17
// 1. Static Constants (Predefined for common languages and nations)
Locale loc1 = Locale.US; // Language: en, Country: US
Locale loc2 = Locale.GERMAN; // Language: de (no country)
Locale loc3 = Locale.GERMANY; // Language: de, Country: DE
// 2. Public constructors (the standard approach in Java 17)
Locale loc4 = new Locale("fr", "CA"); // Language: fr, Country: CA
Locale loc5 = new Locale("es"); // Language: es
// 3. IETF BCP 47 Language Tag
Locale loc6 = Locale.forLanguageTag("zh-CN");
// 4. Locale.Builder (Fluent configuration)
Locale loc7 = new Locale.Builder()
.setLanguage("en")
.setRegion("GB")
.build();
Note
In Java 17 the public constructors such as new Locale("en", "US") are the normal, non-deprecated way to build a locale, and Locale.of(...) does not exist (javac 17 reports cannot find symbol). Java 19 later added Locale.of(...) and deprecated the constructors, so newer tutorials look different; answer 1Z0-829 questions using the Java 17 API.
Inspecting and Setting Defaults
The JVM maintains default locales for the active host environment. Java divides the default locale into two categories:
Locale.Category.FORMAT: Governs formatting of numbers, currencies, and dates.Locale.Category.DISPLAY: Governs localized UI names for languages, countries, and UI strings.
Locale defaultLocale = Locale.getDefault();
Locale formatLocale = Locale.getDefault(Locale.Category.FORMAT);
// Updating defaults
Locale.setDefault(new Locale("fr", "FR"));
ResourceBundle Architecture and File Formats
A ResourceBundle manages locale-specific resources (such as translated text messages). Java supports two storage formats for resource bundles:
- Property Resource Bundles (
.properties): Standard flat text files containingkey=valuepairs. Values are strictlyStringobjects. - Class Resource Bundles (
.java/.class): Concrete classes extendingjava.util.ListResourceBundle. They define agetContents()method returningObject[][].
Advantages of Class Bundles
- Can contain any Java object type (such as
Color,Icon, orbyte[]), whereas.propertiesfiles are restricted entirely to strings. - Values can be computed dynamically at runtime.
- Precedence Rule: If both a
.classbundle and a.propertiesbundle exist with the identical bundle name for a given locale, the.classbundle always takes precedence over the.propertiesbundle!
// Example Class-Based Bundle: Messages_en.java
public class Messages_en extends ListResourceBundle {
@Override
protected Object[][] getContents() {
return new Object[][] {
{ "welcome", "Welcome to our application!" },
{ "maxItems", 500 } // Can store non-String objects!
};
}
}
ResourceBundle Resolution Hierarchy and Fallback Rules
When ResourceBundle.getBundle(baseName, targetLocale) is called, the JVM performs a deterministic search across the classpath to locate the best matching bundle.
The Search Order
Assume the application requests bundle base name AppLabels for new Locale("fr", "CA") (French Canadian), and the system default locale is new Locale("en", "US"):
AppLabels_fr_CA(Target language + target country)AppLabels_fr(Target language only)AppLabels_en_US(Default locale language + default country)AppLabels_en(Default locale language only)AppLabels(Base bundle with no locale suffix)- If none of these files exist on the classpath, Java throws
java.util.MissingResourceException.
At each step, Java looks for a .class file first, followed by a .properties file.
Key Fallback Mechanics (The Critical Exam Trap)
Once a bundle file is matched, Java selects that bundle family. When you query a specific key using bundle.getString(key), Java searches that bundle. If the key is missing:
- The lookup searches up the inheritance hierarchy of that matched bundle:
fr_CAfrbaseAppLabels. - It NEVER falls back to the default locale bundle (
en_USoren)!
Matched Bundle Family:
AppLabels_fr_CA ---> AppLabels_fr ---> AppLabels (Base)
|
[MissingResourceException]
(Never crosses over to AppLabels_en_US!)
Warning
If AppLabels_fr.properties is found, the system default locale AppLabels_en.properties is completely ignored for individual key lookups. If a key exists in AppLabels_en but is absent in both AppLabels_fr and base AppLabels, bundle.getString(key) throws MissingResourceException!
Formatting and Parsing Numbers and Currencies: NumberFormat
The java.text.NumberFormat abstract class formats and parses numbers, percentages, and currencies according to regional conventions.
Factory Methods
Locale us = Locale.US;
Locale germany = Locale.GERMANY;
NumberFormat nfUs = NumberFormat.getInstance(us);
NumberFormat nfDe = NumberFormat.getInstance(germany);
System.out.println(nfUs.format(123456.78)); // 123,456.78 (comma thousands, dot decimal)
System.out.println(nfDe.format(123456.78)); // 123.456,78 (dot thousands, comma decimal)
// Currency Formatting
NumberFormat curUs = NumberFormat.getCurrencyInstance(us);
NumberFormat curDe = NumberFormat.getCurrencyInstance(germany);
System.out.println(curUs.format(49.99)); // $49.99
System.out.println(curDe.format(49.99)); // 49,99 € (comma decimal separator, symbol after a space)
Parsing Behavior and Checked Exceptions
The parse(String) method parses text into a java.lang.Number (Long or Double). It throws a checked java.text.ParseException if parsing fails:
- Prefix Matching:
NumberFormat.parse()begins reading from the start of the string. As soon as it encounters a character that is invalid in the given locale, it stops parsing and returns the number accumulated so far! Trailing unparsed characters are simply ignored. - Invalid Initial Character: If the very first non-whitespace characters cannot be parsed into a number,
parse()throwsParseException.
NumberFormat nf = NumberFormat.getInstance(Locale.US);
Number val1 = nf.parse("45.67xyz"); // Successfully returns 45.67 (stops at 'x')
Number val2 = nf.parse("1,000 items"); // Successfully returns 1000 (stops at ' ')
// nf.parse("x45.67"); // THROWS java.text.ParseException: Unparseable number: "x45.67"
Formatting and Parsing Dates: DateTimeFormatter
The modern java.time.format.DateTimeFormatter replaces legacy SimpleDateFormat. Unlike its predecessor, DateTimeFormatter is immutable and thread-safe.
Predefined and Localized Styles
LocalDateTime ldt = LocalDateTime.of(2026, Month.APRIL, 15, 14, 30);
// Standard localized formatters using FormatStyle
DateTimeFormatter shortDate = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
DateTimeFormatter mediumTime = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM);
DateTimeFormatter fullDateTime = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT);
System.out.println(shortDate.format(ldt)); // e.g., 4/15/26 in US
The UnsupportedTemporalTypeException in Formatting
A DateTimeFormatter configured for dates cannot format a time-only object, and vice versa. Mismatches trigger runtime exceptions:
LocalDate date = LocalDate.of(2026, Month.APRIL, 15);
DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
// COMPILES! But throws UnsupportedTemporalTypeException at runtime:
// java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: ClockHourOfAmPm (in a US locale, whose SHORT time pattern is h:mm a)
String output = timeFormatter.format(date);
Custom Patterns: ofPattern(String)
Custom patterns use case-sensitive symbols:
| Symbol | Meaning | Example for April 15, 2026, 14:30:05 |
|---|---|---|
yyyy / uuuu | 4-digit Year | 2026 |
MM | 2-digit Month (01-12) | 04 |
MMM / MMMM | Short / Full Month Name | Apr / April |
dd | 2-digit Day of Month (01-31) | 15 |
HH | 24-hour Hour of Day (00-23) | 14 |
hh | 12-hour Clock Hour (01-12) | 02 |
mm | Minute of Hour (00-59) | 30 |
ss | Second of Minute (00-59) | 05 |
a | AM / PM Marker | PM |
Warning
Watch out for case sensitivity in custom patterns! MM represents Month, while mm represents Minute. HH represents 24-hour hour, while hh represents 12-hour hour. dd is day of month, whereas DD is day of year!
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");
String formatted = dtf.format(ldt); // "15/04/2026 14:30"
// Parsing strings into temporal objects:
LocalDate parsedDate = LocalDate.parse("15/04/2026", DateTimeFormatter.ofPattern("dd/MM/yyyy"));
// Note: LocalDate.parse() throws unchecked DateTimeParseException if parsing fails!
Suppose the following property files exist on the classpath:
App_fr.propertiescontainswelcome=BienvenueApp_en.propertiescontainswelcome=Welcomeandfarewell=GoodbyeApp.propertiescontainsapp_title=System
The JVM default locale is set to Locale.US. Consider the following code:
Locale fr = new Locale("fr", "FR");
ResourceBundle rb = ResourceBundle.getBundle("App", fr);
System.out.println(rb.getString("welcome"));
System.out.println(rb.getString("farewell"));
What is the outcome when this code executes?
Outputs Bienvenue followed by Goodbye
Throws MissingResourceException immediately when loading the bundle App
Outputs Welcome followed by Goodbye because App_en matches the default locale
Outputs Bienvenue and then throws MissingResourceException on farewell
Consider the following code snippet:
LocalDate date = LocalDate.of(2026, Month.OCTOBER, 11);
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
System.out.println(formatter.format(date));
What is the result of attempting to compile and run this code?
Throws an UnsupportedTemporalTypeException at runtime
Compilation fails because format() requires a TemporalAccessor with both date and time
Outputs 12:00 AM
Outputs 00:00 AM
Consider the following code snippet:
NumberFormat nf = NumberFormat.getInstance(Locale.US);
Number n1 = nf.parse("123.45XYZ");
System.out.println(n1);
Assuming the surrounding method properly handles or declares any checked exceptions, what is printed when this code executes?
Outputs 123.45
Outputs 123
Throws a ParseException at runtime because XYZ is invalid
Compilation fails because parse() returns a primitive double
Sections you finish are checked off in the contents.