基于 Go 语言构建在线论坛(八):消息、视图和日期时间本地化
前面两天因为其他事情耽搁了进度,今天开始恢复 Go Web 编程系列的更新。
我们接着上篇在线论坛的进度,由于之前所有页面和消息文本都是英文的,而我们开发的应用基本都是面向中文用户的,所以需要对项目进行本地化,今天正好借着这个入门项目给大家介绍下如何在 Go Web 应用中进行国际化和本地化编程,由于项目比较简单,我们只介绍消息提示、视图模板和日期格式的本地化,更多本地化实现留待后面本地化专题详细介绍。
注:本系列教程源码已提交到 Github:https://github.com/nonfu/chitchat,可以参照这里获取完整代码。
消息本地化
安装 go-i18n 扩展包
首先来看消息提示文本,消息提示文本通常包括表单验证消息、应用异常消息、接口响应消息等后端接口返回的消息字符串片段,关于这一块的本地化,可以借助 Go 官方自带的 golang.org/x/text
扩展包实现,这个扩展包扩展性好,但是上手起来有点复杂,所以今天学院君使用的是一款更容易上手的第三方扩展包 —— go-i18n。
在使用这个扩展包之前,先在项目根目录下运行如下命令下载相关的扩展包:
go get -u github.com/nicksnyder/go-i18n/v2/i18n
go get -u github.com/nicksnyder/go-i18n/v2/goi18n
下载完成后,我们可以运行 ls -l $GOPATH/bin | grep goi18n
确保 goi18n
命令已经在 $GOPATH
中了:
通过 go-i18n 自动生成翻译文件
接下来,我们来编写消息文本模板用于生成翻译文件。在这个项目中,只有一个消息提示文本,那就是访问的群组不存在时返回的 Cannot read thread
,因此,我们在项目根目录下创建 messages.go
,并基于 go-i18n
提供的类型编写消息模板如下:
messages.go
:
package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
var messages = []i18n.Message{
i18n.Message{
ID: "thread_not_found",
Description: "Thread not exists in db",
Other: "Cannot read thread",
},
}
其中 ID
是消息文本的唯一标识,Other
则是对应的翻译字符串(默认是英文),然后基于 goi18n
命令自动生成翻译文件到 locales
目录(执行前先创建 locales
目录):
mkdir locales
goi18n extract -outdir=locales -format=json messages.go
这样,就会在 locales
目录下生成可以被 go-i18n
包识别并解析的 JSON 格式翻译文件 active.en.json
:
编写中文版本翻译文件
然后,要进行本地化编程,可以在同级目录下创建并编辑 active.zh.json
用于存放消息文本的中文翻译:
是不是像极了 Laravel 框架中的 resources/lang
翻译目录的结构?其实原理都是一样的:
本地化配置初始化
回到在在线论坛项目,打开配置文件 config.json
,新增本地化目录和语言配置:
{
"App": {
...
"Locale": "locales",
"Language": "zh"
},
...
}
然后在 config/config.go
中新增与之映射的结构体字段,以及对应的初始化设置:
package config
import (
"encoding/json"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
"log"
"os"
"sync"
)
type App struct {
...
Locale string
Language string
}
...
type Configuration struct {
App App
Db Database
LocaleBundle *i18n.Bundle
}
var config *Configuration
var once sync.Once
// 通过单例模式初始化全局配置
func LoadConfig() *Configuration {
once.Do(func() {
file, err := os.Open("config.json")
if err != nil {
log.Fatalln("Cannot open config file", err)
}
decoder := json.NewDecoder(file)
config = &Configuration{}
err = decoder.Decode(config)
if err != nil {
log.Fatalln("Cannot get configuration from file", err)
}
// 本地化初始设置
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
bundle.MustLoadMessageFile(config.App.Locale + "/active.en.json")
bundle.MustLoadMessageFile(config.App.Locale + "/active." + config.App.Language + ".json")
config.LocaleBundle = bundle
})
return config
}
注意我们在 App
结构体中新增了一个 *i18n.Bundle
类型的 LocaleBundle
字段,用于存放全局本地化 Bundle 实例,并且在 LoadConfig()
方法中以单例模式初始化该实例。
在处理器方法中返回本地化消息
接下来,我们打开 handlers/helper.go
,在 init
方法中初始化 Localizer 以便被所有处理器方法使用:
package handlers
import (
...
"github.com/nicksnyder/go-i18n/v2/i18n"
. "github.com/xueyuanjun/chitchat/config"
)
var logger *log.Logger
var config *Configuration
var localizer *i18n.Localizer
func init() {
// 获取全局配置实例
config = LoadConfig()
// 获取本地化实例
localizer = i18n.NewLocalizer(config.LocaleBundle, config.App.Language)
...
}
...
最后在 handlers/thread.go
和 handlers/post.go
中调用 errorMessage
辅助函数的地方调用 Localizer 提供的方法对消息文本进行翻译并返回给用户:
if err != nil {
msg := localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "thread_not_found",
})
errorMessage(writer, request, msg)
} else {
...
}
测试消息本地化
重新启动应用,如果试图访问一个不存在的群组页面,就会返回如下中文提示信息:
说明我们的本地化翻译生效了,当然这里只是使用了 go-i18n
提供的最基本的功能,想要了解更多使用示例,可以参考如下链接:
视图本地化
所谓视图本地化指的是静态 HTML 视图模板的本地化,这里就不再适合使用消息文本翻译的方式实现了,最简单的方式就是为每个语言创建独立的视图模板进行本地化,然后在应用代码中通过读取全局配置、用户手动选择、客户端参数(比如 HTML 请求头中的 Accept-Language
字段)、或者域名信息来判断加载那种本地化视图模板,为了简化演示流程,这里我们使用全局配置的方式,也就是我们上面配置文件中设置的 Language
字段。
创建本地化视图模板
首先,我们在 views
目录下新增 en
和 zh
两个子目录,分别用于存放英文视图模板和中文视图模板,然后将原有视图文件移动到 en
目录下,并且在 zh
目录下创建每个视图模板的中文版本,以首页 index.html
为例,对应的中文版本如下:
其他中文视图模板也是类似,将其中的英文文本统一翻译成中文即可。
通过配置加载本地化视图
打开 handlers/helper.go
,在 generateHTML
方法中通过读取全局配置加载对应的本地化视图模板:
func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) {
var files []string
for _, file := range filenames {
files = append(files, fmt.Sprintf("views/%s/%s.html", config.App.Language, file))
}
templates := template.Must(template.ParseFiles(files...))
templates.ExecuteTemplate(writer, "layout", data)
}
非常简单,不再赘述。
注:同时移除
parseTemplateFiles
方法,并将调用该方法的地方调整为调用generateHTML
以避免维护两个地方。
测试视图本地化
重启应用,访问首页,即可看到页面视图已经都是中文显示了:
日期时间本地化
看起来都已经 OK 了,不过还有个小问题,那就是日期时间显示还是英文风格的,对应的实现代码在 models/thread.go
中:
func (thread *Thread) CreatedAtDate() string {
return thread.CreatedAt.Format("Jan 2, 2006 at 3:04pm")
}
我们当然可以直接修改这里来实现类似 2006-01-02 15:04:05
的日期时间格式(该时间节点是 Go 语言元年),不过,学院君这里换一种复杂一点的实现,以便顺手介绍下如何在 Go 视图模板中通过管道模式调用自定义函数。
将自定义函数应用到视图模板
打开 handlers/helper.go
,新增一个格式化日期时间的函数 formatDate
,然后在 generateHTML
方法中将这个函数通过 template.FuncMap
组装后再通过 Funcs
方法应用到视图模板中,这样,就可以在所有视图模板中通过 fdate
别名来调用 formatDate
函数了:
// 生成 HTML 模板
func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) {
var files []string
for _, file := range filenames {
files = append(files, fmt.Sprintf("views/%s/%s.html", config.App.Language, file))
}
funcMap := template.FuncMap{"fdate": formatDate}
t := template.New("layout").Funcs(funcMap)
templates := template.Must(t.ParseFiles(files...))
templates.ExecuteTemplate(writer, "layout", data)
}
...
// 日期格式化辅助函数
func formatDate(t time.Time) string {
datetime := "2006-01-02 15:04:05"
return t.Format(datetime)
}
注:由于 Go 语言是静态编译语言,因此无法做到像 PHP 那样和 HTML 一起混合编码,在 HTML 中嵌入 PHP 脚本可以调用任意 PHP 函数和方法,也正是因为 PHP 对 HTML 的天然友好性,从而得以在 Web 编程时代中大放异彩。
调用自定义函数格式化本地日期时间
然后我们在所有视图文件中将群组创建日期渲染调整为如下方式,即通过管道连接符的方式将 .CreatedAt
变量作为参数传入 fdate
并输出返回值:
{{ .CreatedAt | fdate }}
注意这里一定要使用 .CreatedAt
,这个变量才是 time.Time
类型,而 .CreatedAtDate
是字符串类型。
再次重新启动应用,访问首页和群组详情页就可以看到格式化后的本地日期时间格式了:
下篇教程,我们将演示如何部署 Go Web 应用作为在线论坛项目系列教程的收尾。
注:本系列教程源码已提交到 Github:https://github.com/nonfu/chitchat,可以参照这里获取完整代码。
5 条评论
定义error_message 函数名称, 使用改成了: errorMessage ?
时间本地化设置后出现报错了,
{{ .CreatedAt | fdate }} 错误消息 : 2020/05/15 17:33:15 http: panic serving 127.0.0.1:59188: template: index.html:12: function "fdate" not defined
是不是哪里没配置?
抱歉 这里应该是一致的 我可能是在 GoLand 提示命名风格的时候顺手改成了 errorMessage ?
渲染视图的时候需要把这个方法传递到模板,否则不能解析:
go get -u github.com/nicksnyder/go-i18n/v2/i18n 后 go: downloading github.com/nicksnyder/go-i18n/v2 v2.0.3+incompatible verifying github.com/nicksnyder/go-i18n/v2@v2.0.3+incompatible: github.com/nicksnyder/go-i18n/v2@v2.0.3+incompatible: reading https://mirrors.aliyun.com/goproxy/sumdb/sum.golang.org/lookup/github.com/nicksnyder/go-i18n/v2@v2.0.3+incompatible: