多语言示例

本地化与国际化

先解释两个名词,本地化国际化

  • 本地化 在软件工程中一般将外文界面的软件翻译为本土语言称为本地化,在中国也叫汉化。
  • 国际化 与本地化相反,一般将本土语言翻译成外文称为国际化

其实二者没什么本质区别,在软件工程上都是实现软件的多语言支持。

多语言要求

对c++程序,实现多语言的基础是资源化,所有需要实现多语言的字段,都应该存在于资源中而不应该以明文的方式写在代码中。

例如:

acutPrintf(_T("测试资源"));	// 错误

// 正确的示范:
CString str;
str.LoadString(IDS_STRING1);
acutPrintf(str);

一. 资源副本

实现多语言的方案有很多,我们先说一种最简单的实现方案,这种方案的实现方式是分别编译不同语言版本的程序。这种方案实现简单,我们就以一个简单的mfc程序为例,代码在 ML_Demo_MultiVersion里。

  1. 创建一个简单的MFC 对话框程序
  2. 创建不同的语言配置
  3. 为资源创建不同语言的副本
  4. 在资源ID上右键属性,为不同副本设置条件

注意,它们ID相同,但语言不同,条件也不相同(语言相同其实也没有关系)

原有语言也要设置条件,Condition

  1. 修改不同语言对应的资源内容
  2. 在项目上右键属性,修改资源配置

注意默认配置也要修改

  1. 现在分别运行两个配置可以看到效果
    Debug_enu:
    Debug:
  2. 总结:
    这种方式实现的多语言实现简单,只需要新增加配置,创建已有资源的副本就可以。但维护困难,适合已经完成的项目
    这种方式多语言的资源存在于同一个rc文件中,如果要对资源进行修改,必须同时修改多处,很容易遗漏,也没有太好的办法使用各种本地化工具进行维护。

二、翻译多个资源文件

资源副本存在于同一个rc文件中,不方便管理,我们的第二个方案,是使用不同的rc文件来管理资源。本示例代码在 MLDemo_MultiRc 文件夹中。我们还是以mfc对话框应用程序为例。

  1. 重复方案一中的步骤 1和2,创建 debug_enu配置。
  2. 复制资源文件
    在工程文件夹下,找到MLDemoMultiRc.rc,复制到同名文件夹下的 MLDemoMultiRc_en.rc

注意,在同文件夹中复制,它可以和原rc文件共用同一个resource.h

  1. 将新的资源文件加入工程
    在资源文件夹上右键添加现有项,添加新的资源文件。
  2. 修改资源
    在资源界面下选择英文资源文件,修改其中的资源使用的字体
  3. 为不同配置选择资源
    此时,编译项目会提示资源重复:

我们需要为不同的配置配置字体,在解决方案资源管理器视图分别选择两个rc文件,点击属性,为不同的配置排除不正确的字体:

注意这里是排除而不是选择,所以要为中文配置排除其它语言的资源,为英文配置排除中文资源。

  1. 编译运行

到目前为止,所有操作都是手动完成,与方案一并无区别。也存在当资源更改好维护不便的问题。但是由于不同语言的资源存在于不同的文件,我们可以借助工具。

三、 使用工具翻译多个资源文件

多语言工具有很多,我们在这里使用经典的 Lingobit Localizer 。在方案2的基础上优化:

  1. 打开 Localizer软件,新建项目

  1. 选择本地化的原始语言,我们是中文,目标语言是英文:

  1. 选择要本地化的文件 MLDemoMultiRc.rc

  1. 完成后可以看到资源文件中的所有条目:

  1. 我们忽略代码条目,将中文条目翻译成英文:

  1. 配置翻译文件目标路径:

  1. 生成目标文件:

点击创建或运行,选择要输出的语言。

此时可以看到已经生成了MLDemoMultiRc_en.rc文件

  1. 重新编译运行

  1. 当资源发生改变时,比如添加一个字符串

在Localizer中点击扫描:

