Skema untuk basis data multi bahasa

235

Saya sedang mengembangkan perangkat lunak multi bahasa. Sejauh kode aplikasi berjalan, localizability tidak menjadi masalah. Kami dapat menggunakan sumber daya khusus bahasa dan memiliki semua jenis alat yang bekerja dengan baik dengannya.

Tapi apa pendekatan terbaik dalam mendefinisikan skema basis data multi-bahasa? Katakanlah kita memiliki banyak tabel (100 atau lebih), dan setiap tabel dapat memiliki beberapa kolom yang dapat dilokalkan (sebagian besar kolom nvarchar harus dilokalisasi). Misalnya salah satu tabel mungkin berisi informasi produk:

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

Saya dapat memikirkan tiga pendekatan untuk mendukung teks multibahasa dalam kolom NAME dan DESCRIPTION:

  1. Pisahkan kolom untuk setiap bahasa

    Ketika kami menambahkan bahasa baru ke sistem, kami harus membuat kolom tambahan untuk menyimpan teks yang diterjemahkan, seperti ini:

    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. Tabel terjemahan dengan kolom untuk setiap bahasa

    Alih-alih menyimpan teks yang diterjemahkan, hanya kunci asing ke tabel terjemahan yang disimpan. Tabel terjemahan berisi kolom untuk setiap bahasa.

    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. Tabel terjemahan dengan baris untuk setiap bahasa

    Alih-alih menyimpan teks yang diterjemahkan, hanya kunci asing ke tabel terjemahan yang disimpan. Tabel terjemahan hanya berisi kunci, dan tabel terpisah berisi baris untuk setiap terjemahan ke bahasa.

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

Ada pro dan kontra untuk setiap solusi, dan saya ingin tahu apa pengalaman Anda dengan pendekatan ini, apa yang Anda rekomendasikan dan bagaimana Anda akan merancang skema database multi-bahasa.

qbeuek
sumber
3
Anda dapat memeriksa tautan ini: gsdesign.ro/blog/multilanguage-database-design-approach meskipun membaca komentar sangat membantu
Fareed Alnamrouti
3
LANGUAGE_CODEadalah kunci alami, hindari LANGUAGE_ID.
gavenkoa
1
Saya sudah melihat / menggunakan 2. dan 3., saya tidak merekomendasikan mereka, Anda dengan mudah berakhir dengan baris yatim. Desain @SunWiKung IMO terlihat lebih baik.
Guillaume86
4
Saya lebih suka desain SunWuKungs, yang kebetulan adalah apa yang telah kami terapkan. Namun, Anda perlu mempertimbangkan pengumpulan. Di Sql Server setidaknya, setiap kolom memiliki properti pemeriksaan, yang menentukan hal-hal seperti sensitivitas huruf, kesetaraan (atau tidak) karakter beraksen, dan pertimbangan khusus bahasa lainnya. Apakah Anda menggunakan kumpulan bahasa khusus atau tidak tergantung pada desain aplikasi keseluruhan Anda, tetapi jika Anda salah, itu akan sulit untuk diubah nanti. Jika Anda membutuhkan kumpulan bahasa khusus, maka Anda akan membutuhkan kolom per bahasa, bukan baris per bahasa.
Elroy Flynn

Jawaban:

113

Apa yang Anda pikirkan tentang memiliki tabel terjemahan terkait untuk setiap tabel yang dapat diterjemahkan?

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

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

Dengan cara ini jika Anda memiliki beberapa kolom yang dapat diterjemahkan, maka hanya akan memerlukan satu gabungan untuk mendapatkannya + karena Anda tidak membuat autogenerasi sebuah terjemahanid mungkin akan lebih mudah untuk mengimpor item bersama dengan terjemahan terkait mereka.

Sisi negatifnya adalah jika Anda memiliki mekanisme fallback bahasa yang kompleks, Anda mungkin perlu menerapkannya untuk setiap tabel terjemahan - jika Anda mengandalkan beberapa prosedur tersimpan untuk melakukan itu. Jika Anda melakukannya dari aplikasi, ini mungkin tidak akan menjadi masalah.

