165 lines
11 KiB
Markdown
165 lines
11 KiB
Markdown
|
|
# 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`;
|
|||
|
|
- ключи, на которые никто не ссылается, — предупреждением.
|