Compare commits

...

5 Commits
0.6.3 ... 0.6.4

Author SHA1 Message Date
Petr Mrázek
d6ace374d8 NOISSUE update changelog some more 2019-01-06 22:54:00 +01:00
Petr Mrázek
6a21c043ce NOISSUE bump version to 0.6.4 and update changelog 2019-01-06 22:28:27 +01:00
Petr Mrázek
4474d269cc NOISSUE granular model updates for language model 2019-01-06 22:14:13 +01:00
Petr Mrázek
ec2732ccd1 NOISSUE update FTB URLs 2019-01-04 01:48:36 +01:00
Petr Mrázek
4b7971f60f NOISSUE hotloading of translations and use of local PO files
The hotloading is still inefficient
2019-01-02 01:41:07 +01:00
12 changed files with 748 additions and 62 deletions

View File

@@ -46,7 +46,7 @@ set(MultiMC_NEWS_RSS_URL "https://multimc.org/rss.xml" CACHE STRING "URL to fetc
######## Set version numbers ########
set(MultiMC_VERSION_MAJOR 0)
set(MultiMC_VERSION_MINOR 6)
set(MultiMC_VERSION_HOTFIX 3)
set(MultiMC_VERSION_HOTFIX 4)
# Build number
set(MultiMC_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")

View File

@@ -387,6 +387,8 @@ add_unit_test(JavaVersion
set(TRANSLATIONS_SOURCES
translations/TranslationsModel.h
translations/TranslationsModel.cpp
translations/POTranslator.h
translations/POTranslator.cpp
)
set(TOOLS_SOURCES

View File

@@ -2,6 +2,8 @@
#include <QDomDocument>
#include "FtbPrivatePackManager.h"
#include "net/URLConstants.h"
void FtbPackFetchTask::fetch()
{
publicPacks.clear();
@@ -9,11 +11,11 @@ void FtbPackFetchTask::fetch()
NetJob *netJob = new NetJob("FtbModpackFetch");
QUrl publicPacksUrl = QUrl("https://ftb.cursecdn.com/FTB2/static/modpacks.xml");
QUrl publicPacksUrl = QUrl(URLConstants::FTB_CDN_BASE_URL + "static/modpacks.xml");
qDebug() << "Downloading public version info from" << publicPacksUrl.toString();
netJob->addNetAction(Net::Download::makeByteArray(publicPacksUrl, &publicModpacksXmlFileData));
QUrl thirdPartyUrl = QUrl("https://ftb.cursecdn.com/FTB2/static/thirdparty.xml");
QUrl thirdPartyUrl = QUrl(URLConstants::FTB_CDN_BASE_URL + "static/thirdparty.xml");
qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString();
netJob->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, &thirdPartyModpacksXmlFileData));
@@ -26,7 +28,7 @@ void FtbPackFetchTask::fetch()
void FtbPackFetchTask::fetchPrivate(const QStringList & toFetch)
{
QString privatePackBaseUrl = QString("https://ftb.cursecdn.com/FTB2/static/%1.xml");
QString privatePackBaseUrl = URLConstants::FTB_CDN_BASE_URL + "static/%1.xml";
for (auto &packCode: toFetch)
{

View File

@@ -9,6 +9,8 @@
#include "minecraft/ComponentList.h"
#include "minecraft/GradleSpecifier.h"
#include "net/URLConstants.h"
FtbPackInstallTask::FtbPackInstallTask(FtbModpack pack, QString version)
{
m_pack = pack;
@@ -32,11 +34,11 @@ void FtbPackInstallTask::downloadPack()
QString url;
if(m_pack.type == FtbPackType::Private)
{
url = QString("https://ftb.cursecdn.com/FTB2/privatepacks/%1").arg(packoffset);
url = QString(URLConstants::FTB_CDN_BASE_URL + "privatepacks/%1").arg(packoffset);
}
else
{
url = QString("https://ftb.cursecdn.com/FTB2/modpacks/%1").arg(packoffset);
url = QString(URLConstants::FTB_CDN_BASE_URL + "modpacks/%1").arg(packoffset);
}
job->addNetAction(Net::Download::makeCached(url, entry));
archivePath = entry->getFullPath();

View File

@@ -29,6 +29,7 @@ const QString IMGUR_BASE_URL("https://api.imgur.com/3/");
const QString FMLLIBS_OUR_BASE_URL("https://files.multimc.org/fmllibs/");
const QString FMLLIBS_FORGE_BASE_URL("https://files.minecraftforge.net/fmllibs/");
const QString TRANSLATIONS_BASE_URL("https://files.multimc.org/translations/");
const QString FTB_CDN_BASE_URL("https://ftb.forgecdn.net/FTB2/");
QString getJarPath(QString version);
QString getLegacyJarUrl(QString version);

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

@@ -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 "Json.h"
#include "POTranslator.h"
const static QLatin1Literal defaultLangCode("en");
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 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.fuzzy;
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,19 +124,186 @@ 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();
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::indexRecieved()
{
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 doc = Json::requireObject(Json::requireDocument(data));
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(), d->m_languages.size(), d->m_languages.size() + languages.size() - 1);
for(auto & language: languages)
{
d->m_languages.append(language);
}
endInsertRows();
}
QVariant TranslationsModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
@@ -153,18 +409,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 +485,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("https://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);
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 +520,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(URLConstants::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();
}

View File

@@ -40,7 +40,7 @@ public:
private:
Language *findLanguage(const QString & key);
void loadLocalIndex();
void reloadLocalFiles();
void downloadTranslation(QString key);
void downloadNext();
@@ -54,6 +54,8 @@ private slots:
void indexFailed(QString reason);
void dlFailed(QString reason);
void dlGood();
void translationDirChanged(const QString &path);
private: /* data */
struct Private;

View File

@@ -237,7 +237,8 @@ void MultiMCPage::applySettings()
auto s = MMC->settings();
// Language
s->set("Language", ui->languageBox->itemData(ui->languageBox->currentIndex()).toString());
auto langCode = ui->languageBox->itemData(ui->languageBox->currentIndex()).toString();
s->set("Language", langCode.isEmpty() ? "en" : langCode);
if (ui->resetNotificationsBtn->isChecked())
{

View File

@@ -10,6 +10,8 @@
#include <RWStorage.h>
#include <Env.h>
#include "net/URLConstants.h"
FtbFilterModel::FtbFilterModel(QObject *parent) : QSortFilterProxyModel(parent)
{
currentSorting = Sorting::ByGameVersion;
@@ -214,7 +216,7 @@ void FtbListModel::requestLogo(QString file)
MetaEntryPtr entry = ENV.metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file));
job->addNetAction(Net::Download::makeCached(QUrl(QString("https://ftb.cursecdn.com/FTB2/static/%1").arg(file)), entry));
job->addNetAction(Net::Download::makeCached(QUrl(QString(URLConstants::FTB_CDN_BASE_URL + "static/%1").arg(file)), entry));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::finished, this, [this, file, fullPath]

View File

@@ -1,4 +1,30 @@
# MultiMC 0.6.3
# MultiMC 0.6.4
Update for a better translation workflow, and new FTB API location.
## New or changed features
- FTB API location has changed
MultiMC now uses the new location and should keep working.
- Translations have been overhauled, again
It is now possible to put the translation source `.po` files into the `translations` folder and see changes in MultiMC immediately.
The new translation workflow is like this:
* Get a `.po` file from here the [translations repository](https://github.com/MultiMC/MultiMC5-translate).
* Alternatively, get the `template.pot` and start a new translation based on it.
* Put it in the `translations` folder.
* Edit it with [POEdit](https://poedit.net/).
* See the changes in real time.
* When done, post the changed files on discord, or github.
When using a `.po` file, MultiMC logs which strings are missing from the translation on the currently displayed UI screen(s), and which one are marked as fuzzy. This should make it easy to determine what's important.
# Previous releases
## MultiMC 0.6.3
This is a release mostly aimed at getting all the small changes and fixes out of the door.
@@ -71,8 +97,6 @@ This is a release mostly aimed at getting all the small changes and fixes out of
- GH-2467: Broken (and nonsensical) sorting indicators have been removed from the versions page header.
# Previous releases
## MultiMC 0.6.2
### New or changed features