Files
BiLauncher/doc/cpp/Localization.md
T
2026-10-11 22:32:13 +03:00

10 KiB
Raw Permalink Blame History

Localization

localization.h, localization.cpp

Все тексты интерфейса на всех языках лежат в одном файле, i18n/translations.json, и читаются через этот класс. Стандартные .ts/.qm, lupdate, lrelease и Linguist в проекте не используются. Язык переключается без перезапуска.

Из QML

Синглтон Loc (QML_NAMED_ELEMENT(Loc) + QML_SINGLETON) доступен в любом файле модуля без импорта.

Text { text: Loc.t.settings.title }
Text { text: Loc.t.java.progress.downloading.arg(label) }
DarkCombo { model: Loc.t.profile.authTypes }

Loc.t — дерево JS-объектов, собранное из плоских ключей каталога разбиением по точке.

Почему t сделано свойством, а не методом Loc.t("ключ"). Вызов Q_INVOKABLE не регистрирует зависимость привязки, и текст не обновился бы при смене языка. Чтение Q_PROPERTY с NOTIFY регистрирует, поэтому languageChanged перевычисляет все привязки, где читали Loc.t. Дальнейшие .settings.title — обычные обращения к полям JS-объекта. Следить за ними не нужно, потому что при смене языка дерево заменяется целиком.

Тип свойства — QJSValue. С QVariantMap движок собирал бы новый JS-объект при каждом чтении, а привязок в проекте около полутора сотен. QJSValue строится один раз на каждую смену языка.

Кроме t есть language (что выбрано: system, ru, en), resolvedLanguage (во что это превратилось, всегда ru или en) и availableLanguages(). Последний сейчас не используется: список языков в настройках Main.qml задан руками (stLanguage).

Из C++

#include "localization.h"

emit launchError(Loc::text("launch.error.noProfile"));
emit launchProgress(Loc::text("java.progress.downloading").arg(label));
const QStringList kinds = Loc::list("profile.authTypes");

Это свободные функции, потому что их вызывают и из пространств имён без QObject (launcherpaths.h, javalocator.h, zlibreference.h).

Если ключа нет, возвращается сам ключ, а в отладочной сборке ещё пишется qWarning. В интерфейсе такую строку легко заметить.

Потоки

Loc::text() и Loc::list() можно вызывать из любого потока. Каталог заполняется один раз в load(), до того как BuildSwitcher запустит свой поток, и дальше только читается. На ходу меняется лишь индекс текущего языка, а он QAtomicInt. В худшем случае строка, собранная в момент переключения, окажется на прежнем языке.

setLanguage() вызывается только из GUI-потока, это проверяет Q_ASSERT.

Каталог

Файл попадает в ресурсы через RESOURCES в qt_add_qml_module и читается из :/qt/qml/BiLauncher/i18n/translations.json.

{
  "_meta": {
    "languages": ["ru", "en"],
    "displayNames": { "ru": "Русский", "en": "English" },
    "_readme": ["..."],
    "_glossary": { "сборка": "build", "...": "..." }
  },
  "strings": {
    "settings.title": { "ru": "Настройки запуска", "en": "Launch settings" },
    "java.progress.downloading": { "ru": "Загрузка Java «%1»…", "en": "Downloading Java \"%1\"…" },
    "profile.authTypes": {
      "ru": ["Офлайн (без пароля)", "Ely.by", "Microsoft (лицензия)"],
      "en": ["Offline (no password)", "Ely.by", "Microsoft (licensed)"]
    }
  }
}

Ключи плоские, языки записаны рядом. Так сразу видно, если перевод забыли, а новый язык добавляется ещё одним полем без правок загрузчика. _readme и _glossary — подсказки для тех, кто правит файл. Загрузчик их не читает.

Значение — либо строка, либо список строк (для моделей выпадающих списков). Тип определяется по первому языку.

Имена ключей

<домен>.<вид>.<имя>, сегменты в lowerCamelCase.

  • Домен — область интерфейса, а не файл: app, common, settings, profile, build, seasonal, version, java, loader, auth (auth.ely.*, auth.msa.*), launch, game, switch, storage, zlib.
  • Вид: title, label, button, placeholder, hint, header, tooltip; для сообщений error, progress, status, warning.
  • Имя описывает ситуацию, а не формулировку: gameRunning, downloadFailed, checksumMismatch. Тогда при правке перевода ключ не придётся переименовывать.

Строка, которая нужна в двух и более местах, кладётся в common.*.

%1, %2 пишутся как есть: их одинаково понимают QString::arg() и String.arg() в QML. Набор %N в ru и en должен совпадать, порядок может быть разным. Множественного числа формат не поддерживает, и пока ни одна строка его не требует.

Добавить строку

  1. Запись в strings сразу с ru и en.
  2. Loc.t.домен.вид.имя в QML или Loc::text("домен.вид.имя") в C++.
  3. python3 tools/check_translations.py.

Добавить язык

  1. Код в _meta.languages, название в _meta.displayNames.
  2. Значение для этого кода в каждой записи strings.
  3. Ключ settings.language.<код> с названием языка на нём самом, одинаковым во всех колонках.
  4. Пункт в модели stLanguage и код в stLanguage.codes в Main.qml.
  5. Если нужно, поправить systemLanguage(): сейчас при русской системной локали выбирается русский, при любой другой — английский.

Где хранится выбранный язык

Ключ language в settings.json, по умолчанию system. Меняется в окне настроек и применяется по кнопке «Сохранить».

main.cpp ──► Localization::load() ──► читает settings.json (один раз)
LauncherBackend::updateSettings() ──► Localization::setLanguage()

settings.json записывает только LauncherBackend. Localization его только читает, и только при старте: язык нужен раньше первой привязки QML, а бэкенд создаётся вместе с движком. Обратной зависимости от бэкенда у Localization нет.

Неизвестный язык в файле (если его правили руками) заменяется на system с предупреждением.

Если каталог не найден или не разбирается, это ошибка сборки, потому что файл вкомпилирован в бинарник. main() пишет причину в qCritical и возвращает -1.

Что не переводится при смене языка

Строки, которые уже собраны и лежат в полях C++, остаются на старом языке: LauncherBackend::m_storageIssues собирается при старте, а stage()/status() у BuildSwitcher обновятся только на следующем шаге. Всё это короткоживущие диагностические сообщения.

Отдельно: GameLauncher прячет токен в строке запуска за <скрыт>, и эта строка не берётся из каталога.

Проверка каталога

tools/check_translations.py ничего не меняет и возвращает ненулевой код при любой ошибке. Проверяет:

  • у всех языков одинаковый набор ключей и типы, пустых значений нет, длины списков равны;
  • набор %N совпадает по языкам;
  • каждый Loc::text("…") и Loc::list("…") в C++ есть в каталоге с нужным типом;
  • каждый Loc.t.a.b.c в QML ведёт к существующему ключу (последний сегмент может быть методом JS, например .arg);
  • нигде не осталось tr(, qsTr(, QCoreApplication::translate.

Ключи, на которые никто не ссылается, выводятся предупреждением.

Скрипт смотрит только файлы в корне репозитория. Если исходники разложат по подпапкам, его нужно будет поправить.


При создании этого документа использовался ИИ.