translation of all project, and new translation mechanism

This commit is contained in:
2026-09-03 09:18:48 +03:00
parent 00e7c957e4
commit 01af372ef2
32 changed files with 2813 additions and 537 deletions
+123 -113
View File
@@ -1,5 +1,7 @@
#include "launcherbackend.h"
#include "localization.h"
#include "authservice.h"
#include "gamelauncher.h"
#include "javainstaller.h"
@@ -92,7 +94,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
// следующем — если её удалили, она появится снова.
QString storageError;
if (!LauncherPaths::ensureRootExists(&storageError))
m_storageIssues << tr("Не удалось создать папку лаунчера: %1").arg(storageError);
m_storageIssues << Loc::text("storage.error.launcherDirFailed").arg(storageError);
loadSettings();
// Порядок важен: миграция должна успеть до того, как ensureDataFiles()
@@ -137,7 +139,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
connect(m_javaInstaller, &JavaInstaller::started, this, [this](const QString &label) {
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Загрузка Java «%1»…").arg(label));
emit launchProgress(Loc::text("java.progress.downloading").arg(label));
});
connect(m_javaInstaller, &JavaInstaller::progressChanged,
this, &LauncherBackend::downloadChanged);
@@ -147,7 +149,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
emit downloadChanged();
emit busyChanged();
emit javaRuntimeInstalled(runtimeId);
emit launchProgress(tr("Java установлена: %1")
emit launchProgress(Loc::text("java.status.installed")
.arg(QDir::toNativeSeparators(javaPath)));
if (m_season.stage == SeasonStage::Java)
seasonAdvance();
@@ -157,7 +159,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
invalidateJavaCatalog();
emit downloadChanged();
emit busyChanged();
emit launchError(tr("Не удалось установить Java «%1»: %2").arg(label, message));
emit launchError(Loc::text("java.error.installFailed").arg(label, message));
if (m_season.stage == SeasonStage::Java)
seasonFail({});
});
@@ -165,7 +167,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
invalidateJavaCatalog();
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Загрузка Java «%1» отменена").arg(label));
emit launchProgress(Loc::text("java.status.downloadCancelled").arg(label));
if (m_season.stage == SeasonStage::Java)
seasonFail({});
});
@@ -173,7 +175,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
connect(m_installer, &VersionInstaller::started, this, [this](const QString &id) {
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Загрузка версии «%1»…").arg(id));
emit launchProgress(Loc::text("version.progress.downloading").arg(id));
});
connect(m_installer, &VersionInstaller::progressChanged,
this, &LauncherBackend::downloadChanged);
@@ -184,7 +186,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
emit installedVersionsChanged();
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Версия «%1» загружена").arg(id));
emit launchProgress(Loc::text("version.status.downloaded").arg(id));
// У ванильной сезонной сборки этот установщик и есть весь шаг Loader.
// Внутри установки модлоадера он тоже отчитывается — тогда ход
// передаст ModLoaderInstaller, а не он.
@@ -198,7 +200,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
emit installedVersionsChanged();
emit downloadChanged();
emit busyChanged();
emit launchError(tr("Не удалось загрузить версию «%1»: %2").arg(id, message));
emit launchError(Loc::text("version.error.downloadFailed").arg(id, message));
if (m_season.stage == SeasonStage::Loader && !m_loaderInstaller->isRunning())
seasonFail({});
});
@@ -217,7 +219,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
emit switchChanged();
emit busyChanged();
const int index = indexOfBuildId(toBuildId);
emit launchProgress(tr("Активна сборка «%1»")
emit launchProgress(Loc::text("build.status.active")
.arg(index >= 0 ? m_builds[index].name : QString()));
// Раскатка пака могла быть и той, что доигралась после перезапуска —
@@ -243,15 +245,13 @@ LauncherBackend::LauncherBackend(QObject *parent)
// раскатка означает лишь наполовину обновлённую сборку.
if (m_season.stage == SeasonStage::Applying) {
emit launchError(gameDirIntact
? tr("Файлы сборки не установлены: %1").arg(message)
: tr("Файлы сборки установлены не полностью: %1\n"
"Повторите установку сборки.")
? Loc::text("build.error.filesNotInstalled").arg(message)
: Loc::text("build.error.filesIncomplete")
.arg(message));
} else {
emit launchError(gameDirIntact
? tr("Сборка не переключена: %1").arg(message)
: tr("Сборка не переключена: %1\nСодержимое .minecraft "
"осталось разобранным — повторите переключение.")
? Loc::text("build.error.switchFailed").arg(message)
: Loc::text("build.error.switchFailedDirty")
.arg(message));
}
if (m_season.stage == SeasonStage::Switching
@@ -270,7 +270,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
connect(m_loaderInstaller, &ModLoaderInstaller::started, this, [this](const QString &label) {
emit busyChanged();
emit downloadChanged();
emit launchProgress(tr("Установка %1…").arg(label));
emit launchProgress(Loc::text("loader.progress.installingShort").arg(label));
});
connect(m_loaderInstaller, &ModLoaderInstaller::progressChanged,
this, &LauncherBackend::downloadChanged);
@@ -290,7 +290,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
emit installedVersionsChanged();
emit busyChanged();
emit downloadChanged();
emit launchProgress(tr("Готово: %1 для Minecraft %2")
emit launchProgress(Loc::text("loader.status.done")
.arg(producedVersionId, gameVersion));
if (m_season.stage == SeasonStage::Loader)
seasonAdvance();
@@ -302,7 +302,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
emit installedVersionsChanged();
emit busyChanged();
emit downloadChanged();
emit launchError(tr("%1: %2").arg(label, message));
emit launchError(Loc::text("common.status.pair").arg(label, message));
if (m_season.stage == SeasonStage::Loader)
seasonFail({});
});
@@ -312,7 +312,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
emit installedVersionsChanged();
emit busyChanged();
emit downloadChanged();
emit launchProgress(tr("Установка %1 отменена").arg(label));
emit launchProgress(Loc::text("loader.status.installCancelled").arg(label));
if (m_season.stage == SeasonStage::Loader)
seasonFail({});
});
@@ -322,7 +322,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
emit installedVersionsChanged();
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Загрузка версии «%1» отменена").arg(id));
emit launchProgress(Loc::text("version.status.downloadCancelled").arg(id));
if (m_season.stage == SeasonStage::Loader && !m_loaderInstaller->isRunning())
seasonFail({});
});
@@ -340,7 +340,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
connect(m_packDownloader, &SeasonalPackDownloader::started, this, [this](const QString &label) {
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Загрузка файлов сборки «%1»…").arg(label));
emit launchProgress(Loc::text("seasonal.progress.downloadingBuild").arg(label));
});
connect(m_packDownloader, &SeasonalPackDownloader::progressChanged,
this, &LauncherBackend::downloadChanged);
@@ -356,14 +356,14 @@ LauncherBackend::LauncherBackend(QObject *parent)
[this](const QString &label, const QString &message) {
emit downloadChanged();
emit busyChanged();
emit launchError(tr("Сборка «%1»: %2").arg(label, message));
emit launchError(Loc::text("build.status.named").arg(label, message));
if (m_season.stage == SeasonStage::Downloading)
seasonFail({});
});
connect(m_packDownloader, &SeasonalPackDownloader::canceled, this, [this](const QString &label) {
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Загрузка сборки «%1» отменена").arg(label));
emit launchProgress(Loc::text("seasonal.status.downloadCancelled").arg(label));
if (m_season.stage == SeasonStage::Downloading)
seasonFail({});
});
@@ -577,11 +577,11 @@ void LauncherBackend::removeCustomBuild(int index)
if (m_switcher->isRunning() || m_busy || m_installer->isRunning()
|| m_loaderInstaller->isRunning()) {
emit launchError(tr("Дождитесь окончания текущей операции"));
emit launchError(Loc::text("common.error.busyOperation"));
return;
}
if (m_launcher->isRunning()) {
emit launchError(tr("Сначала закройте игру"));
emit launchError(Loc::text("common.error.closeGameFirst"));
return;
}
@@ -593,7 +593,7 @@ void LauncherBackend::removeCustomBuild(int index)
// Раньше удалялась только запись, а архив сборки оставался лежать в
// galeonLauncher/builds вместе со ссылкой на него в index.json.
if (!m_switcher->forgetBuild(removed.id)) {
emit launchError(tr("Сборка «%1» удалена, но её архив в %2 стереть не удалось")
emit launchError(Loc::text("build.warning.archiveNotRemoved")
.arg(removed.name,
QDir::toNativeSeparators(LauncherPaths::buildStorageDir())));
}
@@ -616,7 +616,7 @@ void LauncherBackend::removeCustomBuild(int index)
}
const CustomBuild &next = m_builds.first();
emit launchProgress(tr("Активна сборка «%1»…").arg(next.name));
emit launchProgress(Loc::text("build.progress.activating").arg(next.name));
// from = -1: архивировать нечего, содержимое удалённой сборки уходит вместе
// с ней. Дальше обычная очистка и распаковка новой активной сборки.
m_switcher->switchTo(-1, QString(), next.id, next.name, gameDir());
@@ -649,22 +649,22 @@ void LauncherBackend::setActiveBuildIndex(int index)
return;
if (m_switcher->isRunning()) {
emit launchError(tr("Смена сборки уже выполняется"));
emit launchError(Loc::text("common.error.switchInProgress"));
return;
}
if (m_launcher->isRunning()) {
emit launchError(tr("Сначала закройте игру — её файлы сейчас переносятся"));
emit launchError(Loc::text("build.error.closeGameFilesMoving"));
return;
}
if (m_busy || m_installer->isRunning() || m_loaderInstaller->isRunning()) {
emit launchError(tr("Дождитесь окончания текущей операции"));
emit launchError(Loc::text("common.error.busyOperation"));
return;
}
// Содержимое .minecraft принадлежит прежней сборке: сначала оно уезжает в
// её архив, и только потом на его место разворачивается выбранная.
const CustomBuild &target = m_builds[index];
emit launchProgress(tr("Смена сборки на «%1»…").arg(target.name));
emit launchProgress(Loc::text("build.progress.switchingTo").arg(target.name));
m_switcher->switchTo(current >= 0 ? m_builds[current].id : -1,
current >= 0 ? m_builds[current].name : QString(),
target.id,
@@ -700,19 +700,19 @@ QString LauncherBackend::launchVersionIdOf(const CustomBuild &build) const
QStringList LauncherBackend::checkInstallation(int buildIndex) const
{
if (buildIndex < 0 || buildIndex >= m_builds.size())
return {tr("Сборка не выбрана")};
return {Loc::text("common.status.noBuildSelected")};
const CustomBuild &entry = m_builds[buildIndex];
const QString id = launchVersionIdOf(entry);
if (id.trimmed().isEmpty())
return {tr("У сборки не выбрана версия Minecraft. Проверьте %1")
return {Loc::text("build.error.noMinecraftVersion")
.arg(QDir::toNativeSeparators(LauncherPaths::customBuildsFile()))};
QString error;
const MinecraftVersion version = VersionLoader::load(gameDir(), id, {}, &error);
if (!version.isValid()) {
return {error.isEmpty()
? tr("Версия «%1» из сборки «%2» не установлена в %3")
? Loc::text("build.error.versionNotInstalled")
.arg(id,
entry.name,
QDir::toNativeSeparators(gameDir() + QStringLiteral("/versions")))
@@ -786,30 +786,30 @@ void LauncherBackend::removeVersion(const QString &versionId)
return;
if (m_installer->versionId() == id || m_installer->isQueued(id)) {
emit launchError(tr("Эта версия сейчас скачивается"));
emit launchError(Loc::text("version.error.currentlyDownloading"));
return;
}
if (m_switcher->isRunning() || m_busy || m_installer->isRunning()
|| m_loaderInstaller->isRunning()) {
emit launchError(tr("Дождитесь окончания текущей операции"));
emit launchError(Loc::text("common.error.busyOperation"));
return;
}
// Какой версией сейчас играют, GameLauncher наружу не отдаёт, поэтому на
// время игры удаление запрещено целиком.
if (m_launcher->isRunning()) {
emit launchError(tr("Сначала закройте игру"));
emit launchError(Loc::text("common.error.closeGameFirst"));
return;
}
QString error;
if (!VersionLoader::remove(gameDir(), id, &error)) {
emit launchError(tr("Не удалось удалить версию: %1").arg(error));
emit launchError(Loc::text("version.error.removeFailed").arg(error));
return;
}
invalidateCatalog();
emit installedVersionsChanged();
emit launchProgress(tr("Версия «%1» удалена").arg(id));
emit launchProgress(Loc::text("version.status.removed").arg(id));
}
QVariantList LauncherBackend::versionCatalog() const
@@ -1009,9 +1009,9 @@ QVariantList LauncherBackend::loaderVersions(const QString &loaderKey,
for (const LoaderVersionEntry &entry : entries) {
QString label = entry.loaderVersion;
if (entry.recommended)
label += tr(" — рекомендуемая");
label += Loc::text("loader.label.recommended");
else if (!entry.stable)
label += tr(" — нестабильная");
label += Loc::text("loader.label.unstable");
result.append(QVariantMap{{QStringLiteral("version"), entry.loaderVersion},
{QStringLiteral("label"), label},
{QStringLiteral("recommended"), entry.recommended},
@@ -1048,13 +1048,13 @@ void LauncherBackend::installLoaderForBuild(int index)
if (index < 0 || index >= m_builds.size())
return;
if (busy()) {
emit launchError(tr("Дождитесь окончания текущей загрузки"));
emit launchError(Loc::text("common.error.busyDownload"));
return;
}
const CustomBuild &build = m_builds[index];
if (build.minecraftVersion.trimmed().isEmpty()) {
emit launchError(tr("Сначала выберите версию Minecraft для сборки «%1»").arg(build.name));
emit launchError(Loc::text("build.error.pickMinecraftVersion").arg(build.name));
return;
}
@@ -1067,7 +1067,7 @@ void LauncherBackend::installLoaderForBuild(int index)
return;
}
if (build.loaderVersion.trimmed().isEmpty()) {
emit launchError(tr("Выберите версию %1 для сборки «%2»")
emit launchError(Loc::text("build.error.pickLoaderVersion")
.arg(loaderTitle(*loader), build.name));
return;
}
@@ -1085,36 +1085,36 @@ void LauncherBackend::installLoaderForBuild(int index)
void LauncherBackend::launchGame(int profileIndex, int buildIndex)
{
if (m_loaderInstaller->isRunning()) {
emit launchError(tr("Идёт установка «%1» — дождитесь окончания")
emit launchError(Loc::text("build.error.installBusy")
.arg(m_loaderInstaller->label()));
return;
}
if (m_installer->isRunning()) {
emit launchError(tr("Идёт загрузка версии «%1» — дождитесь окончания")
emit launchError(Loc::text("version.error.downloadBusy")
.arg(m_installer->versionId()));
return;
}
// Игра ищет java при запуске: стартовать посреди её установки — значит
// найти половину распакованного дерева.
if (m_javaInstaller->isRunning()) {
emit launchError(tr("Идёт установка Java «%1» — дождитесь окончания")
emit launchError(Loc::text("java.error.installBusy")
.arg(m_javaInstaller->label()));
return;
}
if (m_busy) {
emit launchError(tr("Запуск уже выполняется"));
emit launchError(Loc::text("launch.error.alreadyLaunching"));
return;
}
if (m_launcher->isRunning()) {
emit launchError(tr("Игра уже запущена"));
emit launchError(Loc::text("common.error.gameRunning"));
return;
}
if (profileIndex < 0 || profileIndex >= m_profiles.size()) {
emit launchError(tr("Выберите профиль перед запуском"));
emit launchError(Loc::text("launch.error.noProfile"));
return;
}
if (buildIndex < 0 || buildIndex >= m_builds.size()) {
emit launchError(tr("Выберите сборку перед запуском"));
emit launchError(Loc::text("launch.error.noBuild"));
return;
}
@@ -1122,7 +1122,7 @@ void LauncherBackend::launchGame(int profileIndex, int buildIndex)
// в galeonLauncher при запуске.
const QString directory = gameDir();
if (!QDir(directory).exists()) {
emit launchError(tr("Папка .minecraft не найдена: %1. Укажите путь к ней в настройках.")
emit launchError(Loc::text("launch.error.gameDirMissing")
.arg(QDir::toNativeSeparators(directory)));
return;
}
@@ -1130,7 +1130,7 @@ void LauncherBackend::launchGame(int profileIndex, int buildIndex)
const CustomBuild &entry = m_builds[buildIndex];
const QString id = launchVersionIdOf(entry);
if (id.trimmed().isEmpty()) {
emit launchError(tr("У сборки «%1» не выбрана версия Minecraft. Проверьте %2")
emit launchError(Loc::text("launch.error.buildNoVersion")
.arg(entry.name,
QDir::toNativeSeparators(LauncherPaths::customBuildsFile())));
return;
@@ -1140,8 +1140,8 @@ void LauncherBackend::launchGame(int profileIndex, int buildIndex)
const QStringList installed = VersionLoader::installedVersions(directory);
if (!installed.contains(id)) {
emit launchError(installed.isEmpty()
? tr("В %1 нет ни одной установленной версии").arg(versionsDir)
: tr("Версия «%1» из сборки «%2» не установлена в %3. Доступны: %4")
? Loc::text("launch.error.noVersionsInstalled").arg(versionsDir)
: Loc::text("launch.error.versionNotInstalled")
.arg(id,
entry.name,
versionsDir,
@@ -1150,7 +1150,7 @@ void LauncherBackend::launchGame(int profileIndex, int buildIndex)
}
setBusy(true);
emit launchProgress(tr("Чтение описания версии «%1»…").arg(id));
emit launchProgress(Loc::text("launch.progress.readingVersion").arg(id));
QSet<QString> features;
if (settingInt(m_settings, QStringLiteral("windowWidth"), 0) > 0
@@ -1160,7 +1160,7 @@ void LauncherBackend::launchGame(int profileIndex, int buildIndex)
QString error;
m_pendingVersionData = VersionLoader::load(directory, id, features, &error);
if (!m_pendingVersionData.isValid()) {
failLaunch(error.isEmpty() ? tr("Не удалось прочитать версию «%1»").arg(id) : error);
failLaunch(error.isEmpty() ? Loc::text("common.error.versionReadFailed").arg(id) : error);
return;
}
@@ -1169,7 +1169,7 @@ void LauncherBackend::launchGame(int profileIndex, int buildIndex)
probe.versionId = id;
const QStringList missing = GameLauncher::missingFiles(probe, m_pendingVersionData);
if (!missing.isEmpty()) {
failLaunch(tr("В папке игры не хватает файлов (%1):\n%2")
failLaunch(Loc::text("launch.error.gameFilesMissing")
.arg(missing.size())
.arg(missing.join(u'\n')));
return;
@@ -1186,14 +1186,13 @@ void LauncherBackend::launchGame(int profileIndex, int buildIndex)
void LauncherBackend::startMicrosoftLogin(int profileIndex)
{
if (!microsoftAvailable()) {
emit microsoftLoginFailed(tr("Эта сборка лаунчера собрана без Qt WebEngine — "
"показать окно входа Microsoft нечем"));
emit microsoftLoginFailed(Loc::text("auth.msa.error.noWebEngine"));
return;
}
// -1 из QML означает «профиля ещё нет»; внутри держим -2, чтобы отличать
// это состояние от закрытого окна.
if (profileIndex >= m_profiles.size()) {
emit microsoftLoginFailed(tr("Профиль не найден"));
emit microsoftLoginFailed(Loc::text("profile.error.notFound"));
return;
}
m_msaLoginProfile = profileIndex < 0 ? -2 : profileIndex;
@@ -1218,7 +1217,7 @@ QVariantMap LauncherBackend::inspectMicrosoftRedirect(const QString &url) const
void LauncherBackend::finishMicrosoftLogin(const QString &code)
{
if (m_msaLoginProfile == -1) {
emit microsoftLoginFailed(tr("Вход в Microsoft уже отменён"));
emit microsoftLoginFailed(Loc::text("auth.msa.status.alreadyCancelled"));
return;
}
@@ -1249,7 +1248,7 @@ void LauncherBackend::finishMicrosoftLogin(const QString &code)
profile.authType = kMicrosoft;
applyMicrosoftResult(profile, result);
} else {
emit microsoftLoginFailed(tr("Профиль был удалён, пока шёл вход"));
emit microsoftLoginFailed(Loc::text("profile.error.removedDuringLogin"));
return;
}
@@ -1283,7 +1282,7 @@ void LauncherBackend::beginMicrosoftAuthentication()
if (profile.refreshToken.isEmpty()) {
const int index = m_pendingProfile;
failLaunch(tr("Профиль «%1» не подключён к аккаунту Microsoft — войдите в него")
failLaunch(Loc::text("auth.msa.error.profileNeedsLogin")
.arg(profile.name));
emit microsoftReloginRequired(index);
return;
@@ -1316,7 +1315,7 @@ void LauncherBackend::beginMicrosoftAuthentication()
}
if (!result.ok) {
const int index = m_pendingProfile;
failLaunch(tr("Сессия Microsoft истекла — войдите в аккаунт заново (%1)")
failLaunch(Loc::text("auth.msa.error.sessionExpired")
.arg(result.error));
emit microsoftReloginRequired(index);
return;
@@ -1382,11 +1381,11 @@ void LauncherBackend::beginAuthentication()
void LauncherBackend::submitTwoFactorCode(const QString &code)
{
if (m_pendingProfile < 0 || m_pendingProfile >= m_profiles.size()) {
emit launchError(tr("Запуск уже отменён"));
emit launchError(Loc::text("launch.status.alreadyCancelled"));
return;
}
if (code.trimmed().isEmpty()) {
emit launchError(tr("Введите код двухфакторной аутентификации"));
emit launchError(Loc::text("auth.ely.error.enterCode"));
return;
}
@@ -1418,13 +1417,13 @@ void LauncherBackend::cancelPendingLaunch()
void LauncherBackend::continueLaunch(const AuthResult &auth)
{
if (m_pendingProfile < 0 || m_pendingProfile >= m_profiles.size() || m_pendingBuild < 0) {
failLaunch(tr("Запуск был отменён"));
failLaunch(Loc::text("launch.status.cancelled"));
return;
}
const int buildIndex = indexOfBuildId(m_pendingBuild);
if (buildIndex < 0) {
failLaunch(tr("Сборка была удалена, пока шла авторизация"));
failLaunch(Loc::text("build.error.removedDuringAuth"));
return;
}
@@ -1464,8 +1463,7 @@ void LauncherBackend::continueLaunch(const AuthResult &auth)
options.javaPath = managedJavaPath();
if (options.javaPath.isEmpty()) {
if (!m_settings.value(QStringLiteral("javaRuntime")).toString().trimmed().isEmpty())
emit launchProgress(tr("Выбранная сборка Java не найдена в папке лаунчера — "
"ищем установленную в системе"));
emit launchProgress(Loc::text("java.warning.runtimeMissing"));
options.javaPath = m_settings.value(QStringLiteral("javaPath")).toString();
}
options.minMemoryMb = settingInt(m_settings, QStringLiteral("minMemoryMb"), 512);
@@ -1573,10 +1571,20 @@ QVariantMap LauncherBackend::settings() const
void LauncherBackend::updateSettings(const QVariantMap &values)
{
const QString previousGameDir = gameDir();
const QString previousLanguage = m_settings.value(QStringLiteral("language")).toString();
for (auto it = values.cbegin(); it != values.cend(); ++it)
m_settings.insert(it.key(), it.value());
saveSettings();
emit settingsChanged();
// Язык переключается после записи: порядок «сохранили → переключили»
// гарантирует, что выбор переживёт падение сразу после переключения.
// Сам Localization в settings.json не пишет — файл ведёт только бэкенд.
const QString language = m_settings.value(QStringLiteral("language")).toString();
if (language != previousLanguage)
Localization::instance().setLanguage(language);
if (gameDir() != previousGameDir) {
invalidateCatalog();
emit installedVersionsChanged();
@@ -1673,9 +1681,9 @@ void LauncherBackend::rebuildJavaCatalog() const
listed.insert(runtime.id);
const auto remote = m_javaMeta->find(runtime.id);
const QString detail = remote ? (remote->kind == JavaRuntimeKind::Mojang
? tr("Mojang · %1").arg(remote->component)
: tr("Temurin · %1").arg(remote->architecture))
: tr("скачана лаунчером");
? Loc::text("java.label.mojang").arg(remote->component)
: Loc::text("java.label.temurin").arg(remote->architecture))
: Loc::text("java.label.downloadedByLauncher");
m_javaCatalog.append(makeRow(runtime.id,
runtime.kind,
runtime.major,
@@ -1695,8 +1703,8 @@ void LauncherBackend::rebuildJavaCatalog() const
if (listed.contains(entry.id))
continue;
const QString detail = kind == JavaRuntimeKind::Mojang
? tr("Mojang · %1").arg(entry.component)
: tr("Temurin · %1").arg(entry.architecture);
? Loc::text("java.label.mojang").arg(entry.component)
: Loc::text("java.label.temurin").arg(entry.architecture);
m_javaCatalog.append(makeRow(entry.id,
javaKindKey(kind),
entry.major,
@@ -1713,18 +1721,18 @@ void LauncherBackend::rebuildJavaCatalog() const
void LauncherBackend::installJavaRuntime(const QString &runtimeId)
{
if (m_javaInstaller->isRunning()) {
emit launchError(tr("Идёт установка Java «%1» — дождитесь окончания")
emit launchError(Loc::text("java.error.installBusy")
.arg(m_javaInstaller->label()));
return;
}
if (m_installer->isRunning() || m_loaderInstaller->isRunning() || m_switcher->isRunning()) {
emit launchError(tr("Дождитесь окончания текущей операции"));
emit launchError(Loc::text("common.error.busyOperation"));
return;
}
const auto entry = m_javaMeta->find(runtimeId);
if (!entry) {
emit launchError(tr("Сборка Java «%1» не найдена в каталоге").arg(runtimeId));
emit launchError(Loc::text("java.error.buildNotInCatalog").arg(runtimeId));
return;
}
m_javaInstaller->install(*entry);
@@ -1733,13 +1741,13 @@ void LauncherBackend::installJavaRuntime(const QString &runtimeId)
void LauncherBackend::removeJavaRuntime(const QString &runtimeId)
{
if (m_javaInstaller->isRunning() && m_javaInstaller->runtimeId() == runtimeId) {
emit launchError(tr("Эта сборка Java сейчас скачивается"));
emit launchError(Loc::text("java.error.currentlyDownloading"));
return;
}
QString error;
if (!JavaRuntimeStore::remove(runtimeId, &error)) {
emit launchError(tr("Не удалось удалить сборку Java: %1").arg(error));
emit launchError(Loc::text("java.error.removeFailed").arg(error));
return;
}
@@ -1750,7 +1758,7 @@ void LauncherBackend::removeJavaRuntime(const QString &runtimeId)
emit settingsChanged();
}
invalidateJavaCatalog();
emit launchProgress(tr("Сборка Java удалена"));
emit launchProgress(Loc::text("java.status.removed"));
}
QVariantMap LauncherBackend::javaRuntimeInfo(const QString &runtimeId) const
@@ -1851,11 +1859,11 @@ void LauncherBackend::rebuildSeasonalCatalog() const
QString status;
if (!installed)
status = tr("Не установлена");
status = Loc::text("seasonal.status.notInstalled");
else if (updateAvailable)
status = tr("Есть обновление");
status = Loc::text("seasonal.status.updateAvailable");
else
status = tr("Установлена");
status = Loc::text("seasonal.status.installed");
const auto loader = loaderFromKey(entry.loader);
m_seasonalCatalog.append(QVariantMap{
@@ -1863,7 +1871,7 @@ void LauncherBackend::rebuildSeasonalCatalog() const
{QStringLiteral("name"), entry.name},
{QStringLiteral("minecraftVersion"), entry.minecraftVersion},
{QStringLiteral("loader"), entry.loader},
{QStringLiteral("loaderTitle"), loader ? loaderTitle(*loader) : tr("Ванильная")},
{QStringLiteral("loaderTitle"), loader ? loaderTitle(*loader) : Loc::text("loader.label.vanilla")},
{QStringLiteral("loaderVersion"), entry.loaderVersion},
{QStringLiteral("modCount"), entry.modCount},
{QStringLiteral("seasonStart"), formatSeasonDate(entry.seasonStart)},
@@ -1914,21 +1922,21 @@ void LauncherBackend::refreshSeasonalCatalog(bool force)
void LauncherBackend::installSeasonalBuild(const QString &seasonalId)
{
if (m_season.stage != SeasonStage::Idle) {
emit launchError(tr("Установка сборки уже идёт"));
emit launchError(Loc::text("seasonal.error.installInProgress"));
return;
}
if (m_launcher->isRunning()) {
emit launchError(tr("Сначала закройте игру — её файлы сейчас переносятся"));
emit launchError(Loc::text("build.error.closeGameFilesMoving"));
return;
}
if (busy()) {
emit launchError(tr("Дождитесь окончания текущей операции"));
emit launchError(Loc::text("common.error.busyOperation"));
return;
}
const auto entry = m_seasonalMeta->find(seasonalId);
if (!entry) {
emit launchError(tr("Сборка «%1» не найдена в списке — обновите его").arg(seasonalId));
emit launchError(Loc::text("seasonal.error.notInCatalog").arg(seasonalId));
return;
}
@@ -1966,7 +1974,7 @@ void LauncherBackend::installSeasonalBuild(const QString &seasonalId)
m_season.buildId = m_builds[index].id;
setBusy(true);
emit launchProgress(tr("Установка сборки «%1»…").arg(entry->name));
emit launchProgress(Loc::text("seasonal.progress.installing").arg(entry->name));
// Сигнал после seasonAdvance(): шаг цепочки выставляет stage сам, а до его
// вызова seasonalInstalling() ещё вернул бы false.
seasonAdvance();
@@ -1976,7 +1984,7 @@ void LauncherBackend::installSeasonalBuild(const QString &seasonalId)
void LauncherBackend::seasonAdvance()
{
if (indexOfBuildId(m_season.buildId) < 0) {
seasonFail(tr("запись сборки исчезла"));
seasonFail(Loc::text("seasonal.error.entryGone"));
return;
}
@@ -2021,7 +2029,7 @@ bool LauncherBackend::seasonBeginSwitch()
// должна стать активной до всего остального. Содержимое прежней уезжает в
// её архив обычным путём.
m_season.stage = SeasonStage::Switching;
emit launchProgress(tr("Смена сборки на «%1»…").arg(m_builds[index].name));
emit launchProgress(Loc::text("build.progress.switchingTo").arg(m_builds[index].name));
m_switcher->switchTo(current >= 0 ? m_builds[current].id : -1,
current >= 0 ? m_builds[current].name : QString(),
m_builds[index].id,
@@ -2047,19 +2055,19 @@ bool LauncherBackend::seasonBeginJava()
return false;
m_season.stage = SeasonStage::Java;
emit launchProgress(tr("Для сборки нужна Java %1 — устанавливаем").arg(major));
emit launchProgress(Loc::text("seasonal.progress.installingJava").arg(major));
// Каталог Java мог ещё ни разу не читаться: окно выбора могли не открывать.
m_javaMeta->ensureLoaded([this, major](bool ok, const QString &warning) {
if (m_season.stage != SeasonStage::Java)
return;
if (!ok) {
seasonFail(warning.isEmpty() ? tr("не удалось получить каталог Java") : warning);
seasonFail(warning.isEmpty() ? Loc::text("seasonal.error.javaCatalogFailed") : warning);
return;
}
const auto entry = m_javaMeta->bestFor(major);
if (!entry) {
seasonFail(tr("в каталоге нет сборки Java %1 для этой системы").arg(major));
seasonFail(Loc::text("seasonal.error.noJavaBuild").arg(major));
return;
}
m_javaInstaller->install(*entry);
@@ -2119,7 +2127,7 @@ bool LauncherBackend::seasonBeginDownload()
bool LauncherBackend::seasonBeginApply()
{
if (m_season.packPath.isEmpty()) {
seasonFail(tr("файлы сборки не скачаны"));
seasonFail(Loc::text("seasonal.error.filesNotDownloaded"));
return true;
}
@@ -2157,7 +2165,7 @@ void LauncherBackend::seasonFinish()
setBusy(false);
invalidateSeasonalCatalog();
emit seasonalInstallingChanged();
emit launchProgress(tr("Сборка «%1» установлена").arg(name));
emit launchProgress(Loc::text("seasonal.status.buildInstalled").arg(name));
emit seasonalInstallFinished(seasonalId, name);
}
@@ -2176,8 +2184,8 @@ void LauncherBackend::seasonFail(const QString &message)
// Пустое message означает, что о причине уже сообщил тот, кто её нашёл, —
// второй раз ту же ошибку не показываем.
emit launchError(message.isEmpty()
? tr("Установка сборки «%1» прервана").arg(name)
: tr("Установка сборки «%1» прервана: %2").arg(name, message));
? Loc::text("seasonal.error.installAborted").arg(name)
: Loc::text("seasonal.error.installAbortedWhy").arg(name, message));
}
void LauncherBackend::cancelSeasonalInstall()
@@ -2201,7 +2209,7 @@ void LauncherBackend::cancelSeasonalInstall()
case SeasonStage::Applying:
// На этих шагах содержимое .minecraft уже разобрано: отступать некуда,
// операцию нужно довести до конца.
emit launchError(tr("Сейчас переносятся файлы — отменить нельзя"));
emit launchError(Loc::text("switch.error.cannotCancel"));
return;
}
}
@@ -2267,7 +2275,7 @@ void LauncherBackend::openMinecraftFolder()
QDir().mkpath(modsDir);
if (!QDesktopServices::openUrl(QUrl::fromLocalFile(modsDir)))
emit launchError(tr("Не удалось открыть папку с модами"));
emit launchError(Loc::text("launch.error.openModsFailed"));
}
void LauncherBackend::openGameFolder()
@@ -2276,7 +2284,7 @@ void LauncherBackend::openGameFolder()
QDir().mkpath(directory);
if (!QDesktopServices::openUrl(QUrl::fromLocalFile(directory)))
emit launchError(tr("Не удалось открыть папку игры"));
emit launchError(Loc::text("launch.error.openGameDirFailed"));
}
// Повреждённый файл не удаляем: отводим в .bak, чтобы данные можно было спасти
@@ -2286,12 +2294,12 @@ bool LauncherBackend::moveAsideBroken(const QString &path, const QString &reason
const QString backup = path + QStringLiteral(".bak");
QFile::remove(backup);
if (!QFile::rename(path, backup)) {
m_storageIssues << tr("Файл %1 повреждён (%2), и его не удалось отложить в сторону")
m_storageIssues << Loc::text("storage.error.corruptNotSetAside")
.arg(QDir::toNativeSeparators(path), reason);
return false;
}
m_storageIssues << tr("Файл %1 повреждён (%2). Копия сохранена как %3, файл создан заново.")
m_storageIssues << Loc::text("storage.warning.corruptRecreated")
.arg(QDir::toNativeSeparators(path),
reason,
QDir::toNativeSeparators(backup));
@@ -2304,7 +2312,7 @@ QJsonArray LauncherBackend::readArrayFile(const QString &path)
if (!file.exists())
return {}; // первый запуск — это не ошибка
if (!file.open(QIODevice::ReadOnly)) {
m_storageIssues << tr("Не удалось прочитать %1: %2")
m_storageIssues << Loc::text("common.error.readFailed")
.arg(QDir::toNativeSeparators(path), file.errorString());
return {};
}
@@ -2318,7 +2326,7 @@ QJsonArray LauncherBackend::readArrayFile(const QString &path)
}
if (!document.isArray()) {
file.close();
moveAsideBroken(path, tr("ожидался список записей"));
moveAsideBroken(path, Loc::text("storage.error.expectedArray"));
return {};
}
return document.array();
@@ -2330,7 +2338,7 @@ QJsonObject LauncherBackend::readObjectFile(const QString &path)
if (!file.exists())
return {};
if (!file.open(QIODevice::ReadOnly)) {
m_storageIssues << tr("Не удалось прочитать %1: %2")
m_storageIssues << Loc::text("common.error.readFailed")
.arg(QDir::toNativeSeparators(path), file.errorString());
return {};
}
@@ -2344,7 +2352,7 @@ QJsonObject LauncherBackend::readObjectFile(const QString &path)
}
if (!document.isObject()) {
file.close();
moveAsideBroken(path, tr("ожидался набор настроек"));
moveAsideBroken(path, Loc::text("storage.error.expectedObject"));
return {};
}
return document.object();
@@ -2356,14 +2364,14 @@ bool LauncherBackend::writeJsonFile(const QString &path, const QJsonDocument &do
{
QString storageError;
if (!LauncherPaths::ensureRootExists(&storageError)) {
emit launchError(tr("Не удалось создать папку лаунчера: %1").arg(storageError));
emit launchError(Loc::text("storage.error.launcherDirFailed").arg(storageError));
return false;
}
const QByteArray payload = document.toJson();
QFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()) {
emit launchError(tr("Не удалось сохранить %1: %2")
emit launchError(Loc::text("storage.error.saveFailedWhy")
.arg(QDir::toNativeSeparators(path), file.errorString()));
return false;
}
@@ -2429,7 +2437,7 @@ void LauncherBackend::loadData()
m_nextBuildId = build.id + 1;
});
if (skippedBuilds > 0) {
m_storageIssues << tr("В %1 пропущены сборки без имени и версии Minecraft: %2")
m_storageIssues << Loc::text("storage.warning.buildsSkipped")
.arg(QDir::toNativeSeparators(LauncherPaths::customBuildsFile()))
.arg(skippedBuilds);
}
@@ -2472,7 +2480,7 @@ bool LauncherBackend::migrateLegacyBuildsFile()
const QString retired = LauncherPaths::legacyCustomBuildsFile() + QStringLiteral(".migrated");
QFile::remove(retired);
if (!QFile::rename(LauncherPaths::legacyCustomBuildsFile(), retired)) {
m_storageIssues << tr("Сборки перенесены в %1, но старый файл %2 удалить не удалось")
m_storageIssues << Loc::text("storage.warning.migrationLeftover")
.arg(QDir::toNativeSeparators(LauncherPaths::customBuildsFile()),
QDir::toNativeSeparators(
LauncherPaths::legacyCustomBuildsFile()));
@@ -2542,6 +2550,8 @@ void LauncherBackend::loadSettings()
{QStringLiteral("windowWidth"), 0},
{QStringLiteral("windowHeight"), 0},
{QStringLiteral("fullscreen"), false},
// Язык интерфейса: "system" — по системной локали.
{QStringLiteral("language"), QStringLiteral("system")},
{QStringLiteral("launcherName"), QStringLiteral("KishkaLauncher")},
{QStringLiteral("launcherVersion"), QStringLiteral("1.0")},
{QStringLiteral("activeBuildId"), 0},