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