11 KiB
Localization
localization.h / localization.cpp
Единственный источник всех текстов интерфейса. Каталог — i18n/translations.json,
он лежит в ресурсах и правится руками. Штатных .ts/.qm в проекте нет намеренно: lupdate,
lrelease и Linguist не нужны, а все переводы видны в одном файле.
Язык переключается на лету: при смене все привязки QML перевычисляются, перезапуск не требуется.
Доступ из QML
Синглтон зарегистрирован декларативно (QML_NAMED_ELEMENT(Loc) + QML_SINGLETON) и доступен
в любом файле модуля без импорта.
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 читается так же — это просто чтение свойства:
onLaunched: (profileName, buildName) =>
window.showToast(Loc.t.launch.status.started.arg(profileName).arg(buildName), "#4b7a1f")
Доступ из C++
Свободные функции, а не методы: их вызывают и из namespace-обёрток
(LauncherPaths, JavaLocator,
ZlibReference), где никакого QObject нет.
#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
создаст свой поток; дальше он только читается, а копирование QString из хэша безопасно само по
себе. Единственное, что меняется на ходу, — индекс текущего языка, и он QAtomicInt. В худшем
случае сообщение, которое собиралось в момент переключения, уедет на прежнем языке.
setLanguage() и рассылка languageChanged — только поток GUI; это проверяется Q_ASSERT.
Каталог
i18n/translations.json попадает в ресурсы через список RESOURCES в qt_add_qml_module,
поэтому читается по пути :/qt/qml/Minecraft_launcher/i18n/translations.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 обязан совпадать — порядок слов может
отличаться, состав нет. Множественного числа формат не поддерживает; в коде сейчас нет ни одной
строки, которой оно требуется.
Как добавить строку
- Добавить запись в
stringsфайлаi18n/translations.json— сразу сruиen. - Сослаться на неё:
Loc.t.домен.вид.имяв QML илиLoc::text("домен.вид.имя")в C++. - Прогнать
python3 tools/check_translations.py.
Как добавить язык
- Дописать код в
_meta.languagesи название в_meta.displayNames. - Добавить колонку с этим кодом в каждую запись
strings. - Добавить код в
stLanguage.codesи пункт в модель комбобокса в Main.qml, а также ключsettings.language.<код>с эндонимом (название языка не переводится — оно одинаково во всех колонках). - При необходимости поправить
systemLanguage()вlocalization.cpp: сейчас он выбирает русский для русской системной локали и английский во всех остальных случаях.
Выбор языка и его хранение
Ключ настройки — language, значения "system", "ru", "en", по умолчанию "system".
Хранится в settings.json рядом с остальными настройками; в интерфейсе — первым пунктом
диалога «Настройки запуска», применяется по кнопке «Сохранить».
Круг замкнут в одну сторону, циклической зависимости нет:
main.cpp ──► Localization::load() ──► LauncherPaths::settingsFile() (только чтение, один раз)
LauncherBackend::updateSettings() ──► Localization::setLanguage() (в одну сторону)
Localization ничего не знает про LauncherBackend — писать settings.json
по-прежнему может только он. Читать настройки самому приходится потому, что язык нужен раньше,
чем QML вычислит первую привязку, а бэкенд появляется только вместе с движком.
Неизвестное значение (файл правили руками) откатывается на "system" с предупреждением.
Отсутствующий или испорченный каталог — ошибка на старте: main() пишет причину и возвращает
-1. Файл вкомпилирован в бинарник, так что это может быть только ошибка сборки, а лаунчер,
у которого все подписи выглядят как точечные ключи, хуже, чем лаунчер, который сказал, почему
не запустился.
Известное ограничение. Уже сложенные в поля C++ строки не перепереводятся:
LauncherBackend::m_storageIssues собирается при старте, а stage()/status() у
BuildSwitcher заменяются на следующем тике прогресса. Все они
диагностические и короткоживущие.
Проверка
tools/check_translations.py — только чтение, ненулевой код возврата при любой ошибке:
- каждый
Loc::text("…")иLoc::list("…")из C++ есть в каталоге и совпадает по типу значения; - каждый путь
Loc.t.a.b.cиз QML разворачивается в существующий ключ; - наборы ключей у всех языков совпадают, пустых значений нет, типы одинаковы;
- наборы
%Nсовпадают по языкам, длины списков равны; - не осталось ни одного
tr(,qsTr(илиQCoreApplication::translate; - ключи, на которые никто не ссылается, — предупреждением.