GoLang 国际化

2022-06-17 language golang

简介

有两个容易混淆的概念:A) 本地化 L10n,将软件翻译为本地语言的过程,通常是翻译人员的职责;B) 国际化 i18n,使得软件可以被本地化,通常是开发人员的职责。

代码中有很多需要与人交互的内容,一般需要做一些国际化的处理,通常步骤如下:

  1. 从源码中获取需要翻译的字符串。
  2. 翻译字符串,有很多的平台支持,甚至有些是免费的。
  3. 应用已经翻译的字符串,可以是通过数据库、配置文件方式读取,也可以编译成二进制甚至生成代码。
  4. 使用已经翻译字符,包括了 Web 使用 Accept-Language 以及主机上的 LANGUAGE 环境变量。

实际上已经有个 https://golang.org/x/text 代码包支持,官方声明如下。

a repository of text-related packages related to internationalization (i18n) and localization (l10n) 。

其中的 message 库主要用于上述的步骤三,以类似 fmt 的接口来输出已经翻译的字符串,如下是简单示例。

package main
 
import (
    "golang.org/x/text/message"
    "golang.org/x/text/language"
)
 
func main()  {
    p := message.NewPrinter(language.BritishEnglish)
    p.Printf("There are %v flowers in our garden.\n", 1500)

    p = message.NewPrinter(language.Greek)
    p.Printf("There are %v flowers in our garden.", 1500)
}

可以看到代码输出的数字分别为 1,5001.500,其中 NewPrinter() 函数的入参称为 Tag,标识了语言、地域,更多的 Tag 可以参考 pkg.go.dev 中的介绍。

语言设置

以中文为例,通常通过 zh-CN 表示,其代表了 language-contry/region,也就是汉语-大陆地区,主要是因为,同样是简体中文,大陆和新加坡会相差很大,而繁体中文对应的台湾和香港也有差异。

所以,通常除了语言外还会指定地区,这在 GoLang 中被成为 Tag,如上,使用 NewPrinter() 函数的时候需要指定一个有效 Tag 才可以,如下有多种方式创建。

// 内部定义常量
p := message.NewPrinter(language.BritishEnglish)

// 通过字符串解析
lang := language.MustParse("zh-CN") // en-GB
p := message.NewPrinter(lang)

翻译集

也称为 Catalog,定义了翻译后字符串的集合,是一组各个语言的词典,每个词典包含了键及其译文,使用时需要先生成。

package main
 
import (
    "golang.org/x/text/message"
    "golang.org/x/text/language"
)
 
func main()  {
    message.SetString(language.Chinese, "%s went to %s.", "%s去了%s。")
    message.SetString(language.AmericanEnglish, "%s went to %s.", "%s is in %s.")
    message.SetString(language.Chinese, "%s has been stolen.", "%s被偷走了。")
    message.SetString(language.AmericanEnglish, "%s has been stolen.", "%s has been stolen.")
    message.SetString(language.Chinese, "How are you?", "你好吗?.")

    p := message.NewPrinter(language.Chinese)
    p.Printf("%s went to %s.", "彼得", "英格兰")
    fmt.Println()
    p.Printf("%s has been stolen.", "宝石")
    fmt.Println()

    p = message.NewPrinter(language.AmericanEnglish)
    p.Printf("%s went to %s.", "Peter", "England")
    fmt.Println()
    p.Printf("%s has been stolen.", "The Gem")
    fmt.Println()
}

通过 SetString() 指定,区分大小写,也包括换行。上述是手动创建的翻译集,也可以通过 catalog.Builder 让程序生成。

单复数

一些语言中需要处理单复数情况,此时就需要用到 golang.org/x/text/feature/plural 子包,里面存在 SelectF() 函数来处理复数情况。

package main
 
import (
    "golang.org/x/text/message"
    "golang.org/x/text/language"
    "golang.org/x/text/feature/plural"
)

func main()  {
    message.Set(language.English, "I have %d apples.",
        plural.Selectf(1, "%d",
            "=1", "I have an apple.",
            "=2", "I have two apples.",
            "other", "I have %[1]d apples.",
        ),
    )
    message.Set(language.English, "%d days left.",
        plural.Selectf(1, "%d",
            "one", "One day left.",
            "other", "%[1]d days left.",
        ),
    )

    p := message.NewPrinter(language.English)
    p.Printf("I have %d apples.", 1)
    fmt.Println()
    p.Printf("I have %d apples.", 2)
    fmt.Println()
    p.Printf("I have %d apples.", 5)
    fmt.Println()
    p.Printf("%d days left.", 1)
    fmt.Println()
    p.Printf("%d days left.", 10)
    fmt.Println()
}

上述的 Selectf() 可以识别其它的量词,例如 zeroonetwofew 等,或者匹配比较符,例如 >x<x 等。

另外,还可以通过占位符变量进一步处理消息中的量词。

package main
 
import (
    "golang.org/x/text/message"
    "golang.org/x/text/language"
    "golang.org/x/text/feature/plural"
)

func main()  {
    message.Set(language.English, "You are %d minutes late.",
        catalog.Var("m", plural.Selectf(1, "%d",
            "one", "minute",
            "other", "minutes")
        ),
        catalog.String("You are %[1]d ${m} late."),
    )

    p := message.NewPrinter(language.English)
    p.Printf("You are %d minutes late.", 1)
    fmt.Println()
    p.Printf("You are %d minutes late.", 10)
    fmt.Println()
}

其中 catalog.Var() 的第一个参数为字符串,会根据具体的值进行翻译。

最佳实践

多数本地化方案都是将语言的译文分别存于文件里,这些文件被动态加载,可以使用 gotext 命令行,通过如下命令更新、安装。

$ go get -u golang.org/x/text/cmd/gotext
$ go install golang.org/x/text/cmd/gotext

使用时基本分成了两步:

  1. 从代码中提取中需要翻译的键,并写入到文件中。
  2. 更新代码,使得程序可以加载对应的键到翻译集中使用。

如下是一个简单的示例。

package main

//go:generate gotext -srclang=en update -out=catalog/catalog.go -lang=en,zh

import (
    "golang.org/x/text/message"
    "golang.org/x/text/language"
    _ "example.com/foobar/catalog"
)

func main()  {
	p := message.NewPrinter(language.Chinese)
	p.Printf("Hello World!")
	p.Println()
}

执行 mkdir catalog && go generate 命令会在当前目录下生成 locales 目录,包含了英文和中文的键,一般文件名为 locales/cn/out.gotext.json,将其复制为 messages.gotext.json 然后修改为如下。

{
    "language": "zh",
    "messages": [
        {
            "id": "Hello World!",
            "message": "Hello World!",
            "translation": "你好!"
        }
    ]
}

也就是添加了翻译字段,接着重新执行 go generate 命令,然后运行即可,注意,需要引入生成的 catalog/catalog.go 文件,只需要调用 init() 函数即可。

其它

gotext

如上已经安装了 gotext 命令,可以直接查看帮助文档,最常使用的是 update 命令,其它的还有 extractgenerate 等,如下是常见的参数:

  • -srclang 应用使用 BCP 47 标签作为基础语言。
  • -out 产生的消息目录所在路径,使用的是文件相对路径。
  • -lang 通过逗号分割指定多个语言的标签列表。

参数的最后通过完全限定模块路径指定想要翻译的包,例如 example.com/foo/bar,如果多个可以通过空格分割。

注意,该命令只查找代码中的 messge.Printer 相关的几个函数,例如 Printf() Fsprintf() Sprintf() 三个基础函数,其它的如 Sprint()Print() 函数会忽略。