2012-06-18 4 views
16

Я постоянно сталкиваюсь с требованием i18n, когда мои данные (а не мой пользовательский интерфейс) должны быть интернационализированы.Интернационализация содержимого в платформе 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 
} 

Каковы некоторые подходы, которые я мог бы использовать?

Некоторые вещи, которые я пробовал: -

1) Храните ключ ресурса в БД

public class FooEntity 
{ 
    ... 
    public string NameKey { get; set; } 
    public string DescriptionKey { get; set; } 
} 
  • Плюсы: Нет необходимости в сложных запросов, чтобы получить переведенную организация. System.Globalization обрабатывает резервные копии для вас.
  • Недостатки: Переводы не могут быть легко выполнены администратором (необходимо развернуть файлы ресурсов всякий раз, когда меняет Foo с).

2) Используйте тип 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; } 
} 
  • Pros: Все локализованные строки в одной таблице. Проверка может выполняться в строке.
  • Против: запросы ужасные. Обязательно. Включите таблицу LocalizedStrings раз для каждой локализуемой строки в родительском объекте. Резервы сложны и связаны с широким объединением. Не удалось найти способ избежать N + 1 при получении, например. данные для таблицы.

3) Используйте родительский объект со всеми инвариантными свойствами и дочерних сущностей, содержащих все локализованные свойства

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; } 
} 
  • Плюсы: Не так трудно (но все еще слишком сильно), чтобы получить! полный перевод сущности в память.
  • Против: удвоить количество сущностей. Невозможно обрабатывать частичные переводы объекта - особенно в том случае, когда, скажем, имя исходит от es, но описание исходит от es-AR.

У меня есть три требования для решения

  • Пользователей могут редактировать сущности, язык и переводы во время выполнения

  • Пользователей могут поставлять частичные переводы недостающих строк, поступающих из запасного варианта в соответствии с Системой. Глобализация

  • Объекты могут быть внесены в память без запуска например, N + 1 выпусков

+0

Еще не ответил, меня тоже интересовало. – polkduran

+0

Непонятно, что вы считаете приемлемым ответом. Если у кого-то есть вариант 4, у него наверняка будут плюсы и минусы. – explunit

+0

Уточненный вопрос. Я не ожидаю, что там будет идеальное решение, но, надеюсь, есть один лучше, чем я придумал. –

ответ

1

Почему вы не берете лучшее из обоих миров? Имейте CustomResourceManager, который обрабатывает загрузку ресурсов и выбирает правильную культуру и использует CustomResourceReader, который использует любой резервный магазин, который вам нравится. Основная реализация может выглядеть так, опираясь на соглашение Resourceky, которое является Typename_PropertyName_PropertyValue. Если по какой-то причине структура резервного хранилища (csv/excel/mssql/table structure) должна измениться, вы измените только реализацию ResourceReader.

В качестве дополнительного бонуса я также получил реальный/прозрачный прокси-сервер.

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 
    } 

Использование

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/Прозрачное использование прокси

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

Я беспокоюсь о характеристиках запроса этого решения. Если я не ошибаюсь, либо мне нужно загрузить все локализованные строки в приложении в память (со всеми вытекающими из этого проблемами), либо мне нужно вызвать MyRM.GetEntity для каждого отдельного объекта, что будет причиной серьезные проблемы с N + 1, когда я хочу отобразить таблицу сущностей. –

+0

Всё зависит. Если вы беспокоитесь о давлении памяти, вы можете реализовать интеллектуальное кэширующее решение, которое удаляет ресурсные наборы из памяти через x время. Или есть еще проблемы, которые я сейчас не наблюдаю? И вы правы, что вам нужно вызвать GetEntity для каждого объекта. Но это либо то, либо сложный запрос.Одна вещь, которую я пробовал, но не могу работать, - это либо динамический, либо прозрачныйпрокси, который предоставляет локализованные результаты из ваших свойств. Это легко интегрирует локализованный объект в любой текущий код. Затем вы можете сосредоточиться на технической реализации, которая удовлетворяет вашим требованиям. – rene

+0

Более насущной проблемой, я думаю, является чистое обеспечение того, что этот кеш становится недействительным, когда удаленный магазин чтения обновляется. –

1

Первый один достоин, если у вас есть статический контент в базе данных. Например, если у вас есть категории, которые относительно не будут изменены пользователем. Вы можете изменить их при следующем развертывании. Я не принимаю это решение лично. Я не считаю это хорошим решением. Это просто побег проблемы.

Второй является лучшим, но может вызвать проблему, когда у вас есть два или более локализуемых поля в одном объекте. Вы можете немного упрощен и жесткий код языка на нем, как этот

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

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

третий не является хорошим ни. Из этой структуры я не могу быть уверенным, что все узлы (литералы, строки, строки и т. Д.) Переведены в специфическую культуру.

Не обобщайте слишком много. Каждая проблема является специализированной и нуждается в специализированном решении. Слишком большое обобщение вызывает неоправданные проблемы.

+0

Я не хочу де-нормализировать объект LocalizedString, поскольку это будет означать, что пользователи-пользователи не смогут добавлять новые языки. Я не думаю, что это «слишком большая проблема». Это настоящая деловая проблема - мои пользователи часто должны иметь возможность редактировать сущности, языки, переводы без участия разработчика. –

+0

Жаль, что я вас не понял. Я не знал, что вы хотите иметь возможность добавлять языки. – TIKSN

Смежные вопросы