4、Qt框架国际化:QTranslator与语言文件(.ts/.qm)、tr()函数的使用、动态语言切换

Qt 的国际化方案,说白了就是一套「翻译流水线」。你写代码时用 tr() 把字符串包起来,然后工具帮你抽成 .ts 文件,翻译完再编译成 .qm 二进制文件,运行时加载就完事了。我最早接触这套流程时,觉得它比 Java 的 ResourceBundle 要顺手不少——至少不用手动维护一堆 properties 文件。

4.1 核心三件套:tr()、.ts、.qm

先理清这三个东西各自扮演什么角色:

组件 作用 谁负责
tr() 函数 标记需要翻译的字符串 开发者
.ts 文件 XML 格式的翻译源文件 翻译人员 + 工具
.qm 文件 编译后的二进制翻译文件 lrelease 工具

我个人习惯把 tr() 当作「国际化开关」——只要看到它,就知道这行文字将来要进翻译流程。嗯,这里要注意:所有用户可见的字符串都必须用 tr() 包裹,包括按钮文字、标签、状态栏提示、甚至 QMessageBox 里的文本。

核心原则:不要偷懒用 QString 直接写死中文。否则后期改语言时,你会想抽自己。

4.2 tr() 函数的使用细节

先看一个最基础的例子:

// 头文件
#include <QPushButton>

// 构造函数中
QPushButton *btn = new QPushButton(tr("保存(&S)"), this);

这里 tr("保存(&S)") 做了两件事:标记字符串,同时保留快捷键 Alt+S。翻译人员拿到 .ts 文件后,会看到类似这样的条目:

<message>
    <source>保存(&S)</source>
    <translation>Save(&S)</translation>
</message>

我在项目中遇到过一个问题:有人把 tr() 用在了非 UI 线程里。结果翻译死活不生效。为什么?因为 tr() 依赖当前线程的 QCoreApplication 实例。如果你在子线程里调用 tr(),它找不到正确的翻译上下文,就会返回原始字符串。

避坑指南:我曾经在后台工作线程里用 tr() 拼接日志消息,结果日志全是英文。后来改成在主线程预先翻译好,再传给子线程。记住:tr() 只属于主线程。

另外,tr() 支持复数形式。比如:

int fileCount = 3;
label->setText(tr("共 %1 个文件", "", fileCount).arg(fileCount));

翻译人员在 .ts 里会看到复数条目,可以分别定义单数和复数翻译。这个特性在英文里特别有用——英文的 "1 file" 和 "2 files" 是不同的。

4.3 生成 .ts 文件:lupdate 工具

写代码时用 tr() 标记好,接下来就要提取这些字符串了。Qt 提供了 lupdate 命令行工具:

# 扫描当前目录所有 .cpp/.h 文件,生成 zh_CN.ts
lupdate . -ts zh_CN.ts

# 也可以指定多个源文件
lupdate main.cpp widget.cpp -ts zh_CN.ts

生成的 .ts 文件是 XML 格式,你可以用 Qt Linguist 工具打开它,或者直接交给翻译人员用文本编辑器处理。我个人习惯用 Linguist,因为它能高亮显示未翻译的条目,还能预览上下文。

小技巧:.pro 文件里加上 TRANSLATIONS += zh_CN.ts en_US.ts,然后在 Qt Creator 里右键项目 -> "更新翻译",就能一键运行 lupdate。省得每次敲命令行。

4.4 编译 .qm 文件:lrelease 工具

翻译完成后,需要把 .ts 编译成 .qm 二进制文件。这一步用 lrelease

lrelease zh_CN.ts -qm zh_CN.qm

.qm 文件是压缩后的二进制格式,体积小、加载快。你想想看,如果每次启动都去解析 XML,那得多慢。所以 Qt 选择了编译成二进制,运行时直接内存映射。

我建议把 .qm 文件放到 resources.qrc 资源文件里,这样打包时不会遗漏:

<RCC>
    <qresource prefix="/translations">
        <file>zh_CN.qm</file>
        <file>en_US.qm</file>
    </qresource>
</RCC>

4.5 加载翻译:QTranslator 的使用

运行时加载翻译文件,靠的是 QTranslator 类。看代码:

