ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Qt/QML多语言国际化实战:从翻译管线到动态切换

Qt/QML多语言国际化实战:从翻译管线到动态切换

简介:面向 Qt 开发者的多语言国际化示例资源,特别适合希望为现有应用添加汉化与动态翻译支持的团队或个人,覆盖传统 C++ 界面与 Qml 界面两种体系,帮助解决跨语言版本维护和界面实时切换需求。资源内含 Translator_widget 与 Translator_qml 两个子项目,分别演示了从编写 .ts 翻译源文件、使用 lrelease 编译生成 .qm 消息文件,再到在应用中加载并动态切换语言的整体流程。其中 Qml 部分还展示了通过引擎上下文注入翻译对象,让界面文本实时更新的写法,对理解声明式界面下的翻译机制很有帮助。整套资源共 16 个文件,以 pro 工程配置、cpp 逻辑代码、h 头文件、qml 界面、ui 表单以及 ts/qm 翻译文件为主,压缩包仅 10KB,目录层级清晰,非常适合入门者快速理解 i18n 机制,也可作为项目里的最小参考实现。目前已有 1616 人学习下载,对需要实现多国语言无重启切换的 Qt 开发者,是一份简洁实用的参考资料,能直接对照工程学习语言文件的组织与加载细节。

1. Qt / Qml 多国语言:它不是字典表,而是一套带上下文的翻译管线

Qt / Qml 做多国语言,绕不开这套机制:源码里先写一种语言,通过工具把字符串抽成翻译文件,再在运行时按语言加载。很多人第一次接触时会误以为这就像在代码里维护一个HashMap<语言, HashMap<key, value>>,但真正上线后才发现,翻译上下文、占位符、复数形式、热切换这些事,自己造轮子只会越拖越重。Qt 自带的那套.ts/.qm翻译管线,简单场景一天能跑通,但要支撑产品长期迭代,你得先把tr、qsTr、lupdate、lrelease的分工理解透。

这篇笔记面向正在做跨平台桌面、嵌入式设备或移动端应用的开发者。我不讲概念,按「为什么用自带这套 → 怎么建工程 → 怎么把第一批翻译跑起来 → 上线前查哪些坑 → 动态切语言 → 维护成本」的顺序展开,直接给能抄的配置和代码。

2. 先理清分工:tr/qsTr 与 .ts/.qm 的关系,决定你后面少踩一半坑

2.1 三个文件、两个命令和一个编辑器

多语言方案里你会反复接触三种后缀:.ts、.qm、.qrc。.ts是翻译源文件,XML 格式,给人看、给人编辑,里面保存源语言文本、目标语言文本、翻译上下文和翻译状态;.qm是编译后的二进制文件,运行时加载速度更快、体积更小;.qrc是 Qt 资源定义,把.qm编译进程序内部,避免发布时漏掉语言包。

配合这套文件的是两个命令行工具:lupdate从源代码里扫描提取可翻译字符串,生成或合并.ts文件;lrelease把.ts编译成.qm。中间的人工翻译工作则交给 Qt Linguist 这个可视化编辑器,翻译人员不需要接触代码。

我一般会把语言文件放在工程里的i18n/目录,命名带语言后缀,例如app_zh_CN.ts、app_en.ts。这么做的好处是,发布脚本里能清晰看出每个语言包对应什么区域,后续做语言回退也方便。文件流的方向是:源码 →lupdate→.ts→ Linguist 人工翻译 →lrelease→.qm→ QTranslator 加载。

2.2 C++ 侧 tr() 与 QML 侧 qsTr():翻译上下文从哪来

很多人分不清tr和qsTr,以为只是语法不同。实际上它们的翻译上下文来源完全不一样。在 C++ 里,QObject::tr()属于某个类,Linguist 里看到的 context 就是类名,例如MainWindow;在 QML 里,qsTr()的 context 是它所在的 QML 文件的 basename,例如MainPage.qml。

