Search Tutorials


1Z0-830 Java SE 21 - Localization | JavaInUse

1Z0-830 Localization - Java SE 21 Certification Prep

Localization is the process of adapting a Java application to display correctly for users in different countries and languages. Java provides a rich set of APIs for this: Locale identifies a specific language and region, ResourceBundle externalizes translatable text, NumberFormat formats numbers and currencies, DateTimeFormatter formats dates and times, and Collator compares strings correctly for a given language. The 1Z0-830 exam tests your understanding of all of these APIs and, particularly, the resource bundle lookup chain.

Internationalization (i18n) vs Localization (L10n)

These two terms are related but distinct, and the exam may ask you to distinguish them. Internationalization is the upfront engineering work. Localization is the ongoing translation and adaptation work that happens after.
Key Differences:
  • Internationalization (i18n) - Designing and building software so it can support multiple locales without requiring code changes. Done once by developers. Examples: externalizing strings into resource bundles, using locale-aware formatters, avoiding hardcoded date and number formats.
  • Localization (L10n) - Adapting an internationalized application for a specific locale by providing translations, locale-specific formats, and culturally appropriate content. Done repeatedly by translators and regional teams.
i18n = i + 18 letters + n (internationalization)
L10n = L + 10 letters + n (localization)
// Bad: text, currency symbol, and date format are hardcoded for one locale
System.out.println("Welcome to our app");
System.out.println("Price: $" + price);
System.out.println("Date: " + date);

// Good: all locale-sensitive content is externalized and formatted through APIs
ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
System.out.println(bundle.getString("welcome.message"));

// NumberFormat adapts the decimal separator, grouping separator, and currency symbol
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale);
System.out.println(currencyFormat.format(price));

// DateTimeFormatter adapts the date order, separator, and language of month names
DateTimeFormatter dateFormat = DateTimeFormatter
    .ofLocalizedDate(FormatStyle.MEDIUM)
    .withLocale(locale);
System.out.println(dateFormat.format(date));

Locale Class

A Locale object represents a specific geographical, political, or cultural region. It is not a setting that changes behavior on its own - it is a parameter you pass to locale-sensitive APIs so they know how to format output or load the right translations. A Locale can consist of a language alone, a language plus a country, or a language plus a country plus a variant.
// Predefined Locale constants - use these when available to avoid typos
Locale us     = Locale.US;       // en_US - English, United States
Locale uk     = Locale.UK;       // en_GB - English, United Kingdom
Locale french = Locale.FRENCH;   // fr    - French language only, no country
Locale france = Locale.FRANCE;   // fr_FR - French language, France country
Locale german  = Locale.GERMAN;  // de    - German language only
Locale germany = Locale.GERMANY; // de_DE - German language, Germany country
Locale japan   = Locale.JAPAN;   // ja_JP
Locale china   = Locale.CHINA;   // zh_CN

// Constructor approach - still valid but deprecated in Java 19+
Locale l1 = new Locale("en");               // Language only
Locale l2 = new Locale("en", "US");         // Language + country
Locale l3 = new Locale("en", "US", "WIN");  // Language + country + variant

// Builder approach - preferred for explicit construction
// Validates the language and region codes at build time
Locale l4 = new Locale.Builder()
    .setLanguage("en")
    .setRegion("US")
    .build();

// BCP 47 language tag approach - preferred for parsing external locale strings
// Uses hyphen separator (not underscore) in the tag string
Locale l5 = Locale.forLanguageTag("en-US");
Locale l6 = Locale.forLanguageTag("zh-Hans-CN");  // Chinese Simplified, China

// Extracting information from a Locale
Locale locale = Locale.FRANCE;
locale.getLanguage();         // "fr"      - ISO 639 code, always lowercase
locale.getCountry();          // "FR"      - ISO 3166 code, always uppercase
locale.getDisplayLanguage();  // "French"  - in the JVM default locale
locale.getDisplayCountry();   // "France"  - in the JVM default locale
locale.getDisplayName();      // "French (France)"
locale.toLanguageTag();       // "fr-FR"   - BCP 47 format with hyphen

