Лучшие практики для локализации базы данных SQL Server (2005/2008) - PullRequest
12 голосов
/ 03 ноября 2008

Вопрос

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

Например, классическая таблица «Категория» может иметь столбцы «Имя» и «Описание», которые должны быть глобализированы. Один из способов - создать таблицу «Текст» для каждой из ваших сущностей, а затем выполнить объединение, чтобы получить значения на основе предоставленного языка.

Это оставляет вам множество таблиц «Текст», по одной для каждой сущности, которую вы хотите локализовать, с добавлением TextType, чтобы различать различные тексты, которые он может хранить.

Мне любопытно, есть ли какие-либо документированные шаблоны передового опыта / дизайна для реализации такого рода поддержки в базе данных SQL Server 2005/2008 (я говорю конкретно о СУБД, поскольку она может содержать поддерживаемые ключевые слова и такой который помогает в реализации)?

Мысли о подходе XML

Одна идея, над которой я играл (хотя пока только в моей голове), состояла в том, чтобы использовать тип данных XML, представленный в SQL Server 2005. Идея состояла в том, чтобы создать столбцы, которые должны поддерживать локализацию, типа данных XML (и связать схема к нему). XML будет содержать локализованные строки вместе с языковым кодом / культурой, к которой он привязан.

Что-то вроде

Product
ID (int, identity)
Name (XML ...)
Description (XML ...)

Тогда у вас будет что-то вроде XML

<localization>
  <text culture="sv-SE">Detta är ett namn</text>
  <text culture="en-EN">This is a name</text>
</localization>

Вы можете сделать это (это не рабочий код, поэтому я буду использовать *)

SELECT *
From Product
Where Product.ID = 10

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

Кстати, какой бы метод я ни использовал в своем проекте, я все равно буду использовать Linq To SQL (.NET Platform) для запроса к базе данных (подход XML должен быть проблемой, так как он возвращает XElement, который может быть интерпретируемая сторона клиента)

Таким образом, предложение по шаблонам проектирования локализации базы данных и, возможно, комментарии к мысли XML, будут очень оценены.

Ответы [ 10 ]

3 голосов
/ 04 ноября 2008

Я думаю, что вы можете придерживаться XML, который учитывает более чистый дизайн. Я бы пошел дальше и воспользовался атрибутом xml:lang, который предназначен для этого использования :

<l10n>
  <text xml:lang="sv-SE">Detta är ett namn</text>
  <text xml:lang="en-EN">This is a name</text>
</l10n>

Еще один шаг, вы можете выбрать локализованный ресурс в своем запросе через запрос XPath (как это предлагается в комментариях), чтобы избежать какой-либо обработки на стороне клиента. Это даст что-то вроде этого (не проверено):

SELECT Name.value('(l10n/text[lang()="en"])[1]', 'NVARCHAR(MAX)')
  FROM Product
  WHERE Product.ID=10;

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

2 голосов
/ 09 ноября 2010

Вот как я это сделал. Я не использую LINQ или SP для этого, потому что запрос слишком сложен и динамически построен, и это всего лишь отрывок из запроса.

У меня есть таблица продуктов:

* id
* price
* stocklevel
* active
* name
* shortdescription
* longdescription

и таблица продуктов_глобализации:

* id
* products_id
* name
* shortdescription
* longdescription

Как видите, таблица продуктов также содержит все столбцы глобализации. Эти столбцы содержат язык по умолчанию (таким образом, возможность пропустить объединение при запросе культуры по умолчанию - НО я не уверен, стоит ли это усилий, я имею в виду объединение двух таблиц на основе индекса, поэтому. .. - дайте мне отзыв об этом).

Я предпочитаю иметь параллельную таблицу, а не глобальную ресурсную таблицу, потому что в некоторых ситуациях вам может потребоваться сделать то есть MATCH базы данных (MySQL) для нескольких столбцов, таких как MATCH (имя, краткое описание, длинное описание) ПРОТИВ («Нечто здесь»).

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

Псевдо:

string query = "";
if(string.IsNullOrEmpty(culture)) {
   // No culture specified, no join needed.
   query = "SELECT p.price, p.name, p.shortdescription FROM products p WHERE p.price > ?Price";
} else {
   query = "SELECT p.price, case when pg.name is null then p.name else pg.name end as 'name', case when pg.shortdescription is null then p.shortdescription else pg.shortdescription end as 'shortdescription' FROM products p"
   + " LEFT JOIN products_globalization pg ON pg.products_id = p.id AND pg.culture = ?Culture"
   + " WHERE p.price > ?Price";
}

