This is a annoying issue we saw again an again.
The story
Three weeks ago Gustavo Fuentes asked me a solution to implement a multi-language property. After heard his needs my first answer was : “go to IUserType”. He was not completely convinced by my answer and so I gave him some links. Few days after Michal Gabrukiewicz asked the solution, for the same problem, in nhusers-list (how much little is the world of IT, no?). Michal have shared his solution in his blog. Immediately I sent the link to Gustavo but, another time, he does not like it (que hincha pelotas). Five days ago, inspired by an IUserType in uNhAddIns, Gustavo shared his own solution in uNhAddIns issue tracker. As usual in Open Sources, when you share your code, somebodyelse can:
- see it
- use it
- find bugs
- fix bugs
- improve the solution
Waiting results of the election day in Argentina, I have applied the five points to the Gustavo’s solution.
The LocalizablePropertyType
The LocalizablePropertyType is a IUserType and IParameterizedType implementation that map a IDictionary<CultureInfo, string> to a VARCHAR (or what defined in your dialect for a string).
Your entity may look like:
public class EntityWithLocalizableProperty
{
public virtual IDictionary<CultureInfo, string> LocalizedDescriptions
{
get; set;
}
}
And your mapping like
<typedef name="localizable"
class="uNhAddIns.UserTypes.LocalizablePropertyType, uNhAddIns"/>
<class name="EntityWithLocalizableProperty">
<id type="int">
<generator class="native"/>
</id>
<property name="LocalizableDescriptions" type="localizable"/>
</class>
The LocalizablePropertyType has two parameters:
<typedef name="localizable"
class="uNhAddIns.UserTypes.LocalizablePropertyType, uNhAddIns">
<param name="keyValueEncloser">~</param>
<param name="length">560</param>
</typedef>
The keyValueEncloser is a char used to enclose the culture-Info name and the value of the localizable string.
The length is the total length of the field in the DB.
Single string property
If you need a single property to represent the value, for the current culture, its implementation may look like:
public virtual string Description
{
get
{
return
LocalizedDescriptions.Count > 0 ?
LocalizedDescriptions.FirstOrDefault
(
e =>
e.Key.Equals(Thread.CurrentThread.CurrentCulture)
)
.Value
: null;
}
set
{
if (value == null)
{
LocalizedDescriptions.Remove(Thread.CurrentThread.CurrentCulture);
}
else
{
LocalizedDescriptions[Thread.CurrentThread.CurrentCulture] = value;
}
}
}
How is represented a value
Give a dictionary like this:
new Dictionary<CultureInfo, string>
{
{new CultureInfo("es-AR"), "Hola"},
{new CultureInfo("en-US"), "Hello"}
};
its persistent representation, using the char ‘~’ as keyValueEncloser, will be:
~es-AR~~Hola~~en-US~~Hello~
Querying
In term of usability inside the entity class I like very much this solution, perhaps the problem is during querying.
I have used the keyValueEncloser not only to remove some bugs but essentially to simplify commons queries over the localized value. After that I have implemented some extension and some helpers to use in queries.
Given an entity initialized like this
new EntityWithLocalizableProperty
{
LocalizableDescriptions =
new Dictionary<CultureInfo, string>
{
{new CultureInfo("es-AR"), "Hola"},
{new CultureInfo("en-US"), "Hello"}
}
};
by HQL:
s.CreateQuery("from EntityWithLocalizableProperty e where e.LocalizableDescriptions like :pTemplate")
.SetString("pTemplate", Localizable.ConvertToLikeClause("en-US", "H_l%"));
by Criteria:
s.CreateCriteria<EntityWithLocalizableProperty>()
.Add(Localizable.Like("LocalizableDescriptions", "en-US", "H_ll_"));
or using the current culture
s.CreateQuery("from EntityWithLocalizableProperty e where e.LocalizableDescriptions like :pTemplate")
.SetString("pTemplate", "H_l%".ToLocalizableLikeClause())
and by Criteria:
s.CreateCriteria<EntityWithLocalizableProperty>()
.Add(Localizable.Like("LocalizableDescriptions", "H_ll_"))
In practice the Localizable class is the responsible to convert a common values used in the Like clause to the right value following the persistent representation defined above (you can see some others overloads).
The Code
The implementation of LocalizablePropertyType is available in uNhAddIns.
Nice article.
ReplyDeleteWhat happens if a translation contains the "~" character?
This comment has been removed by the author.
ReplyDeleteThe "~" character is only a FM election for this implementation. The real keyValueEncloser must be defined for you keeping in mind it shouldn't be part of the localized value. You can check it with NHibernate Validator.
ReplyDelete@Sukh
ReplyDeleteThe difference is that this solution does not need a different table for localized descriptions, does not need to complicate a simple select to upload an entity, does not need to manage session-filters.
@GF
ReplyDeleteI see your point but I find it smells a bit if we store everything in one field. We need to always keep in mind that there is a character which cant be part of our translation. On the other hand it saves us from having a seperate translation table (as fabio mentions).
here is another solution which might be useful
http://www.webdevbros.net/2009/06/24/create-a-multi-languaged-domain-model-with-nhibernate-and-c/
That solution is what I have linked in the post
ReplyDeleteClick on :
"Michal have shared his solution in his blog."
To see another advantage take a look to the SQLs.
I developed a solution very close to this one and worked well until the end user wanted to sort a list by the localizable column. In that case, all the entities must be retrieved because SQL can't determine which one is the first, which is not really a problem unless the table is very big (our case).
ReplyDeleteIf you need that, another table may be the solution in order to use ORDER BY and retrieve a paged result with only one query. I'm implementing this right now basing the code on Michal's solution.
I think the Michal's solution is the most appropriate in many cases where this solution is useful in some.
ReplyDelete