Локализация/I18n данных базы данных в LINQ to SQL

У меня есть таблицы состояния в моей базе данных и "локализованные" таблицы, содержащие языковые версии этих статусов. Точкой таблицы основного состояния является определение значений идентификатора состояния и других метаданных о статусе. "Локализованная" таблица предназначена для отображения текстового представления в пользовательском интерфейсе в соответствии с предпочтительным языком пользователей. Вот пример схемы:

create table [Language]
(
    ID smallint primary key,
    ISOName varchar(12)
)

create table EmployeeStatus
(
    ID smallint primary key,
    Code varchar(50)
)

create table EmployeeStatusLocalised
(
    EmployeeStatusID smallint,
    LanguageID smallint,
    Description varchar(50),
    constraint PK_EmployeeStatusLocalised primary key
        (EmployeeStatusID, LanguageID),
    constraint FK_EmployeeStatusLocalised_EmployeeStatus foreign key 
        (EmployeeStatusID) references EmployeeStatus (ID),
    constraint FK_EmployeeStatusLocalised_Language foreign key
        (LanguageID) references [Language] (ID)
)

create table Employee
(
    ID int identity(1,1) primary key,
    EmployeeName varchar(50) not null,
    EmployeeStatusID smallint not null,
    constraint FK_Employee_EmployeeStatus foreign key
        (EmployeeStatusID) references EmployeeStatus (ID)
)

Вот как я обычно обращаюсь к этим данным:

select e.EmployeeName, esl.Description as EmployeeStatus
from Employee e
inner join EmployeeStatusLocalised esl on
    e.EmployeeStatusID = esl.EmployeeStatusID and esl.LanguageID = 1

Я не очень доволен тем, что мой LINQ to SQL делает вещи наиболее эффективным способом. Вот пример:

using (var context = new MyDbDataContext())
{
    var item = (from record in context.Employees
                select record).Take(1).SingleOrDefault();

    Console.WriteLine("{0}: {1}", item.EmployeeName,
        item.EmployeeStatus.EmployeeStatusLocaliseds.
            Where(esl => esl.LanguageID == 1).Single().Description);
}

Ответ 1

Лично я мог бы оставить коды EmployeeStatus в БД и переместить всю логику локализации в клиент. Если это веб-приложение (ASP.NET или ASP.NET MVC), вы должны использовать код EmployeeStatus в качестве ключа в файле ресурсов, а затем использовать UICulture = "Авто" и "Культура =" Авто ", чтобы рассказать ASP.NET для подбора правильных ресурсов на основе HTTP-заголовка Accept-Language.

Вы предоставили неиспользуемые (нечувствительные к культуре) ресурсы, встроенные в ваше приложение, и разрешите сборкам satalite переопределять значения по умолчанию, где они нужны.

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

В идеале вы хотите, чтобы кто-то понимал, как переводить текст, чтобы сделать локализацию, и для них использовать какой-то инструмент, с которым им удобно. Существует множество инструментов .resx и приложений, которые позволяют языковым экспертам "делать свою работу".

Если вы застряли с таблицами БД для локализации, потому что "так оно и есть", то, возможно, вам следует запросить поиск отдельно к реальным данным и присоединиться к ним в пользовательском интерфейсе. Это по крайней мере даст вам "путь обновления" к .RESX в будущем.

Вы должны проверить книгу Гая Смита-Ферьера на i18n, если вы заинтересованы в этой области:

http://www.amazon.co.uk/NET-Internationalization-Developers-Guide-Building/dp/0321341384/ref=sr_1_1?ie=UTF8&s=books&qid=1239106912&sr=8-1

Ответ 2

Один из вариантов может заключаться в том, чтобы поддерживать кэш локализованных данных, используя что-то вроде кэширования блока приложений или кэширования ASP.NET, а затем просто ссылайтесь на этот кеш в представлении.

Это ограничило бы количество вызовов базы данных, потому что LINQ, возможно, не нужно будет загружать записи состояния, чтобы получить локализованное описание.

Ответ 3

