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”替换“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。尽情享受。