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
|
|
|
|
При создании этого документа использовался ИИ.
|