1
0
mirror of https://github.com/d47081/qBittorrent.git synced 2025-03-13 05:41:17 +00:00

Merge pull request #9026 from glassez/www-translate

Use independent translation for WebUI
This commit is contained in:
Vladimir Golovnev 2018-10-12 13:39:18 +03:00 committed by GitHub
commit 5d2e35d14d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 92642 additions and 178 deletions

1
.gitignore vendored
View File

@ -22,6 +22,7 @@ qrc_*.cpp
ui_*.h
*.moc
src/lang/qbittorrent_*.qm
src/webui/www/translations/webui_*.qm
.DS_Store
.qmake.stash
src/qbittorrent.app

View File

@ -7,14 +7,15 @@ for(file, TS_FILES) {
}
isEmpty(QMAKE_LRELEASE) {
win32:QMAKE_LRELEASE = $$[QT_INSTALL_BINS]\\lrelease.exe
else:QMAKE_LRELEASE = $$[QT_INSTALL_BINS]/lrelease
win32: QMAKE_LRELEASE = $$[QT_INSTALL_BINS]/lrelease.exe
else: QMAKE_LRELEASE = $$[QT_INSTALL_BINS]/lrelease
unix {
equals(QT_MAJOR_VERSION, 5) {
!exists($$QMAKE_LRELEASE) { QMAKE_LRELEASE = lrelease-qt5 }
}
} else {
!exists($$QMAKE_LRELEASE) { QMAKE_LRELEASE = lrelease }
equals(QT_MAJOR_VERSION, 5) {
!exists($$QMAKE_LRELEASE): QMAKE_LRELEASE = lrelease-qt5
}
}
else {
!exists($$QMAKE_LRELEASE): QMAKE_LRELEASE = lrelease
}
}

View File

@ -11,7 +11,6 @@ api/synccontroller.h
api/torrentscontroller.h
api/transfercontroller.h
api/serialize/serialize_torrent.h
extra_translations.h
webapplication.h
webui.h

View File

