Spring Boot: Internacionalización (i18n)

Spring Boot: Internacionalización (i18n)

La internacionalización es un requisito básico para la mayoría de las aplicaciones y con es Spring Boot se puede conseguir de una forma muy sencilla.

Vamos a ver cómo convertir la aplicación que he venido utilizando en una aplicación multiidioma simplemente haciendo unos pequeños cambios.

Messages.properties

La base para poder tener una aplicación internacionalizada son los archivos de idiomas, en Spring Boot si no cambiamos nada por defecto estos archivos deberán llamarse messages.properties, messages_es.properties, messages_en.properties, messages_es_ES.properties, messages_es_MX.properties, messages_en_US.properties, … es decir, el nombre base del archivo (messages) seguido del codigo del idioma y en el caso de que queramos ser más específicos también del código del país.

Dentro de estos archivos tendremos los pares de clave y valor que identifican a cada uno de los textos que queremos tener en varios idiomas y obviamente la clave para un mismo texto tiene que será la misma en todos los archivos.


saludo.pais = Hola!!! (En messages_es.properties)
saludo.pais = Hola España!!! (En messages_es_ES.properties)
saludo.pais = Hola Mexico!!! (En messages_es_MX.properties)
saludo.pais = Hello USA!!! (En messages_en_US.properties)

Aunque lo más recomendable es tener todos los textos en todos los archivos de idiomas no es obligatorio hacerlo y en ese caso se usara la traducción del siguiente nivel, por ejemplo si no añadimos una traducción al messages_es_MX.properties se buscará en el messages_es.properties y si no está o no existe ese archivo entonces la buscará en el messages.properties y si no se encuentra aquí tampoco entonces se producirá un error.

Por defecto estos archivos tienen que estar en src/main/resorces, sí los dejamos ahí ya tenemos nuestra aplicación con Spring Boot internacionalizada, y lista para mostrar los mensajes en el idioma que reciba del navegador en las cabeceras de los mensajes. Luego veremos cómo permitir que sea el usuario el que decida el idioma en el que quiere ver la aplicación.

Mostrar mensajes internacionalizados en Thymeleaf

Para mostrar mensajes i18n en Thymeleaf simplemente tenemos que incluir la clave del mensaje así #{clave.del.mensaje} dentro de alguna etiqueta de Thymeleaf, lo más habitual será hacerlo en th:text porque es la más típica para mostrar texto, pero se puede usar en cualquier otra que tenga sentido como por ejemplo th:value.

Este es el ejemplo de cómo queda la página con el listado de productos una vez que sustituimos las etiquetas de texto estáticas por las keys de los textos internazionalizados.


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="layouts/base::head(dark)">
  <meta charset="UTF-8"/>
</head>
<body>
  <header th:replace="layouts/base::header(dark)"></header>

  <div class="container">
    <div class="card bg-dark">
      <div class="card-header">
        <h3 th:text="#{productos.lista.titulo}"></h3>
      </div>
      <div class="card-body">
        <table class="table table-striped">
          <thead>
            <tr>
              <th th:text="#{productos.producto.id}"></th>
              <th th:text="#{productos.producto.codigo}"></th>
              <th th:text="#{productos.producto.nombre}"></th>
              <th th:text="#{productos.producto.precio}"></th>    
              <th></th>      
            </tr>
          </thead>
          <tbody>
            <tr th:each="producto: ${productos}">
              <td th:text="${producto.id}"></td>
              <td th:text="${producto.codigo}"></td>
              <td th:text="${producto.nombre}"></td>
              <td th:text="${producto.precio}"></td>
              <td>
                <a th:href="@{editar/} + ${producto.id}" th:text="#{productos.acciones.editar}" class="btn btn-outline-primary"></a>
                <a th:href="@{eliminar/} + ${producto.id}" th:text="#{productos.acciones.eliminar}" class="btn btn-outline-danger"></a>
              </td>
            </tr>
          </tbody>
          <tfoot>
            <tr>
              <td colspan="5">
                <a th:href="@{crear}" class="btn btn-outline-primary btn-block" th:text="#{productos.acciones.anadir}"></a>
              </td>
            </tr>
          </tfoot>
        </table>
      </div>
    </div>
  </div>
  
  <footer th:replace="layouts/base::footer(dark)"></footer>
