Lazarus 项目支持多语言国际化
Lazarus 是一个与 Delphi 兼容的跨平台 RAD 集成开发环境。和 Delphi 一样, Lazarus 可以拖放组件,快速开发 GUI 应用程序。它使用 Free Pascal 作为后端编译器。当前的 Lazarus 版本是 2.2.6 ,内置的 Free Pascal 编译器版本是 3.2.2 。
修复 gettext 包的缺陷
Free Pascal 包含 gettext 包,可以用来实现软件国际化。但是当前版本 gettext 中的 GetLanguageIDs
过程在 Windows 系统下存在缺陷,无法获取到符合规范的 locale 名称:
pascal{$ifdef windows}
procedure GetLanguageIDs(var Lang, FallbackLang: string);
var
Buffer: array[1..4] of {$ifdef Wince}WideChar{$else}char{$endif};
Country: string;
UserLCID: LCID;
begin
//defaults
Lang := '';
FallbackLang:='';
UserLCID := GetUserDefaultLCID;
if GetLocaleInfo(UserLCID, LOCALE_SABBREVLANGNAME, @Buffer[1], 4)<>0 then
FallbackLang := lowercase(copy(Buffer,1,2));
if GetLocaleInfo(UserLCID, LOCALE_SABBREVCTRYNAME, @Buffer[1], 4)<>0 then begin
Country := copy(Buffer,1,2);
// some 2 letter codes are not the first two letters of the 3 letter code
// there are probably more, but first let us see if there are translations
if (Buffer='PRT') then Country:='PT';
Lang := FallbackLang+'_'+Country;
end;
end;
在简体中文的 Windows 环境中,该过程输出的 Lang
值是 ch_CH
,而不是期望的 zh_CN
。虽然在最新的 FPCSource 源代码中已经修复了这个问题,但事实上修复方案是有问题的:
pascal{$ifdef windows}
procedure GetLanguageIDs(var Lang, FallbackLang:AnsiString );
var
Buffer: array[1..4] of {$ifdef Wince}WideChar{$else}AnsiChar{$endif};
Country: AnsiString;
UserLCID: LCID;
begin
//defaults
Lang := '';
FallbackLang:='';
UserLCID := GetUserDefaultLCID;
if GetLocaleInfo(UserLCID, LOCALE_SABBREVLANGNAME, @Buffer[1], 4)<>0 then begin
FallbackLang := lowercase(copy(Buffer,1,2));
// Chinese abbreviation should be zh instead of ch
if (Copy(Buffer,1,3)='CHS') or (Copy(Buffer,1,3)='CHT') then FallbackLang:='zh';
end;
if GetLocaleInfo(UserLCID, LOCALE_SABBREVCTRYNAME, @Buffer[1], 4)<>0 then begin
Country := copy(Buffer,1,2);
// some 2 letter codes are not the first two letters of the 3 letter code
// there are probably more, but first let us see if there are translations
if (Buffer='PRT') then Country:='PT';
if (Copy(Buffer,1,3)='CHN') then Country:='CN';
Lang := FallbackLang+'_'+Country;
end;
end;
可以看到,修复方法仅仅是为了中文环境做了特殊处理。然而引发这个缺陷的主要原因是代码中的 GetLocaleInfo
调用传递了错误的参数。
Windows API GetLocaleInfoA 的原型如下:
c++int GetLocaleInfoA(
[in] LCID Locale,
[in] LCTYPE LCType,
[out, optional] LPSTR lpLCData,
[in] int cchData
);
在 GetLanguageIDs
中调用该 API 时,第二个参数 LCType
分别传递的是 LOCALE_SABBREVLANGNAME
和 LOCALE_SABBREVCTRYNAME
。事实上在 Windows SDK 中已经明确标注了这两个常量已经过时,需要用 LOCALE_SISO639LANGNAME
和 LOCALE_SISO3166CTRYNAME
替代。
c++#define LOCALE_SABBREVLANGNAME 0x00000003 // DEPRECATED arbitrary abbreviated language name, LOCALE_SISO639LANGNAME instead.
#define LOCALE_SABBREVCTRYNAME 0x00000007 // DEPRECATED arbitrary abbreviated country/region name, LOCALE_SISO3166CTRYNAME instead.
正确的修复方法是将 GetLanguageIDs
代码替换为:
pascal{$ifdef windows}
procedure GetLanguageIDs(var Lang, FallbackLang:AnsiString );
var
Buffer: array[1..4] of {$ifdef Wince}WideChar{$else}AnsiChar{$endif};
Country: AnsiString;
UserLCID: LCID;
begin
Lang := '';
FallbackLang:='';
UserLCID := GetUserDefaultLCID;
if GetLocaleInfo(UserLCID, LOCALE_SISO639LANGNAME, @Buffer[1], 4)<>0 then begin
FallbackLang := lowercase(copy(Buffer,1,2));
end;
if GetLocaleInfo(UserLCID, LOCALE_SISO3166CTRYNAME, @Buffer[1], 4)<>0 then begin
Country := copy(Buffer,1,2);
Lang := FallbackLang+'_'+Country;
end;
end;
可以在当前项目下新建 gettext.pas 单元,并将修复的代码写入保存,以覆盖编译器自带的 gettext 包。
在 Lazarus 项目中启用 i18n (国际化)
新建一个名为 project1 的 Lazarus 项目。在默认窗体中加入一个标题为 Hello
的按钮。然后在主窗体单元中定义 resourcestring
节,在此节中定义一个值为 Hello, World!
的资源字符串。接着设置按钮的点击事件,用 ShowMessage
过程显示该资源字符串内容。最后,在主窗体单元中引用 DefaultTranslator
模块。
pascalunit Unit1;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls,
DefaultTranslator;
type
{ TForm1 }
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
public
end;
var
Form1: TForm1;
resourcestring
HelloWorld='Hello, World!';
implementation
{$R *.lfm}
{ TForm1 }
procedure TForm1.Button1Click(Sender: TObject);
begin
ShowMessage(HelloWorld);
end;
end.
将修改过的 gettext.pas 文件(下载)保存到项目源代码根目录下。为了保证自定义的 gettext 代码生效,可以在下次编译时选择「运行 - 清理和构建」重新构建项目。