这意味着同一个英文单词 "Open",在FileMenu类和DialogButton类里可以是两个不同的翻译条目。Qt 在生成.ts时会把 context 一并写入,翻译人员看到的是分组后的列表,而不是一长串平铺字符串。这个特性恰恰是自建字典表很难复制的——字典表通常只有 key 和 value,丢失了「这句话出现在哪个界面区域」的信息。

如果你在 C++ 里需要定义一组常量字符串,避免运行时才翻译,可以这样写:

// 用 QT_TRANSLATE_NOOP 预定义,稍后按上下文取出后再翻译 static const char *menuItems[] = { QT_TRANSLATE_NOOP("MenuBar", "Open"), QT_TRANSLATE_NOOP("MenuBar", "Save As..."), QT_TRANSLATE_NOOP("MenuBar", "Quit") };

这段代码里两行的参数说明:第一个字符串是翻译上下文MenuBar,第二个是源文本。lupdate会提取QT_TRANSLATE_NOOP中的字符串,但它不负责翻译,只是让字符串进入.ts文件。真正翻译时,你需要在代码里手动调用qApp->translate("MenuBar", menuItems[i])。这样做的价值在于,同一份菜单数组可以在代码里反复使用,而翻译状态始终由 Qt 统一管理。

2.3 自己手写语言表的四个坑

在没有 Qt 这类框架的年代,很多项目确实会自建一套语言表:定义一堆 ID,然后每个界面控件初始化时用GetText(ID)去查。我维护过一个模拟项目 X,代码里堆积了几千个ID_BUTTON_31这种命名,翻译人员完全不知道这个 ID 背后对应哪个按钮。这类方案有四个绕不开的坑:

第一,没有上下文。同一个词在菜单、提示框、标题栏里翻译可能不同,纯 ID 表只能靠开发者手工区分,最后往往演化成ID_OPEN_FILE_1、ID_OPEN_URL_2这种丑命名。第二,合并困难。产品每加一个文案,就需要同步修改所有语言文件,漏改只能靠人眼检查。第三,复数、占位符、数字格式这些语言特性,自建方案大多只做了%s替换,遇到英文单复数、俄语多复数、阿拉伯语的双数形式根本没法处理。第四,漏翻译检测跑在运行时,代码到处都是if (text == "")这种临时判断,而不是在发布前静态发现。

所以我的结论是:既然 Qt 已经把提取、合并、编译、回退这套流程做完整了,就不要再自己造一套并行的字典表。自建方案适合极少数不能引入 Qt 工具链的特殊环境,普通 Qt/Qml 项目用原生的这组工具是成本最低的选择。

2.4 最小工程骨架:qmake 和 CMake 都要给出 TRANSLATIONS

先看 qmake 写法,在.pro文件里声明语言文件:

# 列出所有想维护的翻译文件 TRANSLATIONS += \ i18n/app_zh_CN.ts \ i18n/app_en.ts \ i18n/app_ja.ts # 把 qm 编译进资源文件,运行时不依赖外部文件 RESOURCES += i18n/i18n.qrc

这段配置说明两件事:TRANSLATIONS告诉 Qt 工具链「这些是你要扫描合并的翻译目标」,i18n.qrc则在构建时把.qm打进程序资源。之后你再执行lupdate时,工具会根据项目中的SOURCES、HEADERS、以及QML文件自动提取字符串。

对应 CMake 工程,常见做法是这样:

set(TRANSLATIONS i18n/app_zh_CN.ts i18n/app_en.ts i18n/app_ja.ts ) qt5_create_translation(QM_FILES ${TRANSLATIONS})

这里的qt5_create_translation是一个 CMake 辅助函数,它会自动调用lupdate和lrelease,把生成的.qm文件名收集到QM_FILES变量里。需要注意的是,不同 Qt 版本里这个函数名称可能略有差异,Qt 6 下通常写作qt6_add_translation。如果你不想依赖 CMake 的自动化,也可以自己在构建脚本里直接调用lupdate和lrelease两个命令,效果是一样的,只是少了依赖追踪。

