Internationalization (i18n) and Localization (l10n) in Java
A program written on a computer with English settings ships to a user in Germany, and String.format("%.2f", 1234.5) suddenly returns "1234,50" instead of "1234.50". The next call to Double.parseDouble on that string fails with a NumberFormatException. To avoid surprises like this, an application has to be prepared in advance for different languages and regions. That is what internationalization and localization are for.
Internationalization (i18n) is designing an application so that adding a new language doesn't require recompiling and rebuilding it. UI texts are stored outside the code, in properties files loaded at runtime, and number, date and currency formats come from the current locale instead of being hard-coded.
Localization (l10n) is adapting an application to a specific language and region: translating the UI and documentation, and setting up date, time, currency and number formats and other regional conventions.
What i18n and l10n mean
i18n is an abbreviation of the word internationalization: the first letter i, the last letter n, and 18 letters in between. l10n shortens localization the same way: l, 10 letters, n. Such abbreviations are called numeronyms. Developers came up with them to avoid typing long words in full (you will also see the spelling internationalisation in British English, which gives the same i18n).
| Abbreviation | Full word | How it is built | Meaning |
|---|---|---|---|
| i18n | internationalization | i + 18 letters + n | Preparing the code for different languages |
| l10n | localization | l + 10 letters + n | Adapting the application to a specific language and region |
| g11n | globalization | g + 11 letters + n | i18n and l10n together |
| L10n | localization | Same as l10n | The capital L is sometimes used so it isn't confused with the digit 1 |
In Java, one set of classes covers both tasks, and they are tied together by the locale: a java.util.Locale object, which is a pair of language and region such as en-US or de-DE.
Internationalization vs localization
Internationalization (i18n) is done once, during development: the programmer prepares the code so that it can be translated. Localization (l10n) is done for each language separately: a translator or developer adds translations and regional settings without touching the code. Without i18n, localization turns into rewriting the program.
| Aspect | Internationalization (i18n) | Localization (l10n) |
|---|---|---|
| What it is | Making the code independent of language and region | Adapting the application to a specific language and region |
| When | Once, at design and development time | For every new language or market |
| Who does it | The developer | A translator, a developer, a regional specialist |
| Requires a code change | Yes: texts and formats are moved out of the code once | No: you only add a file with translations |
| Java example | Texts moved to a ResourceBundle, dates formatted with DateTimeFormatter and a locale | A new messages_de_DE.properties file with German strings |
What changes from locale to locale
A locale affects more than the interface language. It changes number separators, the order of date parts, month names, the currency symbol and its position, the first day of the week and the sort order of strings. The table shows how the same data looks in three locales with the standard Java 11+ formatters.
| What | en-US | de-DE | ru-RU | Java class |
|---|---|---|---|---|
| The number 1234.5 | 1,234.5 | 1.234,5 | 1 234,5 (non-breaking space) | NumberFormat |
| An amount of 1234.5 in the local currency | $1,234.50 | 1.234,50 € | 1 234,50 ₽ | NumberFormat.getCurrencyInstance |
| March 8, 2026 (LONG style) | March 8, 2026 | 8. März 2026 | 8 марта 2026 г. | DateTimeFormatter |
| First day of the week | Sunday | Monday | Monday | WeekFields.of(locale) |
| Greeting in the UI | Welcome! | Willkommen! | Добро пожаловать! | ResourceBundle |
Why the output differs from Java 8
Since Java 9, locale data comes from Unicode CLDR by default (JEP 252) instead of the old JRE tables. So the same formatter can produce different strings in Java 8 and Java 11+: another currency symbol, another grouping space, a different suffix after the year. Don't compare formatted strings in tests if they were recorded on a different JDK version.
Java tools for internationalization
Internationalization in Java needs no third-party libraries: the standard library has everything. The foundation is Locale (which language and region) and ResourceBundle (where the translations live). The other classes format numbers, dates and messages by the rules of the chosen locale. This lesson only shows what each class is for, without going into its API.
| Class | Package | What it is for |
|---|---|---|
Locale | java.util | Describes a language and region; passed to every formatter |
ResourceBundle | java.util | Loads translations from .properties files for the right locale |
NumberFormat | java.text | Numbers, currencies and percentages by locale rules |
DateTimeFormatter | java.time.format | Dates and times: ofLocalizedDate(FormatStyle) + withLocale (example below) |
MessageFormat | java.text | Templates with parameters such as "Found {0} files", where the translation decides the word order |
Collator | java.text | Sorting strings by the alphabet of a specific language (é, ä, ö) |
String.format(Locale, ...) | java.lang | String formatting with an explicitly given locale |
Frameworks build on the same ideas. For example, Spring Boot internationalization is based on MessageSource, which reads the same messages_xx.properties files and picks the language from the Accept-Language header of the request.
How to internationalize an application: 4 steps
Internationalizing a Java application comes down to four steps. Move all UI strings out of the code into properties files. Set the user's locale explicitly. Load translations through ResourceBundle. Print numbers and dates only through formatters that use that locale.
- Extract the texts. Instead of
System.out.println("Welcome!"), use a key such asmessage.welcomedefined inmessages.properties,messages_de_DE.propertiesand so on. - Determine the locale. Take it from user settings, an HTTP request header or
Locale.getDefault(). - Load the translations.
ResourceBundle.getBundle("messages", locale)finds the matching file itself. If there is no file for that locale, it falls back to a more general one. - Format with the locale. Print numbers, currencies and dates through
NumberFormat,DateTimeFormatterandMessageFormatwith the same locale instead of concatenating strings by hand.
Here is a Java localization example for dates: the same LocalDate is printed in three locales without changing the code.
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
public class LocalizedDateExample {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 3, 8);
Locale[] locales = {Locale.US, Locale.GERMANY, Locale.forLanguageTag("ru-RU")};
for (Locale locale : locales) {
DateTimeFormatter formatter = DateTimeFormatter
.ofLocalizedDate(FormatStyle.LONG)
.withLocale(locale);
System.out.println(locale.toLanguageTag() + ": " + date.format(formatter));
}
}
}
// Output (Java 11+):
// en-US: March 8, 2026
// de-DE: 8. März 2026
// ru-RU: 8 марта 2026 г. To add French, just put Locale.FRANCE into the array. No format strings or month names appear in your code: the JDK supplies them. That is exactly what internationalization buys you.
What changed in modern Java versions
Since Java 9, ResourceBundle reads properties files as UTF-8 (JEP 226), so non-Latin text no longer has to be converted to \uXXXX escapes with the native2ascii tool. The constructors such as new Locale("de", "DE") are deprecated since Java 19. Use Locale.of("de", "DE") or Locale.forLanguageTag("de-DE") instead.
Where the default locale breaks your code
If you don't pass a locale, many Java methods use Locale.getDefault(), that is, the settings of the operating system or the JVM. As a result, the same code behaves differently on a developer's machine and on a server. Process internal data (keys, identifiers, file formats) with Locale.ROOT, and apply the user's locale only to text the user will see.
Mistake 1: a number that can't be read back. This is the example from the start of the lesson.
Locale.setDefault(Locale.GERMANY);
String s = String.format("%.2f", 1234.5);
System.out.println(s); // Output: 1234,50
double d = Double.parseDouble(s); // NumberFormatException: For input string: "1234,50"
// Fix: pass Locale.ROOT explicitly for machine-readable output
String fixed = String.format(Locale.ROOT, "%.2f", 1234.5);
System.out.println(fixed); // Output: 1234.50
System.out.println(Double.parseDouble(fixed)); // Output: 1234.5 Mistake 2: what does this code print?
Locale tr = Locale.forLanguageTag("tr-TR");
System.out.println("title".toUpperCase(tr)); // Output: TİTLE
System.out.println("TITLE".toLowerCase(tr)); // Output: tıtle
System.out.println("title".toUpperCase(Locale.ROOT)); // Output: TITLE In Turkish, the lowercase i has a dotted uppercase form (İ), and the uppercase I has a dotless lowercase form (ı). If the application runs with the Turkish default locale, "title".toUpperCase() without arguments returns "TİTLE". Then an equals("TITLE") comparison, a Map key lookup or command parsing quietly stops working. For keys, enum names and protocols, use toUpperCase(Locale.ROOT) or equalsIgnoreCase.
Frequently Asked Questions
Which comes first, i18n or l10n?
i18n comes first. You prepare the code for translation once: extract the texts and format data with a locale. Then you repeat l10n for each language, adding only translation files. If you start with localization in unprepared code, you will have to find and replace strings across the whole project. See the comparison in the section Internationalization vs localization.
What do a11y and g11n mean, and how are they related to i18n?
They are numeronyms built the same way as i18n. g11n (globalization) is i18n and l10n together, that is, preparing a product for the world market. a11y (accessibility, 11 letters between a and y) means making the product usable for people with disabilities. It is a separate task, but it is often done together with i18n: screen readers, for example, also rely on the UI language. How numeronyms are built is explained in the section What i18n and l10n mean.
Which locale does Java use if I don't specify one?
The default locale, Locale.getDefault(). The JVM determines it at startup from the operating system settings. You can override it with the launch options -Duser.language=en -Duser.country=US or by calling Locale.setDefault(...). Since Java 7, the default is set separately for display (Locale.Category.DISPLAY) and for formatting (Locale.Category.FORMAT).
Do I need to recompile a Java application to add a new language?
Not if i18n is done correctly. Put a new file with the same keys on the classpath, for example messages_fr_FR.properties, and ResourceBundle.getBundle will pick it up for the French locale. The JDK already knows number and date formats. Recompilation is only needed when texts are hard-coded.
How does Spring Boot choose the user's language?
Through a LocaleResolver bean. By default it is AcceptHeaderLocaleResolver, which takes the locale from the browser's Accept-Language header. To let users switch the language with a parameter such as ?lang=de, register a SessionLocaleResolver or CookieLocaleResolver together with a LocaleChangeInterceptor. The texts are then loaded by MessageSource from the same properties files as in plain Java (see Java tools for internationalization).
Comments