@ -1,97 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#ifndef EXTRA_TRANSLATIONS_H
#define EXTRA_TRANSLATIONS_H
#include <QObject>
// Additional translations for Web UI
const char *QBT_WEBUI_TRANSLATIONS[] = {
QT_TRANSLATE_NOOP("HttpServer", "Logout"),
QT_TRANSLATE_NOOP("HttpServer", "Exit qBittorrent"),
QT_TRANSLATE_NOOP("HttpServer", "Download Torrents from their URLs or Magnet links"),
QT_TRANSLATE_NOOP("HttpServer", "Only one link per line"),
QT_TRANSLATE_NOOP("HttpServer", "Upload local torrent"),
QT_TRANSLATE_NOOP("HttpServer", "Are you sure you want to delete the selected torrents from the transfer list?"),
QT_TRANSLATE_NOOP("HttpServer", "Global upload rate limit must be greater than 0 or disabled."),
QT_TRANSLATE_NOOP("HttpServer", "Global download rate limit must be greater than 0 or disabled."),
QT_TRANSLATE_NOOP("HttpServer", "Alternative upload rate limit must be greater than 0 or disabled."),
QT_TRANSLATE_NOOP("HttpServer", "Alternative download rate limit must be greater than 0 or disabled."),
QT_TRANSLATE_NOOP("HttpServer", "Maximum active downloads must be greater than -1."),
QT_TRANSLATE_NOOP("HttpServer", "Maximum active uploads must be greater than -1."),
QT_TRANSLATE_NOOP("HttpServer", "Maximum active torrents must be greater than -1."),
QT_TRANSLATE_NOOP("HttpServer", "Maximum number of connections limit must be greater than 0 or disabled."),
QT_TRANSLATE_NOOP("HttpServer", "Maximum number of connections per torrent limit must be greater than 0 or disabled."),
QT_TRANSLATE_NOOP("HttpServer", "Maximum number of upload slots per torrent limit must be greater than 0 or disabled."),
QT_TRANSLATE_NOOP("HttpServer", "Unable to save program preferences, qBittorrent is probably unreachable."),
QT_TRANSLATE_NOOP("HttpServer", "The port used for incoming connections must be between 1 and 65535."),
QT_TRANSLATE_NOOP("HttpServer", "The port used for the Web UI must be between 1 and 65535."),
QT_TRANSLATE_NOOP("HttpServer", "Save"),
QT_TRANSLATE_NOOP("HttpServer", "qBittorrent client is not reachable"),
QT_TRANSLATE_NOOP("HttpServer", "qBittorrent has been shutdown."),
QT_TRANSLATE_NOOP("HttpServer", "Unable to log in, qBittorrent is probably unreachable."),
QT_TRANSLATE_NOOP("HttpServer", "Invalid Username or Password."),
QT_TRANSLATE_NOOP("HttpServer", "Username"),
QT_TRANSLATE_NOOP("HttpServer", "Password"),
QT_TRANSLATE_NOOP("HttpServer", "Login"),
QT_TRANSLATE_NOOP("HttpServer", "Original authors"),
QT_TRANSLATE_NOOP("HttpServer", "Apply"),
QT_TRANSLATE_NOOP("HttpServer", "Add"),
QT_TRANSLATE_NOOP("HttpServer", "Save files to location:"),
QT_TRANSLATE_NOOP("HttpServer", "Cookie:"),
QT_TRANSLATE_NOOP("HttpServer", "Type folder here"),
QT_TRANSLATE_NOOP("HttpServer", "More information"),
QT_TRANSLATE_NOOP("HttpServer", "Information about certificates"),
QT_TRANSLATE_NOOP("HttpServer", "Save Files to"),
QT_TRANSLATE_NOOP("HttpServer", "IRC: #qbittorrent on Freenode"),
QT_TRANSLATE_NOOP("HttpServer", "Invalid category name:\nPlease do not use any special characters in the category name."),
QT_TRANSLATE_NOOP("HttpServer", "Unknown"),
QT_TRANSLATE_NOOP("HttpServer", "Hard Disk"),
QT_TRANSLATE_NOOP("HttpServer", "Share ratio limit must be between 0 and 9998."),
QT_TRANSLATE_NOOP("HttpServer", "Seeding time limit must be between 0 and 525600 minutes."),
QT_TRANSLATE_NOOP("HttpServer", "Set location"),
QT_TRANSLATE_NOOP("HttpServer", "Limit upload rate"),
QT_TRANSLATE_NOOP("HttpServer", "Limit download rate"),
QT_TRANSLATE_NOOP("HttpServer", "Rename torrent"),
QT_TRANSLATE_NOOP("HttpServer", "Unable to create category")
};
const struct { const char *source; const char *comment; } QBT_WEBUI_COMMENTED_TRANSLATIONS[] = {
QT_TRANSLATE_NOOP3("HttpServer", "Other...", "Save Files to: Watch Folder / Default Folder / Other..."),
QT_TRANSLATE_NOOP3("HttpServer", "Monday", "Schedule the use of alternative rate limits on ..."),
QT_TRANSLATE_NOOP3("HttpServer", "Tuesday", "Schedule the use of alternative rate limits on ..."),
QT_TRANSLATE_NOOP3("HttpServer", "Wednesday", "Schedule the use of alternative rate limits on ..."),
QT_TRANSLATE_NOOP3("HttpServer", "Thursday", "Schedule the use of alternative rate limits on ..."),
QT_TRANSLATE_NOOP3("HttpServer", "Friday", "Schedule the use of alternative rate limits on ..."),
QT_TRANSLATE_NOOP3("HttpServer", "Saturday", "Schedule the use of alternative rate limits on ..."),
QT_TRANSLATE_NOOP3("HttpServer", "Sunday", "Schedule the use of alternative rate limits on ..."),
QT_TRANSLATE_NOOP3("HttpServer", "Upload Torrents", "Upload torrent files to qBittorent using WebUI")
};
#endif // EXTRA_TRANSLATIONS_H

View File