Biarkan saya tahu apa yang Anda pikirkan - Saya juga akan membuat keputusan tentang ini untuk aplikasi kita selanjutnya. Sejauh ini kami telah menggunakan tipe ke-3 Anda.

Komunitas
sumber
2
Opsi ini mirip dengan opsi saya nr 1 tetapi lebih baik. Masih sulit untuk mempertahankan dan membutuhkan membuat tabel baru untuk bahasa baru, jadi saya akan enggan mengimplementasikannya.
qbeuek
28
tidak memerlukan tabel baru untuk bahasa baru - Anda cukup menambahkan baris baru ke tabel _tr yang sesuai dengan bahasa baru Anda, Anda hanya perlu membuat tabel _tr baru jika Anda membuat tabel terjemahan yang baru
3
Saya percaya bahwa ini adalah metode yang baik. metode lain memerlukan banyak gabungan kiri dan ketika Anda bergabung dengan beberapa tabel yang masing-masing dari mereka memiliki terjemahan seperti 3 level, dan masing-masing memiliki 3 bidang Anda perlu 3 * 3 9 bergabung kiri hanya untuk terjemahan .. selain bijaksana 3. Juga lebih mudah untuk menambahkan kendala dll dan saya percaya pencarian lebih beresonansi.
GorillaApe
1
Kapan T_PRODUCTmemiliki 1 juta baris, T_PRODUCT_trakan memiliki 2 juta. Apakah akan mengurangi efisiensi sql banyak?
Mithril
1
@Mithril Either way Anda memiliki 2 juta baris. Setidaknya Anda tidak perlu bergabung dengan metode ini.
David D
56

Ini adalah masalah yang menarik, jadi mari kita necromance.

Mari kita mulai dengan masalah metode 1:
Masalah: Anda melakukan denormalisasi untuk menghemat kecepatan.
Dalam SQL (kecuali PostGreSQL dengan hstore), Anda tidak dapat melewati bahasa parameter, dan mengatakan:

SELECT ['DESCRIPTION_' + @in_language]  FROM T_Products

Jadi, Anda harus melakukan ini:

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 

Yang berarti Anda harus mengubah SEMUA kueri Anda jika Anda menambahkan bahasa baru. Ini secara alami mengarah ke menggunakan "SQL dinamis", jadi Anda tidak perlu mengubah semua pertanyaan Anda.

Ini biasanya menghasilkan sesuatu seperti ini (dan itu tidak dapat digunakan dalam tampilan atau fungsi bernilai tabel dengan cara, yang benar-benar masalah jika Anda benar-benar perlu memfilter tanggal pelaporan)

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

Masalahnya adalah
a) Pemformatan tanggal sangat spesifik-bahasa, sehingga Anda mendapatkan masalah di sana, jika Anda tidak memasukkan dalam format ISO (yang biasanya tidak dilakukan oleh pemrogram varietas kebun, dan dalam kasus lapor pengguna yakin sekali tidak akan melakukan untuk Anda, bahkan jika secara eksplisit diinstruksikan untuk melakukannya).
dan
b) yang paling penting , Anda kehilangan segala jenis pemeriksaan sintaksis . Jika <insert name of your "favourite" person here>mengubah skema karena tiba-tiba persyaratan untuk perubahan sayap, dan tabel baru dibuat, yang lama tersisa tetapi bidang referensi diganti namanya, Anda tidak mendapatkan peringatan apa pun. Laporan bahkan berfungsi ketika Anda menjalankannya tanpa memilih parameter sayap (==> guid.empty). Tapi tiba-tiba, ketika pengguna yang sebenarnya benar-benar memilih sayap ==>booming . Metode ini benar-benar memecah segala jenis pengujian.


