国际化和本地化 - Go Web 开发实战笔记

1,770 阅读7分钟

概述

作为开发者,有时需要开发出支持多国语言、国际化的 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。这里面我们需要解决两点:

  1. 时区问题
  2. 格式问题

$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>