Go 部落格

在 Go 中進行語言和地區比對

Marcel van Lohuizen
2016 年 2 月 9 日

引言

想像具有使用者介面中支援多種語言的應用程式,例如網站。當用戶使用偏好語言清單抵達時,應用程式必須決定要在其對用戶的呈現中使用哪一種語言。這需要找出應用程式支援的語言和用戶偏好的語言之間的最佳比對。本文將說明為何這項決定很困難,以及 Go 可以如何提供協助。

語言標籤

語言標籤(也稱為地區識別碼)是正在使用的語言和/或方言的機器可讀識別碼。對這些語言標籤最常見的參考是 IETF BCP 47 標準,而這也是 Go 函式庫所遵循的標準。以下是一些 BCP 47 語言標籤及其代表的語言或方言範例。

標籤 說明
en 英文
en-US 美式英文
cmn 標準中文
zh 中文,通常為普通話
nl 荷蘭語
nl-BE 佛蘭芒語
es-419 拉丁美洲西班牙語
az, az-Latn 兩種以拉丁字母書寫的亞塞拜然語
az-Arab 以阿拉伯字母書寫的亞塞拜然語

語言標籤的一般格式為語言代碼(上述的「en」、「cmn」、「zh」、「nl」、「az」),後面可選用副標籤,表示文字系統(「-Arab」)、地區(「-US」、「-BE」、「-419」)、變體(牛津英語詞典拼法的「-oxendict」)和擴充(電話簿排序的「-u-co-phonebk」)。如果省略副標籤,會設為最常見的格式,例如「az」的「az-Latn-AZ」。

最常見的語言標籤用法是根據使用者語言偏好的清單,從一組系統支援的語言中進行選擇;例如決定一位偏好使用南非語的使用者,在沒有南非語可供選用的情況下,最適合系統顯示荷蘭語。要找出此類配對,需查閱有關語言相互理解性的資料。

配對後產生的標籤,接著會用於取得語言特定的資源,例如翻譯、排序順序和大小寫演算法。這會涉及另一種配對。例如,由於葡萄牙語沒有特定排序順序,排序套件可能會回到預設語言或「根語言」的排序順序。

語言配對的混亂本質

處理語言標籤很棘手。其中部分原因是因為人類語言的界線不明確,部分原因則是因為語言標籤標準演化的緣故。在本節中,我們將說明處理語言標籤的一些混亂面。

具有不同語言代碼的標籤可能表示相同的語言

由於歷史和政治因素,許多語言代碼隨著時間而改變,導致語言同時具有舊的舊版代碼和新的代碼。但即使是兩個最新的代碼也可能指的是相同的語言。例如,中文(普通話)的官方語言代碼為「cmn」,但「zh」是到目前為止最常使用來標示這種語言的標示語。代碼「zh」官方保留給所謂巨集語言,用於標示漢語語言群組。巨集語言的標籤經常與該語言群中使用人數最多的語言互換使用。

僅配對語言代碼並不足夠

例如,亞塞拜然語(「az」)會根據其使用的國家而以不同的文字系統書寫:「az-Latn」為拉丁字母(預設文字系統),「az-Arab」為阿拉伯字母,而「az-Cyrl」為西里爾字母。如果你將「az-Arab」替換成「az」,結果會以拉丁字母呈現,而且有可能無法被僅認識阿拉伯書寫形式的使用者理解。

不同的地區也可能暗示不同的文字。例如:「zh-TW」和「zh-SG」分別暗示使用正體中文和簡體中文。另一個例子是「sr」(塞爾維亞語)預設使用西里爾字母,但「sr-RU」(在俄羅斯書寫的塞爾維亞語)暗示使用拉丁字母!吉爾吉斯語和其他語言也可以這樣說。

如果您忽略次標籤,您也不妨向使用者呈現希臘語。

最佳匹配可能是使用者未列出的語言

挪威語最常見的書寫形式(「nb」)看起來非常像丹麥語。如果沒有挪威語可用,丹麥語可能是第二個好選擇。同樣地,要求瑞士德語(「gsw」)的使用者可能會很樂意被呈現德語(「de」),儘管相反的情況遠非如此。要求維吾爾語的使用者可能更樂意退而求其次使用中文,而不是英文。還有其他很多類似例子。如果使用者要求的語言不受支援,退而求其次使用英文通常不是最好的做法。

語言的選擇決定了不只是翻譯

假設使用者要求使用丹麥語,德語為第二選擇。如果應用程式選擇德語,它不僅必須使用德語翻譯,還必須使用德語(不是丹麥語)排序。否則,例如,動物清單可能會在「Äffin」之前對「Bär」進行排序。

