Schéma d'une base de données multilingue

236

Je développe un logiciel multilingue. En ce qui concerne le code d'application, la localisation n'est pas un problème. Nous pouvons utiliser des ressources spécifiques aux langues et disposer de toutes sortes d'outils qui fonctionnent bien avec elles.

Mais quelle est la meilleure approche pour définir un schéma de base de données multilingue? Disons que nous avons beaucoup de tables (100 ou plus), et chaque table peut avoir plusieurs colonnes qui peuvent être localisées (la plupart des colonnes nvarchar doivent être localisables). Par exemple, l'un des tableaux peut contenir des informations sur le produit:

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

Je peux penser à trois approches pour prendre en charge le texte multilingue dans les colonnes NOM et DESCRIPTION:

  1. Colonne séparée pour chaque langue

    Lorsque nous ajoutons une nouvelle langue au système, nous devons créer des colonnes supplémentaires pour stocker le texte traduit, comme ceci:

    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. Tableau de traduction avec colonnes pour chaque langue

    Au lieu de stocker du texte traduit, seule une clé étrangère de la table des traductions est stockée. Le tableau des traductions contient une colonne pour chaque langue.

    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. Tableaux de traduction avec des lignes pour chaque langue

    Au lieu de stocker du texte traduit, seule une clé étrangère de la table des traductions est stockée. La table des traductions contient uniquement une clé et une table distincte contient une ligne pour chaque traduction dans une langue.

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

Il y a des avantages et des inconvénients à chaque solution, et je voudrais savoir quelles sont vos expériences avec ces approches, que recommandez-vous et comment allez-vous concevoir un schéma de base de données multilingue.

qbeuek
la source
3
Vous pouvez vérifier ce lien: gsdesign.ro/blog/multilanguage-database-design-approach bien que la lecture des commentaires soit très utile
Fareed Alnamrouti
3
LANGUAGE_CODEsont la clé naturelle, à éviter LANGUAGE_ID.
gavenkoa
1
J'ai déjà vu / utilisé les 2. et 3., je ne les recommande pas, vous vous retrouvez facilement avec des lignes orphelines. La conception de @SunWiKung semble meilleure IMO.
Guillaume86
4
Je préfère la conception SunWuKungs, qui est par coïncidence ce que nous avons mis en œuvre. Cependant, vous devez considérer les classements. Dans Sql Server au moins, chaque colonne possède une propriété de classement, qui détermine des éléments tels que la casse, l'équivalence (ou non) des caractères accentués et d'autres considérations spécifiques à la langue. Que vous utilisiez ou non des classements spécifiques à la langue dépend de la conception globale de votre application, mais si vous vous trompez, il sera difficile de changer plus tard. Si vous avez besoin de classements spécifiques à une langue, vous aurez besoin d'une colonne par langue, pas d'une ligne par langue.
Elroy Flynn

Réponses:

114

Que pensez-vous d'avoir une table de traduction associée pour chaque table traduisible?

CRÉER LE TABLEAU T_PRODUIT (pr_id int, NUMÉRO DE PRIX (18, 2))

CREATE TABLE T_PRODUCT_tr (pr_id INT FK, languagecode varchar, pr_name text, pr_descr text)

De cette façon, si vous avez plusieurs colonnes traduisibles, il ne faudrait qu'une seule jointure pour l'obtenir + puisque vous ne générez pas automatiquement une traduction, il peut être plus facile d'importer des éléments avec leurs traductions associées.

Le côté négatif de ceci est que si vous avez un mécanisme de secours de langage complexe, vous devrez peut-être l'implémenter pour chaque table de traduction - si vous comptez sur une procédure stockée pour le faire. Si vous le faites depuis l'application, cela ne sera probablement pas un problème.

Dites-moi ce que vous en pensez - je suis également sur le point de prendre une décision à ce sujet pour notre prochaine candidature. Jusqu'à présent, nous avons utilisé votre 3ème type.

