多语言数据库的架构

我正在开发一个多语言软件。 就应用程序代码而言,可定位性不是问题。 我们可以使用语言特定的资源,并有各种工具与他们合作。

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

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

我可以想到在NAME和DESCRIPTION列中支持多语言文本的三种方法:

  1. 为每种语言分列

    当我们添加一个新的语言到系统,我们必须创build额外的列来存储翻译文本,如下所示:

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

每种解决scheme都有优点和缺点,我想知道您对这些方法有什么经验,您有什么build议以及如何devise多语言数据库模式。

你如何看待每个可翻译表的相关翻译表?

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)

这样,如果你有多个可翻译的列,它只需要一个单一的连接来获得它+,因为你不是自动生成一个翻译id,可能会更容易导入项目与他们的相关翻译。

不利的一面是,如果你有一个复杂的语言回退机制,你可能需要为每个转换表实现 – 如果你依靠一些存储过程来做到这一点。 如果你从应用程序这样做,这可能不会是一个问题。

让我知道你的想法 – 我也将为我们的下一个应用做出这个决定。 到目前为止,我们已经使用你的第三类。

第三个select是最好的,原因如下:

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

-亚当

这是一个有趣的问题,所以让我们去死亡。

让我们从方法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 

这意味着如果添加新的语言,则必须更改所有的查询。 这自然导致使用“dynamicSQL”,所以你不必改变所有的查询。

这通常会导致类似这样的事情(并且不能在视图或表值函数中使用,如果实际上需要过滤报告date,这确实是一个问题)

 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)date格式是非常特定于语言的,所以如果您不以ISO格式input(一般的花园程序员通常不会这样做,那么在报告的情况下用户确定即使明确指示这样做,也不会为你做什么)。

b) 最重要的是 ,你没有任何语法检查 。 如果<insert name of your "favourite" person here>改变模式,因为突然要求改变翅膀,并创build一个新表,旧的一个离开,但参考字段改名,你没有得到任何警告。 一个报告甚至可以运行,而不用selectwing参数 (==> guid.empty)。 但突然间,当一个实际的用户实际上select一个机翼==> 繁荣这种方法彻底打破了任何一种testing。


方法2:
简而言之:“好”的想法(警告 – 讽刺),让我们结合方法3(很多条目时速度慢)的缺点与方法1相当可怕的缺点。
这种方法的唯一好处是你可以将所有的翻译保存在一张表中,因此维护简单。 然而,同样的事情可以用方法1和一个dynamic的SQL存储过程来实现,一个包含翻译的(可能是临时的)表和目标表的名字是相同的(假设你把所有的文本字段命名为相同)。


方法3:
用于所有翻译的一张表:缺点:您必须在产品表中为要翻译的n个字段存储n个外键。 因此,您必须对n个字段进行n次连接。 当翻译表是全局的时候,它有很多条目,连接变慢。 而且,你总是必须为n个域joinT_TRANSLATION表n次。 这是相当的开销。 现在,当您必须为每个客户提供定制翻译时,您会做什么? 你将不得不添加另外2个n连接到一个额外的表。 如果你必须join10个表格,用2x2xn = 4n个额外的连接,真是一团糟! 而且,这种devise使得使用2个表格的相同翻译成为可能。 如果我在一个表中更改项目名称,我是否真的想要更改另一个表格中的项目以及每个单独的时间?

另外,您不能再删除和重新插入表格,因为现在在产品表中有外键…您当然可以省略设置FK,然后<insert name of your "favourite" person here>可以删除表,然后用newid() [或者通过在插入中指定id,但是具有标识插入OFF ]重新插入所有条目,并且会(并将)导致数据垃圾(和null – 引用例外)真的很快。


方法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 

然后,您可以通过SQL中的XPath-Query获取值,您可以在其中放入stringvariables

 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 

你可以用'.../' + @in_language + '/...'replace/lang/de/...

有点像PostGre hstore,除了由于parsingXML的开销(而不是从PG hstore中的关联数组中读取条目),它变得太慢了加上xml编码使得它太痛苦有用。


方法5(根据SunWuKung的build议,您应该select一个):每个“产品”表的一个转换表。 这意味着每种语言一行,以及几个“文本”字段,所以在N个字段中只需要一个(左)连接。 然后,您可以轻松地在“产品”表中添加默认字段,您可以轻松地删除并重新插入翻译表,并且可以创build第二个自定义翻译表(按需),您也可以删除并重新插入),而你仍然拥有所有的外键。

让我们举个例子来看看这个作品:

首先,创build表格:

 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 (eg 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'等)作为语言表的主键,那么你不必查找语言ID。 但是如果你这样做的话,你可能会想使用IETF语言标签 ,这样做更好,因为你得到的是de-CH和de-DE,这与真正的皮层摄影不一样(在任何地方都是double s而不是ß) ,虽然它是相同的基本语言。 这只是一个很小的细节,可能对您很重要,特别是考虑到en-US和en-CN / en-CA / en-AU或fr-FR / fr-CA有类似的问题。
Quote:我们不需要它,我们只用英文做我们的软件。
答:是的 – 但哪一个?

无论如何,如果你使用一个整数ID,你很灵活,并可以在以后改变你的方法。
而且你应该使用这个整数,因为没有什么比拙劣的Dbdevise更烦人,更具破坏性和麻烦了。

另见RFC 5646 , ISO 639-2 ,

而且,如果你仍然说“我们” 只是提出“只有一种文化”的申请(通常就像en-US) – 因此我不需要额外的整数,这是一个很好的时间和地点来提及IANA语言标签 ,不是吗?
因为他们是这样的:

 de-DE-1901 de-DE-1996 

 de-CH-1901 de-CH-1996 

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

编辑:
之后joinON DELETE CASCADE

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

你可以简单地说: DELETE FROM T_Products ,并且没有违反外键。

至于整理,我会这样做:

A)有你自己的DAL
B)在语言表中保存所需的sorting规则名称

您可能希望将sorting规则放在自己的表中,例如:

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

C)在您的auth.user.language信息中提供sorting规则名称

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 

我通常会去这个方法(不是实际的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' 

因为将所有可翻译的文本放在一个地方使维护变得更容易。 有时翻译会被外包给翻译部门,这样你可以只发送一个大的导出文件,并且很容易地导入。

看看这个例子:

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

我认为没有必要解释,结构描述自己。

我正在寻找本地化的一些技巧,并find了这个话题。 我想知道为什么这是使用:

 CREATE TABLE T_TRANSLATION ( TRANSLATION_ID ) 

所以你得到像user39603这样的build议:

 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' 

在进入技术细节和解决scheme之前,您应该停下来一分钟,并询问一些有关要求的问题。 答案可能会对技术解决scheme产生巨大的影响。 这样的问题的例子是:
– 所有的语言会一直使用吗?
– 谁和何时填写不同语言版本的专栏?
– 当用户需要特定的文本语言而系统中没有任何内容时会发生什么?
– 只有文本需要本地化或者还有其他项目(例如PRICE可以存储在$和€中,因为它们可能不同)

我同意randomizer。 我不明白你为什么需要一个表“翻译”。

我觉得这足够了:

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

“哪一个最好”是根据项目情况。 第一个是易于select和维护,并且性能也是最好的,因为select实体时不需要连接表。 如果您确认您的项目只支持2或3种语言,并且不会增加,则可以使用它。

第二个是奥凯,但很难理解和维护。 performance比第一个糟糕。

最后一个是可扩展性好但性能不好的。 T_TRANSLATION_ENTRY表将变得越来越大,当你想要从某些表中检索实体列表时,这是非常糟糕的。