In the past I have had to deal with globalization many times. Strangely I haven't been able to decide on a truly satisfying design pattern that works in all scenarios. In this case, I'm not referring to static labels and stuff like that, which you'd probably want to push into resource files, but rather dynamic text elements.
To clarify my intent, a assume this simple scenario. We have a Product object, which has the following definition:
class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public double Price { get; set; }
}
Extremely simple, right? Ok, so now suppose that we have some sort of administrative interface where a website's administrator can manage products (CRUD). All is still well ...
But, then suppose that the company that owns the website, decides to go global. They want to sell products world-wide and thus need to accommodate localized descriptions of their products. The issue we are then faced with, making the string values culture aware can be tackled in various different ways. to name a few:
- We could leave our object model as is, and just modify the datastore to persist the same Person object for each culture we want to support;
- We could break our Person object into two separate objects (culture aware & culture unaware) and append a list of the aware objects to the unaware (base) object. obviously, we should modify our data layer accordingly;
- We could wrap the culture specific fields in a newly defined type;
In the past, I've had most success with the second option and was forced to use option 1 in some scenarios where I had to work with some legacy code. The reason I don't like option 1 is that it makes for odd and de-normalized persistence. Also, I don't really like option 2, as it causes complex(er) object models. You get a lot of 'noise'. Moreover, it tends to result in hard(er) to read code. So, during my last globalization endeavor, I decided to explore option 3.
In my case, specifically, only the string values were to become culture aware and persistence was done using XML Serialization techniques (no database). I ended up creating the following simple wrapper class:
public class CultureNameIndexedString : SerializableDictionary
{
///
/// Sets the value of the specified string in the specified culture.
///
/// Culture of the value
/// Value to set
public void Set(CultureInfo culture, string value)
{
Set(culture.Name, value);
}
///
/// Sets the value of the specified string in the specified name.
///
/// Name of the culture for the value
/// Value to set
public void Set(string name, string value)
{
if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name");
if (!string.IsNullOrEmpty(value))
{
var key = name.ToLower();
if (ContainsKey(key))
{
this[key] = value;
}
else
{
Add(key, value);
}
}
else
{
if (ContainsKey(name))
{
Remove(name);
}
}
}
///
/// Tries to get the value of this string for the current culture, returning
/// the string in the default culture is this fails.
///
/// Value
public string GetCurrentOrDefault()
{
var value = GetSpecific(CultureHelper.CurrentCulture);
if (string.IsNullOrEmpty(value))
{
value = GetSpecific(CultureHelper.DefaultCulture);
}
return value;
}
///
/// Tries to get the value of this string for the specified culture, returning
/// the string in the default culture is this fails.
///
/// Culture of the value
/// Value
public string GetSpecificOrDefault(CultureInfo culture)
{
var value = GetSpecific(culture);
if (string.IsNullOrEmpty(value))
{
value = GetSpecific(CultureHelper.DefaultCulture);
}
return value;
}
///
/// Tries to get the value of this string for the specified culture, returning
/// the string in the default culture is this fails.
///
/// Culture (name) of the value
/// Value
public string GetSpecificOrDefault(string name)
{
var value = GetSpecific(name);
if (string.IsNullOrEmpty(value))
{
value = GetSpecific(CultureHelper.DefaultCulture);
}
return value;
}
///
/// Tries to get the value of this string for the specified culture
///
/// Culture of the value
/// Value or null
public string GetSpecific(CultureInfo culture)
{
if (culture == null) throw new ArgumentNullException("culture");
return GetSpecific(culture.Name);
}
///
/// Tries to get the value of this string for the specified name
///
/// Culture (name) of the value
/// Value or null
public string GetSpecific(string name)
{
if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name");
var key = name.ToLower();
return ContainsKey(key) ? this[key] : null;
}
#region Overrides
public override string ToString()
{
return GetCurrentOrDefault();
}
#endregion
}
Note1: The CultureHelper class just wraps configuration issues, and is irrelevant for this post.
Note2: I used an ActionFilter - yes, it was an MVC project - to set the current culture for each request.
As persistence of objects was achieved using serialization, I was forced to use an alternative to the IDictionary<TK, TV> class. As I strongly believe in reusing what's there, I googled around for a while and found the code
here.
Then I modified my (fictive) Person class like so:
class Product
{
public Product()
{
Name = new CultureNameIndexedString();
Description = new CultureNameIndexedString();
}
public Guid Id { get; set; }
public CultureNameIndexedString Name { get; set; }
public CultureNameIndexedString Description { get; set; }
public double Price { get; set; }
}
Note: Because of the need for XML Serialization, I used concrete type definitions rather than interfaces.
Obviously, I wanted to limit the impact of my changes - especially in the UI - so I created a simple override for the ToString() method. So, read-only access to the strings was unchanged. Obviously, for in some cases, I needed to get the value for a specific culture, typically in the CRUD screens. Setting values, sadly, has become quite different, like so:
var person = new Product { Id = id, Price = price };
person.Name.Set("nl-NL", "Schoen");
person.Name.Set("en-US", "Shoe");
person.Name.Set("it-IT", "Scarpa");
I must say I'm quite content with the way this worked out. Integrating this, in a fairly simple object model, took me only a few hours. Thanx to .NET's brilliant XML Serialization capabilities, I didn't even have to change the data access code! Obviously I broke compatibility with previously serialized data, but you can't have it all! ;-)
For other projects, what I'm concerned about, is how to handle immutable objects. That would be slightly more complex to implement, as I would not be able to simply use a dictionary. Another thing we could do, is extend the above functionality to work with other values as well, perhaps even images ... (it happens that also images need to change, based on the culture)
What do you think? Are there better alternatives? What do you use?
54c7727c-8555-4a04-8862-c87338285840|0|.0