使用 App Engine 进行 Go 的 I18n 策略

Posted

技术标签:

【中文标题】使用 App Engine 进行 Go 的 I18n 策略【英文标题】:I18n strategies for Go with App Engine 【发布时间】:2012-12-16 23:20:21 【问题描述】:

我想不一定特定于 GAE,但我很好奇人们使用什么来翻译或本地化他们的 Web 应用程序。

恐怕我自己的方法太天真了,实际上只是通过根据用户配置文件中记录的区域设置值从数据存储中为每个包加载一个实体来解决这个问题。至少这允许提供一些字符串的翻译:

package foo

...

type Messages struct 
    Locale string
    ErrorDatastore string
    LoginSuccessful string
    ...

使用与语言环境对应的字符串 id 存储,然后加载到 Gorilla 上下文或类似内容:

const Messages ContextKey = iota

...

k := datastore.NewKey(c, "Messages", "en_US", 0, nil)
m := new(Messages)
if err := datastore.Get(c, k, m); err != nil 
    ...
 else 
    context.Set(r, Messages, m)

这显然是非常有限的,但至少可以通过 context.Get(r, foo.Messages) 调用代码来获取字符串。谁能指出更有用的实现,或提出更好的方法?

编辑(相关但不完全有用):

gettext: a MO file parser go-18n Internationalization plan for Go Polyglot

【问题讨论】:

关于建议编辑:我保留拼写“本地化”the correct way 的权利。嘿,至少我在标记时使用了“z”! ;) 您期望从“更有用的实现”中获得什么样的功能?对于大多数 i18n 应用程序,键值存储(可能带有一些格式代码)应该可以正常工作。 好吧,我猜想这能让翻译人员更轻松地完成工作。我很好奇是否有我可能错过的其他流行方法(gettext 等)的端口。现在我正在使用chrome.i18n's JSON format 拼凑一个简单的模块,如果没有更完整的实现,我会发布它作为答案。 谷歌搜索“go gettext”会显示这个 - github.com/samuel/go-gettext - 你检查过吗?无论如何,祝你编写该模块,祝你好运! 不,这对我来说是新的,这是使用“go gettext”而不是我一直在搜索的(未引用的)golang gettext 的有趣结果!如果可行,我会调查并发布答案。 【参考方案1】:

Jonathan Chan 指出Samuel Stauffer's go-gettext 似乎可以解决问题。给定目录:

~appname/
 |~app/
 | `-app.go
 |+github.com/
 `-app.yaml

从(假设 *nix)开始:

$ cd appname
$ git clone git://github.com/samuel/go-gettext.git github.com/samuel/go-gettext

源准备不能使用 _("String to be translate") 短格式 due to underscore's special characteristics in Go。您可以使用 -k 标志告诉 xgettext 查找驼峰式函数名称“GetText”。

最小的工作示例:

package app

import (
    "fmt"
    "log"
    "net/http"

    "github.com/samuel/go-gettext"
)

func init () 
    http.HandleFunc("/", home)


func home(w http.ResponseWriter, r *http.Request) 
    d, err := gettext.NewDomain("appname", "locale")
    if err != nil 
        log.Fatal("Failed at NewDomain.")
    

    cat := d.GetCatalog("fr_FR")
    if cat == gettext.NullCatalog 
        log.Fatal("Failed at GetCatalog.")
    

    fmt.Fprintf(w, cat.GetText("Yes."))

创建模板:

$ xgettext -d appname -kGetText -s -o appname.pot app/app.go

注意 -k,没有它就没有输出,因为 xgettext 不会识别对 GetText 的调用。在 appname.pot 中编辑相关字符串、电子邮件等。假设我们正在本地化法语:

$ mkdir -p locale/fr_FR/LC_MESSAGES
$ msginit -l fr_FR -o french.po -i appname.pot

编辑法语.po:

# Appname l10n
# Copyright (C) 2013 Wombat Inc
# This file is distributed under the same license as the appname package.
# Wombat <wombat@example.com>, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: appname v0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-01-13 11:03+1300\n"
"PO-Revision-Date: 2013-01-13 11:10+1300\n"
"Last-Translator: Rich <rich@example.com>\n"
"Language-Team: French\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

