Mapping translations in NHibernate
Posted by Siim on February 24th, 2010Some time ago I wrote about translations. Now was the time to actually implement that feature. Because I don’t have any default language which is always present on an object, I had somehow create it virtually, so in case of translation is not found in current language, the default one (for a given object) will be used. I decided that the first language for an object (that is, the language in which the object was created), is used as a default one.
So my data model looks like this. Firstname_id and lastname_id both refer to the translation table as foreign key relations.
All the objects were mapped as entities and I was trying to map relation between Translation and it’s Localizations as an ordered list. But I soon discovered that ordered collections don’t support bi-directional associations natively. I had a bi-directional relation between Translation and Localizations. So I had to do index handling myself. For that I created a Index property to Localization which is mapped to order_index column. Property looks like this:
public virtual int Index
{
get
{
return Translation.IndexOf(this);
}
private set
{ }
}
And Translation object looks like this:
public abstract class Translation
{
private IList<Localization> _localizations;
public Translation()
{
_localizations = new List<Localization>();
}
public abstract string Context { get; }
public virtual IEnumerable<Localization> Localizations
{
get { return _localizations; }
}
public virtual int IndexOf(Localization localization)
{
return _localizations.IndexOf(localization);
}
public virtual string DefaultValue
{
get
{
var loc = _localizations.FirstOrDefault();
return loc != null ? loc.Value : null;
}
}
public virtual string GetCurrentValue(Language language)
{
return this[language] ?? DefaultValue;
}
public virtual string this[Language language]
{
get
{
var localization = _localizations.SingleOrDefault(x => x.Language.Locale == language.Locale);
return localization == null ? null : localization.Value;
}
set
{
var localization = _localizations.SingleOrDefault(x => x.Language.Equals(language));
if (localization != null && !string.IsNullOrEmpty(value))
{
localization.Value = value;
}
else if (localization != null)
{
RemoveLocalization(localization);
}
else if (!string.IsNullOrEmpty(value))
{
AddLocalization(language, value);
}
}
}
private void RemoveLocalization(Localization localization)
{
_localizations.Remove(localization);
}
private void AddLocalization(Language language, string value)
{
var localization = new Localization(language, value) { Translation = this };
_localizations.Add(localization);
}
}
As you can see, I added indexer property to Translation to manipulate with localizations conveniently. Although I’m not sure if using an object as an indexer has any drawbacks later on… Thoughts welcome.
You may have also noticed abstract Context property on Translation. It isn’t strictly required, but I found it useful in my implementation. By using this I can conveniently ask all the translations for person name or for product name, for example. This is useful when users need to translate all product names in a batch, so I can display them on a single form.
And here are example NHibernate mappings:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
<!-- Translation -->
<class abstract="true" name="Translation" table="[translation]">
<id name="Id" access="property" column="translation_id">
<generator class="identity" />
</id>
<discriminator column="context"/>
<list name="Localizations" cascade="all-delete-orphan" fetch="join" access="field.camelcase-underscore" inverse="true">
<key column="translation_id" />
<index column="order_index" />
<one-to-many class="Localization" />
</list>
<subclass discriminator-value="PersonFirstName" name="PersonFirstNameTranslation">
</subclass>
<subclass discriminator-value="ProductName" name="ProductNameTranslation">
</subclass>
<!-- etc -->
</class>
<!-- Localization -->
<class name="Localization" table="localization">
<id name="Id" access="property" column="localization_id">
<generator class="identity" />
</id>
<many-to-one name="Translation" class="Translation" column="translation_id" not-null="true" />
<many-to-one name="Language" class="Language" column="language_id" not-null="true" />
<property name="Value" column="value"/>
<property name="Index" column="order_index" type="int" update="true" insert="true" />
</class>
<!-- Person -->
<joined-subclass name="Person" extends="Party" table="person">
<key column="party_id" />
<property name="PersonalCode" column="personal_code" />
<property name="DateOfBirth" column="birth_date" access="field.camelcase-underscore" />
<many-to-one name="FirstName" column="firstname_id" class="Translation" fetch="join"
access="property" cascade="all-delete-orphan" not-null="true"/>
<many-to-one name="LastName" column="lastname_id" class="Translation" fetch="join"
access="property" cascade="all-delete-orphan" not-null="true"/>
</joined-subclass>
</hibernate-mapping>
And storing a new object with translations is simple as this:
var person = new Person();
person.PersonalCode = "12345670";
// languages is IList<Language>
foreach (var language in languages)
{
person.FirstName[language] = "First name: " + language.Name;
person.LastName[language] = "Last name: " + language.Name;
}
session.Save(person);
This solution has it’s own drawbacks also, but I found that it’s best for my needs. Currently translations are not reusable. By that I mean when one term is used in multiple places, it has to be maintained separately on each instance. Of course, I can make user to choose from existing translations, but it seems to me that this makes things overly complicated. But again, it depends on the exact context