Esquema para um banco de dados multilíngue


235

Estou desenvolvendo um software multilíngue. No que diz respeito ao código do aplicativo, a localização não é um problema. Podemos usar recursos específicos do idioma e ter todos os tipos de ferramentas que funcionam bem com eles.

Mas qual é a melhor abordagem para definir um esquema de banco de dados multilíngue? Digamos que temos muitas tabelas (100 ou mais) e cada tabela pode ter várias colunas que podem ser localizadas (a maioria das colunas nvarchar deve ser localizável). Por exemplo, uma das tabelas pode conter informações do produto:

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

Posso pensar em três abordagens para oferecer suporte a texto multilíngue nas colunas NAME e DESCRIPTION:

  1. Coluna separada para cada idioma

    Quando adicionamos um novo idioma ao sistema, precisamos criar colunas adicionais para armazenar o texto traduzido, assim:

    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. Tabela de tradução com colunas para cada idioma

    Em vez de armazenar o texto traduzido, apenas uma chave estrangeira para a tabela de traduções é armazenada. A tabela de traduções contém uma coluna para cada idioma.

    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. Tabelas de tradução com linhas para cada idioma

    Em vez de armazenar o texto traduzido, apenas uma chave estrangeira para a tabela de traduções é armazenada. A tabela de traduções contém apenas uma chave e uma tabela separada contém uma linha para cada tradução para um idioma.

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

Há prós e contras em cada solução, e eu gostaria de saber quais são suas experiências com essas abordagens, o que você recomenda e como você desenharia um esquema de banco de dados em vários idiomas.



3
Você pode verificar este link: gsdesign.ro/blog/multilanguage-database-design-approach, embora ler os comentários seja muito útil
Fareed Alnamrouti

3
LANGUAGE_CODEsão chave natural, evite LANGUAGE_ID.
precisa saber é o seguinte

1
Eu já vi / usei os 2. e 3., eu não os recomendo, você acaba facilmente com linhas órfãs. O design do @SunWiKung parece melhor na IMO.
Guillaume86

4
Prefiro o design do SunWuKungs, que por coincidência é o que implementamos. No entanto, você precisa considerar agrupamentos. No Sql Server, pelo menos, cada coluna tem uma propriedade de agrupamento, que determina coisas como distinção entre maiúsculas e minúsculas, equivalência (ou não) de caracteres acentuados e outras considerações específicas do idioma. Se você usa agrupamentos específicos do idioma ou não, depende do design geral do aplicativo, mas se você errar, será difícil mudar mais tarde. Se você precisar de agrupamentos específicos do idioma, precisará de uma coluna por idioma, não de uma linha por idioma.
precisa

Respostas:


113

O que você acha de ter uma tabela de tradução relacionada para cada tabela traduzível?

CREATE TABLE T_PRODUCT (pr_id int, NÚMERO DE PREÇO (18, 2))

CREATE TABLE T_PRODUCT_tr (pr_id INT FK, código de idioma varchar, pr_name text, pr_descr text)

Dessa forma, se você tiver várias colunas traduzíveis, será necessário apenas uma única associação para obtê-la +, uma vez que você não está gerando uma tradução automaticamente, pode ser mais fácil importar itens juntamente com suas traduções relacionadas.

O lado negativo disso é que, se você possui um mecanismo complexo de fallback de idioma, pode ser necessário implementá-lo para cada tabela de conversão - se você está contando com algum procedimento armazenado para fazer isso. Se você fizer isso no aplicativo, isso provavelmente não será um problema.

Deixe-me saber o que você pensa - também estou prestes a tomar uma decisão sobre isso para a nossa próxima aplicação. Até agora, usamos o seu terceiro tipo.


2
Esta opção é semelhante à minha opção nº 1, mas melhor. Ainda é difícil de manter e requer a criação de novas tabelas para novos idiomas, portanto, eu relutaria em implementá-lo.
Qbuek

28
não requer uma nova tabela para um novo idioma - basta adicionar uma nova linha à tabela _tr apropriada com o seu novo idioma, você só precisará criar uma nova tabela _tr se criar uma nova tabela

3
Eu acredito que este é um bom método. outros métodos requerem toneladas de junções esquerdas e, quando você está juntando várias tabelas, cada uma delas tem tradução como 3 níveis de profundidade e cada uma possui 3 campos, você precisa de 3 * 3 9 junções esquerdas apenas para traduções .. caso contrário, também 3. é mais fácil adicionar restrições, etc, e eu acredito que a pesquisa é mais razoável.
GorillaApe

