Schema per un database multilingue


235

Sto sviluppando un software multilingue. Per quanto riguarda il codice dell'applicazione, la localizzazione non è un problema. Siamo in grado di utilizzare risorse specifiche della lingua e avere tutti i tipi di strumenti che funzionano bene con loro.

Ma qual è l'approccio migliore nella definizione di uno schema di database multilingue? Diciamo che abbiamo molte tabelle (100 o più) e ogni tabella può avere più colonne che possono essere localizzate (la maggior parte delle colonne nvarchar dovrebbe essere localizzabile). Ad esempio, una delle tabelle potrebbe contenere informazioni sul prodotto:

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

Mi vengono in mente tre approcci per supportare il testo multilingue nelle colonne NOME e DESCRIZIONE:

  1. Colonna separata per ogni lingua

    Quando aggiungiamo una nuova lingua al sistema, dobbiamo creare colonne aggiuntive per memorizzare il testo tradotto, in questo modo:

    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. Tabella di traduzione con colonne per ogni lingua

    Invece di memorizzare il testo tradotto, viene memorizzata solo una chiave esterna per la tabella delle traduzioni. La tabella delle traduzioni contiene una colonna per ogni lingua.

    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. Tabelle di traduzione con righe per ogni lingua

    Invece di memorizzare il testo tradotto, viene memorizzata solo una chiave esterna per la tabella delle traduzioni. La tabella delle traduzioni contiene solo una chiave e una tabella separata contiene una riga per ogni traduzione in una lingua.

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

Ci sono pro e contro in ogni soluzione, e vorrei sapere quali sono le tue esperienze con questi approcci, cosa consigli e come potresti progettare uno schema di database multilingue.



3
Puoi controllare questo link: gsdesign.ro/blog/multilanguage-database-design-approach sebbene leggere i commenti sia molto utile
Fareed Alnamrouti,

3
LANGUAGE_CODEsono la chiave naturale, evitare LANGUAGE_ID.
gavenkoa,

1
Ho già visto / usato il 2. e 3., non li consiglio, si finisce facilmente con file orfane. Il design di @SunWiKung sembra IMO migliore.
Guillaume86,

4
Preferisco il design di SunWuKungs, che per coincidenza è ciò che abbiamo implementato. Tuttavia, è necessario considerare le regole di confronto. Almeno in SQL Server, ogni colonna ha una proprietà di confronto, che determina elementi come la distinzione tra maiuscole e minuscole, l'equivalenza (o meno) dei caratteri accentati e altre considerazioni specifiche della lingua. Il fatto che tu utilizzi o meno regole di confronto specifiche per la lingua dipende dal design complessivo dell'applicazione, ma se sbagli, sarà difficile cambiare in seguito. Se hai bisogno di regole di confronto specifiche per la lingua, avrai bisogno di una colonna per lingua, non di una riga per lingua.
Elroy Flynn

Risposte:


113

Cosa pensi di avere una tabella di traduzione correlata per ogni tabella traducibile?

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)

In questo modo, se si dispone di più colonne traducibili, per ottenerlo è necessario un solo join + poiché non si sta generando automaticamente un traduttore, potrebbe essere più semplice importare elementi insieme alle relative traduzioni.

Il lato negativo di questo è che se si dispone di un meccanismo di fallback del linguaggio complesso, potrebbe essere necessario implementarlo per ogni tabella di traduzione, se si sta facendo affidamento su una procedura memorizzata per farlo. Se lo fai dall'app, questo probabilmente non sarà un problema.

Fammi sapere cosa ne pensi - Sto anche per prendere una decisione su questo per la nostra prossima applicazione. Finora abbiamo usato il tuo terzo tipo.


2
Questa opzione è simile alla mia opzione n. 1 ma migliore. È ancora difficile da mantenere e richiede la creazione di nuove tabelle per nuove lingue, quindi sarei riluttante a implementarlo.
qbeuek,

28
non richiede una nuova tabella per una nuova lingua - aggiungi semplicemente una nuova riga alla tabella _tr appropriata con la tua nuova lingua, devi solo creare una nuova tabella _tr se crei una nuova tabella traducibile