可以看到新添加的资源已经被导入进来了。

  1. 总结
    使用工具可以方便的管理翻译资源,避免重复劳动,也避免了难维护的缺点。这种方式可以为不同的语言管理不同的资源。一般已经可以满足我们的需求。

四、资源dll

以上三个方案,都是直接将代码编译为不同的语言版本,需要分别向客户提供分发包,或者将程序分为多个语言目录,无法实现同一个程序加载、切换多个不同的语言版本。

要实现在同一个编译版本中实现多语言,就需要使用资源dll。核心思想是将资源编译为单独的dll。

仍然以mfc对话框工程类型为例,资源dll的示例代码存放在 MLDemo_ResDll中。

  1. 创建mfc对话框应用程序
  2. 使用Localizer 创建多语言资源
    这次与之前不同,我们添加两种目标语言: 繁体中文和英文
    生成位置设置在语言对应的子目录中。
  3. 切换当前翻译语言进行翻译

  1. 创建资源dll项目
    在解决方案中添加新建项目:
    选择动态链接库项目,项目名称与语言一致,这里是 zh-TWen并删除所有代码文件
  2. 使用Localizer翻译资源
    翻译后,资源文件刚好生成在两个对应的工程目录中
    将其添加到对应工程中
    此时编译工程会报错
    这是因为resource.h在主工程目录中,我们在包含目录中添加该目录

注意,是资源选项

  1. 还要设置dll入口点为无,否则会报错
2>LINK : error LNK2001: 无法解析的外部符号 __DllMainCRTStartup@12
2>W:\Work\gits\localizationdemo\MLDemo_ResDll\Debug\en.dll : fatal error LNK1120: 1 个无法解析的外部命令

  1. 配置项目生成路径,最终生成目录如下所示:
-- MLDemo_ResDll.exe
-- zh-TW
----- ML_Demo_ResDll.dll
-- en
----- ML_Demo_ResDll.dll

到现在为止,资源dll已经创建,下面应该修改代码来为不同的语言配置不同的资源

  1. 加载资源dll
HINSTANCE _hResInstance = nullptr;

BOOL LoadResDll(LPCTSTR szLan)
{
	if (nullptr != _hResInstance)
	{
		::FreeLibrary(_hResInstance);
		_hResInstance = nullptr;
	}
	
	
	CString strExePath;
	AfxGetModuleFileName(AfxGetInstanceHandle(), strExePath);

	TCHAR szDrive[_MAX_DRIVE];
	TCHAR szDir[_MAX_DIR];
	TCHAR szFName[_MAX_FNAME];
	TCHAR szExt[_MAX_EXT];
	_tsplitpath_s(strExePath, szDrive, szDir, szFName, szExt);

	CString strDir;
	strDir = szDrive;
	strDir += szDir;

	strDir += szLan;

	CString strResDll = strDir + _T("\\") + szFName + _T(".dll");

	_hResInstance = ::LoadLibraryEx(strResDll, nullptr, LOAD_LIBRARY_AS_IMAGE_RESOURCE | LOAD_LIBRARY_AS_DATAFILE);
	
}

为了测试,我们简单的依次弹出不同语言的对话框:

LoadResDll(_T("chs"));	// 其实没有这种语言,会加载失败
	if (nullptr != _hResInstance)
	{
		AfxSetResourceHandle(_hResInstance);
	}

	CMLDemoResDllDlg dlg;
	INT_PTR nResponse = dlg.DoModal();

	LoadResDll(_T("en"));	// 其实没有这种语言,会加载失败
	if (nullptr != _hResInstance)
	{
		AfxSetResourceHandle(_hResInstance);
	}
	
	nResponse = dlg.DoModal();

	LoadResDll(_T("zh-TW"));
	if (nullptr != _hResInstance)
	{
		AfxSetResourceHandle(_hResInstance);
	}

	nResponse = dlg.DoModal();

运行可以看到,三个语言版本的界面会依次弹出。

我们可以通过配置、注册表等方式在程序运行时,用于控制当前语言版本。