@ -34,7 +34,6 @@
#include <stdexcept>
#include <vector>
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QFile>
@ -91,46 +90,6 @@ namespace
return ret;
}
void translateDocument(const QString &locale, QString &data)
{
const QRegularExpression regex("QBT_TR\\((([^\\)]|\\)(?!QBT_TR))+)\\)QBT_TR(\\[CONTEXT=([a-zA-Z_][a-zA-Z0-9_]*)\\])");
const QRegularExpression mnemonic("\\(?&([a-zA-Z]?\\))?");
const bool isTranslationNeeded = !locale.startsWith("en")
|| locale.startsWith("en_AU") || locale.startsWith("en_GB");
int i = 0;
bool found = true;
while (i < data.size() && found) {
QRegularExpressionMatch regexMatch;
i = data.indexOf(regex, i, &regexMatch);
if (i >= 0) {
const QString word = regexMatch.captured(1);
const QString context = regexMatch.captured(4);
QString translation = isTranslationNeeded
? qApp->translate(context.toUtf8().constData(), word.toUtf8().constData(), nullptr, 1)
: word;
// Remove keyboard shortcuts
translation.remove(mnemonic);
// Use HTML code for quotes to prevent issues with JS
translation.replace('\'', "&#39;");
translation.replace('\"', "&#34;");
data.replace(i, regexMatch.capturedLength(), translation);
i += translation.length();
}
else {
found = false; // no more translatable strings
}
data.replace(QLatin1String("${LANG}"), locale.left(2));
data.replace(QLatin1String("${VERSION}"), QBT_VERSION);
}
}
inline QUrl urlFromHostHeader(const QString &hostHeader)
{
if (!hostHeader.contains(QLatin1String("://")))
@ -234,6 +193,43 @@ void WebApplication::sendWebUIFile()
sendFile(localPath);
}
void WebApplication::translateDocument(QString &data)
{
const QRegularExpression regex("QBT_TR\\((([^\\)]|\\)(?!QBT_TR))+)\\)QBT_TR\\[CONTEXT=([a-zA-Z_][a-zA-Z0-9_]*)\\]");
const bool isTranslationNeeded = !m_currentLocale.startsWith("en")
|| m_currentLocale.startsWith("en_AU") || m_currentLocale.startsWith("en_GB")
|| !m_translator.isEmpty();
int i = 0;
bool found = true;
while (i < data.size() && found) {
QRegularExpressionMatch regexMatch;
i = data.indexOf(regex, i, &regexMatch);
if (i >= 0) {
const QString word = regexMatch.captured(1);
const QString context = regexMatch.captured(3);
QString translation = isTranslationNeeded
? m_translator.translate(context.toUtf8().constData(), word.toUtf8().constData(), nullptr, 1)
: word;
// Use HTML code for quotes to prevent issues with JS
translation.replace('\'', "&#39;");
translation.replace('\"', "&#34;");
data.replace(i, regexMatch.capturedLength(), translation);
i += translation.length();
}
else {
found = false; // no more translatable strings
}
data.replace(QLatin1String("${LANG}"), m_currentLocale.left(2));
data.replace(QLatin1String("${VERSION}"), QBT_VERSION);
}
}
WebSession *WebApplication::session()
{
return m_currentSession;
@ -429,6 +425,14 @@ void WebApplication::configure()
if (m_currentLocale != newLocale) {
m_currentLocale = newLocale;
m_translatedFiles.clear();
if (m_translator.load(m_rootFolder + QLatin1String("/translations/webui_") + m_currentLocale)) {
LogMsg(tr("WebUI translation for selected locale (%1) is successfully loaded.")
.arg(m_currentLocale));
}
else {
LogMsg(tr("Couldn't load WebUI translation for selected locale (%1). Falling back to default (en).")
.arg(m_currentLocale), Log::WARNING);
}
}
m_isLocalAuthEnabled = pref->isWebUiLocalAuthEnabled();
@ -490,7 +494,7 @@ void WebApplication::sendFile(const QString &path)
// Translate the file
if (isTranslatable) {
QString dataStr {data};
translateDocument(m_currentLocale, dataStr);
translateDocument(dataStr);
data = dataStr.toUtf8();
m_translatedFiles[path] = {data, lastModified}; // caching translated file

View File

@ -34,6 +34,7 @@
#include <QObject>
#include <QRegularExpression>
#include <QSet>
#include <QTranslator>
#include "api/isessionmanager.h"
#include "base/http/irequesthandler.h"
@ -109,6 +110,8 @@ private:
void sendFile(const QString &path);
void sendWebUIFile();
void translateDocument(QString &data);
// Session management
QString generateSid() const;
void sessionInitialize();
@ -142,6 +145,7 @@ private:
};
QMap<QString, TranslatedFile> m_translatedFiles;
QString m_currentLocale;
QTranslator m_translator;
bool m_isLocalAuthEnabled;
bool m_isAuthSubnetWhitelistEnabled;

View File

@ -10,7 +10,6 @@ HEADERS += \
$$PWD/api/torrentscontroller.h \
$$PWD/api/transfercontroller.h \
$$PWD/api/serialize/serialize_torrent.h \
$$PWD/extra_translations.h \
$$PWD/webapplication.h \
$$PWD/webui.h
@ -29,3 +28,25 @@ SOURCES += \
$$PWD/webui.cpp
RESOURCES += $$PWD/www/webui.qrc
# WebUI Translation
isEmpty(QMAKE_LRELEASE) {
win32: QMAKE_LRELEASE = $$[QT_INSTALL_BINS]/lrelease.exe
else: QMAKE_LRELEASE = $$[QT_INSTALL_BINS]/lrelease
unix {
equals(QT_MAJOR_VERSION, 5) {
!exists($$QMAKE_LRELEASE): QMAKE_LRELEASE = lrelease-qt5
}
}
else {
!exists($$QMAKE_LRELEASE): QMAKE_LRELEASE = lrelease
}
}
WEBUI_TRANSLATIONS = $$files(www/translations/webui_*.ts)
WEBUI_TRANSLATIONS_NOEXT = $$replace(WEBUI_TRANSLATIONS, ".ts", "")
message("Building WebUI translations...")
for(L, WEBUI_TRANSLATIONS_NOEXT) {
message("Processing $${L}")
system("$$QMAKE_LRELEASE -silent $${L}.ts -qm $${L}.qm")
!exists("$${L}.qm"): error("Building WebUI translations failed, cannot continue!")
}

View File

@ -37,22 +37,22 @@
<div id="desktopNavbar">
<ul>
<li>
<a class="returnFalse">QBT_TR(&File)QBT_TR[CONTEXT=MainWindow]</a>
<a class="returnFalse">QBT_TR(File)QBT_TR[CONTEXT=MainWindow]</a>
<ul>
<li><a id="uploadLink"><img class="MyMenuIcon" alt="QBT_TR(&Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/list-add.svg" width="16" height="16"/>QBT_TR(&Add Torrent File...)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="downloadLink"><img class="MyMenuIcon" alt="QBT_TR(Add Torrent &Link...)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/insert-link.svg" width="16" height="16"/>QBT_TR(Add Torrent &Link...)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="uploadLink"><img class="MyMenuIcon" alt="QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/list-add.svg" width="16" height="16"/>QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="downloadLink"><img class="MyMenuIcon" alt="QBT_TR(Add Torrent Link...)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/insert-link.svg" width="16" height="16"/>QBT_TR(Add Torrent Link...)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="logoutLink"><img class="MyMenuIcon" alt="QBT_TR(Logout)QBT_TR[CONTEXT=HttpServer]" src="images/qbt-theme/system-log-out.svg" width="16" height="16"/>QBT_TR(Logout)QBT_TR[CONTEXT=HttpServer]</a></li>
<li><a id="shutdownLink"><img class="MyMenuIcon" alt="QBT_TR(Exit qBittorrent)QBT_TR[CONTEXT=HttpServer]" src="images/qbt-theme/application-exit.svg" width="16" height="16"/>QBT_TR(Exit qBittorrent)QBT_TR[CONTEXT=HttpServer]</a></li>
</ul>
</li>
<li>
<a class="returnFalse">QBT_TR(&Edit)QBT_TR[CONTEXT=MainWindow]</a>
<a class="returnFalse">QBT_TR(Edit)QBT_TR[CONTEXT=MainWindow]</a>
<ul>
<li><a id="resumeAllLink"><img class="MyMenuIcon" alt="QBT_TR(R&esume All)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/media-playback-start.svg" width="16" height="16"/>QBT_TR(R&esume All)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="pauseAllLink"><img class="MyMenuIcon" alt="QBT_TR(P&ause All)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/media-playback-pause.svg" width="16" height="16"/>QBT_TR(P&ause All)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="resumeLink"><img class="MyMenuIcon" alt="QBT_TR(&Resume)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/media-playback-start.svg" width="16" height="16"/>QBT_TR(&Resume)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="pauseLink"><img class="MyMenuIcon" src="images/qbt-theme/media-playback-pause.svg" alt="QBT_TR(&Pause)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(&Pause)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="deleteLink"><img class="MyMenuIcon" src="images/qbt-theme/list-remove.svg" alt="QBT_TR(&Delete)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(&Delete)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="resumeAllLink"><img class="MyMenuIcon" alt="QBT_TR(Resume All)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/media-playback-start.svg" width="16" height="16"/>QBT_TR(Resume All)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="pauseAllLink"><img class="MyMenuIcon" alt="QBT_TR(Pause All)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/media-playback-pause.svg" width="16" height="16"/>QBT_TR(Pause All)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="resumeLink"><img class="MyMenuIcon" alt="QBT_TR(Resume)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/media-playback-start.svg" width="16" height="16"/>QBT_TR(Resume)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="pauseLink"><img class="MyMenuIcon" src="images/qbt-theme/media-playback-pause.svg" alt="QBT_TR(Pause)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Pause)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="deleteLink"><img class="MyMenuIcon" src="images/qbt-theme/list-remove.svg" alt="QBT_TR(Delete)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Delete)QBT_TR[CONTEXT=MainWindow]</a></li>
<li id="topPrioItem" class="divider"><a id="topPrioLink"><img class="MyMenuIcon" src="images/qbt-theme/go-top.svg" alt="QBT_TR(Top Priority)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Top Priority)QBT_TR[CONTEXT=MainWindow]</a></li>
<li id="increasePrioItem"><a id="increasePrioLink"><img class="MyMenuIcon" src="images/qbt-theme/go-up.svg" alt="QBT_TR(Increase Priority)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Increase Priority)QBT_TR[CONTEXT=MainWindow]</a></li>
<li id="decreasePrioItem"><a id="decreasePrioLink"><img class="MyMenuIcon" src="images/qbt-theme/go-down.svg" alt="QBT_TR(Decrease Priority)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Decrease Priority)QBT_TR[CONTEXT=MainWindow]</a></li>
@ -61,34 +61,34 @@
</ul>
</li>
<li>
<a class="returnFalse">QBT_TR(&View)QBT_TR[CONTEXT=MainWindow]</a>
<a class="returnFalse">QBT_TR(View)QBT_TR[CONTEXT=MainWindow]</a>
<ul>
<li><a id="showTopToolbarLink"><img class="MyMenuIcon" src="images/qbt-theme/checked.svg" alt="QBT_TR(&Top Toolbar)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(&Top Toolbar)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="showStatusBarLink"><img class="MyMenuIcon" src="images/qbt-theme/checked.svg" alt="QBT_TR(Status &Bar)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Status &Bar)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="speedInBrowserTitleBarLink"><img class="MyMenuIcon" src="images/qbt-theme/checked.svg" alt="QBT_TR(S&peed in Title Bar)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(S&peed in Title Bar)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id=StatisticsLink><img class="MyMenuIcon" src="images/qbt-theme/view-statistics.svg" alt="QBT_TR(&Statistics)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(&Statistics)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="showTopToolbarLink"><img class="MyMenuIcon" src="images/qbt-theme/checked.svg" alt="QBT_TR(Top Toolbar)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Top Toolbar)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="showStatusBarLink"><img class="MyMenuIcon" src="images/qbt-theme/checked.svg" alt="QBT_TR(Status Bar)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Status Bar)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="speedInBrowserTitleBarLink"><img class="MyMenuIcon" src="images/qbt-theme/checked.svg" alt="QBT_TR(Speed in Title Bar)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Speed in Title Bar)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id=StatisticsLink><img class="MyMenuIcon" src="images/qbt-theme/view-statistics.svg" alt="QBT_TR(Statistics)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Statistics)QBT_TR[CONTEXT=MainWindow]</a></li>
</ul>
</li>
<li>
<a class="returnFalse">QBT_TR(&Tools)QBT_TR[CONTEXT=MainWindow]</a>
<a class="returnFalse">QBT_TR(Tools)QBT_TR[CONTEXT=MainWindow]</a>
<ul>
<li><a id="preferencesLink"><img class="MyMenuIcon" src="images/qbt-theme/configure.svg" alt="QBT_TR(&Options...)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(&Options...)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="preferencesLink"><img class="MyMenuIcon" src="images/qbt-theme/configure.svg" alt="QBT_TR(Options...)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Options...)QBT_TR[CONTEXT=MainWindow]</a></li>
</ul>
</li>
<li>
<a class="returnFalse">QBT_TR(&Help)QBT_TR[CONTEXT=MainWindow]</a>
<a class="returnFalse">QBT_TR(Help)QBT_TR[CONTEXT=MainWindow]</a>
<ul>
<li><a id="docsLink" target="_blank" href="http://wiki.qbittorrent.org/"><img class="MyMenuIcon" src="images/qbt-theme/help-contents.svg" alt="QBT_TR(&Documentation)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(&Documentation)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="bugLink" target="_blank" href="https://www.qbittorrent.org/donate"><img class="MyMenuIcon" src="images/qbt-theme/wallet-open.svg" alt="QBT_TR(Do&nate!)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Do&nate!)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="aboutLink"><img class="MyMenuIcon" src="images/qbt-theme/help-about.svg" alt="QBT_TR(&About)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(&About)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="docsLink" target="_blank" href="http://wiki.qbittorrent.org/"><img class="MyMenuIcon" src="images/qbt-theme/help-contents.svg" alt="QBT_TR(Documentation)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Documentation)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="bugLink" target="_blank" href="https://www.qbittorrent.org/donate"><img class="MyMenuIcon" src="images/qbt-theme/wallet-open.svg" alt="QBT_TR(Donate!)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Donate!)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="aboutLink"><img class="MyMenuIcon" src="images/qbt-theme/help-about.svg" alt="QBT_TR(About)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(About)QBT_TR[CONTEXT=MainWindow]</a></li>
</ul>
</li>
</ul>
</div>
<div id="mochaToolbar">
&nbsp;&nbsp;
<a id="downloadButton"><img class="mochaToolButton" title="QBT_TR(Add Torrent &Link...)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/insert-link.svg" alt="QBT_TR(Add Torrent &Link...)QBT_TR[CONTEXT=MainWindow]" width="24" height="24"/></a>
<a id="uploadButton"><img class="mochaToolButton" title="QBT_TR(&Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/list-add.svg" alt="QBT_TR(&Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" width="24" height="24"/></a>
<a id="downloadButton"><img class="mochaToolButton" title="QBT_TR(Add Torrent Link...)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/insert-link.svg" alt="QBT_TR(Add Torrent Link...)QBT_TR[CONTEXT=MainWindow]" width="24" height="24"/></a>
<a id="uploadButton"><img class="mochaToolButton" title="QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" src="images/qbt-theme/list-add.svg" alt="QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" width="24" height="24"/></a>
<a id="deleteButton" class="divider"><img class="mochaToolButton" title="QBT_TR(Delete)QBT_TR[CONTEXT=TransferListWidget]" src="images/qbt-theme/list-remove.svg" alt="QBT_TR(Delete)QBT_TR[CONTEXT=TransferListWidget]" width="24" height="24"/></a>
<a id="resumeButton" class="divider"><img class="mochaToolButton" title="QBT_TR(Resume)QBT_TR[CONTEXT=TransferListWidget]" src="images/qbt-theme/media-playback-start.svg" alt="QBT_TR(Resume)QBT_TR[CONTEXT=TransferListWidget]" width="24" height="24"/></a>
<a id="pauseButton"><img class="mochaToolButton" title="QBT_TR(Pause)QBT_TR[CONTEXT=TransferListWidget]" src="images/qbt-theme/media-playback-pause.svg" alt="QBT_TR(Pause)QBT_TR[CONTEXT=TransferListWidget]" width="24" height="24"/></a>