3
credo che questo sia un buon metodo. altri metodi richiedono tonnellate di join di sinistra e quando si uniscono più tabelle che ognuna di esse ha una traduzione di 3 livelli di profondità, e ognuna ha 3 campi sono necessari 3 * 3 9 di join solo per le traduzioni .. altro saggio 3. Inoltre è più facile aggiungere vincoli ecc. e credo che la ricerca sia più accettabile.
GorillaApe

1
Quando T_PRODUCTha 1 milione di righe, ne T_PRODUCT_travrebbe 2 milioni. Ridurrebbe molto l'efficienza di sql?
Mithril,

1
@Mithril In entrambi i casi hai 2 milioni di righe. Almeno non è necessario unirsi a questo metodo.
David D,

56

Questo è un problema interessante, quindi andiamo a negrance.

Cominciamo dai problemi del metodo 1:
Problema: stai denormalizzando per risparmiare velocità.
In SQL (tranne PostGreSQL con hstore), non puoi passare un linguaggio di parametri e dire:

SELECT ['DESCRIPTION_' + @in_language]  FROM T_Products

Quindi devi fare questo:

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 

Ciò significa che devi modificare TUTTE le tue query se aggiungi una nuova lingua. Ciò porta naturalmente all'utilizzo di "SQL dinamico", quindi non è necessario modificare tutte le query.

Questo di solito si traduce in qualcosa del genere (e non può essere utilizzato nelle viste o nelle funzioni con valori di tabella a proposito, il che è davvero un problema se devi effettivamente filtrare la data del rapporto)

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

Il problema con questo è
a) La formattazione della data è molto specifica della lingua, quindi c'è un problema lì, se non si inserisce in formato ISO (cosa che di solito non fa il programmatore medio di varietà da giardino, e in caso di un rapporto che l'utente sicuramente non farà per te, anche se esplicitamente incaricato di farlo).
e
b) soprattutto , perdi qualsiasi tipo di controllo della sintassi . Se <insert name of your "favourite" person here>altera lo schema perché improvvisamente i requisiti per l'ala cambiano e viene creata una nuova tabella, quella vecchia è rimasta ma il campo di riferimento è stato rinominato, non si riceve alcun tipo di avviso. Un report funziona anche quando lo si esegue senza selezionare il parametro wing (==> guid.empty). Ma improvvisamente, quando un utente reale seleziona effettivamente un'ala ==> boom . Questo metodo interrompe completamente qualsiasi tipo di test.


Metodo 2:
In poche parole: "Ottima" idea (avvertenza - sarcasmo), combiniamo gli svantaggi del metodo 3 (bassa velocità quando molte voci) con gli svantaggi piuttosto orribili del metodo 1.
L'unico vantaggio di questo metodo è che si mantiene tutte le traduzioni in una tabella e quindi semplificano la manutenzione. Tuttavia, la stessa cosa può essere raggiunta con il metodo 1 e una stored procedure SQL dinamica e una tabella (possibilmente temporanea) contenente le traduzioni e il nome della tabella di destinazione (ed è abbastanza semplice supponendo che tu abbia chiamato tutti i tuoi campi di testo il stesso).


Metodo 3:
una tabella per tutte le traduzioni: Svantaggio: è necessario memorizzare n chiavi esterne nella tabella dei prodotti per n campi che si desidera tradurre. Pertanto, devi fare n join per n campi. Quando la tabella di traduzione è globale, ha molte voci e i join diventano lenti. Inoltre, devi sempre unirti alla tabella T_TRANSLATION n volte per n campi. Questo è piuttosto un sovraccarico. Ora, cosa fai quando devi inserire traduzioni personalizzate per cliente? Dovrai aggiungere altri 2x n join su una tabella aggiuntiva. Se devi unirti, diciamo 10 tavoli, con 2x2xn = 4n join aggiuntivi, che casino! Inoltre, questo design consente di utilizzare la stessa traduzione con 2 tabelle. Se cambio il nome dell'articolo in una tabella, voglio davvero cambiare anche una voce in un'altra tabella OGNI SINGOLO?

