Qt 的国际化机制并不是简单地把中文替换成英文,而是建立在“源码字符串—翻译上下文—翻译文件—运行时加载”这一完整链路之上。其中,tr() 是 Qt 国际化开发中最常用的入口函数。理解 tr() 的工作原理、字符串提取方式以及编码处理规则,有助于避免中文乱码、翻译失效、上下文错误等常见问题。
Qt 国际化为什么需要 tr 函数
Qt 应用程序中的界面文本通常直接写在 C++ 源代码中,例如:
C++QPushButton *button = new QPushButton("确定");
如果程序只面向中文用户,这种写法没有问题。但当软件需要支持英文、日文、韩文等语言时,直接使用固定字符串会让后续维护变得困难。
Qt 提供了 tr() 机制,将“程序中的原始文本”和“最终显示文本”分离。例如:
C++QPushButton *button = new QPushButton(tr("确定"));
这里的 "确定" 不再被简单视为最终显示内容,而是一个可翻译的源字符串。
当应用程序切换到英文环境后,Qt 可以根据加载的翻译文件,将:
确定
转换为:
OK
因此,tr() 的核心价值并不是翻译本身,而是为 Qt 的国际化系统建立一个明确的翻译入口。
需要注意的是,tr() 并不会自己联网调用翻译服务,也不会自动把中文翻译成英文。它主要负责根据上下文查找已经准备好的翻译结果。
tr 函数的基本使用方式
最常见的写法如下:
C++QString text = tr("Hello World");
在 Qt 窗口类中,则经常看到:
C++QLabel *label = new QLabel(tr("用户名")); QPushButton *button = new QPushButton(tr("登录"));
如果使用 Qt Designer 创建界面,生成的 ui_xxx.h 文件中也经常出现类似:
C++label->setText(QCoreApplication::translate("MainWindow", "用户名"));
这两种形式实际上属于同一套国际化体系。
例如定义一个窗口类:
C++class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); private: QLabel *label; };
对应实现:
C++MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { label = new QLabel(tr("欢迎使用系统"), this); }
Qt 的翻译工具能够识别这种字符串,并将其提取到 .ts 文件中。
tr 函数背后的工作原理
理解 tr() 最重要的是明确一个事实:它并不是普通的字符串替换函数。
例如:
C++tr("文件")
Qt 实际上需要知道几个关键信息:
-
当前翻译属于哪个上下文;
-
原始字符串是什么;
-
当前加载了什么语言;
-
翻译文件中是否存在对应翻译;
-
如果存在,应该返回什么内容。
Qt 的翻译系统通常可以抽象成:
源代码 ↓ tr("文件") ↓ 提取翻译源文本 ↓ .ts 翻译文件 ↓ lrelease ↓ .qm 二进制翻译文件 ↓ QTranslator 加载 ↓ 运行时查找翻译 ↓ 显示目标语言文本
例如源代码:
C++label->setText(tr("文件"));
翻译文件可能包含:
XML文件 File
程序启动后加载对应的 .qm 文件,调用 tr("文件") 时,Qt 就能够找到 File。
如果没有找到翻译结果,Qt 通常会返回原始字符串,因此:
C++tr("文件")
即使没有加载翻译文件,也不会直接变成空字符串,而仍然可以显示:
文件
这也是 Qt 国际化机制比较实用的一点。
QObject::tr 与类上下文
tr() 与 Qt 的对象上下文关系非常重要。
例如:
C++class LoginWindow : public QWidget { Q_OBJECT public: LoginWindow(QWidget *parent = nullptr); };
在成员函数中:
C++LoginWindow::LoginWindow(QWidget *parent) : QWidget(parent) { setWindowTitle(tr("用户登录")); }
此时翻译上下文通常与 LoginWindow 类相关。
最终 Qt 翻译系统实际上并不只根据 "用户登录" 查找,而是会结合上下文进行匹配。
这解决了一个非常实际的问题。
假设程序中有两个地方都使用:
C++tr("Open")
一个表示“打开文件”,另一个表示“开放状态”,那么它们在不同上下文中可能对应不同的翻译。
因此,Qt 国际化中的关键概念之一就是:
Context + Source Text
也就是:
翻译上下文 + 原始字符串
而不是简单的:
原始字符串 → 翻译字符串
为什么相同字符串可能需要不同翻译
例如:
C++tr("Open")
在文件菜单中可能表示:
打开
而在某个状态描述中可能具有其他含义。
如果所有字符串都没有上下文,翻译人员很难判断具体语义。
Qt 可以利用类上下文帮助区分这些内容。
例如:
C++class FileWindow : public QWidget { Q_OBJECT };
和:
C++class NetworkWindow : public QWidget { Q_OBJECT };
分别调用:
C++tr("Open")
翻译系统就可以根据不同上下文处理。
对于需要进一步明确语义的场景,还可以使用带注释的翻译形式,例如:
C++tr("Open", "Open file");
第二个参数主要用于帮助翻译人员理解上下文,而不是直接作为用户界面文本。
Qt 国际化中的 QObject::tr 和 QCoreApplication::translate
在 Qt 项目中,还经常看到:
C++QCoreApplication::translate("MainWindow", "文件");
它与:
C++tr("文件")
存在密切关系。
对于继承自 QObject 的类,成员函数中的:
C++tr("文件")
本质上会使用当前类的翻译上下文进行查找。
而:
C++QCoreApplication::translate("MainWindow", "文件");
则可以显式指定上下文。
例如:
C++QString text = QCoreApplication::translate("MainWindow", "文件");
这在 Qt Designer 生成的代码中十分常见。
因此,看到 .ui 生成代码中的 QCoreApplication::translate() 时,不需要认为它是另一套国际化机制,它仍然属于 Qt 的标准翻译系统。
Qt 国际化文件的组成
Qt 国际化开发通常涉及两类主要文件:
.ts .qm
其中 .ts 是可编辑的翻译源文件,而 .qm 是程序运行时使用的二进制翻译文件。
TS 文件
.ts 文件本质上是 XML 格式,可以直接查看。
例如:
XMLversion="2.1" language="zh_CN"> MainWindow Login 登录
其中:
-
表示翻译上下文; -
表示源字符串; -
表示目标语言翻译。
开发人员或翻译人员通常修改的是 .ts 文件。
QM 文件
.qm 是经过编译后的翻译文件。
通常通过:
Bashlrelease app_zh_CN.ts
生成:
app_zh_CN.qm
程序运行时加载 .qm 文件,而不是直接解析 .ts。
这样可以减少运行时开销,同时降低翻译文件体积。
lupdate 与 lrelease 分别负责什么
Qt 国际化开发中,lupdate 和 lrelease 是两个非常关键的工具。
lupdate:提取翻译字符串
例如源代码:
C++setWindowTitle(tr("用户管理"));
执行:
Bashlupdate .
Qt 会扫描项目源代码,并将需要翻译的字符串同步到 .ts 文件。
它解决的是:
源代码 → TS 文件
的问题。
lrelease:编译翻译文件
翻译完成后:
Bashlrelease app_zh_CN.ts
生成:
app_zh_CN.qm
它解决的是:
TS 文件 → QM 文件
的问题。
完整流程可以简单理解为:
C++源码 ↓ lupdate ↓ .ts ↓ 翻译 ↓ lrelease ↓ .qm ↓ QTranslator ↓ 程序运行
QTranslator 如何加载翻译文件
有了 .qm 文件之后,还需要在程序运行时加载它。
典型代码如下:
C++QTranslator translator; if (translator.load("app_zh_CN.qm")) { qApp->installTranslator(&translator); }
通常还会根据系统语言动态选择不同翻译文件。
例如:
app_zh_CN.qm app_en_US.qm app_ja_JP.qm
程序根据当前语言环境加载对应文件。
如果用户切换语言,还可以重新加载翻译文件。
Qt 中的编码处理为什么重要
Qt 国际化经常与中文乱码问题联系在一起,原因主要来自字符串编码、源代码编码、编译器处理方式以及翻译文件编码等多个环节。
现代 Qt 项目通常使用 UTF-8 编码保存源代码,例如:
C++QString text = tr("中文测试");
如果源文件本身使用 UTF-8,并且开发环境、编译器以及 Qt 配置能够正确处理 UTF-8,那么通常不需要手动进行编码转换。
需要避免的是把 UTF-8 字符串错误地当成其他编码处理。
QString 与 UTF-16 的关系
Qt 的 QString 是理解编码问题的关键。
QString 用 Unicode 表示文本,Qt 内部传统上以 UTF-16 作为主要存储形式。
例如:
C++QString text = QStringLiteral("中文");
这里的字符串已经进入 Qt 的 Unicode 字符串体系。
如果需要处理 UTF-8 数据,可以使用:
C++QByteArray data = "中文";
或者进行明确转换:
C++QByteArray utf8 = text.toUtf8();
反过来:
C++QString text = QString::fromUtf8(utf8);
因此需要区分:
QString ↓ Unicode文本 QByteArray ↓ 字节序列
不能简单地把 QByteArray 和 QString 当成同一种类型。
QStringLiteral 与 tr 的区别
Qt 开发中经常看到:
C++QStringLiteral("Hello")
和:
C++tr("Hello")
两者解决的问题并不相同。
QStringLiteral() 主要用于构造编译期 Unicode 字符串。
例如:
C++QString text = QStringLiteral("欢迎");
而:
C++tr("欢迎")
则表示该文本参与 Qt 翻译机制。
因此:
C++QStringLiteral("欢迎")
并不能替代:
C++tr("欢迎")
如果一个字符串需要国际化,应优先使用:
C++tr("欢迎")
如果字符串本身不需要翻译,则可以使用:
C++QStringLiteral("内部标识")
为什么 QByteArray 不应该直接代替 QString
下面这种代码容易引起编码问题:
C++QByteArray data = "中文"; QString text(data);
如果程序对编码来源没有明确约定,就可能产生不可预期的结果。
对于 UTF-8 数据,更明确的写法是:
C++QString text = QString::fromUtf8(data);
如果需要输出 UTF-8:
C++QByteArray data = text.toUtf8();
这种方式能够明确告诉程序:
输入:UTF-8字节 输出:QString Unicode文本
编码边界清晰后,乱码问题通常更容易排查。
tr 函数中的中文乱码如何排查
如果出现:
中文显示为乱码
不要只检查 tr() 本身。
建议按照以下链路逐层排查。
检查源文件编码
首先确认 .cpp、.h 文件是否保存为 UTF-8。
现代 IDE 一般可以直接查看或修改文件编码。
如果源代码本身已经损坏,例如:
C++tr("鐢ㄦ埛")
那么后面的国际化工具自然也无法恢复原始中文。
检查 TS 文件
打开 .ts 文件,确认:
XML用户登录
是否正常。
如果 .ts 中已经是乱码,那么问题通常发生在字符串提取之前或源文件编码阶段。
检查 QM 文件
如果 .ts 正常,但程序显示错误,则重新执行:
Bashlrelease app_zh_CN.ts
确保 .qm 与最新 .ts 文件一致。
检查翻译文件是否真的加载
例如:
C++QTranslator translator; bool ok = translator.load("app_zh_CN.qm"); qDebug() << "translation loaded:" << ok; if (ok) { qApp->installTranslator(&translator); }
如果输出:
translation loaded: false
那么重点应该检查 .qm 文件路径,而不是继续修改 tr()。
翻译文件路径是常见问题
很多国际化问题实际上并不是编码问题,而是翻译文件没有被正确加载。
例如:
C++translator.load("app_zh_CN.qm");
使用相对路径时,文件查找位置通常与当前工作目录有关。
开发环境中能够正常运行,并不代表打包后的程序也能找到文件。
更可靠的方式是使用 Qt 资源系统。
例如将翻译文件加入:
resources.qrc
然后:
C++translator.load(":/translations/app_zh_CN.qm");
这样翻译文件可以直接打包到程序资源中,减少部署时遗漏文件的风险。
动态切换语言时需要注意什么
Qt 支持在程序运行过程中重新安装翻译器。
例如:
C++qApp->installTranslator(&translator);
当语言发生变化时,通常需要让界面重新生成文本。
Qt 的界面类可以通过重新实现:
C++void changeEvent(QEvent *event) { if (event->type() == QEvent::LanguageChange) { // 重新设置界面文本 } QWidget::changeEvent(event); }
如果使用 Qt Designer,通常会调用:
C++ui->retranslateUi(this);
例如:
C++void MainWindow::changeEvent(QEvent *event) { if (event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); } QMainWindow::changeEvent(event); }
这样语言切换后,按钮、标签、菜单等文本才能及时更新。
不要把需要翻译的文本写死在其他位置
下面这种写法不利于国际化:
C++QString title = "设置";
更合理的是:
C++QString title = tr("设置");
同样,菜单、按钮、提示框、状态栏等面向用户的文字,都应该尽可能进入翻译系统。
例如:
C++QMessageBox::information( this, tr("提示"), tr("操作完成") );
这样能够统一进入 Qt 国际化流程。
格式化字符串中的 tr 使用方式
带动态参数的文本也应该参与翻译。
例如:
C++QString message = tr("当前共有 %1 个文件").arg(count);
翻译文件可以根据目标语言调整句子结构。
例如英文:
There are %1 files.
这种方式比:
C++QString message = tr("当前共有") + QString::number(count) + tr("个文件");
更加合理。
因为不同语言的语法结构并不相同,把一句完整语义交给翻译系统,翻译人员才能调整单词顺序。
复数形式不能简单拼接
国际化开发中还需要考虑复数。
例如:
C++tr("%n file(s)", "", count);
Qt 可以根据 %n 以及数量处理不同语言的复数规则。
不同语言的复数变化并不完全一致,因此不要简单依赖:
C++count == 1 ? tr("file") : tr("files");
虽然这种方式在部分项目中可以工作,但面对更多语言时维护成本明显增加。
使用 Qt 提供的复数翻译机制通常更加合理。
tr 不应该用于所有字符串
国际化并不意味着程序里的每个字符串都必须调用 tr()。
例如:
C++QString key = QStringLiteral("user_id"); QString api = QStringLiteral("/api/user/login");
这些字符串属于程序内部协议或数据标识,一般不需要翻译。
如果强行使用:
C++tr("user_id")
反而会增加 .ts 文件中的无意义内容。
可以遵循一个简单原则:
用户能够看到并且需要根据语言变化的文本,应考虑使用
tr();程序内部标识、协议字段、文件格式关键字通常不应该翻译。
使用 tr 时常见的几个错误
错误一:字符串写法不符合提取规则
例如把需要翻译的文本拆得过于复杂:
C++QString text = someFunction("登录");
如果没有使用 Qt 的翻译入口,lupdate 不一定能够按照预期提取。
应该明确写成:
C++QString text = someFunction(tr("登录"));
错误二:修改了 TS,却没有重新 lrelease
修改:
XMLLogin
之后,如果程序继续使用旧的 .qm 文件,界面仍然不会发生变化。
需要重新执行:
Bashlrelease app.ts
错误三:只修改 TS,不检查上下文
如果:
XMLMainWindow
而程序实际使用的是另一个上下文,那么翻译可能无法匹配。
错误四:把编码问题误认为翻译问题
如果:
.ts 正常 .qm 正常
但程序显示乱码,那么应该继续检查运行时数据、字体、编码转换以及显示链路,而不是盲目修改翻译文本。
Qt 国际化项目的推荐目录结构
中大型 Qt 项目可以将翻译文件独立管理,例如:
MyApp/ ├── src/ │ ├── main.cpp │ ├── mainwindow.cpp │ └── mainwindow.h ├── translations/ │ ├── app_zh_CN.ts │ ├── app_en_US.ts │ └── app_ja_JP.ts ├── resources/ │ └── translations.qrc └── CMakeLists.txt
这样可以让源代码、翻译源文件和资源文件职责更加清晰。
如果使用 CMake,还可以结合 Qt 提供的国际化相关 CMake 工具完成翻译文件的更新和编译,从而减少手工执行命令的工作量。
Qt 5 与 Qt 6 国际化开发的注意事项
Qt 5 和 Qt 6 都支持 tr()、QTranslator、.ts、.qm 这一核心国际化体系。
但是实际项目中使用的构建工具、CMake 配置以及 Qt Linguist 工具链可能有所差异。
尤其是在 Qt 6 项目中,推荐结合现代 CMake 工作流管理翻译文件,而不是完全依赖手动执行命令。
无论使用哪个版本,都应该保持以下基本原则:
源码统一使用Unicode文本 ↓ 需要显示给用户的文本使用tr() ↓ lupdate提取 ↓ 翻译TS文件 ↓ lrelease生成QM ↓ QTranslator加载
这套流程稳定之后,Qt 国际化开发并不复杂。
一套完整的 Qt 国际化示例
假设程序中存在一个按钮:
C++QPushButton *button = new QPushButton(tr("开始运行"), this);
使用 lupdate 后,生成类似:
XMLMainWindow 开始运行 Start
然后执行:
Bashlrelease app_en_US.ts
生成:
app_en_US.qm
程序启动时:
C++QTranslator translator; if (translator.load(":/translations/app_en_US.qm")) { qApp->installTranslator(&translator); }
之后:
C++tr("开始运行")
就可能得到:
Start
如果没有安装翻译器,则仍然可以显示:
开始运行
这正是 Qt 国际化机制“默认显示源文本、加载翻译后显示目标文本”的基本工作模式。
如何建立稳定的 Qt 国际化编码规范
对于团队项目,可以制定一套统一规则。
第一,源代码统一使用 UTF-8。
第二,所有面向用户的文本统一通过 tr() 进入翻译系统。
第三,内部变量、协议字段、数据库字段和文件格式关键字不参与翻译。
第四,禁止随意对 QString 和 QByteArray 进行隐式编码转换。
第五,所有翻译文件统一经过 lrelease 生成 .qm。
第六,程序启动时明确检查 QTranslator::load() 的返回值。
第七,动态切换语言时正确处理 QEvent::LanguageChange。
第八,发布程序时将 .qm 文件与程序资源、安装目录保持一致。
做到这些之后,Qt 国际化出现乱码或翻译不生效的问题会明显减少。
总结
tr() 是 Qt 国际化体系中的核心入口,它真正解决的是“让程序中的用户界面文本具备可翻译能力”,而不是直接承担翻译工作。
完整的 Qt 国际化流程可以概括为:
tr() ↓ lupdate ↓ .ts ↓ 人工翻译 ↓ lrelease ↓ .qm ↓ QTranslator ↓ 运行时翻译
编码处理则贯穿整个过程。源代码应保持统一的 Unicode 编码,QString 用于处理文本,QByteArray 更适合表达原始字节数据,在 UTF-8 数据与 QString 之间转换时应明确使用 fromUtf8() 和 toUtf8()。
真正遇到中文乱码时,也不要只盯着 tr()。应该依次检查源代码编码、TS 文件内容、QM 文件生成、翻译器加载、资源路径以及运行时数据转换。只有把整个国际化链路串起来,才能快速定位问题。
对于需要支持多语言的 Qt 桌面应用,合理使用 tr()、规范管理 .ts/.qm 文件,并建立统一的编码规范,不仅能够降低后期维护成本,也能让语言切换、翻译更新和软件发布更加稳定。