Course topics

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

Java
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"));
    }
}
Output
Hello, Asha!
Bonjour, Asha!

NumberFormat formatting the same value for US and German locales

Java
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));
    }
}
Output
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

Java · starter code
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
Java · 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.

WebNest Studio

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