Inoltre non puoi più cancellare e reinserire la tabella, perché ora ci sono chiavi esterne nella TABELLA (E) DEL PRODOTTO ... puoi ovviamente omettere di impostare gli FK, quindi <insert name of your "favourite" person here>puoi eliminare la tabella e reinserire tutte le voci con newid () [o specificando l'id nell'insert, ma con identità-insert OFF ], e ciò porterebbe (e porterebbe) a dati-garbage (e eccezioni riferimento null) molto presto.


Metodo 4 (non elencato): memorizzazione di tutte le lingue in un campo XML nel database. per esempio

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

Quindi è possibile ottenere il valore da XPath-Query in SQL, dove è possibile inserire la variabile stringa

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

E puoi aggiornare il valore in questo modo:

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 

Dove puoi sostituirlo /lang/de/...con'.../' + @in_language + '/...'

Un po 'come l'hstore di PostGre, tranne che a causa del sovraccarico di analizzare XML (invece di leggere una voce da un array associativo in PG hstore) diventa troppo lento più la codifica XML rende troppo doloroso essere utile.


Metodo 5 (come raccomandato da SunWuKung, quello che dovresti scegliere): una tabella di traduzione per ogni tabella "Prodotto". Ciò significa una riga per lingua e diversi campi "di testo", quindi richiede solo UN (a sinistra) join su N campi. Quindi puoi facilmente aggiungere un campo predefinito nella tabella "Prodotto", puoi eliminare e reinserire facilmente la tabella di traduzione e puoi creare una seconda tabella per traduzioni personalizzate (su richiesta), che puoi anche eliminare e reinserire) e hai ancora tutte le chiavi esterne.

Facciamo un esempio per vedere questo LAVORI:

Innanzitutto, crea le tabelle:

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

Quindi inserire i dati

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 quindi interrogare i dati:

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 sei pigro, puoi anche usare ISO-TwoLetterName ('DE', 'EN', ecc.) Come chiave primaria della tabella delle lingue, quindi non devi cercare l'ID della lingua. Ma se lo fai, potresti voler usare invece il tag del linguaggio IETF , il che è meglio, perché ottieni de-CH e de-DE, che non è davvero la stessa ortografia (doppie s invece di ß ovunque) , sebbene sia lo stesso linguaggio di base. Questo è solo un piccolo dettaglio che può essere importante per te, specialmente considerando che en-US / en-GB / en-CA / en-AU o fr-FR / fr-CA hanno problemi simili.
Citazione: non ne abbiamo bisogno, facciamo il nostro software solo in inglese.
Risposta: Sì, ma quale ??

Ad ogni modo, se usi un ID intero, sei flessibile e puoi cambiare metodo in qualsiasi momento successivo.
E dovresti usare quell'intero, perché non c'è niente di più fastidioso, distruttivo e problematico di un design Db pasticciato.

Vedi anche RFC 5646 , ISO 639-2 ,

E, se si sta ancora dicendo "noi" solo facciamo la nostra domanda di "solo una cultura" (come en-US di solito) - quindi non ho bisogno che interi in più, questo sarebbe un buon momento e il luogo per parlare del Tag di lingua IANA , no?
Perché vanno così:

de-DE-1901
de-DE-1996

e

de-CH-1901
de-CH-1996

(c'è stata una riforma dell'ortografia nel 1996 ...) Prova a trovare una parola in un dizionario se è scritta male; questo diventa molto importante nelle applicazioni che si occupano di portali legali e di servizio pubblico.
Ancora più importante, ci sono regioni che stanno cambiando da alfabeti cirillici a latini, che potrebbero essere solo più fastidiosi del fastidio superficiale di qualche oscura riforma dell'ortografia, motivo per cui questa potrebbe essere anche una considerazione importante, a seconda del paese in cui vivi. In un modo o nell'altro, è meglio avere quell'intero lì dentro, nel caso in cui ...

Modifica:
e aggiungendo ON DELETE CASCADE dopo

REFERENCES dbo.T_Products( PROD_Id )

puoi semplicemente dire: DELETE FROM T_Productse non ottenere alcuna violazione di chiave esterna.