Communauté
la source
2
Cette option est similaire à mon option n ° 1 mais en mieux. Il est toujours difficile à maintenir et nécessite la création de nouvelles tables pour de nouvelles langues, donc je serais réticent à l'implémenter.
qbeuek
28
il ne nécessite pas de nouvelle table pour une nouvelle langue - vous ajoutez simplement une nouvelle ligne à la table _tr appropriée avec votre nouvelle langue, vous n'avez besoin de créer une nouvelle table _tr que si vous créez une nouvelle table traduisible
3
je crois que c'est une bonne méthode. d'autres méthodes nécessitent des tonnes de jointures à gauche et lorsque vous joignez plusieurs tables que chacune d'elles a une traduction comme 3 niveaux de profondeur, et chacune a 3 champs, vous avez besoin de 3 * 3 9 jointures à gauche uniquement pour les traductions. il est plus facile d'ajouter des contraintes, etc. et je pense que la recherche est plus efficace.
GorillaApe
1
Quand T_PRODUCTa 1 million de lignes, T_PRODUCT_traurait 2 millions. Cela réduirait-il beaucoup l'efficacité sql?
Mithril
1
@Mithril Dans les deux cas, vous avez 2 millions de lignes. Au moins, vous n'avez pas besoin de jointures avec cette méthode.
David D
56

C'est un problème intéressant, alors nécromancions.

Commençons par les problèmes de la méthode 1:
Problème: vous dénormalisez pour gagner en vitesse.
En SQL (sauf PostGreSQL avec hstore), vous ne pouvez pas passer un langage de paramètres et dire:

SELECT ['DESCRIPTION_' + @in_language]  FROM T_Products

Vous devez donc faire ceci:

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 

Ce qui signifie que vous devez modifier TOUTES vos requêtes si vous ajoutez une nouvelle langue. Cela conduit naturellement à utiliser du "SQL dynamique", vous n'avez donc pas à modifier toutes vos requêtes.

Cela se traduit généralement par quelque chose comme ça (et il ne peut pas être utilisé dans les vues ou les fonctions table par ailleurs, ce qui est vraiment un problème si vous avez réellement besoin de filtrer la date de rapport)

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

Le problème avec ceci est
a) Le formatage de la date est très spécifique à la langue, donc vous obtenez un problème là-bas, si vous n'entrez pas au format ISO (ce que le programmeur de jardin moyen ne fait généralement pas, et en cas de un rapport que l'utilisateur sûr que l'enfer ne fera pas pour vous, même s'il est explicitement chargé de le faire).
et
b) plus important encore , vous perdez tout type de vérification de syntaxe . Si <insert name of your "favourite" person here>modifie le schéma parce que soudainement, les exigences de changement d'aile et qu'une nouvelle table est créée, l'ancienne est partie mais le champ de référence a été renommé, vous n'avez aucun avertissement. Un rapport fonctionne même lorsque vous l'exécutez sans sélectionner le paramètre wing (==> guid.empty). Mais soudain, quand un utilisateur réel sélectionne réellement une aile ==>boom . Cette méthode rompt complètement tout type de test.


Méthode 2:
En résumé: "Grande" idée (avertissement - sarcasme), combinons les inconvénients de la méthode 3 (vitesse lente avec de nombreuses entrées) aux inconvénients plutôt horribles de la méthode 1.
Le seul avantage de cette méthode est que vous gardez toutes les traductions dans une seule table, et donc la maintenance est simple. Cependant, la même chose peut être obtenue avec la méthode 1 et une procédure stockée SQL dynamique, et une table (éventuellement temporaire) contenant les traductions et le nom de la table cible (et est assez simple en supposant que vous avez nommé tous vos champs de texte le même).


Méthode 3:
une table pour toutes les traductions: Inconvénient: vous devez stocker n clés étrangères dans la table des produits pour n champs que vous souhaitez traduire. Par conséquent, vous devez effectuer n jointures pour n champs. Lorsque la table de traduction est globale, elle comporte de nombreuses entrées et les jointures deviennent lentes. De plus, vous devez toujours joindre la table T_TRANSLATION n fois pour n champs. C'est tout à fait une surcharge. Maintenant, que faites-vous lorsque vous devez accepter des traductions personnalisées par client? Vous devrez ajouter encore 2x n jointures sur une table supplémentaire. Si vous devez rejoindre, disons 10 tables, avec 2x2xn = 4n jointures supplémentaires, quel gâchis! De plus, cette conception permet d'utiliser la même traduction avec 2 tableaux. Si je change le nom d'un élément dans une table, est-ce que je veux vraiment changer une entrée dans une autre table aussi CHAQUE FOIS?

