By WebNest Studio Engineering Team
Java - Core Tutorial
Internationalization (i18n) in Java
Internationalization (often abbreviated i18n — "i", 18 letters, "n") is the practice of designing software so it can be adapted to different languages, regions, and cultural conventions without changing its code. Java has built-in support for this through the Locale class, ResourceBundle for externalizing translatable text, and locale-aware formatters for numbers, currencies, and dates.
A well-internationalized Java application never hardcodes user-facing strings or assumes everyone formats numbers and dates the way the developer's own locale does.
Why it matters
An application used in more than one country has to show text in the user's language and format numbers, currency and dates the way that country writes them. The same number is written 1,234.5 in the United States and 1.234,5 in Germany. Building with Locale and resource bundles from the start is far cheaper than finding every hard-coded string later.
Locale — Representing a Language and Region
A Locale object represents a specific combination of language and (optionally) country/region, such as English in the United States (en-US) versus French in France (fr-FR). Locale does not hold any translated text itself — it is simply a key used by other classes (like ResourceBundle and the formatters below) to decide which language- or region-specific behavior to apply. You can get one with new Locale("fr", "FR"), or use built-in constants like Locale.US and Locale.GERMANY, or read the JVM's default with Locale.getDefault().
ResourceBundle — Externalizing Translatable Text
ResourceBundle loads locale-specific text from separate properties files instead of hardcoding strings in Java source, so adding a new language means adding a new properties file — no recompilation of application logic required. Given a base name like "messages", you provide files such as messages_en.properties (English) and messages_fr.properties (French); ResourceBundle.getBundle("messages", locale) automatically picks the file matching the given Locale, falling back to a default messages.properties if no exact match exists.
- messages_en.properties:
greeting=Hello, {0}! - messages_fr.properties:
greeting=Bonjour, {0}! ResourceBundle.getBundle("messages", Locale.FRANCE).getString("greeting")returns the French text.
Locale-Aware Formatting: NumberFormat and DateFormat
Numbers, currency amounts, and dates are formatted differently across cultures — for example, the U.S. writes one thousand and a half as "1,000.5" while Germany writes it as "1.000,5" (commas and periods swapped). NumberFormat.getInstance(locale), NumberFormat.getCurrencyInstance(locale), and DateFormat.getDateInstance(style, locale) automatically apply the correct separators, symbols, and ordering for a given Locale, so the same underlying number or date value displays correctly for each audience without any manual string manipulation.
Examples
ResourceBundle loading greetings for two different locales
import java.util.*;
// messages_en.properties contains: greeting=Hello, {0}!
// messages_fr.properties contains: greeting=Bonjour, {0}!
public class ResourceBundleDemo {
public static void main(String[] args) {
ResourceBundle englishBundle = ResourceBundle.getBundle("messages", Locale.US);
ResourceBundle frenchBundle = ResourceBundle.getBundle("messages", Locale.FRANCE);
String englishTemplate = englishBundle.getString("greeting");
String frenchTemplate = frenchBundle.getString("greeting");
System.out.println(java.text.MessageFormat.format(englishTemplate, "Asha"));
System.out.println(java.text.MessageFormat.format(frenchTemplate, "Asha"));
}
}
Hello, Asha!
Bonjour, Asha!
NumberFormat formatting the same value for US and German locales
import java.text.NumberFormat;
import java.util.Locale;
public class NumberFormatDemo {
public static void main(String[] args) {
double amount = 1234567.891;
NumberFormat usFormat = NumberFormat.getNumberInstance(Locale.US);
NumberFormat germanFormat = NumberFormat.getNumberInstance(Locale.GERMANY);
System.out.println("US format: " + usFormat.format(amount));
System.out.println("German format: " + germanFormat.format(amount));
NumberFormat usCurrency = NumberFormat.getCurrencyInstance(Locale.US);
NumberFormat germanCurrency = NumberFormat.getCurrencyInstance(Locale.GERMANY);
System.out.println("US currency: " + usCurrency.format(amount));
System.out.println("German currency: " + germanCurrency.format(amount));
}
}
US format: 1,234,567.891
German format: 1.234.567,891
US currency: $1,234,567.89
German currency: 1.234.567,89 €
Common Mistakes
- Hardcoding user-facing strings directly in Java source instead of externalizing them into ResourceBundle properties files.
- Assuming every user formats numbers, currency, and dates the way the developer's own locale does, causing confusing or incorrect output for international users.
- Forgetting to specify an encoding for properties files containing non-ASCII characters, leading to garbled accented characters.
- Constructing formatted date/number strings manually with string concatenation instead of using locale-aware NumberFormat/DateFormat.
Exercise
Format the same number for a reader in the United States and for a reader in Germany, using NumberFormat.
Expected output: 1,234,567.891 then 1.234.567,891
import java.text.NumberFormat;
import java.util.Locale;
public class FormatForLocale {
public static void main(String[] args) {
double value = 1234567.891;
// TODO: print the value formatted for Locale.US
// TODO: print the value formatted for Locale.GERMANY
}
}
Show hints
NumberFormat.getNumberInstance(locale)returns a formatter for that locale.- Call
format(value)on the formatter to get the text.
Show solution
import java.text.NumberFormat;
import java.util.Locale;
public class FormatForLocale {
public static void main(String[] args) {
double value = 1234567.891;
System.out.println(NumberFormat.getNumberInstance(Locale.US).format(value)); // 1,234,567.891
System.out.println(NumberFormat.getNumberInstance(Locale.GERMANY).format(value)); // 1.234.567,891
}
}
Quick quiz
1. What does a Locale represent?
- A. A time zone
- B. A character encoding
- C. A language, optionally with a country or region
- D. A currency
Show answer
C. For example, fr is French, and fr-CA is French as used in Canada.
2. The locale is fr_CA and there is no messages_fr_CA.properties. Which file does ResourceBundle try next?
- A. messages_en.properties
- B. It throws an exception immediately
- C. messages.properties
- D. messages_fr.properties
Show answer
D. It falls back from the most specific file to the language-only file and then to the base file.
3. What happens when bundle.getString("title") is called and no file in the chain has that key?
- A. It throws MissingResourceException
- B. It returns the key
- C. It returns null
- D. It returns an empty string
Show answer
A. The base file should therefore contain every key, as the final fallback.
Interview questions
What is the difference between internationalization and localization?
Show answer
Internationalization, shortened to i18n, is designing the application so that it can support different languages and regions without code changes: moving text into resource bundles and using locale-aware formatting. Localization, l10n, is the work of adapting it to one particular locale: translating the text and supplying that locale's formats.
How does ResourceBundle choose which properties file to load?
Show answer
Given a base name and a locale, it looks for the most specific file first, for example messages_fr_CA.properties, then the language-only file messages_fr.properties, and finally the base file messages.properties. A key that is missing from a more specific file is taken from the next one in the chain, so a regional file only needs the entries that differ.
Key Points to Remember
- Locale represents a language/region pair and drives the behavior of ResourceBundle and the formatting classes.
- ResourceBundle loads translated text from locale-specific properties files (e.g. messages_en.properties, messages_fr.properties), avoiding hardcoded strings.
- NumberFormat and DateFormat produce locale-appropriate output for numbers, currency, and dates from the same underlying value.
- Internationalizing early (externalizing text, using locale-aware formatters) avoids expensive rework later.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering