web-dev-qa-db-fra.com

Schéma pour une base de données multilingue

Je développe un logiciel multilingue. En ce qui concerne le code de l'application, la possibilité de localisation ne pose pas de problème. Nous pouvons utiliser des ressources spécifiques à une langue 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 que chaque table peut avoir plusieurs colonnes pouvant être localisées (la plupart des colonnes de nvarchar doivent être localisables). Par exemple, l'une des tables 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 NAME 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. Table de traduction avec des colonnes pour chaque langue

    Au lieu de stocker le texte traduit, seule une clé étrangère de la table de traduction est stockée. La table 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. Tables de traduction avec des lignes pour chaque langue

    Au lieu de stocker le texte traduit, seule une clé étrangère de la table de traduction est stockée. La table de traductions contient uniquement une clé et une table séparée 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)
    )
    

Chaque solution présente des avantages et des inconvénients, et j'aimerais savoir quelles sont vos expériences avec ces approches, que recommandez-vous et comment procéderiez-vous pour concevoir un schéma de base de données multilingue.

221
qbeuek

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

CREATE TABLE T_PRODUCT (pr_id int, PRICE NUMBER (18, 2))

CREATE TABLE T_PRODUCT_tr (pr_id INT FK, code de langue varchar, texte pr_name, pr_descr text)

Ainsi, si vous avez plusieurs colonnes traduisibles, il ne vous faudra plus qu'une seule jointure pour l'obtenir car, étant donné que vous ne générez pas automatiquement un ID de traduction, il sera peut-être plus facile d'importer des éléments avec leurs traductions associées.

L'inconvénient est que, si vous utilisez un mécanisme de secours de langage complexe, vous devrez peut-être l'implémenter pour chaque table de traduction, si vous vous en remettez à une procédure stockée. Si vous le faites depuis l'application, ce ne sera probablement pas un problème.

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

109
SunWuKung

C'est un problème intéressant, alors faisons de la nécromance.

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

SELECT ['DESCRIPTION_' + @in_language]  FROM T_Products

Donc vous devez 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 à l'utilisation de "SQL dynamique", vous n'avez donc pas à modifier toutes vos requêtes.

Cela aboutit généralement à quelque chose comme ceci (et cela ne peut pas être utilisé dans les vues ou les fonctions à valeur de table, ce qui pose un problème si vous devez réellement 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. Vous rencontrez donc un problème si vous ne saisissez pas au format ISO (ce que le programmeur classique ne fait généralement pas, et en cas de un rapport que l'utilisateur ne fera certainement pas pour vous, même si cela est explicitement demandé à l'utilisateur).
et
b) le plus significatif , vous perdez tout type de vérification de la syntaxe . Si <insert name of your "favourite" person here> modifie le schéma car, soudainement, les exigences en matière de modification des ailes et une nouvelle table sont créées, l’ancienne gauche mais le champ de référence renommé, vous ne recevez aucun type d’avertissement. Un rapport fonctionne même lorsque vous l'exécutez sans sélectionner le paramètre Wing (==> guid.empty). Mais tout à coup, lorsqu'un utilisateur sélectionne réellement une aile ==> boum . Cette méthode casse complètement tout type de test.


