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