eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

1. Overview

Internationalization is a process of preparing an application to support various linguistic, regional, cultural or political-specific data. It is an essential aspect of any modern multi-language application.

For further reading, we should know that there’s a very popular abbreviation (probably more popular than the actual name) for internationalization – i18n due to the 18 letters between ‘i’ and ‘n’.

It’s crucial for present-day enterprise programs to serve people from different parts of the world or multiple cultural areas. Distinct cultural or language regions don’t only determine language-specific descriptions but also currency, number representation and even divergent date and time composition.

For instance, let’s focus on country-specific numbers. They have various decimal and thousand separators:

  • 102,300.45 (United States)
  • 102 300,45 (Poland)
  • 102.300,45 (Germany)

There are different date formats as well:

  • Monday, January 1, 2018 3:20:34 PM CET (United States)
  • lundi 1 janvier 2018 15 h 20 CET (France).
  • 2018年1月1日 星期一 下午03时20分34秒 CET (China)

What’s more, different countries have unique currency symbols:

  • £1,200.60 (United Kingdom)
  • € 1.200,60 (Italy)
  • 1 200,60 € (France)
  • $1,200.60 (United States)

An important fact to know is that even if countries have the same currency and currency symbol – like France and Italy – the position of their currency symbol could be different.

2. Localization

Within Java, we have a fantastic feature at our disposal called the Locale class.

It allows us to quickly differentiate between cultural locales and format our content appropriately. It’s essential for within the internationalization process. The same as i18n, Localization also has its abbreviation – l10n.

The main reason for using Locale is that all required locale-specific formatting can be accessed without recompilation. An application can handle multiple locales at the same time so supporting new language is straightforward.

Locales are usually represented by language, country, and variant abbreviation separated by an underscore:

  • de (German)
  • it_CH (Italian, Switzerland)
  • en_US_UNIX (United State, UNIX platform)

2.1. Fields

We have already learned that Locale consists of language code, country code, and variant. There are two more possible fields to set: script and extensions.

Let’s have a look through a list of fields and see what the rules are:

  • Language can be an ISO 639 alpha-2 or alpha-3 code or registered language subtag.
  • Region (Country) is ISO 3166 alpha-2 country code or UN numeric-3 area code.
  • Variant is a case-sensitive value or set of values specifying a variation of a Locale.
  • Script must be a valid ISO 15924 alpha-4 code.
  • Extensions is a map which consists of single character keys and String values.

The IANA Language Subtag Registry contains possible values for language, region, variant and script.

There is no list of possible extension values, but the values must be well-formed BCP-47 subtags. The keys and values are always converted to lower case.

2.2. Locale.Builder

There are several ways of creating Locale objects. One possible way utilizes Locale.Builder. Locale.Builder has five setter methods which we can use to build the object and at the same time validate those values:

Locale locale = new Locale.Builder()
  .setLanguage("fr")
  .setRegion("CA")
  .setVariant("POSIX")
  .setScript("Latn")
  .build();

The String representation of the above Locale is fr_CA_POSIX_#Latn.

It’s good to know that setting ‘variant’ may be a little bit tricky as there’s no official restriction on variant values, although the setter method requires it to be BCP-47 compliant.

Otherwise, it will throw IllformedLocaleException.

In the case where we need to use a value that doesn’t pass validation, we can use Locale constructors as they don’t validate values.

2.3. Constructors

Locale has three constructors:

  • new Locale(String language)
  • new Locale(String language, String country)
  • new Locale(String language, String country, String variant)

A 3-parameter constructor:

Locale locale = new Locale("pl", "PL", "UNIX");

A valid variant must be a String of 5 to 8 alphanumerics or single numeric followed by 3 alphanumerics. We can only apply “UNIX” to the variant field only via constructor as it doesn’t meet those requirements.

However, there’s one drawback of using constructors to create Locale objects – we can’t set extensions and script fields.

2.4. Constants

This is probably the simplest and the most limited way of getting Locales. The Locale class has several static constants which represent the most popular country or language:

Locale japan = Locale.JAPAN;
Locale japanese = Locale.JAPANESE;

2.5. Language Tags

Another way of creating Locale is calling the static factory method forLanguageTag(String languageTag). This method requires a String that meets the IETF BCP 47 standard.

This is how we can create the UK Locale:

Locale uk = Locale.forLanguageTag("en-UK");

2.6. Available Locales

Even though we can create multiple combinations of Locale objects, we may not be able to use them.

An important note to be aware of is that the Locales on a platform are dependent on those that have been installed within the Java Runtime.

As we use Locales for formatting, the different formatters may have an even smaller set of Locales available that are installed in the Runtime.

Let’s check how to retrieve arrays of available locales:

Locale[] numberFormatLocales = NumberFormat.getAvailableLocales();
Locale[] dateFormatLocales = DateFormat.getAvailableLocales();
Locale[] locales = Locale.getAvailableLocales();

After that, we can check whether our Locale resides among available Locales.

We should remember that the set of available locales is different for various implementations of the Java Platform and various areas of functionality.

