Internationalization (i18n)

👉 Complete Guide to Mastering i18n using Resource Bundles, Locale Resolution, and Message Handling in Spring Boot.


What is i18n ?

i18n or Internationalization, is the process of designing your application to adapt to various languages and regions without requiring application changes.

In a Spring Boot microservice, this is crucial for creating a global user experience. Spring’s MessageSource abstraction simplifies this process by resolving messages (text strings) based on a user’s locale.


Table of Contents


Maven Dependencies

The necessary API for i18n, primarily the MessageSource interface, is part of the spring-context dependency. Which is included as part of standard web dependency spring-boot-starter-web.

<!-- pom.xml-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

How to enable i18n ?

The implementation involves autowiring the MessageSource bean, which Spring Boot auto-configures. The key method is getMessage(), where we provide the message key, optional arguments, a default message, and the desired Locale.

The Locale is typically extracted from the incoming HTTP request’s Accept-Language header using LocaleContextHolder.getLocale().

Controller

import java.util.Locale;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloWorldI18n {

    private final MessageSource messageSource;

    // Use constructor injection for MessageSource (Best Practice)
    public HelloWorldI18n(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @GetMapping("/say-hello-i18n")
    public String sayHello() {

        // Get the Locale from the current HTTP Request's Accept-Language header
        var locale = LocaleContextHolder.getLocale();

        // Resolve the message key based on the determined locale
        return this.messageSource.getMessage(
                "good.morning", // The message key
                null,           // Arguments for parametric messages
                "Default - Good Morning", // Fallback message if key not found
                locale          // The target locale
        );
    }
}

Resource Files for Messages

Messages are stored in standard Java .properties files following the naming convention: messages[-<locale>].properties.

Spring Boot automatically looks for these files in the classpath (typically in src/main/resources/).

File Structure

File NameLocaleDescription
messages.propertiesDefault (Fallback)Used when no specific locale file is found.
messages_en.propertiesEnglish (US/UK)Used for English-speaking users.
messages_es.propertiesSpanishUsed for Spanish-speaking users.
messages_de.propertiesGermanUsed for German-speaking users. (Note: ger is non-standard)

Message Content

messages.properties

good.morning=Good Morning (Default)

messages_es.properties

good.morning=Buenos días

Project Reference:
Refer the application source code on Github: Internationalization POC App


How Locale is Determined ?

This mechanism relies on Spring reading the value of the Accept-Language HTTP Header sent by the client (e.g., a browser or another microservice). Based on this header (e.g., es, de-DE, en-US), Spring finds the most appropriate messages[-<locale>].properties file.

The default message file prefix is messages and the suffix is .properties. This can be customized in application.properties using spring.messages.basename.

HTTP Request Header

Header:Accept-Language: es
Action: Spring resolves the message from messages_es.properties.


Message Resolution Flow

Below diagram illustrates how a request’s locale determines the message resolution.

sequenceDiagram
    participant C as Client (e.g., Browser)
    participant A as Spring Boot Microservice
    participant MS as MessageSource (Internal)
    participant RF as Resource Files (.properties)

    C->>A: HTTP GET /say-hello-i18n (Header: Accept-Language: es)
    A->>A: Get Locale from LocaleContextHolder
    A->>MS: messageSource.getMessage("good.morning", ..., locale)
    MS->>RF: Look for messages_es.properties
    alt File Found
        RF-->>MS: Return message "Buenos días"
    else File Not Found
        RF-->>MS: Look for messages.properties (Default)
        RF-->>MS: Return message "Good Morning (Default)"
    end
    MS-->>A: Resolved Message
    A-->>C: HTTP Response (Body: Resolved Message)

Benefits and Drawbacks

AspectBenefits 👍Drawbacks 👎
AccessibilityWider market reach and better user experience for global users.Maintenance Overhead for translating and managing numerous resource files.
CodeClean separation of text content from the Java code (no hardcoded strings).Requires discipline to use message keys everywhere, not just hardcode strings.
ScalabilityEasy to add new languages by simply adding a new .properties file.Context Loss during translation; translators might lack context without proper comments/guidance.

Best practices

For a robust, production-ready microservice implementation, consider these best practices.

  1. Custom Base Name: Define the base name explicitly in application.properties to ensure future-proofing and clarity.

    spring.messages.basename=i18n/messages
    # This would require placing files inside `src/main/resources/i18n/`.
    
  2. Parametric Messages: Use placeholders in messages for dynamic content.
    • messages_en.properties:good.morning.user=Buenos días, {0}
    • Then pass the user from getMessage API as argument:
      • messageSource.getMessage("good.morning.user", new Object[]{"Vivek"}, ...)
  3. Externalizing MessageSource: For large microservice deployments or applications requiring real-time updates, consider implementing a custom MessageSource that loads messages from a centralized database or a configuration service (e.g., Consul, Spring Cloud Config).