mirror of
https://github.com/UltimMC/Launcher.git
synced 2025-10-04 17:09:25 +00:00
Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d6ace374d8 | ||
|
6a21c043ce | ||
|
4474d269cc | ||
|
ec2732ccd1 | ||
|
4b7971f60f |
@@ -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.")
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
|
373
api/logic/translations/POTranslator.cpp
Normal file
373
api/logic/translations/POTranslator.cpp
Normal 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;
|
||||
}
|
16
api/logic/translations/POTranslator.h
Normal file
16
api/logic/translations/POTranslator.h
Normal 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;
|
||||
};
|
@@ -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();
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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())
|
||||
{
|
||||
|
@@ -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]
|
||||
|
30
changelog.md
30
changelog.md
@@ -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
|
||||
|
Reference in New Issue
Block a user