The complete list of supported locales is available on the Oracle’s Java SE Development Kit webpage.

2.7. Default Locale

While working with localization, we might need to know what the default Locale on our JVM instance is. Fortunately, there’s a simple way to do that:

Locale defaultLocale = Locale.getDefault();

Also, we can specify a default Locale by calling a similar setter method:

Locale.setDefault(Locale.CANADA_FRENCH);

It’s especially relevant when we’d like to create JUnit tests that don’t depend on a JVM instance.

3. Numbers and Currencies

This section refers to numbers and currencies formatters that should conform to different locale-specific conventions.

To format primitive number types (int, double) as well as their object equivalents (Integer, Double), we should use NumberFormat class and its static factory methods.

Two methods are interesting for us:

  • NumberFormat.getInstance(Locale locale)
  • NumberFormat.getCurrencyInstance(Locale locale)

Let’s examine a sample code:

Locale usLocale = Locale.US;
double number = 102300.456d;
NumberFormat usNumberFormat = NumberFormat.getInstance(usLocale);

assertEquals(usNumberFormat.format(number), "102,300.456");

As we can see it’s as simple as creating Locale and using it to retrieve NumberFormat instance and formatting a sample number. We can notice that the output includes locale-specific decimal and thousand separators.

Here’s another example:

Locale usLocale = Locale.US;
BigDecimal number = new BigDecimal(102_300.456d);

NumberFormat usNumberFormat = NumberFormat.getCurrencyInstance(usLocale); 
assertEquals(usNumberFormat.format(number), "$102,300.46");

Formatting a currency involves the same steps as formatting a number. The only difference is that the formatter appends currency symbol and round decimal part to two digits.

4. Date and Time

Now, we’re going to learn about dates and times formatting which’s probably more complex than formatting numbers.

First of all, we should know that date and time formatting significantly changed in Java 8 as it contains completely new Date/Time API. Therefore, we’re going to look through different formatter classes.

4.1. DateTimeFormatter

Since Java 8 was introduced, the main class for localizing of dates and times is the DateTimeFormatter class. It operates on classes that implement TemporalAccessor interface, for example, LocalDateTime, LocalDate, LocalTime or ZonedDateTime. To create a DateTimeFormatter we must provide at least a pattern, and then Locale. Let’s see an example code:

Locale.setDefault(Locale.US);
LocalDateTime localDateTime = LocalDateTime.of(2018, 1, 1, 10, 15, 50, 500);
String pattern = "dd-MMMM-yyyy HH:mm:ss.SSS";

DateTimeFormatter defaultTimeFormatter = DateTimeFormatter.ofPattern(pattern);
DateTimeFormatter deTimeFormatter = DateTimeFormatter.ofPattern(pattern, Locale.GERMANY);

assertEquals(
  "01-January-2018 10:15:50.000", 
  defaultTimeFormatter.format(localDateTime));
assertEquals(
  "01-Januar-2018 10:15:50.000", 
  deTimeFormatter.format(localDateTime));

We can see that after retrieving DateTimeFormatter all we have to do is to call the format() method.

For a better understanding, we should familiarize with possible pattern letters.

Let’s look at letters for example:

Symbol  Meaning                     Presentation      Examples
  ------  -------                     ------------      -------
   y       year-of-era                 year              2004; 04
   M/L     month-of-year               number/text       7; 07; Jul; July; J
   d       day-of-month                number            10

   H       hour-of-day (0-23)          number            0
   m       minute-of-hour              number            30
   s       second-of-minute            number            55
   S       fraction-of-second          fraction          978

All possible pattern letters with explanation can be found in the Java documentation of DateTimeFormatter. It’s worth to know that final value depends on the number of symbols. There is ‘MMMM’ in the example which prints the full month name whereas a single ‘M’ letter would give the month number without a leading 0.

To finish on DateTimeFormatter, let’s see how we can format LocalizedDateTime:

LocalDateTime localDateTime = LocalDateTime.of(2018, 1, 1, 10, 15, 50, 500);
ZoneId losAngelesTimeZone = TimeZone.getTimeZone("America/Los_Angeles").toZoneId();

DateTimeFormatter localizedTimeFormatter = DateTimeFormatter
  .ofLocalizedDateTime(FormatStyle.FULL);
String formattedLocalizedTime = localizedTimeFormatter.format(
  ZonedDateTime.of(localDateTime, losAngelesTimeZone));

assertEquals("Monday, January 1, 2018 10:15:50 AM PST", formattedLocalizedTime);

In order to format LocalizedDateTime, we can use the ofLocalizedDateTime(FormatStyle dateTimeStyle) method and provide a predefined FormatStyle.

For a more in-depth look at Java 8 Date/Time API, we have an existing article here.

4.2. DateFormat and SimpleDateFormatter

As it’s still common to work on projects that make use of Dates and Calendars, we’ll briefly introduce capabilities of formatting dates and times with DateFormat and SimpleDateFormat classes.

Let’s analyze abilities of the first one:

GregorianCalendar gregorianCalendar = new GregorianCalendar(2018, 1, 1, 10, 15, 20);
Date date = gregorianCalendar.getTime();

