Схема для многоязычной базы данных

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

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

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

Я могу представить три подхода к поддержке многоязычного текста в столбцах NAME и DESCRIPTION:

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

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

    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)
    )
    
  • Таблица переводов со столбцами для каждого языка

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

    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
    )
    
  • Таблицы переводов со строками для каждого языка

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

    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)
    )
    

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

Ответ 1

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

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

CREATE TABLE T_PRODUCT_tr (pr_id INT FK, languagecode varchar, текст pr_name, текст pr_descr)

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

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

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

Ответ 2

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

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

-Adam

Ответ 3

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

Начнем с задач метода 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 @[email protected] + 'AND (Building_UID  = ''' + @in_building + ''') '
    IF @in_wing    <> '00000000-0000-0000-0000-000000000000' SET @[email protected] + 'AND (Wing_UID  = ''' + @in_wing + ''') '

    EXECUTE (@sql) 

END


GO

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


Способ 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() [или путем указания идентификатора в вставке, но с идентификационной вставкой OFF ], и это приведет (и приведет) к мусору данных (и нулю Ссылка исключения) очень скоро.


Способ 4 (не указан): Сохранение всех языков в поле XML в базе данных. например
-- 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 полей. Затем вы можете легко добавить поле по умолчанию в "Продукт" -table, вы можете легко удалить и повторно вставить таблицу перевода, и вы можете создать вторую таблицу для пользовательских переводов (по запросу), которую вы также можете удалите и вставьте заново), и у вас все еще есть все внешние ключи.

Давайте сделаем пример, чтобы увидеть это работает:

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

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 WITH CHECK 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, что на самом деле не одинаково для орфографии (double 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
Б) Сохраните желаемое имя сопоставления в языковой таблице

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

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}

E) Затем вы можете сделать это в своем 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

Ответ 4

Взгляните на этот пример:

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)
)

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

Ответ 5

Обычно я бы использовал этот подход (не фактический 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'

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

Ответ 6

Прежде чем перейти к техническим деталям и решениям, вы должны остановиться на минуту и ​​задать несколько вопросов о требованиях. Ответы могут оказать огромное влияние на техническое решение. Примерами таких вопросов могут быть:
- Будут ли все языки использоваться постоянно?
- Кто и когда заполнит столбцы разными языковыми версиями?
- Что произойдет, когда пользователю понадобится определенный язык текста, а в системе нет его?

- Только тексты должны быть локализованы или есть другие предметы (например, PRICE можно хранить в $и & euro, потому что они могут быть разными)

Ответ 7

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

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'

Ответ 8

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

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

TA_product: ProductID, ProductPrice
TA_Language: LanguageID, Language
TA_Productname: ProductnameID, ProductID, LanguageID, ProductName

Ответ 9

Будет ли такой подход более жизнеспособным? Скажем, у вас есть таблицы, где требуется переводить более 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)
    )   

Ответ 10

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

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

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