Я бы пошел с COALESCE вместо CASE ELSE, но это помимо сути.

Ну, это мое мнение. Не стесняйтесь критиковать мое предложение ...

С уважением, Ричард

1 голос
/ 04 ноября 2008

Я не вижу преимуществ в использовании XML-столбцов для хранения локализованных значений. За исключением, возможно, того, что у вас есть все локализованные версии одного предмета «в одном месте», если это чего-то стоит для вас.

Я бы предложил использовать столбец cultureID в каждой таблице, в которой есть локализуемые элементы. Таким образом, вам совсем не нужна обработка XML. У вас уже есть данные в реляционной схеме, так зачем вводить еще один уровень сложности, если реляционная схема вполне способна решить проблему?

Допустим, у "sv-SE" есть cultureID = 1, а у "en-EN" - 2.

Тогда ваш запрос будет изменен как

SELECT *
From Product
Where Product.ID = 10 AND Product.cultureID = 1

для шведского клиента.

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

И еще один момент: XML-решение дает вам гибкость, которая вам не нужна: например, вы можете взять значение "sv-SE" из столбца "Name" и значение "en-EN" из колонки «Описание». Однако вам это не нужно, поскольку ваш клиент будет запрашивать только одну культуру за раз. Гибкость обычно имеет стоимость. В этом случае вам нужно проанализировать все столбцы по отдельности, в то время как с решением CultureID вы получите всю запись со всеми значениями, подходящими для требуемой культуры.

1 голос
/ 03 ноября 2008

Это один из вопросов, на которые трудно ответить, потому что в ответе так много «это зависит»: -)

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

В общих чертах я обычно использую один из двух подходов:

  1. Хранить локализованные элементы рядом с исполняемым файлом (локализованные библиотеки ресурсов)
  2. Храните локализованные элементы в БД и вводите столбец localeID в таблицах, содержащих локализованные элементы.

Преимущество первого метода - хорошая поддержка VisualStudio. Преимущество второго - централизованное развертывание.

1 голос
/ 03 ноября 2008

Не понимаю, зачем вам несколько текстовых таблиц. Одной текстовой таблицы с «глобально» уникальным текстовым идентификатором должно быть достаточно. Таблица будет иметь идентификатор, язык, текстовые столбцы, и вы получите текст только на том языке, который вам нужен (или, возможно, не получите текст вообще). Объединение должно быть достаточно эффективным, поскольку комбинация (ID, язык) является первичным ключом.

0 голосов
/ 06 августа 2010

Индексация становится проблемой. Я не думаю, что вы можете индексировать xml, и, конечно, вы не можете индексировать его, если храните его как строку, потому что каждая строка начинается с <localization> <text culture="...">.

0 голосов
/ 29 сентября 2009

Другой подход, который следует учитывать: не храните контент в базе данных, но сохраняйте «приложение», поддерживающее записи базы данных, и «контент» как отдельные объекты.

Я использовал подход, подобный этому, при создании нескольких тем для моего сайта электронной коммерции. Некоторые из продуктов имеют логотип производителя, который также должен соответствовать теме сайта. Поскольку нет реальной поддержки тем в базе данных, у меня возникла проблема. Решение, которое я придумал, состояло в том, чтобы использовать токен в базе данных для идентификации ClientID изображения, а не сохранять URL-адрес изображения (который будет варьироваться в зависимости от темы).

Следуя тому же подходу, вы могли бы изменить свою базу данных с хранения имени и описания продукта на хранение токена имени и описания, который бы идентифицировал ресурс (в файле resx или в базе данных, используя подход Рика Штрола) который содержит контент. Встроенная функциональность .NET будет затем обрабатывать переключение языка, а не пытаться сделать это в базе данных (редко бывает полезно поместить бизнес-логику в базу данных). Затем вы можете использовать токен на клиенте для поиска правильного ресурса.

Label1.Text = GetLocalResourceObject("TokenStoredInDatabase").ToString()

Недостатком этого подхода, очевидно, является синхронизация маркеров базы данных и маркеров ресурсов (поскольку продукты могут быть добавлены без каких-либо описаний), но потенциально это можно сделать проще с помощью поставщика ресурсов, такого как созданный Рик Штрал. Этот подход может не работать, если у вас есть продукты, которые часто меняются, но для некоторых людей он может.

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

