Docker容器的本地化

前言

Docker 容器多用于部署、运行程序或服务,像开发的 Web 应用之类的程序的本地化可以交给容器中运行的程序完成,毕竟用户不会直接操作容器中的 Linux 系统。从这点看,将 Docker 容器本地化似乎是多此一举,但 Linux 中的许多程序可能使用 Linux 的本地化库实现多语言功能,这时程序就需要使用 Linux 的本地化设置。如果想让这些程序在容器中显示正确的语言文字,就必须配置好容器中的 Linux。

本文将给出简单方便的解决方案,不需要安装 locales/locales-all 之类的包、甚至不用改动任何文件。

Linux 的本地化

Arch Linux 安装时,我们会修改 /etc/locale.gen 文件,取消掉想要安装的本地化语言的注释,然后使用 locale-gen 进行安装。最后在 /etc/locale.conf 中设置 LANGLC_ALL 等环境变量,就完成了本地化配置。

local-gen

下面是 locale-gen 命令的关键代码:

echo "Generating locales..."
while read locale charset; do \
	case $locale in \#*) continue;; "") continue;; esac; \
	is_entry_ok || continue
	echo -n "  `echo $locale | sed 's/\([^.\@]*\).*/\1/'`"; \
	echo -n ".$charset"; \
	echo -n `echo $locale | sed 's/\([^\@]*\)\(\@.*\)*/\2/'`; \
	echo -n '...'; \
        if [ -f $LOCALES/$locale ]; then input=$locale; else \
        input=`echo $locale | sed 's/\([^.]*\)[^@]*\(.*\)/\1\2/'`; fi; \
	localedef -i $input -c -f $charset -A /usr/share/locale/locale.alias $locale; \
	echo ' done'; \
done < $LOCALEGEN
echo "Generation complete."

locale-gen 逐行读取 /etc/locale.gen 文件,将每行的空格分割的两个值赋值到 localecharset 变量,其中 # 开头的行将被跳过。最后使用了 localedef 命令编译安装相应的本地化语言。

LANG 和 LC_ALL 环境变量

LANG 、LC_ALL 和其他 LC_ 开头的环境变量用于指明当前系统使用的本地化语言。LC_ALLLANG 是其他 LC_ 开头的环境变量的默认值,并且 LC_ALL 要优先于 LANG 。即,如果同时设置 LC_ALLLANG ,有效的将是 LC_ALL 。将它们写入到 /etc/locale.conf 可以在系统启动时被加载。

Docker 容器本地化的方法

理论上,我们只需对容器中的 Linux 进行相同操作即可。一些 Docker 基镜像并不会提供 locale-gen 命令和 /etc/locale.gen 配置文件。不过,前文我们已经得知 locale-gen 实际上使用 localedef 命令安装本地化语言,因此我们可以直接用 localedef 替代 locale-gen 命令。

但事与愿违的是,一些镜像不会包含本地化必要的文件。也就是说,即便使用 localedef 命令替代 locale-gen 命令,在一些容器中也无法成功。

localedef 编译安装本地化语言需要使用 /usr/share/i18n/locales//usr/share/i18n/charmaps/ 中的文件,上图的镜像中没有,所以无法安装。对于 Ubuntu 系统这些文件在 locales 包中(题外话,安装 locales-all 包就是安装所有本地化语言),使用 apt-get 安装这个包固然没问题,但不要忘了我们的宿主机或许有些什么可以拿来用用。

最终方案

看到这里,你可能会想到下面两个问题:

  1.  localedef 到底创建/修改了什么文件使得系统语言改变?
  2. 既然 /usr/share/i18n/ 目录挂载到容器可以正常的被 localedef 使用;那么如果将 localedef 创建/修改的这些文件挂载到容器相应目录,容器是不是可以直接支持宿主机的本地化语言?

回到 locale-gen 命令,上文贴出的 while 循环的代码前面有几行令人在意。

# Remove all old locale dir and locale-archive before generating new
# locale data.
rm -rf /usr/lib/locale/* || true

从注释可以看出,locale-gen 的运行似乎会在 /usr/lib/locale/ 生成文件。而 locale-gen 基本上只调用了 localedef 命令。事实上, /usr/lib/locale/ 确实是存放编译好的本地化数据文件, localedef 编译的本地化文件就放在该目录。

因此我们最终的、也是最简单方便的解决方法如下:

  1. 在容器中挂载宿主机的 /usr/lib/locale/ 目录
  2. 将宿主机的 LANG / LC_ALL 环境变量设置到容器上

当然,前提是你的宿主机配置好了中文本地化语言。

图形界面

通过一些参数可以在 Docker 容器中运行 X Window 客户端。使用上文的本地化方法可能还不能使 GUI 中的文字显示正常。你可能会看到许多方块,这一般是缺少中文字体文件造成的。同样,我们可以将宿主机的 /usr/share/fonts/ 目录挂载到容器中。

输入法

目前为止,运行在容器中的 GUI 程序可以正常显示中文了。但如果你的宿主机装有 fcitx 之类的输入法,你应该无法正常切换成中文输入。此时,在容器中设置 XMODIFIERS、QT_IM_MODULE 和 GTK_IM_MODULE 环境变量即可正常使用输入法。同样,如果宿主的这些环境变量设置正常,可以直接使用宿主的环境变量的值。

sudo docker run -it --rm -v $XAUTHORITY:/root/.Xauthority:ro \
    -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix:ro \
    -v /usr/lib/locale:/usr/lib/locale:ro -e LANG \
    -v /usr/share/fonts:/usr/share/fonts:ro \
    -e XMODIFIERS -e QT_IM_MODULE -e GTK_IM_MODULE \
    scottyhardy/docker-wine notepad

结果如下图,中文输入法可以正常切换出来。不过文字输入到记事本后显示为方块。在“字体”设置中,发现默认的字体是“System”。换成文泉驿字体后就如下图正常显示了。

其他问题

localedef 等命令及机制,都是建立在 glibc 库基础上的。由于 Docker 的 Alpine 镜像使用的 musl 库而非 glibc,因此本文的方法不能在 Alpine 镜像中直接使用。在 Alpine 中,许多依赖于 glibc 库的程序需要安装好 glibc 库才能正常使用。

如有谬误,望加指正。转载注明出处即可。