1

I have a Spring-boot application that uses JPA and Hibernate. You can find the whole code on this GitHub repository.

My question is how can I add internationalization functionality to a specific column without any foreign keys and by using JSON structure?

For example I would like to define a JPA entity like this:

@Entity
class Book {

    @Id
    private int id;

    private Author author;

    @I18n  //<- this annotation is something that I am looking for 
    private String title;

}

and then the data in title column would be stored like the following for en and de locales:

{"en":"Cologne","de":"Köln"}

And then when the current locale is de the Köln and when the en is set as locale then Cologne fetch in the time of reading data!

Also when we store the data, the passed string is stored in the relevant property in the JSON format. For example if the locale is set to es and user passes Kolne then we have to have the following data in the DB:

{"en":"Cologne","de":"Köln","es":"Kolne"}

It is interesting for me that most of the solutions in the web for hibernate and JPA is based on an old method that we have languages and translations tables. Something like here or here.

However what I am looking for is some solutions like this one which is suggested for Laravel and store the translations exactly in the way that I explained (i.e. in a JSON object and in the same column)!

The only solution that I found and could be somehow relevant (Not 100%) is this one, however it does not working when I tried to test it and it seems does not supported anymore!

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
MJBZA
  • 4,796
  • 8
  • 48
  • 97

2 Answers2

2

Hibernate Types project

First, you need to add the Hibernate Type project dependency.

Afterward, you could use either an HStore or a JSONB column to store the locate-specific titles:

@Entity
@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
class Book {

    @Id
    private int id;

    private Author author;

    @Type(type = "jsonb")
    @Column(name = "localized_titles", columnDefinition = "jsonb")
    private Map<String, String> localizedTitles = new HashMap<>();

    public String getLocalizedTitle(String locale) {
        return localizedTitles.get(locale);
    }

    public String getLocalizedTitle() {
        return localizedTitles.get(LocaleUtil.getDefaultLocale());
    }
}

So, you can call the getLocalizedTitle and pass the current locale to get the current localized title.

Book book = entityManager.find(Book.class, bookId);
String title = book.getLocalizedTitle("en");

Or, you could store the current locale in a ThreadLocal in a class called LocaleUtil:

public class LocaleUtil {
  
    private static final ThreadLocal<String> LOCALE_HOLDER =
        new ThreadLocal<>();

    public static String getLocale() {
        return LOCALE_HOLDER.get();
    }
 
    public static void setLocale(String locale) {
        LOCALE_HOLDER.set(locale);
    }
 
    public static void reset() {
        LOCALE_HOLDER.remove();
    }
}

And store the current locale like this:

LocaleUtil.setLocale("en");

And, then just call the getLocalizedTitle method that takes no argument:

Book book = entityManager.find(Book.class, bookId);
String title = book.getLocalizedTitle();

Check out this PostgreSQLJsonMapTest test case on GitHub for more details about using Hibernate Types to persiste Java Map as JSON column types.

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
0

After some weeks I could return back again to my olingo2 odata server project.

What I wanted to do was simpler than what I expected.

The solution has been suggested by Vlad Mihalcea is good and I appreciate it, however as I mentioned in the question I need a solution that works beside of the Olingo JPA library! However, the suggested solution has this problem that Olingo cannot handle JsonBinaryType.

Here is my suggestion for implementing internationalization beside of Olingo JPA.

Assume we have a BasicModel.java like this:

import java.io.Serializable;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.i18n.LocaleContextHolder;

import java.io.IOException;

public abstract class BaseModel implements Serializable {
    private static final long serialVersionUID = 1L;
    private static ObjectMapper mapper = new ObjectMapper();

    @SuppressWarnings("unchecked")
    protected static Map<String, String> jsonToMap(String json) {
        Map<String, String> map = new HashMap<>();
        try {
            // convert JSON string to Map
            if (json != null) {
                map = (Map<String, String>) mapper.readValue(json, Map.class);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return map;
    }

    protected static String mapToJson(Map<String, String> map) {
        String json = "";
        try {
            // convert map to JSON string
            json = mapper.writeValueAsString(map);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return json;
    }

    protected static String getLang() {
        Locale currentLocale = LocaleContextHolder.getLocale();
        String[] localeStrings = (currentLocale.getLanguage().split("[-_]+"));
        return localeStrings.length > 0 ? localeStrings[0] : "en";
    }
}

This class provides a mechanism for us to convert JSON strings to Map and vice versa.

The code for converters had been adapted from here. For using this snippet of code we need to add this maven dependency:

  <!-- Convert JSON string to Map -->        
  <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-databind</artifactId>
  </dependency>

Finally, whenever in a JPA entity model we want to have i18n for a string property we only need to modify setter and getter methods slightly. For example:


import javax.persistence.*;

import java.util.Map;
import java.util.Set;

/**
 * The persistent class for the actions database table.
 * 
 */
@Entity
@Table(name = "actions")
@NamedQuery(name = "Action.findAll", query = "SELECT a FROM Action a")
public class Action extends BaseModel {
    private static final long serialVersionUID = 1L;

    @Id
    @Column(name = "id", unique = true, nullable = false, length = 255)
    private String id;

    @Column(nullable = false, length = 255)
    private String name;

    public Action() {
    }

    public String getId() {
        return this.id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        Map<String, String> map = jsonToMap(this.name);
        return map.get(getLang());
    }

    public void setName(String name) {
        Map<String, String> map = jsonToMap(this.name);
        map.put(getLang(), name);
        this.name = mapToJson(map);
    }

}
MJBZA
  • 4,796
  • 8
  • 48
  • 97