WeTalk 是如何使用 beego 并实现实时本地化的?

什么是 WeTalk?

WeTalk,作为一款当下比较流行的轻论坛类型程序,是目前为止使用 beego 开发的最重量级开源产品。该产品是由 beego 开发团队成员 slene 创建并维护的,因此可以说 WeTalk 涵盖了当下 beego 所有的高级用法,是一个学习使用 beego 的最佳实例。

什么是实时本地化?

本地化,简单地讲就是程序提供一个多语言的用户界面。那么什么是实时本地化?顾名思义,就是可以随时切换用户界面所使用的语言,像许多国际站点就提供多语言的选项并即时切换,而不是像一些桌面软件,你必须为多个语言分别下载不同的版本。

第三方包

在切入正题之前,先给大家介绍一下本文所涉及到的本地化包:beego/i18n。该包同样是由 beego 开发团队创建并维护的,代码简洁但功能完整,支持包括 代码级模板级 这两种本地化方式。另外,由于底层采用 goconfig 来完成对 INI 格式配置文件的操作,因此包本身的代码非常简洁。

本地化文件

beego/i18n 采用 INI 配置文件来作为本地化文件的格式,每种语言一般都有一个相对应的文件。例如,WeTalk 就为了支持英文与简体中文而拥有 两个本地化文件。作为一个命名习惯,建议以 locale_.ini 来作为本地化文件的文件名。这样做的一个明显的好处就是,可以使用一个循环来完成对所有本地化文件的加载:

setting/conf.go

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

base/base.go

// baseRouter implemented global settings for all other routers.
type BaseRouter struct {
    beego.Controller
    i18n.Locale
    User    models.User
    IsLogin bool
}

如此一来,就可以在接受到用户请求的第一时间获取用户所偏好的语言并进行设置,以便在逻辑中使用本地化的功能。

下面的代码演示了如何分别根据 URL 参数、Cookies、浏览器偏好语言和默认语言对每个请求进行语言选项的设置:

base/base.go

// 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.LocaleTr 方法即可:

func (l Locale) Tr(format string, args ...interface{}) string

该方法的第一个参数接受一个格式化字符串,紧接着便是格式化时需要用到的参数,也就是说,你可以在本地化文件使用像下面这样的写法:

seconds_ago = %d seconds ago

下面的代码演示了如何在控制器方法中进行本地化处理:

base/base.go

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 来指明目标语言。

当然,要想在模板中使用自定义函数,就必须先进行注册:

modules/utils/template.go

beego.AddFuncMap("i18n", i18nHTML)

这里的 i18nHTML 其实是对 i18n.Tr 的一个封装:

modules/utils/template.go

// 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 变量吗?是时候派上用场了:

views/article/show.html

{{i18n .Lang "post.post_author"}}

在模板中处理本地化,就是这么简单。

不同页面的同个关键词

你应该已经注意到,上个小节中的 "post.post_author" 写法有点奇怪,为什么中间会有一个 . 呢?这其实是 goconfig 提供的一个分区功能,也就是说,针对不同的分区,你可以为想用的关键字分别采用不同的翻译字段。

下面的写法就是采用了 INI 配置文件的分区特性:

conf/global/locale_en-US.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 包就很出色地完成了这项任务。

其它案例

本文来自:无闻的博客

感谢作者:无闻

查看原文:WeTalk 是如何使用 beego 并实现实时本地化的?

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。