View File

@ -51,7 +51,7 @@
<fieldset class="settings">
<legend><input type="checkbox" id="mail_notification_checkbox" onclick="updateMailNotification();" />
<label for="mail_notification_checkbox">QBT_TR(Email notification &upon download completion)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<label for="mail_notification_checkbox">QBT_TR(Email notification upon download completion)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<div class="formRow">
<label for="dest_email_txt" class="leftLabelMedium">QBT_TR(To:)QBT_TR[CONTEXT=OptionsDialog]</label><input type="text" id="dest_email_txt" />
</div>
@ -74,7 +74,7 @@
<fieldset class="settings">
<legend><input type="checkbox" id="autorun_checkbox" onclick="updateAutoRun();" />
<label for="autorun_checkbox">QBT_TR(Run e&xternal program on torrent completion)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<label for="autorun_checkbox">QBT_TR(Run external program on torrent completion)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<input type="text" id="autorunProg_txt" style="width: 400px;" /><br/>
<div style="font-style: italic;">QBT_TR(Supported parameters (case sensitive):)QBT_TR[CONTEXT=OptionsDialog]
<ul>
@ -192,7 +192,7 @@
<fieldset class="settings">
<legend><input type="checkbox" id="ipfilter_enabled_checkbox" onclick="updateFilterSettings();" />
<label for="ipfilter_enabled_checkbox">QBT_TR(IP Fi&ltering)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<label for="ipfilter_enabled_checkbox">QBT_TR(IP Filtering)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<label for="ipfilter_text">QBT_TR(Filter path (.dat, .p2p, .p2b):)QBT_TR[CONTEXT=OptionsDialog]</label>
<input type="text" id="ipfilter_text" /><br/>
<input type="checkbox" id="ipfilter_trackers_checkbox" />
@ -248,7 +248,7 @@
<fieldset class="settings">
<legend><input type="checkbox" id="limit_sheduling_checkbox" onclick="updateSchedulingEnabled();" />
<label for="limit_sheduling_checkbox">QBT_TR(Schedule &the use of alternative rate limits)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<label for="limit_sheduling_checkbox">QBT_TR(Schedule the use of alternative rate limits)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
QBT_TR(From:)QBT_TR[CONTEXT=OptionsDialog]
<input type="text" id="schedule_from_hour" style="width: 1.5em;" />:<input type="text" id="schedule_from_min" style="width: 1.5em;" /> QBT_TR(To:)QBT_TR[CONTEXT=OptionsDialog]
<input type="text" id="schedule_to_hour" style="width: 1.5em;" />:<input type="text" id="schedule_to_min" style="width: 1.5em;" />
@ -299,7 +299,7 @@
<fieldset class="settings">
<legend><input type="checkbox" id="queueing_checkbox" onclick="updateQueueingSystem();" />
<label for="queueing_checkbox">QBT_TR(&Torrent Queueing)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<label for="queueing_checkbox">QBT_TR(Torrent Queueing)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<div class="formRow">
<label for="max_active_dl_value" style="margin-left: 20px;" class="leftLabelLarge">QBT_TR(Maximum active downloads:)QBT_TR[CONTEXT=OptionsDialog]</label>
<input type="text" id="max_active_dl_value" style="width: 4em;" />
@ -354,7 +354,7 @@
<fieldset class="settings">
<legend><input type="checkbox" id="add_trackers_checkbox" onclick="updateAddTrackersEnabled();" />
<label for="add_trackers_checkbox">QBT_TR(A&utomatically add these trackers to new downloads:)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<label for="add_trackers_checkbox">QBT_TR(Automatically add these trackers to new downloads:)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<textarea id="add_trackers_textarea" rows="5" cols="70"></textarea>
</fieldset>
</div>
@ -426,7 +426,7 @@
<label for="webui_upnp_checkbox">QBT_TR(Use UPnP / NAT-PMP to forward the port from my router)QBT_TR[CONTEXT=OptionsDialog]</label><br/>
<fieldset class="settings">
<legend><input type="checkbox" id="use_https_checkbox" onclick="updateHttpsSettings();" />
<label for="use_https_checkbox">QBT_TR(&Use HTTPS instead of HTTP)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<label for="use_https_checkbox">QBT_TR(Use HTTPS instead of HTTP)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<div class="formRow">
<label class="leftLabelSmall" for="ssl_key_textarea">QBT_TR(Key:)QBT_TR[CONTEXT=OptionsDialog]</label>
<textarea id="ssl_key_textarea" rows="5" cols="70"></textarea>
@ -471,7 +471,7 @@
<fieldset class="settings">
<legend><input type="checkbox" id="use_dyndns_checkbox" onclick="updateDynDnsSettings();" />
<label for="use_dyndns_checkbox">QBT_TR(Upda&te my dynamic domain name)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<label for="use_dyndns_checkbox">QBT_TR(Update my dynamic domain name)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<select id="dyndns_select">
<option value="0">DynDNS</option>
<option value="1">NO-IP</option>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

