Files

292 lines
10 KiB
C++
Raw Permalink Normal View History

#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