21.1 Internationalization Fundamentals
Internationalization (i18n) is the process of designing applications to support multiple languages and locales. Java provides robust APIs and design patterns for building globally aware applications that adapt to user preferences and regional conventions.
What is Internationalization?
Internationalization encompasses:
- Language: Different spoken and written languages
- Locale: Regional preferences for formatting, sorting, and display conventions
- Character encodings: Support for Unicode and various character sets
- Cultural differences: Variations in date/time formats, number formats, currency symbols
- Left-to-right vs right-to-left text: Arabic, Hebrew, and other RTL languages
// Basic i18n workflow
import java.util.Locale;
import java.util.ResourceBundle;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.Date;
public class I18nBasics {
public static void main(String[] args) {
// 1. Determine user locale
Locale userLocale = selectUserLocale();
// 2. Load locale-specific resources
ResourceBundle messages = ResourceBundle.getBundle("messages", userLocale);
// 3. Format data according to locale
double price = 1234.56;
String formatted = NumberFormat.getCurrencyInstance(userLocale).format(price);
// 4. Display localized message
String greeting = messages.getString("greeting");
System.out.println(greeting + " - " + formatted);
}
private static Locale selectUserLocale() {
// Try to get user's locale preference
String localeString = getUserLocalePreference(); // From profile/settings
if (localeString != null) {
return Locale.forLanguageTag(localeString);
}
// Fall back to system default
return Locale.getDefault(Locale.Category.FORMAT);
}
private static String getUserLocalePreference() {
return null; // Implementation would fetch from user profile
}
}
The Locale Class
Locale represents a specific geographical, political, or cultural region. It encapsulates language, country, and script information.
// Locale construction and usage
import java.util.Locale;
// Creating locales
Locale english = new Locale("en");
Locale usEnglish = new Locale("en", "US");
Locale britishEnglish = new Locale("en", "GB");
Locale germanySwitzerland = new Locale("de", "CH");
// Using predefined Locale constants
Locale us = Locale.US; // English (United States)
Locale france = Locale.FRANCE; // French (France)
Locale germany = Locale.GERMANY; // German (Germany)
Locale japan = Locale.JAPAN; // Japanese (Japan)
Locale china = Locale.CHINA; // Chinese (China)
// Creating locales with script (e.g., simplified vs traditional Chinese)
Locale simplifiedChinese = Locale.forLanguageTag("zh-Hans");
Locale traditionalChinese = Locale.forLanguageTag("zh-Hant");
// Creating locales with variant
Locale norwegianBokmål = new Locale("no", "NO", "BOKMÅL");
// BCP 47 language tag format (modern approach)
Locale swiss = Locale.forLanguageTag("de-CH");
Locale brazil = Locale.forLanguageTag("pt-BR");
Locale india = Locale.forLanguageTag("hi-IN");
System.out.println("US English: " + Locale.US.getDisplayName()); // English (United States)
System.out.println("Germany: " + Locale.GERMANY.getDisplayName()); // German (Germany)
System.out.println("Swiss German: " + swiss.getDisplayName()); // German (Switzerland)
Locale Components
Each Locale has several accessible components:
// Locale components
Locale loc = Locale.forLanguageTag("de-CH");
// Language information
String language = loc.getLanguage(); // "de"
String displayLanguage = loc.getDisplayLanguage(); // "German"
String displayLanguageLocalized = loc.getDisplayLanguage(loc); // "Deutsch"
// Country/Region information
String country = loc.getCountry(); // "CH"
String displayCountry = loc.getDisplayCountry(); // "Switzerland"
String displayCountryLocalized = loc.getDisplayCountry(loc); // "Schweiz"
// Script information (for complex writing systems)
String script = loc.getScript(); // "" for most locales
if (script != null && !script.isEmpty()) {
String displayScript = loc.getDisplayScript();
}
// Variant information
String variant = loc.getVariant(); // "" unless explicitly set
if (variant != null && !variant.isEmpty()) {
String displayVariant = loc.getDisplayVariant();
}
// Language tag (BCP 47 format)
String languageTag = loc.toLanguageTag(); // "de-CH"
System.out.println("Language: " + language);
System.out.println("Country: " + country);
System.out.println("Language Tag: " + languageTag);
Locale Categories
Different aspects of a system may use different locales (e.g., display in French but number format in German).
// Locale categories
import java.util.Locale;
// Format category: Controls number, date, currency formatting
Locale formatLocale = Locale.getDefault(Locale.Category.FORMAT);
System.out.println("Format Locale: " + formatLocale);
// Display category: Controls UI text display
Locale displayLocale = Locale.getDefault(Locale.Category.DISPLAY);
System.out.println("Display Locale: " + displayLocale);
// Setting locale for specific categories
Locale.setDefault(Locale.Category.FORMAT, Locale.GERMANY);
Locale.setDefault(Locale.Category.DISPLAY, Locale.FRANCE);
// Now number formatting uses German conventions
// But UI text uses French
NumberFormat nf = NumberFormat.getInstance();
System.out.println("German-formatted number: " + nf.format(1234.56)); // 1.234,56
// Important: Always use category-specific defaults instead of global JVM default
Supported Locales and Availability
Java provides a fixed set of supported locales for various formatting services.
// Discovering supported locales
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Arrays;
// Get all available locales
Locale[] allLocales = Locale.getAvailableLocales();
System.out.println("Total available locales: " + allLocales.length);
// Get supported locales for NumberFormat
Locale[] numberLocales = NumberFormat.getAvailableLocales();
System.out.println("NumberFormat locales: " + numberLocales.length);
// Get supported locales for DateFormat
Locale[] dateLocales = DateFormat.getAvailableLocales();
System.out.println("DateFormat locales: " + dateLocales.length);
// Check if specific locale is supported
Locale targetLocale = Locale.forLanguageTag("ja-JP");
boolean isSupported = Arrays.asList(numberLocales).contains(targetLocale);
System.out.println("Japanese supported: " + isSupported);
// Find fallback if exact locale not supported
Locale supportedLocale = findSupportedLocale(targetLocale, numberLocales);
static Locale findSupportedLocale(Locale requested, Locale[] supported) {
// Try exact match
for (Locale locale : supported) {
if (locale.equals(requested)) {
return locale;
}
}
// Try language match
for (Locale locale : supported) {
if (locale.getLanguage().equals(requested.getLanguage())) {
return locale;
}
}
// Fall back to English
return Locale.ENGLISH;
}
Locale Matching and Resolution
Applications often need to match user preferences against available locales.
// Locale matching strategies
import java.util.Locale;
import java.util.List;
import java.util.Arrays;
class LocaleResolver {
private List<Locale> supportedLocales;
public LocaleResolver(Locale... locales) {
this.supportedLocales = Arrays.asList(locales);
}
// Strategy 1: Exact match
public Locale resolveExact(Locale requested) {
return supportedLocales.stream()
.filter(l -> l.equals(requested))
.findFirst()
.orElse(null);
}
// Strategy 2: Language match (ignores country/region)
public Locale resolveByLanguage(Locale requested) {
return supportedLocales.stream()
.filter(l -> l.getLanguage().equals(requested.getLanguage()))
.findFirst()
.orElse(null);
}
// Strategy 3: Weighted matching with fallback chain
public Locale resolveWithFallback(Locale requested) {
// Try exact match
Locale exact = resolveExact(requested);
if (exact != null) return exact;
// Try language match
Locale language = resolveByLanguage(requested);
if (language != null) return language;
// Try language with different country
for (Locale supported : supportedLocales) {
if (supported.getLanguage().equals(requested.getLanguage())) {
return supported;
}
}
// Fall back to English, then any available locale
return supportedLocales.stream()
.filter(l -> l.equals(Locale.ENGLISH))
.findFirst()
.orElse(supportedLocales.get(0));
}
// Strategy 4: Quality-weighted language ranges (RFC 4647)
public Locale resolveBestMatch(List<Locale> preferredLocales) {
for (Locale preferred : preferredLocales) {
Locale resolved = resolveWithFallback(preferred);
if (resolved != null) return resolved;
}
return supportedLocales.get(0);
}
}
// Using locale resolver
LocaleResolver resolver = new LocaleResolver(
Locale.ENGLISH,
Locale.FRANCE,
Locale.GERMANY,
Locale.JAPAN
);
Locale bestMatch = resolver.resolveWithFallback(Locale.forLanguageTag("de-CH"));
System.out.println("Best match for de-CH: " + bestMatch); // Likely Locale.GERMANY
Locale and Thread Safety
Locale is immutable and thread-safe, but JVM defaults should be handled carefully.
// Thread-safe locale handling
import java.util.Locale;
import java.text.NumberFormat;
class ThreadLocalLocaleExample {
// Don't use global Locale.setDefault() in multi-threaded apps
private static ThreadLocal<Locale> userLocale = ThreadLocal.withInitial(
() -> Locale.getDefault(Locale.Category.FORMAT)
);
public static void setUserLocale(Locale locale) {
userLocale.set(locale);
}
public static Locale getUserLocale() {
return userLocale.get();
}
public static String formatCurrency(double amount) {
NumberFormat nf = NumberFormat.getCurrencyInstance(getUserLocale());
return nf.format(amount);
}
public static void main(String[] args) throws InterruptedException {
// Thread 1: US user
Thread t1 = new Thread(() -> {
setUserLocale(Locale.US);
System.out.println("Thread 1: " + formatCurrency(1234.56)); // $1,234.56
});
// Thread 2: German user
Thread t2 = new Thread(() -> {
setUserLocale(Locale.GERMANY);
System.out.println("Thread 2: " + formatCurrency(1234.56)); // 1.234,56 €
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
Common Locale Patterns
Frequently-used locale configurations for typical applications.
// Common locale patterns
import java.util.Locale;
import java.util.Arrays;
import java.util.List;
class CommonLocales {
// Core supported locales for a global application
public static final List<Locale> CORE_LOCALES = Arrays.asList(
Locale.ENGLISH, // English (US)
Locale.forLanguageTag("en-GB"), // English (UK)
Locale.FRANCE, // French
Locale.GERMANY, // German
Locale.forLanguageTag("es-ES"), // Spanish
Locale.ITALY, // Italian
Locale.JAPAN, // Japanese
Locale.CHINA, // Simplified Chinese
Locale.forLanguageTag("zh-TW"), // Traditional Chinese
new Locale("pt", "BR") // Portuguese (Brazil)
);
// Right-to-left languages
public static final List<Locale> RTL_LOCALES = Arrays.asList(
Locale.forLanguageTag("ar"), // Arabic
Locale.forLanguageTag("he"), // Hebrew
Locale.forLanguageTag("ur") // Urdu
);
// Check if locale uses right-to-left text
public static boolean isRightToLeft(Locale locale) {
return RTL_LOCALES.stream()
.anyMatch(l -> l.getLanguage().equals(locale.getLanguage()));
}
// Get locale group for related languages
public static List<Locale> getLocaleGroup(String language) {
return switch(language) {
case "en" -> Arrays.asList(Locale.ENGLISH, Locale.forLanguageTag("en-GB"));
case "es" -> Arrays.asList(Locale.forLanguageTag("es-ES"),
Locale.forLanguageTag("es-MX"));
case "pt" -> Arrays.asList(new Locale("pt", "BR"),
Locale.forLanguageTag("pt-PT"));
case "zh" -> Arrays.asList(Locale.CHINA, Locale.forLanguageTag("zh-TW"));
default -> Arrays.asList(Locale.getAvailableLocales()).stream()
.filter(l -> l.getLanguage().equals(language))
.toList();
};
}
}
// Using common locale patterns
System.out.println("RTL Check for Arabic: " +
CommonLocales.isRightToLeft(Locale.forLanguageTag("ar"))); // true
System.out.println("Spanish locales: " +
CommonLocales.getLocaleGroup("es")); // [es_ES, es_MX]
Best Practices
- Never use Locale.setDefault() in multi-threaded applications: Use
ThreadLocalor pass Locale explicitly. - Accept user locale preference: Store in user profile, don't rely on JVM default.
- Use Locale.Category for different aspects: Format and display may need different locales.
- Handle unsupported locales gracefully: Implement fallback chains (language → English → first available).
- Consider script and variant: Some languages require script distinction (Chinese simplified/traditional).
- Test with multiple locales: Especially right-to-left languages and complex character sets.
- Use Locale.forLanguageTag(): BCP 47 format is more flexible and modern than constructor.
- Document locale support: Clearly specify which locales your application supports.