181
src/webui/www/tstool.py Executable file
View File

@ -0,0 +1,181 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# TSTool - script for update qBittorrent WebUI translation files
# Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give permission to
# link this program with the OpenSSL project's "OpenSSL" library (or with
# modified versions of it that use the same license as the "OpenSSL" library),
# and distribute the linked executables. You must obey the GNU General Public
# License in all respects for all of the code used other than "OpenSSL". If you
# modify file(s), you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete this
# exception statement from your version.
import argparse
import copy
import os
import os.path
import re
import sys
import xml.etree.ElementTree as ET
accepted_exts = [".js", ".html", ".css"]
no_obsolete = False
www_folder = "."
ts_folder = os.path.join(www_folder, "translations")
def parseSource(filename, sources):
print("Parsing %s..." % (os.path.normpath(filename)))
with open(filename, encoding = 'utf-8', mode = 'r') as file:
regex = re.compile(
r"QBT_TR\((([^\)]|\)(?!QBT_TR))+)\)QBT_TR\[CONTEXT=([a-zA-Z_][a-zA-Z0-9_]*)\]")
for match in regex.finditer(file.read()):
string = match.group(1)
context = match.group(3)
if context not in sources:
sources[context] = set()
sources[context].add(string)
def processTranslation(filename, sources):
print('Processing %s...' % (os.path.normpath(filename)))
try:
tree = ET.ElementTree(file = filename)
except Exception:
print('\tFailed to parse %s!' % (os.path.normpath(filename)))
return
root = tree.getroot()
for context in root.findall('context'):
context_name = context.find('name').text
has_context = context_name in sources
if not has_context and no_obsolete:
root.remove(context)
continue
for message in context.findall('message'):
for location in message.findall('location'):
message.remove(location)
source = message.find('source').text
translation = message.find('translation')
if has_context and source in sources[context_name]:
sources[context_name].remove(source)
trtype = translation.attrib.get('type')
if (trtype == 'obsolete') or (trtype == 'vanished'):
del translation.attrib['type'] # i.e. finished
else:
if no_obsolete or (translation.attrib.get('type', '') == 'unfinished'):
context.remove(message)
else:
translation.attrib['type'] = 'vanished'
if not has_context:
continue
# add new messages for current context
for source in sources[context_name]:
message = ET.SubElement(context, 'message')
ET.SubElement(message, 'source').text = source
ET.SubElement(message, 'translation', {'type': 'unfinished'})
del sources[context_name]
# add messages for new contexts
for context_name in sources:
context = ET.SubElement(root, 'context')
ET.SubElement(context, 'name').text = context_name
for source in sources[context_name]:
message = ET.SubElement(context, 'message')
ET.SubElement(message, 'source').text = source
ET.SubElement(message, 'translation', {'type': 'unfinished'})
# prettify output xml
indent = ' ' * 4
root.text = '\n'
for context in root.findall('./context'):
context.text = '\n' + indent
context.tail = '\n'
context.find('./name').tail = '\n' + indent
messages = context.findall('./message')
if len(messages) == 0: continue
for message in messages:
message.text = '\n' + (indent * 2)
message.tail = '\n' + indent
elems = message.findall('./')
if len(elems) == 0: continue
for elem in elems:
elem.tail = '\n' + (indent * 2)
elems[-1:][0].tail = '\n' + indent
messages[-1:][0].tail = '\n'
try:
with open(filename, mode = 'wb') as file:
file.write(b'<?xml version="1.0" encoding="utf-8"?>\n'
b'<!DOCTYPE TS>\n')
tree.write(file, encoding = 'utf-8')
except Exception:
print('\tFailed to write %s!' % (os.path.normpath(filename)))
argp = argparse.ArgumentParser(
prog = 'tstool.py', description = 'Update qBittorrent WebUI translation files.')
argp.add_argument('--no-obsolete', dest = 'no_obsolete', action = 'store_true',
default = no_obsolete,
help = 'remove obsolete messages (default: mark them as obsolete)')
argp.add_argument('--www-folder', dest = 'www_folder', action = 'store',
default = www_folder,
help = 'folder with WebUI source files (default: "%s")' % (www_folder))
argp.add_argument('--ts-folder', dest = 'ts_folder', action = 'store',
default = ts_folder,
help = 'folder with WebUI translation files (default: "%s")' % (ts_folder))
args = argp.parse_args()
no_obsolete = args.no_obsolete
www_folder = args.www_folder
ts_folder = args.ts_folder
print("Processing source files...")
nfiles = 0
source_ts = {}
for root, dirs, files in os.walk(www_folder):
for file in files:
if os.path.splitext(file)[-1] in accepted_exts:
parseSource(os.path.join(root, file), source_ts)
nfiles += 1
if nfiles == 0:
print("No source files found!")
sys.exit()
nstrings = sum(len(sublist) for sublist in source_ts)
print("Found %d strings within %d contexts." % (nstrings, len(source_ts)))
print("")
print("Processing translation files...")
for entry in os.scandir(ts_folder):
if (entry.is_file() and entry.name.startswith('webui_')
and entry.name.endswith(".ts")):
processTranslation(entry.path, copy.deepcopy(source_ts))
print("Done!")

