Iain Galloway
Iain Galloway

Reputation: 19190

Internationalization of content in Entity Framework

I keep coming across an i18n requirement where my data (not my UI) needs to be internationalized.

public class FooEntity
{
  public long Id { get; set; }
  public string Code { get; set; } // Some values might not need i18n
  public string Name { get; set } // but e.g. this needs internationalized
  public string Description { get; set; } // and this too
}

What are some approaches I could use?

Some things I've tried:-

1) Store a resource key in the db

public class FooEntity
{
  ...
  public string NameKey { get; set; }
  public string DescriptionKey { get; set; }
}

2) Use a LocalizableString entity type

public class FooEntity
{
  ...

  public int NameId { get; set; }
  public virtual LocalizableString Name { get; set; }

  public int NameId { get; set; }
  public virtual LocalizableString Description { get; set; }
}

public class LocalizableString
{
  public int Id { get; set; }

  public ICollection<LocalizedString> LocalizedStrings { get; set; }
}

public class LocalizedString
{
  public int Id { get; set; }

  public int ParentId { get; set; }
  public virtual LocalizableString Parent { get; set; }

  public int LanguageId { get; set; }
  public virtual Language Language { get; set; }

  public string Value { get; set; }
}

public class Language
{
  public int Id { get; set; }
  public string Name { get; set; }
  public string CultureCode { get; set; }
}

3) Use a parent entity with all the invariant properties and child entities containing all the localized properties

public class FooEntity
{
  ...
  public ICollection<FooTranslation> Translations { get; set; }
}

public class FooTranslation
{
  public long Id { get; set; }

  public int ParentId { get; set; }
  public virtual FooEntity Parent { get; set; }

  public int LanguageId { get; set; }
  public virtual Language Language { get; set; }

  public string Name { get; set }
  public string Description { get; set; }
}

public class Language
{
  public int Id { get; set; }
  public string Name { get; set; }
  public string CultureCode { get; set; }
}

I have three requirements for a solution

Upvotes: 20

Views: 3487

Answers (2)

TIKSN
TIKSN

Reputation: 615

First one is worthy if you have static content in database. For example if you have categories that relatively are not going to be changed by user. You can change them at next deploy. I do not like this solution personally. I do not consider this as a nice solution. This is just an escape of the problem.

Second one is the best but can cause a problem when you have two or more localizable fields in one entity. You can simplify it a bit and hard code languages on it like this

public class LocalizedString
{
  public int Id { get; set; }

  public string EnglishText { get; set; }
  public string ItalianText { get; set; }
  public string ArmenianText { get; set; }
}

Third one is not a good one neither. From this structure I can't be sure that all nodes (literals, lines, strings etc.) translated in specific culture.

Do not generalize too much. Each problem is kind of specialized and it needs specialized solution too. Too much generalization makes unjustified issues.

Upvotes: 1

rene
rene

Reputation: 42483

Why don't you take the best of both worlds? Have a CustomResourceManager that handles the loading of resources and picking the right culture and use a CustomResourceReader that uses whatever backing store you like. A basic implementation could look like this, relying on convention of the Resourceky being Typename_PropertyName_PropertyValue. If for some reason the structure of the backingstore(csv/excel/mssql/table structure) need to change you only have the change the implementation of the ResourceReader.

As an added bonus I also got the real/transparent proxy going.

ResourceManager

class MyRM:ResourceManager
{
    readonly Dictionary<CultureInfo, ResourceSet>  sets = new Dictionary<CultureInfo, ResourceSet>();


    public void UnCache(CultureInfo ci)
    {
        sets.Remove(ci):
    }

    protected override ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
    {
        ResourceSet set;
        if (!sets.TryGetValue(culture, out set))
        {
            IResourceReader rdr = new MyRR(culture);
            set = new ResourceSet(rdr);
            sets.Add(culture,set);
        }
        return set; 
    }

