Схема для мультиязычной базы данных - PullRequest
220 голосов
/ 25 ноября 2008

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

Но каков наилучший подход при определении многоязычной схемы базы данных? Допустим, у нас есть много таблиц (100 или более), и каждая таблица может иметь несколько столбцов, которые могут быть локализованы (большинство столбцов nvarchar должны быть локализуемыми). Например, одна из таблиц может содержать информацию о продукте:

CREATE TABLE T_PRODUCT (
  NAME        NVARCHAR(50),
  DESCRIPTION NTEXT,
  PRICE       NUMBER(18, 2)
)

Я могу представить три подхода для поддержки многоязычного текста в столбцах ИМЯ и ОПИСАНИЕ:

  1. Отдельный столбец для каждого языка

    Когда мы добавляем новый язык в систему, мы должны создать дополнительные столбцы для хранения переведенного текста, например:

    CREATE TABLE T_PRODUCT (
      NAME_EN        NVARCHAR(50),
      NAME_DE        NVARCHAR(50),
      NAME_SP        NVARCHAR(50),
      DESCRIPTION_EN NTEXT,
      DESCRIPTION_DE NTEXT,
      DESCRIPTION_SP NTEXT,
      PRICE          NUMBER(18,2)
    )
    
  2. Таблица перевода с колонками для каждого языка

    Вместо хранения переведенного текста сохраняется только внешний ключ в таблице переводов. Таблица переводов содержит столбец для каждого языка.

    CREATE TABLE T_PRODUCT (
      NAME_FK        int,
      DESCRIPTION_FK int,
      PRICE          NUMBER(18, 2)
    )
    
    CREATE TABLE T_TRANSLATION (
      TRANSLATION_ID,
      TEXT_EN NTEXT,
      TEXT_DE NTEXT,
      TEXT_SP NTEXT
    )
    
  3. Таблицы перевода со строками для каждого языка

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

    CREATE TABLE T_PRODUCT (
      NAME_FK        int,
      DESCRIPTION_FK int,
      PRICE          NUMBER(18, 2)
    )
    
    CREATE TABLE T_TRANSLATION (
      TRANSLATION_ID
    )
    
    CREATE TABLE T_TRANSLATION_ENTRY (
      TRANSLATION_FK,
      LANGUAGE_FK,
      TRANSLATED_TEXT NTEXT
    )
    
    CREATE TABLE T_TRANSLATION_LANGUAGE (
      LANGUAGE_ID,
      LANGUAGE_CODE CHAR(2)
    )
    

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

Ответы [ 10 ]

108 голосов
/ 27 ноября 2008

Что вы думаете о наличии связанной таблицы перевода для каждой переводимой таблицы?

CREATE TABLE T_PRODUCT (pr_id int, НОМЕР ЦЕНЫ (18, 2))

CREATE TABLE T_PRODUCT_tr (pr_id INT FK, код языка varchar, текст pr_name, текст pr_descr)

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

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

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

48 голосов
/ 26 сентября 2014

Это интересный вопрос, так что давайте разберемся.

Давайте начнем с задач метода 1:
Проблема: Вы денормализуете, чтобы сохранить скорость.
В SQL (кроме PostGreSQL с hstore) нельзя передать язык параметров и сказать:

SELECT ['DESCRIPTION_' + @in_language]  FROM T_Products

Итак, вы должны сделать это:

SELECT 
    Product_UID 
    ,
    CASE @in_language 
        WHEN 'DE' THEN DESCRIPTION_DE 
        WHEN 'SP' THEN DESCRIPTION_SP 
        ELSE DESCRIPTION_EN 
    END AS Text 
FROM T_Products 

Это означает, что вы должны изменить ВСЕ ваши запросы, если вы добавите новый язык. Это, естественно, приводит к использованию «динамического SQL», поэтому вам не нужно изменять все ваши запросы.

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

CREATE PROCEDURE [dbo].[sp_RPT_DATA_BadExample]
     @in_mandant varchar(3) 
    ,@in_language varchar(2) 
    ,@in_building varchar(36) 
    ,@in_wing varchar(36) 
    ,@in_reportingdate varchar(50) 