Metode 2:
Singkatnya: "Hebat" ide (peringatan - sarkasme), mari kita gabungkan kelemahan metode 3 (kecepatan lambat ketika banyak entri) dengan kerugian metode yang agak mengerikan 1.
Satu-satunya keuntungan dari metode ini adalah Anda tetap menggunakan semua terjemahan dalam satu tabel, dan karenanya mempermudah pemeliharaan. Namun, hal yang sama dapat dicapai dengan metode 1 dan prosedur tersimpan SQL dinamis, dan tabel (mungkin sementara) yang berisi terjemahan, dan nama tabel target (dan cukup sederhana dengan asumsi Anda memberi nama semua bidang teks Anda sama).


Metode 3:
Satu tabel untuk semua terjemahan: Kerugian: Anda harus menyimpan dan Kunci Asing di tabel produk untuk bidang yang ingin Anda terjemahkan. Oleh karena itu, Anda harus melakukan n bergabung untuk bidang n. Ketika tabel terjemahan bersifat global, ia memiliki banyak entri, dan bergabung menjadi lambat. Selain itu, Anda selalu harus bergabung dengan tabel T_TRANSLATION n kali untuk n bidang. Ini cukup mahal. Sekarang, apa yang Anda lakukan ketika Anda harus mengakomodasi terjemahan khusus per pelanggan? Anda harus menambahkan 2 x n bergabung ke tabel tambahan. Jika Anda harus bergabung, katakan 10 tabel, dengan 2x2xn = 4n tambahan bergabung, sungguh berantakan! Selain itu, desain ini memungkinkan untuk menggunakan terjemahan yang sama dengan 2 tabel. Jika saya mengubah nama item dalam satu tabel, apakah saya benar-benar ingin mengubah entri di tabel lain juga SETIAP WAKTU TUNGGAL?

Plus, Anda tidak dapat menghapus dan menyisipkan lagi tabel, karena sekarang ada kunci asing di TABEL PRODUK ... Anda tentu saja dapat menghilangkan pengaturan FK, dan kemudian <insert name of your "favourite" person here>dapat menghapus tabel, dan memasukkan kembali semua entri dengan newid () [atau dengan menentukan id di sisipan, tetapi memiliki identitas-sisipkan OFF ], dan itu akan (dan akan) menyebabkan data-sampah (dan pengecualian referensi-nol) sangat cepat.


Metode 4 (tidak terdaftar): Menyimpan semua bahasa dalam bidang XML dalam database. misalnya

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

Kemudian Anda bisa mendapatkan nilai dengan XPath-Query di SQL, di mana Anda bisa memasukkan variabel-string

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

Dan Anda dapat memperbarui nilai seperti ini:

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 

Di mana Anda dapat menggantinya /lang/de/...dengan'.../' + @in_language + '/...'

Jenis seperti PostGre hstore, kecuali bahwa karena overhead parsing XML (alih-alih membaca entri dari array asosiatif di PG hstore) itu menjadi terlalu lambat ditambah pengkodean xml membuatnya terlalu menyakitkan untuk berguna.


Metode 5 (seperti yang direkomendasikan oleh SunWuKung, yang harus Anda pilih): Satu tabel terjemahan untuk setiap tabel "Produk". Itu berarti satu baris per bahasa, dan beberapa bidang "teks", sehingga hanya membutuhkan SATU (kiri) bergabung di bidang N. Kemudian Anda dapat dengan mudah menambahkan bidang default di tabel "Produk", Anda dapat dengan mudah menghapus dan memasukkan kembali tabel terjemahan, dan Anda dapat membuat tabel kedua untuk terjemahan khusus (sesuai permintaan), yang juga dapat Anda hapus dan masukkan kembali), dan Anda masih memiliki semua kunci asing.

Mari kita buat contoh untuk melihat KARYA ini:

Pertama, buat tabel:

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

Lalu isi data

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

Dan kemudian meminta data:

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

