Locale & internationalization in Java
Locale-sensitive formatting, toUpperCase(Locale.ROOT), NumberFormat, ResourceBundle.
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.
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,234,567.891 1,234,567.8911,234,567.891 1.234.567,8911234567.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ıtleThe 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"));true truefalse truefalse 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
if (cmd.toUpperCase().equals("QUIT")) {
exit();
}On a Turkish machine this never matches.
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");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
- toUpperCase()/toLowerCase() use the default locale
- Use toLowerCase(Locale.ROOT) for keys, IDs and commands
- NumberFormat.getInstance(locale) formats 1,234.5 vs 1.234,5
- ResourceBundle loads messages_xx.properties per locale
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,234.5 1,234.5
- 1234.5 1234,5
- 1,234.5 1.234,5
- 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")));- title title
- title tıtle
- tıtle tıtle
- 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.