    // sets Localized values on properties
    public T GetEntity<T>(T obj)
    {
        var entityType = typeof(T);
        foreach (var prop in entityType.GetProperties(
                    BindingFlags.Instance   
                    | BindingFlags.Public)
            .Where(p => p.PropertyType == typeof(string) 
                && p.CanWrite 
                && p.CanRead))
        {
            // FooEntity_Name_(content of Name field)
            var key = String.Format("{0}_{1}_{2}", 
                entityType.Name, 
                prop.Name, 
                prop.GetValue(obj,null));

            var val = GetString(key);
            // only set if a value was found
            if (!String.IsNullOrEmpty(val))
            {
                prop.SetValue(obj, val, null);
            }
        }
        return obj;
    }
}

ResourceReader

class MyRR:IResourceReader
{
    private readonly Dictionary<string, string> _dict;

    public MyRR(CultureInfo ci)
    {
        _dict = new Dictionary<string, string>();
        // get from some storage (here a hardcoded Dictionary)
        // You have to be able to deliver a IDictionaryEnumerator
        switch (ci.Name)
        {
            case "nl-NL":
                _dict.Add("FooEntity_Name_Dutch", "nederlands");
                _dict.Add("FooEntity_Name_German", "duits");
                break;
            case "en-US":
                _dict.Add("FooEntity_Name_Dutch", "The Netherlands");
                break;
            case "en":
                _dict.Add("FooEntity_Name_Dutch", "undutchables");
                _dict.Add("FooEntity_Name_German", "german");
                break;
            case "": // invariant
                _dict.Add("FooEntity_Name_Dutch", "dutch");
                _dict.Add("FooEntity_Name_German", "german?");
                break;
            default:
                Trace.WriteLine(ci.Name+" has no resources");
                break;
        }

    }

    public System.Collections.IDictionaryEnumerator GetEnumerator()
    {
        return _dict.GetEnumerator();
    }
    // left out not implemented interface members
  }

Usage

var rm = new MyRM(); 

var f = new FooEntity();
f.Name = "Dutch";
var fl = rm.GetEntity(f);
Console.WriteLine(f.Name);

Thread.CurrentThread.CurrentUICulture = new CultureInfo("nl-NL");

f.Name = "Dutch";
var dl = rm.GetEntity(f);
Console.WriteLine(f.Name);

RealProxy

public class Localizer<T>: RealProxy
{
    MyRM rm = new MyRM();
    private T obj; 

    public Localizer(T o)
        : base(typeof(T))
    {
        obj = o;
    }

    public override IMessage Invoke(IMessage msg)
    {
        var meth = msg.Properties["__MethodName"].ToString();
        var bf = BindingFlags.Public | BindingFlags.Instance ;
        if (meth.StartsWith("set_"))
        {
            meth = meth.Substring(4);
            bf |= BindingFlags.SetProperty;
        }
        if (meth.StartsWith("get_"))
        {
           // get the value...
            meth = meth.Substring(4);
            var key = String.Format("{0}_{1}_{2}",
                                    typeof (T).Name,
                                    meth,
                                    typeof (T).GetProperty(meth, BindingFlags.Public | BindingFlags.Instance
        |BindingFlags.GetProperty).
        GetValue(obj, null));
            // but use it for a localized lookup (rm is the ResourceManager)
            var val = rm.GetString(key);
            // return the localized value
            return new ReturnMessage(val, null, 0, null, null);
        }
        var args = new object[0];
        if (msg.Properties["__Args"] != null)
        {
            args = (object[]) msg.Properties["__Args"];
        }
        var res = typeof (T).InvokeMember(meth, 
            bf
            , null, obj, args);
        return new ReturnMessage(res, null, 0, null, null);
    }
}

Real/Transparent proxy usage

 var f = new FooEntity();
 f.Name = "Dutch";
 var l = new Localizer<FooEntity>(f);
 var fp = (FooEntity) l.GetTransparentProxy();
 fp.Name = "Dutch"; // notice you can use the proxy as is,
                    // it updates the actual FooEntity
 var localizedValue = fp.Name;

Upvotes: 1

Related Questions