1
Quando T_PRODUCTtem 1 milhão de linhas, T_PRODUCT_trteria 2 milhões. Reduziria muito a eficiência do sql?
Mithril

1
@Mithril De qualquer forma, você tem 2 milhões de linhas. Pelo menos você não precisa de junções com esse método.
David D

56

Esta é uma questão interessante, então vamos necromance.

Vamos começar pelos problemas do método 1:
Problema: você está desnormalizando para economizar velocidade.
No SQL (exceto PostGreSQL com hstore), você não pode passar uma linguagem de parâmetro e dizer:

SELECT ['DESCRIPTION_' + @in_language]  FROM T_Products

Então você tem que fazer isso:

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 

O que significa que você deve alterar TODAS as suas consultas se adicionar um novo idioma. Isso naturalmente leva ao uso de "SQL dinâmico", para que você não precise alterar todas as suas consultas.

Isso geralmente resulta em algo assim (e não pode ser usado em exibições ou funções com valor de tabela, a propósito, o que realmente é um problema se você realmente precisar filtrar a data do relatório)

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

O problema é que:
a formatação da data é muito específica do idioma; portanto, você encontra um problema se não inserir no formato ISO (o que o programador comum de variedades de jardins geralmente não faz e, no caso de um relatório que o usuário com certeza não fará por você, mesmo que seja explicitamente instruído a fazê-lo).
e
b) de forma mais significativa , você perder qualquer tipo de verificação de sintaxe . Se <insert name of your "favourite" person here>alterar o esquema, porque de repente os requisitos para alteração de ala e uma nova tabela é criada, a antiga foi deixada, mas o campo de referência foi renomeado, você não recebe nenhum tipo de aviso. Um relatório até funciona quando você o executa sem selecionar o parâmetro wing (==> guid.empty). Mas de repente, quando um usuário real realmente seleciona uma asa ==>boom . Esse método viola completamente qualquer tipo de teste.


Método 2:
Em poucas palavras: idéia "ótima" (aviso - sarcasmo), vamos combinar as desvantagens do método 3 (velocidade lenta em muitas entradas) com as horríveis desvantagens do método 1.
A única vantagem desse método é que você mantém toda a tradução em uma tabela e, portanto, simplifica a manutenção. No entanto, o mesmo pode ser alcançado com o método 1 e um procedimento dinâmico armazenado em SQL, além de uma tabela (possivelmente temporária) contendo as traduções e o nome da tabela de destino (e é bastante simples, assumindo que você nomeou todos os seus campos de texto como mesmo).


Método 3:
Uma tabela para todas as traduções: Desvantagem: você precisa armazenar n Chaves estrangeiras na tabela de produtos para os n campos que deseja traduzir. Portanto, você precisa fazer n junções para n campos. Quando a tabela de conversão é global, ela possui muitas entradas e as junções ficam lentas. Além disso, você sempre deve ingressar na tabela T_TRANSLATION n vezes para n campos. Isso é uma sobrecarga. Agora, o que você faz quando precisa acomodar traduções personalizadas por cliente? Você precisará adicionar outras junções 2x n em uma tabela adicional. Se você tiver que entrar, digamos 10 mesas, com 2x2xn = 4n junções adicionais, que confusão! Além disso, esse design torna possível usar a mesma tradução com 2 tabelas. Se eu alterar o nome do item em uma tabela, eu realmente quero alterar uma entrada em outra tabela também A CADA VEZ?

Além disso, você não pode mais excluir e reinserir a tabela, porque agora existem chaves estrangeiras NA (s) TABELA (S) DE PRODUTO (s) ... é claro que você pode omitir a configuração dos FKs e, em seguida, <insert name of your "favourite" person here>pode excluir a tabela e reinserir todas as entradas com newid () [ou especificando o ID na inserção, mas com a inserção de identidade desativada ], e isso levaria (e levará) a lixo de dados (e exceções de referência nula) muito em breve.


Método 4 (não listado): Armazenando todos os idiomas em um campo XML no banco de dados. por exemplo

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

Em seguida, você pode obter o valor por XPath-Query no SQL, onde você pode colocar a variável de cadeia em

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

