概述
作为开发者,有时需要开发出支持多国语言、国际化的 Web 应用。国际化是将针对某个地区设计的程序进行重构,以使它能够在更多地区使用,本地化是指在一个面向国际化的程序中增加对新地区的支持。
目前 Go 语言的标准包没有提供对 i18n 的支持,可以使用第三方库来实现。
设置默认地区
什么是 Locale
Locale 是一组描述世界上某一特定区域文本格式和语言习惯的设置的集合。
locale 名通常由三个部分组成:
第一部分,是一个强制性的,表示语言的缩写,例如 "en" 表示英文或 "zh" 表示中文。
第二部分,跟在一个下划线之后,是一个可选的国家说明符,用于区分讲同一种语言的不同国家,例如 "en_US" 表示美国英语,而 "en_UK" 表示英国英语。
最后一部分,跟在一个句点之后,是可选的字符集说明符,例如 "zh_CN.gb2312" 表示中国使用 gb2312 字符集。
设置 Locale
-
通过域名设置 Locale
采用域名分级的方式来设置 Locale, 例如,采用
www.asta.com
当做英文站(默认站),而把域名www.asta.cn
当做中文站。这样通过在应用里面设置域名和相应的 locale 的对应关系,就可以设置好地区。这样处理有几点好处:- 通过 URL 就可以很明显的识别
- 用户可以通过域名很直观的知道将访问那种语言的站点
- 在 Go 程序中实现非常的简单方便,通过一个 map 就可以实现
- 有利于搜索引擎抓取,能够提高站点的 SEO。
实现域名的对应 locale 的代码如下:
if r.Host == "www.asta.com" { i18n.SetLocale("en") } else if r.Host == "www.asta.cn" { i18n.SetLocale("zh-CN") } else if r.Host == "www.asta.tw" { i18n.SetLocale("zh-TW") }
通过子域名来设置地区,例如 "en.asta.com" 表示英文站点,"cn.asta.com" 表示中文站点。
实现代码如下所示:prefix := strings.Split(r.Host,".") if prefix[0] == "en" { i18n.SetLocale("en") } else if prefix[0] == "cn" { i18n.SetLocale("zh-CN") } else if prefix[0] == "tw" { i18n.SetLocale("zh-TW") }
一般开发 Web 应用的时候不会采用这种方式,因为首先域名成本比较高,开发一个 Locale 就需要一个域名,而且往往统一名称的域名不一定能申请的到,其次开发者不愿意为每个站点去本地化一个配置。
-
从域名参数设置 Locale
目前最常用的设置 Locale 的方式是在 URL 里面带上参数,例如
www.xxx.com/hello?locale=zh
或者www.asta.com/zh/hello
。这样就可以设置地区:i18n.SetLocale(params["locale"])
。如果希望 URL 地址看上去更加的 RESTful 一点,例如:
www.asta.com/en/books
(英文站点) 和www.asta.com/zh/books
(中文站点),这种方式的 URL 更加有利于 SEO,而且对于用户也比较友好,能够通过 URL 直观的知道访问的站点。那么这样的 URL 地址可以通过 router 来获取 locale:mux.Get("/:locale/books", listbook)
-
从客户端设置地区
有时需要根据客户端的信息而不是通过 URL 来设置 Locale,这些信息可能来自于客户端设置的喜好语言(浏览器中设置),用户的 IP 地址,用户在注册的时候填写的所在地信息等。
-
Accept-Language
客户端请求的时候在 HTTP 头信息里面有 Accept-Language,一般的客户端都会设置该信息。
下面是 Go 语言根据 Accept-Language 实现设置地区的代码:
AL := r.Header.Get("Accept-Language") if AL == "en" { i18n.SetLocale("en") } else if AL == "zh-CN" { i18n.SetLocale("zh-CN") } else if AL == "zh-TW" { i18n.SetLocale("zh-TW") }
-
IP 地址
根据相应的 IP 库,对应访问的 IP 到地区,目前全球比较常用的就是 GeoIP Lite Country 这个库。 -
用户 profile
让用户根据提供的下拉菜单或者别的什么方式的设置相应的 locale,然后将用户输入的信息,保存到与它帐号相关的 profile 中,当用户再次登陆的时候把这个设置复写到 locale 设置中,这样就可以保证该用户每次访问都是基于自己先前设置的 locale 来获得页面。
-
本地化资源
设置好 Locale 之后,就需要存储相应的 Locale 对应的信息(包括:文本信息、时间和日期、货币值、图片、包含文件以及视图等资源)。
本地化文本消息
实现方法是建立与语言相应的 map 来维护一个 key-value 的关系,在输出之前按需从适合的 map 中去获取相应的文本。
示例:
package main
import "fmt"
var locales map[string]map[string]string
func main() {
locales = make(map[string]map[string]string, 2)
en := make(map[string]string, 10)
en["pea"] = "pea"
en["bean"] = "bean"
locales["en"] = en
cn := make(map[string]string, 10)
cn["pea"] = "豌豆"
cn["bean"] = "毛豆"
locales["zh-CN"] = cn
lang := "en"
fmt.Println(msg(lang, "pea"))
fmt.Println(msg(lang, "bean"))
lang = "zh-CN"
fmt.Println(msg(lang, "pea"))
fmt.Println(msg(lang, "bean"))
}
func msg(locale, key string) string {
if v, ok := locales[locale]; ok {
if v2, ok := v[key]; ok {
return v2
}
}
return ""
}
执行以上代码,控制台输出:
pea
bean
豌豆
毛豆
上面示例演示了不同 locale 的文本翻译,实现了中文和英文对于同一个 key 显示不同语言的实现。
上面的示例代码仅用以演示内部的实现方案,而实际数据是存储在 JSON 里面的,所以我们可以通过 json.Unmarshal 来为相应的 map 填充数据。
本地化日期和时间
因为时区的关系,同一时刻,在不同的地区,表示是不一样的,而且因为 Locale 的关系,时间格式也不尽相同,例如中文环境下可能显示:2012年10月24日 星期三 23时11分13秒 CST,而在英文环境下可能显示: Wed Oct 24 23:11:13 CST 2012。这里面我们需要解决两点:
- 时区问题
- 格式问题
$GOROOT/lib/time 包中的 timeinfo.zip 含有 locale 对应的时区的定义,为了获得对应于当前 locale 的时间,首先使用 time.LoadLocation(name string) 获取相应于地区的 locale,比如 Asia/Shanghai 或 America/Chicago 对应的时区信息,然后再利用此信息与调用 time.Now 获得的 Time 对象协作来获得最终的时间。
package main
import (
"fmt"
"time"
)
var locales map[string]map[string]string
func main() {
// 设置语言
locales = make(map[string]map[string]string, 2)
en := make(map[string]string, 10)
locales["en"] = en
cn := make(map[string]string, 10)
locales["zh-CN"] = cn
lang := "zh-CN"
// 设置地区
en["time_zone"] = "America/Chicago"
cn["time_zone"] = "Asia/Shanghai"
loc, _ := time.LoadLocation(msg(lang, "time_zone"))
// 设置时间
t := time.Now()
t = t.In(loc)
en["date_format"] = "%d-%d-%d %d:%d:%d"
cn["date_format"] = "%d年%d月%d日 %d时%d分%d秒"
fmt.Println(date(msg(lang, "date_format"), t))
}
// 格式化 时间
func date(fomate string, t time.Time) string {
year, month, day := t.Date()
hour, min, sec := t.Clock()
return fmt.Sprintf(fomate, year, month, day, hour, min, sec)
}
//输出语言
func msg(locale, key string) string {
if v, ok := locales[locale]; ok {
if v2, ok := v[key]; ok {
return v2
}
}
return ""
}
执行以上代码,控制台输出:
2019年7月29日 17时12分58秒
本地化货币值
各个地区的货币表示也不一样,处理方式也与日期差不多。
示例:
package main
import (
"fmt"
)
var locales map[string]map[string]string
func main() {
// 设置语言
locales = make(map[string]map[string]string, 2)
en := make(map[string]string, 10)
locales["en"] = en
cn := make(map[string]string, 10)
locales["zh-CN"] = cn
lang := "zh-CN"
// 设置货币
en["money"] = "USD %d"
cn["money"] = "¥%d元"
fmt.Println(money_format(msg(lang, "money"), 100)) //¥100元
}
// 输出语言
func msg(locale, key string) string {
if v, ok := locales[locale]; ok {
if v2, ok := v[key]; ok {
return v2
}
}
return ""
}
// 格式化 money 格式
func money_format(fomate string, money int64) string {
return fmt.Sprintf(fomate, money)
}
执行以上代码,控制台输出:
¥100元
本地化视图和资源
根据 Locale 的不同来展示视图,这些视图包含不同的图片、css、js等各种静态资源。
文件目录:
views
|--en //英文模板
|--images //存储图片信息
|--js //存储JS文件
|--css //存储css文件
index.tpl //用户首页
login.tpl //登陆首页
|--zh-CN //中文模板
|--images
|--js
|--css
index.tpl
login.tpl
根据以上目录结构,实现代码如下:
/goweb/src/i18n/view.go
package main
import (
"html/template"
"os"
)
var locales map[string]map[string]string
type View struct {
Lang string
}
func main() {
// 设置语言
locales = make(map[string]map[string]string, 2)
en := make(map[string]string, 10)
locales["en"] = en
cn := make(map[string]string, 10)
locales["zh-CN"] = cn
lang := "zh-CN"
s1, _ := template.ParseFiles("./src/i18n/views/"+lang+"/index.tpl")
VV := View{lang}
s1.Execute(os.Stdout, VV)
}
index.tpl 里面的资源设置如下:
/goweb/src/i18n/views/zh-CN/index.tpl
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>视图</title>
<link href="views/{{.Lang}}/css/bootstrap-responsive.min.css" rel="stylesheet">
</head>
<body>
<script type="text/javascript" src="views/{{.Lang}}/js/jquery/jquery-1.8.0.min.js"></script>
<img src="views/{{.Lang}}/images/btn.png">
</body>
</html>
执行以上代码,控制台输出:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>视图</title>
<link href="views/zh-CN/css/bootstrap-responsive.min.css" rel="stylesheet">
</head>
<body>
<script type="text/javascript" src="views/zh-CN/js/jquery/jquery-1.8.0.min.js"></script>
<img src="views/zh-CN/images/btn.png">
</body>
</html>