Кстати, если вы работаете в магазине электронной коммерции и действительно хотите проиндексировать свои локализованные страницы, вам нужно немного отклониться от, казалось бы, естественного способа, созданного Microsoft. Существует явное несоответствие между практическим и логичным процессом разработки и тем, что Google рекомендует для SEO. Действительно, некоторые веб-мастера жаловались, что их страницы не индексируются поисковыми системами ни для чего, кроме культуры «по умолчанию», потому что поисковые системы будут индексировать только один URL-адрес один раз, даже если он изменяется в зависимости от культуры браузера.

К счастью, есть простой способ обойти это: разместить ссылки на странице, чтобы перевести ее на другие языки на основе параметра строки запроса. Пример этого можно найти (к сожалению, они не позволят мне опубликовать еще одну ссылку !!), и если вы проверите, каждая культура страницы была проиндексирована как Google, так и Yahoo (хотя не Bing). Более продвинутый подход может использовать перезапись URL в сочетании с некоторыми причудливыми регулярными выражениями, чтобы ваша локализованная страница выглядела так, как будто она имеет несколько каталогов, но вместо этого фактически передает на страницу параметр querystring.

0 голосов
/ 13 сентября 2009

Я вижу общую проблему - у вас есть одна сущность, которую вы должны представить как один экземпляр (например, один ProductID из "10"), но у вас есть несколько локализованных текстов разных столбцов / свойств. Это сложный вопрос, и я вижу необходимость в POS-системах, чтобы вы хотели отслеживать только один ProductID = 10, а не несколько продуктов, которые имеют разные ProductID, но это одно и то же с другим текстом.

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

Основным недостатком является объем данных, передаваемых по проводам из БД в сервисный уровень / UI / App. Я бы попытался выполнить некоторые преобразования в конце SQL перед возвратом результата, чтобы вернуть только один пользовательский интерфейс культуры. Вы всегда можете просто ВЫБРАТЬ текущую культуру через xml в sproc и вернуть ее как обычный текст.

В целом, это отличается от, скажем, сообщения блога или CMS для локализации - что я и сделал несколько.

Мой подход к Post scenerio был бы аналогичен подходу TToni, за исключением моделирования данных с точки зрения домена (и немного BDD). С учетом вышесказанного сосредоточьтесь на том, чего вы хотите достичь:

Given a users culture is "sv-se"
When the user views a post list
It should list posts only in "sv-se" culture

Это означает, что пользователь должен видеть список сообщений только для своей культуры. Способ, которым мы реализовали это раньше, состоял в том, чтобы передать набор культур для запроса на основе того, что мог видеть пользователь. Если пользователь установил sv-se в качестве основного, но также выбрал, что он говорит по-английски (en-us), запрос будет:

SELECT * FROM Post WHERE CultureUI IN ('sv-se', 'en-us')

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

Но чтобы вернуться к 1-й части моего ответа, ваша потребность связана с требованием наличия одного экземпляра с несколькими текстами. XML-столбец хорошо подходит для этого.

0 голосов
/ 14 мая 2009

Вот некоторые соображения в блоге Рика Строля:

Локализация базы данных Локализация JavaScript

Я предпочитаю использовать один переключатель в таблице UserSetting, который используется при вызове хранимой процедуры ... вот часть кода