E você pode atualizar o valor assim:

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 

Onde você pode substituir /lang/de/...por'.../' + @in_language + '/...'

Assim como o hstore PostGre, exceto que, devido à sobrecarga de analisar XML (em vez de ler uma entrada de uma matriz associativa no PG hstore), ele fica muito lento, mais a codificação xml torna muito doloroso ser útil.


Método 5 (conforme recomendado pelo SunWuKung, o que você deve escolher): Uma tabela de conversão para cada tabela "Produto". Isso significa uma linha por idioma e vários campos de "texto", portanto, requer apenas UMA (esquerda) junção em N campos. Em seguida, você pode adicionar facilmente um campo padrão na tabela "Produto", excluir e reinserir facilmente a tabela de conversão e criar uma segunda tabela para traduções personalizadas (sob demanda), que também podem ser excluídas. e reinsira), e você ainda terá todas as chaves estrangeiras.

Vamos fazer um exemplo para ver isso FUNCIONA:

Primeiro, crie as tabelas:

CREATE TABLE dbo.T_Languages
(
     Lang_ID int NOT NULL
    ,Lang_NativeName national character varying(200) NULL
    ,Lang_EnglishName national character varying(200) NULL
    ,Lang_ISO_TwoLetterName character varying(10) NULL
    ,CONSTRAINT PK_T_Languages PRIMARY KEY ( Lang_ID )
);

GO




CREATE TABLE dbo.T_Products
(
     PROD_Id int NOT NULL
    ,PROD_InternalName national character varying(255) NULL
    ,CONSTRAINT PK_T_Products PRIMARY KEY ( PROD_Id )
); 

GO



CREATE TABLE dbo.T_Products_i18n
(
     PROD_i18n_PROD_Id int NOT NULL
    ,PROD_i18n_Lang_Id int NOT NULL
    ,PROD_i18n_Text national character varying(200) NULL
    ,CONSTRAINT PK_T_Products_i18n PRIMARY KEY (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id)
);

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  
    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 
    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_Products
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 national character varying(200) NULL
    ,CONSTRAINT PK_T_Products_i18n_Cust PRIMARY KEY ( PROD_i18n_Cust_PROD_Id, PROD_i18n_Cust_Lang_Id )
);

GO

ALTER TABLE dbo.T_Products_i18n_Cust  
    ADD CONSTRAINT FK_T_Products_i18n_Cust_T_Languages 
    FOREIGN KEY(PROD_i18n_Cust_Lang_Id)
    REFERENCES dbo.T_Languages (Lang_ID)

ALTER TABLE dbo.T_Products_i18n_Cust CHECK CONSTRAINT FK_T_Products_i18n_Cust_T_Languages

GO



ALTER TABLE dbo.T_Products_i18n_Cust  
    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

Em seguida, preencha os dados

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

E, em seguida, consulte os dados:

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

Se você é preguiçoso, também pode usar o ISO-TwoLetterName ('DE', 'EN' etc.) como chave primária da tabela de idiomas, e não precisa procurar o ID do idioma. Mas se você fizer isso, talvez queira usar a tag no idioma IETF , o que é melhor, porque você obtém de-CH e de-DE, que não são realmente os mesmos em termos de ortografia (s duplo em vez de ß em todos os lugares) , embora seja o mesmo idioma base. Isso é apenas um pequeno detalhe que pode ser importante para você, especialmente considerando que en-US e en-GB / en-CA / en-AU ou fr-FR / fr-CA têm problemas semelhantes.
Citação: não precisamos, fazemos apenas nosso software em inglês.
Resposta: Sim - mas qual?

De qualquer forma, se você usa um número inteiro, é flexível e pode alterar seu método posteriormente.
E você deve usar esse número inteiro, porque não há nada mais irritante, destrutivo e problemático do que um design de Db danificado.

Veja também RFC 5646 , ISO 639-2 ,

E, se você ainda está dizendo "nós" apenas fazemos nosso pedido para "apenas uma cultura" (como geralmente nos EUA) - portanto, não preciso desse número inteiro extra, esse seria um bom momento e um lugar para mencionar o Tags de idioma da IANA , não?
Porque eles são assim:

de-DE-1901
de-DE-1996

e

de-CH-1901
de-CH-1996

