Thursday, December 10, 2009

Localization support, part 2. Domain modeling

In the previous post we discussed the theoretical possibilities of localization feature implementation in terms of physical organization on database level. Let’s continue the discussion on Domain model level.

Obviously, all these implementation approaches require some common infrastructure, such as automatic binding to Thread.CurrentCulture, temporary switching of current culture and so on. In that case, let’s start with this part.

Infrastructure

  1. To begin with, let’s think of what information do we need.
    First and the most necessary one is an instance of CultureInfo class which is used to represent current culture in particular application code block.
  2. Another point, not so obvious as first one, but also important is some kind of localization policy which describes what should be done if requested localization is not found. Say, you have localizations for “en-US” & “ru-RU” cultures but don’t have one for “fr-FR” culture. What should be done if someone switches Thread.CurrentCulture to “fr-FR” culture and tries to access localized properties? Should new localization for the specified culture be created or default one should be used? If so, which culture is default then?

To answer these questions the notion of immutable LocalizationContext class is introduced. It is defined in pseudo-code as follows:

public class LocalizationContext
{
  public CultureInfo Culture { get; }

  public string CultureName { get; }

  public LocalizationPolicy Policy { get; }

  public static LocalizationContext Current { get; }

}

LocalizationContext.Current property provides a programmer with valid current localization context everywhere it is required.

For now, LocalizationContext.Current is bound to Thread.CurrentThread.CurrentCulture property and changes its value each time current culture of current thread is being changed. Hence, if you want to temporarily change localization context (activate another culture) you are to change Thread.CurrentThread.CurrentCulture property and after doing some work revert it back, which is not robust nor convenient at all. To overcome this problem, LocalizationScope class is added. It acts as a disposable region where specified localization context is activated and after disposal it restores the previous localization scope value. Here is how it works:

// LocalizationContext.Current.Culture is en-US

using(new LocalizationScope(new CultureInfo("ru-RU"))) {
  // LocalizationContext.Current.Culture is ru-RU
  // do some work with ru-RU culture
}

// LocalizationContext.Current.Culture is en-US again

So now LocalizationContext.Current property logic must take into account the presence and configuration of currently active localization scope and fall back to Thread.CurrentCulture in case current localization scope is absent.

Modeling Domain

Say we have a Page class in Domain model with 2 persistent properties: Title & Content, both of them we want to make localizable. Then this is how we do it:

  1. We define PageLocalization - localization class for Page, which contains localized persistent properties. Its primary key consists of 2 fields: a reference to Page and a string representation of CultureInfo, which in turn can be represented as CultureInfo.Name.
  2. We define localizable properties in Page class as NOT persistent. They are no more than wrappers for appropriate localized persistent properties located in localized instance.

Here is localization for page class:

[HierarchyRoot]
public class PageLocalization : Localization<Page>
{
  [Field(Length = 100)]
  public string Title { get; set; }

  [Field]
  public string Content { get; set; }

  public PageLocalization(CultureInfo culture, Page target)
    : base(culture, target)
  {}
}

It inherits Localization<Page> class where key fields are declared.

And here is the Page class:

[HierarchyRoot]
public class Page : Entity
{
  [Field, Key]
  public int Id { get; private set; }

  public string Title
  {
    get { return Localizations.Current.Title; }
    set { Localizations.Current.Title = value; }
  }

  public string Content
  {
    get { return Localizations.Current.Content; }
    set { Localizations.Current.Content = value; }
  }

  [Field, Association(PairTo = "Target", OnOwnerRemove = OnRemoveAction.Cascade)]
  public LocalizationSet<PageLocalization> Localizations { get; private set; }
}

Localizable properties such as Title & Content redirect all calls to currently active PageLocalization which is accessed through Page.Localizations.Current property. What is LocalizationSet<PageLocalization> then?

Believe it or not, LocalizationSet<PageLocalization> is no more than common EntitySet<T> with some additional functionality:

public class LocalizationSet<TItem> : EntitySet<TItem> where TItem : Localization
{
  public TItem this[CultureInfo culture] { get; }

  public TItem Fetch(string cultureName)

  public TItem Current { get; }

  private TItem GetCurrent()

  private TItem Create(CultureInfo culture)
}

Here the decision what to do if localization for the current culture is requested, is made according to localization policy in current localization context.

In the next post we’ll try to figure out how to deal with CRUD operations, LINQ queries and localized entities.

Stay tuned.

4 comments:

  1. I think we must bing LocalizationContext to Session by the same way as it's done for Transaction. Otherwise Session activation won't change LocalizationContext.

    And I'm not sure about changing Thread.CurrentThread.CurrentCulture at all. AFAIK using this property is currently disregarded.

    ReplyDelete
  2. Sure, Localization context should be bound to Session through Session activation/deactivation events.

    ReplyDelete
  3. where the class "Localization" lives ? I do not find it

    ReplyDelete