174 lines
10 KiB
Markdown
174 lines
10 KiB
Markdown
# 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`.
|
||
|
||
Ключи, на которые никто не ссылается, выводятся предупреждением.
|
||
|
||
Скрипт смотрит только файлы в корне репозитория. Если исходники разложат по подпапкам, его нужно
|
||
будет поправить.
|
||
|
||
---
|
||
|
||
При создании этого документа использовался ИИ.
|