// Display names can themselves be localized
// Pass a Locale argument to get the name in that language
locale.getDisplayLanguage(Locale.GERMAN);  // "Franzoesisch"
locale.getDisplayCountry(Locale.GERMAN);   // "Frankreich"

// Default Locale - used when no Locale is specified to a locale-sensitive API
Locale defaultLocale = Locale.getDefault();
Locale.setDefault(Locale.JAPAN);  // Changes the JVM-wide default

// Java 7+: separate defaults for display (text rendering) vs format (numbers/dates)
// Changing one does not change the other
Locale.getDefault(Locale.Category.DISPLAY);  // Used for UI text and messages
Locale.getDefault(Locale.Category.FORMAT);   // Used for number and date formatting

// Get all locales supported by this JVM installation
Locale[] available = Locale.getAvailableLocales();
Exam Tip: Language codes are always lowercase (en, fr, de). Country codes are always uppercase (US, GB, FR). This is a common exam trick - new Locale("EN", "us") is technically allowed but produces a non-standard locale. Also remember: Locale.FRENCH has no country component and is not the same as Locale.FRANCE. This distinction matters for resource bundle lookup.

NumberFormat

NumberFormat is an abstract class in java.text that formats and parses numbers according to locale conventions. Different locales use different decimal separators (period vs comma), different grouping separators, and different currency symbols. Always use NumberFormat rather than string formatting when the output will be shown to users in a localized application.
import java.text.NumberFormat;
import java.util.Locale;

double number = 1234567.89;

// getInstance() with no argument uses the JVM default Locale
NumberFormat nf = NumberFormat.getInstance();
System.out.println(nf.format(number));  // Format depends on default locale

// The same number formatted differently by locale
NumberFormat usFormat = NumberFormat.getInstance(Locale.US);
NumberFormat deFormat = NumberFormat.getInstance(Locale.GERMANY);
NumberFormat frFormat = NumberFormat.getInstance(Locale.FRANCE);

System.out.println(usFormat.format(number)); // 1,234,567.89  (period = decimal, comma = group)
System.out.println(deFormat.format(number)); // 1.234.567,89  (comma = decimal, period = group)
System.out.println(frFormat.format(number)); // 1 234 567,89  (comma = decimal, space = group)

// Currency formatting - adapts symbol, decimal rules, and rounding
double price = 1234.56;
NumberFormat usCurrency = NumberFormat.getCurrencyInstance(Locale.US);
NumberFormat ukCurrency = NumberFormat.getCurrencyInstance(Locale.UK);
NumberFormat jpCurrency = NumberFormat.getCurrencyInstance(Locale.JAPAN);

System.out.println(usCurrency.format(price)); // $1,234.56
System.out.println(ukCurrency.format(price)); // GBP1,234.56
System.out.println(jpCurrency.format(price)); // JPY1,235  (yen has no fractional unit)

// Percentage formatting - multiplies by 100 and appends the locale percent symbol
double ratio = 0.75;
NumberFormat percent = NumberFormat.getPercentInstance(Locale.US);
System.out.println(percent.format(ratio));  // 75%

// Parsing - converts a localized string back to a Number
// IMPORTANT: use locale-appropriate parse(), not Double.parseDouble()
// Double.parseDouble("1.234,56") throws NumberFormatException on a German-format string
String input = "1,234.56";
try {
    Number parsed = usFormat.parse(input);   // Parses using US rules
    double value = parsed.doubleValue();     // 1234.56
} catch (ParseException e) {
    // Thrown if the string does not match the expected format for this locale
}

// parse() is lenient by default - it stops at the first unrecognized character
// "1,234.56 USD" parsed by usFormat returns 1234.56 (stops before the space)
// Use setParseIntegerOnly(true) to parse only the integer portion

// Compact number format (Java 12+) - abbreviates large numbers
NumberFormat shortCompact = NumberFormat.getCompactNumberInstance(
    Locale.US, NumberFormat.Style.SHORT);
NumberFormat longCompact = NumberFormat.getCompactNumberInstance(
    Locale.US, NumberFormat.Style.LONG);

