Esquema para una base de datos multilenguaje

235

Estoy desarrollando un software multilenguaje. En cuanto al código de la aplicación, la localización no es un problema. Podemos usar recursos específicos del idioma y tener todo tipo de herramientas que funcionen bien con ellos.

Pero, ¿cuál es el mejor enfoque para definir un esquema de base de datos multilenguaje? Digamos que tenemos muchas tablas (100 o más), y cada tabla puede tener múltiples columnas que se pueden localizar (la mayoría de las columnas nvarchar deben ser localizables). Por ejemplo, una de las tablas puede contener información del producto:

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

Se me ocurren tres enfoques para admitir texto multilingüe en las columnas NOMBRE y DESCRIPCIÓN:

  1. Columna separada para cada idioma

    Cuando agregamos un nuevo idioma al sistema, debemos crear columnas adicionales para almacenar el texto traducido, así:

    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. Tabla de traducción con columnas para cada idioma.

    En lugar de almacenar texto traducido, solo se almacena una clave foránea para la tabla de traducciones. La tabla de traducciones contiene una columna 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. Tablas de traducción con filas para cada idioma.

    En lugar de almacenar texto traducido, solo se almacena una clave foránea para la tabla de traducciones. La tabla de traducciones contiene solo una clave, y una tabla separada contiene una fila para cada traducción a un 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)
    )

Hay ventajas y desventajas de cada solución, y me gustaría saber cuáles son sus experiencias con estos enfoques, qué recomienda y cómo haría para diseñar un esquema de base de datos en varios idiomas.

qbeuek
fuente
3
Puede consultar este enlace: gsdesign.ro/blog/multilanguage-database-design-approach aunque leer los comentarios es muy útil
Fareed Alnamrouti
3
LANGUAGE_CODESon claves naturales, evitar LANGUAGE_ID.
gavenkoa
1
Ya vi / usé los 2. y 3., no los recomiendo, fácilmente terminas con filas huérfanas. El diseño de @SunWiKung se ve mejor IMO.
Guillaume86
44
Prefiero el diseño de SunWuKungs, que casualmente es lo que hemos implementado. Sin embargo, debe tener en cuenta las intercalaciones. En SQL Server al menos, cada columna tiene una propiedad de intercalación, que determina cosas como la distinción entre mayúsculas y minúsculas, la equivalencia (o no) de caracteres acentuados y otras consideraciones específicas del idioma. Si usa intercalaciones específicas del idioma o no, depende del diseño general de su aplicación, pero si se equivoca, será difícil cambiarlo más adelante. Si necesita intercalaciones específicas del idioma, necesitará una columna por idioma, no una fila por idioma.
Elroy Flynn

Respuestas:

113

¿Qué opinas sobre tener una tabla de traducción relacionada para cada tabla traducible?

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)

De esta manera, si tiene varias columnas traducibles, solo necesitaría una sola combinación para obtenerlo, ya que no está generando automáticamente un ID de traducción, puede ser más fácil importar elementos junto con sus traducciones relacionadas.

El lado negativo de esto es que si tiene un mecanismo de recuperación de idioma complejo, es posible que deba implementarlo para cada tabla de traducción, si confía en algún procedimiento almacenado para hacerlo. Si haces eso desde la aplicación, probablemente esto no sea un problema.

Déjame saber lo que piensas: también estoy a punto de tomar una decisión sobre esto para nuestra próxima aplicación. Hasta ahora hemos usado su 3er tipo.

