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”),儘管反過來就不太成立了。請求維吾爾語的使用者可能更樂於回退到中文而不是英文。其他例子也很多。如果使用者請求的語言不受支援,回退到英文通常不是最好的做法。

語言的選擇不僅僅決定翻譯

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

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

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

假設您的應用程式支援安哥拉葡萄牙語(“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 Common Locale Data Repository (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。即使是格式錯誤的輸入,它也能提取有意義的資訊。例如,“en-USD”會得到“en”,儘管 USD 不是一個有效的子標籤。

Make 不會返回錯誤。無論如何,在發生錯誤時使用預設語言是常見的做法,因此這樣做更方便。請使用 Parse 手動處理任何錯誤。

HTTP Accept-Language 頭部通常用於傳遞使用者期望的語言。ParseAcceptLanguage 函式將其解析成一個語言標籤切片,按偏好順序排列。

預設情況下,language 包不會對標籤進行規範化。例如,它不遵循 BCP 47 關於在“絕大多數”情況下為常見選擇時消除文字的建議。它同樣忽略 CLDR 建議:“cmn”不會被“zh”替換,“zh-Hant-HK”也不會簡化為“zh-HK”。規範化標籤可能會丟失關於使用者意圖的有用資訊。規範化改在 Matcher 中處理。如果程式設計師仍然希望這樣做,也提供了一系列完整的規範化選項。

將使用者首選語言與支援語言進行匹配

Matcher 將使用者首選語言與支援語言進行匹配。如果您不想處理語言匹配的所有複雜細節,強烈建議使用它。

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

Matcher 使用應用程式支援的語言進行初始化,這些語言通常是提供翻譯的語言。這組語言通常是固定的,因此可以在啟動時建立 Matcher。Matcher 經過最佳化,以提高 Match 的效能,儘管會犧牲一些初始化成本。

language 包提供了一組預定義的、最常用的語言標籤,可用於定義支援的語言集。使用者通常不必擔心為支援的語言選擇確切的標籤。例如,美式英語(“en-US”)可以與更常見的英語(“en”)互換使用,因為“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 使用基於規則消除的粗粒度置信度評分。匹配被分為精確 (Exact)、高 (High)(不精確,但沒有已知歧義)、低 (Low)(可能是正確匹配,但也可能不是)或無 (No)。在存在多個匹配項的情況下,有一系列按順序執行的決勝規則。對於多個相等匹配項,返回第一個匹配項。這些置信度得分可能很有用,例如,用於拒絕相對較弱的匹配。它們也用於評估,例如,從語言標籤推斷出最可能的地區或文字。

其他語言的實現通常使用更細粒度、可變尺度的評分。我們發現,在 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 六年
部落格索引