#: app/app.go:15
msgid "Yes."
msgstr "Oui."

生成二进制文件(实际会随应用一起部署的文件):

$ msgfmt -c -v -o locale/fr_FR/LC_MESSAGES/appname.mo french.po

最终目录结构:

~appname/
 |~app/
 | `-app.go
 |~github.com/
 | `~samuel/
 |   `~go-gettext/
 |     +locale/
 |     |-catalog.go
 |     |-domain.go
 |     `-mo.go
 |~locale/
 | `~fr_FR/
 |   `LC_MESSAGES/
 |    `-appname.mo 
 `-app.yaml

(go-gettext 下的 locale 目录保存测试数据,可以删除部署。)

如果一切顺利,访问 appname 应该会显示“Oui”。

【讨论】:

【参考方案2】:

go-i18n 是一个具有一些不错功能的替代包:

实现CLDR plural rules。 将text/template 用于带有变量的字符串。 翻译文件是简单的 JSON。

【讨论】:

go-i18n 的实现对于并发使用安全吗?这在这里很重要,因为 OP 希望在 go web 服务中使用它。我假设每个查询都需要使用由 cookie 为用户语言配置的 go-i18n 的新对象实例? @Frankenstein go-i18n 内部没有同步;但是,不需要同步。您应该在网络服务器的初始化阶段将翻译加载到 go-i18n 中;在 init() 期间或在您开始提供 http 请求之前。之后,状态只是被读取,所以不需要同步。【参考方案3】:

GNU Gettext 作为 i18n 解决方案的事实标准被广泛采用。

要直接使用 Go 项目中的 .po 文件并将所有翻译加载到内存中以获得更好的性能,您可以使用我的包:https://github.com/leonelquinteros/gotext

它相当简单直接。

因此,给定一个位于/path/to/locales/es_ES/default.po 的 default.po 文件(在 GNU gettext 之后格式化:https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html),您可以使用此包加载它并立即开始使用翻译:

import "github.com/leonelquinteros/gotext"

func main() 
    // Configure package
    gotext.SetLibrary("/path/to/locales")
    gotext.SetLanguage("es_ES")

    // Translate text from default domain
    println(gotext.Get("Translate this text"))

如果您希望在字符串中定义翻译以便更“集中”使用,您可以使用 Po 对象解析 PO 格式的字符串:

import "github.com/leonelquinteros/gotext"

func main() 
    // Set PO content
    str := `
msgid "One apple"
msgstr "Una manzana"

msgid "One orange"
msgstr "Una naranja"

msgid "My name is %s"
msgstr "Mi nombre es %s"
`

    // Create Po object
    po := new(Po)
    po.Parse(str)

    // Get a translated string
    println(po.Get("One orange"))

    // Get a translated string using variables inside the translation
    name := "Tom"
    println(po.Get("My name is %s", name))

正如您在最后一个示例中看到的,也可以在翻译字符串中使用变量。

虽然大多数解决方案都非常相似,包括您的解决方案,但使用通用格式作为 gettext 可以带来一些额外的好处。

此外,您的解决方案似乎不适合并发使用(当从多个 goroutine 使用时)。这个包为你处理所有这些。包也有单元测试,欢迎贡献。

【讨论】:

以上是关于使用 App Engine 进行 Go 的 I18n 策略的主要内容,如果未能解决你的问题,请参考以下文章

如何管理 App Engine Go 运行时上下文以避免 App Engine 锁定?

将许多 PropertyList 放入 Google App Engine 数据存储区(在 Go 中)并使用 Query.GetAll 再次加载它们

将 Google App Engine 应用程序从 Django 0.96 迁移到 Django 1.2

Golang、App Engine、通道和线程安全

OpenCensus 未在 Stack Driver 中的 Google App Engine 上显示跟踪

与 Google App Engine 连接的即时 XMPP 客户端