Méthode 2:
En résumé: Idée "géniale" (avertissement - sarcasme), combinons les inconvénients de la méthode 3 (vitesse lente lorsque plusieurs entrées) et les inconvénients plutôt horribles de la méthode 1.
Le seul avantage de cette méthode est que vous conservez toutes les traductions dans un seul tableau, ce qui simplifie la maintenance. Cependant, la même chose peut être obtenue avec la méthode 1, une procédure stockée SQL dynamique et une table (éventuellement temporaire) contenant les traductions, ainsi que le nom de la table cible (il est très simple de supposer que vous avez nommé tous vos champs de texte 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 à traduire. Par conséquent, vous devez faire n jointure pour n champs. Lorsque la table de traduction est globale, elle comporte de nombreuses entrées et les jointures deviennent lentes. En outre, vous devez toujours joindre la table T_TRANSLATION n fois pour n champs. Ceci est tout à fait une surcharge. Maintenant, que faites-vous lorsque vous devez adapter des traductions personnalisées par client? Vous devrez ajouter 2x n autres 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 tables. Si je change le nom de l'élément dans une table, est-ce que je veux vraiment changer une entrée dans une autre table? 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 la (les) table (s) de produit ... vous pouvez bien sûr omettre de régler les FK, puis <insert name of your "favourite" person here> peut supprimer la table et réinsérer toutes les entrées avec newid () [ou en spécifiant l’id dans l’insert, mais avec identity-insert OFF ], et cela mènerait (et conduira) à des données-garbage (et à des exceptions null-reference) très bientôt.


-- 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 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/... avec '.../' + @in_language + '/...'

Un peu comme le PostStore hstore, sauf qu'en raison de la surcharge de l'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.


Faisons un exemple pour voir cette œuvres:

Tout d'abord, créez les tables:

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

Puis remplissez 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 ensuite interroger 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 ISO-TwoLetterName ('DE', 'EN', etc.) comme clé primaire de la table des langues, vous n'avez pas besoin de rechercher l'ID de langue. Mais si vous le faites, vous voudrez peut-être utiliser le balise IETF-language , ce qui est préférable, car vous obtenez de-CH et de-DE, ce qui n’est vraiment pas la même chose du point de vue de l’orthographie ( double s au lieu de ß partout), bien que ce soit la même langue de base. Cela n’est qu’un petit détail qui pourrait vous intéresser, en particulier si vous considérez qu’en-US et en-GB/en-CA/en-AU ou fr-FR/fr-CA ont des problèmes similaires.
Citation: nous n'en avons pas besoin, nous n'utilisons 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 ultérieurement.
Et vous devriez utiliser cet entier, car il n’ya rien de plus ennuyant, de destructeur et de gênant qu’un design Db bâclé.

Voir aussi RFC 5646 , ISO 639-2 ,

Et, si vous dites toujours "nous" seulement faites notre application pour "seulement une culture" (comme en-US généralement) - donc je n'ai pas besoin de cet entier supplémentaire, ce serait un bon moment pour mentionner le balises 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 de portails juridiques et de services publics.
Plus important encore, il existe des régions qui passent de l'alphabet latin au cyrillique, ce qui risque de poser plus de problèmes que la nuisance superficielle d'une obscure réforme de l'orthographe, ce qui explique pourquoi il pays dans lequel vous vivez. D'une manière ou d'une autre, il est préférable d'avoir cet entier, juste au cas où ...

Modifier:
Et en ajoutant ON DELETE CASCADE après

REFERENCES dbo.T_Products( PROD_Id )

vous pouvez simplement dire: DELETE FROM T_Products, et n'obtenez aucune violation de clé étrangère.

En ce qui concerne la collation, je le ferais comme ceci:

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

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

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

C) Avoir le nom de classement disponible dans vos informations auth.user.language

D) Ecrivez 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
49
Stefan Steiger

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

  • Ne nécessite pas de modifier le schéma de base de données pour les nouvelles langues (et donc limiter les changements 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 à 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/éléments pouvant être traduits, etc., vous pouvez utiliser les mêmes tables et le même système, ce qui est très dissocié du reste des données.

-Adam

47
Adam Davis

Jetez un oeil à 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 elle-même.

9
bamburik

Je choisirais 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 tous les textes traduisibles au même endroit rendent la maintenance beaucoup plus facile. Parfois, les traductions sont sous-traitées à des bureaux de traduction. Vous pouvez ainsi leur envoyer un gros fichier d'exportation et l'importer aussi facilement.

8
user39603

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

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

Donc, vous obtenez 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 table Traduction afin 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'
3
randomizer

Avant de passer aux détails techniques et aux solutions, arrêtez-vous une minute et posez quelques questions sur les exigences. Les réponses peuvent avoir un impact énorme sur la solution technique. Des exemples de telles questions seraient:
- 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 a 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 éléments (par exemple, PRIX peut être stocké en $ et en € car ils peuvent être différents)

3
Aleris

L'approche ci-dessous serait-elle viable? Supposons que vous ayez des tables où plus d'une colonne doit être traduite. Donc, pour le produit, vous pouvez avoir à la fois le nom du produit et la description du produit qui doit être traduit. 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)
    )   
1
davey

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
1
Bart VW

"Lequel est le meilleur" est basé sur la situation du projet. Le premier est facile à sélectionner et à gérer, et les performances sont meilleures car il n’est pas nécessaire de joindre des tables lorsqu’on sélectionne une entité. Si vous confirmez que votre projet ne prend en charge que 2 ou 3 langues et qu'il n'augmentera pas, vous pourrez l'utiliser.

Le second est correct mais difficile à comprendre et à maintenir. Et la performance est pire que le premier.

Le dernier est bon pour l'évolutivité mais mauvais pour la performance. La table T_TRANSLATION_ENTRY deviendra de plus en plus grande. C'est terrible de vouloir récupérer une liste d'entités à partir de certaines tables.

0
studyzy