diff options
Diffstat (limited to 'archived/projt-launcher/launcher/translations')
4 files changed, 1336 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/translations/POTranslator.cpp b/archived/projt-launcher/launcher/translations/POTranslator.cpp new file mode 100644 index 0000000000..6e893097d2 --- /dev/null +++ b/archived/projt-launcher/launcher/translations/POTranslator.cpp @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#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(); +} + +POTranslator::~POTranslator() +{ + delete d; +} + +QString POTranslator::translate(const char* context, + const char* sourceText, + const char* disambiguation, + [[maybe_unused]] int n) const +{ + if (disambiguation) + { + auto disambiguationKey = QByteArray(context) + "|" + QByteArray(sourceText) + "@" + QByteArray(disambiguation); + auto iter = d->mapping_disambiguatrion.find(disambiguationKey); + if (iter != d->mapping_disambiguatrion.end()) + { + auto& entry = *iter; + if (entry.text.isEmpty()) + { + qDebug() << "Translation entry has no content:" << disambiguationKey; + } + if (entry.fuzzy) + { + qDebug() << "Translation entry is fuzzy:" << disambiguationKey << "->" << entry.text; + } + return entry.text; + } + } + auto key = QByteArray(context) + "|" + QByteArray(sourceText); + auto iter = d->mapping.find(key); + if (iter != d->mapping.end()) + { + auto& entry = *iter; + if (entry.text.isEmpty()) + { + qDebug() << "Translation entry has no content:" << key; + } + if (entry.fuzzy) + { + qDebug() << "Translation entry is fuzzy:" << key << "->" << entry.text; + } + return entry.text; + } + return QString(); +} + +bool POTranslator::isEmpty() const +{ + return !d->loaded; +} diff --git a/archived/projt-launcher/launcher/translations/POTranslator.h b/archived/projt-launcher/launcher/translations/POTranslator.h new file mode 100644 index 0000000000..8dace908c6 --- /dev/null +++ b/archived/projt-launcher/launcher/translations/POTranslator.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include <QTranslator> + +struct POTranslatorPrivate; + +class POTranslator : public QTranslator +{ + Q_OBJECT + public: + explicit POTranslator(const QString& filename, QObject* parent = nullptr); + virtual ~POTranslator(); + QString translate(const char* context, const char* sourceText, const char* disambiguation, int n) const override; + bool isEmpty() const override; + + private: + POTranslatorPrivate* d; +}; diff --git a/archived/projt-launcher/launcher/translations/TranslationsModel.cpp b/archived/projt-launcher/launcher/translations/TranslationsModel.cpp new file mode 100644 index 0000000000..49d721bbe2 --- /dev/null +++ b/archived/projt-launcher/launcher/translations/TranslationsModel.cpp @@ -0,0 +1,846 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * 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. + * + * ======================================================================== */ + +#include "TranslationsModel.h" + +#include <QCoreApplication> +#include <QDebug> +#include <QDir> +#include <QLibraryInfo> +#include <QLocale> +#include <QMutex> +#include <QTranslator> +#include <locale> + +#include "BuildConfig.h" +#include "FileSystem.h" +#include "Json.h" +#include "net/ChecksumValidator.h" +#include "net/NetJob.h" + +#include "POTranslator.h" + +#include "Application.h" + +const static QLatin1String 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); + } + + QString languageName() const + { + QString result; + if (key == "ja_KANJI") + { + result = locale.nativeLanguageName() + u8" (漢字)"; + } + else if (key == "es_UY") + { + result = u8"Español de Latinoamérica"; + } + 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 + } + else if (key == "en@pirate") + { + result = u8"Tongue of the High Seas"; + } + else if (key == "en@uwu") + { + result = u8"Cute Engwish"; + } + 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? + } + else + { + const auto base_lang = key.section('_', 0, 0); + const QLocale base_locale(base_lang); + result = QLocale::languageToString(base_locale.language()); + if (result == "C") + { + static const QMap<QString, QString> k_languageOverrides = { + { "av", "Avaric" }, { "tay", "Atayal" }, { "tlh", "Klingon" }, + { "tw", "Twi" }, { "ty", "Tahitian" }, { "tzl", "Talossan" }, + { "val", "Valencian" }, { "vls", "Flemish" }, { "zea", "Zeelandic" }, + }; + auto it = k_languageOverrides.find(base_lang); + if (it != k_languageOverrides.end()) + { + result = it.value(); + } + } + if (result.isEmpty() || result == "C") + { + result = base_lang.isEmpty() ? key : base_lang; + } + } + + if (key.contains('_') && !key.contains('@')) + { + auto region = key.section('_', 1); + if (!region.isEmpty()) + { + QLocale::Territory territory = QLocale::codeToTerritory(QStringView{ region }); + auto territory_name = QLocale::territoryToString(territory); + if (!territory_name.isEmpty() && territory != QLocale::AnyTerritory) + { + result += " (" + territory_name + ")"; + } + else + { + result += " (" + region + ")"; + } + } + } + else if (result.isEmpty()) + { + result = key; + } + + return result; + } + + 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 + QList<Language> m_languages = { Language(defaultLangCode) }; + + QString m_selectedLanguage = defaultLangCode; + std::unique_ptr<QTranslator> m_qt_translator; + std::unique_ptr<QTranslator> m_app_translator; + + Net::Download* m_index_task; + QString m_downloadingTranslation; + NetJob::Ptr m_dl_job; + NetJob::Ptr m_index_job; + QString m_nextDownload; + + std::unique_ptr<POTranslator> m_po_translator; + QFileSystemWatcher* watcher; + + const QString m_system_locale = QLocale::system().name(); + const QString m_system_language = m_system_locale.split('_').front(); + + bool no_language_set = false; +}; + +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; + if (!d->no_language_set) + { + reloadLocalFiles(); + } + selectLanguage(selectedLanguage()); +} + +void TranslationsModel::indexReceived() +{ + qDebug() << "Got translations index!"; + d->m_index_job.reset(); + + if (d->no_language_set) + { + reloadLocalFiles(); + + auto language = d->m_system_locale; + if (!findLanguageAsOptional(language).has_value()) + { + language = d->m_system_language; + } + selectLanguage(language); + if (selectedLanguage() != defaultLangCode) + { + updateLanguage(selectedLanguage()); + } + APPLICATION->settings()->set("Language", selectedLanguage()); + d->no_language_set = false; + } + + else 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 ([[maybe_unused]] const Exception& e) + { + qCritical() << "Translations Download Failed: index file not readable"; + return; + } + + 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); + } + } + catch ([[maybe_unused]] Json::JsonException& e) + { + qCritical() << "Translations Download Failed: index file could not be parsed as json"; + } + } +} // namespace + +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; + } + // New entries are sorted into the full list immediately, so a model reset is + // safer than advertising a contiguous append range that no longer exists + // after sorting. + beginResetModel(); + for (auto& language : languages) + { + d->m_languages.append(language); + } + 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; + } + } + return a.languageName().toLower() < b.languageName().toLower(); + }); + endResetModel(); +} + +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.languageName(); + } + case Column::Completeness: + { + return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1); + } + } + qWarning("TranslationModel::data not implemented when role is DisplayRole"); + } + 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([[maybe_unused]] const QModelIndex& parent) const +{ + return d->m_languages.size(); +} + +int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) const +{ + return 2; +} + +QList<Language>::Iterator TranslationsModel::findLanguage(const QString& key) +{ + return std::find_if(d->m_languages.begin(), + d->m_languages.end(), + [key](Language& lang) { return lang.key == key; }); +} + +std::optional<Language> TranslationsModel::findLanguageAsOptional(const QString& key) +{ + auto found = findLanguage(key); + if (found != d->m_languages.end()) + return *found; + return {}; +} + +void TranslationsModel::setUseSystemLocale(bool useSystemLocale) +{ + APPLICATION->settings()->set("UseSystemLocale", useSystemLocale); + QLocale::setDefault(QLocale(useSystemLocale ? QString::fromStdString(std::locale().name()) : defaultLangCode)); +} + +bool TranslationsModel::selectLanguage(QString key) +{ + QString& langCode = key; + auto langPtr = findLanguageAsOptional(key); + + if (langCode.isEmpty()) + { + d->no_language_set = true; + } + + if (!langPtr.has_value()) + { + 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(); + } + + // Protect locale setting with a mutex to prevent thread-safety issues + // QLocale::setDefault is not reentrant, so we serialize access + static QMutex localeMutex; + QMutexLocker locker(&localeMutex); + QLocale::setDefault(QLocale(APPLICATION->settings()->get("UseSystemLocale").toBool() + ? QString::fromStdString(std::locale().name()) + : langCode)); + + // Update the layout direction for RTL languages like Arabic + qApp->setLayoutDirection(QLocale(langCode).textDirection()); + + // if it's the default UI language, finish + if (langCode == defaultLangCode) + { + d->m_selectedLanguage = langCode; + return true; + } + + // otherwise install new translations + bool successful = false; + // Note: Qt translations may not be present in all installations (e.g., custom builds). + // This is a best-effort attempt; the app continues working without Qt translations. + d->m_qt_translator.reset(new QTranslator()); + if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::path(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 != d->m_languages.end()) + { + return index(std::distance(d->m_languages.begin(), found), 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", APPLICATION->network())); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json"); + entry->setStale(true); + auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + "index_v2.json"), entry); + d->m_index_task = task.get(); + d->m_index_job->addNetAction(task); + d->m_index_job->setAskRetry(false); + 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 = findLanguageAsOptional(key); + if (!found.has_value()) + { + 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 = findLanguageAsOptional(key); + if (!lang.has_value()) + { + qWarning() << "Will not download an unknown translation" << key; + return; + } + + d->m_downloadingTranslation = key; + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm"); + entry->setStale(true); + + auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + lang->file_name), entry); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, lang->file_sha1)); + dl->setProgress(dl->getProgress(), lang->file_size); + + d->m_dl_job.reset(new NetJob("Translation for " + key, APPLICATION->network())); + d->m_dl_job->addNetAction(dl); + d->m_dl_job->setAskRetry(false); + + 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(); +} diff --git a/archived/projt-launcher/launcher/translations/TranslationsModel.h b/archived/projt-launcher/launcher/translations/TranslationsModel.h new file mode 100644 index 0000000000..b28e35d468 --- /dev/null +++ b/archived/projt-launcher/launcher/translations/TranslationsModel.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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> +#include <optional> + +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(); + void setUseSystemLocale(bool useSystemLocale); + + private: + QList<Language>::Iterator findLanguage(const QString& key); + std::optional<Language> findLanguageAsOptional(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; +}; |
