diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:51:45 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:51:45 +0300 |
| commit | d3261e64152397db2dca4d691a990c6bc2a6f4dd (patch) | |
| tree | fac2f7be638651181a72453d714f0f96675c2b8b /archived/projt-launcher/launcher/translations/TranslationsModel.cpp | |
| parent | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (diff) | |
| download | Project-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.tar.gz Project-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.zip | |
NOISSUE add archived projects
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
Diffstat (limited to 'archived/projt-launcher/launcher/translations/TranslationsModel.cpp')
| -rw-r--r-- | archived/projt-launcher/launcher/translations/TranslationsModel.cpp | 846 |
1 files changed, 846 insertions, 0 deletions
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(); +} |
