Skip to main content

i18n en Spring Boot: tutorial de configuración de internacionalización

Configure MessageSource, cree archivos properties específicos por región, resuelva configuraciones regionales y renderice plantillas Thymeleaf multilingües; después, automatice las traducciones con IA.

1

Añadir dependencias

Spring Boot Starter Web incluye de serie la configuración automática de MessageSource. Añada Thymeleaf para las plantillas i18n renderizadas en el servidor y el starter de validación para mensajes de error localizados.

Spring Boot configura automáticamente un bean MessageSource que lee messages.properties desde el classpath. Solo necesita una configuración expresa si quiere personalizar el basename, la codificación o el comportamiento de la caché.
pom.xml
<!-- pom.xml — Spring Boot Starter Web includes MessageSource auto-config -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Thymeleaf for server-side rendered templates with i18n -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- Validation (for localized error messages) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2

Configurar MessageSource y LocaleResolver

MessageSource de Spring carga traducciones desde archivos .properties según el basename: messages.properties —predeterminado—, messages_de.properties —alemán— y messages_ja.properties —japonés—. Configure LocaleResolver para decidir qué región utilizar en cada solicitud.

Archivos de traducción

messages.properties
# src/main/resources/messages.properties (default / English)
nav.home=Home
nav.about=About
nav.settings=Settings

greeting=Hello, {0}!
cart.itemCount={0,choice,0#No items|1#1 item|1<{0,number} items}

error.notFound=Page not found
error.serverError=Something went wrong. Please try again.

Configuración de MessageSource

I18nConfig.java
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class I18nConfig {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource source =
            new ReloadableResourceBundleMessageSource();
        source.setBasename("classpath:messages");
        source.setDefaultEncoding("UTF-8");
        source.setCacheSeconds(3600); // reload interval in dev
        return source;
    }

    // Wire MessageSource into Bean Validation
    @Bean
    public LocalValidatorFactoryBean validator(MessageSource messageSource) {
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(messageSource);
        return bean;
    }
}
Si las traducciones devuelven el nombre de la clave en vez del texto, la causa más habitual es un basename incorrecto. El predeterminado es 'messages', que corresponde a messages.properties en el classpath. Si sus archivos tienen otro nombre o están en un subdirectorio, defina expresamente spring.messages.basename.

Resolución de configuraciones regionales

Configure cómo determina Spring la región activa de cada solicitud. CookieLocaleResolver conserva la elección del usuario entre sesiones. LocaleChangeInterceptor permite cambiarla mediante un parámetro de consulta como ?lang=de.

LocaleConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

import java.util.Locale;

@Configuration
public class LocaleConfig implements WebMvcConfigurer {

    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver resolver = new CookieLocaleResolver("lang");
        resolver.setDefaultLocale(Locale.ENGLISH);
        resolver.setCookieMaxAge(3600 * 24 * 365); // 1 year
        return resolver;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang"); // ?lang=de switches locale
        return interceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}
3

Utilizar traducciones en el código

Acceda a los mensajes traducidos en controladores mediante la inyección de MessageSource, en plantillas Thymeleaf con la sintaxis #{...} y en API REST mediante el parámetro Locale resuelto automáticamente.

Controlador con MessageSource

HomeController.java
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Locale;

@Controller
public class HomeController {

    private final MessageSource messageSource;

    public HomeController(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @GetMapping("/")
    public String home(Model model, Locale locale) {
        // Spring injects the resolved Locale automatically
        String greeting = messageSource.getMessage(
            "greeting",
            new Object[]{"World"},
            locale
        );
        model.addAttribute("greeting", greeting);
        return "home";
    }
}

Plantillas Thymeleaf

La expresión #{...} de Thymeleaf resuelve automáticamente las claves de los archivos .properties. Pase parámetros mediante la sintaxis #{key(arg0, arg1)}. La plantilla utiliza la configuración regional resuelta por LocaleResolver.

home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:text="#{nav.home}">Home</title>
</head>
<body>
    <!-- Simple message lookup -->
    <h1 th:text="#{greeting('World')}">Hello, World!</h1>

    <!-- Navigation with i18n -->
    <nav>
        <a href="/" th:text="#{nav.home}">Home</a>
        <a href="/about" th:text="#{nav.about}">About</a>
        <a href="/settings" th:text="#{nav.settings}">Settings</a>
    </nav>

    <!-- Parameterized messages -->
    <p th:text="#{cart.itemCount(3)}">3 items</p>

    <!-- Language switcher -->
    <div>
        <a th:href="@{/(lang=en)}">English</a>
        <a th:href="@{/(lang=de)}">Deutsch</a>
        <a th:href="@{/(lang=ja)}">日本語</a>
    </div>