3. 从源字符串到第一批翻译文件落地:lupdate、Linguist、lrelease

3.1 lupdate 提取:只提源码,不提注释和运行时拼装的文本

执行lupdate之前,先确认源码里所有需要翻译的文本都写在tr()、qsTr()或QT_TRANSLATE_NOOP里。这是这个方案最关键的一条纪律。lupdate不是编译器,它只会提取你显式标记过的字符串;普通字符串字面量、从配置文件读进来的文案、用+拼装出来的句子,它都不会提取。

常用命令是这样:

# 从当前目录递归扫描,生成或合并 i18n/app_zh_CN.ts lupdate . -recursive -ts i18n/app_zh_CN.ts # 指定 QML 也参与扫描,避免漏掉 .qml 里的 qsTr lupdate . -recursive -extensions cpp,h,hpp,qml -ts i18n/app_zh_CN.ts

参数说明:.表示扫描当前目录下所有工程文件;-recursive让工具递归寻找子目录里的源码;-extensions明确列出参与扫描的文件后缀,多平台项目里如果只写了.cpp不带.qml,QML 文案就会全部漏掉,这是新手最容易翻车的地方。-ts指定目标文件,它可以写多个,例如-ts i18n/app_en.ts i18n/app_ja.ts一次更新多个语言。

执行后打开.ts,你会看到类似这样的结构:

<TS version="2.1" language="zh_CN"> <context> <name>MainPage</name> <message> <source>Open File</source> <translation>打开文件</translation> </message> </context> </TS>

这个 XML 的 meaning 很简单:context对应翻译分组,message是一条待翻译记录,translation是目标语言文本。lupdate的合并策略是:新发现的消息追加进去,已经存在的保留既有翻译,源码里已经删掉的消息标记为 obsolete。这个特性后面还会讲到。

3.2 用 Linguist 做一轮翻译:三种状态要分清

拿到.ts后,打开 Qt Linguist 进行人工翻译。界面左侧是 context 分组,中间是源文本,下方是翻译输入框。你需要注意三种状态标记:未翻译会显示一个感叹号,已翻译并匹配源文本的显示绿色对号,模糊翻译显示黄色问号。

模糊翻译往往来自源文本被修改过。例如源代码里原来写的是 "Open File",后来改成 "Open file",lupdate会保留旧翻译但把它标为模糊,提醒翻译人员检查大小写变化是否需要处理。实际项目里,我会要求翻译人员重点关注模糊条目,而不是一眼扫过。

翻译时如果源文本里有占位符,像%1或%2,务必原样保留。Linguist 默认会把占位符在源文本里高亮,方便对照。常见错误是翻译人员为了中文语序把%1和%2调换,这在某些语言里是合法的,但调换后参数错位的问题会在运行时暴露,后面避坑章节会专门讲。

3.3 lrelease 生成 qm:编译期就暴露问题

翻译完成后的命令很简单:

# 生成所有语言的 qm lrelease i18n/app_zh_CN.ts i18n/app_en.ts i18n/app_ja.ts # 或者一次编译目录下所有 ts lrelease i18n/*.ts

lrelease会统计每个文件里有多少条消息未翻译、多少条已翻译,并把结果输出到终端。如果你看到某个语言的 "unfinished" 数值很大,说明翻译没做完,这个语言包发布出去会出现大量英文或源码语言残留。我在发布脚本里会加一行判断,让lrelease遇到未翻译条目时返回非零状态,把构建直接卡住,避免漏翻译上线。

3.4 启动加载 QTranslator 的最小代码