System.out.println(shortCompact.format(1_000));       // 1K
System.out.println(shortCompact.format(1_000_000));   // 1M
System.out.println(longCompact.format(1_000));        // 1 thousand
System.out.println(longCompact.format(1_000_000));    // 1 million

// Controlling decimal precision
NumberFormat precise = NumberFormat.getInstance(Locale.US);
precise.setMinimumFractionDigits(2);  // Always show at least 2 decimal places
precise.setMaximumFractionDigits(2);  // Never show more than 2 decimal places
System.out.println(precise.format(3.14159)); // 3.14
System.out.println(precise.format(3.1));     // 3.10
Exam Tip: NumberFormat.getInstance() with no argument uses the default Locale, not a fixed locale. This means the output of your code can change depending on the JVM's locale setting. Always pass an explicit Locale when the format must be predictable. Also, never use Double.parseDouble() to parse localized number strings - a string like "1.234,56" (German format) will throw a NumberFormatException. Use NumberFormat.parse() instead.

DateTimeFormatter

DateTimeFormatter in java.time.format formats and parses date and time objects from the java.time package. Formatters are immutable and thread-safe. You can use predefined ISO formatters, locale-sensitive formatters using FormatStyle, or custom pattern-based formatters. Use withLocale() to attach a locale to any formatter.
import java.time.*;
import java.time.format.*;
import java.util.Locale;

LocalDate date         = LocalDate.of(2024, 12, 25);
LocalTime time         = LocalTime.of(14, 30, 0);
LocalDateTime dateTime = LocalDateTime.of(date, time);

// Predefined ISO formatters - always use the same format regardless of locale
DateTimeFormatter isoDate = DateTimeFormatter.ISO_LOCAL_DATE;
System.out.println(date.format(isoDate));  // 2024-12-25

// Localized formatters using FormatStyle
// The actual output depends on the locale attached to the formatter
// FormatStyle options from shortest to most verbose: SHORT, MEDIUM, LONG, FULL
DateTimeFormatter shortDate  = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
DateTimeFormatter mediumDate = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
DateTimeFormatter longDate   = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG);
DateTimeFormatter fullDate   = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);

// Output below assumes default locale is en_US
System.out.println(date.format(shortDate));  // 12/25/24
System.out.println(date.format(mediumDate)); // Dec 25, 2024
System.out.println(date.format(longDate));   // December 25, 2024
System.out.println(date.format(fullDate));   // Wednesday, December 25, 2024

// withLocale() returns a new formatter with the given locale - original is unchanged
// DateTimeFormatter is immutable; withLocale() does NOT modify in place
DateTimeFormatter germanDate = DateTimeFormatter
    .ofLocalizedDate(FormatStyle.LONG)
    .withLocale(Locale.GERMANY);
System.out.println(date.format(germanDate)); // 25. Dezember 2024

// Time-only formatting
DateTimeFormatter timeFormat = DateTimeFormatter
    .ofLocalizedTime(FormatStyle.SHORT)
    .withLocale(Locale.US);
System.out.println(time.format(timeFormat)); // 2:30 PM

// DateTime formatting - takes separate FormatStyle for date and time parts
DateTimeFormatter dtFormat = DateTimeFormatter
    .ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT)
    .withLocale(Locale.US);
System.out.println(dateTime.format(dtFormat)); // 12/25/24, 2:30 PM

// Custom pattern - locale-independent, always produces the same output
DateTimeFormatter custom = DateTimeFormatter.ofPattern("dd-MMM-yyyy HH:mm");
System.out.println(dateTime.format(custom)); // 25-Dec-2024 14:30

// Common pattern letter reference:
// y  = year (4 digits for yyyy, 2 for yy)
// M  = month number or name (MM = 01-12, MMM = Jan, MMMM = January)
// d  = day of month (dd = zero-padded)
// H  = hour in 24-hour clock (00-23)
// h  = hour in 12-hour clock (01-12) - use with a (AM/PM marker)
// m  = minute (mm = zero-padded)
// s  = second
// E  = day of week abbreviation (EEEE = full name)
// a  = AM/PM marker
// z  = time zone abbreviation
// Z  = time zone offset (e.g., +0530)

