玩转Qml(12)-再谈动态国际化
本文于
1038
天之前发表,文中内容可能已经过时。
简介
本文是《玩转Qml》系列文章的第十二篇,主要讨论多国语言动态翻译。
之前分享过使用Qt自带翻译的方案,但是效果不太好。这次分享一个非官方的多国语言方案。
源码
《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick
github https://github.com/jaredtao/TaoQuick
访问不了或者速度太慢,可以用国内的镜像网站gitee
https://gitee.com/jaredtao/TaoQuick
效果预览
看一下最终效果
(原始字符串全部为英文,中文为人工翻译。
其它语言使用的百度翻译api批量翻译,不太准确,暂时先这样)
Qt本身的国际化
先来回顾一下,Qt的国际化方案:
C++代码中的字符串使用QObject::tr()包起来,类本身是QObject的子类时可以省略作用域“QObject::”,直接写tr
qml代码中使用qsTr把字符串包起来
pro文件中添加一句TRANSLATIONS += trans_zh.qs ,这个名字起什么无所谓,关键是‘_zh’要有。
调用lrelease工具,扫描项目并生成trans_zh.qs 文件。这个文件是xml格式的,未经过翻译的,需要为这个文件做一些翻译工作。
翻译做好后,调用lupdate工具,生成trans_zh.qm文件。这个文件就是把xml压缩成了二进制。
将qm文件放在运行路径,或者资源文件里。
切换语言时, Qt/C++代码中使用QTranslater加载qm文件,QCoreApplication卸载旧的QTranslater,并安装新的QTranslater。调用
QmlEngine::retranslate函数
在5.10以前的版本,Qt是不能直接动态切换语言的,要么重新启动程序,要么把所有的text都set一遍,retranslate是5.10才有的接口。
存在翻译不全的问题
上面的方案,在TaoQuick中使用了。
明显的问题是,只能翻译静态的内容,动态加载的ListModel,动态切换语言时不能自动刷新。
按照Qt文档所说,Array或者其它数据结构中的内容,也不能自动刷新。
新的方案
这里抛弃Qt的翻译机制,使用自己实现的方案。
1、约定要用到的字符串,全部用英文。
2、翻译文件使用json文件,一个文件翻译一种语言。
文件命名格式language_xx.json, json内容格式如下;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| { "lang": "简体中文", "trans":[ { "key": "Chinese", "value": "简体中文" }, { "key": "Japanese", "value": "日语" }, { "key": "Korean", "value": "韩语" }, { "key": "Menu", "value": "菜单" }, ] }
|
其中lang字段表示当前语言,trans字段是所有的翻译项。
3、实现核心翻译器Trans
自己实现一个Trans类,用来加载翻译包、提供翻译数据,类声明如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #pragma once
#include <QObject> #include <QHash> #include <QList> #include <QString> class Trans : public QObject { Q_OBJECT Q_PROPERTY(QString currentLang READ currentLang WRITE setCurrentLang NOTIFY currentLangChanged) Q_PROPERTY(QStringList languages READ languages NOTIFY languagesChanged) Q_PROPERTY(QString transString READ transString NOTIFY transStringChanged) public: explicit Trans(QObject *parent = nullptr); void loadFolder(const QString &folder);
bool load(QString &lang, const QString &filePath); public: const QString ¤tLang() const;
const QStringList &languages() const;
const QString &transString() const;
public slots: QString trans(const QString &source) const;
void setCurrentLang(const QString ¤tLang); signals: void currentLangChanged(const QString ¤tLang);
void languagesChanged(const QStringList &languages);
void transStringChanged(); protected: void setLanguages(const QStringList &languages);
void initEnglish(); private: QString m_currentLang; QHash<QString, QHash<QString, QString>> m_map; QStringList m_languages; QString m_transString; };
|
其中languages是加载过后支持的所有语言,currentLang是当前语言。
trans函数是用来做翻译的,传入要翻译的字符串,根据当前语言,返回翻译后的字符串。
因为软件到处都要翻译,所以trans函数会被频繁调用,使用QHash<QString, QHash<QString, QString>>这样的
嵌套Hash数据结构,保证查询的平均复杂度为O(1).
transString是一个特殊的属性,其值始终为空,在语言被切换时,会触发transStringChange信号。
这样有什么用呢?先知道这个设定,后面qml部分会详细解释。
cpp 实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
| #include "Trans.h" #include "FileReadWrite.h" #include <QDir> const static auto cEnglisthStr = QStringLiteral("English"); const static auto cChineseStr = QStringLiteral("简体中文"); Trans::Trans(QObject* parent) : QObject(parent) { }
void Trans::loadFolder(const QString& folder) { QDir dir(folder); auto infos = dir.entryInfoList({ "language_*.json" }, QDir::Files); for (auto info : infos) { QString lang; load(lang, info.absoluteFilePath()); } initEnglish(); auto langs = m_map.uniqueKeys(); if (langs.contains(cChineseStr)) { langs.removeAll(cChineseStr); langs.push_front(cChineseStr); } setLanguages(langs); if (m_map.contains(cChineseStr)) { setCurrentLang(cChineseStr); } else { setCurrentLang(cEnglisthStr); } emit transStringChanged(); }
bool Trans::load(QString& lang, const QString& filePath) { lang.clear(); QJsonObject rootObj; if (!TaoCommon::readJsonFile(filePath, rootObj)) { return false; } lang = rootObj.value("lang").toString(); const auto& trans = rootObj.value("trans").toArray(); for (auto i : trans) { auto transObj = i.toObject(); QString key = transObj.value("key").toString(); QString value = transObj.value("value").toString(); m_map[lang][key] = value; } return true; }
const QString &Trans::currentLang() const { return m_currentLang; }
const QStringList &Trans::languages() const { return m_languages; }
const QString &Trans::transString() const { return m_transString; }
void Trans::initEnglish() { if (!m_map.contains(cEnglisthStr)) { QHash<QString, QString> map; if (m_map.contains(cChineseStr)) { map = m_map.value(cChineseStr); } else { map = m_map.value(m_map.keys().first()); } for (auto key : map.uniqueKeys()) { m_map[cEnglisthStr][key] = key; } } }
QString Trans::trans(const QString& source) const { return m_map.value(m_currentLang).value(source, source); }
void Trans::setCurrentLang(const QString& currentLang) { if (m_currentLang == currentLang) return;
m_currentLang = currentLang; emit currentLangChanged(m_currentLang); emit transStringChanged(); }
void Trans::setLanguages(const QStringList& languages) { if (m_languages == languages) return;
m_languages = languages; emit languagesChanged(m_languages); emit transStringChanged(); }
|
4、Qml中使用新的翻译语法
qml中的语法如下:
1 2 3
| Text { text: trans.trans("Welcome") + trans.transString }
|
这是一个很常规的’Qml属性绑定’,或者叫’绑定表达式’, 这样写了以后,text的值依赖于trans.trans()函数返回值和 transString。
当text依赖的属性发出change信号时,qml引擎会重新对这个表达式求值,并把结果赋值给text。
一般情况下,text的值就是trans的返回值,后面的空值不会影响到结果。
当前语言被改变时,函数没有change信号,而transString属性的change信号会被触发,导致qml引擎会重新对这个表达式求值,
此时会重新调用trans函数,按照新的语言返回翻译结果。
Text组件的text属性变化时,会自己刷新UI。
于是,就实现了动态翻译多国语言。
对于ListModel,就把静态字符串换成动态的变量即可:
1 2 3 4 5 6
| ListView { ... delegate: Text { text: trans.trans(modelData) + trans.transString } }
|
复杂一些的格式化字符串,也是没有问题的:
1 2 3
| Text { text: trans.trans("Today is %1, i feel %2").arg(trans.trans("Sunday")).arg(trans.trans("happy")) + trans.transString }
|
对应的翻译文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| { "lang": "简体中文", "trans": [ { "key": "Today is %1, i feel %2", "value": "今天是%1, 我感觉%2" }, { "key": "Sunday", "value": "星期天" }, { "key": "happy", "value": "开心" }, ... ] }
|
关于批量翻译
翻译效果不太理想,不过还是可以分享一下方法。
首先是提取出了所有要翻译的字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| //key.json [ "Chinese", "traditional Chinese", "Cantonese", "classical Chinese", "Japanese", "Korean", "French", "Spanish", "Thai", "Arabic", "Russian", "Portuguese", "German", "Italian", "Greek", "Dutch", "Polish", "Bulgarian", "Estonian", "Danish", ... ]
|
其次是写了一个PowerShell脚本,逐个调用百度翻译API,并把结果按照前面的json输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
|
$baiduId = "xxxxxxxxxx" $baiduSecret = "xxxxxxxxx"
$baiduLangs = @{ zh="中文"; yue="粤语"; wyw="文言文"; jp="日语"; kor="韩语"; fra="法语"; spa="西班牙语"; th="泰语"; ara="阿拉伯语"; ru="俄语"; pt="葡萄牙语"; de="德语"; it="意大利语"; el="希腊语"; nl="荷兰语"; bul="保加利亚语"; est="爱沙尼亚语"; dan="丹麦语"; fin="芬兰语"; cs="捷克语"; rom="罗马尼亚语"; hu="匈牙利语"; }
function getHash([string]$source) { $stringAsStream = [System.IO.MemoryStream]::new() $writer = [System.IO.StreamWriter]::new($stringAsStream) $writer.write($source) $writer.Flush() $stringAsStream.Position = 0 $hash = Get-FileHash -InputStream $stringAsStream -algorithm MD5 return $hash.Hash.toString().toLower() }
function baiduTrans { param( [string]$q, [string]$from = 'en', [string]$to = 'zh' ) $salt = Get-Random $signtoken = "{0}{1}{2}{3}" -f $baiduId, $q, $salt, $baiduSecret $signtoken = getHash $signtoken $body = @{ q = $q from = $from to = $to appid = $baiduId salt = $salt sign = $signtoken } $response = Invoke-RestMethod http://api.fanyi.baidu.com/api/trans/vip/translate -Method Post -Body $body if ($null -ne $response.trans_result) { return $response.trans_result[0].dst } else { return $q } }
function main() { $json = Get-Content 'keys.json' -Encoding utf8 | ConvertFrom-Json foreach ($lang in $baiduLangs.Keys) { Write-Host $lang $tlang = $baiduLangs[$lang] if ($lang -ne "zh") { $tlang = baiduTrans $baiduLangs[$lang] "zh" $lang } $res=@() foreach ($key in $json) { Write-Host $key $dst = baiduTrans $key "en" $lang $t = @{'key'=$key; 'value'=$dst} $res +=$t Start-Sleep -Seconds 1 } $obj = @{ "lang" = $tlang "trans" = $res } $targetFileName = "language_{0}.json" -f $lang $obj | ConvertTo-Json | Set-Content $targetFileName -Encoding UTF8 } }
main
|
结果如下,生成了一堆json文件
总结
Qml中带个尾巴的写法,虽然有些别扭,但是够用、能达到动态翻译的目标。
如果你有更好的思路,欢迎留言交流。