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 六年
博客索引