12

问题

我相信你们中的许多人都面临过将数据库后端本地化到应用程序的挑战。如果你还没有,那么我很有信心说你将来必须这样做的可能性很大。我说的是为您的数据库实体存储多个文本翻译(货币等也是如此)。

例如,经典的“Category”表可能有一个应该全球化的 Name 和 Description 列。一种方法是为每个实体创建一个“文本”表,然后根据提供的语言进行连接以检索值。

这为您留下了许多“文本”表,每个表用于您要本地化的每个实体,并添加了一个 TextType 来区分它可能存储的各种文本。

我很好奇是否有任何记录在案的最佳实践/设计模式将这种支持实现到 SQL Server 2005/2008 数据库中(我特别关注 RDBMS,因为它可能包含受支持的关键字等,这有助于与实施)?

关于 XML 方法的思考

我一直在玩弄的一个想法(尽管到目前为止只是在我的脑海中)是利用 SQL Server 2005 中引入的 XML 数据类型。这个想法是制作应该支持本地化的 XML 数据类型的列(并将模式绑定到它)。XML 将包含本地化字符串以及与之相关的语言代码/文化。

类似的东西

Product
ID (int, identity)
Name (XML ...)
Description (XML ...)

然后你会有像 XML 这样的东西

<localization>
  <text culture="sv-SE">Detta är ett namn</text>
  <text culture="en-EN">This is a name</text>
</localization>

然后你可以这样做(这不是生产代码,所以我将使用 *)

SELECT *
From Product
Where Product.ID = 10

您将获得带有所有本地化文本的产品,这意味着您必须在客户端进行提取。这里最大的问题显然是每次查询都必须返回的额外数据量,好处是没有查找表、连接等的更简洁的设计。

顺便说一句,无论我最终在设计中使用什么方法,我仍将使用 Linq To SQL(.NET 平台)来查询数据库(XML 方法应该是一个问题,因为它会返回一个可以被客户端解释的 XElement-边)

因此,关于数据库本地化设计模式的建议,以及可能对 XML 思想的评论,将非常受欢迎。

4

10 回答 10

3

我认为您可以坚持使用允许更简洁设计的 XML。我会更进一步并利用专为此用途设计xml:lang的属性:

<l10n>
  <text xml:lang="sv-SE">Detta är ett namn</text>
  <text xml:lang="en-EN">This is a name</text>
</l10n>

更进一步,您可以通过XPath 查询(如评论中的建议)在查询中选择本地化资源,以避免任何客户端处理。这会给出这样的东西(未经测试):

SELECT Name.value('(l10n/text[lang()="en"])[1]', 'NVARCHAR(MAX)')
  FROM Product
  WHERE Product.ID=10;

请注意,与单独的表格相比,此解决方案将是一种优雅但效率较低的解决方案。这对于某些应用程序可能没问题。

于 2008-11-04T14:06:39.157 回答
2

这是我的做法。我不为此使用 LINQ 或 SP,因为查询太复杂并且是动态构建的,这只是查询的摘录。

我有一个产品表:

* id
* price
* stocklevel
* active
* name
* shortdescription
* longdescription

和 products_globalization 表:

* id
* products_id
* name
* shortdescription
* longdescription

如您所见,产品表也包含所有全球化列。这些列包含默认语言(因此,在请求默认文化时能够跳过连接 - 但我不确定这是否值得麻烦,我的意思是两个表之间的连接是基于索引的所以。 .. - 给我一些关于这个的反馈)。

我更喜欢在全局资源表上使用并排表,因为在某些情况下,您可能需要在几列上执行即数据库 (MySQL) MATCH,例如 MATCH(name, shortdescription, longdescription) AGAINST ('这里的东西')。

在正常情况下,某些产品翻译可能会丢失,但我仍然想显示所有产品(不仅仅是被翻译的产品)。所以仅仅做一个join是不够的,我们实际上需要根据products-table做一个left join。

伪:

string query = "";
if(string.IsNullOrEmpty(culture)) {
   // No culture specified, no join needed.
   query = "SELECT p.price, p.name, p.shortdescription FROM products p WHERE p.price > ?Price";
} else {
   query = "SELECT p.price, case when pg.name is null then p.name else pg.name end as 'name', case when pg.shortdescription is null then p.shortdescription else pg.shortdescription end as 'shortdescription' FROM products p"
   + " LEFT JOIN products_globalization pg ON pg.products_id = p.id AND pg.culture = ?Culture"
   + " WHERE p.price > ?Price";
}