(houve uma reforma ortográfica em 1996 ...) Tente encontrar uma palavra em um dicionário se ela estiver incorreta; isso se torna muito importante em aplicativos que lidam com portais legais e de serviço público.
Mais importante, existem regiões que estão mudando de alfabetos cirílico para latino, o que pode ser mais problemático do que o incômodo superficial de alguma reforma obscura da ortografia, e é por isso que isso também pode ser uma consideração importante, dependendo do país em que você vive. De uma forma ou de outra, é melhor ter esse número inteiro lá, por via das dúvidas ...

Edit:
E adicionando ON DELETE CASCADE depois

REFERENCES dbo.T_Products( PROD_Id )

você pode simplesmente dizer: DELETE FROM T_Productse não obter nenhuma violação de chave estrangeira.

Quanto ao agrupamento, eu faria assim:

A) Tenha seu próprio DAL
B) Salve o nome do agrupamento desejado na tabela de idiomas

Você pode colocar os agrupamentos em sua própria tabela, por exemplo:

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

C) Tenha o nome do agrupamento disponível em suas informações de auth.user.language

D) Escreva seu SQL assim:

SELECT 
    COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName 
FROM T_Groups 

ORDER BY GroupName COLLATE {#COLLATION}

E) Em seguida, você pode fazer isso no seu DAL:

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

O que lhe dará essa consulta SQL perfeitamente composta

SELECT 
    COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName 
FROM T_Groups 

ORDER BY GroupName COLLATE German_PhoneBook_CI_AI

Boa resposta detalhada, muito obrigado. Mas o que você acha dos problemas de agrupamento na solução do Método 5. Parece que essa não é a melhor maneira quando você precisa classificar ou filtrar o texto traduzido no ambiente multilíngue com diferentes agrupamentos. E, nesse caso, o método 2 (que você ostracizou tão rapidamente :)) poderia ser uma opção melhor, com pequenas modificações indicando o agrupamento de destino para cada coluna localizada.
Eugene Evdokimov

