Qt 是一個很方便的 C++ 應用開發框架(或許現在要加上 Qt Quick 開發框架?),不僅僅是程式編寫方便麵,它提供了很多方便的類庫,而且它也提供了很方便的國際化方案(也就是翻譯成各國語言的方案)。
基本流程
編寫程式碼階段
我們先來說說在 Qt 中實現多國語言翻譯需要使用的基本流程。首先我們需要在編寫程式碼的時候就要使用 Qt 提供的翻譯相關的函式來”包裹”住所有的需要翻譯的字串。
你說哪些才是需要翻譯的字串呢?就是任何會在使用者介面上顯示的字串,如果不會顯示自然就不需要翻譯了。
如果你使用的是 C++ 程式碼,那麼翻譯用的函式就是QObject::tr
函式。大多數時候,我們看到用到這個函式的時候可能都只是一個tr
,因為都是在一個繼承自 QObject 的類中,所以可以直接呼叫父類的成員函式了。如果是在自由函式中想使用的話就要把完整的函式名QObject::tr
寫全了。
如果你寫的是 Qt Quick 的程式,這個函式就變成了 qsTr 或者是 qsTranslate 、qsTranslateNoOp …
程式碼寫完之後,需要在工程檔案 (.pro 檔案) 中新增需要翻譯的原始碼檔案。
SOURCES +=
main.cpp
mainwindow.cpp
TRANSLATIONS =
English.ts
Japanese.ts
使用 lupdate 檢索原始碼生成翻譯原始檔
在工程目錄下執行以下命令
lupdate project.pro
執行完後,目錄下會出現 English.ts 和 Japanese.ts 兩個檔案,這兩個檔案就是由上文中工程檔案中制定的檔名。
使用 linguist 進行翻譯
然後就可以使用 Qt 提供的 lingust 工具開啟上文生成的 .ts 檔案,將對應的詞條翻譯成目標語言就好了。
使用 lrelease 生成最終程式使用的翻譯檔案
當 linguist 翻譯完所有的詞條之後,就需要生成最終給應用程式使用的二進位制翻譯檔案了。
使用如下命令即可生成。
lrelease English.ts
執行完畢,目錄下會多出一個 English.qm 檔案,這個檔案就是最終需要放在應用程式中分發給使用者的。
問題
使用上面的流程就完成了一次基本的程式國際化的流程。一般來說程式的翻譯工作和編碼工作是由不同人來完成的,可能實際完成翻譯的人不熟悉或者不願去熟悉使用 linguist 的使用方法,他們提供給程式設計師的詞條翻譯可能就是一個 Excel 檔案,這個 Excel 檔案通常一列是待翻譯的詞條,另一列是相應的譯文。
解決方法
於是我們可能需要一個將 Qt 的翻譯檔案與 Excel 檔案互相轉換的小工具,這樣我們可以把 Qt 翻譯檔案轉換成 Excel 檔案交給翻譯人員,然後將翻譯人員翻譯好的 Excel 檔案再轉換為 Qt 的翻譯檔案。
要做檔案格式轉換,那麼我們首先得了解一下兩種檔案格式有什麼特點。
Qt TS 檔案格式分析
Qt 的 TS 翻譯檔案其實就是一個 XML 檔案。我們先來看一個翻譯檔案的例子。
<!DOCTYPE TS>
<TS>
<context>
<name>QPushButton</name>
<message>
<source>Hello world!</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>
這是一個還未翻譯的檔案,詞條原文是”Hello world!”。在 ts 檔案中,每個需要翻譯的詞條都是一個 message,message 包含需要翻譯的原文和已翻譯的譯文。
翻譯完成後需要去除 translation 的 type="unfinished"
屬性。
Excel 檔案格式分析
Excel 的檔案格式是 Microsoft ® 的私有格式,Excel 2007 之前的格式是二進位制格式, Excel 2007 之後的是 OOXML 格式。
在這裡我們不用太細的追究它的內部細節,因為它的內部細節非常複雜,不像 ts 檔案就是一個簡單的文字檔案。現在有許多的可以用來讀寫 Excel 檔案的庫,我們只需呼叫這些庫的讀寫函式就能完成我們所需的功能了。
在本文中,我將使用 C# 語言以及 NPOI 庫來讀寫 Excel 檔案。
TS 檔案轉 Excel 檔案
private void convertQtFile2ExcelFile(string qtFileName, string excelFileName)
{
XmlDocument xmlDoc = new XmlDocument();
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreComments = true;
settings.DtdProcessing = DtdProcessing.Ignore;
IWorkbook workbook = new XSSFWorkbook();
ISheet sheet = workbook.CreateSheet("worksheet");
int rowCnt = 0;
IRow headRow = sheet.CreateRow(rowCnt);
rowCnt++;
ICell chCell = headRow.CreateCell(0);
chCell.SetCellValue("源語言");
ICell enCell = headRow.CreateCell(1);
enCell.SetCellValue("目標語言");
XmlReader reader = XmlReader.Create(qtFileName, settings);
xmlDoc.Load(reader);
XmlNode rootNode = xmlDoc.SelectSingleNode("TS");
XmlNodeList xnl = rootNode.ChildNodes;
foreach (XmlNode node in xnl)
{
XmlNodeList nodeList = node.ChildNodes;
string src = "";
string dst = "";
foreach (XmlNode n in nodeList)
{
foreach (XmlNode cn in n.ChildNodes)
{
if (cn.Name == "source")
{
src = cn.InnerText;
}
if (cn.Name == "translation")
{
dst = cn.InnerText;
IRow row = sheet.CreateRow(rowCnt);
rowCnt++;
ICell firstCell = row.CreateCell(0);
firstCell.SetCellValue(src);
ICell secondCell = row.CreateCell(1);
secondCell.SetCellValue(dst);
//Console.WriteLine ("Src = " + src + " dst = " + dst);
}
}
}
}
reader.Close();
using (FileStream fs = File.Create(excelFileName))
{
workbook.Write(fs);
}
}
Excel 轉 TS 檔案
private bool convertExcelFile2QtFile(string excelFileName, string qtFileName)
{
try
{
using (var fs = File.OpenRead(excelFileName))
{
var workBook = new XSSFWorkbook(fs);
var sheet = workBook.GetSheetAt(0);
var translateMap = new Dictionary<string, string>();
for (int i = 1; i < sheet.LastRowNum; i++)
{
IRow row = sheet.GetRow(i);
if (row == null)
{
continue;
}
var srcCell = row.GetCell(0);
var dstCell = row.GetCell(1);
if (srcCell == null)
{
continue;
}
if (dstCell == null)
{
continue;
}
string src = srcCell.ToString();
string translated = dstCell.ToString();
if (translateMap.ContainsKey(src) == false)
{
translateMap.Add(src, translated);
}
}
var document = new XmlDocument();
document.Load(qtFileName);
XmlNodeList xnl = document.SelectNodes("/TS/context");
if (xnl != null)
{
foreach (XmlNode ctxNode in xnl)
{
var msgList = ctxNode.SelectNodes("message");
foreach (XmlNode msg in msgList)
{
string src = "";
foreach (XmlNode cn in msg.ChildNodes)
{
if (cn.Name == "source")
{
src = cn.InnerText;
}
else if (cn.Name == "translation")
{
if (translateMap.ContainsKey(src))
{
cn.InnerText = translateMap[src];
var attrs = cn.Attributes;
attrs.Remove(attrs["type"]);
}
}
}
}
}
}
document.Save(qtFileName);
}
return true;
}
catch (Exception e)
{
MessageBox.Show("Exception:" + e.Message);
return false;
}
}
自動翻譯詞條
其實瞭解了 TS 檔案的結構之後,我們還能做一件事,那就是直接呼叫網上的翻譯介面,自動將所有的詞條翻譯成對應的語言,下面就是一個簡單的例子。
import xml.etree.ElementTree as ET
import os
import httplib
import md5
import urllib
import random
import json
import threading
import requests
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
reload(sys)
sys.setdefaultencoding(`utf-8`)
## 下面填寫你自己的百度翻譯 API 的 appid 和 secretKey
appid = ``
secretKey = ``
myurl = `/api/trans/vip/translate`
def translate(string, targetLanguage):
if string is None:
return ""
salt = random.randint(32768, 65536)
sign = appid + string + str(salt) + secretKey
m1 = md5.new()
m1.update(sign)
sign = m1.hexdigest()
encoded_str = urllib.quote(str(string))
localurl = myurl + `?appid=` + appid + `&q=` +
encoded_str + `&from=auto&to=` +
targetLanguage + `&salt=` + str(salt) + `&sign=` + sign
try:
r = requests.get(`http://api.fanyi.baidu.com/` + localurl)
if r.status_code == 200:
response = r.text
objResponse = json.loads(response)
return objResponse["trans_result"][0]["dst"]
except Exception, e:
print e
return string
class MainWidget(QWidget):
def __init__(self, parent=None):
super(QWidget, self).__init__(parent)
self.setWindowTitle(u"UI Auto Translator")
self.labelTargetLanguage = QLabel(u"目標語言:")
self.comboTargetLanguage = QComboBox()
self.comboTargetLanguage.addItem(u"中文", u"zh")
self.comboTargetLanguage.addItem(u"英語", u"en")
self.comboTargetLanguage.addItem(u"粵語", u"yue")
self.comboTargetLanguage.addItem(u"文言文", u"wyw")
self.comboTargetLanguage.addItem(u"日語", u"jp")
self.comboTargetLanguage.addItem(u"韓語", u"kor")
self.comboTargetLanguage.addItem(u"法語", u"fra")
self.comboTargetLanguage.addItem(u"西班牙語", u"spa")
self.comboTargetLanguage.addItem(u"泰語", u"th")
self.comboTargetLanguage.addItem(u"阿拉伯語", u"ara")
self.comboTargetLanguage.addItem(u"俄語", u"ru")
self.comboTargetLanguage.addItem(u"葡萄牙語", u"pt")
self.comboTargetLanguage.addItem(u"德語", u"de")
self.comboTargetLanguage.addItem(u"義大利語", u"it")
self.comboTargetLanguage.addItem(u"希臘語", u"el")
self.comboTargetLanguage.addItem(u"荷蘭語", u"nl")
self.comboTargetLanguage.addItem(u"波蘭語", u"pl")
self.comboTargetLanguage.addItem(u"保加利亞語", u"bul")
self.comboTargetLanguage.addItem(u"愛沙尼亞語", u"est")
self.comboTargetLanguage.addItem(u"丹麥語", u"dan")
self.comboTargetLanguage.addItem(u"芬蘭語", u"fin")
self.comboTargetLanguage.addItem(u"捷克語", u"cs")
self.comboTargetLanguage.addItem(u"羅馬尼亞語", u"rom")
self.comboTargetLanguage.addItem(u"斯洛維尼亞語", u"slo")
self.comboTargetLanguage.addItem(u"瑞典語", u"swe")
self.comboTargetLanguage.addItem(u"匈牙利語", u"hu")
self.comboTargetLanguage.addItem(u"繁體中文", u"cht")
self.comboTargetLanguage.addItem(u"越南語", u"vie")
self.labelSrcFile = QLabel(u"原始檔")
self.inputSrcFile = QLineEdit()
self.inputSrcBtn = QPushButton(u"開啟檔案")
self.connect(self.inputSrcBtn, SIGNAL(
"clicked()"), self.onInputSrcBtnClicked)
self.labelTargetFile = QLabel(u"儲存檔案")
self.inputTargetFile = QLineEdit()
self.inputTargetBtn = QPushButton(u"選擇檔名")
self.connect(self.inputTargetBtn, SIGNAL(
"clicked()"), self.onInputTargetBtnClicked)
self.cvtBtn = QPushButton(u"開始翻譯")
self.connect(self.cvtBtn, SIGNAL("clicked()"), self.onCvtBtnClicked)
self.gridLayout = QGridLayout()
self.gridLayout.addWidget(self.labelTargetLanguage, 0, 0)
self.gridLayout.addWidget(self.comboTargetLanguage, 0, 1)
self.gridLayout.addWidget(self.labelSrcFile, 1, 0)
self.gridLayout.addWidget(self.inputSrcFile, 1, 1)
self.gridLayout.addWidget(self.inputSrcBtn, 1, 2)
self.gridLayout.addWidget(self.labelTargetFile, 2, 0)
self.gridLayout.addWidget(self.inputTargetFile, 2, 1)
self.gridLayout.addWidget(self.inputTargetBtn, 2, 2)
self.vLayout = QVBoxLayout()
self.vLayout.addLayout(self.gridLayout)
self.vLayout.addWidget(self.cvtBtn)
self.setLayout(self.vLayout)
@pyqtSlot()
def onInputSrcBtnClicked(self):
fileName = QFileDialog.getOpenFileName(
self, u"開啟要翻譯的檔案", u".", u"*.ts")
if fileName.isEmpty():
QMessageBox.warning(self, u"警告", u"未選擇要翻譯的檔案", QMessageBox.Ok)
return
self.inputSrcFile.setText(fileName)
@pyqtSlot()
def onInputTargetBtnClicked(self):
fileName = QFileDialog.getSaveFileName(
self, u"選擇另存為的檔名", u".", u"*.ts")
if fileName.isEmpty():
QMessageBox.warning(self, u"警告", u"未選擇要儲存為的文明名", QMessageBox.Ok)
return
self.inputTargetFile.setText(fileName)
def process_file(self, src, dst, lang):
self.cvtBtn.setText(u"正在翻譯中...")
tree = ET.parse(src)
root = tree.getroot()
source = ""
for ctx in root:
for msg in ctx:
if msg.tag == `message`:
for tag in msg:
if tag.tag == `source`:
source = tag.text
if tag.tag == `translation`:
tag.text = translate(source, lang)
print("source = {0}, translate = {1}".format(source, tag.text))
tag.set(`type`, ``)
tree.write(dst)
self.cvtBtn.setText(u"開始翻譯")
@pyqtSlot()
def onCvtBtnClicked(self):
targetLang = self.comboTargetLanguage.itemData(
self.comboTargetLanguage.currentIndex()).toString()
srcFileName = self.inputSrcFile.text()
if srcFileName.isEmpty():
QMessageBox.warning(self, u"警告", u"未選擇要翻譯的檔案", QMessageBox.Ok)
return
targetFileName = self.inputTargetFile.text()
if targetFileName.isEmpty():
QMessageBox.warning(self, u"警告", u"未選擇要儲存為的文明名", QMessageBox.Ok)
return
t = threading.Thread(target=self.process_file, args=(srcFileName, targetFileName, str(targetLang)))
t.setDaemon(True)
t.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = MainWidget()
widget.show()
app.exec_()