我会选择 COALESCE 而不是 CASE ELSE,但除此之外。

嗯,这就是我的看法。随意批评我的建议...

亲切的问候,理查德

于 2010-11-09T02:52:57.687 回答
1

这是难以回答的问题之一,因为答案中有很多“取决于”:-)

答案取决于数据库中本地化项目的数量、部署方案、缓存问题、访问模式等。如果你能给我们一些关于应用程序有多大、它将有多少并发用户以及如何部署的数据,那将非常有帮助。

一般来说,我通常使用以下两种方法之一:

  1. 将本地化项目存储在可执行文件附近(本地化资源 dll)
  2. 将本地化项目存储在数据库中,并在包含本地化项目的表中引入 localeID 列。

第一种方法的优点是良好的 VisualStudio 支持。第二种的优点是集中部署。

于 2008-11-03T14:09:37.950 回答
1

我不明白你为什么需要多个文本表。具有“全局”唯一文本 ID 的单个文本表就足够了。该表将具有 ID、语言、文本列,并且您只会获得您需要呈现的语言的文本(或者可能根本不检索文本)。连接应该是相当有效的,因为 (ID, language) 的组合是主键。

于 2008-11-03T13:58:01.733 回答
1

我认为使用 XML 列存储本地化值没有任何优势。除非你有一个项目的所有本地化版本“在一个地方”,如果这对你有价值的话。

我建议在每个具有可本地化项目的表中使用cultureID 列。这样,您根本不需要任何 XML 处理。您已经将数据保存在关系模式中,那么当关系模式完全能够处理问题时,为什么还要引入另一层复杂性呢?

假设“sv-SE”有cultureID = 1,“en-EN”有2。

然后您的查询将被修改为

SELECT *
From Product
Where Product.ID = 10 AND Product.cultureID = 1

对于瑞典客户。

我在本地化数据库中经常看到这种解决方案。它可以很好地适应文化数量和数据记录数量。它避免了 XML 解析和处理,并且易于实现。

还有一点:XML 解决方案为您提供了您不需要的灵活性:例如,您可以从“名称”列中获取“sv-SE”值,从“描述”-列。但是,您不需要这个,因为您的客户一次只会请求一种文化。灵活性通常是有代价的。在这种情况下,您需要单独解析所有列,同时使用cultureID 解决方案,您可以获得包含所有值的整个记录​​,其中所有值都适合所请求的文化。

于 2008-11-04T13:30:37.747 回答
0

这里有一些关于 Rick Strahl 博客的想法:

数据库 本地化 JavaScript 本地化

我更喜欢在 UserSetting 表中使用单个开关,通过调用存储过程来使用它......这里有一些代码