    <!-- Conditional text based on locale -->
    <p th:if="${#locale.language == 'ja'}"
       th:text="#{greeting('ユーザー')}">
        こんにちは、ユーザーさん!
    </p>
</body>
</html>
Expresiones de Thymeleaf como #{greeting('World')} pasan argumentos a MessageFormat. El texto estático dentro de etiquetas HTML actúa como respaldo al ver la plantilla sin Spring, lo que ayuda a los diseñadores que trabajan directamente con ella.

Localización de API REST

En API REST, Spring resuelve automáticamente Locale a partir de Accept-Language. Inyéctelo como parámetro del método y páselo a MessageSource. Los clientes cambian de idioma enviando otras cabeceras Accept-Language.

ApiController.java
import org.springframework.context.MessageSource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Locale;
import java.util.Map;

@RestController
@RequestMapping("/api")
public class ApiController {

    private final MessageSource messageSource;

    public ApiController(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @GetMapping("/greeting/{name}")
    public ResponseEntity<Map<String, String>> greeting(
            @PathVariable String name,
            Locale locale) {  // Resolved from Accept-Language header
        String msg = messageSource.getMessage(
            "greeting", new Object[]{name}, locale
        );
        return ResponseEntity.ok(Map.of("message", msg));
    }

    // curl -H "Accept-Language: de" localhost:8080/api/greeting/Max
    // → {"message": "Hallo, Max!"}
}
Las API REST suelen utilizar AcceptHeaderLocaleResolver —basado en cabeceras—, mientras que las aplicaciones web usan CookieLocaleResolver —basado en cookies—. Si ofrece ambas desde la misma aplicación, considere un LocaleResolver personalizado que compruebe primero las cookies y después recurra a Accept-Language.

Mensajes de Bean Validation

Spring resuelve automáticamente los mensajes de restricciones de validación desde MessageSource. Utilice marcadores entre llaves como {validation.name.required} en las anotaciones y defina las traducciones en sus archivos .properties.

Bean Validation i18n
import jakarta.validation.constraints.*;

public class CreateUserRequest {

    @NotBlank(message = "{validation.name.required}")
    @Size(min = 2, max = 50, message = "{validation.name.size}")
    private String name;

