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-Latn-AZ”表示“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 通用语言环境数据存储库 (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”请求德语的电话簿排序。扩展名在匹配中被忽略,但排序包使用它来选择相应的排序顺序变体。

Matcher 使用应用程序支持的语言进行初始化,这些语言通常是存在翻译的语言。此集合通常是固定的,允许在启动时创建匹配器。Matcher 经过优化,可以提高 Match 的性能,但代价是初始化成本。

language 包提供了一组预定义的最常用语言标签,可用于定义支持的集合。用户通常不必担心为支持的语言选择的确切标签。例如,AmericanEnglish(“en-US”)可以与更常见的 English(“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 的六年
博客索引