# Localization `localization.h`, `localization.cpp` Все тексты интерфейса на всех языках лежат в одном файле, `i18n/translations.json`, и читаются через этот класс. Стандартные `.ts`/`.qm`, `lupdate`, `lrelease` и Linguist в проекте не используются. Язык переключается без перезапуска. ## Из QML Синглтон `Loc` (`QML_NAMED_ELEMENT(Loc)` + `QML_SINGLETON`) доступен в любом файле модуля без импорта. ```qml 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](../qml/Main.md) задан руками (`stLanguage`). ## Из C++ ```cpp #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](launcherpaths.md), [javalocator.h](javalocator.md), [zlibreference.h](zlibreference.md)). Если ключа нет, возвращается сам ключ, а в отладочной сборке ещё пишется `qWarning`. В интерфейсе такую строку легко заметить. ### Потоки `Loc::text()` и `Loc::list()` можно вызывать из любого потока. Каталог заполняется один раз в `load()`, до того как [BuildSwitcher](BuildSwitcher.md) запустит свой поток, и дальше только читается. На ходу меняется лишь индекс текущего языка, а он `QAtomicInt`. В худшем случае строка, собранная в момент переключения, окажется на прежнем языке. `setLanguage()` вызывается только из GUI-потока, это проверяет `Q_ASSERT`. ## Каталог Файл попадает в ресурсы через `RESOURCES` в `qt_add_qml_module` и читается из `:/qt/qml/BiLauncher/i18n/translations.json`. ```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](../qml/Main.md). 5. Если нужно, поправить `systemLanguage()`: сейчас при русской системной локали выбирается русский, при любой другой — английский. ## Где хранится выбранный язык Ключ `language` в `settings.json`, по умолчанию `system`. Меняется в окне настроек и применяется по кнопке «Сохранить». ``` main.cpp ──► Localization::load() ──► читает settings.json (один раз) LauncherBackend::updateSettings() ──► Localization::setLanguage() ``` `settings.json` записывает только [LauncherBackend](LauncherBackend.md). Localization его только читает, и только при старте: язык нужен раньше первой привязки QML, а бэкенд создаётся вместе с движком. Обратной зависимости от бэкенда у Localization нет. Неизвестный язык в файле (если его правили руками) заменяется на `system` с предупреждением. Если каталог не найден или не разбирается, это ошибка сборки, потому что файл вкомпилирован в бинарник. `main()` пишет причину в `qCritical` и возвращает `-1`. ### Что не переводится при смене языка Строки, которые уже собраны и лежат в полях C++, остаются на старом языке: `LauncherBackend::m_storageIssues` собирается при старте, а `stage()`/`status()` у [BuildSwitcher](BuildSwitcher.md) обновятся только на следующем шаге. Всё это короткоживущие диагностические сообщения. Отдельно: `GameLauncher` прячет токен в строке запуска за `<скрыт>`, и эта строка не берётся из каталога. ## Проверка каталога `tools/check_translations.py` ничего не меняет и возвращает ненулевой код при любой ошибке. Проверяет: - у всех языков одинаковый набор ключей и типы, пустых значений нет, длины списков равны; - набор `%N` совпадает по языкам; - каждый `Loc::text("…")` и `Loc::list("…")` в C++ есть в каталоге с нужным типом; - каждый `Loc.t.a.b.c` в QML ведёт к существующему ключу (последний сегмент может быть методом JS, например `.arg`); - нигде не осталось `tr(`, `qsTr(`, `QCoreApplication::translate`. Ключи, на которые никто не ссылается, выводятся предупреждением. Скрипт смотрит только файлы в корне репозитория. Если исходники разложат по подпапкам, его нужно будет поправить. --- При создании этого документа использовался ИИ.