北岛夜话
原创工业智能控制领域(PLC、单片机/嵌入式、机器人、通信、机器视觉)的技术及经验分享。
文章570 浏览9851045

使用gettext实现软件的多国语言支持

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

cover.png

一、gettext软件包的获取

1.Windows平台

1.1、手动获取

下载路径:https://mlocati.github.io/articles/gettext-iconv-windows.html

选择你的操作系统的位数、要下载的版本类型及文件类型。版本类型包括:动态库(shared)和静态库(static)两种,文件类型包括:安装版,非安装版及开发包。

我下载的是动态库的非安装版,当然要在程序中调用其函数的话还需要下载开发包。

download.png

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

env-variables.png

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,如下图:

Windows_region.png

Windows_region2.png

取消

感谢您的支持,欢迎常来看看!

扫码支持
一点动力,多少随意

打开支付宝扫一扫,即可进行扫码打赏哦

最后编辑于:2026/06/14作者: 北岛李工

发表评论

恭祝各位朋友在丙午马年新春快乐,工作顺利,阖家安康!

×