第3章多国语言国际化 如果软件产品的用户是国际用户,就会涉及中英文翻译问题。因为给国外用户使用的产品可能需要英文界面,给国内用户使用的产品需要中文界面。这就带来一个问题: 只有一套界面但要提供两种语言,这该怎么办呢?别急,Qt提供了多国语言国际化(简称国际化)方案来解决这个问题。 3.1案例5怎样实现国际化 视频讲解 本案例对应的源代码目录: src/chapter03/ks03_01。程序运行效果见图31。 图31案例5运行效果 Qt提供的方案其实也很简单: 显示文本时调用特定的翻译接口,然后需要开发者提供一个中英文对照的qm文件(二进制翻译文件),最后在程序启动时加载这个翻译文件。下面介绍具体步骤。 (1) 在ui文件或代码中使用英文。 (2) 在提供翻译的类中编写Q_OBJECT宏。 (3) 在pro文件中添加TRANSLATIONS配置。 (4) 使用lupdate命令,提取待翻译内容到ts文件。 (5) 使用linguist(Qt语言家)在ts文件中添加中英文对照翻译,并导出qm文件。 (6) 程序启动时加载qm文件。 现在分步骤进行详细介绍。 1. 在ui文件或代码中使用英文 首先,在ui文件或者在编程时需要显示汉字的地方使用英文。在ui中显示文本时直接键入英文即可,在编程中显示文本时需要调用类的tr()接口进行翻译: m_pLabel2->setText(tr("this is translated by source code")); tr()接口是QObject类的接口,所以调用tr()的类要从QObject类派生。如果待翻译的文本所在的类不是QObject的派生类,那么请使用QObject类或它的派生类来调用tr()接口。 2. 在提供翻译的类中编写Q_OBJECT宏 如果某个类中有文本需要翻译,那么除了要求该类从QObject类派生,还需要使用Q_OBJECT宏。假设类名为CDialog,就需要在CDialog类定义开头添加Q_OBJECT宏,如代码清单31所示。 代码清单31 class CDialog : public QDialog { Q_OBJECT ... }; 如果不编写Q_OBJECT宏,那么在使用lupdate命令提取ts文件时将会报错。 ks03_01/dialog.cpp:7: Class 'CDialog' lacks Q_OBJECT macro Updating 'ks03_01.ts'... 该错误提示的含义是: 在更新ks03_01.ts文件时发现类CDialog缺少Q_OBJECT宏。 3. 在pro文件中添加TRANSLATIONS配置 ts文件是Qt用来进行中英文翻译的文本文件,通过Qt的lupdate命令提取得到ts文件,然后由人工完成翻译。如果想得到ts文件,需要在ks03_01.pro中添加如下内容: TRANSLATIONS = ks03_01.ts ks03_01.ts是lupdate命令提取到的ts文件名称。在配置TRANSLATIONS时也可以带文件路径。 TRANSLATIONS = $$TRAIN_SRC_PATH/translations/ks03_01.ts 这表示将lupdate命令提取的ts文件放到项目的src/translations目录下,文件名称为ks03_01.ts。 4. 使用lupdate命令,提取待翻译内容到ts文件 完成pro文件中的TRANSLATIONS配置后,执行lupdate命令。 lupdate ks03_01.pro lupdate将读取pro文件中的TRANSLATIONS配置并读取源代码文件,将待翻译的文本提取到TRANSLATIONS配置项所指示的ts文件。如果未配置TRANSLATIONS将导致lupdate命令执行失败。 5. 使用linguist在ts文件中添加中英文对照翻译,并导出qm文件 启动linguist,选择【文件】|【打开】菜单项打开ts文件。然后选择【上下文】中的类名,在【字符串】里选中某行源文,将翻译后的文本写在【Translation to 简体中文(中国)】下面的文本框内(见图32)。 图32Qt语言家界面 注意: 标点符号也要一一翻译。 完成一个源文的翻译后,单击源文前面的“?”(见图33)并将其改为“√”。 图33未翻译的源文 完成全部翻译工作后,可以查看图33中【上下文】列表框的内容,检查是否还有未翻译的项目(未翻译的源文前面显示“?”,已翻译的显示“√”)。完成所有翻译后,将ts文件发布为二进制的qm文件。方法是选择【文件】|【另外发布为】菜单项,然后选择发布目录即可。比如,可以将qm文件发布到 $$(PRJROOT)/system/lang目录下。 6. 程序启动时加载qm文件 在main()函数或其他合适的位置加载qm文件。 1) 首先包含所需的头文件(见代码清单32) 代码清单32 #include #include // 国际化 #include // 国际化 2) 加载Qt自带的翻译文件 Qt自带的翻译文件用来实现Qt类中文本的翻译。如代码清单33所示,在标号①处得到本机的语言环境,在标号②处构建QTranslator对象,然后在标号③处安装翻译文件。标号③处调用了QTranslator的load()接口,该接口的参数1用来描述翻译文件名,参数2用来描述翻译文件所在目录。 代码清单33 // 安装Qt自带的中文翻译 const QString localSysName = QLocale::system().name(); // 获取本机系统的语言环境 ① QScopedPointer qtTranslator(new QTranslator(QCoreApplication::instance())); ② if (qtTranslator->load(QStringLiteral("qt_") + localSysName, QLibraryInfo:: location(QLibraryInfo::TranslationsPath))) ③ QCoreApplication::installTranslator(qtTranslator.take()); } Qt的翻译文件并未把所有英文都翻译成中文(比如从Designer中拖出的QDialogButtonBox中的OK、Cancel按钮上的文本),开发者需要自己翻译这些英文。翻译Qt自带文本的方法是将这些文本按照ts文件的格式键入ts文件(见代码清单34)。之间是QPlatformTheme类的翻译内容。name用来描述类名,每一组message用来描述一个源文和翻译的对照,其中source表示源文,translation表示翻译,location表示包含源文的代码行(可能不止一处)。完成人工翻译后,用linguist进行发布即可。 代码清单34 QPlatformTheme OK 确定 Cancel 取消 为什么QDialogButtonBox中的按钮没有被翻译成中文呢?这是因为在Qt的源代码里,为QDialogButtonBox的按钮进行翻译时,使用的是QPlatformTheme类。 QCoreApplication::translate ("QPlatformTheme" , "OK" ); 这里的ts文件可以作为Qt的补丁。可以将上述ts文件的内容专门保存为一个公共的ts文件,然后把这个文件提供给各项目组使用。 3) 加载项目的翻译文件 加载完Qt自带的翻译文件后,应加载项目的翻译文件,见代码清单35。 代码清单35 QString strPath = qgetenv("TRAINDEVHOME"); // 获取环境变量所指向的路径 strPath += "/system/lang"; // $TRAINDEVHOME/system/lang/ks03_01.qm QScopedPointer gpTranslator(new QTranslator(QCoreApplication::instance())); if (gpTranslator->load("ks03_01.qm", strPath)) { ① QCoreApplication::installTranslator(gpTranslator.take()); } 国际化在Qt软件开发过程中是非常重要的组成部分。即使目前没有计划将产品推向国际市场,软件开发者也应该养成使用国际化进行编程的习惯。因为一旦将来需要将产品推向国际市场,无须对源代码做任何修改就能马上推出产品,这会减少很多不必要的工作量。 3.2知识点几种常见的国际化编程场景 视频讲解 3.1节介绍了国际化开发的基本步骤,本节将介绍几种常见的国际化编程场景,源代码目录: src/chapter03/ks03_02。程序运行效果见图34。这些场景的核心内容虽然一样, 图34ks03_02运行效果 但是分别采用了不同的编程方式来实现国际化。本节介绍4种常见的国际化编程场景。 (1) 在UI界面文件中使用英文。 (2) 在代码中使用tr(字符串常量)。 (3) 在代码中使用tr(字符串变量)。 (4) 在非QObject派生类中实现国际化。 下面进行详细介绍。 1. 在UI界面文件中使用英文 在界面中使用英文是一种常见的国际化编程场景。在使用Designer绘制界面时,直接键入英文,然后按照3.1节介绍的步骤执行即可实现国际化。 2. 在代码中使用tr(字符串常量) 在代码中使用tr(字符串常量)的场景其实在3.1节介绍过,即在类中使用tr("xxx")。这要求类从QObject派生,并且类定义的开头要加上Q_OBJECT宏。 3. 在代码中使用tr(字符串变量) 当同一个类的多处代码存在相同的待翻译文本时,可以使用tr(字符串变量)的方法解决。如果是不同类的代码中使用同一个文本的情况,应参考下面将要讲述的“在非QObject派生类中实现国际化”方案进行处理。对于同一个类的不同代码行中出现的相同文本,可以定义一个const字符串变量来存储,这样做有两个原因: (1) 避免在多处使用时因笔误导致拼写错误。 (2) 如果需要修改源文,只需要在定义const字符串变量的代码处改一次就可以了。 使用tr(字符串变量)的代码可以这样写: const char* c_strInfo = "cannot save file: disk full!"; cout << tr(c_strInfo) << endl; 现在出现一个问题: 如果能通过定义字符串变量的方式实现国际化编程,为什么不直接定义一个字符串数组呢?这样就可以通过数组访问这些源文,不是更方便吗?这么做听上去不错,但Qt的lupdate命令无法提取字符串数组中的源文,这会导致ts文件中不含这些源文。那该怎样解决这个问题呢?一个可行的方法是: 在使用字符串数组的类的构造函数中把所有字符串用tr调用一遍,如代码清单36所示。 代码清单36 static const char* s_description [] = { "X-Axis", "X-Grid", "Y-Axis", "Y-Grid"}; static const char* s_coordinate [] = { "X", "Y", "width", "height"}; CMyClass:: CMyClass() { // 下面的代码是为了让lupdate可以自动提取源文 { tr("X-Axis");tr("X-Grid");tr("Y-Axis");tr("Y-Grid"); ① tr("X"); tr("Y"); tr("width"); tr("height"); ② } ... } QString CMyClass:: getOName() { // 在真正需要翻译时再使用数组和下标访问这些字符串 return tr(s_description[0]); } 代码清单36中用两个const char*类型的静态数组s_description、s_coordinate来存放字符串,然后在CMyClass的构造函数中标号①、标号②处对数组中的全部字符串执行了一次tr()调用。这样,lupdate会将这些源文提取到ts文件中,然后就可以实现国际化了。 4. 在非QObject派生类中实现国际化 在介绍国际化编程方法时,一再强调源文所在的类一定要从QObject派生(直接派生、间接派生都可以),但如果不希望类从QObject派生,该怎么办呢?其实很简单,定义一个公共类,然后把源文交给这个公共类翻译就行了。当然了,这个公共类要从QObject派生。假设有一个类CMyClass,这个类不从QObject派生,那么可以定义另外一个类CCommonString,由它负责替CMyClass实现国际化,如代码清单37所示。 代码清单37 // commonstring.h #pragma once #include class CCommonString : public QObject { Q_OBJECT }; // myclass.h class CMyClass { ... public: void func(void); }; // myclass.cpp #include "commonstring.h" #include "myclass.h" const char* c_strInfo = "This also work!"; void CMyClass::func(void){ cout << CCommonString::tr("This will Work!") << endl; ① cout << CCommonString::tr(c_strInfo) << endl; } 代码清单37中定义了CMyClass类,在标号①处有需要进行翻译的源文,但因为CMyClass不是QObject的派生类,所以无法正常翻译。现在定义CCommonString类,由它代替CMyClass实现翻译(见标号①处代码)。 注意: 在使用commonstring.h的项目pro文件中,应该将该头文件添加到HEADERS配置项(见代码清单38标号①处),否则可能导致项目构建失败。 代码清单38 HEADERS += $$(TRAINDEVHOME)/src/gui_base.pri \ ks03_02.pro \ dialog.h \ $$TRAIN_INCLUDE_PATH/base/commonstring.h ① 3.3知识点中英文翻译失败如何处理 视频讲解 在3.1节、3.2节介绍了国际化开发的方法及常见的开发场景,用这些方法可以实现正常的中英文翻译。但在实际开发过程中经常会碰到翻译失败的情况,这到底是怎么回事呢?下面列举几种可能导致翻译失败的原因: (1) 文本控件尺寸不合适导致无法完整显示源文。 (2) ts文件中找不到源文或者只有源文没有翻译后的文本。 (3) ts文件的翻译项中存在type="vanished"或者type="unfinished"。 (4) 没有将qm文件发布到指定目录。 (5) 程序中未正确加载qm文件。 下面进行详细介绍。 1. 文本控件尺寸不合适导致无法完整显示源文 因文本控件的尺寸不合适导致遮挡源文是比较低级的问题,但现实中也有可能碰到。 2. ts文件中找不到源文或者只有源文没有翻译后的文本 使用lupdate将源文提取到ts文件时,可能由于某种原因未能成功提取到源文,或者虽然提取到源文,却忘记做人工翻译工作。如果源文在ts文件中已存在,那么只需要补充翻译即可; 如果ts文件中找不到源文,就要检查具体原因了。 1) 源文出现拼写错误 如果源文出现拼写错误或者把标点符号的半角/全角弄错了,这会导致翻译失败。这时,应认真核对源文的拼写。 2) 忘记将源文添加到ts文件 当人工编写ts文件时,可能忘记将源文添加到ts文件。这时只需要将源文添加到ts文件并增加翻译。 3) 修改代码后,忘记执行lupdate 如果修改了代码但忘记执行lupdate,也会导致翻译失败。解决的方法就是执行lupdate以及国际化的后续操作步骤。 4) 忘记对源文调用tr()接口 如果没有使用tr()对源文进行翻译,也会导致翻译失败。这时只需要对源文执行tr()调用。 3. ts文件的翻译项中存在type="vanished"或者type="unfinished" 如果ts文件的翻译项中存在属性: type = vanished或者type = unfinished(见代码清单39标号①处),也会导致翻译失败。此时需要补充翻译,并将“type = vanished”或者“type = unfinished”字样删除,然后再执行发布。 代码清单39 CResourceInput Cancel 4. 没有将qm文件发布到指定目录 如果指定目录中没有qm文件,可能有两种原因。 1) 未执行发布动作 linguist的【文件】菜单中有【发布】或者【发布到】菜单项。如果忘记执行发布动作,就无法生成qm格式的翻译文件。因此,请在修改ts文件后及时发布。 2) 发布到错误的目录 将qm文件发布到错误的目录,这可是个低级错误,应该避免犯这种错误。 5. 程序中未正确加载qm文件 如果在代码中将qm文件名、文件路径写错或者根本没有编写安装qm文件的代码,必然导致翻译失败。请务必在代码中正确编写安装项目翻译文件的代码,见代码清单310。 代码清单310 // 安装项目的翻译文件 QString strPath = qgetenv("TRAINDEVHOME");  // 获取环境变量所指向的路径 strPath += "/system/lang"; QScopedPointer gpTranslator(new QTranslator(QCoreApplication::instance())); if (gpTranslator->load("ks03_01.qm", strPath)) { QCoreApplication::installTranslator(gpTranslator.take()); } 3.4配套练习 1. 当产品实现国际化后,如果需要显示为英文该怎么办? 2. 建立一个对话框项目,在界面中添加两个QLabel控件,使用Designer在第一个QLabel控件中键入Hello,在代码中将第二个QLabel控件显示“Nice to meet you!”。实现这两个QLabel控件的国际化,使其在运行时分别显示“你好”“很高兴见到你!”。 3. 分析代码清单311中导致翻译失败的原因。 代码清单311 CResourceInput Cancel