// Parsing with a formatter
LocalDate parsed = LocalDate.parse("2024-12-25", DateTimeFormatter.ISO_LOCAL_DATE);
LocalDate custom2 = LocalDate.parse("25-Dec-2024",
    DateTimeFormatter.ofPattern("dd-MMM-yyyy"));
// parse() throws DateTimeParseException if the input does not match the pattern
Exam Tip: ofLocalizedDate() only works with date objects (LocalDate). ofLocalizedTime() only works with time objects (LocalTime). ofLocalizedDateTime() only works with date-time objects (LocalDateTime). Applying the wrong formatter to the wrong object type throws a DateTimeException. Also remember that withLocale() returns a new formatter - it does not modify the original, because DateTimeFormatter is immutable.

ResourceBundle

A ResourceBundle externalizes locale-specific text so that message strings are not hardcoded in your Java source. The two kinds of resource bundle are properties files (plain text key-value pairs, used for string resources) and Java classes extending ListResourceBundle (used when you need non-String values or programmatic control). The base name and locale together determine which file is loaded.
// Naming convention for properties files:
// basename.properties                  (default / fallback - must exist)
// basename_language.properties         (language-specific)
// basename_language_country.properties (language + country specific)

// messages.properties (default fallback - should contain ALL keys)
// greeting=Hello
// farewell=Goodbye
// welcome=Welcome, {0}!

// messages_fr.properties (French overrides)
// greeting=Bonjour
// farewell=Au revoir

// messages_fr_CA.properties (French Canada overrides - only what differs)
// farewell=Bye

// Loading a ResourceBundle - uses default Locale if none specified
ResourceBundle bundle   = ResourceBundle.getBundle("messages");
ResourceBundle frBundle = ResourceBundle.getBundle("messages", Locale.FRENCH);

System.out.println(frBundle.getString("greeting")); // "Bonjour" (from messages_fr)
// getString() throws MissingResourceException if the key is not found in any bundle

// Checking and iterating keys
Set<String> keys = bundle.keySet();  // Returns keys from this bundle AND its parents
if (bundle.containsKey("greeting")) {
    System.out.println(bundle.getString("greeting"));
}

// MessageFormat - substitutes arguments into a pattern string at runtime
// The {0}, {1} placeholders in the properties file are positional arguments
// welcome=Welcome, {0}! Today is {1}.
String pattern = bundle.getString("welcome");
String result  = MessageFormat.format(pattern, "Alice", "Monday");
// Result: "Welcome, Alice! Today is Monday."

// Java class-based ResourceBundle (ListResourceBundle)
// Useful when values are not Strings, or when content is generated programmatically
public class Messages_en extends ListResourceBundle {
    @Override
    protected Object[][] getContents() {
        return new Object[][] {
            {"greeting", "Hello"},
            {"farewell", "Goodbye"},
            {"maxRetries", 3}  // Integer value - not possible in .properties files
        };
    }
}

// Retrieving non-String values - use getObject() and cast
Object value   = bundle.getObject("maxRetries");
Integer retries = (Integer) value;
// getString() on a non-String value throws ClassCastException
Exam Tip: Java class bundles (compiled .class files) have higher priority than properties files with the same base name and locale. If both Messages_fr.class and messages_fr.properties exist on the classpath, the class file is loaded and the properties file is ignored. Also, the default bundle (messages.properties or Messages.class) should contain every key your application uses - locale-specific bundles only need to override the keys that differ.

Resource Bundle Lookup

When you call ResourceBundle.getBundle(baseName, locale), Java searches for the best matching bundle using a defined lookup sequence. It tries the most specific combination first (language + country) and works toward the least specific (the default bundle). Individual keys are also inherited: if a key is missing from the matched bundle, Java walks up the chain to find it in a less-specific bundle. This means a single key can come from any level of the hierarchy.
// Full lookup order when requested locale is fr_FR and default locale is en_US
// Java tries each of these in order and uses the FIRST one found:

// 1.  messages_fr_FR.java  (or .class)
// 2.  messages_fr_FR.properties
// 3.  messages_fr.java
// 4.  messages_fr.properties
// 5.  messages_en_US.java      (default locale chain starts here)
// 6.  messages_en_US.properties
// 7.  messages_en.java
// 8.  messages_en.properties
// 9.  messages.java            (base bundle - last resort)
// 10. messages.properties
// If none found: throws MissingResourceException

