实现多国语言

大概一般的软件设计都需要至少支持两种以上的语言, 但是上一个软件中的多国语言设计一直是我心中的遗憾。今天刚看好看完“3D数学基础”, 学DX又提不起太大致, 于是决定重新写一个多国语言的demo也算弥补了上次的遗憾。

因为第一次设计多国语言的实现, 在设计中踩了许多坑,甚至于后来只能去切换主UI上的所有按钮的字符, 而所有对话框的UI则永远都英文状态。 除了鸡肋我实在想不到更能形象说明这个功能的形容词了。

设计之初的功能定义是可以让程序在运行过程中自由切换多国语言。

为了实现这个功能, 让每一个非模态对话框(包括主UI)都实现一个函数叫OnChangeLanguage(const char *locale)的函数。 当语言菜单被切换到其他语言时, 主UI循环调用所有已被创建的非模态对话框的OnChangeLanguage函数, 让每个对话框自行去语言文件中找到自己需要的相应的语言字符串。

模态对话框则要更特别一些, 因为只要模态对话框处于active状态,他的父UI则全部处于挂起状态。 于是定义所有的模态对话框都需要提供一个public的变量叫m_szCurrLanguage, 在此模态对话框被调用DoModal之前, 此变量会被其父窗口设置为当前的语言。 然后模态对话框则在Initialize的时候根据当前的m_szCurrLanguage自行去语言文件中取得相应的控件字符串去显示出来。

每个语言文件都采用了一个独立的Windows下的ini文件格式进行存储, 切换不同的语言时, 去不同的ini文件中索引。 ini文件的格式 以id=string的方式存储, id是指需要这个字符串的控件ID, string是这个控件需在显示的字符串。

事实上制定出这样一套规则, 坏的味道已经出来了, 但我还沉浸在多国语言的高大上之处而混然不知。


现在重新看来, 在软件运行过程中, 动态切换语言根本就是不必要的, 这是一个过度设计。在拿掉这个过度设计之后重新去审视这个需要, 就会发现整个实现很简单明了。

首先实现一个多国语言模块, 大概有init, exit, get三个接口。

init函数用于在程序运行时,传入当前语系。
exit函数用于在程序退出时, 释放语言文件的资源。
get函数则用于返回某字符串在当前语系下对应的字符串。

不管是模态对话框还是非模态对话框, 在显示之初肯定都会调用Initialize函数去初始化一些控件等操作, 而且这个Initialize在此对话框的生命周期内只会被一次。

那么只需要在Initialize函数中通过多国语言模块将字符串转换为要显示的字符串, 并用来将控件初始化问题就OK了。

用id=string的方式去定义语言文件会有很大的局限性, 比如重复ID无法处理, 大量重复字符串浪费资料, 某些log无法被翻译等。

所以语言文件应该被定义为类似字典的机制。同样采用每种语言一个语言文件的方式, 那么语言文件的格式可以使用string_src=string_dst的格式去定义。

多国语言模块的init函数的功能其实就是将当前程序所需要的语言文件加载, 并按照某种格式在组织在内存中。 get函数的功能就是去快速从源字符串索引到需要显示的字符串。

因为翻译功能被抽象成了一个单独的模块, 那么一旦发现此模块效率不足, 只需要去重写或优化这个模块即可, 并不需要去动到动到调用此模块的所有对话框。

多国语言模块可以用hash或某种排序算法进行排序以加速查找以及采用atom的方式来存储翻译文本节省空间。

幸运的是,lua虚拟机的全局表的效率已经足够好, 而且lua虚拟机本身并不大, 完全可以对lua虚拟机做一个简答的封装来实现多国语言模块。

试了一下大概只花了几十行代码就将lua虚拟机封装成了一个多国语言模块。

lua编码风格

最近都是在看lua代码, 并在其基础上进行修改和增加功能. 在代码中看到了不少个人感觉很不好的现象, 就忍不住吐槽一下.

lua做为一门动态语言, 其弱类型及灵活性, 的确大大加快了开发和修改的效率. 但是这种自由有时不加以限制的使用, 有时候可能会造成很严重的后果.

先以我有限的理解说一下lua语言对于访问控制的有限支持.

定义变量或函数只有加上local才代表局部变量或函数, 否则只要这个模块被加载就可以被其他模块访问.
require代表要去加载某个模块, 如果两次调用require去调用同一模块并不会造成同一模块的多次加载.
lua5.1之后加入的module(…,seeall)函数可以自动导出lua函数中的非local函数及变量

在我所看到的代码中到处都是大文件(大的都有1W多), 魔数, 全局变量, 循环依赖, 如果某一模块已经加载并不会再去调用rquire函数等各种问题.

全局变量的存在使得看似将程序分了多个模块, 但是由于全局变量可以被所有模块相互访问和修改, 全无依赖层次可言. 最后不得不将这些模块化为一个整体模块来使用.
魔数让看代码的人搞不情作者意图, 还多了许多可能修改错误的机会.
并没有显式调用require会导致实在看不情模块之间的依赖关系, 对于把握整个程序的结果来看非常不利.
单个文件1W行的代码就像是一大滩稀泥放在那, 让你敢看不敢碰.


虽然lua给予的代码访问限制很少, 甚至于还提供了module(…,seeall)这样方便的函数. 但是如果我们不加限制的去使用只会使程序成为一滩无法维护的烂泥.

《C Programming Language》中有这样一句话 “…限制不仅提倡了经济性, 更在某种程序上提倡了设计了优雅”.

在参考了网上大神的lua源码和《lua程序设计》以后, 我觉得可以采用部分限制来提供代码的可读和可维护性.

require函数的作用就是去加载一个没有被加载过的模块, 并将此模块的返回值记录下来, 作为每次调用require函数的返回值来使用. 那么便可以在require函数上来做文章.

首先除非有必要, 应该舍弃module(…,seeall)函数的调用, 采用返回表的方式来返回某个模块的所有导出接口. 代码如下:

--bar.lua

local bar = {}

local v1 = 3
local v2 = 4

function bar.test()
print("----bar.lua---, v1, v2", v1, v2)
end

return bar

--foo.lua
local bar = require("bar")
bar.test()

虽然相比module(…,seeall)函数来讲, 代码中可以要多打几个字. 但是相对于这种方式提供的好处来讲却是巨大的.

因为采用require函数的返回值来调用相应的模块函数, 那么就限制了只要模块有依赖就必须去显式调用require.
就算不小心写漏了local, 只要不会有意在bar表中去赋值, 那么外部模块也并不能去访问bar模块中的v1和v2变量

由于lua中字符串管理是采用类似《C接口与实现》中的atom的管理方式, 因此字符串比较与整数比较的效率几乎是一样的.
那么其实可以通过直接用字符串传入参数或采用下面这种方式来避免魔数.

--foo.bar

local foo = {}
foo.state = {ONE = "the first step of state machine", TWO = "the step is do something"}

foo.test(step)
if(step == foo.state.ONE) then
dosomething()
elesif (step == foo.state.TWO) then
dosomething()
end
end

毫无疑问我更偏爱上面的方式, 即省了注释, 还能有与使用magic number一样的效率, 在调用此函数时, 还可以减少击键次数.

当然这些限制, 只能某种程序上解决一定的问题, 提高了代码的可读性. 但是像循环依赖, 刻意的全局变量等设计问题, 是无法靠变这些限制来解决的.