Java · lesson 66 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Java Localization: ResourceBundle and Locale Support
Internationalize Java applications with ResourceBundle — load locale-specific messages, work with Locale, and understand fallback behavior.
By SkillByExample · Updated
Localization means showing each user text, numbers and dates in their own language and
conventions. Java separates the two halves: ResourceBundle looks up translated messages, and
Locale-aware formatters render numbers, currencies and dates. Examples run on JDK 21 with the
source launcher; the .properties files sit in the same folder as the .java file, which is on
the classpath.
Messages from ResourceBundle
Each language gets a properties file named basename_language[_COUNTRY].properties, plus a base
file used as the fallback.
# File: messages.properties
greeting=Hello
farewell=Goodbye
welcome=Welcome, {0}! You have {1} unread messages.
# File: messages_fr.properties
greeting=Bonjour
farewell=Au revoir
welcome=Bienvenue, {0} ! Vous avez {1} messages non lus.
# File: messages_fr_CA.properties
greeting=Allô
import java.util.Locale;
import java.util.ResourceBundle;
public class Bundles {
public static void main(String[] args) {
for (Locale loc : new Locale[] {Locale.ENGLISH, Locale.FRENCH, Locale.CANADA_FRENCH}) {
ResourceBundle b = ResourceBundle.getBundle("messages", loc);
System.out.printf("%-6s %-8s %-10s (from %s)%n", loc, b.getString("greeting"),
b.getString("farewell"), b.getLocale().equals(Locale.ROOT) ? "base file" : "messages_" + b.getLocale());
}
}
}
$ java Bundles.java
en Hello Goodbye (from base file)
fr Bonjour Au revoir (from messages_fr)
fr_CA Allô Au revoir (from messages_fr_CA)
fr_CA found its own file, which defines only greeting; the missing farewell was inherited
from its parent, messages_fr. English has no file, so it used the base file. The accented
Allô is read correctly because bundles have been read as UTF-8 since Java 9.
The default-locale trap
When no file matches the requested locale, Java tries the JVM’s default locale before the base file.
import java.util.Locale;
import java.util.ResourceBundle;
public class Fallback {
public static void main(String[] args) {
System.out.println("default locale: " + Locale.getDefault());
ResourceBundle ja = ResourceBundle.getBundle("messages", Locale.JAPANESE);
System.out.println("ja -> " + ja.getString("greeting"));
ResourceBundle strict = ResourceBundle.getBundle("messages", Locale.JAPANESE,
ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT));
System.out.println("ja, no default-locale fallback -> " + strict.getString("greeting"));
}
}
$ java Fallback.java
default locale: en_US
ja -> Hello
ja, no default-locale fallback -> Hello
$ java -Duser.language=fr -Duser.country=FR Fallback.java
default locale: fr_FR
ja -> Bonjour
ja, no default-locale fallback -> Hello
On an English machine both lookups end at the base file. Run the same program with a French
default locale and the Japanese user suddenly gets French. getNoFallbackControl skips the
default-locale step, which is usually what a server serving many users wants.
Locale
A Locale is a language plus an optional region (and script or variant). Build one from a BCP 47
tag or with Locale.of (Java 19+).
import java.util.Locale;
public class Locales {
public static void main(String[] args) {
Locale[] locales = {Locale.US, Locale.FRANCE, Locale.forLanguageTag("pt-BR"), Locale.of("en", "IN"), Locale.JAPAN};
for (Locale l : locales) {
System.out.printf("%-6s tag=%-6s %s / %s%n", l, l.toLanguageTag(),
l.getDisplayName(Locale.ENGLISH), l.getDisplayName(l));
}
}
}
$ java Locales.java
en_US tag=en-US English (United States) / English (United States)
fr_FR tag=fr-FR French (France) / français (France)
pt_BR tag=pt-BR Portuguese (Brazil) / português (Brasil)
en_IN tag=en-IN English (India) / English (India)
ja_JP tag=ja-JP Japanese (Japan) / 日本語 (日本)
toString() uses an underscore, the language tag a hyphen — use tags in HTTP headers and URLs.
getDisplayName(l) names each locale in its own language, the way a language picker should.
new Locale("en", "IN") still works but has been deprecated since Java 19.
MessageFormat: placeholders, numbers and plurals
MessageFormat fills {0}, {1} and formats them for the locale. A choice format picks
wording by number.
import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;
public class Messages {
public static void main(String[] args) {
for (Locale loc : new Locale[] {Locale.US, Locale.FRANCE}) {
String template = ResourceBundle.getBundle("messages", loc).getString("welcome");
System.out.println(new MessageFormat(template, loc).format(new Object[] {"Alice", 1234}));
}
String plural = "You have {0,choice,0#no messages|1#one message|1<{0,number,integer} messages}.";
for (int n : new int[] {0, 1, 5}) {
System.out.println(MessageFormat.format(plural, n));
}
}
}
$ java Messages.java
Welcome, Alice! You have 1,234 unread messages.
Bienvenue, Alice ! Vous avez 1 234 messages non lus.
You have no messages.
You have one message.
You have 5 messages.
The number argument is grouped per locale: a comma in English, a narrow no-break space in French
(it looks like an ordinary space). In the choice pattern, 0# matches exactly 0, 1# matches 1,
and 1< matches anything greater than 1, so each count gets grammatical wording. Real plural
rules differ by language — Polish has three forms — which is another reason the pattern belongs
in each language’s properties file.
Numbers, currencies and dates
Formatting is locale data, not translation: the same value is written differently per region.
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
public class Formats {
public static void main(String[] args) {
double amount = 1234567.891;
LocalDate date = LocalDate.of(2026, 3, 14);
for (Locale loc : new Locale[] {Locale.US, Locale.GERMANY, Locale.FRANCE, Locale.forLanguageTag("en-IN"), Locale.JAPAN}) {
System.out.printf("%-6s %-16s %-18s %s%n", loc,
NumberFormat.getNumberInstance(loc).format(amount),
NumberFormat.getCurrencyInstance(loc).format(amount),
date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(loc)));
}
}
}
$ java Formats.java
en_US 1,234,567.891 $1,234,567.89 Mar 14, 2026
de_DE 1.234.567,891 1.234.567,89 € 14.03.2026
fr_FR 1 234 567,891 1 234 567,89 € 14 mars 2026
en_IN 1,234,567.891 ₹1,234,567.89 14-Mar-2026
ja_JP 1,234,567.891 ¥1,234,568 2026/03/14
Germany swaps the separators, France groups with a narrow no-break space, and yen has no minor
unit, so the amount is rounded to a whole number. Notice en_IN: Indian convention groups in
lakhs (12,34,567), but NumberFormat.getNumberInstance uses a fixed grouping size of three, so
JDK 21 prints 1,234,567 — locale data has limits, and some conventions need a library such as
ICU4J. The currency comes from the locale’s country; for real
money pass a Currency explicitly rather than trusting the user’s locale.
Practice
- Add a Spanish bundle with only
greeting=Hola, and printgreetingandfarewellforLocale.of("es").
Solution
# File: messages_es.properties
greeting=Hola
import java.util.Locale;
import java.util.ResourceBundle;
public class Spanish {
public static void main(String[] args) {
ResourceBundle b = ResourceBundle.getBundle("messages", Locale.of("es"));
System.out.println(b.getString("greeting") + " / " + b.getString("farewell"));
}
}
$ java Spanish.java
Hola / Goodbye
farewell is missing from the Spanish file, so it comes from the base file — partial translations
degrade gracefully instead of failing.
- Ask for a key that exists in no file. What is thrown, and how do you avoid it?
Solution
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
public class MissingKey {
public static void main(String[] args) {
ResourceBundle b = ResourceBundle.getBundle("messages", Locale.FRENCH);
System.out.println(b.containsKey("checkout") ? b.getString("checkout") : "[checkout]");
try {
b.getString("checkout");
} catch (MissingResourceException e) {
System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}
$ java MissingKey.java
[checkout]
MissingResourceException: Can't find resource for bundle java.util.PropertyResourceBundle, key checkout
containsKey checks the whole parent chain, so a fallback like [checkout] makes a missing
translation visible on screen instead of crashing the page.
- Format 0.256 as a percentage and 1500000 in compact form (“1.5M”) for the US and Germany.
Solution
import java.text.NumberFormat;
import java.util.Locale;
public class Compact {
public static void main(String[] args) {
for (Locale loc : new Locale[] {Locale.US, Locale.GERMANY}) {
NumberFormat pct = NumberFormat.getPercentInstance(loc);
pct.setMaximumFractionDigits(1);
NumberFormat compact = NumberFormat.getCompactNumberInstance(loc, NumberFormat.Style.SHORT);
compact.setMaximumFractionDigits(1);
System.out.println(loc + ": " + pct.format(0.256) + " | " + compact.format(1_500_000));
}
}
}
$ java Compact.java
en_US: 25.6% | 1.5M
de_DE: 25,6 % | 1,5 Mio.
getCompactNumberInstance (Java 12+) uses CLDR’s abbreviations, so German writes “Mio.” and puts
a space before the percent sign.