程序启动时需要按当前语言加载对应的.qm。一个必须注意的细节是,QTranslator的加载顺序和加载位置会直接影响生效范围。我一般把语言初始化放在QApplication创建之后、窗口对象创建之前,这样所有控件一出生就拿到正确翻译。

#include <QApplication> #include <QTranslator> #include <QSettings> int main(int argc, char *argv[]) { QApplication app(argc, argv); // 从配置读取语言,默认中文 QSettings settings("MyCompany", "ExampleApp"); QString lang = settings.value("language", "zh_CN").toString(); QTranslator translator; // 从资源文件加载,路径前缀由 qrc 决定 bool loaded = translator.load(":/i18n/app_" + lang + ".qm"); if (loaded) { app.installTranslator(&translator); } // 语言状态保存到全局,方便后续动态切换 app.setProperty("currentLanguage", lang); return app.exec(); }

这是 C++ 入口的加载方式的一个常见简化写法。translator.load()返回 bool,条件不满足时不要往下执行installTranslator,否则等于装了一个空翻译器。这里把翻译器声明在栈上,生命周期覆盖整个main函数,避免局部变量提前析构导致翻译失效。QSettings保存语言值,用户切换后下次启动依然生效。需要指出的是,如果你的界面完全由 QML 写成,也可以在 QML 里用Qt.resolvedUrl加载.qm,但统一放在 C++ 入口更便于管理多语言状态。

3.5 手动跑一遍完整流程

把前面几步串起来,一套最简单的发布流程如下:

# 1. 更新所有语言文件 lupdate . -recursive -extensions cpp,h,hpp,qml -ts i18n/*.ts # 2. 人工翻译:打开每个 ts 文件进行处理 linguist i18n/app_en.ts # 3. 编译所有语言包 lrelease i18n/*.ts # 4. 构建并运行程序,切换系统语言验证

这套流程第一次跑通后,你就有了一个最基本的国际化基础。后面要做的两件事:一是把多语言做成运行时动态切换,二是把维护工作固定到自动化脚本里。接下来的避坑清单,是我在多语言项目里遇到过的真实问题,建议对照检查自己的代码。

4. 多语言上线前必看的避坑清单:现象、原因、解决

4.1 qsTr 直接返回原文,翻译完全不生效

现象:QML 文件里的qsTr("Open File")在界面上始终显示英文,即使.qm文件已经加载成功。

原因:这个场景绝大多数情况下是因为翻译上下文没匹配上。qsTr的上下文是当前 QML 文件的 basename,但lupdate扫描时用的文件名和运行时引擎记录的文件名不一致,例如文件名大小写不同、路径里混入了符号链接,就会导致 Linguist 里那条翻译挂在MainPage下,运行时却去查mainpage的上下文。另一种常见原因是,自定义 QML 组件没有继承QQmlEngine的翻译函数,某些非标准组件里qsTr实际是不可解析的。

解决:先确认.ts里 context 名,对比运行时实际发出的内容。可以临时在 QML 里写console.log(qsTr("Open File"))看输出,再和 Linguist 里的翻译对照。路径、大小写、文件名的任何不一致都会让翻译失效。若使用的是较老版本 Qt Quick Controls,还需要检查是否缺少QT_QML_DEBUG之类的限定。

4.2 源文本只改了一个字,翻译全部变成未翻译

现象:开发者调整了源码里的英文文案,比如 "Open File" 改成 "Open file",lupdate之后 Linguist 里出现一条新消息,旧消息被标为 obsolete,翻译人员不得不重新翻译一整条,而实际上改动只是大小写。

原因:Qt 的翻译条目以「源文本 + 上下文」的组合作为唯一标识。源文本发生任何字符级变化,包括空格、标点、大小写,lupdate都会认为这是一条新字符串。

解决:改文案前,先在 Linguist 里查看旧条目的翻译,然后在源码里同步修改,lupdate会把旧条目标记为模糊而不是新增。最好的习惯是让源文本尽量稳定,把措辞修改集中到某个版本,而不是频繁地小改。另外,发布前用lupdate -noobsolete清理那些已经失效的旧条目,避免.ts文件里堆积大量历史垃圾。

