Files
2026-09-03 09:16:56 +03:00

165 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`;
- ключи, на которые никто не ссылается, — предупреждением.