Вы можете использовать LoadOptions в DataContext, поэтому он загружает данные в исходный запрос. Что-то вокруг строк:

var options = new DataLoadOptions();
options.AssociateWith<Employee>(e=>
    e.EmployeeStatus.EmployeeStatusLocaliseds
    .Where(esl => esl.LanguageID == 1)
    );
options.LoadWith<Employee>(e=>e.EmployeeStatus.EmployeeStatusLocaliseds);

    using (var context = new MyDbDataContext())
    {
        context.LoadOptions = options;
        var item = (from record in context.Employees
                    select record).Take(1).SingleOrDefault();

        Console.WriteLine("{0}: {1}", item.EmployeeName,
            item.EmployeeStatus.EmployeeStatusLocaliseds
            .Single().Description
        );
    }

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

Ответ 4

Кроме того: в этом случае я рассмотрел бы код статуса (а не id) как первичный ключ и имел бы Code денормализованный в Employee; это сохраняет внешний ключ, но уменьшает количество подключений и навигаций. Это также позволяет вам сопоставить код с enum в вашем коде .NET.

Я бы, вероятно, использовал ленивый (по запросу) кеш текстовых значений i18n; это:

  • минимизирует сложность запроса
  • минимизирует пропускную способность /IO
  • минимизирует накладные расходы на идентификацию объектов/изменений
  • минимизирует количество "материализаций", которые постоянно происходят.
  • позволяет вам программировать только против перечисления в объектной модели, но отображать текст i18n
  • абстрагирует реализацию i18n (чтобы вы могли переключиться на resx или аналогичный, если вам нужно, или на службу автоматического перевода)
  • позволяет получать значения i18n без привязки к существующему запросу данных - т.е. для заполнения раскрывающегося списка для экрана поиска (что не имеет ничего общего с отдельными сотрудниками, поэтому примерный запрос не помогает)

Я предполагаю, что данные i18n медленно меняются, поэтому подход к кэшу идеален.

Я бы загрузил все связанные строки по одному языку за раз - так что в первый раз, когда требуется статус в валлийском (например), я бы загрузил все строки состояния для валлийского языка и кеш к стандартному коду (cy) для языка.

Чтобы упростить просмотр кода, рассмотрите возможность использования метода расширения для сотрудника (на уровне пользовательского интерфейса):

public static class EmployeeExtensions {
    public static string GetStatusText(this Employee emp) {
         /* do your funky thing, presumably using the HttpContext or
         some other thread-static value to resolve the current culture */
    }
}

то на ваш взгляд вы можете использовать:

<%=emp.GetStatusText()%>

и т.д.. К сожалению, нет свойств расширения, но с помощью метода вы также имеете возможность передать языковой код в метод (после добавления параметра):

<%=emp.GetStatusText(lang)%>

Ответ 5

Я нашел неплохое решение для нашего сценария.

"Дизайн" выглядит следующим образом:

  • Создайте таблицу под названием "GlobalizedString" с идентификатором PK.
  • Создайте таблицу с названием "LocalizedString" со ссылкой на выше и следующие поля:
    • CultureId (ссылка на таблицу поиска культуры)
    • Содержимое (строка, которая будет находиться на указанном выше языке)

В GlobalizedString добавьте свойство "Content" (обратите внимание, что это не запрашивается через LINQ2SQL, но работает в LINQ2Objects), который выглядит так:

public string Content
{
  get { return LocalizedStrings.Single( 
           x => x.Culture.CultureCode == 'my current CultureInfo code'); }
  set { /* exercise for reader */ }
}

Итак, вместо столбцов nvarchar вместо этого вы указываете таблицу GlobalizedString.

Теперь вместо этого вы используете обычную "логику" (например, привязку), вы просто ссылаетесь на свойство Content для GlobalizedString, чтобы получить контент для текущего языка:)

Как уже говорилось ранее, это свойство Content не работает на Linq2SQL, и я все еще ищу легкий способ сделать это возможным (предложения приветствуются).

В противном случае система хорошо нас обслуживает:)