Here are some reflections and solutions on how to localize strings in hibernate objects.

The model

I am assuming the localized strings will be stored in the database in a structure similar to:

Table MY_OBJECT:

  • LOCALIZED_FIELD

Table LOCALIZED_DATA:

  • CATEGORY (object type)
  • LOCALE
  • LOCALIZED_FK (stores the id of the localized object but without foreign key restrictions)
  • FIELD (field of the object)
  • DATA (the actual localization)

First ideea (bad)

Create a bag of composite elements with the actual localizations:

<bag name="localizedData" table="LOCALIZED_DATA" where="CATEGORY='myObject'">
 <key column="LOCALIZED_FK" />
 <composite-element>
 <property name="category" column="CATEGORY" type="java.lang.String"/>
 <property name="locale" column="LOCALE" type="java.lang.String"/>
 <property name="name" column="DATA" type="java.lang.String"/>
 </composite-element>
</bag>

There are several cons against this method and the biggest one is that this will generate a large number of extra requests to the database. When loading an object hibernate will generate an extra request to load the localizedData and since there is no primary key (id) for a composite element there is also no way of caching these objects. All the localized strings will be loaded each time instead of finding just the right one. It will also require a bag for each localized property in the object.

Second ideea

The second ideea is based on the example found here which implies the creation of a special data type which will handle the localization. However this example has the limitation that if you wish for you application to handle multiple locales at the same time (based on the http request for instance) then you have to find a way to pass the locale information to the LabelUserType object and I found no way of doing so.

Third ideea (complete)

This last ideea uses bits of the previous ideea and is based on interceptors for localization.Using interceptors has the advantage that the interceptor can be configured per session thus allowing to handle multiple locales in the same application and pass the locale information to the locale loading process instead of depending on a singletone like access to the current locale.

I am also using a custom type but this only derives from StringType and it’s purpose is just to mark the field which are localized.

public class LocalizationInterceptor extends EmptyInterceptor{
...
 @Override
 public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
 if(entity instanceof Localized){
 Localized lEntity = (Localized)entity;
 for (int i = 0; i < types.length; i++) {
 //identifies the field which is localized
 if(types[i] instanceof LocalizedStringType){
 //translate here
 state[i] = getLocalizedString(lEntity.getCategory(), (Long)id, propertyNames[i], (String)state[i]);
 }
 }
 }
 return false;
 }

The ideea is to identify the objects which contain localized data by forcing them to implement the Localized interface and then to identify the fields which are localized as they are of type LocalizedStringType which is an empty subclass of StringType used for this purpose alone.

This interceptor can be then initialized per session:

LocalizationInterceptor localizationInterceptor = new LocalizationInterceptor();
hsession = factory.openSession(localizationInterceptor);
localizationInterceptor.init(hsession, locale.toString());

The caching part

My first approach was to add an id field to the LocalizedData object and to use a search of the form:

from l in class LocalizedData where
l.category = ... and l.locale = ... and l.code = and l.localizedFk = ...

but I realized that this approach could not be cached by hibernate because it would first require all possible queries to be made due to the way the query cache and the object cache work in hibernate.

So I chosed the following mapping:

<hibernate-mapping package="com.mccsoft.diapason.data">
 <class name="LocalizedData" table="LOCALIZED_DATA">
 <cache usage="transactional"/>
 <composite-id name="id">
 <key-property name="category" type="java.lang.String" column="CATEGORY"/>
 <key-property name="locale" type="java.lang.String" column="LOCALE"/>
 <key-property name="localizedFk" type="java.lang.Long" column="LOCALIZED_FK"/>
 <key-property name="code" type="java.lang.String" column="CODE"/>
 </composite-id>
 <property name="data" type="java.lang.String">
 <column name="DATA" not-null="true" />
 </property>
 </class>
</hibernate-mapping>

By using a composite id with the same properties as the query I could be sure that a cache per these parameters will be created. Thus the getLocalizedString function:

public String getLocalizedString(String category, Long id, String code, String defaultValue){
 log.info("Getting " + locale + " data for: " + category + "/" + id + "." + code);
 LocalizedDataId lId = new LocalizedDataId(locale, category, id, code);
 LocalizedData lData = (LocalizedData)currentSession.get(LocalizedData.class, lId);
 if(lData == null){
 return defaultValue;
 }else{
 return lData.getData();
 }
 }

and the cache initialization for a locale:

public static void initLocalizedCache(Session session, String locale){
 Query query = session.createQuery("from l in class com.mccsoft.diapason.data.LocalizedData where l.id.locale = :locale");
 query.setParameter("locale", locale);
 query.list();
 }

The only disadvantage I can find until now is that for missing locale values a query will be still made but this can be solved by storing a “null” value for these locales to enable the hibernate cache to store these locales also.

Comments:

max -

hi len, “I am also using a custom type but this only derives from StringType and it’s purpose is just to mark the field which are localized.” i’m wondering how to do this, could you please give an example. thanks


max -

hi len, i just found out: @org.hibernate.annotations.TypeDefs( { @org.hibernate.annotations.TypeDef(name = “titleDef”, typeClass = my.LocalizedStringType.class), @org.hibernate.annotations.TypeDef(name = “textDef”, typeClass = my.LocalizedStringType.class) }) … @Type(type = “titleDef”) public String getTitle() { return title; } … @Type(type = “textDef”) public String getText() { return text; }


len -

Hello Max, I did not used adnotations just added: in the mapping definition file.


Jiri -

Hi Len, many thanks for this topic, it was quite useful for me. I am not a Hibernate expert and I wonder is there necessary to override method for localized message storing into the interceptor or is this something Hibernate can deal with automaticaly? How do you solve the LocalizedData save? Thanx in advance.


len -

Hi Jiri, actually for the save I handle it somewhere else manually. It’s related to the editing of data, the article describes more how the data is used/accessed.