NOISSUE Flatten gui and logic libraries into MultiMC

This commit is contained in:
Petr Mrázek
2021-07-25 19:11:59 +02:00
parent dd13368085
commit 20b9f2b42a
1113 changed files with 1228 additions and 1401 deletions

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -0,0 +1,653 @@
#include "TranslationsModel.h"
#include <QCoreApplication>
#include <QTranslator>
#include <QLocale>
#include <QDir>
#include <QLibraryInfo>
#include <QDebug>
#include <FileSystem.h>
#include <net/NetJob.h>
#include <net/ChecksumValidator.h>
#include <Env.h>
#include <BuildConfig.h>
#include "Json.h"
#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 = {Language (defaultLangCode)};
QString m_selectedLanguage = defaultLangCode;
std::unique_ptr<QTranslator> m_qt_translator;
std::unique_ptr<QTranslator> m_app_translator;
std::shared_ptr<Net::Download> m_index_task;
QString m_downloadingTranslation;
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);
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:
{
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 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)
{
return lang.key == key;
});
if(found == d->m_languages.end())
{
return nullptr;
}
else
{
return found;
}
}
bool TranslationsModel::selectLanguage(QString key)
{
QString &langCode = key;
auto langPtr = findLanguage(key);
if(!langPtr)
{
qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode;
langCode = defaultLangCode;
}
else
{
langCode = langPtr->key;
}
// uninstall existing translators if there are any
if (d->m_app_translator)
{
QCoreApplication::removeTranslator(d->m_app_translator.get());
d->m_app_translator.reset();
}
if (d->m_qt_translator)
{
QCoreApplication::removeTranslator(d->m_qt_translator.get());
d->m_qt_translator.reset();
}
/*
* FIXME: potential source of crashes:
* 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 = QLocale(langCode);
QLocale::setDefault(locale);
// if it's the default UI language, finish
if(langCode == defaultLangCode)
{
d->m_selectedLanguage = langCode;
return true;
}
// otherwise install new translations
bool successful = false;
// FIXME: this is likely never present. FIX IT.
d->m_qt_translator.reset(new QTranslator());
if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::location(QLibraryInfo::TranslationsPath)))
{
qDebug() << "Loading Qt Language File for" << langCode.toLocal8Bit().constData() << "...";
if (!QCoreApplication::installTranslator(d->m_qt_translator.get()))
{
qCritical() << "Loading Qt Language File failed.";
d->m_qt_translator.reset();
}
else
{
successful = true;
}
}
else
{
d->m_qt_translator.reset();
}
if(langPtr->localFileType == FileType::PO)
{
qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "...";
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
{
d->m_app_translator.reset();
}
}
else
{
d->m_app_translator.reset();
}
d->m_selectedLanguage = langCode;
return successful;
}
QModelIndex TranslationsModel::selectedIndex()
{
auto found = findLanguage(d->m_selectedLanguage);
if(found)
{
// QVector iterator freely converts to pointer to contained type
return index(found - d->m_languages.begin(), 0, QModelIndex());
}
return QModelIndex();
}
QString TranslationsModel::selectedLanguage()
{
return d->m_selectedLanguage;
}
void TranslationsModel::downloadIndex()
{
if(d->m_index_job || d->m_dl_job)
{
return;
}
qDebug() << "Downloading Translations Index...";
d->m_index_job.reset(new NetJob("Translations Index"));
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::indexReceived);
d->m_index_job->start();
}
void TranslationsModel::updateLanguage(QString key)
{
if(key == defaultLangCode)
{
qWarning() << "Cannot update builtin language" << key;
return;
}
auto found = findLanguage(key);
if(!found)
{
qWarning() << "Cannot update invalid language" << key;
return;
}
if(!found->updated)
{
downloadTranslation(key);
}
}
void TranslationsModel::downloadTranslation(QString key)
{
if(d->m_dl_job)
{
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(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();
}
void TranslationsModel::downloadNext()
{
if(!d->m_nextDownload.isEmpty())
{
downloadTranslation(d->m_nextDownload);
d->m_nextDownload.clear();
}
}
void TranslationsModel::dlFailed(QString reason)
{
qCritical() << "Translations Download Failed:" << reason;
d->m_dl_job.reset();
downloadNext();
}
void TranslationsModel::dlGood()
{
qDebug() << "Got translation:" << d->m_downloadingTranslation;
if(d->m_downloadingTranslation == d->m_selectedLanguage)
{
selectLanguage(d->m_selectedLanguage);
}
d->m_dl_job.reset();
downloadNext();
}
void TranslationsModel::indexFailed(QString reason)
{
qCritical() << "Translations Index Download Failed:" << reason;
d->m_index_job.reset();
}

View File

@@ -0,0 +1,64 @@
/* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QAbstractListModel>
#include <memory>
struct Language;
class TranslationsModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit TranslationsModel(QString path, QObject *parent = 0);
virtual ~TranslationsModel();
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);
QModelIndex selectedIndex();
QString selectedLanguage();
void downloadIndex();
private:
Language *findLanguage(const QString & key);
void reloadLocalFiles();
void downloadTranslation(QString key);
void downloadNext();
// hide copy constructor
TranslationsModel(const TranslationsModel &) = delete;
// hide assign op
TranslationsModel &operator=(const TranslationsModel &) = delete;
private slots:
void indexReceived();
void indexFailed(QString reason);
void dlFailed(QString reason);
void dlGood();
void translationDirChanged(const QString &path);
private: /* data */
struct Private;
std::unique_ptr<Private> d;
};