4.3 热切换语言后界面仍然显示旧语言

现象:程序运行中调用installTranslator换了一批.qm文件,窗口标题变了,但 QML 界面里大量文本纹丝不动。

原因:QML 的翻译结果不会自动重新求值。qsTr在组件首次创建时就把结果绑定到了文本属性上,之后语言变了,老字符串不会自动刷新,必须触发引擎重新遍历所有可翻译项。另一个常见原因是,多次切换语言时只installTranslator了新翻译器,没有先移除旧的,导致多个翻译器叠加,后面的查不到时回落到前面,看起来像没切换成功。

解决:切换顺序应当是先removeTranslator旧实例,再installTranslator新实例,然后触发刷新。具体做法下一章会给出完整代码。如果界面里有从 C++ 传入 QML 的字符串,这些字符串也要在 C++ 侧重新翻译一次并重新赋值。

4.4 qm 文件加载失败或者中文乱码

现象:在开发机上一切正常,打包后换一台机器运行,界面变成英文或出现带问号的乱码。

原因:.qm文件没有随程序一起发布,或者路径不对。当把.qm放进.qrc资源文件时,需要用:/前缀访问;如果误写了相对路径,程序会到工作目录找文件,而工作目录在双击启动时往往不是安装目录,于是加载失败。另一个原因是部分旧版本 Qt 在读取非 UTF-8 编码的.ts时会产生乱码。

解决:优先把.qm打成资源文件,确保安装包里一定有。加载时用QFile::exists做一次前置判断,translator.load()的返回值也一定要检查。对于编码问题,统一把.ts文件保存为 UTF-8,不要用带 BOM 的格式,否则某些老版本lrelease会把 BOM 当作源文本的一部分。

4.5 翻译把 %1 和 %2 调换后,运行时显示错乱

现象:一条消息里有两个占位符,原文是%1 个文件,耗时 %2 秒,法语翻译成%2 secondes pour %1 fichiers,结果界面上时间和数量互换了位置。

原因:这不是程序 bug,而是翻译导致参数顺序变化。Qt 允许翻译人员调整占位符顺序,翻译规范里这是合法的,但代码里如果按固定的索引位置取参数,就会出现语义错位。

解决:代码里尽量不要依赖占位符的顺序,或在文档里明确要求翻译人员保持物理顺序。复杂的本地化中,更好的做法是用命名占位符,比如%1在翻译后仍对应同一个源参数。还有一个强制手段:在lrelease之后写一个脚本,扫描.ts里的translation内容,统计占位符集合是否和source一致,不一致直接报警。这个检查我每次发版前都会跑一遍,省了不少线上问题。

5. 运行时动态切换语言:从重启到热更新的落地做法

5.1 最低成本方案:保存语言配置并重启

如果产品对切换语言没有实时性要求,最稳妥的做法是先保存设置,再重启应用。这个方案尤其适合嵌入式设备,界面资源有限,动态刷新的成本高,重启一次反而最简单。

