Files
minecraft-launcher/localization.cpp
T

292 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "localization.h"
#include "launcherpaths.h"
#include <QCoreApplication>
#include <QFile>
#include <QJSEngine>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QLocale>
#include <QThread>
#include <QVariantMap>
#include <utility>
// ── Про потоки ──────────────────────────────────────────────────────────────
//
// Каталог (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<QStringList> 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