#pragma once #include #include #include #include #include #include #include #include #include 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 m_singles; // ключ -> значения по языкам QHash> m_lists; // ключ -> списки по языкам QStringList m_codes; // ["ru", "en"] — порядок из _meta QHash 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