Go 语言 应用 国际化与本地化(i18n)

在软件开发过程中,如果您的软件用户面向全球,那么对于国际化的支持则必不可少。

什么是软件国际化与本地化?

在信息技术领域,国际化与本地化(英文:internationalization and localization) 是指修改软件使之能适应目标市场的语言、地区差异以及其他需求。

国际化是指在设计软件时,将软件与特定语言及地区脱钩的过程。 当软件被移植到不同的语言及地区时,软件本身不用做内部工程上的改变或修正。 本地化则是指当移植软件时,加上与特定区域设置有关的信息和翻译文件的过程。

国际化和本地化之间的区别虽然微妙,但却很重要。 用一项产品来说,国际化通常只需做一次,但本地化则要针对不同的区域各做一次(最常见的是需要翻译多种语言)。 这两者之间是互补的,并且两者合起来才能让一个系统适用于各地。

国际化工具

gettext 0 是 GNU国际化与本地化(i18n)函数库。

它被广泛应用于编写多语言程序,提供了几乎对所有编程语言的支持

备注

gettext 支持的语言包括但不限于:

  • C/C++

  • Pascal

  • Python

  • Lua

  • Ruby

  • Bash

  • Java

  • PHP

  • Perl

Go 语言国际化

在 Go 语言中开发面向命令行的国际化(i18n)应用,建议使用: https://pkg.go.dev/golang.org/x/text 国际化库。

备注

gettext 在 Go 语言的国际化应用中主要有一个缺点:

gettext 需要使用(翻译结果)编译之后的 mo 二进制文件, Go 语言的软件一般都是单一文件,虽然有办法把翻译结果嵌入进可执行文件,但是多了一个步骤。

golang.org/x/text 库会把翻译之后的文本编译成 Go 语言代码,使用正常的 Go 语言编译流程即可。 直接编译还具有更高的运行时效率。

golang.org/x/text

本教程会使用 golang.org/x/text 库创建一个简单命令行工具(支持: 中文、英文)以帮助您理解。

备注

此命令行工具源码: go_i18n_cli

备注

为了方便后面的翻译字符串的提取和处理, 我们可以从 golang.org/x/text/cmd/gotext 下载 gotext

go install golang.org/x/text/cmd/gotext

如果您使用 go_i18n_cli 还可以使用如下命令自动下载

make i18n-install-gotext

gotext 使用

gotext 使用 BCP47 语言标签。

通常您可以使用如下函数自动获取当前系统所使用的语言:

import (
    "golang.org/x/text/language"
    "golang.org/x/text/message"
    "os"
)

// GetLanguage 获取运行此任务使用的语言
func GetLanguage() language.Tag {
    for _, key := range []string{"LANGUAGE", "LC_ALL", "LANG"} {
        if tag, err := language.Parse(os.Getenv(key)); err == nil {
            return tag
        }
    }
    return language.English
}

// GetPrinter 获取 i18n 翻译 Printer
func GetPrinter() *message.Printer {
    return message.NewPrinter(GetLanguage())
}

任何需要翻译的字符串都需要使用 message.Printer.X**f 函数来获取翻译结果。

func I18nHello(name string) {
    i18n := GetPrinter()
    println(i18n.Sprintf("Hello: %s", name))
}

其中的: Hello: %s 会被 gotext 工具自动提取。

警告

必须使用 Printer 中以 f 结尾的函数, 使用其他函数字符串不会被 gotext 自动提取。

提取需要翻译的字符串 & 更新翻译编译代码

使用 gotext 自动提取需要翻译的字符,并且自动编译翻译内容到 Go 语言文件。

gotext -srclang=en update -out=translations.go -lang=en,zh go_i18n_cli

备注

-srclang 指定源语言 update 是更新命令 (我们直接抽取翻译并且自动更新翻译内容到 Go 语言文件) -out 参数是翻译文本编译后的保存到的 Go 语言文件 -lang 翻译目标语言(也就是应用程序需要支持的语言列表) go_i18n_cli Go 包名(也就是我们需要提取此包的所有需要翻译的文本)

在您第一次使用 gotext 提取的时候,可能会遇到如下信息:

zh: Missing entry for "i18n".
zh: Missing entry for "Hello: {Name}".

这是因为 gotext 提取出来的文本并没有被翻译成中文,这是正常的。

执行上面的命令之后此时 cli 目录结构应该为:

$ tree cli
cli
├── demo.go
├── locales
│   ├── en
│   │   └── out.gotext.json
│   └── zh
│       └── out.gotext.json
└── translations.go

其中: locales 目录保存了我们自动提取出来需要翻译的字符串。

translations.go 文件则保存了翻译文本编译之后的 Go 语言代码。

人工翻译

在提取出 locales 目录之后,我们需要人工对其中的内容进行翻译, 翻译之后的文件对应的名称为: messages.gotext.json

例如:

> $ tree cli/locales
cli/locales
├── en
│   ├── messages.gotext.json
│   └── out.gotext.json
└── zh
    ├── messages.gotext.json
    └── out.gotext.json

2 directories, 4 files

编译翻译文本为 Go 语言代码

重新更新编译文件 make i18n-update 或者 cd cli && gotext -srclang=en update -out=translations.go -lang=en,zh go_i18n_cli

此时您可以看到 cli/translations.go 已经保存翻译文本编译的 Go 语言代码:

> $ cat cli/translations.go
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.

package cli

import (
    "golang.org/x/text/language"
    "golang.org/x/text/message"
    "golang.org/x/text/message/catalog"
)

type dictionary struct {
    index []uint32
    data  string
}

func (d *dictionary) Lookup(key string) (data string, ok bool) {
    p, ok := messageKeyToIndex[key]
    if !ok {
        return "", false
    }
    start, end := d.index[p], d.index[p+1]
    if start == end {
        return "", false
    }
    return d.data[start:end], true
}

func init() {
    dict := map[string]catalog.Dictionary{
        "en": &dictionary{index: enIndex, data: enData},
        "zh": &dictionary{index: zhIndex, data: zhData},
    }
    fallback := language.MustParse("en")
    cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
    if err != nil {
        panic(err)
    }
    message.DefaultCatalog = cat
}

var messageKeyToIndex = map[string]int{
    "Hello: %s": 1,
    "i18n":      0,
}

var enIndex = []uint32{ // 3 elements
    0x00000000, 0x00000005, 0x00000012,
} // Size: 36 bytes

const enData string = "\x02i18n\x02Hello: %[1]s"

var zhIndex = []uint32{ // 3 elements
    0x00000000, 0x0000000a, 0x00000018,
} // Size: 36 bytes

const zhData string = "\x02国际化\x02您好: %[1]s"

// Total table size 114 bytes (0KiB); checksum: 262FC1A6

编译 & 运行测试

编译(make build)之后, 运行测试:

> $ LANGUAGE=zh ./main
您好: 国际化

> $ LANGUAGE=en ./main
Hello: i18n

总结

使用 gotext 完成命令行应用的国际化是一个很好的选择(对于 Web 应用可能并不适合)。

主要有以下步骤:

  1. 引入 gotext

  2. 所有对用户展示的字符使用 gotext.message.Printer.X***f 函数获取

  3. 使用 gotext 命令行工具提取出需要翻译的字符

  4. 人工翻译需要翻译的字符

  5. 使用 gotext 命令行工具编译翻译之后的字符串为 Go 语言文件

  6. 重新编译 Go 语言应用

参考资料

0

gettext