i18n 国际化多语言本质上就是先写好一堆映射,在根据想要的语言取对应的文字。
Golang 的 i18n 多语言方案网上查了一下,文章都讲的不太细致,而且代码看起来也不太好理解。
之前写 Python 代码时有使用过 pybabel 做多语言集成,通过命令生成 pot、po、mo 等文件,然后代码中通过函数取 msgid 就能动态获取到指定的语言,pybabel 实现原理其实就是对 GNU gettext 进行了封装。
gettext 是专门用来做多语言的,它由一系列命令行工具,比如 xgettext、msginit、msgmerge、msgfmt 等,像 linux 和 mac 默认都自带安装好了。
pybabel 使用流程主要是先提取代码中需要设置多语言的文字生成一个 .pot 格式的模板文件,然后根据这个模板创建对应语言的 .po 翻译文件,然后把 .po 文件编译成 .mo 文件就可以被函数动态读取。
当代码中的多语言文字修改或新增了,就需要再次生成一下新的 pot 模板文件,把新增的多语言文字加到模板中,然后讲新模板和之前翻译好的 po 文件进行合并,合并后原来的 po 文件会将本次新增的多语言加入进来,然后对其进行翻译,完成后编译新的 mo 文件。
以上这个流程在 gettext 命令工具中分别需要使用 xgettext 提取代码中的多语言生成模板、msginit 创建 po 文件、msgfmt 编译为 mo 文件、msgmerge 合并新的 po 文件。当拥有 po、mo 文件时,就能使用多种支持的语言进行读取了。
由于比较熟悉这一套流程,因此研究了一下在 golang 和其 web 框架比如 gin 中如何使用 gettext 实现多语言集成,这里记录一下要点。
golang 的 gettext 库也有好几个,这里我选择使用 gettext-go ,代码比较简洁而且支持 embed。唯一折腾了半天的是 mo 文件的路径问题,必要按固定规则才行,尝试了很多遍才成功。
在 Golang 中使用 gettext-go 实现 i18n 多语言
大致步骤如下:
1、 生成你的 po、mo 文件。
2、 必须先绑定你的 mo 翻译文件:gettext.BindLocale(gettext.New("domain", "path"))
这里的 domain 和 path 一定要注意命名,domain 你可以记忆为 mo 文件的名称,path 是存放你所有多语言的父目录路径,整个路径为 path/xx/LC_MESSSAGES/yy.mo
,
其中 path 就是 New 方法中的参数路径,xx 为任意名字标识语言,LC_MESSSAGES 路径必须要,yy 为 domain 参数值,只有这样才能读取到。
3、 完成绑定后调用gettext.SetLanguage("xx")
指定你要取的目标语言
4、 再调用gettext.Gettext
传入 po 文件中 msgid 的值就能返回对应语言的 msgstr
在 Gin 中使用 gettext-go 实现 i18n 多语言
golang 中获取多语言成功了,在 gin 中就容易了,你可以参考我的 Gin web 开发项目模版——pink-lady。使用中间件获取用户指定的或者请求头中可能的语言来设置目标语言,代码中的返回都使用 gettext.Gettext 获取即可,需要注意的时 gettext.Gettext 的参数必须有对应的翻译文字才行。
网上可以搜到的文章基本都是使用类似 gin-contib/i18n的实现方案,个人觉得使用起来不是很好用,因为在多语言处理时,使用 POEdit 这种专业翻译的软件可以很友好的完成人工的翻译 review,他可以提示你各种错误,类似于我们编程使用的 IDE。
获取用户的语言首先用户可以通过 url 参数指定语言,一旦指定语言,就将该语言保存到 cookie 中,下次从 cookie 获取,如果 cookie 没有则获取用户请求头中的 Accept-Language,可以使用 golang.org/x/text/language
这个包提供的 ParseAcceptLanguage
方法获取最匹配的语言作为用户语言。
相关阅读:Go 语言中处理多语言支持:如何实现语言与地区匹配
补充说明:目前 pink-lady 的语言切换方式和 gin-contrib/i18n 的实现是类似的,是通过请求头中的 Accept-Language 或 url querystring 参数来判断的,这对 i18n 功能的实现来说是完全满足要求的,但是对于 SEO 却不友好,同一个 url 有多种语言,内容相同但语言不同,搜索引擎它不好理解,这也是待优化的地方,而 SEO 良好的实现应该是通过 url 中的 path 来判断语言,比如 https://example.com/en/post/1,这样搜索引擎在判断本地化话时才能更加清晰。
中间件实现可参考 pink-lady GinSetLanguage 中间件设置 i18n 语言。
如果要在 html 模板中替换多语言,可以将 gettext.Gettext 注册到 gin 的自定义模板方法中,然后在 html 中所有需要被翻译的地方都使用这个模板方法处理一下,这样就能得到多语言。
模板注册参考 pink-lady 模板方法注册
需要注意的问题:
- 不要在一个请求处理时创建 gettexter,当你的翻译文件变大后,服务会卡死。
- 为每一种语言初始化一个对应的 gettexter,不要所有语言共用一个 gettexter,否则并发量上来后会出现语言错乱。
xgettext 自动提取代码中的待翻译的文字生成 pot 模板
通过 xgettext 无法直接提取 golang 代码和 golang html 模板中的翻译文字,解决办法是 sed 替换为 xgettext 能识别的字符,然后再提取。
比如 html 模板中的模板方法写法 {{ _text "翻译我"}}
无法被识别,可以替换为 c 语言版本的 gettext("翻译我")
后在提取为 html.pot:
find . -name "*.html"
| xargs perl -pe "s/{{\s*_text [\"\`](.+?)[\"\`]\s*}}/{{ gettext(\"\1\") }}/g"
| xgettext --no-wrap --no-location --language=
│ c --from-code=UTF-8 --output=html.pot -
go 文件中也无法识别,同样操作替换后再提取为 go.pot
find . -name "*.go"
| xargs perl -pe "s/gettext.Gettext/gettext/g"
| xgettext --no-wrap --no-location --language=c --from-code=UTF-8 --output=go.pot -
然后再把两个 pot 合并为一个 pot 文件(messages.pot):
xgettext --no-wrap --no-location *.pot -o messages.pot
最后我们统一使用这个 pot 生成各种语言的翻译文件后编译即可。