Интернационализация контента в платформе Entity Framework - программирование
Подтвердить что ты не робот

Интернационализация контента в платформе Entity Framework

Я все время сталкиваюсь с требованием i18n, где мои данные (а не мой пользовательский интерфейс) должны быть интернационализированы.

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) Сохраните ключ ресурса в db

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; }
}
  • Плюсы: все локализованные строки в одной таблице. Проверка может выполняться в строке.
  • Минусы: запросы ужасны. Обязательно. Включите таблицу 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.

У меня есть три требования к решению

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

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

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

4b9b3361

Ответ 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/Transparent 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;

Ответ 2

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

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

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

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

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

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