AS
BEGIN
    DECLARE @sql varchar(MAX), @reportingdate datetime

    -- Abrunden des Eingabedatums auf 00:00:00 Uhr
    SET @reportingdate = CONVERT( datetime, @in_reportingdate) 
    SET @reportingdate = CAST(FLOOR(CAST(@reportingdate AS float)) AS datetime)
    SET @in_reportingdate = CONVERT(varchar(50), @reportingdate) 

    SET NOCOUNT ON;


    SET @sql='SELECT 
         Building_Nr AS RPT_Building_Number 
        ,Building_Name AS RPT_Building_Name 
        ,FloorType_Lang_' + @in_language + ' AS RPT_FloorType 
        ,Wing_No AS RPT_Wing_Number 
        ,Wing_Name AS RPT_Wing_Name 
        ,Room_No AS RPT_Room_Number 
        ,Room_Name AS RPT_Room_Name 
    FROM V_Whatever 
    WHERE SO_MDT_ID = ''' + @in_mandant + ''' 

    AND 
    ( 
        ''' + @in_reportingdate + ''' BETWEEN CAST(FLOOR(CAST(Room_DateFrom AS float)) AS datetime) AND Room_DateTo 
        OR Room_DateFrom IS NULL 
        OR Room_DateTo IS NULL 
    ) 
    '

    IF @in_building    <> '00000000-0000-0000-0000-000000000000' SET @sql=@sql + 'AND (Building_UID  = ''' + @in_building + ''') '
    IF @in_wing    <> '00000000-0000-0000-0000-000000000000' SET @sql=@sql + 'AND (Wing_UID  = ''' + @in_wing + ''') '

    EXECUTE (@sql) 

END


GO

Проблема с этим
а) Форматирование даты зависит от языка, поэтому у вас возникает проблема, если вы не вводите в формате ISO (чего обычно не делает средний программист, работающий с садом, а в случае отчета пользователь уверен как, черт возьми, не будет делать для вас, даже если явно указано, чтобы сделать это).
и
б) наиболее значительно , вы теряете любую проверку синтаксиса . Если <insert name of your "favourite" person here> изменяет схему из-за внезапного изменения требований к крылу, и создается новая таблица, старая остается, но поле ссылки переименовывается, вы не получаете никаких предупреждений. Отчет даже работает , когда вы его запускаете, не выбрав параметр wing (==> guid.empty). Но неожиданно, когда реальный пользователь фактически выбирает крыло ==> boom . Этот метод полностью нарушает любой вид тестирования.

<ч />

Метод 2:
В двух словах: «Отличная» идея (предупреждение - сарказм), давайте объединим недостатки метода 3 (низкая скорость при большом количестве записей) с довольно ужасными недостатками метода 1.
Единственным преимуществом этого метода является то, что вы храните все переводы в одной таблице и, следовательно, делаете обслуживание простым. Однако то же самое может быть достигнуто с помощью метода 1 и хранимой процедуры динамического SQL, а также (возможно, временной) таблицы, содержащей переводы и имя целевой таблицы (и это довольно просто, если вы назвали все свои текстовые поля то же самое).

<ч />

Метод 3:
Одна таблица для всех переводов: Недостаток: Вы должны хранить n внешних ключей в таблице продуктов для n полей, которые вы хотите перевести. Следовательно, вы должны сделать n объединений для n полей. Когда таблица перевода является глобальной, в ней много записей, и соединения становятся медленными. Кроме того, вы всегда должны присоединиться к таблице T_TRANSLATION n раз для n полей. Это довольно накладные расходы. Теперь, что вы делаете, когда вы должны приспособить индивидуальные переводы для каждого клиента? Вам нужно будет добавить еще 2x n объединений на дополнительную таблицу. Если вам нужно объединить, скажем, 10 таблиц с 2x2xn = 4n дополнительных объединений, что за беспорядок! Также этот дизайн позволяет использовать один и тот же перевод с 2 таблицами. Если я изменю имя элемента в одной таблице, действительно ли я хочу изменить запись в другой таблице также КАЖДЫЙ ОДИН РАЗ?

Кроме того, вы больше не можете удалять и повторно вставлять таблицу, потому что в ТАБЛИЦЕ ПРОДУКТА теперь есть внешние ключи ... вы, конечно, можете опустить установку FK, и тогда <insert name of your "favourite" person here> может удалить таблицу, и вставьте все записи с помощью newid () [или указав идентификатор в вставке, но имея identity-insert OFF ], и это приведет (и приведет) к мусору данных (и исключениям нулевой ссылки) очень скоро.

<ч /> Способ 4 (не указан): Хранение всех языков в поле XML в базе данных. * 1038 например *

-- CREATE TABLE MyTable(myfilename nvarchar(100) NULL, filemeta xml NULL )


;WITH CTE AS 
(
      -- INSERT INTO MyTable(myfilename, filemeta) 
      SELECT 
             'test.mp3' AS myfilename 
            --,CONVERT(XML, N'<?xml version="1.0" encoding="utf-16" standalone="yes"?><body>Hello</body>', 2) 
            --,CONVERT(XML, N'<?xml version="1.0" encoding="utf-16" standalone="yes"?><body><de>Hello</de></body>', 2) 
            ,CONVERT(XML
            , N'<?xml version="1.0" encoding="utf-16" standalone="yes"?>
<lang>
      <de>Deutsch</de>
      <fr>Français</fr>
      <it>Ital&amp;iano</it>
      <en>English</en>
</lang>
            ' 
            , 2 
            ) AS filemeta 
) 

SELECT 
       myfilename
      ,filemeta
      --,filemeta.value('body', 'nvarchar') 
      --, filemeta.value('.', 'nvarchar(MAX)') 

      ,filemeta.value('(/lang//de/node())[1]', 'nvarchar(MAX)') AS DE
      ,filemeta.value('(/lang//fr/node())[1]', 'nvarchar(MAX)') AS FR
      ,filemeta.value('(/lang//it/node())[1]', 'nvarchar(MAX)') AS IT
      ,filemeta.value('(/lang//en/node())[1]', 'nvarchar(MAX)') AS EN
FROM CTE 

Затем вы можете получить значение с помощью XPath-Query в SQL, где вы можете поместить строковую переменную в

filemeta.value('(/lang//' + @in_language + '/node())[1]', 'nvarchar(MAX)') AS bla

И вы можете обновить значение следующим образом:

UPDATE YOUR_TABLE
SET YOUR_XML_FIELD_NAME.modify('replace value of (/lang/de/text())[1] with "&quot;I am a ''value &quot;"')
WHERE id = 1 

Где вы можете заменить /lang/de/... на '.../' + @in_language + '/...'

Вроде как в PostGre hstore, за исключением того, что из-за накладных расходов на синтаксический анализ XML (вместо чтения записи из ассоциативного массива в PG hstore) он становится слишком медленным, а кодировка xml делает его слишком болезненным, чтобы быть полезным.

<ч /> Метод 5 (в соответствии с рекомендацией SunWuKung, тот, который вы должны выбрать): Одна таблица перевода для каждой таблицы «Продукт». Это означает одну строку на язык и несколько «текстовых» полей, поэтому требуется только ОДНО (слева) соединение по N полей. Затем вы можете легко добавить поле по умолчанию в «Product» -таблицу, вы можете легко удалить и повторно вставить таблицу перевода, и вы можете создать вторую таблицу для пользовательских переводов (по запросу), которую вы также можете удалить и вставьте заново), и у вас все еще есть все внешние ключи.

Давайте сделаем пример, чтобы увидеть это РАБОТЫ:

Сначала создайте таблицы:

CREATE TABLE dbo.T_Languages
(
     Lang_ID int NOT NULL
    ,Lang_NativeName national character varying(200) NULL
    ,Lang_EnglishName national character varying(200) NULL
    ,Lang_ISO_TwoLetterName character varying(10) NULL
    ,CONSTRAINT PK_T_Languages PRIMARY KEY ( Lang_ID )
);

GO




CREATE TABLE dbo.T_Products
(
     PROD_Id int NOT NULL
    ,PROD_InternalName national character varying(255) NULL
    ,CONSTRAINT PK_T_Products PRIMARY KEY ( PROD_Id )
); 

GO



CREATE TABLE dbo.T_Products_i18n
(
     PROD_i18n_PROD_Id int NOT NULL
    ,PROD_i18n_Lang_Id int NOT NULL
    ,PROD_i18n_Text national character varying(200) NULL
    ,CONSTRAINT PK_T_Products_i18n PRIMARY KEY (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id)
);

GO

-- ALTER TABLE dbo.T_Products_i18n  WITH NOCHECK ADD  CONSTRAINT FK_T_Products_i18n_T_Products FOREIGN KEY(PROD_i18n_PROD_Id)
ALTER TABLE dbo.T_Products_i18n  
    ADD CONSTRAINT FK_T_Products_i18n_T_Products 
    FOREIGN KEY(PROD_i18n_PROD_Id)
    REFERENCES dbo.T_Products (PROD_Id)
ON DELETE CASCADE 
GO

ALTER TABLE dbo.T_Products_i18n CHECK CONSTRAINT FK_T_Products_i18n_T_Products
GO

ALTER TABLE dbo.T_Products_i18n 
    ADD  CONSTRAINT FK_T_Products_i18n_T_Languages 
    FOREIGN KEY( PROD_i18n_Lang_Id )
    REFERENCES dbo.T_Languages( Lang_ID )
ON DELETE CASCADE 
GO

ALTER TABLE dbo.T_Products_i18n CHECK CONSTRAINT FK_T_Products_i18n_T_Products
GO



CREATE TABLE dbo.T_Products_i18n_Cust
(
     PROD_i18n_Cust_PROD_Id int NOT NULL
    ,PROD_i18n_Cust_Lang_Id int NOT NULL
    ,PROD_i18n_Cust_Text national character varying(200) NULL
    ,CONSTRAINT PK_T_Products_i18n_Cust PRIMARY KEY ( PROD_i18n_Cust_PROD_Id, PROD_i18n_Cust_Lang_Id )
);

GO

ALTER TABLE dbo.T_Products_i18n_Cust  
    ADD CONSTRAINT FK_T_Products_i18n_Cust_T_Languages 
    FOREIGN KEY(PROD_i18n_Cust_Lang_Id)
    REFERENCES dbo.T_Languages (Lang_ID)

ALTER TABLE dbo.T_Products_i18n_Cust CHECK CONSTRAINT FK_T_Products_i18n_Cust_T_Languages

GO



ALTER TABLE dbo.T_Products_i18n_Cust  
    ADD CONSTRAINT FK_T_Products_i18n_Cust_T_Products 
    FOREIGN KEY(PROD_i18n_Cust_PROD_Id)
REFERENCES dbo.T_Products (PROD_Id)
GO

ALTER TABLE dbo.T_Products_i18n_Cust CHECK CONSTRAINT FK_T_Products_i18n_Cust_T_Products
GO

Затем заполните данные

DELETE FROM T_Languages;
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (1, N'English', N'English', N'EN');
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (2, N'Deutsch', N'German', N'DE');
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (3, N'Français', N'French', N'FR');
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (4, N'Italiano', N'Italian', N'IT');
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (5, N'Russki', N'Russian', N'RU');
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (6, N'Zhungwen', N'Chinese', N'ZH');

DELETE FROM T_Products;
INSERT INTO T_Products (PROD_Id, PROD_InternalName) VALUES (1, N'Orange Juice');
INSERT INTO T_Products (PROD_Id, PROD_InternalName) VALUES (2, N'Apple Juice');
INSERT INTO T_Products (PROD_Id, PROD_InternalName) VALUES (3, N'Banana Juice');
INSERT INTO T_Products (PROD_Id, PROD_InternalName) VALUES (4, N'Tomato Juice');
INSERT INTO T_Products (PROD_Id, PROD_InternalName) VALUES (5, N'Generic Fruit Juice');

DELETE FROM T_Products_i18n;
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (1, 1, N'Orange Juice');
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (1, 2, N'Orangensaft');
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (1, 3, N'Jus d''Orange');
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (1, 4, N'Succo d''arancia');
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (2, 1, N'Apple Juice');
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (2, 2, N'Apfelsaft');

DELETE FROM T_Products_i18n_Cust;
INSERT INTO T_Products_i18n_Cust (PROD_i18n_Cust_PROD_Id, PROD_i18n_Cust_Lang_Id, PROD_i18n_Cust_Text) VALUES (1, 2, N'Orangäsaft'); -- Swiss German, if you wonder

А затем запросить данные:

DECLARE @__in_lang_id int
SET @__in_lang_id = (
    SELECT Lang_ID
    FROM T_Languages
    WHERE Lang_ISO_TwoLetterName = 'DE'
)

SELECT 
     PROD_Id 
    ,PROD_InternalName -- Default Fallback field (internal name/one language only setup), just in ResultSet for demo-purposes
    ,PROD_i18n_Text  -- Translation text, just in ResultSet for demo-purposes
    ,PROD_i18n_Cust_Text  -- Custom Translations (e.g. per customer) Just in ResultSet for demo-purposes
    ,COALESCE(PROD_i18n_Cust_Text, PROD_i18n_Text, PROD_InternalName) AS DisplayText -- What we actually want to show 
FROM T_Products 

LEFT JOIN T_Products_i18n 
    ON PROD_i18n_PROD_Id = T_Products.PROD_Id 
    AND PROD_i18n_Lang_Id = @__in_lang_id 

LEFT JOIN T_Products_i18n_Cust 
    ON PROD_i18n_Cust_PROD_Id = T_Products.PROD_Id
    AND PROD_i18n_Cust_Lang_Id = @__in_lang_id

Если вы ленивы, то вы также можете использовать ISO-TwoLetterName ('DE', 'EN' и т. Д.) В качестве первичного ключа языковой таблицы, тогда вам не нужно искать идентификатор языка , Но если вы сделаете это, вы, возможно, захотите использовать вместо него тег IETF-language , что лучше, потому что вы получаете de-CH и de-DE, что на самом деле не одно и то же с точки зрения орфографии (двойной s вместо везде), хотя это один и тот же базовый язык. Это очень маленькая деталь, которая может быть важна для вас, особенно если учесть, что en-US и en-GB / en-CA / en-AU или fr-FR / fr-CA имеют схожие проблемы.
Цитата: нам это не нужно, мы делаем наши программы только на английском языке.
Ответ: Да, но какой?

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

См. Также RFC 5646 , ISO 639-2 ,

И, если вы все еще говорите "мы" только , создаем наше приложение для "только одна культура" (как обычно в en-US) - поэтому мне не нужно это дополнительное целое число, это было бы хорошее время и место для упоминания языковых тегов IANA , не так ли?
Потому что они идут так:

de-DE-1901
de-DE-1996

и

de-CH-1901
de-CH-1996

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

Редактировать:
И добавив ON DELETE CASCADE после

REFERENCES dbo.T_Products( PROD_Id )

Вы можете просто сказать: DELETE FROM T_Products и не получить никакого нарушения внешнего ключа.

Что касается сортировки, я бы сделал это так:

А) Имейте свой собственный DAL
B) Сохраните нужное имя сопоставления в языковой таблице

Возможно, вы захотите поместить сопоставления в их собственную таблицу, например ::

SELECT * FROM sys.fn_helpcollations() 
WHERE description LIKE '%insensitive%'
AND name LIKE '%german%' 

C) Укажите имя сортировки в вашей информации auth.user.language

D) Напишите ваш SQL следующим образом:

SELECT 
    COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName 
FROM T_Groups 

ORDER BY GroupName COLLATE {#COLLATION}

Д) Затем вы можете сделать это в своем DAL:

cmd.CommandText = cmd.CommandText.Replace("{#COLLATION}", auth.user.language.collation)

Что даст вам этот прекрасно составленный SQL-запрос

SELECT 
    COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName 
FROM T_Groups 

ORDER BY GroupName COLLATE German_PhoneBook_CI_AI
47 голосов
/ 25 ноября 2008

Третий вариант - лучший по нескольким причинам:

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

-Adam

9 голосов
/ 12 августа 2013

Посмотрите на этот пример:

PRODUCTS (
    id   
    price
    created_at
)

LANGUAGES (
    id   
    title
)

TRANSLATIONS (
    id           (// id of translation, UNIQUE)
    language_id  (// id of desired language)
    table_name   (// any table, in this case PRODUCTS)
    item_id      (// id of item in PRODUCTS)
    field_name   (// fields to be translated)
    translation  (// translation text goes here)
)

Я думаю, что объяснять не нужно, структура сама себя описывает.

8 голосов
/ 25 ноября 2008

Обычно я бы пошел на этот подход (не фактический sql), это соответствует вашему последнему варианту.

table Product
productid INT PK, price DECIMAL, translationid INT FK

table Translation
translationid INT PK

table TranslationItem
translationitemid INT PK, translationid INT FK, text VARCHAR, languagecode CHAR(2)

view ProductView
select * from Product
inner join Translation
inner join TranslationItem
where languagecode='en'

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

3 голосов
/ 06 августа 2012

Я искал несколько советов по локализации и нашел эту тему. Мне было интересно, почему это используется:

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

Таким образом, вы получите что-то вроде того, что user39603 предлагает:

table Product
productid INT PK, price DECIMAL, translationid INT FK

table Translation
translationid INT PK

table TranslationItem
translationitemid INT PK, translationid INT FK, text VARCHAR, languagecode CHAR(2)

view ProductView
select * from Product
inner join Translation
inner join TranslationItem
where languagecode='en'

Разве вы не можете просто покинуть стол, Перевод, так что вы получите это:

    table Product
    productid INT PK, price DECIMAL

    table ProductItem
    productitemid INT PK, productid INT FK, text VARCHAR, languagecode CHAR(2)

    view ProductView
    select * from Product
    inner join ProductItem
    where languagecode='en'
3 голосов
/ 25 ноября 2008

Прежде чем перейти к техническим деталям и решениям, вам следует остановиться на минуту и ​​задать несколько вопросов о требованиях. Ответы могут оказать огромное влияние на техническое решение. Примеры таких вопросов:
- Будут ли все языки использоваться постоянно?
- Кто и когда будет заполнять колонки версиями на разных языках?
- Что происходит, когда пользователю понадобится определенный язык текста, а в системе его нет?
- Только тексты должны быть локализованы или есть и другие элементы (например, PRICE можно хранить в $ и & euro; потому что они могут отличаться)

1 голос
/ 13 декабря 2012

Будет ли жизнеспособен следующий подход? Скажем, у вас есть таблицы, в которых нужно перевести более 1 столбца. Таким образом, для продукта у вас может быть как название продукта, так и описание продукта, которые необходимо перевести. Не могли бы вы сделать следующее:

CREATE TABLE translation_entry (
      translation_id        int,
      language_id           int,
      table_name            nvarchar(200),
      table_column_name     nvarchar(200),
      table_row_id          bigint,
      translated_text       ntext
    )

    CREATE TABLE translation_language (
      id int,
      language_code CHAR(2)
    )   
1 голос
/ 22 августа 2012

Я согласен с рандомизатором. Не понимаю, зачем вам таблица "перевод".

Думаю, этого достаточно:

TA_product: ProductID, ProductPrice
TA_Language: LanguageID, Language
TA_Productname: ProductnameID, ProductID, LanguageID, ProductName
0 голосов
/ 03 апреля 2013

«Какой из них лучший» зависит от ситуации в проекте. Первый легко выбрать и поддерживать, а также производительность является наилучшей, поскольку при выборе объекта не требуется объединять таблицы. Если вы подтвердили, что ваш проект поддерживает только 2 или 3 языка, и он не увеличится, вы можете использовать его.

Второй - окей, но его трудно понять и поддерживать. И производительность хуже, чем у первого.

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

...