De plus, vous ne pouvez plus supprimer et réinsérer la table, car il y a maintenant des clés étrangères DANS LES TABLEAUX DE PRODUITS ... vous pouvez bien sûr omettre de définir les FK, puis <insert name of your "favourite" person here>supprimer la table et réinsérer toutes les entrées avec newid () [ou en spécifiant l'id dans l'insertion, mais ayant l' identité-insertion désactivée ], et cela conduirait (et entraînera) très rapidement des données-garbage (et des exceptions de référence nulle).


Méthode 4 (non répertoriée): stockage de toutes les langues dans un champ XML de la base de données. par exemple

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

Ensuite, vous pouvez obtenir la valeur par XPath-Query en SQL, où vous pouvez mettre la variable de chaîne dans

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

Et vous pouvez mettre à jour la valeur comme ceci:

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 

Où vous pouvez remplacer /lang/de/...par'.../' + @in_language + '/...'

Un peu comme l'hstore PostGre, sauf qu'en raison de la surcharge d'analyse XML (au lieu de lire une entrée d'un tableau associatif dans PG hstore), il devient beaucoup trop lent et l'encodage xml le rend trop pénible pour être utile.


Méthode 5 (comme recommandé par SunWuKung, celle que vous devez choisir): Une table de traduction pour chaque table "Produit". Cela signifie une ligne par langue et plusieurs champs "texte", il ne nécessite donc qu'une seule jointure (gauche) sur N champs. Ensuite, vous pouvez facilement ajouter un champ par défaut dans la table "Produit", vous pouvez facilement supprimer et réinsérer la table de traduction, et vous pouvez créer une deuxième table pour les traductions personnalisées (sur demande), que vous pouvez également supprimer et réinsérez), et vous avez toujours toutes les clés étrangères.

Faisons un exemple pour voir ce TRAVAIL:

Créez d'abord les tableaux:

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

Remplissez ensuite les données

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

Et puis interrogez les données:

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 vous êtes paresseux, vous pouvez également utiliser l'ISO-TwoLetterName ('DE', 'EN', etc.) comme clé primaire de la table des langues, vous n'avez donc pas à rechercher l'ID de la langue. Mais si vous le faites, vous voudrez peut-être utiliser la balise de langue IETF à la place, ce qui est mieux, car vous obtenez de-CH et de-DE, ce qui n'est vraiment pas la même chose en termes d'ortographie (double s au lieu de ß partout) , bien qu'il s'agisse du même langage de base. C'est un tout petit détail qui peut être important pour vous, surtout si l'on considère que en-US et en-GB / en-CA / en-AU ou fr-FR / fr-CA ont des problèmes similaires.
Quote: nous n'en avons pas besoin, nous ne faisons que notre logiciel en anglais.
Réponse: Oui - mais lequel ??

Quoi qu'il en soit, si vous utilisez un ID entier, vous êtes flexible et pouvez modifier votre méthode à tout moment.
Et vous devez utiliser cet entier, car il n'y a rien de plus ennuyeux, destructeur et gênant qu'une conception Db bâclée.

Voir aussi RFC 5646 , ISO 639-2 ,

Et, si vous dites toujours "nous" ne faisons notre demande que pour " une seule culture" (comme en-US en général) - donc je n'ai pas besoin de cet entier supplémentaire, ce serait le bon moment et l'endroit pour mentionner le Les balises de langue IANA , n'est-ce pas?
Parce qu'ils vont comme ça:

de-DE-1901
de-DE-1996

et

de-CH-1901
de-CH-1996

(il y a eu une réforme de l'orthographe en 1996 ...) Essayez de trouver un mot dans un dictionnaire s'il est mal orthographié; cela devient très important dans les applications traitant des portails juridiques et de service public.
Plus important encore, certaines régions passent de l'alphabet cyrillique à l'alphabet latin, ce qui peut être plus gênant que la nuisance superficielle d'une réforme obscure de l'orthographe, c'est pourquoi cela pourrait également être un facteur important, selon le pays dans lequel vous vivez. D'une façon ou d'une autre, il vaut mieux avoir cet entier là, juste au cas où ...

Edit:
Et en ajoutant ON DELETE CASCADE après

REFERENCES dbo.T_Products( PROD_Id )

vous pouvez simplement dire: DELETE FROM T_Productset n'obtenir aucune violation de clé étrangère.

Quant au classement, je le ferais comme ceci:

A) Disposez de votre propre DAL
B) Enregistrez le nom du classement souhaité dans la table des langues

