Files

109 lines
5.5 KiB
C++
Raw Permalink Normal View History

#pragma once
#include <QAtomicInt>
#include <QHash>
#include <QJSValue>
#include <QList>
#include <QObject>
#include <QString>
#include <QStringList>
#include <QVariantList>
#include <QtQml/qqmlregistration.h>
class QQmlEngine;
class QJSEngine;
// Единственный источник всех текстов интерфейса. Каталог — i18n/translations.json,
// он лежит в ресурсах и правится руками; штатных .ts/.qm в проекте нет намеренно.
//
// Из QML читается свойством: text: Loc.t.settings.title
// Из C++ — свободной функцией: Loc::text("settings.title")
//
// Почему свойство, а не Q_INVOKABLE: вызов метода не регистрирует зависимость
// привязки, и text: Loc.t("ключ") никогда бы не обновился при смене языка.
// Чтение же Q_PROPERTY с сигналом NOTIFY зависимость регистрирует, поэтому
// languageChanged перевычисляет все привязки разом. Дальнейшие .settings.title —
// обычные обращения к членам JS-объекта, и отслеживать их не нужно: при смене
// языка дерево заменяется целиком.
//
// Тип свойства — QJSValue, а не QVariantMap: QVariantMap пересобирался бы в новый
// JS-объект при каждом чтении, а привязок в проекте полторы сотни. QJSValue
// строится один раз на смену языка.
class Localization : public QObject
{
Q_OBJECT
QML_NAMED_ELEMENT(Loc)
QML_SINGLETON
Q_PROPERTY(QJSValue t READ t NOTIFY languageChanged)
// Что выбрано пользователем: "system", "ru" или "en".
Q_PROPERTY(QString language READ language NOTIFY languageChanged)
// Во что это разрешилось: всегда "ru" или "en", никогда "system".
Q_PROPERTY(QString resolvedLanguage READ resolvedLanguage NOTIFY languageChanged)
public:
static Localization &instance();
// Фабрика синглтона для движка QML. Объект к этому моменту уже загружен
// из main(), здесь только запоминается движок и строится дерево.
static Localization *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine);
// Читает каталог из ресурсов и язык из settings.json. Вызывается ровно один
// раз из main() до создания QQmlApplicationEngine. false — каталога нет или
// он битый; это ошибка сборки, и запускаться дальше незачем.
bool load();
QJSValue t() const { return m_tree; }
QString language() const { return m_language; }
QString resolvedLanguage() const;
// Только из GUI-потока: меняет дерево и рассылает languageChanged.
void setLanguage(const QString &code);
// Для комбобокса в настройках: [{ "code": "ru", "name": "Русский" }, …],
// первым пунктом — "system".
Q_INVOKABLE QVariantList availableLanguages() const;
// Рабочие лошадки Loc::text/Loc::list. Потокобезопасны: см. комментарий к
// полям ниже.
[[nodiscard]] QString textFor(const char *key) const;
[[nodiscard]] QStringList listFor(const char *key) const;
signals:
void languageChanged();
private:
explicit Localization(QObject *parent = nullptr);
void rebuildTree();
// Каталог заполняется один раз в load() и дальше только читается — поэтому
// обращаться к нему можно из любого потока без блокировок. Единственное, что
// меняется на ходу, — m_index, и он атомарный.
QHash<QString, QStringList> m_singles; // ключ -> значения по языкам
QHash<QString, QList<QStringList>> m_lists; // ключ -> списки по языкам
QStringList m_codes; // ["ru", "en"] — порядок из _meta
QHash<QString, QString> m_displayNames;
QAtomicInt m_index{0}; // индекс текущего языка в m_codes
QString m_language{QStringLiteral("system")};
QJSEngine *m_engine = nullptr;
QJSValue m_tree;
};
// ── Доступ из C++ ───────────────────────────────────────────────────────────
//
// Свободные функции, а не методы: их вызывают и из namespace-обёрток
// (LauncherPaths, JavaLocator, ZlibReference), где никакого QObject нет.
namespace Loc {
// Перевод по ключу. Ключа нет — возвращается сам ключ, в отладочной сборке ещё
// и qWarning: строка вида "launch.error.gameRunning" в интерфейсе сразу видна.
[[nodiscard]] QString text(const char *key);
// Для ключей со значением-списком (модели комбобоксов).
[[nodiscard]] QStringList list(const char *key);
} // namespace Loc