2012-06-18 19 views
16

Ciągle spotykam się z wymogiem i18n, w którym moje dane (nie mój interfejs) muszą być umiędzynarodowione.Internacjonalizacja treści w 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 
} 

Jakie metody mogę zastosować?

Niektóre rzeczy Próbowałem: -

1) Przechowywać kluczowym zasobem w db

public class FooEntity 
{ 
    ... 
    public string NameKey { get; set; } 
    public string DescriptionKey { get; set; } 
} 
  • Plusy: Nie ma potrzeby stosowania skomplikowanych zapytań, aby uzyskać przetłumaczone jednostka. System.Globalization obsługuje awarie dla ciebie.
  • Wady: Tłumaczenia nie mogą być łatwo zarządzane przez administratora (muszą wdrażać pliki zasobów, gdy tylko zmienią się moje Foo s).

2) Za pomocą jednostki typu LocalizableString

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 Wszystkie lokalizowane łańcuchy w tej samej tabeli. Sprawdzanie poprawności może być wykonywane na łańcuchu.
  • Minusy: zapytania są okropne. Należy .Zawiera tabelę LocalizedStrings jeden raz dla każdego zlokalizowanego ciągu znaków na elemencie nadrzędnym. Fallouty są trudne i wiążą się z dużym łączeniem. Nie znalazłem sposobu na uniknięcie N + 1 podczas pobierania np. dane dla tabeli.

3) Za pomocą jednostki dominującej ze wszystkimi niezmiennych właściwościach i jednostek podrzędnych zawierających wszystkie miejscowe właściwości

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; } 
} 
  • Plusy: Nie tak trudne (ale wciąż zbyt trudne), aby dostać! pełne tłumaczenie jednostki na pamięć.
  • Przeciw: Podwójna liczba elementów. Nie można obsłużyć częściowych tłumaczeń obiektu - szczególnie w przypadku, gdy, powiedzmy, nazwa pochodzi od es, ale opis nadchodzi od es-AR.

Mam trzy wymagania dotyczące rozwiązania

  • użytkownicy mogą edytować podmioty, języków i tłumaczenia w czasie wykonywania

  • Użytkownicy mogą dostarczyć częściowe tłumaczenia z brakujących ciągów pochodzących z awaryjnej zgodnie z System.Globalization

  • Elementy można wprowadzić do pamięci bez uruchamiania na przykład do Wydania N + 1

+0

Nadal nie otrzymałem odpowiedzi, również mnie zainteresowałem. – polkduran

+0

Nie jest jasne, co byś uznał za akceptowalną odpowiedź. Jeśli ktoś ma opcję 4, prawdopodobnie będzie miał również za i przeciw. – explunit

+0

Wyjaśnione pytanie. Nie spodziewam się, że będzie to idealne rozwiązanie, ale mam nadzieję, że jest jeszcze lepsze rozwiązanie niż dotychczas. –

Odpowiedz

1

Dlaczego nie weźmiesz najlepszych z obu światów? Posiadaj CustomResourceManager, który obsługuje ładowanie zasobów i wybiera odpowiednią kulturę i używa CustomResourceReader, który używa dowolnego sklepu, który lubisz. Podstawowa implementacja mogłaby wyglądać tak, opierając się na konwencji Resourceky to Typename_PropertyName_PropertyValue. Jeśli z jakiegoś powodu struktura sklepu (csv/excel/mssql/struktura tabeli) musi się zmienić, musisz tylko zmienić implementację ResourceReadera.

Jako dodatkowy bonus otrzymałem również prawdziwe/transparentne proxy.

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 
    } 

Zastosowanie

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

Prawdziwe wykorzystanie proxy/Transparent

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

Martwię się o cechy zapytania tego rozwiązania. Jeśli się nie mylę, to albo muszę załadować wszystkie zlokalizowane ciągi w aplikacji do pamięci (z wszystkimi problemami, które się z tym wiążą), albo muszę zadzwonić do MyRM.GetEntity na każdą pojedynczą jednostkę - co spowoduje poważne problemy z N + 1, gdy chcę wyświetlić tabelę podmiotów. –

+0

To zależy. Jeśli masz problem z pamięcią, możesz zaimplementować inteligentne rozwiązanie do buforowania, które usunie zasoby z pamięci po x czasie. Czy jest więcej problemów, których teraz nie nadzoruję? I masz rację, że musisz zadzwonić do GetEntity na każdej jednostce. Ale jest to albo złożone zapytanie.Jedna rzecz, którą próbowałem, ale nie mogłem pracować, to dynamiczny lub transparentproxy, który eksponuje zlokalizowane wyniki z twoich właściwości. To integruje zlokalizowaną jednostkę w dowolnym bieżącym kodzie w sposób płynny. Następnie możesz skupić się na technicznej implementacji, która spełnia Twoje wymagania. – rene

+0

Wydaje mi się, że pilniejszą kwestią jest zapewnienie, że pamięć podręczna zostanie unieważniona po zaktualizowaniu magazynu zdalnego odczytu. –

1

Pierwszy jest warta, jeśli masz statyczny w bazie. Na przykład, jeśli masz kategorii, które względnie nie zostaną zmienione przez użytkownika. Możesz je zmienić przy następnym wdrożeniu. Nie stosuję tego rozwiązania osobiście. Nie uważam tego za dobre rozwiązanie. To tylko ucieczka problemu.

Drugi to jest najlepszy, ale może powodować problemy, gdy masz dwa lub więcej zlokalizowanych pól w jednym obiekcie. Można uprościć nieco i trudno kodu w różnych językach w tym tak

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

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

trzeci nie jest dobry ani. Z tej struktury nie mogę być pewny, że wszystkie węzły (literały, linie, ciągi itd.) Przełożyły się na określoną kulturę.

Nie generalizuj zbyt wiele. Każdy problem jest specjalistyczny i wymaga specjalistycznego rozwiązania. Zbyt duża generalizacja powoduje nieuzasadnione problemy.

+0

Nie mam ochoty znormalizować istoty LocalizedString, ponieważ będzie to oznaczać, że administratorzy nie będą mogli dodawać nowych Języki. Nie sądzę, że jest to problem "uogólniający zbyt wiele". Jest to prawdziwy problem biznesowy - moi użytkownicy często muszą mieć możliwość edytowania encji, języków, tłumaczeń bez udziału programisty. –

+0

Niestety nie zrozumiano cię poprawnie. Nie wiedziałem, że chcesz też móc dodawać języki. – TIKSN