原文:《A Simple Way to Internationalize in Go with go-i18n》
作者:Theo PhraseApp Content Team
以前我们用了 golang.org/x/text
包来实现 Go 的国际化。
(译注:这一篇译文在 《[译] 手把手教你 Go 程序的国际化和本土化》)
尽管那个包拓展性很好,但操作起来较为困难,而且文档也不清楚。要想用更简单的办法来本地化我们的
Go 程序,还有另一种方案,叫 go-i18n。
go-i18n 支持:
- 200 多种语言的复数形式
- 带有命名的字符串
- 任一种格式的翻译文件(如 JSON, TOML, YAML 等)
- 文档很好
但它当前还不支持性别规则,也不支持复杂的模版变量。不过对于大多数情况,本地化现有程序已经足够了。 这篇教程中,我们将看到一些实际案例,同时试着去集成 PhraseApp 的上下文编辑器到程序中。所有示例 代码都托管在 GitHub 上了。来开始吧!
定义和翻译消息
在使用这个库之前,我们需要先将它下载到 $GOPATH,使用下面命令:
$ go get -u github.com/nicksnyder/go-i18n/v2/i18n
接着新建一个文件,来测试一下翻译:
$ touch example.go
文件: example.go
package main
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func main() {
}
第一步是建立语言包,包含支持的语言和默认语言。我们来建一个默认为英语的:
// 第一步: 新建语言包
func main() {
bundle := &i18n.Bundle{DefaultLanguage: language.English}
}
为了进行翻译,需要创建一个 Localizer 对象,传入我们需要翻译的语言列表。有了一系列语言包后, 程序会按照语言标签去选择对应的语言包。
// 第二步:使用一到多个语言标签来创建 localizer
loc := i18n.NewLocalizer(bundle, language.English.String())
现在还没有任何要翻译的消息,我们来加一些:
// 第三步:定义消息
messages := &i18n.Message{
ID: "Emails",
Description: "The number of unread emails a user has",
One: "{{.Name}} has {{.Count}} email.",
Other: "{{.Name}} has {{.Count}} emails.",
}
从上面能看到复数规则的使用,还用到了模版变量。
最后一步,进行翻译操作:
// 第四步:翻译消息
messagesCount := 2
translation := loc.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: messages,
TemplateData: map[string]interface{}{
"Name": "Theo",
"Count": messagesCount,
},
PluralCount: messagesCount,
})
fmt.Println(translation)
如果出错的话,MustLocalize 方法会出现 panic。有一个相关的方法叫 Localize ,会返回错误。
上面的代码中,我们传了 messagesCount 到 TemplateData 和 PluralCount 中,以正确生成复数,这至关重要。
定义分隔符
如果不喜欢双括弧的话,我们有选项去定义另外的分割符。只需要设定 LeftDelim 和 RightDelim 并把消息里的字符串设置为对应的符号就行了。
// 定义不同的分隔符
messages = &i18n.Message{
ID: "Notifications",
Description: "The number of unread notifications a user has",
One: "<<.Name>> has <<.Count>> notification.",
Other: "<<.Name>> has <<.Count>> notifications.",
LeftDelim: "<<",
RightDelim: ">>",
}
notificationsCount := 1
translation = loc.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: messages,
TemplateData: map[string]interface{}{
"Name": "Theo",
"Count": notificationsCount,
},
PluralCount: notificationsCount,
})
fmt.Println(translation)
从文件加载消息
我们也可以从文件加载翻译内容。要这样用的话,需要先在语言包里定义 Unmarshal 函数,然后从某个文件 读取消息。
// 从文件解析
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
bundle.MustLoadMessageFile("en.json")
bundle.MustLoadMessageFile("el.json")
loc = i18n.NewLocalizer(bundle, "el")
messagesCount = 10
translation = loc.MustLocalize(&i18n.LocalizeConfig{
MessageID: "messages",
TemplateData: map[string]interface{}{
"Name": "Alex",
"Count": messagesCount,
},
PluralCount: messagesCount,
})
fmt.Println(translation)
JSON 文件的内容如下:
文件: el.json
{
"hello_world": "Για σου Κόσμε",
"messages": {
"description": "The number of messages a person has",
"one": "Ο {{.Name}} έχει {{.Count}} μύνημα.",
"other": "Ο {{.Name}} έχει {{.Count}} μύνηματα."
}
}
文件: en.json
{
"hello_world": "Hello World",
"messages": {
"description": "The number of messages a person has",
"one": "{{.Name}} has {{.Count}} message.",
"other": "{{.Name}} has {{.Count}} messages."
}
}
试着运行完整的程序,看,翻译完成了。
$ go run example.go
Theo has 2 emails.
Nick has 1 notification.
Ο Alex έχει 10 μύνηματα.
使用命令行工具
这个库还带了个命令行工具,用来自动提取与合并翻译文件。
首先,我们需要安装
$ go get -u github.com/nicksnyder/go-i18n/v2/goi18n
目前它提供了两个命令:
- extract:从源程序提取消息并输出到特定格式的文件
- merge:合并两到多个特定格式文件中的消息
来看看这两个命令的使用示例。
先创建一个文件,名为 messages.go
$ touch messages.go
文件: messages.go
package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
var messages = i18n.Message{
ID: "invoices",
Description: "The number of invoices a person has",
One: "You can {{.Count}} invoice",
Other: "You have {{.Count}} invoices",
}
使用 extract 命令来导出消息为 JSON 格式。
$ mkdir out
$ goi18n extract -outdir=out -format=json newMessages.go
文件: out/active.en.json
{
"invoices": {
"description": "The number of invoices a person has",
"one": "You can {{.Count}} invoice",
"other": "You have {{.Count}} invoices"
}
}
接着对两个已有的翻译文件,我们来合并它们:
$ goi18n merge -outdir=out -format=json en.json out/active.en.json
文件: out/active.en.json
{
"hello_world": "Hello World",
"invoices": {
"description": "The number of invoices a person has",
"one": "You can {{.Count}} invoice",
"other": "You have {{.Count}} invoices"
},
"messages": {
"description": "The number of messages a person has",
"one": "{{.Name}} has {{.Count}} message.",
"other": "{{.Name}} has {{.Count}} messages."
}
}
如你所见,我们已将所有消息都合并到单个文件里了。
集成 PhraseApp 上下文编辑器
PhraseApp 的上下文编辑器是个帮你在翻译时提升质量的工具, 它提供有用的上下文信息。借助这个编辑器,你可以在编辑文本的同时预览你的网站。
尽管 go-i18n 还没有内置这个编辑器,但你可以按照这篇向导来集成:
然后我们可以注册自己的模版过滤器,并集成到自己的应用中。
来看看如何通过简单几步使用它。
新建一个叫 inContext.go 的文件,加入下面代码。
$ touch inContext.go
文件: inContext.go
package main
import (
"html/template"
"log"
"net/http"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
"encoding/json"
"flag"
"fmt"
)
var page = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
<body>
<h1>{{ .Title }}</h1>
{{range .Paragraphs}}<p>{{ . }}</p>{{end}}
</body>
</html>
`))
func main() {
bundle := &i18n.Bundle{DefaultLanguage: language.English}
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
for _,lang := range []string{"en" ,"el"} {
bundle.MustLoadMessageFile(fmt.Sprintf("active.%v.json", lang))
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
lang := r.FormValue("lang")
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(bundle, lang, accept)
name := r.FormValue("name")
if name == "" {
name = "Alex"
}
myInvoicesCount := 10
helloPerson := localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "HelloPerson",
},
TemplateData: map[string]interface{}{
"Name": name,
},
})
myInvoices := localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "invoices",
},
TemplateData: map[string]interface{}{
"Count": myInvoicesCount,
},
PluralCount: myInvoicesCount,
})
err := page.Execute(w, map[string]interface{}{
"Title": helloPerson,
"Paragraphs": []string{
myInvoices,
},
})
if err != nil {
panic(err)
}
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
这段程序创建一个 Web 服务器,提供一个带有默认语言的页面。如果你打开浏览器并访问 localhost:8080/?lang=el
将会看到翻译后的希腊语页面。
要想集成 PhraseApp 的上下文编辑器进去,我们需要把模版变量封装到 {{__phrase_
和 __}}
两个分隔符之间,接着加载 JavaScript 客户端。
可以使用 https://golang.org/pkg/text/template/#Template.Funcs 函数去注册我们自己的翻译过滤器, 一旦配置好后,就可以用来封装上面的参数。现在就来做。
文件: inContext.go
// 从配置加载
var isPhraseAppEnabled bool
func init() {
flag.BoolVar(&isPhraseAppEnabled,"phraseApp", false, "Enable PhraseApp mode")
flag.Parse()
}
var apiToken = os.Getenv("PHRASE_APP_TOKEN")
func translate(s string) string {
if isPhraseAppEnabled {
return "{{__phrase_" + s + "__}}"
} else {
return s
}
}
var funcs = template.FuncMap{
"translate": translate,
}
这里我们加了一个 translate 函数,它按照 phraseApp 的配置(去设置是不是替换字符串为模版变量)。
现在只需要把这个过滤器加到每个模版参数上,再加上 PhraseApp 脚本就好了。
var page = template.Must(template.New("").Funcs(funcs).Parse(`
<!DOCTYPE html>
<html lang= {{ .CurrentLocale }}>
<body>
<h1>{{ translate .Title }}</h1>
{{range .Paragraphs}}<p>{{ translate . }}</p>{{end}}
</body>
window.PHRASEAPP_CONFIG = {
projectId: {{ .apiToken }}
};
(function() {
var phraseapp = document.createElement('script'); phraseapp.type = 'text/javascript'; phraseapp.async = true;
phraseapp.src = ['https://', 'phraseapp.com/assets/in-context-editor/2.0/app.js?', new Date().getTime()].join('');
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(phraseapp, s);
})();
</html>
`))
更新模版以包含 apiToken 参数。
err := page.Execute(w, map[string]interface{}{
"apiToken": apiToken,
"Title": helloPerson,
"CurrentLocale": language.Greek.String(),
"Paragraphs": []string{
myInvoices,
},
})
如果你没有令牌(token),到 https://phraseapp.com/ 注册并使用试用版。
在你设置好账号后,就可以新建项目并在 Project Setting 页找到 projectId 的令牌。
在启动服务端前,用这个令牌去设置环境变量 PHRASE_APP_TOKEN
的值。
打开应用的页面后,你将看到 PhraseApp 登录的页面,在授权后你会看到翻译后的文本, 这些字符串旁边带有编辑按钮。下面还显示编辑器面板。
在这里你可以轻松管理你的翻译啦!
结论
这篇文章里,我们看了如何使用 go-i18n 库去翻译 Go 应用,还了解了如何集成 PhraseApp 的上下文编辑器进来。如果你还有什么问题,别犹豫,在下面留言或私信我。感谢阅读,下次再见!