Jika Anda malas, maka Anda juga dapat menggunakan ISO-TwoLetterName ('DE', 'EN', dll.) Sebagai kunci utama tabel bahasa, maka Anda tidak perlu mencari id bahasa. Tetapi jika Anda melakukannya, Anda mungkin ingin menggunakan tag bahasa IETF sebagai gantinya, yang lebih baik, karena Anda mendapatkan de-CH dan de-DE, yang benar-benar tidak sama ortografi-bijaksana (double s bukannya ß di mana-mana) , meskipun bahasa dasarnya sama. Itu hanya detail kecil yang mungkin penting bagi Anda, terutama mengingat bahwa en-US dan en-GB / en-CA / en-AU atau fr-FR / fr-CA memiliki masalah yang sama.
Quote: kita tidak membutuhkannya, kita hanya melakukan perangkat lunak kita dalam bahasa Inggris
Jawab: Ya - tapi yang mana ??

Bagaimanapun, jika Anda menggunakan ID integer, Anda fleksibel, dan dapat mengubah metode Anda nanti.
Dan Anda harus menggunakan integer itu, karena tidak ada yang lebih menyebalkan, destruktif, dan merepotkan daripada desain Db yang gagal.

Lihat juga RFC 5646 , ISO 639-2 ,

Dan, jika Anda masih mengatakan "kami" hanya membuat aplikasi kami untuk "hanya satu budaya" (seperti biasanya di AS) - karena itu saya tidak memerlukan integer tambahan, ini akan menjadi waktu dan tempat yang tepat untuk menyebutkan Tag bahasa IANA , bukan?
Karena mereka pergi seperti ini:

de-DE-1901
de-DE-1996

dan

de-CH-1901
de-CH-1996

(ada reformasi ortografi pada tahun 1996 ...) Cobalah menemukan kata dalam kamus jika salah eja; ini menjadi sangat penting dalam aplikasi yang berhubungan dengan portal layanan hukum dan publik.
Lebih penting lagi, ada daerah yang berubah dari huruf kecil ke huruf latin, yang mungkin lebih merepotkan daripada gangguan dangkal dari beberapa reformasi ortografi yang tidak jelas, yang mengapa ini mungkin menjadi pertimbangan penting juga, tergantung pada negara tempat Anda tinggal. Dengan satu atau lain cara, lebih baik untuk memiliki integer di sana, untuk berjaga-jaga ...

Sunting:
Dan dengan menambahkan ON DELETE CASCADE setelah

REFERENCES dbo.T_Products( PROD_Id )

Anda bisa mengatakan:, DELETE FROM T_Productsdan tidak mendapatkan pelanggaran kunci asing.

Adapun pemeriksaan, saya akan melakukannya seperti ini:

A) Memiliki DAL Anda sendiri
B) Simpan nama kolasi yang diinginkan dalam tabel bahasa

Anda mungkin ingin meletakkan koleksi di meja mereka sendiri, misalnya:

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

C) Siapkan nama collation di informasi auth.user.language Anda

D) Tulis SQL Anda seperti ini:

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

ORDER BY GroupName COLLATE {#COLLATION}

E) Kemudian, Anda dapat melakukan ini di DAL Anda:

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