Comunidad
fuente
2
Esta opción es similar a mi opción nr 1 pero mejor. Todavía es difícil de mantener y requiere crear nuevas tablas para nuevos idiomas, por lo que sería reacio a implementarlo.
qbeuek
28
no requiere una nueva tabla para un nuevo idioma: simplemente agregue una nueva fila a la tabla _tr apropiada con su nuevo idioma, solo necesita crear una nueva tabla _tr si crea una nueva tabla traducible
3
Creo que este es un buen método. otros métodos requieren toneladas de combinaciones izquierdas y cuando une varias tablas, cada una de ellas tiene traducción como 3 niveles de profundidad, y cada una tiene 3 campos, necesita 3 * 3 9 combinaciones izquierdas solo para las traducciones ... de lo contrario 3. También es más fácil agregar restricciones, etc. y creo que la búsqueda es más razonable.
GorillaApe
1
Cuando T_PRODUCTtiene 1 millón de filas, T_PRODUCT_trtendría 2 millones. ¿Reduciría mucho la eficiencia de sql?
Mithril
1
@ Mitril De cualquier manera, tienes 2 millones de filas. Al menos no necesitas uniones con este método.
David D
56

Este es un tema interesante, así que vamos a la nigromancia.

Comencemos por los problemas del método 1:
Problema: está desnormalizando para ahorrar velocidad.
En SQL (excepto PostGreSQL con hstore), no puede pasar un lenguaje de parámetros y decir:

SELECT ['DESCRIPTION_' + @in_language]  FROM T_Products

Entonces tienes que hacer esto:

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 

Lo que significa que debe modificar TODAS sus consultas si agrega un nuevo idioma. Naturalmente, esto lleva a usar "SQL dinámico", por lo que no tiene que alterar todas sus consultas.

Esto generalmente resulta en algo como esto (y no se puede usar en vistas o funciones con valores de tabla por cierto, lo que realmente es un problema si realmente necesita filtrar la fecha del informe)

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

El problema con esto es
a) El formato de fecha es muy específico del idioma, por lo que tiene un problema allí, si no ingresa en formato ISO (que el programador promedio de variedades de jardín generalmente no hace, y en caso de un informe que el usuario seguramente no hará por usted, incluso si se le indica explícitamente que lo haga).
y
b) lo más importante , se pierde cualquier tipo de comprobación de sintaxis . Si <insert name of your "favourite" person here>altera el esquema porque de repente los requisitos para el cambio de alas, y se crea una nueva tabla, la anterior se fue pero se renombró el campo de referencia, no recibe ningún tipo de advertencia. Un informe incluso funciona cuando lo ejecuta sin seleccionar el parámetro de ala (==> guid.empty). Pero de repente, cuando un usuario real realmente selecciona un ala ==>auge . Este método infringe completamente cualquier tipo de prueba.


Método 2:
En pocas palabras: "Gran" idea (advertencia - sarcasmo), combinemos las desventajas del método 3 (velocidad lenta cuando hay muchas entradas) con las desventajas bastante horribles del método 1.
La única ventaja de este método es que usted mantiene todas las traducciones en una tabla y, por lo tanto, simplifican el mantenimiento Sin embargo, se puede lograr lo mismo con el método 1 y un procedimiento almacenado dinámico de SQL, y una tabla (posiblemente temporal) que contiene las traducciones, y el nombre de la tabla de destino (y es bastante simple asumiendo que usted nombró todos sus campos de texto) mismo).


Método 3:
una tabla para todas las traducciones: Desventaja: debe almacenar n claves foráneas en la tabla de productos para n campos que desea traducir. Por lo tanto, debe hacer n combinaciones para n campos. Cuando la tabla de traducción es global, tiene muchas entradas y las uniones se vuelven lentas. Además, siempre debe unirse a la tabla T_TRANSLATION n veces para n campos. Esto es un gran gasto. Ahora, ¿qué haces cuando debes acomodar traducciones personalizadas por cliente? Tendrá que agregar otras 2x n combinaciones en una tabla adicional. Si tiene que unirse, digamos 10 tablas, con 2x2xn = 4n combinaciones adicionales, ¡qué desastre! Además, este diseño permite utilizar la misma traducción con 2 tablas. Si cambio el nombre del elemento en una tabla, ¿realmente quiero cambiar también una entrada en otra tabla CADA VEZ?