</body>
</html>

Mostrar mensajes internacionalizados en el código Java

Los mensajes internacionalizados no solo nos sirven para utilizarlos directamente en la vista con Thymeleaf, jsp, jsf, … también podemos usarlos desde el propio código Java para enviar mensajes i18n en servicios Rest por ejemplo como veremos en posteriores artículos.

La forma de acceder a los mensajes internacionalizados en Spring Boot es mediante MessageSource y su método .getMessage() donde le indicaremos la clave del mensaje que queremos obtener y el idioma en el que queremos hacerlo, podemos indicar el idioma de forma manual o utilizar LocaleContextHolder.getLocale() para obtener el idioma actual de la aplicación.


@Autowired
private MessageSource mensajes;

public String getSaludo() {
	return mensajes.getMessage("saludo.pais", null, LocaleContextHolder.getLocale());
}

Adicionalmente si el mensaje tiene parámetros también podemos indicárselos. Por ejemplo si tuviésemos el mensaje saludo.usuario = Hola, {1}! y el siguiente código conseguiríamos tener un mensaje con partes parametrizables sin tener que hacer cosas raras como dividirlo en trozos.


@Autowired
private MessageSource mensajes;

public String getSaludo(String nombre) {
	return mensajes.getMessage("saludo.usuario", new String[]{nombre}, LocaleContextHolder.getLocale());
}

Internacionalizar los mensajes de error de los validadores

En los anteriores artículos ya habíamos dejado los mensajes de error de los validadores en los archivos messages.properties para los validadores predefinidos y ValidationMessages.properties para los validadores personalizados que creamos.

Para internacionalizarlos simplemente tenemos que añadir un nuevo archivo para cada uno de los idiomas que queramos tener, el messages.properties es el archivo estándar así que ya los deberíamos tener pero tambien tenemos que hacerlo con el ValidationMessages.properties.

Para evitarnos tener que mantener los mensajes de los validadores personalizados en archivos distintos podemos crearnos una clase de configuración y añadir el @Bean correspondiente para que los validadores usen los archivos de idioma de la aplicación.


package com.programandoointentandolo.tsb.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class ConfiguracionValidador {

    @Autowired
    MessageSource messageSource;

    @Bean
    public LocalValidatorFactoryBean getValidator() {
        LocalValidatorFactoryBean validatorFactory = new LocalValidatorFactoryBean();
        validatorFactory.setValidationMessageSource(messageSource);
        return validatorFactory;
    }
}

Y así ya podemos tener todos los mensajes de los validadores juntos. También he aprovechado para dejar los mensajes parametrizados para no tener que tocarlos en el caso de tener que cambiar los valores de los validadores y evitar que los mensajes no se correspondan con lo que se está validando porque si ya es mala idea teniendo un solo archivo de idioma con unos cuantos el fallo está casi asegurado.


# Validadores producto
Size.producto.codigo = El código del producto tiene que tener entre {2} y {1} caracteres
Pattern.producto.codigo = El código del producto solo puede tener letras mayúsculas o números
NotEmpty.producto.nombre = El nombre del producto es obligatorio
NotNull.producto.precio = El precio es obligatorio
Min.producto.precio = El precio mínimo es {1}
Max.producto.precio = El precio máximo es {1}
NotNull.producto.unidadesMinimas = El mínimo es una unidad
Min.producto.unidadesMinimas = El mínimo es {1} unidad

# Validadores personalizados
productoCodigoValido.mensajePorDefecto = Código no valido
productoPrecioUnidadesMinimo.mensajePorDefecto = El precio total para el número mínimo de unidades tiene que ser mayor que 10

Permitir al usuario seleccionar el idioma