2
@Eugene Evdokimov: Sim, mas "ORDER BY" sempre será um problema, porque você não pode especificá-lo como variável. Minha abordagem seria salvar o nome do agrupamento na tabela de idiomas e colocá-lo nas informações do usuário. Então, em cada Instrução SQL, você pode dizer ORDER BY COLUMN_NAME {#collation} e, em seguida, pode fazer uma substituição no seu dal (cmd.CommandText = cmd.CommandText.Replace ("{# COLLATION}", auth.user. . language.collation) Alternativamente, você pode classificar no código do aplicativo, por exemplo, usando LINQ Isso também tomar alguma carga de processamento fora de seu banco de dados para relatórios, o relatório tipos de qualquer maneira...
Stefan Steiger

oo Essa deve ser a resposta SO mais longa que eu já vi e vi pessoas fazerem programas inteiros em respostas. Voce é bom.
Domino

Pode concordo totalmente solução de SunWuKung é a melhor
Domi

48

A terceira opção é a melhor, por alguns motivos:

  • Não requer alteração do esquema do banco de dados para novos idiomas (e, portanto, limita as alterações de código)
  • Não requer muito espaço para idiomas não implementados ou traduções de um item específico
  • Oferece a maior flexibilidade
  • Você não acaba com tabelas esparsas
  • Você não precisa se preocupar com chaves nulas e verificar se está exibindo uma tradução existente em vez de alguma entrada nula.
  • Se você alterar ou expandir seu banco de dados para abranger outros itens / itens / coisas traduzíveis, etc, poderá usar as mesmas tabelas e sistema - isso é muito desacoplado do restante dos dados.

-Adão


1
Eu concordo, embora pessoalmente eu tenha uma tabela localizada para cada tabela principal, para permitir que chaves estrangeiras sejam implementadas.
Neil Barnwell

1
Embora a terceira opção seja a implementação mais limpa e sólida do problema, é mais complexa do que a primeira. Acho que exibir, editar e relatar a versão geral exige tanto esforço extra que nem sempre é aceitável. Eu implementei as duas soluções, o mais simples foi o suficiente quando os usuários precisaram de uma tradução somente leitura (às vezes ausente) da linguagem "principal" do aplicativo.
Ric

12
E se a tabela do produto contiver vários campos traduzidos? Ao recuperar produtos, você precisará fazer uma associação adicional por campo traduzido, o que resultará em graves problemas de desempenho. Também há (IMO) complexidade adicional para inserção / atualização / exclusão. A única vantagem disso é o menor número de tabelas. Eu usaria o método proposto por SunWuKung: acho que é um bom equilíbrio entre problemas de desempenho, complexidade e manutenção.
Frosty Z

@ rics- Eu concordo, bem, o que você sugere ...?
sabre

@ Adam- Estou confuso, talvez eu tenha entendido errado. Você sugeriu o terceiro, certo? Por favor, explique com mais detalhes como serão as relações entre essas tabelas? Você quer dizer que precisamos implementar as tabelas Translation e TranslationEntry para cada tabela no DB?
sabre

9

Veja este exemplo:

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

Eu acho que não há necessidade de explicar, a estrutura se descreve.


isso é bom. mas como você pesquisaria (por exemplo, product_name)?
Illuminati

Você teve um exemplo ao vivo em algum lugar da sua amostra? Você teve algum problema ao usá-lo?
David Létourneau 11/01

Claro, eu tenho um projeto imobiliário multilíngue, suportamos 4 idiomas. A pesquisa é um pouco complicada, mas é rápida. Obviamente, em grandes projetos, pode ser mais lento do que precisa. Em projetos pequenos ou médios, tudo bem.
bamburik

8

Eu normalmente usaria essa abordagem (não o sql real), isso corresponde à sua última opção.

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'

Porque ter todos os textos traduzíveis em um só lugar facilita muito a manutenção. Às vezes, as traduções são terceirizadas para agências de tradução, dessa forma você pode enviar a eles apenas um grande arquivo de exportação e importá-lo novamente com a mesma facilidade.


1
Para que finalidade serve a Translationtabela ou a TranslationItem.translationitemidcoluna?
DanMan

4

Antes de ir para detalhes e soluções técnicas, você deve parar por um minuto e fazer algumas perguntas sobre os requisitos. As respostas podem ter um enorme impacto na solução técnica. Exemplos de tais perguntas seriam:
- Todos os idiomas serão usados ​​o tempo todo?
- Quem e quando preencherá as colunas com as diferentes versões de idiomas?
- O que acontece quando um usuário precisa de um determinado idioma de texto e não existe nenhum no sistema?
- Apenas os textos devem ser localizados ou também existem outros itens (por exemplo, PRICE pode ser armazenado em $ e € porque podem ser diferentes)


Sei que a localização é um tópico muito mais amplo e estou ciente dos problemas que você traz à minha atenção, mas atualmente estou procurando uma resposta para um problema muito específico de design de esquema. Suponho que novos idiomas serão adicionados gradualmente e cada um será traduzido quase completamente.
Qbuek

3

Eu estava procurando algumas dicas para localização e encontrei este tópico. Eu queria saber por que isso é usado:

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

Então você obtém algo como user39603 sugere:

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'

Você não pode simplesmente deixar a tabela de fora para obter o seguinte:

    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'

1
Certo. Eu chamaria a ProductItemmesa de algo parecido ProductTextsou ProductL10nnão. Faz mais sentido.
DanMan

1

Eu concordo com o randomizador. Não vejo por que você precisa de uma "tradução" de tabela.

Eu acho que isso é suficiente:

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

1

A abordagem abaixo seria viável? Digamos que você tenha tabelas em que mais de uma coluna precisa ser traduzida. Portanto, para o produto, você pode ter o nome e a descrição do produto que precisam ser traduzidos. Você poderia fazer o seguinte:

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

0

"Qual é o melhor" é baseado na situação do projeto. O primeiro é fácil de selecionar e manter, e também o melhor desempenho, pois não é necessário ingressar nas tabelas quando a entidade é selecionada. Se você confirmou que seu projeto é compatível apenas com 2 ou 3 idiomas e não aumenta, você pode usá-lo.

O segundo é ok, mas é difícil de entender e manter. E o desempenho é pior que o primeiro.

O último é bom em escalabilidade, mas ruim em desempenho. A tabela T_TRANSLATION_ENTRY se tornará cada vez maior; é terrível quando você deseja recuperar uma lista de entidades de algumas tabelas.


0

Este documento descreve as possíveis soluções e as vantagens e desvantagens de cada método. Prefiro a "localização da linha" porque você não precisa modificar o esquema do banco de dados ao adicionar um novo idioma.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.