Localization Concern
Localization (also known as internationalization) is one of the concerns that is most of the times overlooked when we design an application. We almost never find it through the requirements, and if we do or if we ask about it, we usually postpone thinking about it and we underestimate the effort of adding it later. In this post I will summaries few key aspects which take little time to take into consideration in our design, and they can save a lot of effort on the long term.
Localization Service
One of the first things that I do is to define, what I call the Localization Service. It is nothing more than a simple interface, with one or two simple methods:
public interface ILocalizationService
{
string GetText(string localizationKey);
string Format<T>(T value);
}
Notice that the functions do not take as input parameters the language or culture code. The implementation will take care of taking them from the current user. The interface stays simple.
At the beginning of the project I don’t do more than this. I just put in a trivial implementation that doesn’t do much and I postpone the rest of the decisions. Now when screens are built we can just call this interface, and later we’ll make a real implementation of it. We already have a big gain: when we’ll build the localization we don’t need to go through all the screens and modify them to translate the texts. The localization service is called from the beginning.
To make it a simple call, we can have a static wrapper:
public static class Localizer
{
public static string GetText(string localizationKey)
{
var s = ServiceLocator.Current.GetInstance<ILocalizationService>();
return s.GetText(key);
}
// same for Format<T>(..)
}
We can decide later if the translations are stored in resource files or in the database or somewhere else. For now we can just pick the quickest implementation (hardcoded in a dictionary maybe) and move on. We can change it later without modifying the existent screens.
Localization Key
The next thing to consider are some conventions for building the localization keys. We are going to have many texts and it will make a big difference to have some consistent and meaningful keys rather than randomly written strings.
To do this I usually try to define some categories for the translated strings. Then for each category we can define conventions of patterns on how we will create the keys. In most of the applications we’ll have something similar with the following:
- Labels on UI elements - these are specific texts that appear on different screens. Things like buttons, menus, options, labels, etc
- Pattern:
<EntityName>.<ControllType>.<LabelKey>
- Example:
Person.Button.ChangeAddress
- Pattern:
- Specific messages or text - These are text that are specific to a functionality or a screen
- Pattern:
<MessageType>.<Functionality>.<MessageKey>
- Example:
Message.ManagePersons.ConfirmEditAddress
- Pattern:
- Standard (or generic) labels or messages - these are text that appear o different screens of the applications
- Pattern:
<MessageType>.<MessageKey>
- Example:
ErrorMessage.UnknownError
- Pattern:
- Metadata - These are names of business entities or their properties that need to be displayed. Usually these are column names in list screns or labels in edit screens
- Pattern:
<EntityType>.<Property>
- Example:
Person.Name
- Pattern:
With such categories and conventions in place, we get many benefits in debugging, managing translations and even in translating texts.
If the application screens are built by templates (like all of the list or edit screens are similar and are around one business entity), later, we could go even further and write generic code which builds the localization key based on the type of the screen and the type of the entity, and it could automatically call the localization service. For example in a razor view, we could write a html helper like:
// usage
@Html.LabelForEx(m => m.Subject);
// implementation
public static MvcHtmlString LabelForEx<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
string entityName = ModelMetadata.GetEntityName(expression, html.ViewData);
string propName = ModelMetadata.GetPropName(expression, html.ViewData);
string localizationKey = $"{entityName}.{propName}";
string text = Localizer.GetText(localizationKey);
return html.LabelFor(expression, text);
}
I think that these two: the Localization Service Interface and the Conventions for the Localization Keys are the aspects that should be addressed by the design at the beginning of the project. Next I will go through other two important aspects of localization: Managing Translations and Data Localization.
Managing Translations
One of the aspects that is usually ignored when designing for localization is the process of entering and managing the translated texts in different languages: translating the application.
This process can be difficult if the one that does the translation does not have the context of the text she is translating. Usually a word by word translation in a table is not working well. It can be even more difficult if she does not get fast feedback of the changes into the application screens. Emailing the translations to the developers and waiting for a new release can be very annoying. This difficult process can be even more costly if it was postponed until the last moment and it happens a few weeks before the release into production, when usually there are many other urgent things.
The conventions for the localization keys can play an important role in this. They could give some context, and if the one that translates the application can upload the translations into the app and get fast feedback is usually good enough. This means that we need to design and implement some functionality to upload and show the translated text, to avoid a painful process.
Another approach that works well is to implement into the application functionality to translate it. For a web app, the one that does the translation will access the application in “translate mode” and when she hovers the mouse on a text a floating div with an input is shown where she can input the translated text. The text is saved into the database and the page reloaded with the translation in it.
Even if this sounds difficult to implement, it is not and for an application that has a large variety in the texts it displays and needs to be translated in many languages, it worths the effort and makes the translation changes easy.
Data Localzation
Data Localization is about keeping some of the properties of the business entities in more languages. Imagine that your e-commerce app gets used in France and it would be better to have a translation for the name and the description of your products. For instance for the name of the mouse product, you will need to store its name in french: souris d’ordinateur.
One of the solution to implement this is to create a translation table for each table that has columns, which should be kept in more languages. This allows us to add more languages over time.
The columns of the Products
will keep the data in the default language (or language agnostic) and the Products_Trans
table will keep the translations in a specific language. Here we’ll have only the columns that need translations: Name
and Description
.
If we are to add this functionality later into our project, we need to go back in all the existent screens and change them not to read data from one table (Products
), but also to join it with the translations table (Products_Trans
). This may be very costly, because changing tens of screens built months ago may put our project in jeopardy.
The alternative, is to build some generic mechanism that automatically does the join under the scenes, based on some conventions and metadata. If we use Entity Framework, LINQ and we have the data access made though a single central point as I’ve described in Separating Data Access Concern post, then this can be achieved.
We need to rely on some conventions:
- the translation tables and EF mapped entities have the same name with
_Trans
suffix - the translated columns have same name with the ones in the main table
- some catalog that gives the entity names (tables), for which there is a translation table
With this, and by knowing that the all the LINQ queries go through our one Repository
and UnitOfWork
implementation as described in the above post, we intercept the lambda expression of each query, parse it, and recreate it with the join for the translation table.
To implement this we make that all the IQueryable our Repository returns to be a wrapper over the one returned by the EF.
private IQueryable<T> GetEntitiesInternal<T>(bool localized) where T : class
{
DbSet<T> dbSet = context.Set<T>();
return localized ? new DataLocalizationQueryable<T>(dbSet, this .cultureProvider) : dbSet;
}
The DataLocalizationQeryable
wrapper uses a visitor to go through the lambda expression and for each member assignment node, from the Select
statement, which needs to be translated, gets the value from the related property of the translation entity. Here is a code snippet that gives an idea of how the wrapper is implemented:
public class DataLocalizationQueryable<T> : IOrderedQueryable<T>
{
private IQueryable<T> query;
private ICultureProvider cultureProvider;
private ExpressionVisitor transVisitor;
public DataLocalizationQueryable(IQueryable<T> query, ICultureProvider cultureProvider)
{
…
transVisitor = new DataLocalizationVisitor(this.cultureProvider.GetCurrentUICulture());
this.Provider = new DataLocalizationQueryProvider(query.Provider, this.translationVisitor, cultureProvider);
}
public IEnumerator<T> GetEnumerator()
{
return query.Provider.CreateQuery<T>(
this.transVisitor.Visit(query.Expression)).GetEnumerator();
}
class DataLocalizationQueryProvider : IQueryProvider
{
private IQueryProvider efProvider;
private ExpressionVisitor visitor;
private readonly ICultureProvider cultureProvider;
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
return new DataLocalizationQueryable<TElement>(
efProvider.CreateQuery<TElement>(expression), cultureProvider);
}
}
class DataLocalizationExpressionVisitor : ExpressionVisitor
{
private const string suffix = "_Trans";
private const string langCodePropName = "LanguageCode";
private readonly CultureInfo currentCulture;
public DataLocalizationExpressionVisitor(CultureInfo currentCulture)
{ … }
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
{ … }
…
}
Even if modifying lambda expressions at runtime isn’t a trivial task, we do it only once as an extension to the data access and we avoid going back and modifying tens of screens.
With this, we have covered the most common aspects of localization and we’ve seen that if we pay some thought to it when we design our application we can easily avoid high costs or painful processes on the long run.