// Once a bundle is selected, key lookup also inherits up the parent chain.
// Example files:
// messages_fr_FR.properties:  farewell=Salut
// messages_fr.properties:     greeting=Bonjour, farewell=Au revoir
// messages.properties:        greeting=Hello, farewell=Goodbye, welcome=Welcome

ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.FRANCE);
// The "controlling" bundle is messages_fr_FR (step 1 above)
bundle.getString("farewell");  // "Salut"    - found in messages_fr_FR
bundle.getString("greeting");  // "Bonjour"  - not in fr_FR, inherited from messages_fr
bundle.getString("welcome");   // "Welcome"  - not in fr_FR or fr, inherited from messages

// Key not found in any bundle in the chain
try {
    bundle.getString("nonexistent.key");
} catch (MissingResourceException e) {
    System.out.println("Key not found: " + e.getKey());
}

// Best practices for resource bundles:
// 1. Always provide a default bundle (basename.properties) with every key
// 2. In locale-specific bundles, only include keys that actually differ from the default
// 3. Use containsKey() before getString() in code paths where a key might be optional
// 4. Use MessageFormat patterns ({0}, {1}) for messages with dynamic content
// 5. Avoid concatenating translated strings with untranslated fragments - word order varies by language
Exam Tip: The lookup finds the most specific bundle file, then key lookup inherits up the parent chain. These are two separate steps. A key does NOT have to exist in the controlling bundle - it can be found anywhere in the chain all the way up to the base bundle. If the same key exists in both messages_fr.properties and messages.properties, the one in messages_fr is used because it is closer to the requested locale. The default locale chain (steps 5-8) is only searched if no bundle at all is found for the requested locale chain.

Locale-Sensitive Comparisons

String.compareTo() and String.compareToIgnoreCase() compare strings by Unicode code point value, which does not produce the correct alphabetical ordering for all languages. For example, in Swedish the letter a-with-a-ring sorts after z, not alongside a. Collator in java.text performs linguistically correct comparisons for a specific locale and is the right tool for sorting user-visible text.
import java.text.Collator;

// Obtain a Collator for a specific locale
Collator collator = Collator.getInstance(Locale.GERMAN);

// compare() returns negative, zero, or positive just like Comparator.compare()
int result = collator.compare("Aachen", "Aechen");
// Result follows German alphabetical rules where ae is treated as a-umlaut variant

// Use Collator as a Comparator to sort a list correctly for the locale
List<String> cities = Arrays.asList("Zurich", "Aachen", "Aechen");
cities.sort(collator);  // Sorted according to German linguistic rules

// Collator strength - controls which differences are considered significant
collator.setStrength(Collator.PRIMARY);   // Only base letter differences matter
                                          // "a" == "A" == "ae" at PRIMARY
collator.setStrength(Collator.SECONDARY); // Base letters and accents differ; case is ignored
                                          // "a" == "A" but "a" != "ae"
collator.setStrength(Collator.TERTIARY);  // Default: base letters, accents, AND case all differ
                                          // "a" != "A" != "ae"
collator.setStrength(Collator.IDENTICAL); // Everything differs, including Unicode canonical equivalents

// CollationKey - precomputed sort key for efficient repeated comparisons
// Useful when sorting a large list where each string is compared many times
CollationKey key1 = collator.getCollationKey("Aachen");
CollationKey key2 = collator.getCollationKey("Aechen");
int cmp = key1.compareTo(key2);  // Faster than collator.compare() in a sort loop
Exam Tip: Never use String.compareTo() to sort text that will be displayed to users in a localized application. The Unicode code point ordering it uses is not alphabetically correct for most languages. Use Collator.getInstance(locale) instead. CollationKey is an optimization for cases where the same strings are compared many times - it precomputes the comparison key so the sort is faster.

1Z0-830 Java SE 21 Certification - Table of Contents

Master all exam topics with comprehensive study guides and practice examples.


Popular Posts