#include "localization.h" #include "launcherpaths.h" #include #include #include #include #include #include #include #include #include #include #include // ── Про потоки ────────────────────────────────────────────────────────────── // // Каталог (m_singles, m_lists, m_codes, m_displayNames) заполняется ровно один // раз в load(), который отрабатывает в main() до того, как BuildSwitcher создаст // свой QThread. Дальше эти поля только читаются, а копирование QString из хэша // безопасно само по себе — поэтому Loc::text() зовётся из рабочего потока // BuildArchiveWorker без единой блокировки. // // Единственное, что меняется на ходу, — m_index, и он QAtomicInt. В худшем // случае сообщение, уже собиравшееся в момент переключения, уедет на прежнем // языке; заметить это невозможно. // // setLanguage(), m_tree, rebuildTree() и languageChanged — только GUI-поток. namespace { const QString kCatalogPath = QStringLiteral(":/qt/qml/Minecraft_launcher/i18n/translations.json"); const QString kSystem = QStringLiteral("system"); // Язык по системной локали. Промежуточных вариантов нет: языков в каталоге два. QString systemLanguage() { return QLocale::system().language() == QLocale::Russian ? QStringLiteral("ru") : QStringLiteral("en"); } // Язык из settings.json. Читается напрямую, не через LauncherBackend: тот // появляется только вместе с QML, а язык нужен раньше первой привязки. // Обратной зависимости это не создаёт — файл только читается. QString storedLanguage() { QFile file(LauncherPaths::settingsFile()); if (!file.open(QIODevice::ReadOnly)) return kSystem; const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); if (!doc.isObject()) return kSystem; const QJsonValue value = doc.object().value(QStringLiteral("language")); return value.isString() ? value.toString() : kSystem; } } // namespace Localization::Localization(QObject *parent) : QObject(parent) { } Localization &Localization::instance() { static Localization loc; return loc; } Localization *Localization::create(QQmlEngine *, QJSEngine *jsEngine) { Localization &loc = instance(); loc.m_engine = jsEngine; loc.rebuildTree(); // Синглтон живёт до конца процесса и движку не принадлежит — иначе сборщик // мусора QML попытается удалить статический объект. QJSEngine::setObjectOwnership(&loc, QJSEngine::CppOwnership); return &loc; } bool Localization::load() { QFile file(kCatalogPath); if (!file.open(QIODevice::ReadOnly)) { qCritical("Каталог переводов не найден: %s", qPrintable(kCatalogPath)); return false; } QJsonParseError error{}; const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); if (error.error != QJsonParseError::NoError) { qCritical("Каталог переводов испорчен: %s (смещение %d)", qPrintable(error.errorString()), error.offset); return false; } if (!doc.isObject()) { qCritical("Каталог переводов должен быть объектом JSON"); return false; } const QJsonObject root = doc.object(); const QJsonObject meta = root.value(QStringLiteral("_meta")).toObject(); const QJsonArray languages = meta.value(QStringLiteral("languages")).toArray(); m_codes.clear(); for (const QJsonValue &code : languages) m_codes << code.toString(); if (m_codes.isEmpty()) { qCritical("В каталоге переводов не перечислен ни один язык (_meta.languages)"); return false; } const QJsonObject names = meta.value(QStringLiteral("displayNames")).toObject(); m_displayNames.clear(); for (auto it = names.constBegin(); it != names.constEnd(); ++it) m_displayNames.insert(it.key(), it.value().toString()); const QJsonObject strings = root.value(QStringLiteral("strings")).toObject(); if (strings.isEmpty()) { qCritical("В каталоге переводов нет ни одной строки (strings)"); return false; } m_singles.clear(); m_lists.clear(); for (auto it = strings.constBegin(); it != strings.constEnd(); ++it) { const QJsonObject entry = it.value().toObject(); // Тип значения определяется по первому языку и обязан совпадать у всех: // за этим следит tools/check_translations.py. const bool isList = entry.value(m_codes.first()).isArray(); if (isList) { QList perLanguage; perLanguage.reserve(m_codes.size()); for (const QString &code : std::as_const(m_codes)) { QStringList values; const QJsonArray array = entry.value(code).toArray(); for (const QJsonValue &value : array) values << value.toString(); perLanguage << values; } m_lists.insert(it.key(), perLanguage); } else { QStringList perLanguage; perLanguage.reserve(m_codes.size()); for (const QString &code : std::as_const(m_codes)) perLanguage << entry.value(code).toString(); m_singles.insert(it.key(), perLanguage); } } m_language = storedLanguage(); if (m_language != kSystem && !m_codes.contains(m_language)) { qWarning("Неизвестный язык в настройках: %s — берём системный", qPrintable(m_language)); m_language = kSystem; } m_index.storeRelaxed(int(m_codes.indexOf(resolvedLanguage()))); #ifndef QT_NO_DEBUG for (auto it = m_singles.constBegin(); it != m_singles.constEnd(); ++it) { for (int i = 0; i < m_codes.size(); ++i) { if (it.value().value(i).isEmpty()) { qWarning("Пустой перевод: ключ %s, язык %s", qPrintable(it.key()), qPrintable(m_codes.at(i))); } } } #endif return true; } QString Localization::resolvedLanguage() const { return m_language == kSystem ? systemLanguage() : m_language; } void Localization::setLanguage(const QString &code) { Q_ASSERT(QThread::currentThread() == qApp->thread()); QString wanted = code; if (wanted != kSystem && !m_codes.contains(wanted)) { qWarning("Неизвестный язык: %s — берём системный", qPrintable(wanted)); wanted = kSystem; } if (wanted == m_language) return; m_language = wanted; m_index.storeRelaxed(int(m_codes.indexOf(resolvedLanguage()))); rebuildTree(); emit languageChanged(); } QVariantList Localization::availableLanguages() const { QVariantList out; out << QVariantMap{{QStringLiteral("code"), kSystem}, {QStringLiteral("name"), textFor("settings.language.system")}}; for (const QString &code : m_codes) { out << QVariantMap{{QStringLiteral("code"), code}, {QStringLiteral("name"), m_displayNames.value(code, code)}}; } return out; } QString Localization::textFor(const char *key) const { const QString id = QString::fromLatin1(key); const auto it = m_singles.constFind(id); if (it == m_singles.constEnd()) { #ifndef QT_NO_DEBUG qWarning("Localization: нет ключа %s", key); #endif return id; } return it.value().value(m_index.loadRelaxed()); } QStringList Localization::listFor(const char *key) const { const QString id = QString::fromLatin1(key); const auto it = m_lists.constFind(id); if (it == m_lists.constEnd()) { #ifndef QT_NO_DEBUG qWarning("Localization: нет списка %s", key); #endif return {id}; } return it.value().value(m_index.loadRelaxed()); } namespace { // Раскладывает плоский ключ "a.b.c" по вложенным картам. Рекурсия, а не спуск // указателем: QVariant не отдаёт ссылку на вложенную карту, её приходится // доставать копией и класть обратно. void insertPath(QVariantMap &node, const QStringList &parts, int depth, const QVariant &value) { if (depth == parts.size() - 1) { node[parts.at(depth)] = value; return; } QVariantMap child = node.value(parts.at(depth)).toMap(); insertPath(child, parts, depth + 1, value); node[parts.at(depth)] = child; } } // namespace // Дерево для QML: плоские ключи "a.b.c" разбираются в вложенные объекты, чтобы // в разметке писалось Loc.t.a.b.c. Строится один раз на смену языка. void Localization::rebuildTree() { if (!m_engine) return; const int index = m_index.loadRelaxed(); QVariantMap root; for (auto it = m_singles.constBegin(); it != m_singles.constEnd(); ++it) insertPath(root, it.key().split(QLatin1Char('.')), 0, it.value().value(index)); for (auto it = m_lists.constBegin(); it != m_lists.constEnd(); ++it) insertPath(root, it.key().split(QLatin1Char('.')), 0, it.value().value(index)); m_tree = m_engine->toScriptValue(root); } namespace Loc { QString text(const char *key) { return Localization::instance().textFor(key); } QStringList list(const char *key) { return Localization::instance().listFor(key); } } // namespace Loc