10 KiB
Localization
localization.h, localization.cpp
Все тексты интерфейса на всех языках лежат в одном файле, i18n/translations.json, и читаются
через этот класс. Стандартные .ts/.qm, lupdate, lrelease и Linguist в проекте не
используются. Язык переключается без перезапуска.
Из QML
Синглтон Loc (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 — дерево 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 задан руками (stLanguage).
Из C++
#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, javalocator.h,
zlibreference.h).
Если ключа нет, возвращается сам ключ, а в отладочной сборке ещё пишется qWarning. В интерфейсе
такую строку легко заметить.
Потоки
Loc::text() и Loc::list() можно вызывать из любого потока. Каталог заполняется один раз в
load(), до того как BuildSwitcher запустит свой поток, и дальше только
читается. На ходу меняется лишь индекс текущего языка, а он QAtomicInt. В худшем случае строка,
собранная в момент переключения, окажется на прежнем языке.
setLanguage() вызывается только из GUI-потока, это проверяет Q_ASSERT.
Каталог
Файл попадает в ресурсы через RESOURCES в qt_add_qml_module и читается из
:/qt/qml/BiLauncher/i18n/translations.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 должен совпадать, порядок может быть разным. Множественного числа формат не
поддерживает, и пока ни одна строка его не требует.
Добавить строку
- Запись в
stringsсразу сruиen. Loc.t.домен.вид.имяв QML илиLoc::text("домен.вид.имя")в C++.python3 tools/check_translations.py.
Добавить язык
- Код в
_meta.languages, название в_meta.displayNames. - Значение для этого кода в каждой записи
strings. - Ключ
settings.language.<код>с названием языка на нём самом, одинаковым во всех колонках. - Пункт в модели
stLanguageи код вstLanguage.codesв Main.qml. - Если нужно, поправить
systemLanguage(): сейчас при русской системной локали выбирается русский, при любой другой — английский.
Где хранится выбранный язык
Ключ language в settings.json, по умолчанию system. Меняется в окне настроек и применяется
по кнопке «Сохранить».
main.cpp ──► Localization::load() ──► читает settings.json (один раз)
LauncherBackend::updateSettings() ──► Localization::setLanguage()
settings.json записывает только LauncherBackend. Localization его только
читает, и только при старте: язык нужен раньше первой привязки QML, а бэкенд создаётся вместе с
движком. Обратной зависимости от бэкенда у Localization нет.
Неизвестный язык в файле (если его правили руками) заменяется на system с предупреждением.
Если каталог не найден или не разбирается, это ошибка сборки, потому что файл вкомпилирован в
бинарник. main() пишет причину в qCritical и возвращает -1.
Что не переводится при смене языка
Строки, которые уже собраны и лежат в полях C++, остаются на старом языке:
LauncherBackend::m_storageIssues собирается при старте, а stage()/status() у
BuildSwitcher обновятся только на следующем шаге. Всё это короткоживущие
диагностические сообщения.
Отдельно: GameLauncher прячет токен в строке запуска за <скрыт>, и эта строка не берётся из
каталога.
Проверка каталога
tools/check_translations.py ничего не меняет и возвращает ненулевой код при любой ошибке.
Проверяет:
- у всех языков одинаковый набор ключей и типы, пустых значений нет, длины списков равны;
- набор
%Nсовпадает по языкам; - каждый
Loc::text("…")иLoc::list("…")в C++ есть в каталоге с нужным типом; - каждый
Loc.t.a.b.cв QML ведёт к существующему ключу (последний сегмент может быть методом JS, например.arg); - нигде не осталось
tr(,qsTr(,QCoreApplication::translate.
Ключи, на которые никто не ссылается, выводятся предупреждением.
Скрипт смотрит только файлы в корне репозитория. Если исходники разложат по подпапкам, его нужно будет поправить.
При создании этого документа использовался ИИ.