DateFormat ffInstance = DateFormat.getDateTimeInstance(
  DateFormat.FULL, DateFormat.FULL, Locale.ITALY);
DateFormat smInstance = DateFormat.getDateTimeInstance(
  DateFormat.SHORT, DateFormat.MEDIUM, Locale.ITALY);

assertEquals("giovedì 1 febbraio 2018 10.15.20 CET", ffInstance.format(date));
assertEquals("01/02/18 10.15.20", smInstance.format(date));

DateFormat works with Dates and has three useful methods:

  • getDateTimeInstance
  • getDateInstance
  • getTimeInstance

All of them take predefined values of DateFormat as a parameter. Each method is overloaded, so passing Locale is possible as well. If we want to use a custom pattern, as it’s done in DateTimeFormatter, we can use SimpleDateFormat. Let’s see a short code snippet:

GregorianCalendar gregorianCalendar = new GregorianCalendar(
  2018, 1, 1, 10, 15, 20);
Date date = gregorianCalendar.getTime();
Locale.setDefault(new Locale("pl", "PL"));

SimpleDateFormat fullMonthDateFormat = new SimpleDateFormat(
  "dd-MMMM-yyyy HH:mm:ss:SSS");
SimpleDateFormat shortMonthsimpleDateFormat = new SimpleDateFormat(
  "dd-MM-yyyy HH:mm:ss:SSS");

assertEquals(
  "01-lutego-2018 10:15:20:000", fullMonthDateFormat.format(date));
assertEquals(
  "01-02-2018 10:15:20:000" , shortMonthsimpleDateFormat.format(date));

5. Customization

Due to some good design decisions, we’re not tied to a locale-specific formatting pattern, and we can configure almost every detail to be fully satisfied with an output.

To customize number formatting, we can use DecimalFormat and DecimalFormatSymbols.

Let’s consider a short example:

Locale.setDefault(Locale.FRANCE);
BigDecimal number = new BigDecimal(102_300.456d);

DecimalFormat zeroDecimalFormat = new DecimalFormat("000000000.0000");
DecimalFormat dollarDecimalFormat = new DecimalFormat("$###,###.##");

assertEquals(zeroDecimalFormat.format(number), "000102300,4560");
assertEquals(dollarDecimalFormat.format(number), "$102 300,46");

The DecimalFormat documentation shows all possible pattern characters. All we need to know now is that “000000000.000” determines leading or trailing zeros, ‘,’ is a thousand separator, and ‘.’ is decimal one.

It’s also possible to add a currency symbol. We can see below that the same result can be achieved by using DateFormatSymbol class:

Locale.setDefault(Locale.FRANCE);
BigDecimal number = new BigDecimal(102_300.456d);

DecimalFormatSymbols decimalFormatSymbols = DecimalFormatSymbols.getInstance();
decimalFormatSymbols.setGroupingSeparator('^');
decimalFormatSymbols.setDecimalSeparator('@');
DecimalFormat separatorsDecimalFormat = new DecimalFormat("$###,###.##");
separatorsDecimalFormat.setGroupingSize(4);
separatorsDecimalFormat.setCurrency(Currency.getInstance(Locale.JAPAN));
separatorsDecimalFormat.setDecimalFormatSymbols(decimalFormatSymbols);

assertEquals(separatorsDecimalFormat.format(number), "$10^2300@46");

As we can see, DecimalFormatSymbols class enables us to specify any number formatting we can imagine.

To customize SimpleDataFormat, we can use DateFormatSymbols.

Let’s see how simple is a change of day names:

Date date = new GregorianCalendar(2018, 1, 1, 10, 15, 20).getTime();
Locale.setDefault(new Locale("pl", "PL"));

DateFormatSymbols dateFormatSymbols = new DateFormatSymbols();
dateFormatSymbols.setWeekdays(new String[]{"A", "B", "C", "D", "E", "F", "G", "H"});
SimpleDateFormat newDaysDateFormat = new SimpleDateFormat(
  "EEEE-MMMM-yyyy HH:mm:ss:SSS", dateFormatSymbols);

assertEquals("F-lutego-2018 10:15:20:000", newDaysDateFormat.format(date));

6. Resource Bundles

Finally, the crucial part of internationalization in the JVM is the Resource Bundle mechanism.

The purpose of a ResourceBundle is to provide an application with localized messages/descriptions which can be externalized to the separate files. We cover usage and configuration of the Resource Bundle in one of our previous articles – guide to the Resource Bundle.

7. Conclusion

Locales and the formatters that utilize them are tools that help us create an internationalized application. These tools allow us to create an application which can dynamically adapt to the user’s linguistic or cultural settings without multiple builds or even needing to worry about whether Java supports the Locale.

In a world where a user can be anywhere and speak any language, the ability to apply these changes means our applications can be more intuitive and understandable by more users globally.

When working with Spring Boot applications, we also have a convenient article for Spring Boot Internationalization.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – LS – NPI (cat=Java)
announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

eBook Jackson – NPI EA – 3 (cat = Jackson)