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.

Last updated: October 2026

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:

  1. Language Code: Mandatory, 2 or 3 lowercase letters conforming to ISO 639 (e.g., en for English, fr for French, de for German, ja for Japanese).
  2. Country / Region Code: Optional, 2 uppercase letters conforming to ISO 3166 or 3 digits conforming to UN M.49 (e.g., US for United States, CA for Canada, DE for Germany, GB for Great Britain).
  3. Variant / Script: Optional additional markers (e.g., script Latn or variant POSIX).

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:

  1. Property Resource Bundles (.properties): Standard flat text files containing key=value pairs. Values are strictly String objects.
  2. Class Resource Bundles (.java / .class): Concrete classes extending java.util.ListResourceBundle. They define a getContents() method returning Object[][].

Advantages of Class Bundles

  • Can contain any Java object type (such as Color, Icon, or byte[]), whereas .properties files are restricted entirely to strings.
  • Values can be computed dynamically at runtime.
  • Precedence Rule: If both a .class bundle and a .properties bundle exist with the identical bundle name for a given locale, the .class bundle always takes precedence over the .properties bundle!
// 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"):

  1. AppLabels_fr_CA (Target language + target country)
  2. AppLabels_fr (Target language only)
  3. AppLabels_en_US (Default locale language + default country)
  4. AppLabels_en (Default locale language only)
  5. AppLabels (Base bundle with no locale suffix)
  6. 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_CA →\rightarrow fr →\rightarrow base AppLabels.
  • It NEVER falls back to the default locale bundle (en_US or en)!
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:

  1. 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.
  2. Invalid Initial Character: If the very first non-whitespace characters cannot be parsed into a number, parse() throws ParseException.
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:

SymbolMeaningExample for April 15, 2026, 14:30:05
yyyy / uuuu4-digit Year2026
MM2-digit Month (01-12)04
MMM / MMMMShort / Full Month NameApr / April
dd2-digit Day of Month (01-31)15
HH24-hour Hour of Day (00-23)14
hh12-hour Clock Hour (01-12)02
mmMinute of Hour (00-59)30
ssSecond of Minute (00-59)05
aAM / PM MarkerPM

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!
Test Your Knowledge

Suppose the following property files exist on the classpath:

  • App_fr.properties contains welcome=Bienvenue
  • App_en.properties contains welcome=Welcome and farewell=Goodbye
  • App.properties contains app_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?

A

Outputs Bienvenue followed by Goodbye

B

Throws MissingResourceException immediately when loading the bundle App

C

Outputs Welcome followed by Goodbye because App_en matches the default locale

D

Outputs Bienvenue and then throws MissingResourceException on farewell

Test Your Knowledge

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?

A

Throws an UnsupportedTemporalTypeException at runtime

B

Compilation fails because format() requires a TemporalAccessor with both date and time

C

Outputs 12:00 AM

D

Outputs 00:00 AM

Test Your Knowledge

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?

A

Outputs 123.45

B

Outputs 123

C

Throws a ParseException at runtime because XYZ is invalid

D

Compilation fails because parse() returns a primitive double

Sections you finish are checked off in the contents.