关于本地化:多语言数据库的模式

Schema for a multilanguage database

我正在开发一个多语言软件。就应用程序代码而言,本地化不是问题。我们可以使用特定于语言的资源,并拥有各种能够与它们一起工作的工具。

但是,定义多语言数据库模式的最佳方法是什么?假设我们有很多表(100个或更多),并且每个表可以有多个可以本地化的列(nvarchar的大多数列应该是可本地化的)。例如,其中一个表可能包含产品信息:

1
2
3
4
5
CREATE TABLE T_PRODUCT (
  NAME        NVARCHAR(50),
  DESCRIPTION NTEXT,
  PRICE       NUMBER(18, 2)
)

我可以想到三种方法来支持名称和描述列中的多语言文本:

  • 每种语言单独列

    当我们向系统添加新语言时,必须创建额外的列来存储翻译后的文本,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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)
    )
  • 带有每种语言列的翻译表

    不存储翻译文本,只存储翻译表的外键。翻译表包含每种语言的一列。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    )
  • 具有每种语言行的翻译表

    不存储翻译文本,只存储翻译表的外键。翻译表只包含一个键,而单独的表包含每种语言翻译的行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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)
    )
  • 每个解决方案都有优缺点,我想知道您对这些方法有哪些经验,您推荐什么,以及如何设计多语言数据库模式。


    对于每个可翻译表都有一个相关的翻译表,您认为如何?

    CREATE TABLE T_PRODUCT (pr_id int, PRICE NUMBER(18, 2))

    CREATE TABLE T_PRODUCT_tr (pr_id INT FK, languagecode varchar, pr_name text, pr_descr text)

    这样,如果您有多个可翻译列,则只需要一个联接即可获得它+,因为您没有自动生成translationid,因此导入项目及其相关翻译可能更容易。

    这的消极方面是,如果您有一个复杂的语言回退机制,那么可能需要为每个翻译表实现这个机制——如果您依赖某个存储过程来实现这个机制的话。如果你从应用程序中做到这一点,这可能不是问题。

    让我知道你的想法-我也将为我们的下一个申请做一个决定。到目前为止,我们已经使用了您的第三种类型。


    第三种选择是最好的,原因如下:

    • 不需要为新语言修改数据库模式(从而限制代码更改)
    • 对于未实现的语言或特定项目的翻译不需要很大的空间
    • 提供最大的灵活性
    • 你不会以稀疏的表格结束
    • 您不必担心空键,也不必检查是否显示了一个现有的翻译而不是一些空条目。
    • 如果更改或扩展数据库以包含其他可翻译项目/事物/等,则可以使用相同的表和系统-这与其他数据非常不耦合。

    -亚当


    这是一个有趣的问题,所以让我们来讨论一下。好的。

    让我们从方法1的问题开始:
    问题:您正在取消规格化以节省速度。
    在SQL中(postgresql和hstore除外),不能传递参数语言,并说:好的。

    1
    SELECT ['DESCRIPTION_' + @in_language]  FROM T_Products

    所以你必须这样做:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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",因此您不必更改所有查询。好的。

    这通常会导致类似的结果(顺便说一下,它不能用于视图或表值函数中,如果您实际需要筛选报告日期,这确实是一个问题)好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    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格式输入(普通的花园品种程序员通常不会这样做,并且在报告的情况下,用户肯定不会为您做,即使明确指示这样做)。< BR>
    b)最重要的是,您不必进行任何语法检查。如果因为突然改变了对wing的要求而改变了模式,并且创建了一个新的表,那么旧的表就离开了,但是引用字段被重命名了,您就不会得到任何警告。即使在不选择wing参数(=>guid.empty)的情况下运行报表,它也能正常工作。但突然间,当实际用户实际选择机翼=>动臂时。这种方法完全打破了任何测试。好的。


    方法2:
    简而言之:"好"的想法(警告-讽刺),让我们把方法3的缺点(当有许多条目时速度较慢)和方法1的相当可怕的缺点结合起来。< BR>这种方法的唯一优点是将所有翻译保存在一个表中,从而使维护变得简单。但是,使用方法1和动态SQL存储过程、包含翻译的(可能是临时的)表以及目标表的名称(假设您将所有文本字段命名为相同的名称,这很简单)。好的。


    方法3:
    所有翻译的一个表:缺点:对于要翻译的n个字段,必须在Products表中存储n个外键。因此,必须对n个字段执行n个联接。当转换表是全局的时,它有许多条目,并且连接变慢。此外,对于n个字段,您总是必须加入t_翻译表n次。这是相当大的开销。现在,当您必须为每个客户提供自定义翻译时,您要做什么?您必须在一个额外的表中添加另一个2XN联接。如果你必须连接,比如说10个表,有2x2xn=4n个附加连接,那真是一团糟!此外,这种设计还可以在两个表中使用相同的翻译。如果更改一个表中的项名称,是否确实要每次都更改另一个表中的项?好的。

    另外,您不能再删除和重新插入该表,因为现在产品表中有外键…当然,您可以省略设置fks,然后可以删除该表,并使用newid()[或通过在insert中指定id,但禁用identity insert]重新插入所有条目,这很快就会(并且将)导致数据垃圾(和空引用异常)。好的。


    方法4(未列出):将所有语言存储在数据库的XML字段中。例如好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    -- 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

    然后,您可以在SQL中通过xpath查询获取该值,在该查询中可以将字符串变量放入好的。

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

    您可以这样更新值:好的。

    1
    2
    3
    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

    你可以用'.../' + @in_language + '/...'代替/lang/de/...。好的。

    有点像postgre hstore,但由于分析XML的开销(而不是从pg hstore中的关联数组中读取条目),它变得太慢,加上XML编码会使它变得太痛苦而无法使用。
    好的。


    方法5(根据孙武功的建议,您应该选择一种方法):每个"产品"表都有一个翻译表。这意味着每种语言有一行,并且有几个"文本"字段,所以在n个字段上只需要一个(左)联接。然后,您可以轻松地在"product"表中添加默认字段,您可以轻松地删除和重新插入翻译表,还可以为自定义翻译(按需)创建第二个表,您还可以删除和重新插入该表,并且您仍然拥有所有外键。好的。

    让我们举一个例子来说明这一点:好的。

    首先,创建表:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    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

    然后填写数据好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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

    然后查询数据:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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

    如果你很懒惰,那么你也可以使用is o twolettername(‘de’,‘en’等)作为语言表的主键,那么你不必查找语言ID。但是如果你这样做了,你可能想使用ietf语言标记,这是更好的,因为你得到了de ch和de de,而这实际上不是相同的或是不同的(double s,而不是f?尽管它是相同的基础语言。这对您来说可能是一个很重要的小细节,特别是考虑到en-US和en-GB/en-CA/en-AU或fr-fr/fr-CA有类似的问题。
    报价:我们不需要它,我们只做英文软件。
    回答:是-但是哪一个??BR/>好的。

    无论如何,如果您使用整数ID,那么您是灵活的,并且可以在以后的任何时候更改您的方法。< BR>你应该使用这个整数,因为没有什么比糟糕的数据库设计更烦人、更具破坏性和更麻烦的了。好的。

    另见RFC 5646,ISO 639-2,好的。

    而且,如果你仍然说"我们"只是为了"一种文化"(就像我们通常所说的那样),那么我不需要额外的整数,这将是一个提到IANA语言标签的好时机和地点,不是吗?< BR>因为他们是这样的:好的。

    1
    2
    de-DE-1901
    de-DE-1996

    和好的。

    1
    2
    de-CH-1901
    de-CH-1996

    (1996年有一项正字法改革…)如果拼写错误,请尝试在字典中查找单词;这在处理法律和公共服务门户的应用程序中非常重要。
    更重要的是,有些地区正在从西里尔字母改为拉丁字母,这可能比一些模糊的正字法改革表面上的麻烦更麻烦,这也是为什么这可能也是一个重要的考虑因素,取决于你居住的国家。不管怎样,最好是用那个整数,以防万一…好的。

    编辑:
    在后面加上EDOCX1[0]好的。

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

    您可以简单地说:DELETE FROM T_Products,并且不违反外键。好的。

    至于排序规则,我会这样做:好的。

    a)拥有自己的DAL
    b)将所需的排序规则名称保存在语言表中

    好的。

    您可能希望将排序规则放在自己的表中,例如:好的。

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

    c)在auth.user.language信息中提供排序规则名称好的。

    d)这样编写SQL:好的。

    1
    2
    3
    4
    5
    SELECT
        COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
    FROM T_Groups

    ORDER BY GroupName COLLATE {#COLLATION}

    e)然后,您可以在DAL中执行此操作:好的。

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

    然后,它将为您提供这个完美组合的SQL查询好的。

    1
    2
    3
    4
    5
    SELECT
        COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
    FROM T_Groups

    ORDER BY GroupName COLLATE German_PhoneBook_CI_AI

    好啊。


    看看这个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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),这与您的最后一个选项相对应。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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'

    因为把所有可翻译的文本放在一个地方会使维护变得容易得多。有时翻译工作外包给翻译局,这样你就可以只发送一个大的导出文件,然后很容易地将其导入回去。


    我在寻找本地化的一些技巧,并找到了这个主题。我想知道为什么要用这个:

    1
    2
    3
    CREATE TABLE T_TRANSLATION (
       TRANSLATION_ID
    )

    所以你会得到用户39603的建议:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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'

    你不能把表的翻译放在一边,这样你就可以得到:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
        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'


    在讨论技术细节和解决方案之前,您应该停下来问一些关于需求的问题。答案会对技术解决方案产生巨大的影响。此类问题的例子如下:-所有语言都会一直使用吗?-谁和什么时候用不同的语言版本填充列?-当用户需要某种语言的文本而系统中没有文本时会发生什么?-只有文本要本地化,或者还有其他项目(例如,价格可以以美元和&euro;存储,因为它们可能不同)


    下面的方法可行吗?假设您有多于一列需要转换的表。因此,对于产品,您可以同时拥有需要翻译的产品名称和产品描述。你能做一下吗?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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
    2
    3
    TA_product: ProductID, ProductPrice
    TA_Language: LanguageID, Language
    TA_Productname: ProductnameID, ProductID, LanguageID, ProductName

    "哪一个最好"是根据项目情况而定的。第一种方法易于选择和维护,而且性能最好,因为在选择实体时不需要联接表。如果您确认您的poject只支持2或3种语言,并且不会增加,那么您可以使用它。

    第二个是好的,但很难理解和保持。而且性能比第一个差。

    最后一个在可伸缩性方面很好,但在性能方面却很差。t_translation_条目表将变得越来越大,当您想要从一些表中检索实体列表时,这是非常糟糕的。