gettext是GNU发布的多国语言工具包,是国际化(i18n)和本地化(l10n)的工具合集,便于管理软件中的文本翻译,使其支持多国语言。“i18n”是英文"internationalization"的缩写,由于字母i和n之间有18个字符,因此简写为“i18n”,表示“国际化”的意思。另外还有“l10n”是"localization"的缩写,表示“本地化”;“g11n”是“globalization”的缩写,表示“全球化”;“m17n”是“multilingualization”的缩写,表示“多语言化”。今天这篇文章,介绍下gettext的使用(包括Windows和Linux两个平台)。

一、gettext软件包的获取
1.Windows平台
1.1、手动获取
下载路径:https://mlocati.github.io/articles/gettext-iconv-windows.html
选择你的操作系统的位数、要下载的版本类型及文件类型。版本类型包括:动态库(shared)和静态库(static)两种,文件类型包括:安装版,非安装版及开发包。
我下载的是动态库的非安装版,当然要在程序中调用其函数的话还需要下载开发包。

如果下载的安装版,直接单击运行安装即可。如果是非安装版,则将其解压到某个目录,比如C:\gettext下,然后全局环境变量Path中添加该路径(bin文件夹),如下图所示:

1.2、使用MSYS2自动获取
如果你使用了MSYS2,可以通过包管理器自动获取,命令如下:
pacman -S gettext
2、Linux平台获取
以Ubnutu系统为例,输入如下命令:
sudo apt-get update
sudo apt-get install gettext
二、gettext的工具简介
1、xgettext
从源代码中提取标记的可翻译字符串,生成.pot文件。"pot"是"portable object template"的缩写,即“便携对象模版”,是用于多国语言翻译的模版。其基本语法为:
xgettext -o <输出文件> --from-code=<编码> --keyword=<关键字> <输入文件>
例如:
xgettext -o ./locale/fltk_i18n.pot ../src/main.cpp --from-code=UTF-8 --keyword=_
2、msginit
将便携对象模版文件(.pot)转换为特定语言的便携对象文本(.po)文件。"po"是"portable object"的缩写,它其实是文本文件。其基本语法为:
msginit -i <输入文件> -o <输出文件> -l <目标语系[.编码]>
其中:
输入文件是.pot的模版文件,如果没有指明,则在当前文件夹下查找。
输出文件是.po的文件;
目标语系是LL_CC的格式,比如:中文简体 zh_CN,中文繁体 zh_TW,美式英语 en_US,德语 de_DE。
目标语系后面可以添加编码,比如:zh_CN.UTF-8
例如:
msginit -i ./locale/fltk_i18n.pot -o ./locale/zh_CN/LC_MESSAGES/fltk_i18n.po -l zh_CN.UTF-8
3、msgfmt
将翻译完成的便携对象文本文件(.po)编译成二进制的语言文件(.mo),便于程序读取。其基本语法为:
msgfmt -o <输出文件> <输入文件>
其中:
输出文件:其路径格式遵循规则"locale/LL_CC/LC_MESSAGES/文件名.mo"
LL_CC是语系的泛写,使用时应替换为具体语系。比如zh_CN 或zh_TW等。假设程序名为myapp.exe,则输出中文简体文件的路径为 “./locale/zh_CN/LC_MESSAGES/myapp.mo”。"LC"是“Local Category”的缩写,即“本地类别”;MESSAGES表示界面消息。
输入文件是已经翻译完成的.po文件
文件结构一般如下:
your_project/
└── locale/ # ← bindtextdomain(domain, "这里")
├── en_US/
│ └── LC_MESSAGES/ # ← 固定名,大小写敏感
│ └── myapp.po # ← 人工编辑
│ └── myapp.mo # ← msgfmt 编译产物,程序真正加载
├── zh_CN/
│ └── LC_MESSAGES/
│ └── myapp.po
│ └── myapp.mo
└── myapp.pot # 模板文件,放根目录,不属于任何语言
例如:
msgfmt -o ./locale/zh_CN/LC_MESSAGES/fltk_i18n.mo ./locale/zh_CN/LC_MESSAGES/fltk_i18n.po
4、msgunfmt
将二进制语言文件(.mo)转换为便携对象文本文件(.po),可用于查看二进制语言文件是否被损坏。其基本语法为:
msgunfmt <输入文件>
例如:
msgunfmt ./locale/zh_CN/LC_MESSAGES/myapp.mo
5、msgmerge
将旧的便携对象文本文件(.po)与新模版(.pot)合并,保留已有翻译的同时标记新增、修改或删除的条目,其基本语法为:
msgmerge --update old.po new_template.pot
上述命令会直接覆盖老的翻译文件。如果想生成新的,可以使用:
msgmerge old.po new_template.pot new.po
三、实现步骤
1、头文件与宏定义
需要包含头文件
#include <libintl.h>
#include <locale.h>
为了方便调用gettext,定义宏
#define _(str) gettext(str)
2、添加必要代码:
setlocale(LC_ALL, "");
bindtextdomain("myapp", "./locale");
textdomain("myapp");
2.1、setlocale()函数:
C 标准库 <locale.h> 中的一个函数,用于设置或查询程序的本地化信息。它允许程序员指定用于字符分类、字符转换、货币格式、日期和时间格式、数字格式等的区域设置。
#include <locale.h>
char *setlocale(int category, const char *locale);
参数:
category:指定要设置或查询的本地化类别。可以是以下宏之一:
LC_ALL:设置或查询所有本地化类别。
LC_COLLATE:设置或查询字符串比较的本地化信息。
LC_CTYPE:设置或查询字符处理的本地化信息。
LC_MONETARY:设置或查询货币格式的本地化信息。
LC_NUMERIC:设置或查询数字格式的本地化信息。
LC_TIME:设置或查询时间格式的本地化信息。
locale:指定要设置的本地化信息。可以是以下之一:
"":设置为用户环境变量中的默认设置。
NULL:查询当前的本地化信息。
具体的区域设置名称:如 "en_US.UTF-8",用于设置特定的区域设置。
返回值
如果 locale 为 NULL,返回一个指向当前区域设置信息字符串的指针。
如果 locale 非 NULL 并且成功设置,返回一个指向该区域设置信息字符串的指针。
如果 locale 非 NULL 并且设置失败,返回 NULL。
参考:https://www.runoob.com/cprogramming/c-function-setlocale.html
2.2、bindtextdomain()函数
#include <libintl.h>
char * bindtextdomain (const char * domainname, const char * dirname);
该函数用于设置包含指定消息域(message domain)消息目录的目录层级的根目录。
一个消息域是一组可翻译的msgid消息集合。通常,每个软件包都拥有自己的消息域。之所以需要调用 bindtextdomain是因为软件包的安装前缀并不总是与 <libintl.h>头文件及 libc/libintl库的安装前缀相同。
消息目录的预期路径为:dirname/locale/category/domainname.mo,其中 locale是区域设置名称,category是区域设置的某个类别(如 LC_MESSAGES)。
domainname必须是一个非空字符串。
若 dirname不为 NULL,则属于domainname域的消息目录根目录将被设置为dirname。该函数会在需要时复制传入的字符串。如果程序后续打算调用chdir函数,则务必确保dirname是一个绝对路径;否则无法保证能正确找到消息目录。
若dirname为NULL,则函数返回此前为domainname域设置的基目录。
返回值
成功时,bindtextdomain函数在可能更新后,返回domainname域当前的基目录。返回的字符串在下一次针对同一 domainname调用bindtextdomain之前均有效,且不得修改或释放。若发生内存分配失败,该函数会将errno设为 ENOMEM。
参考:https://www.man7.org/linux/man-pages/man3/bindtextdomain.3.html
2.3、textdomain()函数
#include <libintl.h>
char * textdomain (const char * domainname);
该函数用于设置或获取当前的消息域。
一个消息域是一组可翻译的msgid消息集合。通常,每个软件包都拥有自己的消息域。域名用于确定查找翻译时所使用的消息目录;它必须是一个非空字符串。
当前消息域会被gettext、ngettext函数使用,同时也会被dgettext、dcgettext、dngettext和dcngettext函数在传入NULL作为 domainname参数时采用。
若domainname不为NULL,则将当前消息域设置为domainname。函数内部存储的是domainname参数的副本。
若domainname为NULL,则函数返回当前的消息域。
返回值
成功时,textdomain函数在可能更新后,返回当前的消息域。返回的字符串在下一次调用 textdomain之前均有效,且不得修改或释放。若发生内存分配失败,该函数会将errno设为 ENOMEM并返回 NULL。
参考:https://www.man7.org/linux/man-pages/man3/textdomain.3.html
3、在代码中标记需要翻译的字符串,用"_()"修饰。比如:
printf("%s\n", _("Hello World!"));
4、使用xgettext工具从源代码中提取标记的字符串,生成便携对象模版文件(.pot)
5、使用msginit工具将便携对象模版文件(.pot)转换为特定语言的便携对象文本文件(.po)
6、对特定语言的便携对象文本文件进行翻译
7、使用msgfmt工具将翻译完成的便携对象文本文件(.po)转换成二进制消息文件(.mo)
8、编译程序的源代码
例如:
g++ -o fltk_i18n.exe ../src/main.cpp -lintl -lfltk -L ../lib/lib/windows_release -I ../lib/include
四、示例及可能的问题
下面是一个简单的程序示例:
#include <stdio.h>
#include <stdlib.h>
#include <locale.h> // 用于 setlocale
#include <libintl.h>
#define _(str) gettext(str)
#define FORCE_UTF8_BAIT() \
do \
{ \
volatile const char *__u = _("ø"); \
(void)__u; \
} while (0)
int main()
{
// 设置本地信息
setlocale(LC_ALL, "");
// 绑定域名
bindtextdomain("myapp", "./locale");
// 指定当前使用的域名
textdomain("myapp");
// --- UTF-8 诱饵 ---
FORCE_UTF8_BAIT();
// --- 测试输出 ---
printf("%s\n", _("Hello World!"));
return 0;
}
其中:
宏定义 FORCE_UTF8_BAIT()是我在测试中发现如果源代码中没有非ASCII字符,则.pot .po等文件不能被设置为UTF-8编码格式。因此增加了一个不需要翻译的非ASCII码字符。后续可观察,若无该问题,则该宏不必要。
另外我在Windows命令行试图打印中文字符,折腾了很久,发现Windows的命令行窗口默认不采用UTF-8编码,导致转换后的输出为乱码。
为了使其支持中文,需要在控制面板->区域中设置支持UTF-8,如下图:


北岛夜话



发表评论