CREATE TABLE [dbo].[Lang_en_US_Msg](
    [MsgId] [int] IDENTITY(1,1) NOT NULL,
    [MsgKey] [varchar](200) NOT NULL,
    [MsgTxt] [varchar](2000) NOT NULL,
    [MsgDescription] [varchar](2000) NOT NULL,
 CONSTRAINT [PK_Lang_US-us__Msg] PRIMARY KEY CLUSTERED 
(
    [MsgId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

CREATE TABLE [dbo].[User](
    [UserId] [int] IDENTITY(1,1) NOT NULL,
    [FirstName] [varchar](50) NOT NULL,
    [MiddleName] [varchar](50) NULL,
    [LastName] [varchar](50) NULL,
    [DomainName] [varchar](50) NULL,
 CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED 
(
    [UserId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE TABLE [dbo].[UserSetting](
    [UserSettingId] [int] IDENTITY(1,1) NOT NULL,
    [UserId] [int] NOT NULL,
    [CultureInfo] [varchar](50) NOT NULL,
    [GuiLanguage] [varchar](10) NOT NULL,
 CONSTRAINT [PK_UserSetting] PRIMARY KEY CLUSTERED 
(
    [UserSettingId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

 ALTER TABLE [dbo].[UserSetting] ADD  CONSTRAINT [DF_UserSetting_CultureInfo]  DEFAULT ('fi-FI') FOR [CultureInfo]
 GO

 CREATE TABLE [dbo].[Lang_fi_FI_Msg](
    [MsgId] [int] IDENTITY(1,1) NOT NULL,
    [MsgKey] [varchar](200) NOT NULL,
    [MsgTxt] [varchar](2000) NOT NULL,
    [MsgDescription] [varchar](2000) NOT NULL,
    [DbSysNameForExpansion] [varchar](50) NULL,
 CONSTRAINT [PK_Lang_Fi-fi__Msg] PRIMARY KEY CLUSTERED 
(
    [MsgId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE PROCEDURE [dbo].[procGui_GetPageMsgs]
@domainUser varchar(50) ,           -- the domain_user performing the action  
@msgOut varchar(4000) OUT,        -- the (error) msg to be shown to the user   
@debugMsgOut varchar(4000) OUT ,   -- this variable holds the debug msg to be shown if debug level is enabled   
@ret int OUT                  -- the variable indicating success or failure 

AS                            
BEGIN -- proc start                            
 SET NOCOUNT ON;                            

declare @procedureName varchar(200)        
declare @procStep varchar(4000)  


set @procedureName = ( SELECT OBJECT_NAME(@@PROCID))        
set @msgOut = ' '     
set @debugMsgOut = ' '     
set @procStep = ' '     


BEGIN TRY        --begin try                  
set @ret = 1 --assume false from the beginning                  

--===============================================================
 --debug   set @procStep=@procStep + 'GETTING THE GUI LANGUAGE FOR THIS USER '
--===============================================================

declare @guiLanguage nvarchar(10)




if ( @domainUser is null)
    set @guiLanguage = (select Val from AppSetting where Name='guiLanguage')
else 
    set @guiLanguage = (select GuiLanguage from UserSetting us join [User] u on u.UserId = us.UserId where u.DomainName=@domainUser)

set @guiLanguage = REPLACE ( @guiLanguage , '-' , '_' ) ;


--===============================================================
set @procStep=@procStep + ' BUILDING THE SQL QUERY '
--===============================================================

DECLARE @sqlQuery AS nvarchar(2000)
SET @sqlQuery = 'SELECT  MsgKey , MsgTxt FROM dbo.lang_' + @guiLanguage + '_Msg'


--===============================================================
set @procStep=@procStep + 'EXECUTING THE SQL QUERY'
--===============================================================
print @sqlQuery

    exec sp_executesql @sqlQuery

    set @debugMsgOut = @procStep
    set @ret = @@ERROR                  


END TRY        --end try                  

BEGIN CATCH                        
 PRINT 'In CATCH block.                         
 Error number: ' + CAST(ERROR_NUMBER() AS varchar(10)) + '                        
 Error message: ' + ERROR_MESSAGE() + '                        
 Error severity: ' + CAST(ERROR_SEVERITY() AS varchar(10)) + '                        
 Error state: ' + CAST(ERROR_STATE() AS varchar(10)) + '                        
 XACT_STATE: ' + CAST(XACT_STATE() AS varchar(10));                        

set @msgOut = 'Failed to execute ' + @sqlQuery             
set @debugMsgOut = ' Error number: ' + CAST(ERROR_NUMBER() AS varchar(10)) +               
 'Error message: ' + ERROR_MESSAGE() + 'Error severity: ' + CAST(ERROR_SEVERITY() AS varchar(10)) +               
 'Error state: ' + CAST(ERROR_STATE() AS varchar(10)) + 'XACT_STATE: ' + CAST(XACT_STATE() AS varchar(10))                        

--record the error in the database                        
--debug    
 --EXEC [dbo].[procUtils_DebugDb]
    --  @DomainUser = @domainUser,
    --  @debugmsg = @debugMsgOut,
    --  @ret = 1,
    --  @procedureName = @procedureName ,
    --  @procedureStep = @procStep

 -- set @ret = 1                       

END CATCH                        


return  @ret                                   
END --procedure end                             
0 голосов
/ 17 декабря 2008

Мне нравится подход XML, потому что решение с отдельной таблицей НЕ вернет результат, если, например, нет шведского перевода (cultureID = 1), если вы не выполните внешнее соединение. Но тем не менее вы не можете вернуться к английскому языку. С подходом XML вы можете просто вернуться к английскому языку. Любые новости о подходе XML в производственной среде?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...