Files

173 lines
10 KiB
Markdown
Raw Permalink Normal View History

2026-09-03 09:16:56 +03:00
# Localization
2026-10-11 22:32:13 +03:00
`localization.h`, `localization.cpp`
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
Все тексты интерфейса на всех языках лежат в одном файле, `i18n/translations.json`, и читаются
через этот класс. Стандартные `.ts`/`.qm`, `lupdate`, `lrelease` и Linguist в проекте не
используются. Язык переключается без перезапуска.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
## Из QML
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
Синглтон `Loc` (`QML_NAMED_ELEMENT(Loc)` + `QML_SINGLETON`) доступен в любом файле модуля без
импорта.
2026-09-03 09:16:56 +03:00
```qml
Text { text: Loc.t.settings.title }
Text { text: Loc.t.java.progress.downloading.arg(label) }
DarkCombo { model: Loc.t.profile.authTypes }
```
2026-10-11 22:32:13 +03:00
`Loc.t` — дерево JS-объектов, собранное из плоских ключей каталога разбиением по точке.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
Почему `t` сделано свойством, а не методом `Loc.t("ключ")`. Вызов `Q_INVOKABLE` не регистрирует
зависимость привязки, и текст не обновился бы при смене языка. Чтение `Q_PROPERTY` с `NOTIFY`
регистрирует, поэтому `languageChanged` перевычисляет все привязки, где читали `Loc.t`. Дальнейшие
`.settings.title` — обычные обращения к полям JS-объекта. Следить за ними не нужно, потому что при
смене языка дерево заменяется целиком.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
Тип свойства — `QJSValue`. С `QVariantMap` движок собирал бы новый JS-объект при каждом чтении, а
привязок в проекте около полутора сотен. `QJSValue` строится один раз на каждую смену языка.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
Кроме `t` есть `language` (что выбрано: `system`, `ru`, `en`), `resolvedLanguage` (во что это
превратилось, всегда `ru` или `en`) и `availableLanguages()`. Последний сейчас не используется:
список языков в настройках [Main.qml](../qml/Main.md) задан руками (`stLanguage`).
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
## Из C++
2026-09-03 09:16:56 +03:00
```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");
```
2026-10-11 22:32:13 +03:00
Это свободные функции, потому что их вызывают и из пространств имён без `QObject`
([launcherpaths.h](launcherpaths.md), [javalocator.h](javalocator.md),
[zlibreference.h](zlibreference.md)).
Если ключа нет, возвращается сам ключ, а в отладочной сборке ещё пишется `qWarning`. В интерфейсе
такую строку легко заметить.
2026-09-03 09:16:56 +03:00
### Потоки
2026-10-11 22:32:13 +03:00
`Loc::text()` и `Loc::list()` можно вызывать из любого потока. Каталог заполняется один раз в
`load()`, до того как [BuildSwitcher](BuildSwitcher.md) запустит свой поток, и дальше только
читается. На ходу меняется лишь индекс текущего языка, а он `QAtomicInt`. В худшем случае строка,
собранная в момент переключения, окажется на прежнем языке.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
`setLanguage()` вызывается только из GUI-потока, это проверяет `Q_ASSERT`.
2026-09-03 09:16:56 +03:00
## Каталог
2026-10-11 22:32:13 +03:00
Файл попадает в ресурсы через `RESOURCES` в `qt_add_qml_module` и читается из
`:/qt/qml/BiLauncher/i18n/translations.json`.
2026-09-03 09:16:56 +03:00
```json
{
"_meta": {
"languages": ["ru", "en"],
2026-10-11 22:32:13 +03:00
"displayNames": { "ru": "Русский", "en": "English" },
"_readme": ["..."],
"_glossary": { "сборка": "build", "...": "..." }
2026-09-03 09:16:56 +03:00
},
"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)"]
}
}
}
```
2026-10-11 22:32:13 +03:00
Ключи плоские, языки записаны рядом. Так сразу видно, если перевод забыли, а новый язык
добавляется ещё одним полем без правок загрузчика. `_readme` и `_glossary` — подсказки для тех, кто
правит файл. Загрузчик их не читает.
Значение — либо строка, либо список строк (для моделей выпадающих списков). Тип определяется по
первому языку.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
### Имена ключей
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
`<домен>.<вид>.<имя>`, сегменты в lowerCamelCase.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
- **Домен** — область интерфейса, а не файл: `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`. Тогда при правке перевода ключ не придётся переименовывать.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
Строка, которая нужна в двух и более местах, кладётся в `common.*`.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
`%1`, `%2` пишутся как есть: их одинаково понимают `QString::arg()` и `String.arg()` в QML. Набор
`%N` в `ru` и `en` должен совпадать, порядок может быть разным. Множественного числа формат не
поддерживает, и пока ни одна строка его не требует.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
## Добавить строку
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
1. Запись в `strings` сразу с `ru` и `en`.
2. `Loc.t.домен.вид.имя` в QML или `Loc::text("домен.вид.имя")` в C++.
3. `python3 tools/check_translations.py`.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
## Добавить язык
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
1. Код в `_meta.languages`, название в `_meta.displayNames`.
2. Значение для этого кода в каждой записи `strings`.
3. Ключ `settings.language.<код>` с названием языка на нём самом, одинаковым во всех колонках.
4. Пункт в модели `stLanguage` и код в `stLanguage.codes` в [Main.qml](../qml/Main.md).
5. Если нужно, поправить `systemLanguage()`: сейчас при русской системной локали выбирается
русский, при любой другой — английский.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
## Где хранится выбранный язык
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
Ключ `language` в `settings.json`, по умолчанию `system`. Меняется в окне настроек и применяется
по кнопке «Сохранить».
2026-09-03 09:16:56 +03:00
```
2026-10-11 22:32:13 +03:00
main.cpp ──► Localization::load() ──► читает settings.json (один раз)
LauncherBackend::updateSettings() ──► Localization::setLanguage()
2026-09-03 09:16:56 +03:00
```
2026-10-11 22:32:13 +03:00
`settings.json` записывает только [LauncherBackend](LauncherBackend.md). Localization его только
читает, и только при старте: язык нужен раньше первой привязки QML, а бэкенд создаётся вместе с
движком. Обратной зависимости от бэкенда у Localization нет.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
Неизвестный язык в файле (если его правили руками) заменяется на `system` с предупреждением.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
Если каталог не найден или не разбирается, это ошибка сборки, потому что файл вкомпилирован в
бинарник. `main()` пишет причину в `qCritical` и возвращает `-1`.
### Что не переводится при смене языка
Строки, которые уже собраны и лежат в полях C++, остаются на старом языке:
2026-09-03 09:16:56 +03:00
`LauncherBackend::m_storageIssues` собирается при старте, а `stage()`/`status()` у
2026-10-11 22:32:13 +03:00
[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`.
Ключи, на которые никто не ссылается, выводятся предупреждением.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
Скрипт смотрит только файлы в корне репозитория. Если исходники разложат по подпапкам, его нужно
будет поправить.
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
---
2026-09-03 09:16:56 +03:00
2026-10-11 22:32:13 +03:00
При создании этого документа использовался ИИ.