项目编译后,会在根目录下生成 unit1.lsj 文件,以及在项目 lib\x86_64-win64 目录下生成 unit1.rsj 文件。这两个文件分别包含了窗体和资源字符串节中的文本。可以使用 Free Pascal 自带的命令行工具 rstconv 来生成 PO 文件:
cmdrstconv -i unit1.rsj -o unit1.po
所幸 Lazarus 支持自动生成 POT 文件。在主菜单中选择「工程 - 工程选项」,在「工程选项 - 国际化」中勾选「启用i18n(国际化)」选项:

按 Ctrl+F9 编译项目。当编译成功后,会在项目根目录下生成一个名为 project1.pot 的模板文件。如果没有生成该文件,可以修改一下窗体的标题或者资源字符串的值,然后再次编译。
用 Poedit 打开 project1.pot 模板文件,将其中的条目全部翻译好,并将生成的 PO 文件命名为 project1.zh_CN.po 并保存到项目的 locale 目录下。

再次按 F9 运行程序,可以发现界面已经基本汉化,但消息框的确认按钮依然还是英文的。还需要本地化 LCL 和 Lazarus 提供的组件。

可以在 Lazarus 安装路径下的 lcl\languages 目录中找到 lclstrconsts.zh_CN.po 文件。将该文件也复制到项目 locale 目录下便可实现 LCL 组件的中文本地化。不过官方提供的中文翻译质量很差,可以自己润色一下。另外,可以在 Lazarus 安装路径下的 components 目录中找到各组件的 languages 文件夹。如果使用了这些组件,也需要将对应组件 languages 文件夹下的 *.zh_CN.po 文件复制到项目 locale 目录下。此外,可以用 Poedit 或 msgfmt 命令将 PO 文件编译成 MO 文件,后者是二进制格式,加载速度能更快一些。
DefaultTranslator
最终通过调用 LCLTranslator
的 SetDefaultLang
过程来实现本地化的。它会先尝试加载 PO/POT 文件,如果失败则尝试加载 MO 文件,并会尝试通过下面的路径顺序来查找文件(以语言代码为 zh_CN 、应用程序名为 project1 举例):
- ./zh_CN/project1.po
- ./languages/zh_CN/project1.po
- ./locale/zh_CN/project1.po
- ./locale/zh_CN/LC_MESSAGES/project1.po
- /usr/share/locale/zh_CN/LC_MESSAGES/project1.po (Linux)
- ./zh/project1.po
- ./languages/zh/project1.po
- ./locale/zh/project1.po
- ./locale/zh/LC_MESSAGES/project1.po
- ./project1.zh_CN.po
- ./locale/project1.zh_CN.po
- ./languages/project1.zh_CN.po
- /usr/share/locale/zh/LC_MESSAGES/project1.po (Linux)
- ./project1.zh.po
- ./locale/project1.zh.po
- ./languages/project1.zh.po
- ./project1.pot
- ./locale/project1.pot
- ./languages/project1.pot
其中 5 和 13 仅在 Linux/Unix 平台下生效,17 - 18 仅在查找 PO 文件时生效。