translation of all project, and new translation mechanism
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user