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 支持动态切换,但需要你做一些额外工作。
核心思路是:
- 移除旧的翻译器
- 安装新的翻译器
- 刷新所有 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 的国际化方案,说白了就是三步走:
- 写代码时用
tr()标记字符串 - 用
lupdate生成.ts,翻译后lrelease编译成.qm - 运行时用
QTranslator加载.qm,动态切换时刷新界面
这套方案成熟、稳定,而且 Qt 官方一直在维护。你想想看,从 Qt 4 到 Qt 6,tr() 的用法几乎没变过。这就是技术选型时「选大厂、选稳定」的好处。
下一章我会讲如何用 QML 做国际化,以及 qsTr() 和 tr() 的区别。到时候你会发现,QML 的国际化思路和 C++ 侧一脉相承,但细节上又有不少差异。