Tener la aplicación internacionalizada esta bien pero si no le dejamos al usuario que seleccione el idioma se nos va a quedar un poco coja la cosa porque aunque lo más normal es que el usuario tenga configurado su navegador con su idioma esto no tiene por qué ser siempre así y además en el caso de que no tengamos la aplicación en su idioma quizás podría querer usar un idioma distinto del que tengamos por defecto.

Para que el usuario pueda cambiar el idioma necesitamos cambiar el LocaleResolver de nuestra aplicación y convertirlo en un SessionLocaleResolver añadiendo el @Bean localeResolver() en una clase de configuración para que el idioma se mantenga en la sesión del usuario, también tenemos que añadir un LocaleChangeInterceptor y hacer que nuestra clase de configuración implemente WebMvcConfigurer para así poder sobrescribir su método void addInterceptors(InterceptorRegistry registry) para añadir el LocaleChangeInterceptor y de esta forma cada vez que enviemos el parámetro con el nombre que disidiésemos se cambiará el idioma para el usuario.


package com.programandoointentandolo.tsb.config;

import java.util.Locale;

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.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

@Configuration
public class ConfiguracionMensajes implements WebMvcConfigurer {

  @Bean
  public LocaleResolver localeResolver() {
    SessionLocaleResolver localeResolver = new SessionLocaleResolver();
    localeResolver.setDefaultLocale(Locale.getDefault());
    return localeResolver;
  }

  @Bean
  public LocaleChangeInterceptor localeChangeInterceptor() {
    LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor();
    localeInterceptor.setIgnoreInvalidLocale(true);
    localeInterceptor.setParamName("idioma");
    return localeInterceptor;
  }

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

}

Aunque quizás sobra decirlo hay que tener cuidado con el nombre del parámetro que elegimos para evitar que lo reutilicemos para alguna otra función y se cambie el idioma cuando no deba suceder.

Ahora ya tenemos nuestra aplicación preparada para que cambie el idioma siempre que venga en una petición el parámetro idioma o el nombre que le hayamos dado en el interceptor.

La forma de enviarlo ahora ya dependerá de cómo lo queramos hacer, podemos enviarlo en cada request, solo cuando cambiemos de idioma o como mejor nos parezca, para no complicarnos muchos simplemente añadimos un enlace en la cabecera al home de nuestra aplicación con el idioma queremos utilizar.


<span class="idiomas">
  <a th:href="|@{/}?idioma=es|"><img th:src="@{/img/idiomas/spain.svg}" width="20" height="20" alt="Español"></a>
  <a th:href="|@{/}?idioma=es_MX|"><img th:src="@{/img/idiomas/mexico.svg}" width="20" height="20" alt="Español Mexico"></a>
  <a th:href="|@{/}?idioma=en|"><img th:src="@{/img/idiomas/usa.svg}" width="20" height="20" alt="English"></a>
</span>

Cambiar la ubicación de los archivos de idiomas en Spring Boot

Como dijimos al principio los archivos de idiomas por defecto se llaman messages y están directamente el directorio de resources pero ambas cosas podemos cambiarlas en nuestro application.yml o en una clase de configuración si lo preferimos así.

Para dejar nuestros archivos dentro del subdirectorio i18n de reosurces manteniendo el nombre messages tenemos que añadir esta configuración a nuestro application.yml


spring:
  messages:
    basename: i18n/messages
    encoding: UTF-8

O añadir el siguiente @Bean teniendo cuidado de no olvidarnos de incluir classpath: antes de nuestra carpeta.


@Bean
public MessageSource messageSource() {
  ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
  messageSource.setBasename("classpath:i18n/messages");
  messageSource.setDefaultEncoding("UTF-8");
  return messageSource;
}

Y como siempre añado los cambios que hemos hecho aquí al proyecto para que los puedas probar o revisar. Si quieres probar el código tal y como estaba en el momento correspondiente a este post puedes hacerlo desde este enlace y si prefieres echarle un vistazo a como se encuentra en este momento o al repositorio en general pues puedes hacerlo aquí.