Vous voudrez peut-être placer les classements dans leur propre tableau, par exemple:

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

C) Ayez le nom du classement disponible dans vos informations auth.user.language

D) Écrivez votre SQL comme ceci:

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

ORDER BY GroupName COLLATE {#COLLATION}

E) Ensuite, vous pouvez le faire dans votre DAL:

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

Ce qui vous donnera alors cette requête SQL parfaitement composée

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
la source
Bonne réponse détaillée, merci beaucoup. Mais que pensez-vous des problèmes de classement dans la solution de la méthode 5. Il semble que ce ne soit pas le meilleur moyen lorsque vous avez besoin de trier ou de filtrer le texte traduit dans l'environnement multilingue avec différents classements. Et dans ce cas, la méthode 2 (que vous avez "ostracisée" si rapidement :)) pourrait être une meilleure option avec de légères modifications indiquant le classement cible pour chaque colonne localisée.
Eugene Evdokimov
2
@Eugene Evdokimov: Oui, mais "ORDER BY" sera toujours un problème, car vous ne pouvez pas le spécifier en tant que variable. Mon approche serait d'enregistrer le nom du classement dans la table des langues et de l'avoir dans le userinfo. Ensuite, sur chaque instruction SQL, vous pouvez dire ORDER BY COLUMN_NAME {#collation}, puis vous pouvez effectuer un remplacement dans votre dal (cmd.CommandText = cmd.CommandText.Replace ("{# COLLATION}", auth.user. language.collation). Vous pouvez également trier le code de votre application, par exemple à l'aide de LINQ. Cela réduirait également la charge de traitement de votre base de données. Pour les rapports, le rapport trie quand même.
Stefan Steiger
oo Ce doit être la réponse SO la plus longue que j'ai vue, et j'ai vu des gens faire des programmes entiers dans les réponses. Vous êtes doué.
Domino
Je suis tout à fait d'accord pour dire que la solution de SunWuKung est la meilleure
Domi
48

La troisième option est la meilleure, pour plusieurs raisons:

  • Ne nécessite pas de modifier le schéma de la base de données pour les nouvelles langues (et donc de limiter les modifications de code)
  • Ne nécessite pas beaucoup d'espace pour les langues non implémentées ou les traductions d'un élément particulier
  • Fournit le plus de flexibilité
  • Vous ne vous retrouvez pas avec des tables clairsemées
  • Vous n'avez pas à vous soucier des clés nulles et de vérifier que vous affichez une traduction existante au lieu d'une entrée nulle.
  • Si vous modifiez ou développez votre base de données pour englober d'autres éléments / choses / etc traduisibles, vous pouvez utiliser les mêmes tables et le même système - cela est très dissocié du reste des données.

-Adam

Adam Davis
la source
1
Je suis d'accord, bien que personnellement, j'aurais une table localisée pour chaque table principale, afin de permettre l'implémentation des clés étrangères.
Neil Barnwell
1
Bien que la troisième option soit la mise en œuvre la plus claire et la plus saine du problème, elle est plus complexe que la première. Je pense que l'affichage, l'édition et le rapport de la version générale nécessitent tellement d'efforts supplémentaires qu'elle n'est pas toujours acceptable. J'ai implémenté les deux solutions, la plus simple était suffisante lorsque les utilisateurs avaient besoin d'une traduction en lecture seule (parfois manquante) de la langue de l'application "principale".
rics
12
Que faire si la table des produits contient plusieurs champs traduits? Lors de la récupération des produits, vous devrez effectuer une jointure supplémentaire par champ traduit, ce qui entraînera de graves problèmes de performances. Il existe également (IMO) une complexité supplémentaire pour l'insertion / la mise à jour / la suppression. Le seul avantage de ceci est le plus petit nombre de tables. Je choisirais la méthode proposée par SunWuKung: je pense que c'est un bon équilibre entre les performances, la complexité et les problèmes de maintenance.
Frosty Z
@ rics- Je suis d'accord, eh bien, que proposez-vous à ...?
sabre
@ Adam- Je suis confus, j'ai peut-être mal compris. Vous avez suggéré le troisième, non? Veuillez expliquer plus en détail comment vont se passer les relations entre ces tables? Vous voulez dire que nous devons implémenter les tables Translation et TranslationEntry pour chaque table de la base de données?
sabre
9

Jetez un œil à cet exemple:

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

Je pense qu'il n'y a pas besoin d'expliquer, la structure se décrit.

bamburik
la source
c'est bon. mais comment rechercheriez-vous (par exemple nom_produit)?
Illuminati
Aviez-vous un exemple en direct quelque part de votre échantillon? Avez-vous rencontré des problèmes en l'utilisant?
David Létourneau
Bien sûr, j'ai un projet immobilier multilingue, nous supportons 4 langues. La recherche est un peu compliquée, mais c'est rapide. Bien sûr, dans les grands projets, cela peut être plus lent que nécessaire. Dans les petits ou moyens projets, c'est ok.
bamburik
8

Je préfère généralement cette approche (pas le sql réel), cela correspond à votre dernière option.

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'

Parce que le fait d'avoir tous les textes traduisibles au même endroit facilite la maintenance. Parfois, les traductions sont sous-traitées à des bureaux de traduction, de cette façon, vous pouvez leur envoyer un seul gros fichier d'exportation et les réimporter tout aussi facilement.

user39603
la source
1
À quoi sert le Translationtableau ou la TranslationItem.translationitemidcolonne?
DanMan
4

Avant de passer aux détails techniques et aux solutions, vous devez vous arrêter une minute et poser quelques questions sur les exigences. Les réponses peuvent avoir un impact énorme sur la solution technique. Voici des exemples de telles questions:
- Toutes les langues seront-elles utilisées tout le temps?
- Qui et quand remplira les colonnes avec les différentes versions linguistiques?
- Que se passe-t-il lorsqu'un utilisateur aura besoin d'une certaine langue de texte et qu'il n'y en a pas dans le système?
- Seuls les textes doivent être localisés ou il y a aussi d'autres articles (par exemple le PRIX peut être stocké en $ et € car ils peuvent être différents)

Aleris
la source
Je sais que la localisation est un sujet beaucoup plus large et je suis conscient des problèmes que vous portez à mon attention, mais actuellement je cherche une réponse à un problème très spécifique de conception de schéma. Je suppose que de nouvelles langues seront ajoutées progressivement et chacune sera traduite presque complètement.
qbeuek
3

Je cherchais quelques conseils pour la localisation et j'ai trouvé ce sujet. Je me demandais pourquoi cela est utilisé:

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

Vous obtenez donc quelque chose comme user39603 suggère:

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'

Ne pouvez-vous pas simplement laisser la traduction de la table pour que vous obteniez ceci:

    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'
randomiseur
la source
1
Sûr. J'appellerais la ProductItemtable quelque chose comme ProductTextsou ProductL10nbien. A plus de sens.
DanMan
1

Je suis d'accord avec randomizer. Je ne vois pas pourquoi vous avez besoin d'une table "traduction".

Je pense que cela suffit:

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

L'approche ci-dessous serait-elle viable? Supposons que vous ayez des tableaux où plus d'une colonne doit être traduite. Ainsi, pour le produit, vous pouvez avoir à la fois le nom du produit et la description du produit qui doivent être traduits. Pourriez-vous faire ce qui suit:

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
la source
0

"Lequel est le meilleur" est basé sur la situation du projet. Le premier est facile à sélectionner et à maintenir, et les performances sont également meilleures car il n'a pas besoin de joindre des tables lors de la sélection d'une entité. Si vous avez confirmé que votre projet ne prend en charge que 2 ou 3 langues et qu'il n'augmentera pas, vous pouvez l'utiliser.

Le second est correct mais est difficile à comprendre et à maintenir. Et la performance est pire que la première.

Le dernier est bon en termes d'évolutivité mais mauvais en performances. La table T_TRANSLATION_ENTRY deviendra de plus en plus grande, c'est terrible lorsque vous voulez récupérer une liste d'entités de certaines tables.

étudier
la source
0

Ce document décrit les solutions possibles et les avantages et inconvénients de chaque méthode. Je préfère la "localisation en ligne" car vous n'avez pas à modifier le schéma de base de données lors de l'ajout d'une nouvelle langue.

Jaska
la source