根據使用者的偏好語言選擇支援的語言就像握手演算法一樣:您首先確定使用哪種通訊協定(語言),然後在整個會話期間持續使用此協定進行所有通訊。

將語言的「父代」用作備援並非易事

假設您的應用程式支援安哥拉葡萄牙語(「pt-AO」)。golang.org/x/text 中的套件,例如排序和顯示,可能不具備此方言的特定支援。在此種情況下正確的作法是匹配最相近的父系方言。語言會以階層方式排列,每個特定語言都有更通用的父系。例如,「en-GB-oxendict」的父系為「en-GB」,其父系為「en」,其父系為未定義的語言「und」,又稱為根語言。在排序的情況下,沒有葡萄牙語的特定排序順序,因此排序套件會選擇根語言的排序順序。顯示套件支援的與安哥拉葡萄牙語最接近的父系是歐洲葡萄牙語(「pt-PT」),而不是較為明顯的「pt」,這表示巴西語。

一般而言,父系關係並不簡單。再舉幾個例子,「es-CL」的父系為「es-419」,「zh-TW」的父系為「zh-Hant」,「zh-Hant」的父系為「und」。如果您透過單純移除次標籤來計算父系,您可能會選擇使用者無法理解的「方言」。

Go 語言的語言比對

Go 套件 golang.org/x/text/language 實作語言標籤的 BCP 47 標準,並新增根據 Unicode 通用當地資料庫 (CLDR) 中發布的資料,決定要使用哪種語言的支援。

以下是一個範例程式,說明如何將使用者的語言偏好與應用程式的支援語言配對

package main

import (
    "fmt"

    "golang.org/x/text/language"
    "golang.org/x/text/language/display"
)

var userPrefs = []language.Tag{
    language.Make("gsw"), // Swiss German
    language.Make("fr"),  // French
}

var serverLangs = []language.Tag{
    language.AmericanEnglish, // en-US fallback
    language.German,          // de
}

var matcher = language.NewMatcher(serverLangs)

func main() {
    tag, index, confidence := matcher.Match(userPrefs...)

    fmt.Printf("best match: %s (%s) index=%d confidence=%v\n",
        display.English.Tags().Name(tag),
        display.Self.Name(tag),
        index, confidence)
    // best match: German (Deutsch) index=1 confidence=High
}

建立語言標籤

從使用者提供的語言代碼字串建立 language.Tag 最簡單的方式是使用 language.Make。它會從格式錯誤的輸入中萃取有意義的資訊。例如,即使 USD 不是有效的子標籤,「en-USD」仍會產生「en」。

Make 沒有傳回錯誤。無論如何,在發生錯誤時使用預設語言是一種常見做法,因此這讓操作更方便。使用 Parse 手動處理任何錯誤。

HTTP Accept-Language 標頭通常用於傳遞使用者的偏好語言。變數接受語言剖析函數會將其剖析成一個語言標籤切片,並依照偏好排序。

語言套件預設不會將標籤正規化。例如,它不會按照 BCP 47 的建議,在「絕大多數」中移除腳本,如果它是常見選擇的話。它類似會忽略 CLDR 的建議:「cmn」不會取代「zh」,而「zh-Hant-HK」不會簡化為「zh-HK」。正規化標籤可能會移除關於使用者意圖的有用資訊。正規化會在比對器中處理。如果程式設計師仍然想要這樣做的話,可以使用完整的正規化選項陣列。

將使用者偏好的語言配對至支援語言

比對器會將使用者偏好的語言配對至支援語言。強烈建議使用者使用它,如果他們不想要處理配對語言的種種複雜性。

Match 方法可能會傳遞使用者設定 (來自於 BCP 47 延伸) 從偏好標籤傳遞到選取的支援標籤。因此,很重要的一點是,由 Match 傳回的標籤是用來取得特定語言的資源。例如,「de-u-co-phonebk」對德文要求電話簿排序。延伸不考慮配對,而是由排序套件用來選取對應的分類順序變異。

比對器會使用應用程式支援的語言來初始化,這些語言通常是使用翻譯的語言。這個集合通常是固定的,允許在啟動時建立比對器。比對器經過最佳化,可以提升 Match 的效能,而犧牲初始化成本。

語言套件提供一套預先定義的,最常使用的語言標籤,可供定義支援語言。使用者通常不必擔心為支援語言選擇的確切標籤。例如,美國英語 («en-US») 可以與更常見的英文 («en») 交換使用,而英文預設為美國。對於比對器來說,這是一樣的。應用程式甚至可以加入兩者,允許對「en-US」有更具體的美國俚語。

