Golang 简单的国际化多语言支持思路

语言: CN / TW / HK

前言

在我之前的软件开发过程中,无论是开源软件还是公司的项目,我在程序中输出的提示多是蹩脚的英文,这大概是代码多用英文,习惯使然,但这对使用者或许并不好,后来在公司的项目中,我们大抵会使用英文和中文重复一段信息,然后不论是中文用户还是英文用户也都能简单的阅读。

这通常是不错的,但最近老板们想要推进大库研发,也就是很多人在一个很大的仓库上进行协助,这不仅需要代码托管后端的架构改造,也需要客户端工具的支持,还有一些其它组件的协作,与代码托管的服务端客户端的工作主要就落在了我的身上,既然要做客户端,那么客户端的多语言要不要做,怎么做?

自大约十年之前,我在大学寝室使用 Win32 编写 Wifi 热点开启助手的时候就接触过多语言,那个时候我使用 ini 存储对应语言的字符串,程序运行的过程中动态加载相关的字符串,这样就实现了多语言。实际上在 Windows 系统还存在一些其他的国际化支持方案,比如使用 Windows Resources 存储字符串,操作系统按照语言环境加载相关的翻译,另外这些资源还可以存在单独的 dll 中,新增的语言只需要添加该语言的翻译然后编译成 dll 分发即可,这种机制在 Windows 程序中被广泛运用。

如果要去了解国际化还可以看看 chromium, vscode 的国际化,这里篇幅有限,不做赘述。

国际化的思路

回过头来,如果要在 Golang 客户端程序中实现国际化,我应该怎么做?

首先,我目前想到的方案是,国际化本质上是字符串替换,那么比较简单的就是我们建立字符串映射表,在 golang 中,我们使用 map[string]string 就可以做到,其次,资源怎么存?golang 1.16 新增了 go embed 可以将资源嵌入到程序中,这样我们就可以把翻译文件嵌入到 golang 二进制文件中,然后使用 embed.FS 去读取,这样就可以根据实际的 locale 进行切换,这样不就实现了国际化么?

演示

为了证明我的思路正确,我花了一点时间写了个简单的项目 golang-i18n-demo

在这个项目中,在 Windows 中,我使用 GetUserDefaultLocaleName 获得当前的区域名称,在其他操作系统中,我使用环境变量获取当前的区域名称: var localeEnvs = []string{"LC_ALL", "LC_MESSAGES", "LANG"}

资源文件我使用 toml 存储,这种格式严格约束,有好的工具容易编写和格式化,格式化字符串可以直接存储到 TOML 的 引号键 中( 引号键遵循与基本字符串或字面量字符串相同的规则并允许你使用更为广泛的键名 ),TOML 文件的解析也比 JSON 高效,因此选择 TOML 格式存储是一个较好的选择。

在项目中,我们添加了如下多语言翻译:

# zh-CN.toml
"current os '%s'\n" = "当前系统为 '%s'\n"
ok = "好的"
cancel = "取消"

编写源代码:

package main

import (
	"fmt"
	"os"
	"runtime"

	"github.com/fcharlie/golang-i18n-demo/modules/locale"
)

func main() {
	os.Setenv("LC_ALL", "zh_CN.UTF8")
	_ = locale.DelayInitializeLocale()
	fmt.Fprintf(os.Stderr, "load ok='%v'\n", locale.LoadString("ok"))
	locale.Fprintf(os.Stderr, "current os '%s'\n", runtime.GOOS)
}

运行:

load ok='好的'
当前系统为 'windows'

进阶

这个演示足够简单,并未涉及多语言的切换,当然对于 cli 程序来说,多语言一般是一次初始化即可,对于 GUI 程序还需要实现切换机制,这个并不难,在更改语言后调用 DelayInitializeLocale,或者修改其参数即可。

对于 Web 服务,我们还可以同时加载所有语言翻译,按照用户请求的语言偏好选择相应的翻译即可。

另外,我们还可以给 Git 提交 PR,在 Push 过程中将 Locale 设置传递给服务端,改进服务端的提示。

总结

Windows 的资源字符串(Resources)存储在 PE 的 Resource Directory,golang embed 并不是如此,而是采取的更通用的方式(毕竟 ELF 和 Mach-O 没有类似的结构),这样带来的后果可能是运行内存的增加,但这些在现在的大内存环境下,影响并没有那么严重。