# Localization `localization.h` / `localization.cpp` Единственный источник всех текстов интерфейса. Каталог — [`i18n/translations.json`](#каталог), он лежит в ресурсах и правится руками. Штатных `.ts`/`.qm` в проекте нет намеренно: `lupdate`, `lrelease` и Linguist не нужны, а все переводы видны в одном файле. Язык переключается на лету: при смене все привязки QML перевычисляются, перезапуск не требуется. ## Доступ из QML Синглтон зарегистрирован декларативно (`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.a.b.c` — это обращение к вложенным объектам дерева, которое строится из плоских ключей каталога разбиением по точке. **Почему свойство, а не метод.** Вызов `Q_INVOKABLE` не регистрирует зависимость привязки, и `text: Loc.t("ключ")` никогда бы не обновился при смене языка. Чтение `Q_PROPERTY` с сигналом `NOTIFY` зависимость регистрирует: `languageChanged` перевычисляет все привязки, которые читали `Loc.t`. Сегменты после `t` — обычные обращения к членам JS-объекта, отслеживать их не нужно, потому что при смене языка дерево заменяется целиком. Тип свойства — `QJSValue`, а не `QVariantMap`: `QVariantMap` пересобирался бы в новый JS-объект при каждом чтении, а привязок в проекте полторы сотни. `QJSValue` строится один раз на смену языка. Из тела JS-функции `Loc.t` читается так же — это просто чтение свойства: ```qml onLaunched: (profileName, buildName) => window.showToast(Loc.t.launch.status.started.arg(profileName).arg(buildName), "#4b7a1f") ``` ## Доступ из C++ Свободные функции, а не методы: их вызывают и из namespace-обёрток ([LauncherPaths](launcherpaths.md), [JavaLocator](javalocator.md), [ZlibReference](zlibreference.md)), где никакого `QObject` нет. ```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"); ``` Ключа нет — возвращается сам ключ, а в отладочной сборке ещё и `qWarning`: строка вида `launch.error.noProfile` в интерфейсе сразу бросается в глаза. ### Потоки `Loc::text()` и `Loc::list()` можно звать из любого потока. Каталог заполняется ровно один раз в `load()`, который отрабатывает в `main()` до того, как [BuildSwitcher](BuildSwitcher.md) создаст свой поток; дальше он только читается, а копирование `QString` из хэша безопасно само по себе. Единственное, что меняется на ходу, — индекс текущего языка, и он `QAtomicInt`. В худшем случае сообщение, которое собиралось в момент переключения, уедет на прежнем языке. `setLanguage()` и рассылка `languageChanged` — только поток GUI; это проверяется `Q_ASSERT`. ## Каталог `i18n/translations.json` попадает в ресурсы через список `RESOURCES` в `qt_add_qml_module`, поэтому читается по пути `:/qt/qml/Minecraft_launcher/i18n/translations.json`. ```json { "_meta": { "languages": ["ru", "en"], "displayNames": { "ru": "Русский", "en": "English" } }, "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)"] } } } ``` Ключи плоские, языки рядом: забытый перевод виден на соседней строке, правка пары — один участок файла, а третий язык добавляется колонкой без правок загрузчика. **Имя ключа — `<домен>.<вид>.<имя>`**, сегменты в 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`; для сообщений `error`, `progress`, `status`, `warning`. - **Имя** описывает условие, а не формулировку, чтобы перевод не переименовывал ключ: `gameRunning`, `downloadFailed`, `checksumMismatch`. Литерал, который нужен в двух и более файлах, живёт в `common.*`. Подстановки `%1`/`%2` сохраняются дословно: их одинаково понимают `QString::arg()` и QML-овский `String.arg()`. **Набор `%N` в `ru` и `en` обязан совпадать** — порядок слов может отличаться, состав нет. Множественного числа формат не поддерживает; в коде сейчас нет ни одной строки, которой оно требуется. ## Как добавить строку 1. Добавить запись в `strings` файла `i18n/translations.json` — сразу с `ru` и `en`. 2. Сослаться на неё: `Loc.t.домен.вид.имя` в QML или `Loc::text("домен.вид.имя")` в C++. 3. Прогнать `python3 tools/check_translations.py`. ## Как добавить язык 1. Дописать код в `_meta.languages` и название в `_meta.displayNames`. 2. Добавить колонку с этим кодом в каждую запись `strings`. 3. Добавить код в `stLanguage.codes` и пункт в модель комбобокса в [Main.qml](../qml/Main.md), а также ключ `settings.language.<код>` с эндонимом (название языка не переводится — оно одинаково во всех колонках). 4. При необходимости поправить `systemLanguage()` в `localization.cpp`: сейчас он выбирает русский для русской системной локали и английский во всех остальных случаях. ## Выбор языка и его хранение Ключ настройки — `language`, значения `"system"`, `"ru"`, `"en"`, по умолчанию `"system"`. Хранится в `settings.json` рядом с остальными настройками; в интерфейсе — первым пунктом диалога «Настройки запуска», применяется по кнопке «Сохранить». Круг замкнут в одну сторону, циклической зависимости нет: ``` main.cpp ──► Localization::load() ──► LauncherPaths::settingsFile() (только чтение, один раз) LauncherBackend::updateSettings() ──► Localization::setLanguage() (в одну сторону) ``` `Localization` ничего не знает про [LauncherBackend](LauncherBackend.md) — писать `settings.json` по-прежнему может только он. Читать настройки самому приходится потому, что язык нужен раньше, чем QML вычислит первую привязку, а бэкенд появляется только вместе с движком. Неизвестное значение (файл правили руками) откатывается на `"system"` с предупреждением. Отсутствующий или испорченный каталог — ошибка на старте: `main()` пишет причину и возвращает `-1`. Файл вкомпилирован в бинарник, так что это может быть только ошибка сборки, а лаунчер, у которого все подписи выглядят как точечные ключи, хуже, чем лаунчер, который сказал, почему не запустился. **Известное ограничение.** Уже сложенные в поля C++ строки не перепереводятся: `LauncherBackend::m_storageIssues` собирается при старте, а `stage()`/`status()` у [BuildSwitcher](BuildSwitcher.md) заменяются на следующем тике прогресса. Все они диагностические и короткоживущие. ## Проверка `tools/check_translations.py` — только чтение, ненулевой код возврата при любой ошибке: - каждый `Loc::text("…")` и `Loc::list("…")` из C++ есть в каталоге и совпадает по типу значения; - каждый путь `Loc.t.a.b.c` из QML разворачивается в существующий ключ; - наборы ключей у всех языков совпадают, пустых значений нет, типы одинаковы; - наборы `%N` совпадают по языкам, длины списков равны; - не осталось ни одного `tr(`, `qsTr(` или `QCoreApplication::translate`; - ключи, на которые никто не ссылается, — предупреждением.