Go 部落格
在 Go 中進行語言和地區比對
引言
想像具有使用者介面中支援多種語言的應用程式,例如網站。當用戶使用偏好語言清單抵達時,應用程式必須決定要在其對用戶的呈現中使用哪一種語言。這需要找出應用程式支援的語言和用戶偏好的語言之間的最佳比對。本文將說明為何這項決定很困難,以及 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。享受吧。