2022-05-02 18:33:21 +01:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
/*
|
|
|
|
* PolyMC - Minecraft Launcher
|
|
|
|
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
|
2022-05-26 22:18:54 +01:00
|
|
|
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
2022-05-02 18:33:21 +01:00
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, version 3.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
* This file incorporates work covered by the following copyright and
|
|
|
|
* permission notice:
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
#include "TranslationsModel.h"
|
|
|
|
|
|
|
|
#include <QCoreApplication>
|
|
|
|
#include <QTranslator>
|
|
|
|
#include <QLocale>
|
|
|
|
#include <QDir>
|
|
|
|
#include <QLibraryInfo>
|
|
|
|
#include <QDebug>
|
2021-11-21 22:21:12 +00:00
|
|
|
|
|
|
|
#include "FileSystem.h"
|
|
|
|
#include "net/NetJob.h"
|
|
|
|
#include "net/ChecksumValidator.h"
|
|
|
|
#include "BuildConfig.h"
|
2019-01-02 00:41:07 +00:00
|
|
|
#include "Json.h"
|
|
|
|
|
|
|
|
#include "POTranslator.h"
|
2016-12-05 01:29:08 +00:00
|
|
|
|
2021-11-21 22:21:12 +00:00
|
|
|
#include "Application.h"
|
|
|
|
|
2022-05-02 18:10:45 +01:00
|
|
|
const static QLatin1String defaultLangCode("en_US");
|
2021-04-08 18:05:44 +01:00
|
|
|
|
2019-01-02 00:41:07 +00:00
|
|
|
enum class FileType
|
|
|
|
{
|
|
|
|
NONE,
|
|
|
|
QM,
|
|
|
|
PO
|
|
|
|
};
|
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
struct Language
|
|
|
|
{
|
2019-01-02 00:41:07 +00:00
|
|
|
Language()
|
|
|
|
{
|
|
|
|
updated = true;
|
|
|
|
}
|
|
|
|
Language(const QString & _key)
|
|
|
|
{
|
|
|
|
key = _key;
|
2021-04-13 13:53:21 +01:00
|
|
|
locale = QLocale(key);
|
2019-01-02 00:41:07 +00:00
|
|
|
updated = (key == defaultLangCode);
|
|
|
|
}
|
|
|
|
|
2021-12-08 00:23:12 +00:00
|
|
|
QString languageName() const {
|
|
|
|
QString result;
|
|
|
|
if(key == "ja_KANJI") {
|
|
|
|
result = locale.nativeLanguageName() + u8" (漢字)";
|
2021-12-11 12:06:14 +00:00
|
|
|
}
|
|
|
|
else if(key == "es_UY") {
|
|
|
|
result = u8"español de Latinoamérica";
|
2021-12-08 00:23:12 +00:00
|
|
|
}
|
2022-11-19 16:28:27 +00:00
|
|
|
else if(key == "en_NZ") {
|
|
|
|
result = u8"New Zealand English"; // No idea why qt translates this to just english and not to New Zealand English
|
|
|
|
}
|
2022-10-30 22:50:56 +00:00
|
|
|
else if(key == "en@pirate") {
|
|
|
|
result = u8"Tongue of the High Seas";
|
|
|
|
}
|
|
|
|
else if(key == "en@uwu") {
|
|
|
|
result = u8"Cute Engwish";
|
|
|
|
}
|
2022-11-18 21:21:55 +00:00
|
|
|
else if(key == "tok") {
|
|
|
|
result = u8"toki pona";
|
|
|
|
}
|
|
|
|
else if(key == "nan") {
|
|
|
|
result = u8"閩南語"; // Using traditional Chinese script. Not sure if we should use simplified instead?
|
|
|
|
}
|
2021-12-08 00:23:12 +00:00
|
|
|
else {
|
|
|
|
result = locale.nativeLanguageName();
|
|
|
|
}
|
2022-09-24 10:43:54 +01:00
|
|
|
|
|
|
|
if (result.isEmpty()) {
|
|
|
|
result = key;
|
|
|
|
}
|
2021-12-08 00:23:12 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2019-01-02 00:41:07 +00:00
|
|
|
float percentTranslated() const
|
|
|
|
{
|
|
|
|
if (total == 0)
|
|
|
|
{
|
|
|
|
return 100.0f;
|
|
|
|
}
|
2019-01-08 01:20:36 +00:00
|
|
|
return 100.0f * float(translated) / float(total);
|
2019-01-02 00:41:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void setTranslationStats(unsigned _translated, unsigned _untranslated, unsigned _fuzzy)
|
|
|
|
{
|
|
|
|
translated = _translated;
|
|
|
|
untranslated = _untranslated;
|
|
|
|
fuzzy = _fuzzy;
|
|
|
|
total = translated + untranslated + fuzzy;
|
|
|
|
}
|
|
|
|
|
2019-01-06 21:14:13 +00:00
|
|
|
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;
|
2019-01-08 01:20:36 +00:00
|
|
|
total = other.total;
|
2019-01-06 21:14:13 +00:00
|
|
|
localFileType = other.localFileType;
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
QString key;
|
|
|
|
QLocale locale;
|
|
|
|
bool updated;
|
2019-01-02 00:41:07 +00:00
|
|
|
|
|
|
|
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;
|
2016-12-05 01:29:08 +00:00
|
|
|
};
|
|
|
|
|
2019-01-06 21:14:13 +00:00
|
|
|
|
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
struct TranslationsModel::Private
|
|
|
|
{
|
|
|
|
QDir m_dir;
|
2018-07-15 13:51:05 +01:00
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
// initial state is just english
|
2019-01-02 00:41:07 +00:00
|
|
|
QVector<Language> m_languages = {Language (defaultLangCode)};
|
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
QString m_selectedLanguage = defaultLangCode;
|
|
|
|
std::unique_ptr<QTranslator> m_qt_translator;
|
|
|
|
std::unique_ptr<QTranslator> m_app_translator;
|
2018-07-15 13:51:05 +01:00
|
|
|
|
2023-06-05 09:12:16 +01:00
|
|
|
Net::Download* m_index_task;
|
2016-12-05 01:29:08 +00:00
|
|
|
QString m_downloadingTranslation;
|
2021-11-21 22:21:12 +00:00
|
|
|
NetJob::Ptr m_dl_job;
|
|
|
|
NetJob::Ptr m_index_job;
|
2016-12-05 01:29:08 +00:00
|
|
|
QString m_nextDownload;
|
2019-01-02 00:41:07 +00:00
|
|
|
|
|
|
|
std::unique_ptr<POTranslator> m_po_translator;
|
|
|
|
QFileSystemWatcher *watcher;
|
2022-01-12 16:44:58 +00:00
|
|
|
|
2022-01-13 09:20:53 +00:00
|
|
|
const QString m_system_locale = QLocale::system().name();
|
|
|
|
const QString m_system_language = m_system_locale.split('_').front();
|
|
|
|
|
2022-01-12 16:44:58 +00:00
|
|
|
bool no_language_set = false;
|
2016-12-05 01:29:08 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
TranslationsModel::TranslationsModel(QString path, QObject* parent): QAbstractListModel(parent)
|
|
|
|
{
|
|
|
|
d.reset(new Private);
|
|
|
|
d->m_dir.setPath(path);
|
2019-03-08 00:21:04 +00:00
|
|
|
FS::ensureFolderPathExists(path);
|
2019-01-02 00:41:07 +00:00
|
|
|
reloadLocalFiles();
|
|
|
|
|
|
|
|
d->watcher = new QFileSystemWatcher(this);
|
|
|
|
connect(d->watcher, &QFileSystemWatcher::directoryChanged, this, &TranslationsModel::translationDirChanged);
|
|
|
|
d->watcher->addPath(d->m_dir.canonicalPath());
|
2016-12-05 01:29:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
TranslationsModel::~TranslationsModel()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2019-01-02 00:41:07 +00:00
|
|
|
void TranslationsModel::translationDirChanged(const QString& path)
|
|
|
|
{
|
|
|
|
qDebug() << "Dir changed:" << path;
|
2022-01-17 08:49:47 +00:00
|
|
|
if (!d->no_language_set)
|
|
|
|
{
|
|
|
|
reloadLocalFiles();
|
|
|
|
}
|
|
|
|
selectLanguage(selectedLanguage());
|
|
|
|
}
|
|
|
|
|
|
|
|
void TranslationsModel::indexReceived()
|
|
|
|
{
|
|
|
|
qDebug() << "Got translations index!";
|
|
|
|
d->m_index_job.reset();
|
2022-01-12 16:44:58 +00:00
|
|
|
|
|
|
|
if (d->no_language_set)
|
|
|
|
{
|
2022-01-17 08:49:47 +00:00
|
|
|
reloadLocalFiles();
|
|
|
|
|
2022-01-13 09:20:53 +00:00
|
|
|
auto language = d->m_system_locale;
|
|
|
|
if (!findLanguage(language))
|
2022-01-12 16:44:58 +00:00
|
|
|
{
|
2022-01-13 09:20:53 +00:00
|
|
|
language = d->m_system_language;
|
2022-01-12 16:44:58 +00:00
|
|
|
}
|
2022-01-13 09:20:53 +00:00
|
|
|
selectLanguage(language);
|
2022-01-12 16:44:58 +00:00
|
|
|
if (selectedLanguage() != defaultLangCode)
|
|
|
|
{
|
|
|
|
updateLanguage(selectedLanguage());
|
|
|
|
}
|
2022-01-13 08:41:51 +00:00
|
|
|
APPLICATION->settings()->set("Language", selectedLanguage());
|
2022-01-12 16:44:58 +00:00
|
|
|
d->no_language_set = false;
|
|
|
|
}
|
2019-01-02 00:41:07 +00:00
|
|
|
|
2022-01-17 08:49:47 +00:00
|
|
|
else if(d->m_selectedLanguage != defaultLangCode)
|
2019-01-02 00:41:07 +00:00
|
|
|
{
|
|
|
|
downloadTranslation(d->m_selectedLanguage);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
namespace {
|
2019-01-06 21:14:13 +00:00
|
|
|
void readIndex(const QString & path, QMap<QString, Language>& languages)
|
2019-01-02 00:41:07 +00:00
|
|
|
{
|
|
|
|
QByteArray data;
|
|
|
|
try
|
|
|
|
{
|
|
|
|
data = FS::read(path);
|
|
|
|
}
|
|
|
|
catch (const Exception &e)
|
|
|
|
{
|
|
|
|
qCritical() << "Translations Download Failed: index file not readable";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
int index = 1;
|
|
|
|
try
|
|
|
|
{
|
2020-06-28 12:25:53 +01:00
|
|
|
auto toplevel_doc = Json::requireDocument(data);
|
|
|
|
auto doc = Json::requireObject(toplevel_doc);
|
2019-01-02 00:41:07 +00:00
|
|
|
auto file_type = Json::requireString(doc, "file_type");
|
2021-10-21 22:27:01 +01:00
|
|
|
if(file_type != "MMC-TRANSLATION-INDEX")
|
2019-01-02 00:41:07 +00:00
|
|
|
{
|
|
|
|
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");
|
|
|
|
|
2019-01-06 21:14:13 +00:00
|
|
|
languages.insert(lang.key, lang);
|
2019-01-02 00:41:07 +00:00
|
|
|
index++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (Json::JsonException & e)
|
|
|
|
{
|
|
|
|
qCritical() << "Translations Download Failed: index file could not be parsed as json";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void TranslationsModel::reloadLocalFiles()
|
|
|
|
{
|
2019-01-06 21:14:13 +00:00
|
|
|
QMap<QString, Language> languages = {{defaultLangCode, Language(defaultLangCode)}};
|
2019-01-02 00:41:07 +00:00
|
|
|
|
2019-01-06 21:14:13 +00:00
|
|
|
readIndex(d->m_dir.absoluteFilePath("index_v2.json"), languages);
|
2019-01-02 00:41:07 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-01-06 21:14:13 +00:00
|
|
|
auto langIter = languages.find(langCode);
|
|
|
|
if(langIter != languages.end())
|
2019-01-02 00:41:07 +00:00
|
|
|
{
|
2019-01-06 21:14:13 +00:00
|
|
|
auto & language = *langIter;
|
2019-01-02 00:41:07 +00:00
|
|
|
if(int(fileType) > int(language.localFileType))
|
|
|
|
{
|
|
|
|
language.localFileType = fileType;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if(fileType == FileType::PO)
|
|
|
|
{
|
|
|
|
Language localFound(langCode);
|
|
|
|
localFound.localFileType = FileType::PO;
|
2019-01-06 21:14:13 +00:00
|
|
|
languages.insert(langCode, localFound);
|
2019-01-02 00:41:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-06 21:14:13 +00:00
|
|
|
// 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;
|
|
|
|
}
|
2021-04-13 13:53:21 +01:00
|
|
|
beginInsertRows(QModelIndex(), 0, d->m_languages.size() + languages.size() - 1);
|
2019-01-06 21:14:13 +00:00
|
|
|
for(auto & language: languages)
|
|
|
|
{
|
|
|
|
d->m_languages.append(language);
|
|
|
|
}
|
2022-01-13 09:20:53 +00:00
|
|
|
std::sort(d->m_languages.begin(), d->m_languages.end(), [this](const Language& a, const Language& b) {
|
|
|
|
if (a.key != b.key)
|
|
|
|
{
|
|
|
|
if (a.key == d->m_system_locale || a.key == d->m_system_language)
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (b.key == d->m_system_locale || b.key == d->m_system_language)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2022-09-24 10:48:14 +01:00
|
|
|
return a.languageName().toLower() < b.languageName().toLower();
|
2021-04-13 13:53:21 +01:00
|
|
|
});
|
2019-01-06 21:14:13 +00:00
|
|
|
endInsertRows();
|
2019-01-02 00:41:07 +00:00
|
|
|
}
|
|
|
|
|
2019-01-08 01:20:36 +00:00
|
|
|
namespace {
|
|
|
|
enum class Column
|
|
|
|
{
|
|
|
|
Language,
|
2019-01-14 00:36:04 +00:00
|
|
|
Completeness
|
2019-01-08 01:20:36 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
QVariant TranslationsModel::data(const QModelIndex& index, int role) const
|
|
|
|
{
|
|
|
|
if (!index.isValid())
|
|
|
|
return QVariant();
|
2018-07-15 13:51:05 +01:00
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
int row = index.row();
|
2019-01-08 01:20:36 +00:00
|
|
|
auto column = static_cast<Column>(index.column());
|
2018-07-15 13:51:05 +01:00
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
if (row < 0 || row >= d->m_languages.size())
|
|
|
|
return QVariant();
|
2018-07-15 13:51:05 +01:00
|
|
|
|
2019-01-08 01:20:36 +00:00
|
|
|
auto & lang = d->m_languages[row];
|
2016-12-05 01:29:08 +00:00
|
|
|
switch (role)
|
|
|
|
{
|
|
|
|
case Qt::DisplayRole:
|
2019-01-08 01:20:36 +00:00
|
|
|
{
|
|
|
|
switch(column)
|
|
|
|
{
|
|
|
|
case Column::Language:
|
|
|
|
{
|
2021-12-08 00:23:12 +00:00
|
|
|
return lang.languageName();
|
2019-01-08 01:20:36 +00:00
|
|
|
}
|
2019-01-14 00:36:04 +00:00
|
|
|
case Column::Completeness:
|
2019-01-08 01:20:36 +00:00
|
|
|
{
|
2022-05-02 18:10:45 +01:00
|
|
|
return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1);
|
2019-01-08 01:20:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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));
|
|
|
|
}
|
2016-12-05 01:29:08 +00:00
|
|
|
case Qt::UserRole:
|
2021-04-13 13:53:21 +01:00
|
|
|
return lang.key;
|
2016-12-05 01:29:08 +00:00
|
|
|
default:
|
|
|
|
return QVariant();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-08 01:20:36 +00:00
|
|
|
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");
|
|
|
|
}
|
2019-01-14 00:36:04 +00:00
|
|
|
case Column::Completeness:
|
2019-01-08 01:20:36 +00:00
|
|
|
{
|
2019-01-14 00:36:04 +00:00
|
|
|
return tr("Completeness");
|
2019-01-08 01:20:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if(role == Qt::ToolTipRole)
|
|
|
|
{
|
|
|
|
switch(column)
|
|
|
|
{
|
|
|
|
case Column::Language:
|
|
|
|
{
|
|
|
|
return tr("The native language name.");
|
|
|
|
}
|
2019-01-14 00:36:04 +00:00
|
|
|
case Column::Completeness:
|
2019-01-08 01:20:36 +00:00
|
|
|
{
|
2019-01-14 00:36:04 +00:00
|
|
|
return tr("Completeness is the percentage of fully translated strings, not counting automatically guessed ones.");
|
2019-01-08 01:20:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return QAbstractListModel::headerData(section, orientation, role);
|
|
|
|
}
|
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
int TranslationsModel::rowCount(const QModelIndex& parent) const
|
|
|
|
{
|
|
|
|
return d->m_languages.size();
|
|
|
|
}
|
|
|
|
|
2019-01-08 01:20:36 +00:00
|
|
|
int TranslationsModel::columnCount(const QModelIndex& parent) const
|
|
|
|
{
|
|
|
|
return 2;
|
|
|
|
}
|
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
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);
|
2022-01-12 16:44:58 +00:00
|
|
|
|
2022-01-13 07:29:50 +00:00
|
|
|
if (langCode.isEmpty())
|
2022-01-12 16:44:58 +00:00
|
|
|
{
|
|
|
|
d->no_language_set = true;
|
|
|
|
}
|
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
if(!langPtr)
|
|
|
|
{
|
|
|
|
qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode;
|
|
|
|
langCode = defaultLangCode;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
langCode = langPtr->key;
|
|
|
|
}
|
2018-07-15 13:51:05 +01:00
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
// 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();
|
|
|
|
}
|
2018-07-15 13:51:05 +01:00
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
/*
|
|
|
|
* 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.
|
|
|
|
*/
|
2021-04-13 13:53:21 +01:00
|
|
|
QLocale locale = QLocale(langCode);
|
2016-12-05 01:29:08 +00:00
|
|
|
QLocale::setDefault(locale);
|
2018-07-15 13:51:05 +01:00
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
// if it's the default UI language, finish
|
|
|
|
if(langCode == defaultLangCode)
|
|
|
|
{
|
|
|
|
d->m_selectedLanguage = langCode;
|
|
|
|
return true;
|
|
|
|
}
|
2018-07-15 13:51:05 +01:00
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
// 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();
|
|
|
|
}
|
2018-07-15 13:51:05 +01:00
|
|
|
|
2019-01-02 00:41:07 +00:00
|
|
|
if(langPtr->localFileType == FileType::PO)
|
2016-12-05 01:29:08 +00:00
|
|
|
{
|
|
|
|
qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "...";
|
2019-01-02 00:41:07 +00:00
|
|
|
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
|
2016-12-05 01:29:08 +00:00
|
|
|
{
|
|
|
|
qCritical() << "Loading Application Language File failed.";
|
|
|
|
d->m_app_translator.reset();
|
|
|
|
}
|
2019-01-02 00:41:07 +00:00
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2016-12-05 01:29:08 +00:00
|
|
|
else
|
|
|
|
{
|
2019-01-02 00:41:07 +00:00
|
|
|
d->m_app_translator.reset();
|
2016-12-05 01:29:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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()
|
|
|
|
{
|
2017-01-05 03:05:08 +00:00
|
|
|
if(d->m_index_job || d->m_dl_job)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
2016-12-05 01:29:08 +00:00
|
|
|
qDebug() << "Downloading Translations Index...";
|
2023-01-24 19:52:09 +00:00
|
|
|
d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network()));
|
2021-11-21 22:21:12 +00:00
|
|
|
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json");
|
2019-01-02 00:41:07 +00:00
|
|
|
entry->setStale(true);
|
2023-06-05 09:12:16 +01:00
|
|
|
auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry);
|
|
|
|
d->m_index_task = task.get();
|
|
|
|
d->m_index_job->addNetAction(task);
|
2016-12-05 01:29:08 +00:00
|
|
|
connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed);
|
2019-03-08 00:21:04 +00:00
|
|
|
connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived);
|
2021-12-31 04:27:59 +00:00
|
|
|
d->m_index_job->start();
|
2016-12-05 01:29:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2019-01-02 00:41:07 +00:00
|
|
|
auto lang = findLanguage(key);
|
|
|
|
if(!lang)
|
|
|
|
{
|
|
|
|
qWarning() << "Will not download an unknown translation" << key;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
d->m_downloadingTranslation = key;
|
2021-11-21 22:21:12 +00:00
|
|
|
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm");
|
2016-12-05 01:29:08 +00:00
|
|
|
entry->setStale(true);
|
2019-01-02 00:41:07 +00:00
|
|
|
|
2020-07-18 15:18:02 +01:00
|
|
|
auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + lang->file_name), entry);
|
2019-01-02 00:41:07 +00:00
|
|
|
auto rawHash = QByteArray::fromHex(lang->file_sha1.toLatin1());
|
|
|
|
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash));
|
2022-04-22 02:12:14 +01:00
|
|
|
dl->setProgress(dl->getProgress(), lang->file_size);
|
2019-01-02 00:41:07 +00:00
|
|
|
|
2023-01-24 19:52:09 +00:00
|
|
|
d->m_dl_job.reset(new NetJob("Translation for " + key, APPLICATION->network()));
|
2019-01-02 00:41:07 +00:00
|
|
|
d->m_dl_job->addNetAction(dl);
|
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood);
|
|
|
|
connect(d->m_dl_job.get(), &NetJob::failed, this, &TranslationsModel::dlFailed);
|
2019-01-02 00:41:07 +00:00
|
|
|
|
2021-12-31 04:27:59 +00:00
|
|
|
d->m_dl_job->start();
|
2016-12-05 01:29:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2018-07-15 13:51:05 +01:00
|
|
|
|
2016-12-05 01:29:08 +00:00
|
|
|
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();
|
|
|
|
}
|