2012-06-18 15 views
16

Sigo encontrándome con un requisito i18n en el que mis datos (no mi IU) deben ser internacionalizados.Internacionalización del contenido en Entity Framework

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 
} 

¿Cuáles son algunos de los enfoques que pueda usar?

Algunas cosas que he intentado: -

1) almacenar una clave de recursos en el PP

public class FooEntity 
{ 
    ... 
    public string NameKey { get; set; } 
    public string DescriptionKey { get; set; } 
} 
  • Pros: No es necesario el caso de consultas para obtener una traducción entidad. System.Globalization maneja los retrocesos para usted.
  • Contras: Las traducciones no pueden ser fácilmente administradas por un usuario administrador (tienen que implementar archivos de recursos cada vez que cambie mi Foo).

2) utilizar un tipo de LocalizableString entidad

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; } 
} 
  • Pros: Todas las cadenas localizadas en la misma tabla. La validación se puede realizar por cadena.
  • Contras: Las consultas son horribles. Tiene que incluir la tabla LocalizedStrings una vez para cada cadena localizable en la entidad padre. Los retrocesos son difíciles e implican uniones extensas. No ha encontrado una manera de evitar N + 1 al recuperar, p. datos para una tabla.

3) Utilizar una entidad matriz con todas las propiedades invariantes y entidades secundarias que contienen todas las propiedades localizadas

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; } 
} 
  • Pros: No es tan difícil (pero sigue siendo demasiado duro) para obtener! una traducción completa de una entidad en la memoria.
  • Contras: Doble el número de entidades. No se pueden manejar las traducciones parciales de una entidad, especialmente el caso en el que, por ejemplo, el nombre proviene de es pero la descripción procede del es-AR.

que tengo tres requisitos para una solución

  • Los usuarios pueden editar las entidades, idiomas y traducciones en tiempo de ejecución

  • Los usuarios pueden proporcionar traducciones parciales con las secuencias que faltan procedentes de un repliegue según System.Globalization

  • Las entidades se pueden traer a la memoria sin ejecutarlas p. ej. N + 1 números

+0

Todavía no respondido, a mí también me interesó. – polkduran

+0

No está claro lo que consideraría una respuesta aceptable. Si alguien tiene una opción 4, es probable que también tenga pros/contras. – explunit

+0

Pregunta aclarada. No espero que haya una solución perfecta, pero espero que haya una mejor que la que he tenido hasta ahora. –

Respuesta

1

¿Por qué no toma lo mejor de ambos mundos? Tenga un CustomResourceManager que maneje la carga de recursos y elija la cultura correcta y use un CustomResourceReader que use la tienda de respaldo que desee. Una implementación básica podría verse así, dependiendo de la convención de que Resourceky sea Typename_PropertyName_PropertyValue. Si por alguna razón la estructura del backingstore (csv/excel/mssql/estructura de tabla) necesita cambiar, solo tiene que cambiar la implementación del ResourceReader.

Como una ventaja adicional, también obtuve el proxy real/transparente.

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 
    } 

Uso

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); 
    } 
} 

el uso de proxy real/Transparente

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; 
+0

Me preocupan las características de consulta de esta solución. Si no me equivoco, o bien necesito cargar todas las cadenas localizadas en la aplicación en la memoria (con todos los problemas que vienen con eso), o tengo que llamar a MyRM.GetEntity en cada entidad individual, lo que va a causar problemas graves de N + 1 cuando quiero mostrar una tabla de entidades. –

+0

Depende. Si su preocupación es la presión de la memoria, podría implementar una solución de almacenamiento en caché inteligente que elimine resourceSets de la memoria después de x tiempo. ¿O hay más problemas que no superviso ahora? Y tiene razón en que debe llamar a GetEntity en cada entidad. Pero es eso o tiene una consulta compleja.Una cosa que probé pero que no pude conseguir fue un proxy dinámico o transparente que expone resultados localizados de tus propiedades. Eso integra la entidad localizada en cualquier código actual a la perfección. Y luego puede enfocarse en una implementación técnica que satisfaga sus requisitos. – rene

+0

El problema más acuciante, creo, es garantizar que este caché quede invalidado cuando se actualice la tienda de lectura remota. –

1

Primero uno es digno si tiene contenido estático en la base de datos. Por ejemplo, si tiene categorías que relativamente no van a ser cambiadas por el usuario. Puede cambiarlos en la siguiente implementación. No hago esta solución personalmente. No lo considero una buena solución. Esto es solo un escape del problema.

El segundo es el mejor pero puede causar un problema cuando tiene dos o más campos localizables en una sola entidad. Puede simplificado a idiomas bits y código duro en ello como esto

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

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

tercera no es buena ni. A partir de esta estructura, no puedo estar seguro de que todos los nodos (literales, líneas, cadenas, etc.) se traduzcan en una cultura específica.

No generalice demasiado. Cada problema es algo especializado y también necesita una solución especializada. Demasiada generalización genera problemas injustificados.

+0

No me gusta des-normalizar la entidad LocalizedString, ya que esto significará que los usuarios administradores no podrán agregar nuevos idiomas. No creo que esto sea un problema de "generalización excesiva". Este es un problema empresarial genuino: mis usuarios a menudo necesitan poder editar entidades, idiomas y traducciones sin la intervención de un desarrollador. –

+0

Lo siento, no te entendí bien. No sabía que también quisieras agregar idiomas. – TIKSN