玩转Qml(12)-再谈动态国际化
- 簡介
- 源碼
- 效果預覽
- Qt本身的國際化
- 存在翻譯不全的問題
- 新的方案
- 關于批量翻譯
- 總結
簡介
本文是《玩轉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內容格式如下;
{"lang": "簡體中文","trans":[{"key": "Chinese","value": "簡體中文"},{"key": "Japanese","value": "日語"},{"key": "Korean","value": "韓語"},{"key": "Menu","value": "菜單"},] }其中lang字段表示當前語言,trans字段是所有的翻譯項。
3、實現核心翻譯器Trans
自己實現一個Trans類,用來加載翻譯包、提供翻譯數據,類聲明如下:
//Trans.h #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)//空字符串。用于動態翻譯時,通過change信號觸發transQ_PROPERTY(QString transString READ transString NOTIFY transStringChanged) public:explicit Trans(QObject *parent = nullptr);//加載指定文件夾void loadFolder(const QString &folder);//加載指定文件。成功時返回true,lang參數輸出文件代表的語言。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;// <"English", <"key", "value">>QHash<QString, QHash<QString, QString>> m_map;QStringList m_languages;//always emptyQString m_transString; };其中languages是加載過后支持的所有語言,currentLang是當前語言。
trans函數是用來做翻譯的,傳入要翻譯的字符串,根據當前語言,返回翻譯后的字符串。
因為軟件到處都要翻譯,所以trans函數會被頻繁調用,使用QHash<QString, QHash<QString, QString>>這樣的
嵌套Hash數據結構,保證查詢的平均復雜度為O(1).
transString是一個特殊的屬性,其值始終為空,在語言被切換時,會觸發transStringChange信號。
這樣有什么用呢?先知道這個設定,后面qml部分會詳細解釋。
cpp 實現如下:
//Trans.cpp #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中的語法如下:
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,就把靜態字符串換成動態的變量即可:
ListView {...delegate: Text {text: trans.trans(modelData) + trans.transString} }復雜一些的格式化字符串,也是沒有問題的:
Text {text: trans.trans("Today is %1, i feel %2").arg(trans.trans("Sunday")).arg(trans.trans("happy")) + trans.transString }對應的翻譯文件:
{"lang": "簡體中文","trans": [{"key": "Today is %1, i feel %2","value": "今天是%1, 我感覺%2"},{"key": "Sunday","value": "星期天"},{"key": "happy","value": "開心"},...] }關于批量翻譯
翻譯效果不太理想,不過還是可以分享一下方法。
首先是提取出了所有要翻譯的字符串:
//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輸出。
# trans.ps1# 需要注冊百度翻譯的app,獲得id和secret$baiduId = "xxxxxxxxxx" $baiduSecret = "xxxxxxxxx" # 列出百度翻譯 支持的語言 $baiduLangs = @{zh="中文";# cht="繁體中文";yue="粵語";wyw="文言文";jp="日語";kor="韓語";fra="法語";spa="西班牙語";th="泰語";ara="阿拉伯語";ru="俄語";pt="葡萄牙語";de="德語";it="意大利語";el="希臘語";nl="荷蘭語";# pl="波蘭語";bul="保加利亞語";est="愛沙尼亞語";dan="丹麥語";fin="芬蘭語";cs="捷克語";rom="羅馬尼亞語";# slo="斯洛文尼亞語";# swe="瑞典語";hu="匈牙利語";# vie="越南語"; } # 計算MD5 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 MD5return $hash.Hash.toString().toLower() }# 百度翻譯的調用 function baiduTrans {param([string]$q,[string]$from = 'en',[string]$to = 'zh')# 隨機鹽$salt = Get-Random# token拼接$signtoken = "{0}{1}{2}{3}" -f $baiduId, $q, $salt, $baiduSecret# 計算md5$signtoken = getHash $signtoken# POST body$body = @{q = $qfrom = $fromto = $toappid = $baiduIdsalt = $saltsign = $signtoken}$response = Invoke-RestMethod http://api.fanyi.baidu.com/api/trans/vip/translate -Method Post -Body $body#return $response.dst.toString()if ($null -ne $response.trans_result) {return $response.trans_result[0].dst }else {return $q} } # 主函數 function main() {# 讀取keys.json$json = Get-Content 'keys.json' -Encoding utf8 | ConvertFrom-Json# 每種語言都翻譯一遍foreach ($lang in $baiduLangs.Keys) {Write-Host $lang$tlang = $baiduLangs[$lang]# 目標語言的名稱,默認都用中文表示的。這里將名稱翻譯成對應的語言。比如:英語 就叫'English', 日語就用'日本語'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# 延時1秒,畢竟沒有充值,被限速了,每秒只能請求1次。Start-Sleep -Seconds 1}# 按格式輸出$obj = @{"lang" = $tlang"trans" = $res}$targetFileName = "language_{0}.json" -f $lang$obj | ConvertTo-Json | Set-Content $targetFileName -Encoding UTF8} }# baiduTrans "apple" "en" "zh"main結果如下,生成了一堆json文件
總結
Qml中帶個尾巴的寫法,雖然有些別扭,但是夠用、能達到動態翻譯的目標。
如果你有更好的思路,歡迎留言交流。
總結
以上是生活随笔為你收集整理的玩转Qml(12)-再谈动态国际化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 小白到学会python要多久_零基础小白
- 下一篇: pixel-anchor 相关概念