void saveAndRestart(const QString &lang) { QSettings settings("MyCompany", "ExampleApp"); settings.setValue("language", lang); settings.sync(); // 重启当前进程 qApp->quit(); QProcess::startDetached(qApp->applicationFilePath(), QStringList()); }

这里的settings.sync()很关键,它保证配置写入磁盘,避免进程退出时数据还没落盘,重启后读取到旧语言。重启方案虽然粗暴,但它的优势是状态干净,不存在刷新不完整的问题。代价是用户切换语言时要经历一次闪退式的重启,体验一般,但可靠。

5.2 事件驱动热切换:installTranslator 加 retranslate

对桌面应用来说,更好的体验是热切换。核心思路只有两步:更换翻译器,然后让界面上所有已经显示的文本重新取一遍翻译。C++ 侧做起来相对简单,因为控件可以遍历,QML 侧则需要靠引擎层面的刷新机制。

class LanguageManager : public QObject { Q_OBJECT public: Q_INVOKABLE bool switchLanguage(const QString &lang) { if (lang == currentLang) { return true; } // 先移除旧翻译器,避免叠加 if (m_translator) { qApp->removeTranslator(m_translator); } static QTranslator newTranslator; if (!newTranslator.load(":/i18n/app_" + lang + ".qm")) { return false; } m_translator = &newTranslator; qApp->installTranslator(m_translator); currentLang = lang; emit languageChanged(); return true; } signals: void languageChanged(); private: QTranslator *m_translator = nullptr; QString currentLang; };

这个类用了static QTranslator来保证翻译器不会提前析构。每次切换都先移除旧实例,再安装新实例,这样查找链不会叠加。languageChanged信号发给 QML,QML 侧收到后主动刷新所有qsTr文本。注意static修饰的翻译器只能持有一个实例,代码里需要用一个成员指针记录当前实例,后面移除时才能找到正确的对象。

QML 侧可以这样连接:

Connections { target: languageManager function onLanguageChanged() { // 强制引擎重新加载翻译 engine.retranslate(); // 部分场景需要重新设置布局方向 window.layoutDirection = Qt.application.layoutDirection; } }

这里的retranslate()是引擎提供的刷新入口,它会向已注册的组件广播翻译变更信号。如果工程里的 QML 组件用了property string text: qsTr("..."),那么属性会自动重新求值。若某些文本来自 C++ 模型,需要在 C++ 侧收到languageChanged后更新模型数据,QML 再通过 binding 自动刷新。

5.3 刷新范围不止文本:字体、方向、日期与数字格式

切换语言不仅是换文字。阿拉伯语、希伯来语需要从左到右反转成从右到左,日期格式、数字分隔符、货币符号都需要跟着区域变化。很多人只换了文本,结果阿拉伯语界面排版全乱。

我一般会在语言变化后同步做这几件事:

刷新项处理方式关联对象
文本翻译retranslate()或重新赋值QML Text / Button / Label
布局方向setLayoutDirection(Qt::RightToLeft)QQuickWindow / QApplication
日期时间用QLocale(lang)重新格式化所有日期型展示
数字与小数用QLocale的toString系列统计、金额
字体族中文、日文、阿拉伯文可能需要不同的 fallback 字体全局字体设置

处理日期最笨也最可靠的方法是,让所有展示日期时间的地方不要直接拼字符串,而是统一走一个工具函数:

QString formatDateTime(const QDateTime &dt, const QString &lang) { QLocale locale(lang); return locale.toString(dt, QLocale::ShortFormat); }

这样的好处是,切换语言后只要语言参数变了,所有调用点自动获得新格式,不需要每个界面单独改。字体方面,如果你发现阿拉伯文或泰文显示成方框,优先检查系统是否有对应字体,并在 QML 的font.family里设置一个覆盖多种文字的字体栈。

5.4 一段可复制的 LanguageManager 整合代码

把上面几个部分拼起来,我会这样组织主入口:

int main(int argc, char *argv[]) { QApplication app(argc, argv); LanguageManager langManager; langManager.switchLanguage(readSavedLanguage()); QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("languageManager", &langManager); engine.load(QUrl(QStringLiteral("qrc:/Main.qml"))); return app.exec(); }

这段代码把languageManager暴露给 QML,QML 里的按钮切换语言时直接调用languageManager.switchLanguage("ja"),然后通过信号刷新界面。注意switchLanguage返回 bool,如果.qm不存在会返回false,这时候要弹提示而不是让界面处于半切换状态。集成这个模式后,动态语言切换就能稳定工作了。还有一点小经验:调试时在 QML 里打印qsTr的返回值,能快速判断当前实际生效的是哪个翻译器,比逐个界面排查快得多。

6. 把多语言做成可维护的系统:复数、回退与自动化验证

6.1 复数规则与占位符:翻译不只是逐字替换

Qt 的翻译系统支持复数形式,你可以用%n来让不同语言展示不同的单复数形态。例如qsTr("%n file(s)"),在英文里会生成 "1 file" 和 "2 files",在俄语里则会按 1、2-4、5+ 等规则生成不同条目。Linguist 的复数编辑器会列出目标语言的所有复数变体,翻译人员需要把每个变体都填上,漏填会导致运行时回退到源码语言。

占位符数量也需要注意。不要在一个字符串里塞太多%1 %2 %3,一旦超过三个,翻译人员在调整语序时极易出错。更优雅的做法是拆成短句,让每条翻译消息保持独立,例如把 "You have %1 unread messages, %2 of them are urgent" 拆成两条,虽然多了一次翻译,但维护成本大幅下降。

6.2 多级回退链:防止小语种缺失直接崩到英文

一个产品通常不会把每种语言都翻译到 100%。常见做法是维护回退链:请求的语言找不到翻译时,依次回退到备选语言,最后回退到源语言。Qt 的 QTranslator 自带查找机制,加载较多语言包时,翻译器会按安装顺序从后往前查找,所以回退链可以通过加载顺序实现:

// 先装回退语言,再装首选语言 if (lang != "en") translatorEn.load(":/i18n/app_en.qm"); if (lang != "zh_CN") translatorZh.load(":/i18n/app_zh_CN.qm"); app.installTranslator(&translatorEn); app.installTranslator(&translatorZh);

这段代码里,translatorZh是后安装的首选翻译器,查找时会优先命中;找不到时再落到translatorEn。这个模式对小语种非常有用:法语缺失的翻译会显示英文,而不是显示中文。回退链设计是国际化长期运维的核心,语言覆盖率永远不可能 100%,有一套明确的回退策略比追求全量翻译更实际。

6.3 用伪翻译自动化检查截断与漏翻译

伪翻译是指把源语言自动替换成带前缀的特殊字符串,例如给每个翻译文本前后加上[#],运行界面时看到哪个位置没有[#],就说明那个文本没有走翻译系统。这个技巧能一次性暴露硬编码字符串、漏提取的qsTr以及布局截断问题。

自动化检查可以分成三层:第一层,lupdate后统计每个.ts的 unfinished 数量;第二层,用脚本对比.ts中 source 和 translation 的占位符集合;第三层,在 CI 里构建一版伪翻译语言包,跑一遍截图对比。这三层做完,多语言上线前的质量问题基本都能兜住。我的习惯是把第一层和第二层做成发布脚本的一部分,不满足条件直接构建失败,而不是靠人工在 Linguist 里肉眼检查。

6.4 中文输入法与字体回退的几个附带注意

如果你的源语言是中文,翻译成其他语言后,字体通常要按目标语言调整。中文用系统中文字体,日文需要日文字体,阿拉伯文需要 RTL 字体。QML 里可以通过Qt.application.font设置全局面板默认字体,但单一字体无法覆盖所有文字,所以字体回退顺序很重要。另一个容易被忽略的是输入法:界面切换成日文后,文本输入框的输入法候选项还是跟随系统输入法,不建议在应用层强行绑定输入法,否则会破坏用户的操作习惯。

做多语言久了,我的最大教训是:不要把翻译当成上线前的一个周末任务,而是从第一天就把所有用户可见文本收口到tr()/qsTr(),并且让源语言保持稳定。语言本质上和代码一样,是需要持续维护的资产。上面这套流程看着繁琐,但真正坚持跑上两三个版本,你的团队就会形成习惯,翻译质量也只会越做越稳。希望这份笔记能帮你在 Qt/Qml 项目里少走几步弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进