什么是 WeTalk?
WeTalk,作为一款当下比较流行的轻论坛类型程序,是目前为止使用 beego 开发的最重量级开源产品。该产品是由 beego 开发团队成员 slene 创建并维护的,因此可以说 WeTalk 涵盖了当下 beego 所有的高级用法,是一个学习使用 beego 的最佳实例。
什么是实时本地化?
本地化,简单地讲就是程序提供一个多语言的用户界面。那么什么是实时本地化?顾名思义,就是可以随时切换用户界面所使用的语言,像许多国际站点就提供多语言的选项并即时切换,而不是像一些桌面软件,你必须为多个语言分别下载不同的版本。
第三方包
在切入正题之前,先给大家介绍一下本文所涉及到的本地化包:beego/i18n。该包同样是由 beego 开发团队创建并维护的,代码简洁但功能完整,支持包括 代码级 和 模板级 这两种本地化方式。另外,由于底层采用 goconfig 来完成对 INI 格式配置文件的操作,因此包本身的代码非常简洁。
本地化文件
beego/i18n 采用 INI 配置文件来作为本地化文件的格式,每种语言一般都有一个相对应的文件。例如,WeTalk 就为了支持英文与简体中文而拥有 两个本地化文件。作为一个命名习惯,建议以 locale_
来作为本地化文件的文件名。这样做的一个明显的好处就是,可以使用一个循环来完成对所有本地化文件的加载:
func settingLocales() {
// load locales with locale_LANG.ini files
langs := "en-US|zh-CN"
for _, lang := range strings.Split(langs, "|") {
lang = strings.TrimSpace(lang)
files := []string{"conf/" + "locale_" + lang + ".ini"}
if fh, err := os.Open(files[0]); err == nil {
fh.Close()
} else {
files = nil
}
if err := i18n.SetMessage(lang, "conf/global/"+"locale_"+lang+".ini", files...); err != nil {
beego.Error("Fail to set message file: " + err.Error())
os.Exit(2)
}
}
Langs = i18n.ListLangs()
}
代码级本地化
要想在控制器中使用本地化,就需要先初始化每个请求的语言。为此,WeTalk 为每个控制器都添加了一个嵌入结构 i18n.Locale
:
// baseRouter implemented global settings for all other routers.
type BaseRouter struct {
beego.Controller
i18n.Locale
User models.User
IsLogin bool
}
如此一来,就可以在接受到用户请求的第一时间获取用户所偏好的语言并进行设置,以便在逻辑中使用本地化的功能。
下面的代码演示了如何分别根据 URL 参数、Cookies、浏览器偏好语言和默认语言对每个请求进行语言选项的设置:
// setLang sets site language version.
func (this *BaseRouter) setLang() bool {
isNeedRedir := false
hasCookie := false
// get all lang names from i18n
langs := setting.Langs
// 1. Check URL arguments.
lang := this.GetString("lang")
// 2. Get language information from cookies.
if len(lang) == 0 {
lang = this.Ctx.GetCookie("lang")
hasCookie = true
} else {
isNeedRedir = true
}
// Check again in case someone modify by purpose.
if !i18n.IsExist(lang) {
lang = ""
isNeedRedir = false
hasCookie = false
}
// 3. check if isLogin then use user setting
if len(lang) == 0 && this.IsLogin {
lang = i18n.GetLangByIndex(this.User.Lang)
}
// 4. Get language information from 'Accept-Language'.
if len(lang) == 0 {
al := this.Ctx.Input.Header("Accept-Language")
if len(al) > 4 {
al = al[:5] // Only compare first 5 letters.
if i18n.IsExist(al) {
lang = al
}
}
}
// 4. DefaucurLang language is English.
if len(lang) == 0 {
lang = "en-US"
isNeedRedir = false
}
// Save language information in cookies.
if !hasCookie {
this.setLangCookie(lang)
}
// Set language properties.
this.Data["Lang"] = lang
this.Data["Langs"] = langs
this.Lang = lang
return isNeedRedir
}
其中,isNeedRedir
变量用于表示用户是否是通过 URL 指定来决定语言选项的,为了保持 URL 整洁,WeTalk 在遇到这种情况时自动将语言选项设置到 Cookies 中然后重定向。
代码 this.Data["Lang"] = curLang.Lang
是将用户语言选项设置到名为 Lang
的模板变量中,使得能够在模板中处理语言问题。
以下两行:
this.Data["CurLang"] = curLang.Name
this.Data["RestLangs"] = restLangs
主要用于实现用户自由切换语言的按钮显示。
控制器本地化处理
由于我们已经在控制器中嵌入了一个 i18n.Locale
结构,因此在需要进行本地化处理的时候,只要调用 i18n.Locale
的 Tr 方法即可:
func (l Locale) Tr(format string, args ...interface{}) string
该方法的第一个参数接受一个格式化字符串,紧接着便是格式化时需要用到的参数,也就是说,你可以在本地化文件使用像下面这样的写法:
seconds_ago = %d seconds ago
下面的代码演示了如何在控制器方法中进行本地化处理:
if valid, ok := this.Data[errName].(*validation.Validation); ok {
valid.SetError(fieldName, this.Tr(errMsg))
}
模板级本地化
相对于代码级,模板级本地化显得更为常用,所以 beego/i18n 还专门提供了对模板中进行本地化处理的支持,即 Tr 函数:
func Tr(lang, format string, args ...interface{}) string
与之前的方法不同,该函数还要求传入一个参数 lang
来指明目标语言。
当然,要想在模板中使用自定义函数,就必须先进行注册:
beego.AddFuncMap("i18n", i18nHTML)
这里的 i18nHTML
其实是对 i18n.Tr
的一个封装:
// get HTML i18n string
func i18nHTML(lang, format string, args ...interface{}) template.HTML {
return template.HTML(i18n.Tr(lang, format, args...))
}
这样做主要是为了翻译结果不被 Go 语言的模板引擎作转义处理。当然,一般情况下你直接将 i18n.Tr
注册为模板函数也没有问题。
还记得之前在控制器语言选项设置方法里的那个 Lang
变量吗?是时候派上用场了:
{{i18n .Lang "post.post_author"}}
在模板中处理本地化,就是这么简单。
不同页面的同个关键词
你应该已经注意到,上个小节中的 "post.post_author"
写法有点奇怪,为什么中间会有一个 .
呢?这其实是 goconfig 提供的一个分区功能,也就是说,针对不同的分区,你可以为想用的关键字分别采用不同的翻译字段。
下面的写法就是采用了 INI 配置文件的分区特性:
[topic]
favorite_remove = Remove Favorite
favorite_add = Add Favorite
favorite_already = Favorited
[post]
post_new = New Post
post_edit = Edit Post
set_best = Set Best
remove_best = Remove Best
歧义处理
由于 .
是作为分区的标志,所以当您的键名出现该符号的时候,会出现歧义导致语言处理失败。这时,您只需要在整个键名前加上一个额外的 . 即可避免歧义。
假设你的键名为 about.
,为了避免歧义,我们需要使用:
{{i18n .Lang ".about."}}
来获取正确的本地化结果。
当前存在的不足
- 也许你已经发现,每次在模板中调用
i18n
函数,都需要传入.Lang
参数。其实,这是因为 beego 无法动态指定模板函数的实现,所以对应的翻译函数都必须在beego.Run()
之前设定。而对于那些允许在控制器中指定模板函数的框架(xweb)来说,则可以直接通过设定this.Tr
作为模板函数来省略.Lang
字段。
其它说明
- 如果未找到相应键的对应值,则会输出键的原字符串。例如:当键为
hi
并未在本地化文件中找到以该字符串命名的键,则会将hi
直接作为翻译结果返回给调用者。 - i18n 包提供一个 命令行工具 来帮助简化开发步骤。
总结
对于一个网站而言,能够支持实时的语言切换是一个对用户非常友好的行为。此外,不像命令行或其它类型的程序,网站的多语言支持很大程度上需要 i18n 包能够提供对模板级本地化的支持,而 beego/i18n 包就很出色地完成了这项任务。
其它案例
有疑问加站长微信联系(非本文作者)