ResourceString与国际化(转)
如果您写的软件需要考虑到转换成为不同语言,那么由Borland C++ Builder EnterpriseEdition
所提供给您的多国语言翻译环境将是极为有帮助的。他可以很快的帮助您将各国语言版本制作出来。并且也提供动态的方式让您可以轻易的制作出您所需要的版本。甚至可以让您在不用变动程序代码本身的状态下,将GUI
的外观字型等进行适度的调整。除了多国语言接口的制作外,透过本文后面所叙述的技巧,将可以让您动态的切换不同的语系。这些BCB
都已经帮您处理得很好了,不过有些小细节您仍然需要注意,小心的处理或是避开,否则很容易的做不出来您想要的结果。
首先,我们先来看一下在设计程序时要注意的东西。
第一个版本务必使用英文。这点非常重要,其理由有二。第一点是通常比较好的文字翻译最好是由该国自己翻译。举例来说,要制作日文版本,当然是请地道的日本人来翻译会好的多。那么你认为懂得英文的日本人多,还是懂得中文的日本人多?同理,换成韩文,法文与西班牙文也是一样。第二点是由于在处理上,
BCB
所提供的环境通常比较能够正确的支持英文及该平台的语系。也就是说,您可以很容易找到可同时显示及处理英文及日文的平台,或者是英文及法文的平台。因为,没有一个语系的平台不能处理英文。可是你比较难找到一个可同时处理中文及日文的平台,或是同时处理中文及法文的平台。例外,还有一个理由未列在上面的是,在大部分的状况下,英文的句子长度比较长。
不要强制设定任何组件及Form的预设字形。这是因为每个平台系统所使用的预设字型及大小都有所不同。像是英文操作系统中预设字型是
Tohoma,但是中文及日文的windows操作系统就不是这样。使用的语系一定要使用正确语系的字型才有办法显示。所以,使用预设字形,可让AP字型随着系统自动调整为正确字形。或酗ㄗㄠo比较好看(
大部分非英语系Windows 操作系统的预设英文字型都很难看,不知理由为何?
),但至少是正确的。要做的这点很简单,确定您所有组件的属性中,只要有ParentFont这项的一定要强制设为true。尤其是Form的
ParentFont一定要设为true。否则你一定会在多国语言版本的过程中遇到big trouble 。相信我!否则我就不会写这篇文章来提醒你了。
别把要显示的字符串写死在程序里。像是这样: ShowMessage("Hello World!");或是 MainForm->Caption =
"Main Form" ; 理由很简单,写在程序里的字符串Borland C++
Builder的工具无法帮你处理他们,这样在翻译时,就得自己把所有的Source
Code搜寻一遍找出这些字符串。平常,随便写个小程序,这样的字符串就已经有十来个,您想想看,要是稍微大点的Project,这会有多恐怖?所以,我们要采用一个集中的方法,这个方法还要能够与Borland
C++ Builder的翻译工具相结合。或许您之前看过我写的那个使用Resource File
的StringTable的文章。但是,我要告诉你,忘了它吧。我在写那篇文章的时候没注意到Borland C++
Builder无法使用那个方法。也不是说那个方法是C++ Builder完全不能用,而是,如果您想要动态进行各国语系的切换,请放弃使用Resouce
File的StringTable。不过,就算不为这个理由,我也是建议你换成本文后面要介绍的Resourcestring的方式,理由是他比较方便使用。也比较像是C++
Builder的作法。不过,这个作法的缺点有二,一为要使用Pascal的语法。别担心,保证不要一分钟就学会。二是只有C++
Builder跟Delphi可用。其它的Compiler像是MS VC++就不能用啰。
写程序时,无论任何理由都不要让自己的程序绑死在某个语系上面。例如:你可能会想说要好看一点所以写成"92年2月2日"。可是,这样的东西在其它语言马上变成乱码。因此,基本的方式是使用国际通用的格式。当然最好的方式还是要能自动转换成为各国的常用表示方法。但是这个部分属于i10n的范畴,我还没研究过,有空再说啰。
现在,就让我们来看看这个Delphi语法的ResourceString要怎么用在C++
Builder中。其实,在多国语言上,Borland已经准备好了一个范例,放在$(BCB)\ExamplesAppsRichEdit。这个范例很重要的,他里面有些程序是我们后面介绍动态变换语系时要借来用用的。不过,这边我们先借他的ReConst.pas这个档案来用一下。您会看到这个档案有下面的内容:
unit ReConst;interfaceuses Windows;const ENGLISH =
(SUBLANG_ENGLISH_US shl 10) or LANG_ENGLISH; GERMAN =
(SUBLANG_GERMAN shl 10) or
LANG_GERMAN; FRENCH = (SUBLANG_FRENCH shl 10) or
LANG_FRENCH;resourcestring SUntitled = 'Untitled'; SPercent_s = '%s
- %s'; SSaveChanges = 'Save Changes?'; SConfirmation =
'Confirmation'; SNumberbetween = 'The number must be between 1 and
1638.'; SLcid = '1033'; { US = 1033; DE = 1031; FR = 1036
}implementationend.看到那个resourcestring了没?没错,就是这样!只要把那些resourcestring中的字符串换成你自己的定义就可以啰。稍微要注意的是这个字符串是使用单引号来括住的,因为他是Pascal语法嘛!例如,您可定义自己的字符串为:
resourcestring MyResString1 = 'My First resource string'
;你可能觉得SLcid那行有点看不懂,别担心!在那行的分号后面的字符串全是批注。因为,在Pascal中,大括号被当成批注符号使用。您也可以如法炮制的来为您的字符串加上批注。另外,const中定义的是常数,其型态为Word,也就是2
Bytes。值介于0-65535之间。所以,建议您一并将程序内用到的常数定义在此,这样比较符合集中管理的精神。最后,别忘了将第一行的Unit叙述换掉。不过,您要先为您自己的档案命名。建议您学ReConst.pas
的命名方式。例如,您的Project缩写是My,那么就叫他MyConst.pas吧。所以,"Unit ReConst;"也要改成"Unit
MyConst;"才行啰。好吧,完成后,请将这个pas 档案加入到你的Project 中。OK!光是这样是没用的。因为,C++ 需要有include
档案。那么,MyConst.pas的include档案从何而来?还好,C++ Builder有自动产生include
档案的能力,请在打开MyConst.pas后,按下Alt-F9,让C++
Builder单独compile它。成左尔陧A您就可看到MyConst.hpp这个档案的出现。理论上,您该在所有的cpp
档案中都会用到这个include档案才对。另外,别把这个include档案跟precompiledheader的技巧合用喔。因为他会为常数进行初始化,所以precompiled
header技巧遇到他就做不出来了。请个别在需要的程序文件中include这个header file就可以了。 OK!
接下来要怎么在程序内使用这些常数及Resource字符串呢?非常简单!!用就对了!举例来说,您要使用MyResString1。您可使用下面的方式:
ShowMessage(MyConst_MyResString1);是的!在您要用的Resource
string名称前面加个"MyConst_"的prefix。就可以了。当然,这个prefix完全是根据您Unit的定义而来。您可参考前面步骤所做出的include档案中,的namespace名称,就可看到啰。在了解了这个部分后,接下来就是要把你的程序写好啰。请注意,多国语言化的翻译务必在最后进行。而且是要在程序完成度已经相当高的时候才进行。否则,画面一下子变化太大的话,C++
Builder的多国语言环境也吃不消的。现在,假设您的程序已经完成了,接下来要如何才能够进行多国语言化呢?首先,您需要开启Resource DLL
Wizard来制作Resource DLL。你可在主选单中依照New->Others->New->Resource DLL
Wizard来开启它。接下来这个精灵程序会给您一些指示,并请您选择要制作的语系。
首先,您在精灵中遇到的第一个问题是精灵会询问你要对哪个Project 制作多国语言版本。把你要制作的Project
打勾。然后,继续后面的步骤。这边有一点要注意的是BCB 多国语言环境似乎有点问题,如果您的Project
放在一些中文目录中,可能会找不到,所以建议您的Project 要放在纯英文的路径内。接着,就是要选择您要转换的语系了。C++ Builder
的精灵会列出所有的语系让您选择。您可以一次把要做的语系全部选出来。也可以一次只选一些,下次要加入其它语系时,请重复这个步骤就可以了。别担心旧的语系会被局戚AC++
Builder
已经帮您处理好了。选好你的语系后按下下一步,您就会看到这些语系所对的目录。例如:中文就是CHT,简体是CHS,日文是JPN等等。接着,下一个画面,BCB
会问您有没有其它的要加入的处理的档案。如果要加入的应该都是dfm 檔或是rc档案。在接着下一步就是要产生语系了。C++ Builder
最后还会再列出一次要进行的语系给你看。
没问题的话,就按Finish啰。在最后列出所有语系的地方,有一栏为Update Mode 。如果您是第一次对这个语系建立Resource
DLL,那么这个字段就是Create New 。如果您之前已经建立过这个语系了,而且您又再次使用Resource
DLL来建立他。没问题,精灵会帮您处理好,所以您会看到Update Mode
中所显示的是Update。如果,您不想要只是Update而已,而是要重新建立新的,在Update上面点一下,一个combo box
会出现让您选择。最后都没问题的话,您就会看到精灵跟您说他需要重新编译您的程序以方便建立各国语言。在此同时,精灵会要求您将Project存成Project
Group。这是因为每个语系都是自己一个Project 。用成Project Group才能统一管理。对于这个Project
Group的名称我的建议是在Project名称后面加上"_i18n"的字尾。
当然,或许您会习惯在前面加上ML的前置词,代表Multiple Language。不过,我还是觉得用i18n好。存盘成功且Project
也重新Compile成功。这时,精灵会顺便帮您叫出翻译管理员(Translation Manager)。在这个Translation
Manager中,您需要使用其中的Workspace 画面来帮助您进行翻译的工作。Translation
Manager的用法不难,大家看看就会了。重点是除了字符串外,您还可透过翻译管理员调整组件大小,避免不同语系下,字符串被组件破坏的窘境。您随时都可透过主选单的View->Translation
Manager来叫出翻译管理员。只是每次翻译完,您都需要针对有修改过的语系重新编译。每个单独语系都会编译出自己的Resource DLL
。这些档案的名字就是Project的名字,加上以语系的名字作扩展名。所以,如果您有一个名为Project1.exe的档案。那么他对应的中文繁体语系档就是Project1.CHT。日文就是Project1.JPN。这些语系档案要与执行档放置于同一个目录下。至于我们前面所说的resourcestring,他会出现在各语系的Resource
script 中。您也是透过翻译管理员来编辑他们。
不过,在这里C++ Builder一直到6.0 update 2都有这么个Bug 。这个Bug
会导致您的resourcestring在切换语系时,不会被载入。好的,要如何解决他呢?简单的很,请开启你的Project
Mananger,将每个语系的Project加入一个名为XXX_DRC.rc的档案。其中,XXX就是你的Project名称。这个档案在每个语系自己的目录下都有一个,您需要为每个语系的Project
加入他。当然,别弄错目录,把不同的语系档案混在一起啰。如果您要设定这个Project在目前平台要使用何种语系,可利用Project->Languages->Set
Active设定预设语系。实际上,您的程序是根据下面这个Registry的值来决定要使用何种语系Resource DLL:
HKEY_CURRENT_USER\Software\Borland\Locales\[Exec file
name]其中"Exec file
name"就是你的执行文件名(含路径),这个Registry的值是您希望的语系的名称。例如,如果您需要这个软件启动时是中文版,那么这个Registry就应该填CHT
。其它以此类推。
由于,要显示哪个语系是透过上述的Registry所决定的,因此,您需要在安装程序中就正确的设定这个Registry的值。以便能够正确的显示正确的语系。
或阴z要问,难道没有办法自动判断系统语系,然后进行切换吗?有的!这就是我们下面所要介绍的,不过我们要介绍的是远比自动判断语系更厉害的,也就是动态切换语系。让您的使用者,可以动态的切换所要显示的语系。嗯!厉害吧!
好了,首先第一部要做的是把前面所提到C++ Builder
范例中的那个RichEdit目录找出来。接着,仔细找找,看下面是不是有个叫做reinit.pas的档案。有吧!没有的话,就去跟别人Copy吧。
把这个档案放到你的Project里面来。然后在您要让使用者动态切换的地方加上由这个档案所产生的include档案。例如:
#include "reinit.hpp"OK! 第一次,你的系统里面当然没有这个档案啰!请利用Compile Unit的方式产生他。
接着,我们就是要利用这个档案所提供的函式来帮助我们达到动态切换语系的效果。首先,假设您希望的是能够自动判断系统语系然后进行切换,那么找个合适的地方(通常是main
function的开头。)加入下面的程序代码:
if(LoadNewResourceModule(GetSystemDefaultLCID()))
{
ReinitializeForms(); }这样,便可正确的显示语系啰。上面这几行,其实就是C++
Builder自己偷偷在用的。所以,如果你有提供Resource
DLL,而且没有依照前面所说的在Registry中指定语系。那么你的程序就会自己依照抓取系统语系了。正因如此,我们只需要把GetSystemDefaultLCID()换掉,换成特定的语系就可以啰。所以,现在要做的就是如何制作出给特定语系LCID
呢?首先,在C++ Builder的include目录中,找到winnt.h 这个档案。找到LANG_
及SUBLANG_开头的定义宏。我们以法文为例。要做出法文的LCID,其作法如下:
LCID lcid = (SUBLANG_FRENCH << 10) | LANG_FRENCH
;这样就可以了,很明显LANG_
是在定义国家的语言。那么SUBLANG_呢?指的是地方语系。比如说,同样是中文,在LANG_CHINESE之下,就有这些SUBLANG:
#define
SUBLANG_CHINESE_TRADITIONAL 0x01
// Chinese (Taiwan) #define
SUBLANG_CHINESE_SIMPLIFIED
0x02 // Chinese (PR China) #define
SUBLANG_CHINESE_HONGKONG
0x03 // Chinese (Hong Kong S.A.R., P.R.C.)
#define SUBLANG_CHINESE_SINGAPORE
0x04 // Chinese (Singapore) #define
SUBLANG_CHINESE_MACAU
0x05 // Chinese (Macau
S.A.R.)全部使用上面的计算方式,就可组合出您要的LCID。然后再把他喂给LoadNewResourceModule(),就可加载正确的 Resource
DLL了。前提是那个Resource DLL 档案要存在喔。到此为止,您应该已经能够掌握C++ Builder
中多国语言的作法啰。最后再提醒您一点,一旦您对于Form上面或是resourcestring有所新增,删除,或是其它外观上的变动。务必要用
Project->Languages->Update Resource DLLs来更新您的Resource
DLL。并且重新编译出各语系的版本。千万不要让自己重复一直做这个工作,否则您会很累。
多国语言一定要等所有的画面都确定之后,甚至是经过相当程度测试之后再进行。也就是说,英文的 Release
版本没出来前,别做多国语言化。否则,一定会浪费掉数倍的时间。无论任何理由都一定要坚持这点。否则,请直接叫公司给您一个专门做多国语言化的小组,然后将Project
的时间延长一倍。
最后附带提醒一点!当您将英文的稿送去给别人翻译时,无论哪一国的语言,都请翻译人员坚守一个原则,翻译过后的字符串长度,不可超过原来英文的长度。除非那个字符串的翻译非常固定,例如像是Yes,
No, OK, Cancel
等。翻译人员如果跟你说做不到,那么就换个翻译。这点也很重要,因为单是『字译』,一个句子长度可能远超过原来的句子。这会对你的程序产生极大的困扰。如果,为了这个原因,你要针对各语系调整你的画面。那么,请直接将Project
的时间再延长一倍,看看有没有可能做完。务必跟翻译人员坚持这点!请他用『意译』的方式调整句子的长度。我们这里所说的长度是画面上看到的长度(单位为像点),不是指一个句子有几个字。如果你是在帮公司开发程序,建议你尽可能在一开始的时候就把多国语言的问题考虑进去。否则等到程序越写越大,才想到要改成多国语言时,就会非常痛苦了。
-----------------------------------------------------------------------
在Delphi编程的那段“古老”的日子里(就是在版本4之前),在程序中使用字符串有两个基本的方法。你可以使用字符串将它们嵌入到源程序中,例如:
MessageDlg( 'Leave your stinkin' mitts off that
button, fool!',mtError, [mbOK], 0);
或者,你可以创建一个文本文件(使用.rc扩展名),例如:
STRINGTABLE DISCARDABLE
{
1 "Dialog Expert"
2 "Dialog Expert from demonstration Expert DLL."
3 "Application Expert"
4 "Application Expert from demonstration Expert DLL"
5 "&Create"
6
"&Next"
7 "An application name is required!"
8 "The application name is not a valid identifier."
9 "The path entered does not exist."
10 MAIN.PAS"
11 "MAIN.DFM"
12 "MAIN.TXT"
...
// Variable names.
20 "StatusLine"
...
}
然后你需要做的工作是将它编译成为资源文件,加入到Delphi的工程或者单元中,使用命令行工具Brcc32.exe编译,然后在程序中当你使用到这些字符串时,使用LoadStr等函数将它们从资源中提取出来。这看起来有点麻烦,而且你可能会被困在这麻烦的操作中,因此你可能会无休止地将字符串加入到你的源代码中而不是使用资源。
现在,resourcestring关键字的出现,可以帮助你摆脱这麻烦的工作。resourcestring带给我们两个好处:可以简单地加入字符串,而且所有的字符串集中保存在同一个位置;同时,使用resourcestring提供更好的内存管理,因为所有在resourcestring部分的字符串是以资源形式保存在应用程序中。
让我们赶快进入使用resourcestring关键字的新世界,增加一个单元到你的工程中,名字是ResStrngs(或者其它名字),然后将所有的字符串(特别是那些将会被用户看到的字符串:列表的内容,错误消息等等)加入到这个单元的接口(Interface)部分,就像下面一样:
unit ResStrngs;
interface
resourcestring
// 著名的军事家
SGeneralElectric
= 'General Electric';
SGeneralMills
= 'General Mills';
SGeneralUsage
= 'General Usage';
SGeneralHospital
= 'General Hospital';
SGeneralLedger
= 'General Ledger';
SGeneralProtectionFault = 'General Protection Fault';
SGeneralSQLError
= 'General SQL Error';
SGeneralLeeSpeaking = 'General
Lee Speaking';
SCorporalPunishment
= 'Corporal Punishment';
SSgtFury
=
'Sgt. Fury';
SSgtCarter
=
'Sgt. Carter';
SSgtSchultz
= 'Sgt.
Schultz';
SSargentShriver
= 'Sargent Shriver';
SCaptKangaroo
= 'Capt. Kangaroo';
SCaptUnderpants
= 'Capt. Underpants';
SColonelKlink
= 'Colonel Klink';
SPrivateBenjamin
= 'Private Benjamin';
SPrivateProperty
= 'Private Property';
SLeftenantDan
= 'Leftenant Dan';
SMutineerChristian =
'Mutineer Christian';
SAtlantaHawks
= 'Atlanta Hawks';
// 友好的提示
SDontSleepInTheSubwayDarlin =
'Don't sleep in
the subway darlin'';
// 你还可以继续增加字符串
implementation
end.
在任何可能引用这些字符串的单元的实现(implementation)部分的uses语句中加入此单元。然后,你可以这样使用这些字符串:
if ItIsPetulasVirtualHusband and
HeIsLate then
MessageDlg(SDontSleepInTheSubwayDarlin,
mtInformation, [mbOK], 0);
还有一个例子:Borland/Inprise同样使用字符串资源,你可以看看..\source\vcl目录下的consts.pas、dbconsts.pas等文件。
将字符串集中放在resourcestring部分还有一个好处就是,通常程序员并不是最适合写用户将会看到的反馈信息或者错误消息的人,他们写的往往太技术性,例如:“模块xyz:生成子线程时发生一个未预期错误”,这对用户来说等于没说,也许“请在开始前保存修改”会更好一些。
将这些消息字符串单独保存在一个分离的文件中,可以让那些适合编写用户消息的人来处理(当然,要有程序员来当顾问,以便确定每条消息表示什么含义)。如果你不想让这些非编程人员来修改你的.pas文件,你可以将这些字符串保存到一个文件文件中交给他们处理,当他们处理完成后,你再将他们修改后的字符串复制到你的resourcestring部分。
最后但是也非常重要的是,当把用户或以看到的字符串都收集在一个地方,可以让你很容易地让你的应用程序国际化和本地化。使用Delphi的ITE(Integrated
Translation Environment
集成翻译环境),国际化和本地化你的应用程序的字符串几乎是半自动完成的。使用ITE,你可以为每种语言创建一个独立的.dll。如果你发布时带有多个.dll文件,通过程序运行的计算机系统的地区号,你的程序可以自动调用对应的.dll。
ITE主要的工具就是资源DLL向导(Resource DLL Wizard) (File | New |
Resource DLL Wizard) 和翻译管理器(Translation
Manager)。翻译管理器用于输入翻译的内容,可以查看Delphi的帮助文件的"Integrated Translation
Environment"部分获得具体信息。
除Delphin提供的ITE外,还有第三方的相关工具。我喜欢使用来自 "in the box." 的工具,当然,像ITE一样,只要他工作得好。
---------------------------------------------------------------------------------
如何运行时修改
假设在Project1.exe里有一個ID为100的Resouce
String,Value为"abc",想将abc改为123,可以试如下操作
procedure TForm1.Button2Click(Sender:
TObject);
var
h: THandle;
b: LongBool;
s: String;
s2: PWideChar;
iMemAlloc: Integer;
begin
s
:= Edit2.text;
iMemAlloc := Length(s) *
SizeOf(WideChar);
s2
:= AllocMem(iMemAlloc);
StringToWideChar(s,
S2, iMemAlloc);
h := BeginUpdateResource('Project1.exe',
false);
b
:= UpdateResource(h, RT_STRING,
MakeIntResource(100), LANG_NEUTRAL, s2,
iMemAlloc);
if
b then ShowMessage('a');
EndUpdateResource(h,
False);
end;
--------------------------------------------------------------------------
The better alternative to resourcestring
Instead of using
resourcestrings, there is a better alternative:
ShowMessage (_('Action
aborted'));
The _() is a pascal function inside gnugettext.pas, which translates the
text. It returns a widestring, unlike resourcestring, which is limited to
ansistring. You can use _() together with ansistrings as you like, because
Delphi automatically converts widestrings to strings when needed. Another
benefit of this is that you can write comments, that the translator can use to
make better translations:
// Message shown to the user after the user clicks Abort
button
ShowMessage (_('Action aborted'));
You can also write the comment in the same line:
ShowMessage (_('Action aborted')); // Message to user when clicking Abort
button
But only the // style comment is supported - you cannot use { } or (* *)
comments for this purpose.
Good comments normally lead to good translations. If the translator has a
copy of the source code, poedit and kbabel can both show the location in the
source code to the translator. This makes sense with _(), because the translator
might get a good idea, what this is about, even if the translator isn't a
programmer.
In other words, there are many reasons to use _() instead of resourcestrings.
If you create a new application, don't even think about resourcestrings - just
go directly for the _() solution.
---
本文章使用“国华软件”出品的博客内容离线管理软件MultiBlogWriter撰写并发布