CREATE TABLE [dbo].[Lang_en_US_Msg](
    [MsgId] [int] IDENTITY(1,1) NOT NULL,
    [MsgKey] [varchar](200) NOT NULL,
    [MsgTxt] [varchar](2000) NOT NULL,
    [MsgDescription] [varchar](2000) NOT NULL,
 CONSTRAINT [PK_Lang_US-us__Msg] PRIMARY KEY CLUSTERED 
(
    [MsgId] 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].[User](
    [UserId] [int] IDENTITY(1,1) NOT NULL,
    [FirstName] [varchar](50) NOT NULL,
    [MiddleName] [varchar](50) NULL,
    [LastName] [varchar](50) NULL,
    [DomainName] [varchar](50) NULL,
 CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED 
(
    [UserId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE TABLE [dbo].[UserSetting](
    [UserSettingId] [int] IDENTITY(1,1) NOT NULL,
    [UserId] [int] NOT NULL,
    [CultureInfo] [varchar](50) NOT NULL,
    [GuiLanguage] [varchar](10) NOT NULL,
 CONSTRAINT [PK_UserSetting] PRIMARY KEY CLUSTERED 
(
    [UserSettingId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

 ALTER TABLE [dbo].[UserSetting] ADD  CONSTRAINT [DF_UserSetting_CultureInfo]  DEFAULT ('fi-FI') FOR [CultureInfo]
 GO

 CREATE TABLE [dbo].[Lang_fi_FI_Msg](
    [MsgId] [int] IDENTITY(1,1) NOT NULL,
    [MsgKey] [varchar](200) NOT NULL,
    [MsgTxt] [varchar](2000) NOT NULL,
    [MsgDescription] [varchar](2000) NOT NULL,
    [DbSysNameForExpansion] [varchar](50) NULL,
 CONSTRAINT [PK_Lang_Fi-fi__Msg] PRIMARY KEY CLUSTERED 
(
    [MsgId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE PROCEDURE [dbo].[procGui_GetPageMsgs]
@domainUser varchar(50) ,           -- the domain_user performing the action  
@msgOut varchar(4000) OUT,        -- the (error) msg to be shown to the user   
@debugMsgOut varchar(4000) OUT ,   -- this variable holds the debug msg to be shown if debug level is enabled   
@ret int OUT                  -- the variable indicating success or failure 

AS                            
BEGIN -- proc start                            
 SET NOCOUNT ON;                            

declare @procedureName varchar(200)        
declare @procStep varchar(4000)  


set @procedureName = ( SELECT OBJECT_NAME(@@PROCID))        
set @msgOut = ' '     
set @debugMsgOut = ' '     
set @procStep = ' '     


BEGIN TRY        --begin try                  
set @ret = 1 --assume false from the beginning                  

--===============================================================
 --debug   set @procStep=@procStep + 'GETTING THE GUI LANGUAGE FOR THIS USER '
--===============================================================

declare @guiLanguage nvarchar(10)




if ( @domainUser is null)
    set @guiLanguage = (select Val from AppSetting where Name='guiLanguage')
else 
    set @guiLanguage = (select GuiLanguage from UserSetting us join [User] u on u.UserId = us.UserId where u.DomainName=@domainUser)

set @guiLanguage = REPLACE ( @guiLanguage , '-' , '_' ) ;


--===============================================================
set @procStep=@procStep + ' BUILDING THE SQL QUERY '
--===============================================================

DECLARE @sqlQuery AS nvarchar(2000)
SET @sqlQuery = 'SELECT  MsgKey , MsgTxt FROM dbo.lang_' + @guiLanguage + '_Msg'


--===============================================================
set @procStep=@procStep + 'EXECUTING THE SQL QUERY'
--===============================================================
print @sqlQuery

    exec sp_executesql @sqlQuery

    set @debugMsgOut = @procStep
    set @ret = @@ERROR                  


END TRY        --end try                  

BEGIN CATCH                        
 PRINT 'In CATCH block.                         
 Error number: ' + CAST(ERROR_NUMBER() AS varchar(10)) + '                        
 Error message: ' + ERROR_MESSAGE() + '                        
 Error severity: ' + CAST(ERROR_SEVERITY() AS varchar(10)) + '                        
 Error state: ' + CAST(ERROR_STATE() AS varchar(10)) + '                        
 XACT_STATE: ' + CAST(XACT_STATE() AS varchar(10));                        

set @msgOut = 'Failed to execute ' + @sqlQuery             
set @debugMsgOut = ' Error number: ' + CAST(ERROR_NUMBER() AS varchar(10)) +               
 'Error message: ' + ERROR_MESSAGE() + 'Error severity: ' + CAST(ERROR_SEVERITY() AS varchar(10)) +               
 'Error state: ' + CAST(ERROR_STATE() AS varchar(10)) + 'XACT_STATE: ' + CAST(XACT_STATE() AS varchar(10))                        

--record the error in the database                        
--debug    
 --EXEC [dbo].[procUtils_DebugDb]
    --  @DomainUser = @domainUser,
    --  @debugmsg = @debugMsgOut,
    --  @ret = 1,
    --  @procedureName = @procedureName ,
    --  @procedureStep = @procStep

 -- set @ret = 1                       

END CATCH                        


return  @ret                                   
END --procedure end                             
于 2009-05-14T19:07:34.180 回答
0

我喜欢 XML 方法,因为如果没有瑞典语翻译 (cultureID = 1),除非您进行外部连接,否则分离表解决方案不会返回结果。但尽管如此,你不能回退到英语。使用 XML 方法,您可以简单地回退到英语。有关于在生产环境中使用 XML 方法的消息吗?

于 2008-12-17T17:27:23.630 回答
0

我总体上看到了 delim - 您必须将一个实体表示为单个实例(例如,一个 ProductID 为“10”),但具有不同列/属性的多个本地化文本。这是一个艰难的过程,我确实看到了对 POS 系统的需求,即您只想跟踪一个 ProductID = 10,而不是多个具有不同 ProductID 的产品,但它们是相同的,只是文本不同。

我倾向于您和其他人已经在此处概述的 XML 列解决方案。是的,它是通过网络传输更多数据 - 但是,它使事情变得简单,并且如果数据包站点成为问题,可以使用 XElement 进行过滤。

主要缺点是通过线路从 DB 传输到服务层/UI/App 的数据量。在返回结果之前,我会尝试在 SQL 端进行一些转换,只返回一种文化 UI。您总是可以通过 xml 在存储过程中选择正确的文化,并将其作为普通文本返回。

总的来说,这与博客文章或 CMS 需要本地化(我已经做过一些)不同。

我对 Post 场景的方法与 TToni 的方法类似,除了从域的角度对数据进行建模(以及一点 BDD)。话虽如此,请专注于您想要实现的目标:

Given a users culture is "sv-se"
When the user views a post list
It should list posts only in "sv-se" culture

这意味着用户应该只看到他们文化的帖子列表。我们之前实现这一点的方式是传入一组文化以根据用户可以看到的内容进行查询。如果用户将“sv-se”设置为他们的主要用户,但也选择了他们说美国英语(en-us),那么查询将是:

SELECT * FROM Post WHERE CultureUI IN ('sv-se', 'en-us')

请注意,这如何为您提供所有帖子及其不同的 PostID,这是该语言独有的。PostID 在博客上并不那么重要,因为每个帖子都绑定到不同的语言。如果有副本被转录,那么在这里也可以正常工作,因为每个帖子都是该文化所独有的,因此会获得一组独特的评论等。

但是回到我回答的第一部分,您的需求源于需要具有多个文本的单个实例的要求。Xml 列非常适合。

于 2009-09-13T05:04:26.873 回答
0

要考虑的另一种方法:不要将内容存储在数据库中,而是将支持数据库记录的“应用程序”和“内容”作为单独的实体保持。

在为我的电子商务网站创建多个主题时,我使用了类似的方法。一些产品具有制造商徽标,该徽标也必须与网站主题相匹配。由于主题没有真正的数据库支持,所以我遇到了问题。我想出的解决方案是使用数据库中的令牌来识别图像的 ClientID,而不是存储图像的 URL(这会因主题而异)。

遵循相同的方法,您可以将数据库从存储产品的名称和描述更改为存储名称标记和描述标记,以识别资源(在 resx 文件中或使用 Rick Strahl 方法的数据库中)内容。.NET 的内置功能然后会处理语言切换,而不是尝试在数据库中进行(将业务逻辑放在数据库中很少是一个好主意)。然后,您可以使用客户端上的令牌来查找正确的资源。

Label1.Text = GetLocalResourceObject("TokenStoredInDatabase").ToString()

这种方法的缺点显然是使数据库令牌和资源令牌保持同步(因为可以在没有任何描述的情况下添加产品),但使用诸如 Rick Strahl 创建的资源提供程序可能会更容易完成。如果您的产品经常更换,这种方法可能不起作用,但对某些人来说可能。

优点是您有少量数据要从数据库传输到客户端,您的内容与数据库完全分离,并且您的数据库不需要比现在更复杂。

附带说明一下,如果您正在经营一家电子商务商店,并且确实希望将您的本地化页面编入索引,那么您必须稍微偏离 Microsoft 创建的看似自然的方式。实用且合乎逻辑的设计流程与 Google对 SEO 的建议显然存在分歧。事实上,一些网站管理员抱怨他们的页面没有被搜索引擎索引为“默认”文化,因为搜索引擎只会索引单个 URL,即使它根据浏览器的文化而改变。

幸运的是,有一种简单的方法可以解决这个问题:在页面上放置链接,以根据查询字符串参数将其翻译成其他语言。可以找到一个这样的例子(哎呀,他们不会让我发布另一个链接!!)如果你检查一下,页面的每种文化都已被谷歌和雅虎索引(尽管不是必应)。更高级的方法可以使用 URL 重写与一些花哨的正则表达式相结合,使您的单个本地化页面看起来像它具有多个目录,但实际上将查询字符串参数传递给页面。

于 2009-09-29T11:50:00.343 回答
0

索引成为一个问题。我不认为你可以索引 xml,当然,如果你将它存储为字符串,你就不能索引它,因为每个字符串都以<localization> <text culture="...">.

于 2010-08-05T20:34:41.990 回答