使用gettext 进行国际化

一般来说,使用gettext 为程序添加多语言支持时,需要遵循一些标准的步骤。另外就是,对于在main() 函数之前就初始化的变量需要特殊处理。 为此,特整理笔记备忘。

最小可用示例

该示例代码仅保证多语言功能可用:

#include <iostream>
#include <locale.h>
#include <libintl.h>

#define _(STRING) gettext(STRING)

int main() {
    setlocale(LC_ALL, "");  // 指定语言,例如en_US,zh_CN,zh_CN.UTF-8 等
    bindtextdomain("myapp", "./locale"); // .locale 是语言包的路径,可以是相对路径也可以是绝对路径
    textdomain("myapp");
    // 上面的myapp 可以改成自己喜欢的名字,但是文件路径需要遵循以下规则  
    // ./locale/{zh_CN}/LC_MESSAGES/{myapp}.mo  
    // 其中.mo 可以通过以下步骤生成

    std::cout << _("Hello, world!") << std::endl;
    return 0;
}

提取、翻译

# 从main.cpp 中提取待翻译的内容
xgettext --from-code=UTF-8 -k_ main.cpp -o messages.pot

# 同时提取多个文件
find . -name "*.cpp" | xargs xgettext --from-code=UTF-8 -k_ -o messages.pot

通过poedit 可以打开.pot 文件,并将翻译结果保存到相应的路径下。

静态变量翻译

上面内容不太重要,多试试就好了。但是静态变量的翻译就很有意思了。

#include <iostream>
#include <locale.h>
#include <libintl.h>

#define _(STRING) gettext(STRING)

const char * bye = _("Goodbye!");

int main() {
    setlocale(LC_ALL, "");  // 指定语言,例如en_US,zh_CN,zh_CN.UTF-8 等
    bindtextdomain("myapp", "./locale"); // .locale 是语言包的路径,可以是相对路径也可以是绝对路径
    textdomain("myapp");

    std::cout << _("Hello, world!") << std::endl;  // 可以正常显示对应语言
    std::cout << bye << std::endl;  // 无法正常显示对应语言
    return 0;
}

因为bye 是在main() 之前初始化的,所以在main() 执行时就获取不到对应的译文了。为此,可以封装一个函数:

#define _(STRING) _translate(STRING)
void _translate(const char* msg) {
    setlocale(LC_ALL, "");  // 指定语言,例如en_US,zh_CN,zh_CN.UTF-8 等
    bindtextdomain("myapp", "./locale"); // .locale 是语言包的路径,可以是相对路径也可以是绝对路径
    textdomain("myapp");

    return gettext(msg);
}

多语言初始化标识符

每次运行_() 前都设置下语言就好了。但是这样会造成额外的开销,为此我们可以通过一个全局的标识符gettext_initialized 来表示语言是否初始化过了。

#define _(STRING) _translate(STRING)

bool gettext_initialized = false;

void ensure_gettext_initialized() {
    if (!gettext_initialized) {
        setlocale(LC_ALL, "");
        bindtextdomain("gmsh", "./locale");
        textdomain("gmsh");
        gettext_initialized = true;
    }
}
const char* _translate(const char* msg) {
    ensure_gettext_initialized();
    return gettext(msg);
}

多线程安全

其实上面的代码在大部分情况下已经可用了,但是多线程的情况下可能会有一丢丢的问题(但仔细想想其实无关紧要,最多是多初始化几次而已)。为此,可以添加线程锁,以下是完整代码(未作验证):

#include <iostream>
#include <locale.h>
#include <libintl.h>
#include <mutex>

#define _(STRING) _translate(STRING)
std::mutex gettext_mutex;
bool gettext_initialized = false;

void ensure_gettext_initialized() {
    std::lock_guard<std::mutex> lock(gettext_mutex);
    if (!gettext_initialized) {
        setlocale(LC_ALL, "");  // 也可用export LC_MESSAGES=zh_CN.UTF-8 指定语言
        bindtextdomain("myapp", "./locale");
        textdomain("myapp");
        gettext_initialized = true;
    }
}

const char* _translate(const char* msg) {
    ensure_gettext_initialized();
    return gettext(msg);
}


const char* messages[] = {
    _("Hello"),
    _("Goodbye!")
};
const char * bye = _("Goodbye!");

int main() {
    std::cout << _("Hello, world!") << std::endl;
    
    std::cout << bye << std::endl; 
    for (auto key : messages) {
        std::cout << key << std::endl;  // 运行时翻译
    }
    return 0;
}

该操作甚至可以翻译数组messages[] 中的字符串。😮

其他

修改FLTK 支持字体

默认显示汉语乱码,可以修改FlGui::FlGui() 构造函数,添加:

FlGui::FlGui(int argc, char **argv, bool quitShouldExit,
             void (*error_handler)(const char *fmt, ...))
  : _quitShouldExit(quitShouldExit), lastContextWindow(0)
{
  Fl::set_font(FL_HELVETICA, "Noto Sans CJK SC"); // 确保系统已安装该字体

  // ...
}

批量替换文件内容

在vscode 中,使用ctrl+shift+f 可以全局查询,并按正则表达式替换内容:

Msg::Error\s*\(\s*"((?:[^"\\]|\\.)*)"\s*\)

匹配"" 中的字符串,支持转义字符。并替换为

Msg::Error(_("$1"))

Msg 中常见的消息类型有:WarningDebugErrorInfoStatuBar,其他例如窗口图标,则需要具体考虑了。

CMAKE

添加了I18n.h 之后,要记得在对应的.cpp 文件中实现方法,并且在同级的CMAKELIST.txt 中检查是否将文件添加进去,否则可能造成编译时找不到定义的错误。