Per quanto riguarda la collazione, lo farei così:

A) Avere il proprio DAL
B) Salvare il nome della fascicolazione desiderata nella tabella della lingua

Potresti voler mettere le regole di confronto nella loro tabella, ad esempio:

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

C) Avere il nome della collazione disponibile nelle informazioni auth.user.language

D) Scrivi il tuo SQL in questo modo:

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

ORDER BY GroupName COLLATE {#COLLATION}

E) Quindi, puoi farlo nel tuo DAL:

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

Che ti darà quindi questa query SQL perfettamente 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

Buona risposta dettagliata, molte grazie. Cosa ne pensi dei problemi di confronto nella soluzione Metodo 5. Sembra che questo non sia il modo migliore quando è necessario ordinare o filtrare il testo tradotto nell'ambiente multilingue con diverse regole di confronto. E in tal caso il Metodo 2 (che hai "ostracizzato" così rapidamente :)) potrebbe essere un'opzione migliore con lievi modifiche che indicano le regole di confronto del target per ogni colonna localizzata.
Eugene Evdokimov,

2
@Eugene Evdokimov: Sì, ma "ORDER BY" sarà sempre un problema, perché non è possibile specificarlo come variabile. Il mio approccio sarebbe quello di salvare il nome della collation nella tabella delle lingue e di averlo nel userinfo. Quindi, su ogni istruzione SQL puoi pronunciare ORDER BY COLUMN_NAME {#collation} e quindi puoi fare una sostituzione nel tuo dal (cmd.CommandText = cmd.CommandText.Replace ("{# COLLATION}", auth.user. language.collation) In alternativa, è possibile ordinare il codice dell'applicazione, ad esempio utilizzando LINQ. Ciò richiederebbe anche un carico di elaborazione dal database. Per i report, il report ordina comunque
Stefan Steiger,

oo Questa deve essere la risposta SO più lunga che abbia mai visto, e ho visto le persone fare interi programmi in risposte. Sei bravo.
Domino,

Sono assolutamente d'accordo che la soluzione di SunWuKung sia la migliore
Domi il

48

La terza opzione è la migliore, per alcuni motivi:

  • Non richiede la modifica dello schema del database per le nuove lingue (e quindi la limitazione delle modifiche al codice)
  • Non richiede molto spazio per le lingue non implementate o le traduzioni di un particolare elemento
  • Fornisce la massima flessibilità
  • Non finisci con le tabelle sparse
  • Non devi preoccuparti delle chiavi null e verificare che stai visualizzando una traduzione esistente anziché una voce null.
  • Se modifichi o espandi il tuo database per includere altri oggetti / cose traducibili / ecc. Puoi usare le stesse tabelle e lo stesso sistema - questo è molto disaccoppiato dal resto dei dati.

-Adamo


1
Sono d'accordo, anche se personalmente avrei una tabella localizzata per ogni tabella principale, per consentire l'implementazione di chiavi esterne.
Neil Barnwell,

1
Sebbene la terza opzione sia l'implementazione più pulita e solida del problema, è più complessa della prima. Penso che visualizzare, modificare, riportare la versione generale abbia bisogno di così tanti sforzi extra che non è sempre accettabile. Ho implementato entrambe le soluzioni, la più semplice era sufficiente quando gli utenti avevano bisogno di una traduzione di sola lettura (a volte mancante) della lingua dell'applicazione "principale".
rics

12
Cosa succede se la tabella dei prodotti contiene diversi campi tradotti? Quando si recuperano i prodotti, è necessario eseguire un ulteriore join per campo tradotto, con conseguenti gravi problemi di prestazioni. C'è anche (IMO) ulteriore complessità per inserire / aggiornare / eliminare. Il vantaggio unico di questo è il numero inferiore di tabelle. Vorrei scegliere il metodo proposto da SunWuKung: penso che sia un buon equilibrio tra prestazioni, complessità e problemi di manutenzione.
Frosty Z,

@ rics- Sono d'accordo, beh cosa suggerisci di ...?
sciabola

@Adamo- Sono confuso, forse ho frainteso. Hai suggerito il terzo, giusto? Per favore, spiegalo più in dettaglio come saranno le relazioni tra quei tavoli? Vuoi dire che dobbiamo implementare le tabelle Translation e TranslationEntry per ogni tabella nel DB?
sciabola

9

Dai un'occhiata a questo esempio:

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

Penso che non sia necessario spiegare, la struttura si descrive da sola.


questo è buono. ma come cercherai (ad esempio nome_prodotto)?
Illuminati,

Hai avuto un esempio live da qualche parte del tuo campione? Hai avuto problemi usando?
David Létourneau,

Certo, ho un progetto immobiliare multilingue, sosteniamo 4 lingue. La ricerca è un po 'complicata, ma è veloce. Ovviamente in progetti di grandi dimensioni potrebbe essere più lento del necessario. Nei progetti di piccole o medie dimensioni va bene.
bamburik,

8

Di solito preferirei questo approccio (non sql effettivo), questo corrisponde alla tua ultima opzione.

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'

Perché avere tutti i testi traducibili in un unico posto rende la manutenzione molto più semplice. A volte le traduzioni sono esternalizzate agli uffici di traduzione, in questo modo è possibile inviare loro solo un grosso file di esportazione e importarlo altrettanto facilmente.


1
A cosa serve la Translationtabella o la TranslationItem.translationitemidcolonna?
DanMan,

4

Prima di andare ai dettagli tecnici e alle soluzioni, dovresti fermarti un minuto e porre alcune domande sui requisiti. Le risposte possono avere un impatto enorme sulla soluzione tecnica. Esempi di tali domande sarebbero:
- Verranno utilizzate tutte le lingue in ogni momento?
- Chi e quando riempirà le colonne con le diverse versioni linguistiche?
- Cosa succede quando un utente avrà bisogno di una determinata lingua di un testo e non ce n'è nel sistema?
- Solo i testi devono essere localizzati o ci sono anche altri elementi (ad esempio PREZZO può essere memorizzato in $ e € perché potrebbero essere diversi)


So che la localizzazione è un argomento molto più ampio e sono consapevole dei problemi che mi porti alla mia attenzione, ma attualmente sto cercando una risposta per un problema molto specifico nella progettazione dello schema. Presumo che le nuove lingue verranno aggiunte in modo incrementale e ciascuna verrà tradotta quasi completamente.
qbeuek,

3

Stavo cercando alcuni suggerimenti per la localizzazione e ho trovato questo argomento. Mi chiedevo perché questo è usato:

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

Quindi ottieni qualcosa come suggerisce user39603:

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'

Non puoi semplicemente lasciare la traduzione del tavolo in modo da ottenere questo:

    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
Sicuro. Chiamerei il ProductItemtavolo qualcosa di simile ProductTextso ProductL10ncomunque. Ha più senso.
DanMan,

1

Sono d'accordo con randomizer. Non vedo perché hai bisogno di una "traduzione" da tavolo.

Penso che sia abbastanza:

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

1

L'approccio di seguito sarebbe praticabile? Supponi di avere tabelle in cui è necessario tradurre più di 1 colonna. Quindi per il prodotto potresti avere sia il nome del prodotto che la descrizione del prodotto che devono essere tradotti. Potresti fare quanto segue:

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 è il migliore" si basa sulla situazione del progetto. Il primo è facile da selezionare e mantenere, e anche le prestazioni sono le migliori poiché non è necessario unire le tabelle quando si seleziona un'entità. Se hai confermato che il tuo poject supporta solo 2 o 3 lingue e non aumenterà, puoi usarlo.

Il secondo è okey ma è difficile da capire e mantenere. E la performance è peggiore della prima.

L'ultimo è buono per la scalabilità ma cattivo per le prestazioni. La tabella T_TRANSLATION_ENTRY diventerà sempre più grande, è terribile quando si desidera recuperare un elenco di entità da alcune tabelle.


0

Questo documento descrive le possibili soluzioni e i vantaggi e gli svantaggi di ciascun metodo. Preferisco la "localizzazione di righe" perché non è necessario modificare lo schema DB quando si aggiunge una nuova lingua.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.