I\'m sure many of you have been faced by the challenge of localizing a database backend to an application. If you\'ve not then I\'d be pretty confident in
Here some thoghts on the Rick Strahl's blog:
Localization of database Localization of JavaScript
I do prefer to use a single switch in a UserSetting table , which is used by calling stored procedure ... here some of the code
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]
GO
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
I like the XML approach, because the separate-table-solution would NOT return a result if e.g. there is no swedish translation (cultureID = 1) unless you do an outer join. But nevertheless you can NOT fall back to English. With the XML approach you simply can fall back to English. Any news on the XML approach in a producitve environment?
I see the delima overall - you have a single entity you must represent as a single instance (one ProductID of "10" for example), but have multiple localized text of different columns/properties. That is a tough one, and I do see the need for POS systems, that you only want to track that one ProductID = 10, not multiple products that have different ProductIDs, but are the same thing with just different text.
I would lean towards the XML column solution you and others have outlined here already. Yes, it's more data transfering over the wire - but, it keeps things simple and can be filtered with XElement if packet site becomes an issue.
The main drawback being the amount of data transfered over the wire from the DB to the service layer/UI/App. I would try to do some transformation on the SQL end before returning the result, to only return the one culture UI. You could always just SELECT the currect culsture via xml in an sproc, and return it as normal text as well.
Overall, this is different then, say, a Blog Post or CMS need for localization - which I've done a few of.
My approach to the Post scenerio would be similar to TToni's, with the exception of modelling the data from a the Domain's perspective (and a touch of BDD). With that said, focus on what you want to achieve:
Given a users culture is "sv-se"
When the user views a post list
It should list posts only in "sv-se" culture
This means that the user should see a list of posts only for their culture. The way we implemented this before was to pass in a set of cultures to query for based on what the user could see. If the user has 'sv-se' set as their primary, but also has selected they speak US English (en-us), then the query would be:
SELECT * FROM Post WHERE CultureUI IN ('sv-se', 'en-us')
Notice how this gives you all posts and their different PostID, unique to that language. The PostID isn't as important here on blogs because each post is bound to a different language. If there are copies being transcribed, that works fine here too as each post is unique to that culture, and therefore gets a unique set of comments and such.
But to go back to the 1st part of my answer, your need stems from the requirement of needing a single instance with multiple texts. An Xml column fits that fine.
Indexing becomes an issue. I don't think you can index xml, and of course, you can't index it if you store it as a string because every string will start with <localization> <text culture="...">
.