Además, ya no puede eliminar y volver a insertar la tabla, porque ahora hay claves foráneas EN LA (S) TABLA (S) DEL PRODUCTO ... por supuesto, puede omitir la configuración de los FK y luego <insert name of your "favourite" person here>puede eliminar la tabla y volver a insertarla todas las entradas con newid () [o especificando la identificación en la inserción, pero con la inserción de identidad desactivada ], y eso (y conducirá) a basura de datos (y excepciones de referencia nula) muy pronto.


Método 4 (no listado): Almacenar todos los idiomas en un campo XML en la base de datos. p.ej

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

Luego puede obtener el valor mediante XPath-Query en SQL, donde puede colocar la variable de cadena en

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

Y puede actualizar el valor de esta manera:

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 

Donde puedes reemplazar /lang/de/...con'.../' + @in_language + '/...'

Al igual que el hstore de PostGre, excepto que debido a la sobrecarga de analizar XML (en lugar de leer una entrada de una matriz asociativa en PG hstore) se vuelve demasiado lento y la codificación xml hace que sea demasiado doloroso ser útil.


Método 5 (según lo recomendado por SunWuKung, el que debe elegir): una tabla de traducción para cada tabla de "Producto". Eso significa una fila por idioma y varios campos de "texto", por lo que solo se requiere UNA (izquierda) para unirse en N campos. Luego puede agregar fácilmente un campo predeterminado en la tabla "Producto", puede eliminar y volver a insertar fácilmente la tabla de traducción, y puede crear una segunda tabla para traducciones personalizadas (bajo demanda), que también puede eliminar y vuelva a insertar), y todavía tiene todas las claves foráneas.

Hagamos un ejemplo para ver esto FUNCIONA:

Primero, crea las tablas:

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

Luego complete los datos

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

Y luego consulta los datos:

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

Si eres perezoso, entonces también puedes usar el ISO-TwoLetterName ('DE', 'EN', etc.) como clave principal de la tabla de idiomas, entonces no tienes que buscar la identificación del idioma. Pero si lo hace, tal vez desee usar la etiqueta de idioma IETF , lo cual es mejor, porque obtiene de-CH y de-DE, que en realidad no es lo mismo en cuanto a ortografía (doble s en lugar de ß en todas partes) , aunque es el mismo lenguaje base. Es solo un pequeño detalle que puede ser importante para usted, especialmente teniendo en cuenta que en-US y en-GB / en-CA / en-AU o fr-FR / fr-CA tiene problemas similares.
Cita: no lo necesitamos, solo hacemos nuestro software en inglés.
Respuesta: Sí, pero ¿cuál?

De todos modos, si usa una ID de entero, es flexible y puede cambiar su método en cualquier momento posterior.
Y debe usar ese número entero, porque no hay nada más molesto, destructivo y problemático que un diseño Db fallido.

Ver también RFC 5646 , ISO 639-2 ,

Y, si todavía dice "nosotros" solo hacemos nuestra solicitud para "solo una cultura" (como en EE. UU. Por lo general), por lo tanto, no necesito ese número entero adicional, este sería un buen momento y lugar para mencionar el Etiquetas de idioma IANA , ¿no?
Porque van así:

de-DE-1901
de-DE-1996

y

de-CH-1901
de-CH-1996

(hubo una reforma ortográfica en 1996 ...) Intente encontrar una palabra en un diccionario si está mal escrita; Esto se vuelve muy importante en aplicaciones que tratan con portales legales y de servicio público.
Más importante aún, hay regiones que están cambiando de alfabetos cirílicos a latinos, lo que puede ser más problemático que la molestia superficial de alguna reforma de ortografía oscura, por lo que esto podría ser una consideración importante también, dependiendo del país en el que viva. De una forma u otra, es mejor tener ese número entero allí, por si acaso ...

Editar:
y agregando ON DELETE CASCADE después

REFERENCES dbo.T_Products( PROD_Id )

simplemente puede decir: DELETE FROM T_Productsy no obtener ninguna violación de clave externa.

En cuanto a la recopilación, lo haría así:

A) Tenga su propio DAL
B) Guarde el nombre de intercalación deseado en la tabla de idiomas

Es posible que desee colocar las intercalaciones en su propia tabla, por ejemplo:

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

