aboutsummaryrefslogtreecommitdiff
path: root/api/logic/translations
diff options
context:
space:
mode:
Diffstat (limited to 'api/logic/translations')
-rw-r--r--api/logic/translations/POTranslator.cpp373
-rw-r--r--api/logic/translations/POTranslator.h16
-rw-r--r--api/logic/translations/TranslationsModel.cpp446
-rw-r--r--api/logic/translations/TranslationsModel.h14
4 files changed, 788 insertions, 61 deletions
diff --git a/api/logic/translations/POTranslator.cpp b/api/logic/translations/POTranslator.cpp
new file mode 100644
index 00000000..1ffcb9a4
--- /dev/null
+++ b/api/logic/translations/POTranslator.cpp
@@ -0,0 +1,373 @@
+#include "POTranslator.h"
+
+#include <QDebug>
+#include "FileSystem.h"
+
+struct POEntry
+{
+ QString text;
+ bool fuzzy;
+};
+
+struct POTranslatorPrivate
+{
+ QString filename;
+ QHash<QByteArray, POEntry> mapping;
+ QHash<QByteArray, POEntry> mapping_disambiguatrion;
+ bool loaded = false;
+
+ void reload();
+};
+
+class ParserArray : public QByteArray
+{
+public:
+ ParserArray(const QByteArray &in) : QByteArray(in)
+ {
+ }
+ bool chomp(const char * data, int length)
+ {
+ if(startsWith(data))
+ {
+ remove(0, length);
+ return true;
+ }
+ return false;
+ }
+ bool chompString(QByteArray & appendHere)
+ {
+ QByteArray msg;
+ bool escape = false;
+ if(size() < 2)
+ {
+ qDebug() << "String fragment is too short";
+ return false;
+ }
+ if(!startsWith('"'))
+ {
+ qDebug() << "String fragment does not start with \"";
+ return false;
+ }
+ if(!endsWith('"'))
+ {
+ qDebug() << "String fragment does not end with \", instead, there is" << at(size() - 1);
+ return false;
+ }
+ for(int i = 1; i < size() - 1; i++)
+ {
+ char c = operator[](i);
+ if(escape)
+ {
+ switch(c)
+ {
+ case 'r':
+ msg += '\r';
+ break;
+ case 'n':
+ msg += '\n';
+ break;
+ case 't':
+ msg += '\t';
+ break;
+ case 'v':
+ msg += '\v';
+ break;
+ case 'a':
+ msg += '\a';
+ break;
+ case 'b':
+ msg += '\b';
+ break;
+ case 'f':
+ msg += '\f';
+ break;
+ case '"':
+ msg += '"';
+ break;
+ case '\\':
+ msg.append('\\');
+ break;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ {
+ int octal_start = i;
+ while ((c = operator[](i)) >= '0' && c <= '7')
+ {
+ i++;
+ if (i == length() - 1)
+ {
+ qDebug() << "Something went bad while parsing an octal escape string...";
+ return false;
+ }
+ }
+ msg += mid(octal_start, i - octal_start).toUInt(0, 8);
+ break;
+ }
+ case 'x':
+ {
+ // chomp the 'x'
+ i++;
+ int hex_start = i;
+ while (isxdigit(operator[](i)))
+ {
+ i++;
+ if (i == length() - 1)
+ {
+ qDebug() << "Something went bad while parsing a hex escape string...";
+ return false;
+ }
+ }
+ msg += mid(hex_start, i - hex_start).toUInt(0, 16);
+ break;
+ }
+ default:
+ {
+ qDebug() << "Invalid escape sequence character:" << c;
+ return false;
+ }
+ }
+ escape = false;
+ }
+ else if(c == '\\')
+ {
+ escape = true;
+ }
+ else
+ {
+ msg += c;
+ }
+ }
+ if(escape)
+ {
+ qDebug() << "Unterminated escape sequence...";
+ return false;
+ }
+ appendHere += msg;
+ return true;
+ }
+};
+
+void POTranslatorPrivate::reload()
+{
+ QFile file(filename);
+ if(!file.open(QFile::OpenMode::enum_type::ReadOnly | QFile::OpenMode::enum_type::Text))
+ {
+ qDebug() << "Failed to open PO file:" << filename;
+ return;
+ }
+
+ QByteArray context;
+ QByteArray disambiguation;
+ QByteArray id;
+ QByteArray str;
+ bool fuzzy = false;
+ bool nextFuzzy = false;
+
+ enum class Mode
+ {
+ First,
+ MessageContext,
+ MessageId,
+ MessageString
+ } mode = Mode::First;
+
+ int lineNumber = 0;
+ QHash<QByteArray, POEntry> newMapping;
+ QHash<QByteArray, POEntry> newMapping_disambiguation;
+ auto endEntry = [&]() {
+ auto strStr = QString::fromUtf8(str);
+ // NOTE: PO header has empty id. We skip it.
+ if(!id.isEmpty())
+ {
+ auto normalKey = context + "|" + id;
+ newMapping.insert(normalKey, {strStr, fuzzy});
+ if(!disambiguation.isEmpty())
+ {
+ auto disambiguationKey = context + "|" + id + "@" + disambiguation;
+ newMapping_disambiguation.insert(disambiguationKey, {strStr, fuzzy});
+ }
+ }
+ context.clear();
+ disambiguation.clear();
+ id.clear();
+ str.clear();
+ fuzzy = nextFuzzy;
+ nextFuzzy = false;
+ };
+ while (!file.atEnd())
+ {
+ ParserArray line = file.readLine();
+ if(line.endsWith('\n'))
+ {
+ line.resize(line.size() - 1);
+ }
+ if(line.endsWith('\r'))
+ {
+ line.resize(line.size() - 1);
+ }
+
+ if(!line.size())
+ {
+ // NIL
+ }
+ else if(line[0] == '#')
+ {
+ if(line.contains(", fuzzy"))
+ {
+ nextFuzzy = true;
+ }
+ }
+ else if(line.startsWith('"'))
+ {
+ QByteArray temp;
+ QByteArray *out = &temp;
+
+ switch(mode)
+ {
+ case Mode::First:
+ qDebug() << "Unexpected escaped string during initial state... line:" << lineNumber;
+ return;
+ case Mode::MessageString:
+ out = &str;
+ break;
+ case Mode::MessageContext:
+ out = &context;
+ break;
+ case Mode::MessageId:
+ out = &id;
+ break;
+ }
+ if(!line.chompString(*out))
+ {
+ qDebug() << "Badly formatted string on line:" << lineNumber;
+ return;
+ }
+ }
+ else if(line.chomp("msgctxt ", 8))
+ {
+ switch(mode)
+ {
+ case Mode::First:
+ break;
+ case Mode::MessageString:
+ endEntry();
+ break;
+ case Mode::MessageContext:
+ case Mode::MessageId:
+ qDebug() << "Unexpected msgctxt line:" << lineNumber;
+ return;
+ }
+ if(line.chompString(context))
+ {
+ auto parts = context.split('|');
+ context = parts[0];
+ if(parts.size() > 1 && !parts[1].isEmpty())
+ {
+ disambiguation = parts[1];
+ }
+ mode = Mode::MessageContext;
+ }
+ }
+ else if (line.chomp("msgid ", 6))
+ {
+ switch(mode)
+ {
+ case Mode::MessageContext:
+ case Mode::First:
+ break;
+ case Mode::MessageString:
+ endEntry();
+ break;
+ case Mode::MessageId:
+ qDebug() << "Unexpected msgid line:" << lineNumber;
+ return;
+ }
+ if(line.chompString(id))
+ {
+ mode = Mode::MessageId;
+ }
+ }
+ else if (line.chomp("msgstr ", 7))
+ {
+ switch(mode)
+ {
+ case Mode::First:
+ case Mode::MessageString:
+ case Mode::MessageContext:
+ qDebug() << "Unexpected msgstr line:" << lineNumber;
+ return;
+ case Mode::MessageId:
+ break;
+ }
+ if(line.chompString(str))
+ {
+ mode = Mode::MessageString;
+ }
+ }
+ else
+ {
+ qDebug() << "I did not understand line: " << lineNumber << ":" << QString::fromUtf8(line);
+ }
+ lineNumber++;
+ }
+ endEntry();
+ mapping = std::move(newMapping);
+ mapping_disambiguatrion = std::move(newMapping_disambiguation);
+ loaded = true;
+}
+
+POTranslator::POTranslator(const QString& filename, QObject* parent) : QTranslator(parent)
+{
+ d = new POTranslatorPrivate;
+ d->filename = filename;
+ d->reload();
+}
+
+QString POTranslator::translate(const char* context, const char* sourceText, const char* disambiguation, int n) const
+{
+ if(disambiguation)
+ {
+ auto disambiguationKey = QByteArray(context) + "|" + QByteArray(sourceText) + "@" + QByteArray(disambiguation);
+ auto iter = d->mapping_disambiguatrion.find(disambiguationKey);
+ if(iter != d->mapping_disambiguatrion.end())
+ {
+ auto & entry = *iter;
+ if(entry.text.isEmpty())
+ {
+ qDebug() << "Translation entry has no content:" << disambiguationKey;
+ }
+ if(entry.fuzzy)
+ {
+ qDebug() << "Translation entry is fuzzy:" << disambiguationKey << "->" << entry.text;
+ }
+ return entry.text;
+ }
+ }
+ auto key = QByteArray(context) + "|" + QByteArray(sourceText);
+ auto iter = d->mapping.find(key);
+ if(iter != d->mapping.end())
+ {
+ auto & entry = *iter;
+ if(entry.text.isEmpty())
+ {
+ qDebug() << "Translation entry has no content:" << key;
+ }
+ if(entry.fuzzy)
+ {
+ qDebug() << "Translation entry is fuzzy:" << key << "->" << entry.text;
+ }
+ return entry.text;
+ }
+ return QString();
+}
+
+bool POTranslator::isEmpty() const
+{
+ return !d->loaded;
+}
diff --git a/api/logic/translations/POTranslator.h b/api/logic/translations/POTranslator.h
new file mode 100644
index 00000000..6d518560
--- /dev/null
+++ b/api/logic/translations/POTranslator.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <QTranslator>
+
+struct POTranslatorPrivate;
+
+class POTranslator : public QTranslator
+{
+ Q_OBJECT
+public:
+ explicit POTranslator(const QString& filename, QObject * parent = nullptr);
+ QString translate(const char * context, const char * sourceText, const char * disambiguation, int n) const override;
+ bool isEmpty() const override;
+private:
+ POTranslatorPrivate * d;
+};
diff --git a/api/logic/translations/TranslationsModel.cpp b/api/logic/translations/TranslationsModel.cpp
index 185cc8c4..29a952b0 100644
--- a/api/logic/translations/TranslationsModel.cpp
+++ b/api/logic/translations/TranslationsModel.cpp
@@ -8,24 +8,113 @@
#include <QDebug>
#include <FileSystem.h>
#include <net/NetJob.h>
+#include <net/ChecksumValidator.h>
#include <Env.h>
-#include <net/URLConstants.h>
+#include <BuildConfig.h>
+#include "Json.h"
-const static QLatin1Literal defaultLangCode("en");
+#include "POTranslator.h"
+
+const static QLatin1Literal defaultLangCode("en_US");
+
+enum class FileType
+{
+ NONE,
+ QM,
+ PO
+};
struct Language
{
+ Language()
+ {
+ updated = true;
+ }
+ Language(const QString & _key)
+ {
+ key = _key;
+ locale = QLocale(key);
+ updated = (key == defaultLangCode);
+ }
+
+ float percentTranslated() const
+ {
+ if (total == 0)
+ {
+ return 100.0f;
+ }
+ return 100.0f * float(translated) / float(total);
+ }
+
+ void setTranslationStats(unsigned _translated, unsigned _untranslated, unsigned _fuzzy)
+ {
+ translated = _translated;
+ untranslated = _untranslated;
+ fuzzy = _fuzzy;
+ total = translated + untranslated + fuzzy;
+ }
+
+ bool isOfSameNameAs(const Language& other) const
+ {
+ return key == other.key;
+ }
+
+ bool isIdenticalTo(const Language& other) const
+ {
+ return
+ (
+ key == other.key &&
+ file_name == other.file_name &&
+ file_size == other.file_size &&
+ file_sha1 == other.file_sha1 &&
+ translated == other.translated &&
+ fuzzy == other.fuzzy &&
+ total == other.fuzzy &&
+ localFileType == other.localFileType
+ );
+ }
+
+ Language & apply(Language & other)
+ {
+ if(!isOfSameNameAs(other))
+ {
+ return *this;
+ }
+ file_name = other.file_name;
+ file_size = other.file_size;
+ file_sha1 = other.file_sha1;
+ translated = other.translated;
+ fuzzy = other.fuzzy;
+ total = other.total;
+ localFileType = other.localFileType;
+ return *this;
+ }
+
QString key;
QLocale locale;
bool updated;
+
+ QString file_name = QString();
+ std::size_t file_size = 0;
+ QString file_sha1 = QString();
+
+ unsigned translated = 0;
+ unsigned untranslated = 0;
+ unsigned fuzzy = 0;
+ unsigned total = 0;
+
+ FileType localFileType = FileType::NONE;
};
+
+
struct TranslationsModel::Private
{
QDir m_dir;
// initial state is just english
- QVector<Language> m_languages = {{defaultLangCode, QLocale(defaultLangCode), false}};
+ QVector<Language> m_languages = {Language (defaultLangCode)};
+
QString m_selectedLanguage = defaultLangCode;
std::unique_ptr<QTranslator> m_qt_translator;
std::unique_ptr<QTranslator> m_app_translator;
@@ -35,45 +124,285 @@ struct TranslationsModel::Private
NetJobPtr m_dl_job;
NetJobPtr m_index_job;
QString m_nextDownload;
+
+ std::unique_ptr<POTranslator> m_po_translator;
+ QFileSystemWatcher *watcher;
};
TranslationsModel::TranslationsModel(QString path, QObject* parent): QAbstractListModel(parent)
{
d.reset(new Private);
d->m_dir.setPath(path);
- loadLocalIndex();
+ FS::ensureFolderPathExists(path);
+ reloadLocalFiles();
+
+ d->watcher = new QFileSystemWatcher(this);
+ connect(d->watcher, &QFileSystemWatcher::directoryChanged, this, &TranslationsModel::translationDirChanged);
+ d->watcher->addPath(d->m_dir.canonicalPath());
}
TranslationsModel::~TranslationsModel()
{
}
+void TranslationsModel::translationDirChanged(const QString& path)
+{
+ qDebug() << "Dir changed:" << path;
+ reloadLocalFiles();
+ selectLanguage(selectedLanguage());
+}
+
+void TranslationsModel::indexReceived()
+{
+ qDebug() << "Got translations index!";
+ d->m_index_job.reset();
+ if(d->m_selectedLanguage != defaultLangCode)
+ {
+ downloadTranslation(d->m_selectedLanguage);
+ }
+}
+
+namespace {
+void readIndex(const QString & path, QMap<QString, Language>& languages)
+{
+ QByteArray data;
+ try
+ {
+ data = FS::read(path);
+ }
+ catch (const Exception &e)
+ {
+ qCritical() << "Translations Download Failed: index file not readable";
+ return;
+ }
+
+ int index = 1;
+ try
+ {
+ auto toplevel_doc = Json::requireDocument(data);
+ auto doc = Json::requireObject(toplevel_doc);
+ auto file_type = Json::requireString(doc, "file_type");
+ if(file_type != "MMC-TRANSLATION-INDEX")
+ {
+ qCritical() << "Translations Download Failed: index file is of unknown file type" << file_type;
+ return;
+ }
+ auto version = Json::requireInteger(doc, "version");
+ if(version > 2)
+ {
+ qCritical() << "Translations Download Failed: index file is of unknown format version" << file_type;
+ return;
+ }
+ auto langObjs = Json::requireObject(doc, "languages");
+ for(auto iter = langObjs.begin(); iter != langObjs.end(); iter++)
+ {
+ Language lang(iter.key());
+
+ auto langObj = Json::requireObject(iter.value());
+ lang.setTranslationStats(
+ Json::ensureInteger(langObj, "translated", 0),
+ Json::ensureInteger(langObj, "untranslated", 0),
+ Json::ensureInteger(langObj, "fuzzy", 0)
+ );
+ lang.file_name = Json::requireString(langObj, "file");
+ lang.file_sha1 = Json::requireString(langObj, "sha1");
+ lang.file_size = Json::requireInteger(langObj, "size");
+
+ languages.insert(lang.key, lang);
+ index++;
+ }
+ }
+ catch (Json::JsonException & e)
+ {
+ qCritical() << "Translations Download Failed: index file could not be parsed as json";
+ }
+}
+}
+
+void TranslationsModel::reloadLocalFiles()
+{
+ QMap<QString, Language> languages = {{defaultLangCode, Language(defaultLangCode)}};
+
+ readIndex(d->m_dir.absoluteFilePath("index_v2.json"), languages);
+ auto entries = d->m_dir.entryInfoList({"mmc_*.qm", "*.po"}, QDir::Files | QDir::NoDotAndDotDot);
+ for(auto & entry: entries)
+ {
+ auto completeSuffix = entry.completeSuffix();
+ QString langCode;
+ FileType fileType = FileType::NONE;
+ if(completeSuffix == "qm")
+ {
+ langCode = entry.baseName().remove(0,4);
+ fileType = FileType::QM;
+ }
+ else if(completeSuffix == "po")
+ {
+ langCode = entry.baseName();
+ fileType = FileType::PO;
+ }
+ else
+ {
+ continue;
+ }
+
+ auto langIter = languages.find(langCode);
+ if(langIter != languages.end())
+ {
+ auto & language = *langIter;
+ if(int(fileType) > int(language.localFileType))
+ {
+ language.localFileType = fileType;
+ }
+ }
+ else
+ {
+ if(fileType == FileType::PO)
+ {
+ Language localFound(langCode);
+ localFound.localFileType = FileType::PO;
+ languages.insert(langCode, localFound);
+ }
+ }
+ }
+
+ // changed and removed languages
+ for(auto iter = d->m_languages.begin(); iter != d->m_languages.end();)
+ {
+ auto &language = *iter;
+ auto row = iter - d->m_languages.begin();
+
+ auto updatedLanguageIter = languages.find(language.key);
+ if(updatedLanguageIter != languages.end())
+ {
+ if(language.isIdenticalTo(*updatedLanguageIter))
+ {
+ languages.remove(language.key);
+ }
+ else
+ {
+ language.apply(*updatedLanguageIter);
+ emit dataChanged(index(row), index(row));
+ languages.remove(language.key);
+ }
+ iter++;
+ }
+ else
+ {
+ beginRemoveRows(QModelIndex(), row, row);
+ iter = d->m_languages.erase(iter);
+ endRemoveRows();
+ }
+ }
+ // added languages
+ if(languages.isEmpty())
+ {
+ return;
+ }
+ beginInsertRows(QModelIndex(), 0, d->m_languages.size() + languages.size() - 1);
+ for(auto & language: languages)
+ {
+ d->m_languages.append(language);
+ }
+ std::sort(d->m_languages.begin(), d->m_languages.end(), [](const Language& a, const Language& b) {
+ return a.key.compare(b.key) < 0;
+ });
+ endInsertRows();
+}
+
+namespace {
+enum class Column
+{
+ Language,
+ Completeness
+};
+}
+
+
QVariant TranslationsModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
return QVariant();
int row = index.row();
+ auto column = static_cast<Column>(index.column());
if (row < 0 || row >= d->m_languages.size())
return QVariant();
+ auto & lang = d->m_languages[row];
switch (role)
{
case Qt::DisplayRole:
- return d->m_languages[row].locale.nativeLanguageName();
+ {
+ switch(column)
+ {
+ case Column::Language:
+ {
+ return lang.locale.nativeLanguageName();
+ }
+ case Column::Completeness:
+ {
+ QString text;
+ text.sprintf("%3.1f %%", lang.percentTranslated());
+ return text;
+ }
+ }
+ }
+ case Qt::ToolTipRole:
+ {
+ return tr("%1:\n%2 translated\n%3 fuzzy\n%4 total").arg(lang.key, QString::number(lang.translated), QString::number(lang.fuzzy), QString::number(lang.total));
+ }
case Qt::UserRole:
- return d->m_languages[row].key;
+ return lang.key;
default:
return QVariant();
}
}
+QVariant TranslationsModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ auto column = static_cast<Column>(section);
+ if(role == Qt::DisplayRole)
+ {
+ switch(column)
+ {
+ case Column::Language:
+ {
+ return tr("Language");
+ }
+ case Column::Completeness:
+ {
+ return tr("Completeness");
+ }
+ }
+ }
+ else if(role == Qt::ToolTipRole)
+ {
+ switch(column)
+ {
+ case Column::Language:
+ {
+ return tr("The native language name.");
+ }
+ case Column::Completeness:
+ {
+ return tr("Completeness is the percentage of fully translated strings, not counting automatically guessed ones.");
+ }
+ }
+ }
+ return QAbstractListModel::headerData(section, orientation, role);
+}
+
int TranslationsModel::rowCount(const QModelIndex& parent) const
{
return d->m_languages.size();
}
+int TranslationsModel::columnCount(const QModelIndex& parent) const
+{
+ return 2;
+}
+
Language * TranslationsModel::findLanguage(const QString& key)
{
auto found = std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language & lang)
@@ -121,7 +450,7 @@ bool TranslationsModel::selectLanguage(QString key)
* In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created.
* This function is not reentrant.
*/
- QLocale locale(langCode);
+ QLocale locale = QLocale(langCode);
QLocale::setDefault(locale);
// if it's the default UI language, finish
@@ -153,18 +482,48 @@ bool TranslationsModel::selectLanguage(QString key)
d->m_qt_translator.reset();
}
- d->m_app_translator.reset(new QTranslator());
- if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path()))
+ if(langPtr->localFileType == FileType::PO)
{
qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "...";
- if (!QCoreApplication::installTranslator(d->m_app_translator.get()))
+ auto poTranslator = new POTranslator(FS::PathCombine(d->m_dir.path(), langCode + ".po"));
+ if(!poTranslator->isEmpty())
+ {
+ if (!QCoreApplication::installTranslator(poTranslator))
+ {
+ delete poTranslator;
+ qCritical() << "Installing Application Language File failed.";
+ }
+ else
+ {
+ d->m_app_translator.reset(poTranslator);
+ successful = true;
+ }
+ }
+ else
{
qCritical() << "Loading Application Language File failed.";
d->m_app_translator.reset();
}
+ }
+ else if(langPtr->localFileType == FileType::QM)
+ {
+ d->m_app_translator.reset(new QTranslator());
+ if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path()))
+ {
+ qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "...";
+ if (!QCoreApplication::installTranslator(d->m_app_translator.get()))
+ {
+ qCritical() << "Installing Application Language File failed.";
+ d->m_app_translator.reset();
+ }
+ else
+ {
+ successful = true;
+ }
+ }
else
{
- successful = true;
+ d->m_app_translator.reset();
}
}
else
@@ -199,55 +558,15 @@ void TranslationsModel::downloadIndex()
}
qDebug() << "Downloading Translations Index...";
d->m_index_job.reset(new NetJob("Translations Index"));
- MetaEntryPtr entry = ENV.metacache()->resolveEntry("translations", "index");
- d->m_index_task = Net::Download::makeCached(QUrl("http://files.multimc.org/translations/index"), entry);
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("translations", "index_v2.json");
+ entry->setStale(true);
+ d->m_index_task = Net::Download::makeCached(QUrl("https://files.multimc.org/translations/index_v2.json"), entry);
d->m_index_job->addNetAction(d->m_index_task);
connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed);
- connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexRecieved);
+ connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived);
d->m_index_job->start();
}
-void TranslationsModel::indexRecieved()
-{
- qDebug() << "Got translations index!";
- d->m_index_job.reset();
- loadLocalIndex();
- if(d->m_selectedLanguage != defaultLangCode)
- {
- downloadTranslation(d->m_selectedLanguage);
- }
-}
-
-void TranslationsModel::loadLocalIndex()
-{
- QByteArray data;
- try
- {
- data = FS::read(d->m_dir.absoluteFilePath("index"));
- }
- catch (const Exception &e)
- {
- qCritical() << "Translations Download Failed: index file not readable";
- return;
- }
- QVector<Language> languages;
- QList<QByteArray> lines = data.split('\n');
- // add the default english.
- languages.append({defaultLangCode, QLocale(defaultLangCode), true});
- for (const auto line : lines)
- {
- if(!line.isEmpty())
- {
- auto str = QString::fromLatin1(line);
- str.remove(".qm");
- languages.append({str, QLocale(str), false});
- }
- }
- beginResetModel();
- d->m_languages.swap(languages);
- endResetModel();
-}
-
void TranslationsModel::updateLanguage(QString key)
{
if(key == defaultLangCode)
@@ -274,13 +593,28 @@ void TranslationsModel::downloadTranslation(QString key)
d->m_nextDownload = key;
return;
}
+ auto lang = findLanguage(key);
+ if(!lang)
+ {
+ qWarning() << "Will not download an unknown translation" << key;
+ return;
+ }
+
d->m_downloadingTranslation = key;
MetaEntryPtr entry = ENV.metacache()->resolveEntry("translations", "mmc_" + key + ".qm");
entry->setStale(true);
+
+ auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + lang->file_name), entry);
+ auto rawHash = QByteArray::fromHex(lang->file_sha1.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash));
+ dl->m_total_progress = lang->file_size;
+
d->m_dl_job.reset(new NetJob("Translation for " + key));
- d->m_dl_job->addNetAction(Net::Download::makeCached(QUrl(URLConstants::TRANSLATIONS_BASE_URL + key + ".qm"), entry));
+ d->m_dl_job->addNetAction(dl);
+
connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood);
connect(d->m_dl_job.get(), &NetJob::failed, this, &TranslationsModel::dlFailed);
+
d->m_dl_job->start();
}
diff --git a/api/logic/translations/TranslationsModel.h b/api/logic/translations/TranslationsModel.h
index 8a9298d4..17e5b124 100644
--- a/api/logic/translations/TranslationsModel.h
+++ b/api/logic/translations/TranslationsModel.h
@@ -1,4 +1,4 @@
-/* Copyright 2013-2018 MultiMC Contributors
+/* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,8 +28,10 @@ public:
explicit TranslationsModel(QString path, QObject *parent = 0);
virtual ~TranslationsModel();
- virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
- virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex & parent) const override;
bool selectLanguage(QString key);
void updateLanguage(QString key);
@@ -40,7 +42,7 @@ public:
private:
Language *findLanguage(const QString & key);
- void loadLocalIndex();
+ void reloadLocalFiles();
void downloadTranslation(QString key);
void downloadNext();
@@ -50,10 +52,12 @@ private:
TranslationsModel &operator=(const TranslationsModel &) = delete;
private slots:
- void indexRecieved();
+ void indexReceived();
void indexFailed(QString reason);
void dlFailed(QString reason);
void dlGood();
+ void translationDirChanged(const QString &path);
+
private: /* data */
struct Private;