比對範例

考慮下列比對器和支援語言清單

var supported = []language.Tag{
    language.AmericanEnglish,    // en-US: first language is fallback
    language.German,             // de
    language.Dutch,              // nl
    language.Portuguese          // pt (defaults to Brazilian)
    language.EuropeanPortuguese, // pt-pT
    language.Romanian            // ro
    language.Serbian,            // sr (defaults to Cyrillic script)
    language.SerbianLatin,       // sr-Latn
    language.SimplifiedChinese,  // zh-Hans
    language.TraditionalChinese, // zh-Hant
}
var matcher = language.NewMatcher(supported)

讓我們檢視針對不同使用者偏好的語言支援清單來配對。

對於使用者偏好為「he」(希伯來語),最佳配對為「en-US」(美式英語)。並無合適配對,因此配對器使用備援語言(支援清單中的第一個)。

對於使用者偏好為「hr」(克羅埃西亞語),最佳配對為「sr-Latn」(使用拉丁文字的塞爾維亞語),因為當兩者以相同的文字書寫時,塞爾維亞語和克羅埃西亞語能夠相互理解。

對於使用者偏好為「ru, mo」(俄語,隨後為摩爾多瓦語),最佳配對為「ro」(羅馬尼亞語),因為摩爾多瓦語現在正式分類為「ro-MD」(摩爾多瓦的羅馬尼亞語)。

對於使用者偏好為「zh-TW」(台灣國語),最佳配對為「zh-Hant」(使用繁體中文書寫的國語),而非「zh-Hans」(使用簡體中文書寫的國語)。

對於使用者偏好為「af, ar」(南非語,隨後為阿拉伯語),最佳配對為「nl」(荷蘭語)。兩個偏好都沒有得到直接支援,但拿荷蘭語和南非語比較,比拿備援英語和兩者相比時,發現荷蘭語更相近。

對於使用者偏好為「pt-AO, id」(安哥拉葡萄牙語,隨後為印尼文),最佳配對為「pt-PT」(歐洲葡萄牙語),而非「pt」(巴西葡萄牙語)。

對於使用者偏好為「gsw-u-co-phonebk」(使用電話簿排序規則的瑞士德語),最佳配對為「de-u-co-phonebk」(使用電話簿排序規則的德語)。在伺服器的語言清單中,德語與瑞士德語最為匹配,而電話簿排序規則選項已繼承。

可信度分數

Go 使用以規則為基礎的消除方式,進行粗略的可信度評分。一項配對被分類為「精確」、「高」(非精確,但沒有已知的歧義性)、「低」(可能是正確配對,但也可能不是)或「否」。如果有數個配對,則有一組會以順序執行的斷平規則。如果有多個相同的配對,會傳回第一個配對。這些可信度分數可能很有用,例如用來拒絕比較差的配對。它們也用於評分,例如根據語言標籤找出最可能的區域或文字。

其他語言中的實作經常使用更細緻的、變異比例的評分。我們發現,在 Go 實作中使用粗略的評分,最後會變得較容易實作、更易於維護且更快速,表示我們可以處理更多規則。

顯示受支援語言

golang.org/x/text/language/display 套件允許以多種語言來命名語言標籤。它也包含「自我」命名,用來以自身的語言顯示標籤。

例如

    var supported = []language.Tag{
        language.English,            // en
        language.French,             // fr
        language.Dutch,              // nl
        language.Make("nl-BE"),      // nl-BE
        language.SimplifiedChinese,  // zh-Hans
        language.TraditionalChinese, // zh-Hant
        language.Russian,            // ru
    }

    en := display.English.Tags()
    for _, t := range supported {
        fmt.Printf("%-20s (%s)\n", en.Name(t), display.Self.Name(t))
    }

列印

English              (English)
French               (français)
Dutch                (Nederlands)
Flemish              (Vlaams)
Simplified Chinese   (简体中文)
Traditional Chinese  (繁體中文)
Russian              (русский)

在第二欄,請注意字母大小寫的使用方式的差異,反映出各自語言的規則。

結論

乍看之下,語言標籤看起來像是條理分明的結構化資料,但因為它們描述的是人類語言,語言標籤之間的關係結構其實相當複雜。這通常會讓人覺得引誘,尤其是那些使用英語的程式設計師,只想使用語言標籤進行文字操控,來撰寫特定的語言配對。如上所述,這麼做可能會產生可怕的結果。

Go 的 golang.org/x/text/language 套件解決了這個複雜的問題,同時仍提供一個簡單易用的 API。享受吧。

下一篇: 發行 Go 1.6
上一篇: Go 的六年
部落格索引