#include <QApplication>
#include <QTranslator>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    // 创建翻译器
    QTranslator translator;
    
    // 加载 .qm 文件
    // 从资源文件加载
    translator.load(":/translations/zh_CN.qm");
    
    // 或者从文件系统加载
    // translator.load("zh_CN.qm", "translations/");
    
    // 安装到应用
    app.installTranslator(&translator);

    // 创建主窗口...
    MainWindow w;
    w.show();

    return app.exec();
}

这里有个细节:load() 返回 bool,表示是否加载成功。我建议你检查返回值,如果加载失败就打印日志或回退到默认语言。我曾经因为路径写错,导致整个应用启动后全是英文,排查了半天才发现是 .qm 文件没找到。

注意:installTranslator() 必须在创建任何 UI 之前调用。否则已经创建好的控件不会自动更新语言。如果你需要动态切换语言,就得手动刷新所有界面。

4.6 动态语言切换:不用重启应用

用户希望点一下按钮就能切换语言,而不是关掉应用再打开。Qt 支持动态切换,但需要你做一些额外工作。

核心思路是:

  1. 移除旧的翻译器
  2. 安装新的翻译器
  3. 刷新所有 UI 控件的文本

看一个完整的切换函数:

void MainWindow::switchLanguage(const QString &langCode)
{
    // 1. 移除旧的翻译器
    qApp->removeTranslator(&m_translator);

    // 2. 加载新的 .qm 文件
    QString qmPath = QString(":/translations/%1.qm").arg(langCode);
    if (!m_translator.load(qmPath)) {
        qWarning() << "Failed to load translation:" << qmPath;
        return;
    }

    // 3. 安装新的翻译器
    qApp->installTranslator(&m_translator);

    // 4. 刷新界面
    // 方法一:重新设置所有文本
    ui->retranslateUi(this);  // 如果你用了 Qt Designer 生成的 UI 类

    // 方法二:手动刷新
    // m_label->setText(tr("欢迎"));
    // m_button->setText(tr("确定"));

    // 5. 触发重绘
    this->update();
}

这里的关键是 ui->retranslateUi()。如果你用 Qt Designer 设计界面,生成的 ui_xxx.h 文件里会自动包含一个 retranslateUi() 函数,它会重新调用所有控件的 setText() 并传入 tr() 包裹的字符串。说白了,就是帮你把界面上的文字全部重新设置一遍。

个人经验:如果你手动创建控件,建议也写一个类似的 retranslateUi() 函数,把所有 setText() 集中管理。否则切换语言时,你会漏掉某个按钮或标签,界面就会变成中英混杂的「四不像」。

4.7 动态切换的注意事项

嗯,这里有几个坑,我踩过,你最好别踩:

  • QComboBox 的条目:如果你用 addItem(tr("选项一")) 添加条目,切换语言后不会自动更新。你需要清空再重新添加,或者用 setItemText() 逐个更新。
  • QTableWidget 的表头:表头文字也需要手动刷新。建议在 retranslateUi() 里调用 setHorizontalHeaderLabels()
  • 动态创建的控件:如果你在运行时 new 了一个 QLabel,它的文本不会自动翻译。你需要在创建时就传入 tr() 包裹的字符串,并在切换语言时重新设置。
  • 非 UI 数据:比如你有一个 QStringList 存储了翻译前的文字,切换语言后这个列表不会自动更新。你需要重新从 tr() 获取。

最佳实践:我建议把所有需要翻译的字符串集中到一个函数里,比如 retranslateAll(),然后在切换语言时调用它。这样你只需要维护一个地方,不会遗漏。

4.8 总结一下

Qt 的国际化方案,说白了就是三步走:

  1. 写代码时用 tr() 标记字符串
  2. lupdate 生成 .ts,翻译后 lrelease 编译成 .qm
  3. 运行时用 QTranslator 加载 .qm,动态切换时刷新界面

这套方案成熟、稳定,而且 Qt 官方一直在维护。你想想看,从 Qt 4 到 Qt 6,tr() 的用法几乎没变过。这就是技术选型时「选大厂、选稳定」的好处。

下一章我会讲如何用 QML 做国际化,以及 qsTr()tr() 的区别。到时候你会发现,QML 的国际化思路和 C++ 侧一脉相承,但细节上又有不少差异。