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”替換“az-Arab”,結果將是拉丁字母,並且可能無法被只瞭解阿拉伯語的使用者理解。

不同的地區也可能暗示不同的書寫系統。例如:“zh-TW”和“zh-SG”分別暗示使用繁體中文和簡體中文。再例如,“sr”(塞爾維亞語)預設使用西裡爾字母,但“sr-RU”(在俄羅斯書寫的塞爾維亞語)則暗示使用拉丁字母!對於吉爾吉斯語和其他語言也可以說類似的話。

如果您忽略子標籤,那麼展示給使用者的可能是希臘語。

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

挪威語(“nb”)最常見的書寫形式看起來非常像丹麥語。如果不支援挪威語,丹麥語可能是個不錯的選擇。同樣,請求瑞士德語(“gsw”)的使用者可能會很高興地看到德語(“de”),儘管反之則不然。請求維吾爾語的使用者可能更願意回退到中文而不是英文。還有很多其他例子。如果不支援使用者請求的語言,回退到英語通常不是最佳選擇。

語言的選擇決定了翻譯以外的事情

假設使用者請求丹麥語,第二選擇是德語。如果應用程式選擇了德語,它不僅必須使用德語翻譯,還必須使用德語(而不是丹麥語)的排序。否則,例如,動物列表可能會將“Bär”排在“Äffin”之前。

在使用者偏好語言中選擇一種支援的語言就像一種握手協議:首先確定使用哪種協議進行通訊(語言),然後在會話期間一直使用該協議進行所有通訊。

使用語言的“父代”作為回退並非易事

假設您的應用程式支援安哥拉葡萄牙語(“pt-AO”)。`golang.org/x/text` 中的包,例如排序和顯示,可能沒有針對該方言的特定支援。在這種情況下,正確的做法是匹配最接近的父方言。語言按層次結構排列,每種特定語言都有一個更通用的父代。例如,“en-GB-oxendict”的父代是“en-GB”,其父代是“en”,而“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.Make` 從使用者給定的語言程式碼字串建立 `language.Tag` 的最簡單方法。它可以從格式不正確的輸入中提取有意義的資訊。例如,“en-USD”將得到“en”,即使 USD 不是有效的子標籤。

Make 不返回錯誤。如果發生錯誤,通常的做法是使用預設語言,因此這使其更方便。使用 `Parse` 手動處理任何錯誤。

HTTP `Accept-Language` 標頭通常用於傳遞使用者的首選語言。`ParseAcceptLanguage` 函式將其解析為一系列按偏好排序的語言標籤。

預設情況下,語言包不會規範化標籤。例如,它不會遵循 BCP 47 關於在“絕大多數”情況下消除指令碼的建議。它同樣忽略 CLDR 建議:“cmn”不會被替換為“zh”,並且“zh-Hant-HK”不會被簡化為“zh-HK”。規範化標籤可能會丟棄有關使用者意圖的有用資訊。規範化在 `Matcher` 中處理。如果程式設計師仍希望這樣做,則提供了一整套規範化選項。

將使用者偏好的語言與支援的語言進行匹配

Matcher 用於將使用者偏好的語言與支援的語言進行匹配。強烈建議使用者使用它,以避免處理語言匹配的所有複雜性。

`Match` 方法可能會將使用者設定(來自 BCP 47 擴充套件)從首選標籤傳遞到選定的支援標籤。因此,由 `Match` 返回的標籤用於獲取特定語言的資源非常重要。例如,“de-u-co-phonebk”請求德語的電話簿排序。“匹配”時會忽略該擴充套件,但排序包會使用它來選擇相應的排序順序變體。

`Matcher` 使用應用程式支援的語言進行初始化,這些語言通常是提供翻譯的語言。此集合通常是固定的,允許在啟動時建立匹配器。`Matcher` 經過最佳化,以提高 `Match` 的效能,但會犧牲初始化成本。

語言包提供了一組預定義的、最常用的語言標籤,可用於定義支援集。使用者通常不必擔心為支援的語言選擇確切的標籤。例如,美式英語(“en-US”)可以與更常見的英語(“en”)互換使用,後者預設為美式英語。對於 Matcher 來說,這都沒關係。應用程式甚至可以同時新增兩者,從而為“en-US”提供更具體的美國俚語。

匹配示例

考慮以下 Matcher 和支援的語言列表。

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` 包允許用多種語言命名語言標籤。它還包含一個“Self”命名器,用於用其自身的語言顯示標籤。

例如

    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 的六年
部落格索引