大概一般的软件设计都需要至少支持两种以上的语言, 但是上一个软件中的多国语言设计一直是我心中的遗憾。今天刚看好看完“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虚拟机封装成了一个多国语言模块。
非常好,第二种设计很好,和android里面的相似,他为每一种语言添加一个文件,然后运行的时候去相应的文件去找,也是key-value形式存储,根本不会动态切换的,都是加载一次。