Yang kemudian akan memberi Anda SQL-Query yang disusun dengan sempurna ini

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
sumber
Respons rinci yang baik, terima kasih banyak. Tapi apa yang Anda pikirkan tentang masalah pengumpulan dalam solusi Metode 5. Tampaknya ini bukan cara terbaik ketika Anda perlu menyortir atau memfilter teks yang diterjemahkan dalam lingkungan multibahasa dengan susunan yang berbeda. Dan dalam kasus seperti itu Metode 2 (yang Anda "dikucilkan" begitu cepat :)) bisa menjadi pilihan yang lebih baik dengan sedikit modifikasi yang menunjukkan target pengumpulan untuk setiap kolom yang dilokalkan.
Eugene Evdokimov
2
@Eugene Evdokimov: Ya, tetapi "ORDER BY" akan selalu menjadi masalah, karena Anda tidak dapat menetapkannya sebagai variabel. Pendekatan saya adalah menyimpan nama collation di tabel bahasa, dan memilikinya di userinfo. Kemudian, pada setiap SQL-Statement Anda dapat mengatakan ORDER BY COLUMN_NAME {#collation}, dan kemudian Anda dapat melakukan penggantian di dal Anda (cmd.CommandText = cmd.CommandText.Replace ("{# COLLATION}", auth.user. . language.collation) Atau, Anda bisa menyortir dalam kode aplikasi Anda, misalnya menggunakan LINQ ini juga akan mengambil beberapa beban pengolahan off database Anda Untuk laporan, macam laporan pula...
Stefan Steiger
oo Ini pasti jawaban SO terpanjang yang pernah saya lihat, dan saya melihat orang membuat seluruh program sebagai jawaban. Kamu baik.
Domino
Dapat sepenuhnya menyetujui solusi SunWuKung adalah yang terbaik
Domi
48

Opsi ketiga adalah yang terbaik, karena beberapa alasan:

  • Tidak perlu mengubah skema database untuk bahasa baru (dan dengan demikian membatasi perubahan kode)
  • Tidak memerlukan banyak ruang untuk bahasa yang tidak diterapkan atau terjemahan dari item tertentu
  • Memberikan fleksibilitas paling tinggi
  • Anda tidak berakhir dengan tabel yang jarang
  • Anda tidak perlu khawatir tentang kunci nol dan memeriksa bahwa Anda menampilkan terjemahan yang sudah ada alih-alih beberapa entri nol.
  • Jika Anda mengubah atau memperluas basis data Anda untuk mencakup item / benda / yang dapat diterjemahkan lainnya, Anda dapat menggunakan tabel dan sistem yang sama - ini sangat tidak dapat dipisahkan dari data lainnya.

-Adam

Adam Davis
sumber
1
Saya setuju, meskipun secara pribadi saya akan memiliki tabel lokal untuk setiap tabel utama, untuk memungkinkan kunci asing diimplementasikan.
Neil Barnwell
1
Meskipun opsi ketiga adalah implementasi masalah yang paling bersih dan sehat, ini lebih kompleks daripada yang pertama. Saya pikir menampilkan, mengedit, melaporkan versi umum membutuhkan upaya ekstra sehingga tidak selalu dapat diterima. Saya telah menerapkan kedua solusi, lebih sederhana sudah cukup ketika pengguna membutuhkan terjemahan read-only (kadang-kadang hilang) dari bahasa aplikasi "utama".
rics
12
Bagaimana jika tabel produk berisi beberapa bidang yang diterjemahkan? Saat mengambil produk, Anda harus melakukan satu bergabung tambahan per bidang yang diterjemahkan, yang akan mengakibatkan masalah kinerja yang parah. Ada juga kompleksitas tambahan (IMO) untuk menyisipkan / memperbarui / menghapus. Satu-satunya keuntungan dari ini adalah jumlah tabel yang lebih rendah. Saya akan menggunakan metode yang diusulkan oleh SunWuKung: Saya pikir ini adalah keseimbangan yang baik antara masalah kinerja, kompleksitas, dan pemeliharaan.
Frosty Z
@ rics- Saya setuju, baik apa yang Anda sarankan untuk ...?
saber
@ Adam- Saya bingung, mungkin saya salah paham. Anda menyarankan yang ketiga, bukan? Tolong jelaskan secara lebih rinci bagaimana hubungan antara tabel-tabel itu? Maksud Anda, kami harus menerapkan tabel Terjemahan dan Terjemahan untuk setiap tabel dalam DB?
saber
9

Lihatlah contoh ini:

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

Saya pikir tidak perlu dijelaskan, strukturnya menggambarkan dirinya sendiri.

bamburik
sumber
ini bagus. tetapi bagaimana Anda mencari (misalnya product_name)?
Illuminati
Apakah Anda memiliki contoh langsung di suatu tempat dari sampel Anda? Apakah Anda mendapatkan masalah dengan menggunakannya?
David Létourneau
Tentu, saya punya proyek real estat multibahasa, kami mendukung 4 bahasa. Pencariannya agak rumit, tapi cepat. Tentu saja dalam proyek besar mungkin lebih lambat dari yang seharusnya. Dalam proyek kecil atau menengah tidak masalah.
bamburik
8

Saya biasanya akan pergi untuk pendekatan ini (bukan sql sebenarnya), ini sesuai dengan opsi terakhir Anda.

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'

Karena memiliki semua teks yang dapat diterjemahkan di satu tempat membuat pemeliharaan jadi lebih mudah. Terkadang terjemahan dialihkan ke biro terjemahan, dengan cara ini Anda dapat mengirimnya hanya satu file ekspor besar, dan mengimpornya kembali dengan mudah.

pengguna39603
sumber
1
Apa tujuan Translationtabel atau TranslationItem.translationitemidkolom itu berfungsi?
DanMan
4

Sebelum masuk ke detail dan solusi teknis, Anda harus berhenti sejenak dan mengajukan beberapa pertanyaan tentang persyaratan. Jawabannya dapat berdampak besar pada solusi teknis. Contoh pertanyaan seperti itu adalah:
- Apakah semua bahasa akan digunakan setiap saat?
- Siapa dan kapan akan mengisi kolom dengan versi bahasa yang berbeda?
- Apa yang terjadi ketika pengguna membutuhkan bahasa teks tertentu dan tidak ada dalam sistem?
- Hanya teks yang akan dilokalkan atau ada juga item lain (misalnya PRICE dapat disimpan dalam $ dan € karena mungkin berbeda)

Aleris
sumber
Saya tahu bahwa lokalisasi adalah topik yang jauh lebih luas dan saya menyadari masalah yang Anda bawa ke perhatian saya, tetapi saat ini saya sedang mencari jawaban untuk masalah desain skema yang sangat spesifik. Saya berasumsi bahwa bahasa baru akan ditambahkan secara bertahap dan masing-masing akan diterjemahkan hampir sepenuhnya.
qbeuek
3

Saya mencari beberapa tips untuk pelokalan dan menemukan topik ini. Saya bertanya-tanya mengapa ini digunakan:

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

Jadi Anda mendapatkan sesuatu seperti yang disarankan 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'

Tidak bisakah kamu meninggalkan tabel Terjemahan sehingga kamu mendapatkan ini:

    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'
pengacak
sumber
1
Tentu. Aku akan memanggil ProductItemmeja itu sesuatu seperti ProductTextsatau ProductL10nmeskipun. Lebih masuk akal.
DanMan
1

Saya setuju dengan pengacak. Saya tidak mengerti mengapa Anda memerlukan tabel "terjemahan".

Saya pikir, ini sudah cukup:

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

Apakah pendekatan di bawah ini layak? Katakanlah Anda memiliki tabel di mana lebih dari 1 kolom perlu diterjemahkan. Jadi untuk produk Anda bisa memiliki nama produk & deskripsi produk yang perlu diterjemahkan. Bisakah Anda melakukan hal berikut:

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

"Yang mana yang terbaik" didasarkan pada situasi proyek. Yang pertama adalah mudah untuk memilih dan memelihara, dan juga kinerjanya yang terbaik karena tidak perlu bergabung dengan tabel ketika memilih entitas. Jika Anda mengonfirmasi bahwa poject Anda hanya mendukung 2 atau 3 bahasa, dan itu tidak akan bertambah, Anda dapat menggunakannya.

Yang kedua okey tetapi sulit dimengerti dan dipelihara. Dan kinerjanya lebih buruk daripada yang pertama.

Yang terakhir bagus dalam skalabilitas tetapi buruk dalam kinerja. Tabel T_TRANSLATION_ENTRY akan menjadi lebih besar dan lebih besar, mengerikan ketika Anda ingin mengambil daftar entitas dari beberapa tabel.

studyzy
sumber
0

Dokumen ini menjelaskan kemungkinan solusi serta kelebihan dan kekurangan masing-masing metode. Saya lebih suka "pelokalan baris" karena Anda tidak harus mengubah skema DB saat menambahkan bahasa baru.

Jaska
sumber