Lazarus 项目支持多语言国际化(进阶篇)
在之前的一篇文章中简单介绍了如何在 Lazarus 项目中启用多语言国际化。不过使用 DefaultTranslator
或 SetDefaultLang()
无法实现运行时动态切换 GUI 的语言。更准确地说,在程序中首次调用 LCLTranslator.SetDefaultLang()
,并将参数 ForceUpdate
设为 True
,是能够在运行时修改 GUI 语言的,但若再次调用便无效了。
查看 SetDefaultLang()
源代码:
pascalfunction SetDefaultLang(Lang: string; Dir: string = ''; LocaleFileName: string = ''; ForceUpdate: boolean = true): string;
{ Arguments:
Lang - language (e.g. 'ru', 'de', 'zh_CN'); empty argument is default language.
Dir - custom translation files subdirectory (e.g. 'mylng'); empty argument means searching only in predefined subdirectories.
LocaleFileName - custom translation file name; empty argument means that the name is the same as the one of executable.
ForceUpdate - true means forcing immediate interface update. Only should be set to false when the procedure is
called from unit Initialization section. User code normally should not specify it.
}
var
lcfn: string;
LocalTranslator: TUpdateTranslator;
i: integer;
begin
Result := '';
LocalTranslator := nil;
// search first po translation resources
try
lcfn := FindLocaleFileName('.po', Lang, Dir, LocaleFileName, Result);
if lcfn <> '' then
begin
Translations.TranslateResourceStrings(lcfn);
LocalTranslator := TPOTranslator.Create(lcfn);
end
else
begin
// try now with MO translation resources
lcfn := FindLocaleFileName('.mo', Lang, Dir, LocaleFileName, Result);
if lcfn <> '' then
begin
GetText.TranslateResourceStrings(UTF8ToSys(lcfn));
LocalTranslator := TDefaultTranslator.Create(lcfn);
end;
end;
except
Result := '';
lcfn := '';
end;
if lcfn<>'' then
TranslateLCLResourceStrings(Lang, lcfn);
if LocalTranslator<>nil then
begin
if Assigned(LRSTranslator) then
LRSTranslator.Free;
LRSTranslator := LocalTranslator;
// Do not update the translations when this function is called from within
// the unit initialization.
if ForceUpdate=true then
begin
for i := 0 to Screen.CustomFormCount-1 do
LocalTranslator.UpdateTranslation(Screen.CustomForms[i]);
for i := 0 to Screen.DataModuleCount-1 do
LocalTranslator.UpdateTranslation(Screen.DataModules[i]);
end;
end;
end;
该函数首先查找本地的 .po 或 .mo 翻译文件;然后,将程序的资源字符串翻译成本地语言;接下来,创建一个新的 TUpdateTranslator
对象替代全局对象。
在调用过该函数后,新创建的 GUI 组件界面都将会使用本地语言。若要对之前已创建的组件界面进行本地化,则可以将函数参数 ForceUpdate
设为 True
。这将遍历所有已创建的可视组件中所有类型为可视文本的属性,并逐一调用 TUpdateTranslator.TranslateStringProperty()
进行翻译。最终,会调用 TMOFile.Translate()
或 TPOFile.Translate()
将原生语言(比如英文)翻译成本地语言(比如中文)。
msgid | msgstr |
---|---|
“File” | “文件” |
“Edit” | “编辑” |
“Help” | “帮助” |
“About” | “关于” |
… | … |
当第一次调用 SetDefaultLang()
时,可视组件是原生语言,可以正确翻译成本地语言;而第二次调用 SetDefaultLang()
时,可视组件界面已经被翻译成某个本地语言,TMOFile.Translate()
或 TPOFile.Translate()
无法再将其翻译成另一种本地语言。
通常的解决方案是,销毁已经创建的窗体,然后重新创建。窗体在创建时会加载经过二次本地化的资源字符串。不过这种方案的缺点也很明显,主窗体重建相当于程序重新启动,开发者需要自行保存和还原窗体的状态。对于程序的用户来说,体验也相当不好。
另一种解决方案是,先将已经本地化的 GUI 组件还原成原生语言,也就是初始状态,然后再次进行本地化翻译。
遗憾的是,LCLTranslator
并没有提供翻译还原的功能,这需要开发者自己实现。其原理就是,在本地化翻译的同时,记录一个 msgstr 到 msgid 逆映射表,再根据这个映射表进行文本还原。
需要注意的是,不同的原生语言,可能被翻译成同一个本地语言条目。这样在还原时就需要对文本条目的来源加以区分,而不能简单地根据翻译条目的文本映射关系进行还原。比如:英文 Property 和 Attribute 都会被翻译成「属性」。如果只用简单的文本映射,就无法确定「属性」是该还原成 Property 还是 Attribute 了。
msgstr | msgid |
---|---|
“Form1.Label1.Caption#属性” | “Property” |
“Form2.Edit1.Hint#属性” | “Attribute” |
… | … |
这里实现了一个 TRestorableTranslator
类来替代默认的 TDefaultTranslator
,具体实现代码如下:
pascalunit MyTranslator;
{$mode ObjFPC}{$H+}
interface
uses
Classes, SysUtils, Generics.Collections, LCLTranslator, TypInfo;
type
{ TRestorableTranslator }
TRestorableTranslator = class(TDefaultTranslator)
type
{ TRestoreTranslator }
TRestoreTranslator = class(TUpdateTranslator)
private
FTranslatedTerms: specialize TDictionary<string, string>;
public
constructor Create;
destructor Destroy; override;
property TranslatedTerms: specialize TDictionary<string, string> read FTranslatedTerms;
procedure TranslateStringProperty(Sender: TObject; const Instance: TPersistent;
PropInfo: PPropInfo; var Content: string); override;
end;
private
FRestoreTranslator: TRestoreTranslator;
public
constructor Create(MOFileName: string);
destructor Destroy; override;
procedure TranslateStringProperty(Sender: TObject; const Instance: TPersistent;
PropInfo: PPropInfo; var Content: string); override;
procedure Restore;
end;
implementation
function GetPersistentPath(APersistent: TPersistent): string;
var
TempPersistent: TPersistent;
NamePath: string;
begin
Result := '';
TempPersistent := APersistent;
while Assigned(TempPersistent) do
begin
NamePath := TempPersistent.GetNamePath;
if NamePath <> '' then Result := NamePath + '.' + Result;
{$IFDEF DEBUG}{$objectChecks-}{$ENDIF}
TempPersistent := TPersistentAccess(TempPersistent).GetOwner;
{$IFDEF DEBUG}{$objectChecks+}{$ENDIF}
end;
end;
{ TRestorableTranslator }
constructor TRestorableTranslator.Create(MOFileName: string);
begin
inherited Create(MOFileName);
FRestoreTranslator := TRestoreTranslator.Create;
end;
destructor TRestorableTranslator.Destroy;
begin
FreeAndNil(FRestoreTranslator);
inherited Destroy;
end;
procedure TRestorableTranslator.TranslateStringProperty(Sender: TObject; const Instance: TPersistent;
PropInfo: PPropInfo; var Content: string);
var
OriginalContent, NamePath: string;
begin
OriginalContent := Content;
inherited;
if Content <> OriginalContent then
begin
NamePath := GetPersistentPath(Instance) + PropInfo^.Name;
FRestoreTranslator.TranslatedTerms.TryAdd(NamePath + #4 + Content, OriginalContent);
end;
end;
procedure TRestorableTranslator.Restore;
var
i: integer;
LocalTranslator: TAbstractTranslator;
begin
LocalTranslator := LRSTranslator;
LRSTranslator := FRestoreTranslator;
for i := 0 to Screen.CustomFormCount-1 do
FRestoreTranslator.UpdateTranslation(Screen.CustomForms[i]);
for i := 0 to Screen.DataModuleCount-1 do
FRestoreTranslator.UpdateTranslation(Screen.DataModules[i]);
LRSTranslator := LocalTranslator;
end;
{ TRestorableTranslator.TRestoreTranslator }
constructor TRestorableTranslator.TRestoreTranslator.Create;
begin
FTranslatedTerms := specialize TDictionary<string, string>.Create;
end;
destructor TRestorableTranslator.TRestoreTranslator.Destroy;
begin
FreeAndNil(FTranslatedTerms);
inherited Destroy;
end;
procedure TRestorableTranslator.TRestoreTranslator.TranslateStringProperty(Sender: TObject; const Instance: TPersistent;
PropInfo: PPropInfo; var Content: string);
var
Value, NamePath: string;
begin
NamePath := GetPersistentPath(Instance) + PropInfo^.Name;
if FTranslatedTerms.TryGetValue(NamePath + #4 + Content, Value) then
Content := Value;
end;
end.
只要在二次本地化翻译前,调用 TRestorableTranslator.Restore
将已翻译的组件还原成最初的状态即可。