🧰 Core APIs Toolbox · Intermediate

Locale & internationalization in Java

Locale-sensitive formatting, toUpperCase(Locale.ROOT), NumberFormat, ResourceBundle.

🧩 The mysteryYour app's "quit" command works everywhere — except for users in Turkey, who can never exit. The culprit is a single letter: "i".

Language + region

A **Locale** names a language and region: en-US, de-DE, ar-BH. It changes how numbers, dates, currencies and even upper/lowercase are produced. Create one with Locale.of("en", "US") (Java 19+) or constants like Locale.US.

🔮 Predict it

One number, two countries

What does this print?

double n = 1234567.891;
var us = NumberFormat.getInstance(Locale.US);
var de = NumberFormat.getInstance(
    Locale.GERMANY);
System.out.println(us.format(n));
System.out.println(de.format(n));
  1. 1,234,567.891 1,234,567.891
  2. 1,234,567.891 1.234.567,891
  3. 1234567.891 1234567,891
Show the answer

The US groups with commas and uses a decimal dot; Germany does the opposite. Always format numbers for humans with the user's Locale.

Case is cultural

Turkish has dotted İ/i and dotless I/ı. So in Turkish, lowercase of I is **ı** and uppercase of i is **İ. toUpperCase()/toLowerCase() with no argument use the default locale** — the user's machine decides!

"TITLE".toLowerCase(Locale.ROOT);
// title
"TITLE".toLowerCase(Locale.of("tr"));
// tıtle
🔮 Predict it

The Turkish test

What does this print?

var tr = Locale.of("tr");
System.out.println(
    "quit".toUpperCase(tr).equals("QUIT"));
System.out.println("quit"
    .toUpperCase(Locale.ROOT).equals("QUIT"));
  1. true true
  2. false true
  3. false false
Show the answer

false — in Turkish, "quit" uppercases to "QUİT" with a dotted İ. **Locale.ROOT** gives the neutral, machine-friendly QUIT.

Matching a command

✗ Depends on the user's locale
if (cmd.toUpperCase().equals("QUIT")) {
    exit();
}

On a Turkish machine this never matches.

✓ Locale-neutral
if (cmd.toUpperCase(Locale.ROOT)
        .equals("QUIT")) {
    exit();
}

Use Locale.ROOT (or equalsIgnoreCase) for keys, IDs and commands.

Translating the UI

**ResourceBundle** keeps translations out of code: put texts in messages_en.properties, messages_ar.properties… and ResourceBundle.getBundle("messages", locale) picks the right file, falling back to the default messages.properties.

var b = ResourceBundle.getBundle(
    "messages", Locale.of("ar"));
String label = b.getString("button.save");
💼 In the real world

Global apps

The "Turkey test" is a well-known checklist item for i18n bugs: keys and enum names lowercased with the default locale break only in Turkey or Azerbaijan. Apps serving English and Arabic users rely on ResourceBundles and Locale-aware formatting for every number and date.

Key takeaways

  1. toUpperCase()/toLowerCase() use the default locale
  2. Use toLowerCase(Locale.ROOT) for keys, IDs and commands
  3. NumberFormat.getInstance(locale) formats 1,234.5 vs 1.234,5
  4. ResourceBundle loads messages_xx.properties per locale
🤯 Did you know?

Turkish is not alone: Azerbaijani has the same dotted/dotless i rules, so Java treats both languages specially in case conversion.

Practice questions

What does this print?

var us = NumberFormat.getInstance(Locale.US);
var de = NumberFormat.getInstance(Locale.GERMANY);
System.out.println(us.format(1234.5));
System.out.println(de.format(1234.5));
  1. 1,234.5 1,234.5
  2. 1234.5 1234,5
  3. 1,234.5 1.234,5
  4. 1.234,5 1,234.5
Check your answer

1,234.5 1.234,5. The US uses a comma for thousands and a dot for decimals; Germany does the opposite.

What does this print?

String s = "TITLE";
System.out.println(s.toLowerCase(Locale.ROOT));
System.out.println(s.toLowerCase(Locale.of("tr")));
  1. title title
  2. title tıtle
  3. tıtle tıtle
  4. TITLE title
Check your answer

title tıtle. Turkish has dotted and dotless i. Its lowercase of "I" is the dotless "ı", so the second line differs. Locale.ROOT gives the neutral result.

Next world — I/O, files and networking: how does Java read a file, and why can two programs disagree about the letter "é"?