Course topics

By WebNest Studio

Spring Boot Tutorial

Internationalization (i18n) in Spring Boot

India alone has dozens of languages, and a platform that serves learners in English, Hindi and Tamil reaches far more people. Internationalization (i18n) means designing your application so text, dates and numbers can be shown in the user's language and format without code changes; localization (l10n) is adding the actual translations.

Spring Boot supports i18n through MessageSource and locale resolution. This lesson covers message bundles, choosing the locale from the Accept-Language header or a user preference, translating API messages and validation errors, using messages in Thymeleaf, and formatting numbers, currencies and dates per locale.

Message Bundles

Put default texts in src/main/resources/messages.properties and translations in files named by locale: messages_hi.properties, messages_ta.properties. Spring Boot auto-configures a MessageSource when messages.properties exists. Keys are shared; values can contain placeholders {0}, {1} filled at runtime. Save files as UTF-8 (Spring Boot reads them as UTF-8 by default via spring.messages.encoding).

Resolving the Locale

A LocaleResolver decides the current locale for each request. Spring Boot's default, AcceptHeaderLocaleResolver, uses the browser's Accept-Language header — ideal for REST APIs. For web apps where users pick a language, use a CookieLocaleResolver or SessionLocaleResolver with a LocaleChangeInterceptor that reads ?lang=hi. In controllers and services, LocaleContextHolder.getLocale() or a Locale method parameter gives you the current locale.

Translating Validation and Error Messages

Bean Validation messages can reference bundle keys: @NotBlank(message = "{student.name.required}"). Spring's LocalValidatorFactoryBean uses the same MessageSource, so validation errors come back in the user's language. ProblemDetail responses can also be localised: Spring MVC looks up keys such as problemDetail.title.<exception class> in the message source.

Formatting Numbers, Currency and Dates

Translation is only half of i18n. The number one lakh twenty thousand is written 1,20,000.00 in India and 120,000.00 in the US; dates differ too. Use NumberFormat.getCurrencyInstance(locale) and DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale), or Thymeleaf's #numbers and #temporals helpers, rather than hand-built strings. Store dates in UTC and convert for display.

Examples

Message bundles and configuration

Java
# src/main/resources/messages.properties (default, English)
greeting=Welcome, {0}!
course.enrolled=You are enrolled in {0}.
student.name.required=Name is required.

# messages_hi.properties (Hindi)
greeting=स्वागत है, {0}!
course.enrolled=आपने {0} में दाख़िला ले लिया है।
student.name.required=नाम आवश्यक है।

# messages_ta.properties (Tamil)
greeting=வரவேற்கிறோம், {0}!
course.enrolled=நீங்கள் {0} இல் சேர்ந்துள்ளீர்கள்.
student.name.required=பெயர் தேவை.

# application.properties
spring.messages.basename=messages
spring.messages.fallback-to-system-locale=false
Output
(Spring Boot auto-configures a MessageSource from these files.)

A REST endpoint that answers in the Accept-Language locale

Java
@RestController
public class GreetingController {

    private final MessageSource messages;

    public GreetingController(MessageSource messages) {
        this.messages = messages;
    }

    @GetMapping("/api/greeting")
    public Map<String, String> greet(@RequestParam String name, Locale locale) {
        return Map.of("message", messages.getMessage("greeting", new Object[] {name}, locale));
    }
}
Output
curl -H "Accept-Language: en" "localhost:8080/api/greeting?name=Asha" -> {"message":"Welcome, Asha!"}
curl -H "Accept-Language: hi" "localhost:8080/api/greeting?name=Asha" -> {"message":"स्वागत है, Asha!"}
curl -H "Accept-Language: ta" "localhost:8080/api/greeting?name=Asha" -> {"message":"வரவேற்கிறோம், Asha!"}
curl -H "Accept-Language: fr" "localhost:8080/api/greeting?name=Asha" -> {"message":"Welcome, Asha!"}   (fallback to default bundle)

Localised validation errors and a user-selectable language for web pages

Java
public record StudentRequest(@NotBlank(message = "{student.name.required}") String name) {}

// POST /api/students with Accept-Language: hi and an empty name
// -> 400 with "नाम आवश्यक है।"

@Configuration
public class LocaleConfig implements WebMvcConfigurer {

    @Bean
    LocaleResolver localeResolver() {
        CookieLocaleResolver resolver = new CookieLocaleResolver("lang");
        resolver.setDefaultLocale(Locale.ENGLISH);
        return resolver;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");               // /courses?lang=hi switches and remembers
        registry.addInterceptor(interceptor);
    }
}

<!-- Thymeleaf -->
<h1 th:text="#{greeting(${user.name})}">Welcome!</h1>
<a th:href="@{''(lang=en)}">English</a> | <a th:href="@{''(lang=hi)}">हिन्दी</a>
Output
GET /courses?lang=hi -> page rendered in Hindi, cookie lang=hi set
GET /courses          -> still Hindi (read from cookie)

Locale-aware number, currency and date formatting

Java
BigDecimal price = new BigDecimal("120000.50");
LocalDate date = LocalDate.of(2026, 9, 27);

for (Locale locale : List.of(Locale.of("en", "IN"), Locale.of("hi", "IN"), Locale.US, Locale.GERMANY)) {
    NumberFormat money = NumberFormat.getCurrencyInstance(locale);
    money.setCurrency(Currency.getInstance("INR"));
    String when = date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale));
    System.out.println(locale + " -> " + money.format(price) + " | " + when);
}
Output
en_IN -> ₹1,20,000.50 | 27-Sept-2026
hi_IN -> ₹1,20,000.50 | 27 सित॰ 2026
en_US -> ₹120,000.50 | Sep 27, 2026
de_DE -> 120.000,50 ₹ | 27.09.2026

Common Mistakes

  • Hard-coding user-facing strings in Java code or templates instead of message keys.
  • Saving translation files in a non-UTF-8 encoding, producing garbled Hindi or Tamil text.
  • Concatenating translated fragments ("You have " + n + " courses") instead of using placeholders, which breaks word order in other languages.
  • Formatting currency and dates manually with a fixed pattern that is wrong for most locales.
  • Leaving fallback-to-system-locale enabled, so the server's OS language unexpectedly becomes the fallback.

Key Points to Remember

  • messages.properties plus messages_<locale>.properties hold translations; Spring Boot auto-configures MessageSource.
  • AcceptHeaderLocaleResolver (default) suits APIs; Cookie/Session resolvers with LocaleChangeInterceptor suit web apps.
  • Use {key} references in validation messages to localise errors.
  • Use #{...} in Thymeleaf and MessageSource.getMessage in code, with placeholders for dynamic values.
  • Format numbers, currency and dates with locale-aware formatters.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.