2012-06-18 13 views
16

Continuo a riscontrare un requisito i18n in cui i miei dati (non la mia interfaccia utente) devono essere internazionalizzati.Internazionalizzazione del contenuto in 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 
} 

Quali sono alcuni approcci che potrei utilizzare?

alcune cose che ho provato: -

1) memorizzare una chiave di risorsa nel db

public class FooEntity 
{ 
    ... 
    public string NameKey { get; set; } 
    public string DescriptionKey { get; set; } 
} 
  • Pro: Non c'è bisogno di query complesse per ottenere un tradotta entità. System.Globalization gestisce i fallback per te.
  • Contro: Le traduzioni non possono essere facilmente gestite da un utente amministratore (è necessario distribuire i file di risorse ogni volta che il mio cambio s).

2) Utilizzare un tipo LocalizableString un'entità

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; } 
} 
  • Pro: Tutte le stringhe localizzate nella stessa tabella. La convalida può essere eseguita per stringa.
  • Contro: le query sono orribili. Devono. Includere la tabella LocalizedStrings una volta per ogni stringa localizzabile sull'entità padre. I fallback sono difficili e richiedono un'estesa adesione. Non ho trovato un modo per evitare N + 1 durante il recupero ad es. dati per una tabella.

3) Utilizzare un'entità padre con tutte le proprietà invarianti e entità figlio che contengono tutte le proprietà localizzate

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; } 
} 
  • Pro: Non è così difficile (ma ancora troppo duro) per ottenere! una traduzione completa di un'entità in memoria.
  • Contro: Raddoppia il numero di entità. Non è possibile gestire le traduzioni parziali di un'entità, in particolare nel caso in cui, ad esempio, il nome provenga da es ma la descrizione provenga da es-AR.

ho tre requisiti per una soluzione

  • gli utenti possono modificare le entità, lingue e traduzioni in fase di esecuzione

  • Gli utenti in grado di fornire traduzioni parziali con le stringhe mancanti provenienti da un ripiego come da System.Globalization

  • Le entità possono essere messe in memoria senza eseguire in ad es. N + 1 problemi

+0

Ancora non ho risposto, ero interessato anche io. – polkduran

+0

Non è chiaro quale considerereste una risposta accettabile. Se qualcuno ha un'opzione 4 è probabile che abbia anche pro/contro. – explunit

+0

Domanda chiarita. Non mi aspetto che ci sia una soluzione perfetta, ma spero ce ne sia uno migliore di quello che ho inventato finora. –

risposta

1

Perché non prendi il meglio di entrambi i mondi? Avere un CustomResourceManager che gestisce il caricamento delle risorse e scegliere la giusta cultura e utilizzare un CustomResourceReader che utilizza il backing store desiderato. Un'implementazione di base potrebbe assomigliare a questa, basandosi sulla convenzione del Resourceky che è Typename_PropertyName_PropertyValue. Se per qualche motivo la struttura del backingstore (csv/excel/mssql/struttura della tabella) ha bisogno di cambiare hai solo la modifica l'implementazione di ResourceReader.

Come bonus aggiuntivo ho anche ottenuto il proxy reale/trasparente.

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

reale/trasparente utilizzo di proxy

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

Sono preoccupato per le caratteristiche della query di questa soluzione. Se non sbaglio, o ho bisogno di caricare tutte le stringhe localizzate nell'applicazione in memoria (con tutti i problemi che ne derivano), o ho bisogno di chiamare MyRM.GetEntity su ogni singola entità - che sta per causare gravi problemi di N + 1 quando voglio visualizzare una tabella di entità. –

+0

Dipende. Se la pressione della memoria è la tua preoccupazione, potresti implementare una soluzione di caching intelligente che rimuova i resourceSet dalla memoria dopo x volta. O ci sono più problemi che non supervisiono ora? E hai ragione che devi chiamare GetEntity su ogni entità. Ma è o quello o ha una query complessa.Una cosa che ho provato ma che non riuscivo a far funzionare era un proxy dinamico o trasparente che espone i risultati localizzati dalle tue proprietà. Ciò integra perfettamente l'entità localizzata in qualsiasi codice corrente. E poi puoi concentrarti su un'implementazione tecnica che soddisfi le tue esigenze. – rene

+0

Il problema più urgente, penso, è quello di garantire che questa cache venga invalidata quando viene aggiornato l'archivio di lettura remoto. –

1

In primo luogo si è degno se si dispone di contenuto statico nel database. Ad esempio se hai categorie che relativamente non verranno modificate dall'utente. Puoi cambiarli alla prossima distribuzione. Io non questa soluzione personalmente. Non lo considero una buona soluzione. Questa è solo una via di fuga del problema.

Secondo uno è il migliore ma può causare un problema quando si dispone di due o più campi localizzabili in un'entità. È possibile semplificato un po 'e codificare lingue su come questo

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

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

Terzo uno non è una buona nessuno dei due. Da questa struttura non posso essere sicuro che tutti i nodi (letterali, linee, stringhe, ecc.) Siano tradotti in una cultura specifica.

Non generalizzare troppo. Ogni problema è specializzato e richiede anche soluzioni specializzate. Troppa generalizzazione crea problemi ingiustificati.

+0

Non ho voglia di de-normalizzare l'entità LocalizedString perché questo significherebbe che gli utenti admin non saranno in grado di aggiungere nuovi le lingue. Non penso che questo sia un problema di "generalizzazione troppo". Questo è un vero problema di business: i miei utenti spesso devono essere in grado di modificare entità, lingue, traduzioni senza l'intervento di uno sviluppatore. –

+0

Scusa se non ti ho capito bene. Non sapevo che tu volessi essere in grado di aggiungere anche delle lingue. – TIKSN