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


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

но каков наилучший подход к определению многоязыковой схемы базы данных? Допустим, у нас есть много таблиц (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. таблицы перевода со строками для каждого языка

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

    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 201

10 ответов:

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

создать таблицу T_PRODUCT(pr_id int, PRICE NUMBER (18, 2))

создать таблицу T_PRODUCT_tr (pr_id INT FK, languagecode varchar, pr_name text, pr_descr text)

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

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

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

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


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


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

плюс вы не можете удалить и повторно вставить таблицу больше, потому что теперь есть внешние ключи в таблице продуктов... вы можете, конечно, опустить настройку FKs, а затем <insert name of your "favourite" person here> можно удалить таблицу и повторно вставить все записи с newid () [или указав идентификатор во вставке, но имея identity-вставить Выкл], и это приведет (и приведет) к мусору данных (и исключениям с нулевой ссылкой) очень скоро.


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

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

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

CREATE TABLE [dbo].[T_Languages](
    [Lang_ID] [int] NOT NULL,
    [Lang_NativeName] [nvarchar](200) NULL,
    [Lang_EnglishName] [nvarchar](200) NULL,
    [Lang_ISO_TwoLetterName] [varchar](10) NULL,
 CONSTRAINT [PK_T_Languages] PRIMARY KEY CLUSTERED 
(
    [Lang_ID] 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].[T_Products](
    [PROD_Id] [int] NOT NULL,
    [PROD_InternalName] [nvarchar](255) NULL,
 CONSTRAINT [PK_T_Products] PRIMARY KEY CLUSTERED 
(
    [PROD_Id] 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].[T_Products_i18n](
    [PROD_i18n_PROD_Id] [int] NOT NULL,
    [PROD_i18n_Lang_Id] [int] NOT NULL,
    [PROD_i18n_Text] [nvarchar](200) NULL,
 CONSTRAINT [PK_T_Products_i18n] PRIMARY KEY CLUSTERED 
(
    [PROD_i18n_PROD_Id] ASC,
    [PROD_i18n_Lang_Id] 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].[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]  WITH CHECK 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_Languages]
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] [nvarchar](200) NULL,
 CONSTRAINT [PK_T_Products_i18n_Cust] PRIMARY KEY CLUSTERED 
(
    [PROD_i18n_Cust_PROD_Id] ASC,
    [PROD_i18n_Cust_Lang_Id] 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].[T_Products_i18n_Cust]  WITH CHECK ADD  CONSTRAINT [FK_T_Products_i18n_Cust_T_Languages] FOREIGN KEY([PROD_i18n_Cust_Lang_Id])
REFERENCES [dbo].[T_Languages] ([Lang_ID])
GO

ALTER TABLE [dbo].[T_Products_i18n_Cust] CHECK CONSTRAINT [FK_T_Products_i18n_Cust_T_Languages]
GO

--ALTER TABLE [dbo].[T_Products_i18n_Cust]  WITH NOCHECK ADD  CONSTRAINT [FK_T_Products_i18n_Cust_T_Products] FOREIGN KEY([PROD_i18n_Cust_PROD_Id])
ALTER TABLE [dbo].[T_Products_i18n_Cust]  WITH CHECK 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 tag вместо этого, что лучше, потому что вы получаете de-CH и de-DE, что на самом деле не одно и то же ортографическое (двойное s вместо β везде), хотя это один и тот же базовый язык. Что как маленький детали, которые могут быть важны для вас, особенно учитывая, что en-US и en-GB/en-CA/en-AU или fr-FR/fr-CA имеют аналогичные проблемы.
цитата: нам это не нужно, мы только делаем наше программное обеспечение на английском языке.
ответ: Да - но какой ??

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

см. также RFC 5646,ISO 639-2,

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

de-DE-1901
de-DE-1996

и

de-CH-1901
de-CH-1996

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

Edit:
И добавляя ON DELETE CASCADE после

REFERENCES [dbo].[T_Products] ([PROD_Id])

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

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

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

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

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

В) иметь имя параметров сортировки доступны в свой двиг.пользователь.информация о языке

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

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

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

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

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

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

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

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

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'

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

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

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

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

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

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

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