Jose Luis Estevez
Jose Luis Estevez

Reputation: 63

How to localize individual values of domain objects in a REST API build with Spring Boot?

I want to implement an API Rest that verifies the internationalized database and replaces it with the one found in the MESSAGES table according to the regional configuration. The product in the database:

Insert into PRODUCTS (ID, NAME, PRICE) VALUES (1, 'products.name.generatedproductcode', 100.00);

Where NAME is an i18n key of the MESSAGE table

In the MESSAGES table

Insert into MESSAGES (LOCALE,KEY_REFERENCE,VALUE) values ('es_ES','products.name.generatedproductcode','teléfono móvil');
Insert into MESSAGES (LOCALE,KEY_REFERENCE,VALUE) values ('en_GB','products.name.generatedproductcode','mobile phone');

I would like to have a situation where I have to duplicate code that I can mark with an annotation like in18n the field or using aspectj to replace the value with the language that exists in the Locale

@Entity
public class Product {
    private Long id;
    @CustomI18nResource
    private String name;
    private Double price;
}

There are some limitations for example the Locale would have it only in HTTP. I am currently replacing manually from the controller and the value (something expensive at the code level since there are several entities). I am thinking what I should do as a solution to send the KEY to the frontend and that the i18n be administered in the front. Has anyone faced this problem and how has it been resolved?

Solution:

Oliver's answer is just what i was trying to do thank you very much

I have written the solution and published it on Github here:

https://github.com/jestevez/springboot-i18n-database

Upvotes: 1

Views: 1680

Answers (1)

Oliver Drotbohm
Oliver Drotbohm

Reputation: 83081

The abstraction that's probably useful here is MessageSource. Within Spring Framework it's usually used to resolve messages against a resource bundle (a properties file) but you can easily implement a database backed variant, probably rather using JDBC to avoid overhead and heavily caching the values.

If you have that, you can use a MessageSourceAccessor e.g. in your controller code to prepare a model to be rendered for the domain object at hand that would know which values to i18nize, lookup the translated value and put that into the model. The …Accessor abstraction picks up the locale from whatever LocaleResolver is configured. IIRC, in Spring Boot this is an AcceptHeaderLocaleResolver, i.e. the value given in the Accept-Language HTTP request header defines which language is chosen.

If you want to completely automate this you might want to plug into your serialization mechanism to do the translation transparently for you. Jackson allows the customization of serializers so that those could use the MessageSourceAccessor to automatically translate all fields equipped with a certain annotation.

@Retention(RetentionPolicy.RUNTIME)
@interface JsonInternationalized {} 

@RequiredArgsConstructor
class I18nModule extends SimpleModule {

  private final MessageSource messageSource;

  @Override
  public void setupModule(SetupContext context) {

    MessageSourceAccessor accessor = new MessageSourceAccessor(messageSource);
    InternationalizedSerializer serializer = new InternationalizedSerializer(accessor);
    context.addBeanSerializerModifier(new InternationalizingBeanSerializerModifier(serializer));
  }

  @RequiredArgsConstructor
  static class InternationalizingBeanSerializerModifier extends BeanSerializerModifier {

    private final InternationalizedSerializer serializer;


    @Override
    public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
        List<BeanPropertyWriter> beanProperties) {

      for (BeanPropertyWriter writer : beanProperties) {
        if (writer.getAnnotation(JsonInternationalized.class) != null) {
          writer.assignSerializer(serializer);
        }
      }

      return beanProperties;
    }
  }

  @RequiredArgsConstructor
  static class InternationalizedSerializer extends ToStringSerializer {

    private static final long serialVersionUID = -2391442803792997283L;

    private final MessageSourceAccessor accessor;

    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
      gen.writeString(accessor.getMessage(value.toString()));
    }
  }
}

Here are the important aspects:

  1. We define an annotation to trigger i18n on.
  2. I18nModule registers a BeanSerializerModifier that inspects the writers for bean properties annotated with said annotation and registers a custom serializer for those properties that will resolve the property via the MessageSource handed into the module.

In a Spring Boot application, you can get this module to work by simply regitering it as Spring bean in a configuration class:

@Bean
I18nModule i18nModule(MessageSource messageSource) {
  return new I18nModule(messageSource);
}

As you can see this requires a bit of ceremony. I'm gonna take this back to the team to see whether we can improve the out of the box experience a bit.

Upvotes: 4

Related Questions