C) Tenga el nombre de intercalación disponible en su información de idioma de usuario.

D) Escribe tu SQL así:

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

ORDER BY GroupName COLLATE {#COLLATION}

E) Entonces, puedes hacer esto en tu DAL:

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

Lo que luego le dará esta consulta SQL perfectamente compuesta

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

ORDER BY GroupName COLLATE German_PhoneBook_CI_AI
Stefan Steiger
fuente
Buena respuesta detallada, muchas gracias. Pero, ¿qué piensa sobre los problemas de colación en la solución del Método 5? Parece que esta no es la mejor manera cuando necesita ordenar o filtrar el texto traducido en el entorno multilingüe con diferentes intercalaciones. Y en tal caso, el Método 2 (que "excluyó" tan rápidamente :)) podría ser una mejor opción con ligeras modificaciones que indican la clasificación objetivo para cada columna localizada.
Eugene Evdokimov
2
@Eugene Evdokimov: Sí, pero "ORDER BY" siempre será un problema, porque no puede especificarlo como una variable. Mi enfoque sería guardar el nombre de la clasificación en la tabla de idiomas y tener esto en la información del usuario. Luego, en cada instrucción SQL puede decir ORDER BY COLUMN_NAME {#collation}, y luego puede hacer un reemplazo en su dal (cmd.CommandText = cmd.CommandText.Replace ("{# COLLATION}", auth.user. . language.collation) Alternativamente, puede ordenar en el código de aplicación, por ejemplo, utilizando LINQ Esto también llevaría algún carga de procesamiento fuera de su base de datos para los informes, los tipos de informes de todos modos...
Stefan Steiger
oo Esta debe ser la respuesta SO más larga que he visto, y vi a personas hacer programas completos en respuestas. Estas bien.
Domino
Estoy totalmente de acuerdo en que la solución de SunWuKung es la mejor
Domi
48

La tercera opción es la mejor, por algunas razones:

  • No requiere alterar el esquema de la base de datos para nuevos idiomas (y, por lo tanto, limitar los cambios de código)
  • No requiere mucho espacio para idiomas no implementados o traducciones de un elemento en particular
  • Proporciona la mayor flexibilidad.
  • No terminas con mesas dispersas
  • No tiene que preocuparse por las claves nulas y comprobar que está mostrando una traducción existente en lugar de alguna entrada nula.
  • Si cambia o expande su base de datos para abarcar otros elementos / cosas traducibles / etc., puede usar las mismas tablas y sistema, esto está muy desacoplado del resto de los datos.

-Adán

Adam Davis
fuente
1
Estoy de acuerdo, aunque personalmente tendría una tabla localizada para cada tabla principal, para permitir la implementación de claves externas.
Neil Barnwell
1
Aunque la tercera opción es la implementación más limpia y sólida del problema, es más compleja que la primera. Creo que mostrar, editar e informar la versión general requiere tanto esfuerzo adicional que no siempre es aceptable. He implementado ambas soluciones, la más simple fue suficiente cuando los usuarios necesitaban una traducción de solo lectura (a veces falta) del lenguaje de aplicación "principal".
rics
12
¿Qué sucede si la tabla de productos contiene varios campos traducidos? Al recuperar productos, tendrá que hacer una unión adicional por campo traducido, lo que provocará graves problemas de rendimiento. También hay (IMO) complejidad adicional para insertar / actualizar / eliminar. La única ventaja de esto es el menor número de tablas. Optaría por el método propuesto por SunWuKung: creo que es un buen equilibrio entre el rendimiento, la complejidad y los problemas de mantenimiento.
Frosty Z
@ rics- Estoy de acuerdo, bueno, ¿qué sugieres para ...?
saber
@ Adam: estoy confundido, tal vez no he entendido bien. Usted sugirió el tercero, ¿verdad? Por favor explique con más detalle cómo serán las relaciones entre esas tablas. ¿Quiere decir que tenemos que implementar las tablas Translation y TranslationEntry para cada tabla en DB?
saber
9

Echa un vistazo a este ejemplo:

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

Creo que no hay necesidad de explicar, la estructura se describe a sí misma.

bamburik
fuente
Esto es bueno. pero, ¿cómo buscarías (por ejemplo, nombre_producto)?
Illuminati
¿Tuviste un ejemplo en vivo en alguna parte de tu muestra? ¿Tuviste algún problema al usarlo?
David Létourneau
Claro, tengo un proyecto inmobiliario multilingüe, admitimos 4 idiomas. La búsqueda es un poco complicada, pero es rápida. Por supuesto, en proyectos grandes puede ser más lento de lo necesario. En proyectos pequeños o medianos está bien.
bamburik
8

Por lo general, optaría por este enfoque (no sql real), esto corresponde con su última opción.

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 tener todos los textos traducibles en un solo lugar hace que el mantenimiento sea mucho más fácil. A veces, las traducciones se subcontratan a oficinas de traducción, de esta manera puede enviarles un solo gran archivo de exportación e importarlo de nuevo con la misma facilidad.

usuario39603
fuente
1
¿Para qué sirve la Translationtabla o la TranslationItem.translationitemidcolumna?
DanMan
4

Antes de ir a detalles técnicos y soluciones, debe detenerse por un minuto y hacer algunas preguntas sobre los requisitos. Las respuestas pueden tener un gran impacto en la solución técnica. Ejemplos de tales preguntas serían:
- ¿Se usarán todos los idiomas todo el tiempo?
- ¿Quién y cuándo llenará las columnas con las diferentes versiones de idioma?
- ¿Qué sucede cuando un usuario necesitará cierto idioma de un texto y no hay ninguno en el sistema?
- Solo se deben localizar los textos o también hay otros elementos (por ejemplo, el PRECIO se puede almacenar en $ y € porque pueden ser diferentes)

Aleris
fuente
Sé que la localización es un tema mucho más amplio y soy consciente de los problemas que usted me llama la atención, pero actualmente estoy buscando una respuesta para un problema muy específico del diseño de esquemas. Supongo que se agregarán nuevos idiomas de forma incremental y cada uno se traducirá casi por completo.
qbeuek
3

Estaba buscando algunos consejos para la localización y encontré este tema. Me preguntaba por qué se usa esto:

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

Entonces obtienes algo como user39603 sugiere:

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'

¿No puedes dejar la tabla? Traducción para que obtengas esto:

    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'
aleatorizador
fuente
1
Por supuesto. Llamaría a la ProductItemmesa algo así ProductTextso algo así ProductL10n. Tiene más sentido.
DanMan
1

Estoy de acuerdo con randomizer. No veo por qué necesitas una tabla de "traducción".

Creo que esto es suficiente:

TA_product: ProductID, ProductPrice
TA_Language: LanguageID, Language
TA_Productname: ProductnameID, ProductID, LanguageID, ProductName
Bart VW
fuente
1

¿Sería viable el siguiente enfoque? Supongamos que tiene tablas en las que se necesita traducir más de 1 columna. Entonces, para el producto, podría tener tanto el nombre del producto como la descripción del producto que necesitan traducción. ¿Podría hacer lo siguiente?

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)
    )   
davey
fuente
0

"Cuál es el mejor" se basa en la situación del proyecto. El primero es fácil de seleccionar y mantener, y también el rendimiento es mejor ya que no es necesario unir las tablas cuando se selecciona la entidad. Si confirmó que su proyecto solo admite 2 o 3 idiomas y no aumentará, puede usarlo.

El segundo es okey pero es difícil de entender y mantener. Y el rendimiento es peor que el primero.

El último es bueno en escalabilidad pero malo en rendimiento. La tabla T_TRANSLATION_ENTRY se volverá más y más grande, es terrible cuando desea recuperar una lista de entidades de algunas tablas.

estudio
fuente
0

Este documento describe las posibles soluciones y las ventajas y desventajas de cada método. Prefiero la "localización de filas" porque no tiene que modificar el esquema de base de datos al agregar un nuevo idioma.

Jaska
fuente