What is internationalization?
In this article we are going to add Internationalization Support to our Spring Rest API customer endpoint. Internationalization is the process of creating an application that can be adapted to different languages and regions. What we are trying to achieve is to add support for messages in multiple languages. Therefore, clients with different language preferences will get specific messages based on the language.
Default Auto Configuration
By default, Spring Boot looks for the presence of a messages resource bundle at the root of the classpath. Autoconfiguration applies when default properties file is available (messages.properties by default). Spring boot searches for message files in the scr/main/resources folder. This can be changed with the property as show below:
spring.messages.basename=messages,config.i18n.messages
If the properties file is found, Spring Boot configures a MessageSource bean for resolving messages, with support for the parameterization and internationalization of such messages.
Spring Boot also configures a LocaleResolver implementation that looks for locales. The default implementation is AcceptHeaderLocaleResolver which inspects the Accept-Language header to match the locale for the request.
Create Messages files
Messages are stored in key-value pairs in messages_xx.properties files. The xx denotes the language code (normally a two letter code like en, fr, it...). Also the region/country (usually a two letter code) can be added for more fine grained control on locales.
For our application, two properties files will be created in the resources folder. The first one is the fallback messages.properties file with English content. It will be used when no locale is specified in the request. The second one is to support Spanish language and will be named messages_es.properties
The keys are the same in each localised file. The values or descriptions of them are particular to each language. For instance, the English content in the fallback file contians the below entries
customer.name.required=Name is required.
customer.name.size=Name must be at least 3 characters and at most 20 characters.
customer.name.invalid=Name can only contain letters and spaces.
customer.email.required=Email is required.
customer.email.invalid=Email is invalid.
customer.dob.past=Date of Birth must be in the past.
and the Spanish language file
customer.name.required=Nombre es obligatorio.
customer.name.size=Nombre debe tener al menos 3 caracteres y 20 a lo sumo.
customer.name.invalid=Nombre solo puede estar formado por letras y espacios.
customer.email.required=Email es obligatorio.
customer.email.invalid=Email es invalido.
customer.dob.past=Fecha de nacimiento debe estar en el pasado.
Using the keys in the code
Now the message keys are ready to be used in our application. Customer controller validates the customer request when sending a POST or PUT request. As the validations are the same for both cases, let's look at the scenario where a customer is updated
@PutMapping("{customerId}")
public ResponseEntity<Customer> updateCustomer(
@PathVariable("customerId") Long id,
@Valid @RequestBody CustomerRequest customerRequest) {
Customer customer = customerRepo.findById(id)
.orElseThrow(() -> new EntityNotFoundException(id,
Customer.class));
Customer updatedCustomer = CustomerUtils.convertToCustomer(
customerRequest);
updatedCustomer.setId(id);
customerRepo.save(updatedCustomer);
return ResponseEntity.ok(updatedCustomer);
}
In the CustomerRequest class, the key is placed in the message attribute of the constrains.
public record CustomerRequest(
@NotBlank(message = "{customer.name.required}")
@Size(min = 3, max = 20, message = "{customer.name.size}")
@Pattern(regexp = "[a-zA-Z\\s]+", message = "
{customer.name.invalid}") String name,
@NotBlank(message = "{customer.email.required}")
@Email(message = "{customer.email.invalid}") String email,
@Past(message = "{customer.dob.past}") LocalDate dateOfBirth) {}
Finally, the Adviser to handle validation exceptions
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
RestErrorResponse handleException(MethodArgumentNotValidException ex) {
String message = ex.getFieldErrors()
.stream()
.map(e -> e.getDefaultMessage())
.reduce("Errors found:", String::concat);
return new RestErrorResponse(
HttpStatus.BAD_REQUEST.value(), message,
LocalDateTime.now());
}
Now it is time to test. Our first attempt will be to make a put request without informing the Accept-Language header as shown in the next figure
It falls back to the messages.properties file, hence errors are displayed in English. The second request will send the Accept Language header with value es. The result can be viewed below
The errors are coming back in Spanish now. If more languages need to be supported by our application, then it is as simple as creating a new message file for the desired locale.
One thing that is still hardcoded in English is the "Errors found:" literal prefix as part of the error message. Let's move it to the properties files in the next section.
MessageSource class
The class MessageSource from org.springframework.context provides methods to receive translated messages. SpringBoot will create this bean for us. Then, it is injected in the classes where messages will be used. In our case, it is constructor-injected in the ControllerAdviser as below
@RestControllerAdvice
public class GlobalExceptionHandler {
MessageSource messageSource;
public GlobalExceptionHandler(MessageSource messageSource) {
this.messageSource = messageSource;
}
...
}
Next, the key-value messages will be added to the language specific property files. For the English language
errors.found=Errors found:
And for Spanish
errors.found=Errores encontrados :
Last step is to replace the hardcoded value by the key in the handler method.
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
RestErrorResponse handleException(MethodArgumentNotValidException ex, Locale locale) {
String message = ex.getFieldErrors().stream()
.map(e -> e.getDefaultMessage())
.reduce(messageSource.getMessage("errors.found", null,
locale), String::concat);
return new RestErrorResponse(
HttpStatus.BAD_REQUEST.value(), message,
LocalDateTime.now());
}
Note that the locale can be passed as a parameter in the Exception Handler. As expected the value is correctly translated when re-running the tests. The following response is returned when informing the es value in the Accept Language header
{
"status": 400,
"message": "Errores encontrados : Nombre debe tener al menos 3 caracteres y 20 a lo sumo.Fecha de nacimiento debe estar en el pasado.Email es invalido. ",
"timestamp": "2023-05-27T19:10:57.0064035"
}
Custom Exception messages
In this section, we will translate custom exception messages. In our application, EntityNotFoundException is thrown if the customer to be updated does not exist. The message is hardcoded in the Exception class. However, we want this message to be available for the supported languages.
First step is to add a new key for the error when this exception is raised.
--- english file
entity.notFound=Entity {0} for id {1} was not found.
--- spanish file
entity.notFound=Entity {0} for id {1} was not found.
The values {0} and {1} are placeholders for parameters. The first parameter will go into position {0} and the second paramerter will be applied to {1}.
Second step is to declare a new Exception handler method for it. Again, MessageSource class provides the method to get the message. Parameters are passed in the second argument of getMethodMessage as a Object array. Both values are stored in the custom Exception and are set at instatiation time.
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
RestErrorResponse handleException(EntityNotFoundException ex,
Locale locale) {
return new RestErrorResponse(HttpStatus.BAD_REQUEST.value(),
messageSource.getMessage(
"entity.notFound",
new Object[]{ ex.getTheClassName(), ex.getId()} , locale),
LocalDateTime.now());
}
The custom exception code is below
public class EntityNotFoundException extends RuntimeException {
private Long id;
private Class theClass;
public EntityNotFoundException(@NotNull Long id,@NotNull Class
theClass) {
super(theClass.getName()+" "+id+" not found!");
this.id = id;
this.theClass = theClass;
}
public Long getId() {
return id;
}
public String getTheClassName() {
return theClass.getSimpleName();
}
}
Time to test. Let's try to update a non-existing customer.
Conclusion
Let's review the main points of what you have learned in this article:
- What is internationalization and how it is enable in SpringBoot.
- What are message files.
- Create multi language custom constrains error messages.
- Create multi language custom exception error messages.
Now you can implement your own multi language solution in your Spring projects. Source code can be found here.
Subscribe for more content on Java and Spring!
Top comments (0)