谈到多语言开发,首先跃进我们脑海的
,
最简单,最直接的办法,就是在要显示语言相关字符的地方用个
If … Else
判断,
根据当前语言设置把相关字符串硬编码到程序中。其伪代码如下:
If language is English than
Show English
Else if language is Chinese
Show Chinese
这种方法简单,直接。但是遍布程序的
IF…Else
判断,不仅仅是让我们在每个判断处多写四五行代码。当我们面对几千,甚至几万个这样的判断得时候,随时可能漏掉其中的一部份,这会让我们你的程序看起来像这样:
Hello.
这是一个多语言程序的例子。
(此处本应是英文)
.
It demonstrates how to deliver a multilingual program.
如果我们不想让自己写出来的多语言程序变成像上面一样多语混合的怪物的话,很有必要这他进行改进,最小化其中重复的
If … Else
判断。怎样才能避免这种重复的判断呢?
改进还是障眼法?
我见过一个支持中(繁体)英文双语
MFC
程序的实现。它把所有的多语言相关的字符串放到资源文件的
String Table
中。然后通过一个
MultilingualLoadString
函数根据当前语言设置加载相应的字符串。该函数实现伪代码如下:
void
MultilingualLoadString(CString& str,UINT ID)
{
if( g_language == 'English')
str.LoadString(ID); //Load English string
else
str.LoadString(++ID); //Load Chinese string
}
这个
MultilingualLoadString
函数的实现看起来,
嗯,
有点像,魔术。是的,魔术。在看到结果或注释之前,你不会想到它的作用是加载不同语言版本的字符串。一个简单的
++ID
是怎样将相应的中英文字符串区分开来的?让我们来看看魔术背后的道具,
MFC
的
String Table.
图
1 - String Table
String Table
(
图
1 - String Table
)包含了三个字段,
ID, Value,
和
Caption. ID
其实是
Value
字段的一个别名
(
编译时替换为相应
Value )
,
CString
类的
LoadString
以
Value
的取值加载字符串。
MultilingualLoadString
的实现依赖于,加字符串到
String Table
的时候设置所有中文字符串的
Value
值比与其对应的英文字符串大
1
且只大
1
这样一个前提。
使用
方案
1
(我们把这一实现称为
方案
1
)后,添加新的语种看起来像是变得简单了。我们所要做的只有两件事:加一个
Else
分支和往
String Table
中添加新的字符串。但是,在我为这巧妙的实现举杯庆祝之前,再看看
String Table
和
MultiLoadString
所依赖的大前提——各种语言向对应字符串的
Value
值必须是连续的整数。这意味着往
String Table
添加字符串的时候,
我们必须显式指定每个字符串的
Value
值。当我们需要添加第三种语言的支持的时候,这一前提依赖导致的结果是——我们必须重新设定
String Table
里所有字符串的
Value
(如
图
1 - String Table
)……
这让我想起了小时候玩的一个游戏。从
1
开始数数,看能数到多大而不出错。现在,我们大家也一起来玩玩这个游戏。在继续往下读这篇文章之前,停下来,开口数数:
1
,
2
,
3
,
4
,
5
……然后记下你的结果(建议大家在后面的回复中列出自己的成绩,比比谁最厉害。如果你数到
500
还没出错,请马上联系我,我很愿意认识你)。
如果你刚才玩过这个游戏(如果没玩,请先玩这个游戏,之后再往下读)的话,相信你会有和我相同的看法——添加新语种的支持时,手动给
String Table
里的字符串编号出错的可能性比在源代码里硬编码字符串更高,需要更多的时间和成本来预防及测试。也就是说,
方案
1
并没有让事情变得简单,而是相反,让它更复杂了。怎样才能同时避免复杂的
If…Else
判断和手动字符串编号?怎样才能最小化在添加新语种是所需的改动?
从可以解决的小问题入手
暂且抛开手动字符串编码的复杂性不谈。所有的字符串放在同一个资源文件中,为资源文件的根新带来了很大的不便。为此,我们按字符串语言的不同将其分为两个资源文件。如
图
1 - String Table
可分为如下两个资源文件:
IDS_TEST 106 Test
IDS_YEAR 108 Y
IDS_MONTH 110 M
IDS_DAY 112 D
IDS_YES 114 Yes
File 1 Resources.en.rc
IDS_TEST 107
測試
IDS_YEAR 109
年
IDS_MONTH 111
月
IDS_DAY 113
日
IDS_YES 115
是
File 2 Resources.zh.rc
细心的读者可能已经注意到,拆分后,中文字符串的
ID
少了
“
_C”
,改为与相应的英文字符串
ID
相同。正是这一小小的改动,让另一个新的字符串加载方案变得可行。这个新的字符串加载方案是:根据当前语言设置从不同的文件加载,其实现伪代码
(
本文主要介绍多语言开发的方案,不对其
MFC
实现进行展开
)
如下:
String GetString(string, language)
{
If language is English
Load string from Resources.en.rc
Else
Load string from Resources.zh.rc
Return loaded string
}
方案
2
我们将这一实现称为
方案
2
。
方案
2
根据资源文件名的来区分不同语言版本的字符串,而不再是
方案
1
中的每个字符串对应的
Value
值,也就是说,
方案
1
中一直困扰我们的字符串排序问题也不复存在了。需要添加新的语种时,
方案
2
只需添加相应的资源文件同时在函数
GetString
里加一个
Else
分支即可。
相对于
方案
1
来说,
方案
2
是个激动人心的改进。但
方案
2
仍有它的不足之处。第一,需要添加新的语种时,仍需修改
GetString
函数。修改意味着重新编译,重新测试,有出错的风险,需要更大的工作量。一个好的设计应该是对修改封闭,对扩展开放的。第二,它只支持字符串的多语言化。一个多语言的项目语言相关的不仅仅是字符串,还有音频,位图等资源。针对以上两点不足,我们提出了下面的方案
3.
更上一层楼
为了使
方案
2
具有更大的可扩展性,我们将其实现为一个独立的
Resource
类。其主要成员如下:
class Resource
{
public Resource(string resFile)
{
this.resFile = resFile;
//set the resources file
}
public string GetString(string str)
{
//load string from resFile
}
public object GetObject(string obj)
{
//load other resources (eg. Bitmap
, audio
) from resFile
}
public string ResFile
{
get
{
return resFile; //return current resource file
}
set
{
resFile = value; //set the resource file at run time
}
}
private string resFile;
}
方案
3
Resource
类的资源加载函数
GetString
和
GetObject
根据成员变量
resFile
从不同的资源文件中加载。调用方只须简单的调用
Resource
类的成员函数
GetString
就可以加载特定语言的字符串(加载位图等其他资源可调用
GetObject
函数)。
同时,
resFile
的读写器提供了运行时更改资源文件的接口,以支持动态语言切换。
Resource
类在需要更新资源文件或添加新的语种时支持时无需任何代码改动。
君子善假于物
多语言开发并不是一个新鲜的话题,相反,它有很多现成的工具支持。本文主要介绍多语言开发的思路,但我并不提倡重新发明轮子。利用现有的工具,能有效地加快我们的开发速度,减少工作量,时间,和成本。
.NET Framework
提供了类似
Resource
类的
ResourceManager
类。
ResourceManager
类可以查找特定区域(语言)的资源。
通过其成员函数
GetString
和
GetObject
访问特定区域(语言)的资源。默认情况下,这些方法返回当前区域(语言)(由
Thread.CurrentThread.CurrentUICulture
和
Thread.CurrentThread.CurrentCulture
指定,
如果没有显式指定,则为当前操作系统设置
)
的资源。下面提供一个使用
ResourceManager
的例子,具体请参阅
MSDN
。
假设:
已经在项目中添加资源文件 MultilingualString.resx
和 MultilingualString.zh-CN.resx. “Hello”
为资源文件中的字符串。
using
System;
using
System.Collections.Generic;
using
System.Text;
using
System.Resources;
using
System.Reflection;
using
System.Globalization;
using
System.Threading;
namespace
MultilingualHello
{
class Program
{
static void Main(string[] args)
{
ResourceManager rm = new ResourceManager("MultilingualHello.MultilingualString", Assembly.GetExecutingAssembly());
CultureInfo ci = new CultureInfo("zh-CN");
Thread.CurrentThread.CurrentUICulture = ci;
string str = rm.GetString("Hello");
Console.WriteLine(str);
}
}
}
Sample 1 ResourceManager Sample