View File

@ -45,5 +45,55 @@
<file>public/css/login.css</file>
<file>public/login.html</file>
<file>public/scripts/lib/mootools-1.2-core-yc.js</file>
<file>translations/webui_ar.qm</file>
<file>translations/webui_be.qm</file>
<file>translations/webui_bg.qm</file>
<file>translations/webui_ca.qm</file>
<file>translations/webui_cs.qm</file>
<file>translations/webui_da.qm</file>
<file>translations/webui_de.qm</file>
<file>translations/webui_el.qm</file>
<file>translations/webui_en.qm</file>
<file>translations/webui_en_AU.qm</file>
<file>translations/webui_en_GB.qm</file>
<file>translations/webui_eo.qm</file>
<file>translations/webui_es.qm</file>
<file>translations/webui_eu.qm</file>
<file>translations/webui_fi.qm</file>
<file>translations/webui_fr.qm</file>
<file>translations/webui_gl.qm</file>
<file>translations/webui_he.qm</file>
<file>translations/webui_hi_IN.qm</file>
<file>translations/webui_hr.qm</file>
<file>translations/webui_hu.qm</file>
<file>translations/webui_hy.qm</file>
<file>translations/webui_id.qm</file>
<file>translations/webui_is.qm</file>
<file>translations/webui_it.qm</file>
<file>translations/webui_ja.qm</file>
<file>translations/webui_ka.qm</file>
<file>translations/webui_ko.qm</file>
<file>translations/webui_lt.qm</file>
<file>translations/webui_lv_LV.qm</file>
<file>translations/webui_ms_MY.qm</file>
<file>translations/webui_nb.qm</file>
<file>translations/webui_nl.qm</file>
<file>translations/webui_oc.qm</file>
<file>translations/webui_pl.qm</file>
<file>translations/webui_pt_BR.qm</file>
<file>translations/webui_pt_PT.qm</file>
<file>translations/webui_ro.qm</file>
<file>translations/webui_ru.qm</file>
<file>translations/webui_sk.qm</file>
<file>translations/webui_sl.qm</file>
<file>translations/webui_sr.qm</file>
<file>translations/webui_sv.qm</file>
<file>translations/webui_tr.qm</file>
<file>translations/webui_uk.qm</file>
<file>translations/webui_uz@Latn.qm</file>
<file>translations/webui_vi.qm</file>
<file>translations/webui_zh.qm</file>
<file>translations/webui_zh_HK.qm</file>
<file>translations/webui_zh_TW.qm</file>
</qresource>
</RCC>