    @Email(message = "{validation.email.invalid}")
    private String email;
}

// In messages.properties:
// validation.name.required=Name is required
// validation.name.size=Name must be between {min} and {max} characters
// validation.email.invalid=Please enter a valid email address
//
// In messages_de.properties:
// validation.name.required=Name ist erforderlich
// validation.name.size=Name muss zwischen {min} und {max} Zeichen lang sein
// validation.email.invalid=Bitte geben Sie eine gültige E-Mail-Adresse ein
4

Gestionar plurales y variables

Spring utiliza java.text.MessageFormat para interpolación y plurales. El patrón ChoiceFormat gestiona reglas básicas, pero para admitir completamente ICU —6 formas en árabe, 3 en ruso—, añada la biblioteca ICU4J.

MessageFormat Plurals
# MessageFormat plural syntax in messages.properties
# Uses java.text.ChoiceFormat — NOT ICU plural rules
cart.itemCount={0,choice,0#No items|1#1 item|1<{0,number} items}

# For more complex plurals, use ICU4J:
# 1. Add dependency: com.ibm.icu:icu4j
# 2. Use ICUMessageSource instead of ResourceBundleMessageSource
#
# Then you can write ICU-style plurals:
# cart.items={count, plural, one {# item} other {# items}}

# Variables with MessageFormat:
welcome.message=Welcome, {0}! You have {1,number} new {1,choice,1#notification|1<notifications}.
order.total=Order total: {0,number,currency}
event.date=Event date: {0,date,long}
ChoiceFormat no es lo mismo que las reglas de plural ICU. Utiliza intervalos numéricos (0#, 1#, 1<) en vez de categorías CLDR (zero, one, two, few, many y other). Es insuficiente para idiomas con reglas complejas, como árabe, polaco o ruso; utilice MessageFormat de ICU4J.

Automatizar traducciones

Cuando termine de configurar i18n, traduzca los archivos .properties con IA. Desde el IDE, pida a su asistente que traduzca el archivo de origen o utilice la CLI de i18n Agent en su proceso de CI/CD.

Terminal
# Translate your .properties files with AI
# In your IDE, ask your AI assistant:
> Translate src/main/resources/messages.properties to German, Japanese, and Spanish

✓ messages_de.properties created (1.2s)
✓ messages_ja.properties created (1.5s)
✓ messages_es.properties created (1.1s)

# Or use the CLI in CI/CD:
npx i18n-agent translate src/main/resources/messages.properties --lang de,ja,es
Traduzca de forma incremental: cuando añada claves nuevas a messages.properties, traduzca solo esas claves en vez de volver a generar todos los archivos regionales. Así conserva las traducciones revisadas por personas.

Automatizar la calidad de la traducción

Detecte las claves ausentes y los marcadores rotos antes de publicar con i18n-validate. Pruebe la interfaz con pseudotraducciones mediante i18n-pseudo antes de recibir las reales.

Sin configuración con spring-locale-chain

spring-locale-chain es un starter de Spring Boot de código abierto que configura automáticamente LocaleResolver, LocaleChangeInterceptor y la validación de regiones admitidas con una sola dependencia. Defina sus configuraciones regionales en application.yml y la biblioteca hará el resto.

pom.xml
<!-- Add spring-locale-chain for zero-config locale resolution -->
<dependency>
    <groupId>io.github.i18n-agent</groupId>
    <artifactId>spring-locale-chain</artifactId>
    <version>1.0.0</version>
</dependency>

Estructura de archivos recomendada

Project Structure
my-spring-app/
├── src/main/
│   ├── java/com/example/
│   │   ├── config/
│   │   │   ├── I18nConfig.java          # MessageSource bean
│   │   │   └── LocaleConfig.java        # LocaleResolver + interceptor
│   │   ├── controller/
│   │   │   └── HomeController.java      # Uses MessageSource
│   │   └── MyApplication.java
│   └── resources/
│       ├── messages.properties          # Default (English)
│       ├── messages_de.properties       # German
│       ├── messages_ja.properties       # Japanese
│       ├── messages_es.properties       # Spanish
│       ├── application.yml              # Spring config
│       └── templates/
│           └── home.html                # Thymeleaf with #{...}
├── pom.xml
└── build.gradle

Errores habituales

Los caracteres no ASCII se muestran ilegibles

Los archivos .properties de Java utilizan de forma predeterminada ISO-8859-1, no UTF-8. Caracteres como las diéresis (ü) o CJK se muestran ilegibles. Solución: defina spring.messages.encoding=UTF-8 en application.yml o utilice escapes Unicode como \u00FC en sus archivos. ReloadableResourceBundleMessageSource de Spring Boot usa UTF-8 de forma predeterminada, pero ResourceBundleMessageSource no.

ChoiceFormat falla con plurales no ingleses

ChoiceFormat de Java ({0,choice,0#|1#|1<}) solo admite intervalos numéricos; no puede expresar categorías CLDR como 'few' o 'many'. Idiomas como el árabe —6 formas—, el polaco —3— y el ruso —3— necesitan ICU4J para pluralizar correctamente. No suponga que ChoiceFormat gestiona todos los idiomas.

Los cambios de traducción no se reflejan

ResourceBundleMessageSource almacena de forma predeterminada los paquetes en caché indefinidamente. Durante el desarrollo, utilice ReloadableResourceBundleMessageSource con cacheSeconds=0 para ver los cambios sin reiniciar. En producción, defina una duración razonable —por ejemplo, 3.600 segundos— que equilibre rendimiento y velocidad de actualización.

Respaldo inesperado a la configuración regional de JVM

De forma predeterminada, Spring recurre a la configuración de la JVM —Locale.getDefault()—, no al archivo messages.properties. Defina spring.messages.fallback-to-system-locale=false en application.yml para utilizar siempre el paquete predeterminado. De lo contrario, un servidor con la JVM en 'fr' mostrará francés en vez de inglés si falta una clave en la región solicitada.

Pruebe i18n Agent ahora

Arrastre y suelte aquí su archivo de traducción

JSON, YAML, PO, XML, CSV, Markdown, Properties

o haga clic para seleccionar

Idiomas de destino

No es necesario registrarsePresupuesto al instante

Respaldo de configuraciones regionales con spring-locale-chain

Cuando falta una clave en una configuración regional como pt-BR, Spring Boot pasa directamente a la predeterminada en vez de comprobar primero la principal pt.

Terminal
<!-- Maven -->
<dependency>
  <groupId>ai.i18nagent</groupId>
  <artifactId>spring-locale-chain</artifactId>
</dependency>
Configuration
# application.yml
locale-chain:
  fallbacks:
    pt-BR:
      - pt
      - en
    zh-Hant-HK:
      - zh-Hant
      - zh
      - en

Consulte nuestra guía de respaldo de configuraciones regionales para ver todos los frameworks admitidos y las 75 cadenas integradas. Learn more →

Preguntas frecuentes