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
+28 -28
View File
@@ -87,14 +87,14 @@ Dialog {
const problems = buildsDialog.backend.checkInstallation(buildsDialog.editIndex)
bdStatus.ok = problems.length === 0
bdStatus.text = problems.length === 0
? qsTr("Всё на месте, сборку можно запускать")
: qsTr("Не хватает файлов (%1): %2").arg(problems.length).arg(problems[0])
? Loc.t.build.status.ready
: Loc.t.build.error.missingFiles.arg(problems.length).arg(problems[0])
}
function newBuildName() {
const taken = buildsDialog.backend.customBuildNames
for (var n = 1; ; ++n) {
const candidate = qsTr("Сборка %1").arg(n)
const candidate = Loc.t.build.title.numbered.arg(n)
if (taken.indexOf(candidate) === -1)
return candidate
}
@@ -128,7 +128,7 @@ Dialog {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: qsTr("Удалить сборку?")
text: Loc.t.build.title.removeConfirm
color: "#ffffff"
font.pixelSize: 17
font.bold: true
@@ -152,7 +152,7 @@ Dialog {
wrapMode: Text.Wrap
color: "#ffffff"
font.pixelSize: 13
text: qsTr("Сборка «%1» будет удалена без возможности восстановления.")
text: Loc.t.build.hint.removeIrreversible
.arg(removeConfirm.buildName)
}
@@ -163,7 +163,7 @@ Dialog {
wrapMode: Text.Wrap
color: "#cc6666"
font.pixelSize: 12
text: qsTr("Вместе с ней удалится её архив: моды, конфиги и миры этой сборки восстановить будет нельзя.")
text: Loc.t.build.warning.archiveGoesToo
}
Text {
@@ -173,7 +173,7 @@ Dialog {
wrapMode: Text.Wrap
color: "#cc6666"
font.pixelSize: 12
text: qsTr("Сборка сейчас активна: содержимое .minecraft принадлежит ей и будет очищено, а на его место развернётся следующая сборка.")
text: Loc.t.build.warning.removingActive
}
Text {
@@ -183,7 +183,7 @@ Dialog {
wrapMode: Text.Wrap
color: "#888888"
font.pixelSize: 12
text: qsTr("Это последняя сборка. Содержимое .minecraft останется на месте.")
text: Loc.t.build.hint.removingLast
}
}
@@ -200,7 +200,7 @@ Dialog {
spacing: 12
Button {
text: qsTr("Отмена")
text: Loc.t.common.button.cancel
width: 130; height: 36
contentItem: Text {
text: parent.text
@@ -216,7 +216,7 @@ Dialog {
}
Button {
text: qsTr("Удалить")
text: Loc.t.common.button.remove
width: 130; height: 36
contentItem: Text {
text: parent.text
@@ -271,7 +271,7 @@ Dialog {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: qsTr("Пользовательские сборки")
text: Loc.t.common.title.customBuilds
color: "#ffffff"
font.pixelSize: 17
font.bold: true
@@ -330,7 +330,7 @@ Dialog {
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
visible: buildRow.index === buildsDialog.backend.activeBuildIndex
text: qsTr("активна")
text: Loc.t.build.label.active
color: "#91B315"
font.pixelSize: 10
}
@@ -380,7 +380,7 @@ Dialog {
Text {
anchors.centerIn: parent
text: qsTr("+ Новая сборка")
text: Loc.t.build.button.new
color: "#91B315"
font.pixelSize: 12
}
@@ -425,21 +425,21 @@ Dialog {
LabelledField {
id: bdName
width: parent.width
label: qsTr("Название сборки")
placeholder: qsTr("как её видно в списке")
label: Loc.t.build.label.name
placeholder: Loc.t.build.placeholder.name
onEditingFinished: buildsDialog.commit({"name": bdName.text.trim()})
}
LabelledField {
id: bdServer
width: parent.width
label: qsTr("Адрес сервера")
placeholder: qsTr("host:port (необязательно)")
label: Loc.t.build.label.serverAddress
placeholder: Loc.t.build.placeholder.serverAddress
onEditingFinished: buildsDialog.commit({"serverUrl": bdServer.text.trim()})
}
Text {
text: qsTr("Версия Minecraft")
text: Loc.t.common.label.minecraftVersion
color: "#aaaaaa"
font.pixelSize: 11
}
@@ -464,7 +464,7 @@ Dialog {
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
color: bdMinecraft.selectedId === "" ? "#666666" : "#ffffff"
text: bdMinecraft.selectedId === "" ? qsTr("Выберите версию")
text: bdMinecraft.selectedId === "" ? Loc.t.build.button.pickVersion
: bdMinecraft.selectedId
}
@@ -472,7 +472,7 @@ Dialog {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Выбрать…")
text: Loc.t.common.button.choose
color: "#91B315"
font.pixelSize: 12
}
@@ -489,7 +489,7 @@ Dialog {
}
Text {
text: qsTr("Модлоадер")
text: Loc.t.build.label.modLoader
color: "#aaaaaa"
font.pixelSize: 11
}
@@ -541,7 +541,7 @@ Dialog {
width: parent.width
backend: buildsDialog.backend
loaderKey: "forge"
title: qsTr("Minecraft Forge")
title: Loc.t.loader.name.forge
gameVersion: bdMinecraft.selectedId
onUserChecked: loaderPanel.keepOnly(bdForge)
onChanged: loaderPanel.commitSelection()
@@ -551,7 +551,7 @@ Dialog {
width: parent.width
backend: buildsDialog.backend
loaderKey: "fabric"
title: qsTr("Fabric Loader")
title: Loc.t.loader.name.fabric
gameVersion: bdMinecraft.selectedId
onUserChecked: loaderPanel.keepOnly(bdFabric)
onChanged: loaderPanel.commitSelection()
@@ -561,7 +561,7 @@ Dialog {
width: parent.width
backend: buildsDialog.backend
loaderKey: "neoforge"
title: qsTr("NeoForge")
title: Loc.t.loader.name.neoforge
gameVersion: bdMinecraft.selectedId
onUserChecked: loaderPanel.keepOnly(bdNeoForge)
onChanged: loaderPanel.commitSelection()
@@ -571,7 +571,7 @@ Dialog {
width: parent.width
backend: buildsDialog.backend
loaderKey: "quilt"
title: qsTr("Quilt Loader")
title: Loc.t.loader.name.quilt
gameVersion: bdMinecraft.selectedId
onUserChecked: loaderPanel.keepOnly(bdQuilt)
onChanged: loaderPanel.commitSelection()
@@ -604,7 +604,7 @@ Dialog {
spacing: 12
Button {
text: qsTr("Установить")
text: Loc.t.common.button.install
width: 140; height: 36
enabled: buildsDialog.editIndex >= 0 && !buildsDialog.backend.busy
opacity: enabled ? 1.0 : 0.45
@@ -622,7 +622,7 @@ Dialog {
}
Button {
text: qsTr("Сделать активной")
text: Loc.t.build.button.makeActive
width: 170; height: 36
enabled: buildsDialog.editIndex >= 0
&& buildsDialog.editIndex !== buildsDialog.backend.activeBuildIndex
@@ -642,7 +642,7 @@ Dialog {
}
Button {
text: qsTr("Закрыть")
text: Loc.t.common.button.close
width: 120; height: 36
enabled: !buildsDialog.backend.switching
opacity: enabled ? 1.0 : 0.45
+3 -1
View File
@@ -40,6 +40,8 @@ qt_add_executable(appMinecraft_launcher
qt_add_qml_module(appMinecraft_launcher
URI Minecraft_launcher
SOURCES
localization.h
localization.cpp
launcherbackend.h
launcherbackend.cpp
launcherpaths.h
@@ -90,7 +92,7 @@ qt_add_qml_module(appMinecraft_launcher
LoaderRow.qml
ProgressPanel.qml
${LAUNCHER_MSA_QML_FILES}
RESOURCES images/Folder/Folder_Active.svg images/Folder/Folder_Idle.svg images/Folder/Folder_Pressed.svg images/Options/Options_active.svg images/Options/Options_Idle.svg images/Options/Options_Pressed.svg images/Play_Button/Play_Active.svg images/Play_Button/Play_Idle.svg images/Play_Button/Play_pressed.svg images/Profile_Box/Asset_23.jpg images/Profile_Box/Asset_23.svg images/Profile_Box/Asset_24.jpg images/Profile_Box/Asset_24.svg images/Profile_Box/Kishka_Profile_Active.svg images/Profile_Box/Kishka_Profile_Idle.svg images/Profile_Box/Kishka_Profile_open.svg images/Scroll/Scroll_Lever.svg images/Scroll/Scroll_palka.svg images/Version_box/Kishka_Profile_Open.svg images/Version_box/Kishka_Ver_Active.svg images/Version_box/Kishka_Ver_idle.svg images/Trash.svg images/Pencil.svg images/GovuztTW8AAHqBf.jpeg images/photo_2025-12-16_15-04-17.jpg images/photo_2025-12-21_02-30-09.jpg
RESOURCES images/Folder/Folder_Active.svg images/Folder/Folder_Idle.svg images/Folder/Folder_Pressed.svg images/Options/Options_active.svg images/Options/Options_Idle.svg images/Options/Options_Pressed.svg images/Play_Button/Play_Active.svg images/Play_Button/Play_Idle.svg images/Play_Button/Play_pressed.svg images/Profile_Box/Asset_23.jpg images/Profile_Box/Asset_23.svg images/Profile_Box/Asset_24.jpg images/Profile_Box/Asset_24.svg images/Profile_Box/Kishka_Profile_Active.svg images/Profile_Box/Kishka_Profile_Idle.svg images/Profile_Box/Kishka_Profile_open.svg images/Scroll/Scroll_Lever.svg images/Scroll/Scroll_palka.svg images/Version_box/Kishka_Profile_Open.svg images/Version_box/Kishka_Ver_Active.svg images/Version_box/Kishka_Ver_idle.svg images/Trash.svg images/Pencil.svg images/GovuztTW8AAHqBf.jpeg images/photo_2025-12-16_15-04-17.jpg images/photo_2025-12-21_02-30-09.jpg i18n/translations.json
)
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
+16 -16
View File
@@ -38,9 +38,9 @@ Dialog {
}
readonly property var categories: [
{ key: "java", title: qsTr("Java"), hint: qsTr("сборки Mojang") },
{ key: "jdk", title: qsTr("JDK"), hint: qsTr("Temurin, с инструментами") },
{ key: "jre", title: qsTr("JRE"), hint: qsTr("Temurin, только запуск") }
{ key: "java", title: Loc.t.common.java.kind.java, hint: Loc.t.java.filter.mojang },
{ key: "jdk", title: Loc.t.common.java.kind.jdk, hint: Loc.t.java.filter.temurinJdk },
{ key: "jre", title: Loc.t.common.java.kind.jre, hint: Loc.t.java.filter.temurinJre }
]
// Каталог приходит из C++ уже отсортированным (новые сверху, скачанные
@@ -122,7 +122,7 @@ Dialog {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: qsTr("Версия Java")
text: Loc.t.java.label.version
color: "#ffffff"
font.pixelSize: 17
font.bold: true
@@ -203,7 +203,7 @@ Dialog {
wrapMode: Text.Wrap
color: "#888888"
font.pixelSize: 10
text: qsTr("Выбранной версии игры нужна Java %1 или новее")
text: Loc.t.java.hint.required
.arg(picker.requiredMajor)
}
}
@@ -227,7 +227,7 @@ Dialog {
anchors.right: parent.right
anchors.margins: 8
height: 32
placeholderText: qsTr("Поиск версии…")
placeholderText: Loc.t.common.placeholder.searchVersion
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
@@ -281,7 +281,7 @@ Dialog {
}
contentItem: Text {
leftPadding: installedOnlyBox.indicator.width + 8
text: qsTr("Только скачанные")
text: Loc.t.java.filter.downloadedOnly
color: "#aaaaaa"
font.pixelSize: 11
verticalAlignment: Text.AlignVCenter
@@ -364,9 +364,9 @@ Dialog {
const parts = [runtimeRow.modelData.detail,
runtimeRow.modelData.coverage]
if (runtimeRow.modelData.sizeMb > 0)
parts.push(qsTr("%1 МБ").arg(runtimeRow.modelData.sizeMb))
parts.push(Loc.t.common.status.megabytes.arg(runtimeRow.modelData.sizeMb))
if (runtimeRow.tooOld)
parts.push(qsTr("не хватит для выбранной версии игры"))
parts.push(Loc.t.java.warning.tooOldForVersion)
return parts.join(" · ")
}
}
@@ -441,10 +441,10 @@ Dialog {
font.pixelSize: 12
font.italic: true
text: picker.backend.javaCatalogLoading
? qsTr("Загрузка списка сборок Java…")
? Loc.t.java.progress.loadingCatalog
: picker.backend.javaCatalog.length === 0
? qsTr("Список сборок Java недоступен — проверьте соединение")
: qsTr("Ничего не найдено")
? Loc.t.java.error.catalogUnavailable
: Loc.t.common.status.nothingFound
}
}
}
@@ -467,10 +467,10 @@ Dialog {
color: picker.selectedEntry === null ? "#666666" : "#91B315"
font.pixelSize: 13
text: picker.selectedEntry === null
? qsTr("Сборка не выбрана")
? Loc.t.common.status.noBuildSelected
: picker.selectedEntry.installed
? picker.selectedEntry.label
: qsTr("%1 — будет скачана").arg(picker.selectedEntry.label)
: Loc.t.java.status.willDownload.arg(picker.selectedEntry.label)
}
Row {
@@ -480,7 +480,7 @@ Dialog {
spacing: 12
Button {
text: qsTr("Отмена")
text: Loc.t.common.button.cancel
width: 110; height: 36
contentItem: Text {
text: parent.text
@@ -497,7 +497,7 @@ Dialog {
Button {
text: picker.selectedEntry !== null && !picker.selectedEntry.installed
? qsTr("Скачать") : qsTr("Выбрать")
? Loc.t.common.button.download : Loc.t.common.button.select
width: 110; height: 36
enabled: picker.selectedEntry !== null
opacity: enabled ? 1.0 : 0.45
+2 -2
View File
@@ -169,8 +169,8 @@ Column {
color: loaderRow.backend.loaderVersionsLoading(loaderRow.loaderKey, loaderRow.gameVersion)
? "#888888" : "#cc6666"
text: loaderRow.backend.loaderVersionsLoading(loaderRow.loaderKey, loaderRow.gameVersion)
? qsTr("Загрузка списка версий %1…").arg(loaderRow.title)
: qsTr("%1 не поддерживает Minecraft %2").arg(loaderRow.title)
? Loc.t.loader.progress.loadingVersions.arg(loaderRow.title)
: Loc.t.loader.status.unsupported.arg(loaderRow.title)
.arg(loaderRow.gameVersion)
}
}
+90 -60
View File
@@ -9,14 +9,14 @@ Window {
height: 720
visible: true
flags: Qt.Window
title: qsTr("Minecraft Launcher")
title: Loc.t.app.title
// ── Backend ────────────────────────────────────────────────────────────
LauncherBackend {
id: backend
onLaunched: (profileName, buildName, serverUrl) => {
window.showToast(qsTr("Запущено: %1 — %2").arg(profileName).arg(buildName), "#4b7a1f")
window.showToast(Loc.t.launch.status.started.arg(profileName).arg(buildName), "#4b7a1f")
}
onLaunchProgress: (message) => window.showToast(message, "#3a5a8c", 0)
onLaunchError: (message) => window.showToast(message, "#cc3333", 8000)
@@ -27,20 +27,20 @@ Window {
}
onGameFinished: (exitCode, crashed) => {
window.showToast(crashed || exitCode !== 0
? qsTr("Игра завершилась с ошибкой (код %1)").arg(exitCode)
: qsTr("Игра закрыта"),
? Loc.t.game.status.crashed.arg(exitCode)
: Loc.t.game.status.closed,
crashed || exitCode !== 0 ? "#cc3333" : "#555555")
}
onMicrosoftLoginUrlReady: (url) => window.openMicrosoftLogin(url)
// Выбор в списке здесь не трогаем: новый профиль уже выбран тем, кто
// его создал, а повторный вход мог быть и не в последний профиль.
onMicrosoftLoginSucceeded: (playerName) =>
window.showToast(qsTr("Вход выполнен: %1").arg(playerName), "#4b7a1f")
window.showToast(Loc.t.auth.status.signedInAs.arg(playerName), "#4b7a1f")
onMicrosoftLoginFailed: (message) => window.showToast(message, "#cc3333", 8000)
onMicrosoftReloginRequired: (profileIndex) => backend.startMicrosoftLogin(profileIndex)
onGameOutput: (line) => console.log(line)
onSeasonalInstallFinished: (seasonalId, buildName) => {
window.showToast(qsTr("Сборка «%1» установлена — можно запускать").arg(buildName),
window.showToast(Loc.t.seasonal.status.readyToLaunch.arg(buildName),
"#4b7a1f", 8000)
}
}
@@ -69,7 +69,7 @@ Window {
const component = Qt.createComponent("MicrosoftLoginDialog.qml")
if (component.status !== Component.Ready) {
backend.cancelMicrosoftLogin()
window.showToast(qsTr("Окно входа Microsoft недоступно: сборка без Qt WebEngine"),
window.showToast(Loc.t.auth.msa.error.windowUnavailable,
"#cc3333", 8000)
return
}
@@ -134,11 +134,11 @@ Window {
anchors.leftMargin: 24
anchors.bottomMargin: 24
title: qsTr("Загрузка %1").arg(backend.downloadVersion)
title: Loc.t.common.progress.downloadingNamed.arg(backend.downloadVersion)
fraction: backend.downloadProgress
status: backend.downloadStatus
detail: backend.downloadBytesTotal > 0
? qsTr("%1 — %2 / %3 МБ").arg(backend.downloadStatus)
? Loc.t.common.progress.bytes.arg(backend.downloadStatus)
.arg(window.formatMb(backend.downloadBytesDone))
.arg(window.formatMb(backend.downloadBytesTotal))
: ""
@@ -201,8 +201,8 @@ Window {
// висела бы поверх собственного меню.
ToolTip.visible: profileBox.hovered && !profileBox.down
ToolTip.text: backend.profileNames.length === 0
? qsTr("Профили игрока — добавьте первый профиль")
: qsTr("Профиль игрока: %1").arg(profileBox.displayText)
? Loc.t.profile.hint.addFirst
: Loc.t.profile.label.current.arg(profileBox.displayText)
contentItem: Text {
id: profileText
@@ -263,7 +263,7 @@ Window {
Text {
anchors.centerIn: parent
text: "+ Добавить профиль"
text: Loc.t.profile.button.add
color: "#91B315"
font.pixelSize: 12
}
@@ -394,7 +394,7 @@ Window {
contentItem: Text {
text: backend.activeBuildName !== "" ? backend.activeBuildName
: qsTr("Сборка не выбрана")
: Loc.t.common.status.noBuildSelected
color: "#ffffff"
font: buildButton.font
verticalAlignment: Text.AlignVCenter
@@ -424,7 +424,7 @@ Window {
}
ToolTip.visible: hovered && !buildsDialog.opened
ToolTip.text: qsTr("Пользовательские сборки")
ToolTip.text: Loc.t.common.title.customBuilds
onClicked: buildsDialog.open()
}
@@ -450,7 +450,7 @@ Window {
}
ToolTip.visible: hovered
ToolTip.text: qsTr("Открыть папку с модами Minecraft")
ToolTip.text: Loc.t.app.tooltip.openMods
onClicked: backend.openMinecraftFolder()
}
@@ -478,7 +478,7 @@ Window {
}
ToolTip.visible: hovered
ToolTip.text: qsTr("Настройки запуска: Java, память, папка игры")
ToolTip.text: Loc.t.app.tooltip.settings
onClicked: settingsDialog.load()
}
@@ -497,7 +497,7 @@ Window {
hoverEnabled: true
contentItem: Text {
text: qsTr("Сезонные сборки")
text: Loc.t.common.title.seasonalBuilds
color: "#ffffff"
font.pixelSize: 14
elide: Text.ElideRight
@@ -513,7 +513,7 @@ Window {
}
ToolTip.visible: hovered
ToolTip.text: qsTr("Готовые сборки с сервера: моды, конфиги и Java одной кнопкой")
ToolTip.text: Loc.t.app.tooltip.seasonal
onClicked: seasonalDialog.openCatalog()
}
@@ -548,7 +548,7 @@ Window {
implicitHeight: 52 // tells Dialog how tall the header is
Text {
anchors.centerIn: parent
text: "Новый профиль"
text: Loc.t.profile.title.new
color: "#ffffff"
font.pixelSize: 17
font.bold: true
@@ -570,7 +570,7 @@ Window {
id: pfName
x: 20
width: parent.width - 40
placeholderText: "Имя профиля"
placeholderText: Loc.t.profile.placeholder.name
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
@@ -587,7 +587,7 @@ Window {
width: parent.width - 40
// Ник лицензионного профиля приходит из аккаунта: поля здесь нет.
visible: pfAuth.currentIndex !== 2
placeholderText: "Логин"
placeholderText: Loc.t.profile.placeholder.login
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
@@ -603,7 +603,7 @@ Window {
x: 20
width: parent.width - 40
visible: pfAuth.currentIndex === 1
placeholderText: "Пароль Ely.by"
placeholderText: Loc.t.profile.placeholder.elyPassword
echoMode: TextInput.Password
color: "#ffffff"
placeholderTextColor: "#666666"
@@ -620,8 +620,8 @@ Window {
x: 20
width: parent.width - 40
model: backend.microsoftAvailable
? ["Офлайн (без пароля)", "Ely.by", "Microsoft (лицензия)"]
: ["Офлайн (без пароля)", "Ely.by"]
? Loc.t.profile.authTypes
: Loc.t.profile.authTypes.slice(0, 2)
}
}
@@ -638,7 +638,7 @@ Window {
spacing: 12
Button {
text: "Отмена"
text: Loc.t.common.button.cancel
width: 110; height: 36
contentItem: Text {
text: parent.text
@@ -654,7 +654,7 @@ Window {
}
Button {
text: "Добавить"
text: Loc.t.common.button.add
width: 110; height: 36
contentItem: Text {
text: parent.text
@@ -736,7 +736,7 @@ Window {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: "Редактирование профиля"
text: Loc.t.profile.title.edit
color: "#ffffff"
font.pixelSize: 17
font.bold: true
@@ -758,7 +758,7 @@ Window {
id: epName
x: 20
width: parent.width - 40
placeholderText: "Имя профиля"
placeholderText: Loc.t.profile.placeholder.name
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
@@ -775,7 +775,7 @@ Window {
width: parent.width - 40
// Для лицензии логин не вводят: ниже показан ник из аккаунта.
visible: epAuth.currentIndex !== 2
placeholderText: "Логин"
placeholderText: Loc.t.profile.placeholder.login
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
@@ -810,7 +810,7 @@ Window {
x: 20
width: parent.width - 40
visible: epAuth.currentIndex === 1
placeholderText: "Пароль Ely.by"
placeholderText: Loc.t.profile.placeholder.elyPassword
echoMode: TextInput.Password
color: "#ffffff"
placeholderTextColor: "#666666"
@@ -829,8 +829,8 @@ Window {
// Тип уже сохранённого профиля показываем всегда: иначе в
// сборке без WebEngine он молча стал бы офлайновым.
model: (backend.microsoftAvailable || editProfileDialog.msProfile)
? ["Офлайн (без пароля)", "Ely.by", "Microsoft (лицензия)"]
: ["Офлайн (без пароля)", "Ely.by"]
? Loc.t.profile.authTypes
: Loc.t.profile.authTypes.slice(0, 2)
}
// Состояние аккаунта Microsoft и кнопка повторного входа: токен
@@ -842,8 +842,8 @@ Window {
visible: epAuth.currentIndex === 2
Text {
text: editProfileDialog.msLinked ? qsTr("Вход выполнен")
: qsTr("Вход не выполнен")
text: editProfileDialog.msLinked ? Loc.t.auth.status.signedIn
: Loc.t.auth.status.signedOut
color: editProfileDialog.msLinked ? "#91B315" : "#aaaaaa"
font.pixelSize: 13
}
@@ -888,7 +888,7 @@ Window {
spacing: 12
Button {
text: "Отмена"
text: Loc.t.common.button.cancel
width: 110; height: 36
contentItem: Text {
text: parent.text
@@ -904,7 +904,7 @@ Window {
}
Button {
text: "ОК"
text: Loc.t.common.button.ok
width: 110; height: 36
contentItem: Text {
text: parent.text
@@ -972,7 +972,7 @@ Window {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: "Двухфакторная аутентификация"
text: Loc.t.auth.ely.title.twoFactor
color: "#ffffff"
font.pixelSize: 15
font.bold: true
@@ -993,7 +993,7 @@ Window {
Text {
x: 20
width: parent.width - 40
text: qsTr("Введите код из приложения-аутентификатора для «%1»")
text: Loc.t.auth.ely.hint.enterCode
.arg(twoFactorDialog.profileName)
color: "#aaaaaa"
font.pixelSize: 12
@@ -1031,7 +1031,7 @@ Window {
spacing: 12
Button {
text: "Отмена"
text: Loc.t.common.button.cancel
width: 110; height: 36
contentItem: Text {
text: parent.text
@@ -1047,7 +1047,7 @@ Window {
}
Button {
text: "Войти"
text: Loc.t.common.button.signIn
width: 110; height: 36
contentItem: Text {
text: parent.text
@@ -1080,6 +1080,9 @@ Window {
// Выбранная сборка Java живёт в свойстве, а не в поле ввода: её выбирают
// в отдельном окне, а записывается она только по «Сохранить».
property string javaRuntimeId: ""
// Раскрытый путь к папке игры: хранится свойством, а не присваивается
// тексту напрямую, — иначе подпись не пережила бы смену языка.
property string resolvedGameDir: ""
// Не привязка: javaRuntimeInfo() — обычный вызов, и сам он не
// пересчитается, когда сборка докачается. Обновляем по событиям.
property var javaRuntimeInfo: null
@@ -1107,8 +1110,8 @@ Window {
stWidth.text = String(s.windowWidth)
stHeight.text = String(s.windowHeight)
stFullscreen.checked = s.fullscreen === true
stResolved.text = qsTr("Папка игры: %1").arg(s.resolvedGameDir)
stJavaList.text = backend.detectedJava().join("\n") || qsTr("Java не найдена")
stLanguage.currentIndex = Math.max(0, stLanguage.codes.indexOf(s.language || "system"))
settingsDialog.resolvedGameDir = s.resolvedGameDir
open()
}
@@ -1123,7 +1126,7 @@ Window {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: "Настройки запуска"
text: Loc.t.settings.title
color: "#ffffff"
font.pixelSize: 17
font.bold: true
@@ -1141,19 +1144,43 @@ Window {
topPadding: 18
bottomPadding: 18
// Язык — настройка уровня приложения, поэтому стоит над параметрами
// запуска. Подписи в модели переводятся, коды рядом — нет.
Column {
x: 20
width: parent.width - 40
spacing: 3
Text {
text: Loc.t.settings.language.label
color: "#aaaaaa"
font.pixelSize: 11
}
DarkCombo {
id: stLanguage
width: parent.width
height: 32
readonly property var codes: ["system", "ru", "en"]
model: [Loc.t.settings.language.system,
Loc.t.settings.language.ru,
Loc.t.settings.language.en]
}
}
LabelledField {
id: stGameDir
x: 20
width: parent.width - 40
label: "Папка .minecraft"
placeholder: "по умолчанию"
label: Loc.t.settings.label.gameDir
placeholder: Loc.t.settings.placeholder.gameDir
}
LabelledField {
id: stJavaPath
x: 20
width: parent.width - 40
label: "Путь к Java"
placeholder: "автоопределение"
label: Loc.t.settings.label.javaPath
placeholder: Loc.t.settings.placeholder.javaPath
}
// Сборка Java из папки лаунчера. Выбрана — запуск идёт ей, а не тем,
@@ -1164,7 +1191,7 @@ Window {
spacing: 3
Text {
text: "Версия Java из папки лаунчера"
text: Loc.t.settings.label.javaRuntime
color: "#aaaaaa"
font.pixelSize: 11
}
@@ -1186,12 +1213,12 @@ Window {
color: settingsDialog.javaRuntimeId === "" ? "#666666" : "#ffffff"
text: {
if (settingsDialog.javaRuntimeId === "")
return qsTr("не выбрана — искать в системе")
return Loc.t.settings.status.javaRuntimeNone
const info = settingsDialog.javaRuntimeInfo
if (!info)
return settingsDialog.javaRuntimeId
return info.installed ? info.label
: qsTr("%1 — скачивается").arg(info.label)
: Loc.t.settings.status.javaRuntimeDownloading.arg(info.label)
}
}
@@ -1199,7 +1226,7 @@ Window {
anchors.right: clearJava.left
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Выбрать…")
text: Loc.t.common.button.choose
color: "#91B315"
font.pixelSize: 12
}
@@ -1240,25 +1267,25 @@ Window {
LabelledField {
id: stMinMemory
width: 100
label: "-Xms, МБ"
label: Loc.t.settings.label.minMemory
validator: IntValidator { bottom: 0; top: 1048576 }
}
LabelledField {
id: stMaxMemory
width: 100
label: "-Xmx, МБ"
label: Loc.t.settings.label.maxMemory
validator: IntValidator { bottom: 0; top: 1048576 }
}
LabelledField {
id: stWidth
width: 100
label: "Ширина окна"
label: Loc.t.settings.label.windowWidth
validator: IntValidator { bottom: 0; top: 16384 }
}
LabelledField {
id: stHeight
width: 100
label: "Высота окна"
label: Loc.t.settings.label.windowHeight
validator: IntValidator { bottom: 0; top: 16384 }
}
}
@@ -1267,14 +1294,14 @@ Window {
id: stJvmArgs
x: 20
width: parent.width - 40
label: "Дополнительные аргументы JVM"
label: Loc.t.settings.label.jvmArgs
placeholder: "-XX:+UseG1GC …"
}
CheckBox {
id: stFullscreen
x: 20
text: "Запускать в полноэкранном режиме"
text: Loc.t.settings.label.fullscreen
// Индикатор Basic-стиля — крупный светлый квадрат, на тёмном
// фоне диалога он не читается. Рисуем свой в палитре окна.
@@ -1307,6 +1334,7 @@ Window {
Text {
id: stResolved
text: Loc.t.settings.status.resolvedGameDir.arg(settingsDialog.resolvedGameDir)
x: 20
width: parent.width - 40
color: "#888888"
@@ -1315,6 +1343,7 @@ Window {
}
Text {
id: stJavaList
text: backend.detectedJava().join("\n") || Loc.t.settings.status.javaNotFound
x: 20
width: parent.width - 40
color: "#888888"
@@ -1336,7 +1365,7 @@ Window {
spacing: 12
Button {
text: "Отмена"
text: Loc.t.common.button.cancel
width: 110; height: 36
contentItem: Text {
text: parent.text
@@ -1352,7 +1381,7 @@ Window {
}
Button {
text: "Сохранить"
text: Loc.t.common.button.save
width: 110; height: 36
contentItem: Text {
text: parent.text
@@ -1378,7 +1407,8 @@ Window {
"jvmArgs": stJvmArgs.text.trim(),
"windowWidth": parseInt(stWidth.text) || 0,
"windowHeight": parseInt(stHeight.text) || 0,
"fullscreen": stFullscreen.checked
"fullscreen": stFullscreen.checked,
"language": stLanguage.codes[stLanguage.currentIndex]
})
}
+3 -3
View File
@@ -47,7 +47,7 @@ Dialog {
backend.finishMicrosoftLogin(info.code)
} else {
backend.cancelMicrosoftLogin()
msLogin.failed(info.error !== "" ? info.error : "Вход в Microsoft не завершён")
msLogin.failed(info.error !== "" ? info.error : Loc.t.auth.msa.error.incomplete)
}
}
@@ -62,7 +62,7 @@ Dialog {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: "Вход в аккаунт Microsoft"
text: Loc.t.auth.msa.title.signIn
color: "#ffffff"
font.pixelSize: 17
font.bold: true
@@ -110,7 +110,7 @@ Dialog {
}
Button {
anchors.centerIn: parent
text: "Отмена"
text: Loc.t.common.button.cancel
width: 110; height: 36
contentItem: Text {
text: parent.text
+18 -18
View File
@@ -21,13 +21,13 @@ Dialog {
// Ширины колонок в одном месте: их повторяют и шапка, и делегат.
readonly property var columns: [
{ key: "name", title: qsTr("Название"), width: 250, align: Text.AlignLeft },
{ key: "minecraftVersion", title: qsTr("Версия"), width: 90, align: Text.AlignLeft },
{ key: "loaderLabel", title: qsTr("Загрузчик"), width: 150, align: Text.AlignLeft },
{ key: "modCount", title: qsTr("Модов"), width: 70, align: Text.AlignRight },
{ key: "seasonStart", title: qsTr("Начало"), width: 110, align: Text.AlignLeft },
{ key: "seasonEnd", title: qsTr("Конец"), width: 110, align: Text.AlignLeft },
{ key: "status", title: qsTr("Статус"), width: 140, align: Text.AlignLeft }
{ key: "name", title: Loc.t.common.header.name, width: 250, align: Text.AlignLeft },
{ key: "minecraftVersion", title: Loc.t.common.header.version, width: 90, align: Text.AlignLeft },
{ key: "loaderLabel", title: Loc.t.common.header.loader, width: 150, align: Text.AlignLeft },
{ key: "modCount", title: Loc.t.seasonal.header.mods, width: 70, align: Text.AlignRight },
{ key: "seasonStart", title: Loc.t.seasonal.header.start, width: 110, align: Text.AlignLeft },
{ key: "seasonEnd", title: Loc.t.seasonal.header.end, width: 110, align: Text.AlignLeft },
{ key: "status", title: Loc.t.common.header.status, width: 140, align: Text.AlignLeft }
]
modal: true
@@ -86,7 +86,7 @@ Dialog {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: qsTr("Сезонные сборки")
text: Loc.t.common.title.seasonalBuilds
color: "#ffffff"
font.pixelSize: 17
font.bold: true
@@ -151,9 +151,9 @@ Dialog {
color: seasons.backend.seasonalCatalogError === "" ? "#888888" : "#cc6666"
font.pixelSize: 13
text: seasons.backend.seasonalCatalogLoading
? qsTr("Загрузка списка сборок…")
? Loc.t.seasonal.progress.loadingCatalog
: (seasons.backend.seasonalCatalogError === ""
? qsTr("Сборок пока нет")
? Loc.t.seasonal.status.catalogEmptyYet
: seasons.backend.seasonalCatalogError)
}
@@ -268,16 +268,16 @@ Dialog {
font.pixelSize: 11
text: {
if (!seasons.selectedEntry)
return qsTr("Выберите сборку в списке")
return Loc.t.seasonal.hint.pickBuild
const entry = seasons.selectedEntry
var line = qsTr("Ревизия %1").arg(entry.revision)
var line = Loc.t.seasonal.status.revision.arg(entry.revision)
const size = seasons.formatMb(entry.sizeBytes)
if (size !== "")
line += qsTr(" · %1 МБ").arg(size)
line += Loc.t.seasonal.status.sizeSuffix.arg(size)
if (entry.serverUrl !== "")
line += " · " + entry.serverUrl
if (entry.updateAvailable)
line += qsTr(" · установлена ревизия %1").arg(entry.installedRevision)
line += Loc.t.seasonal.status.installedRevisionSuffix.arg(entry.installedRevision)
return line
}
}
@@ -296,8 +296,8 @@ Dialog {
height: 36
enabled: !seasons.backend.seasonalCatalogLoading
contentItem: Text {
text: seasons.backend.seasonalCatalogLoading ? qsTr("Обновление…")
: qsTr("Обновить список")
text: seasons.backend.seasonalCatalogLoading ? Loc.t.common.status.updating
: Loc.t.common.button.refreshList
color: refreshButton.enabled ? "#ffffff" : "#888888"
font.pixelSize: 13
horizontalAlignment: Text.AlignHCenter
@@ -319,8 +319,8 @@ Dialog {
enabled: seasons.selectedEntry !== null && !seasons.backend.busy
contentItem: Text {
text: seasons.selectedEntry && seasons.selectedEntry.updateAvailable
? qsTr("Обновить")
: qsTr("Установить")
? Loc.t.common.button.update
: Loc.t.common.button.install
color: installButton.enabled ? "#ffffff" : "#888888"
font.pixelSize: 13
font.bold: true
+22 -22
View File
@@ -39,11 +39,11 @@ Dialog {
}
readonly property var categories: [
{ key: "release", title: qsTr("Релизы") },
{ key: "snapshot", title: qsTr("Снапшоты") },
{ key: "old_beta", title: qsTr("Беты") },
{ key: "old_alpha", title: qsTr("Альфы") },
{ key: "other", title: qsTr("Прочие") }
{ key: "release", title: Loc.t.version.filter.releases },
{ key: "snapshot", title: Loc.t.version.filter.snapshots },
{ key: "old_beta", title: Loc.t.version.filter.betas },
{ key: "old_alpha", title: Loc.t.version.filter.alphas },
{ key: "other", title: Loc.t.version.filter.other }
]
// Каталог приходит из C++ уже отсортированным (новые сверху), поэтому здесь
@@ -167,7 +167,7 @@ Dialog {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: qsTr("Удалить версию?")
text: Loc.t.version.title.removeConfirm
color: "#ffffff"
font.pixelSize: 17
font.bold: true
@@ -192,9 +192,9 @@ Dialog {
color: "#ffffff"
font.pixelSize: 13
text: removeConfirm.sizeMb > 0
? qsTr("Файлы версии «%1» будут удалены из versions/ — освободится около %2 МБ. Скачать её заново можно в любой момент.")
? Loc.t.version.hint.removeWithSize
.arg(removeConfirm.versionId).arg(removeConfirm.sizeMb)
: qsTr("Файлы версии «%1» будут удалены из versions/. Скачать её заново можно в любой момент.")
: Loc.t.version.hint.remove
.arg(removeConfirm.versionId)
}
@@ -205,7 +205,7 @@ Dialog {
wrapMode: Text.Wrap
color: "#cc6666"
font.pixelSize: 12
text: qsTr("На эту версию опираются установленные профили модлоадеров: %1. Без неё они не запустятся — их придётся переустановить.")
text: Loc.t.version.warning.loaderProfilesDepend
.arg(removeConfirm.dependents.join(", "))
}
@@ -216,7 +216,7 @@ Dialog {
wrapMode: Text.Wrap
color: "#cc6666"
font.pixelSize: 12
text: qsTr("Версию используют сборки: %1. Файлы понадобится скачать заново кнопкой «Установить».")
text: Loc.t.version.warning.buildsDepend
.arg(removeConfirm.builds.join(", "))
}
@@ -226,7 +226,7 @@ Dialog {
wrapMode: Text.Wrap
color: "#888888"
font.pixelSize: 12
text: qsTr("Библиотеки и ресурсы в libraries/ и assets/ общие для всех версий и остаются на месте.")
text: Loc.t.version.hint.sharedFilesStay
}
}
@@ -243,7 +243,7 @@ Dialog {
spacing: 12
Button {
text: qsTr("Отмена")
text: Loc.t.common.button.cancel
width: 130; height: 36
contentItem: Text {
text: parent.text
@@ -259,7 +259,7 @@ Dialog {
}
Button {
text: qsTr("Удалить")
text: Loc.t.common.button.remove
width: 130; height: 36
contentItem: Text {
text: parent.text
@@ -289,7 +289,7 @@ Dialog {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: qsTr("Версия Minecraft")
text: Loc.t.common.label.minecraftVersion
color: "#ffffff"
font.pixelSize: 17
font.bold: true
@@ -370,7 +370,7 @@ Dialog {
anchors.right: parent.right
anchors.margins: 8
height: 32
placeholderText: qsTr("Поиск версии…")
placeholderText: Loc.t.common.placeholder.searchVersion
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
@@ -424,7 +424,7 @@ Dialog {
}
contentItem: Text {
leftPadding: installedOnlyBox.indicator.width + 8
text: qsTr("Только установленные")
text: Loc.t.version.filter.installedOnly
color: "#aaaaaa"
font.pixelSize: 11
verticalAlignment: Text.AlignVCenter
@@ -542,10 +542,10 @@ Dialog {
font.pixelSize: 12
font.italic: true
text: picker.backend.catalogLoading
? qsTr("Загрузка списка версий…")
? Loc.t.version.progress.loadingList
: picker.backend.versionCatalog.length === 0
? qsTr("Список версий недоступен — проверьте соединение")
: qsTr("Ничего не найдено")
? Loc.t.version.error.listUnreachable
: Loc.t.common.status.nothingFound
}
}
}
@@ -567,7 +567,7 @@ Dialog {
elide: Text.ElideRight
color: picker.selectedId === "" ? "#666666" : "#91B315"
font.pixelSize: 13
text: picker.selectedId === "" ? qsTr("Версия не выбрана") : picker.selectedId
text: picker.selectedId === "" ? Loc.t.version.status.notSelected : picker.selectedId
}
Row {
@@ -577,7 +577,7 @@ Dialog {
spacing: 12
Button {
text: qsTr("Отмена")
text: Loc.t.common.button.cancel
width: 110; height: 36
contentItem: Text {
text: parent.text
@@ -593,7 +593,7 @@ Dialog {
}
Button {
text: qsTr("Выбрать")
text: Loc.t.common.button.select
width: 110; height: 36
enabled: picker.selectedId !== ""
opacity: enabled ? 1.0 : 0.45
+18 -17
View File
@@ -1,4 +1,5 @@
#include "authservice.h"
#include "localization.h"
#include <QCryptographicHash>
#include <QDir>
@@ -30,7 +31,7 @@ QString describeElyError(const QJsonObject &response, int status)
const QString error = response.value(QStringLiteral("error")).toString();
if (!error.isEmpty())
return error;
return QObject::tr("Сервер авторизации вернул код %1").arg(status);
return Loc::text("auth.error.serverCode").arg(status);
}
} // namespace
@@ -45,7 +46,7 @@ AuthResult AuthService::offline(const QString &nickname)
{
AuthResult result;
if (nickname.isEmpty()) {
result.error = tr("Для офлайн-запуска нужен никнейм");
result.error = Loc::text("auth.offline.error.noNickname");
return result;
}
@@ -102,7 +103,7 @@ void AuthService::loginElyBy(const QString &login,
{
if (login.isEmpty()) {
AuthResult result;
result.error = tr("Укажите логин Ely.by в настройках профиля");
result.error = Loc::text("auth.ely.error.noLogin");
callback(result);
return;
}
@@ -115,14 +116,14 @@ void AuthService::loginElyBy(const QString &login,
return;
}
emit progress(tr("Проверка сохранённого токена Ely.by…"));
emit progress(Loc::text("auth.ely.progress.checkingToken"));
postJson(QStringLiteral("/auth/validate"),
{{QStringLiteral("accessToken"), accessToken}},
[this, login, password, token, accessToken, callback](
int status, const QJsonObject &, const QString &transportError) {
if (!transportError.isEmpty()) {
AuthResult result;
result.error = tr("Нет связи с authserver.ely.by: %1").arg(transportError);
result.error = Loc::text("auth.ely.error.unreachable").arg(transportError);
callback(result);
return;
}
@@ -138,7 +139,7 @@ void AuthService::loginElyBy(const QString &login,
}
// Токен протух — пробуем продлить его, не спрашивая пароль.
emit progress(tr("Обновление токена Ely.by…"));
emit progress(Loc::text("auth.ely.progress.refreshingToken"));
postJson(QStringLiteral("/auth/refresh"),
{{QStringLiteral("accessToken"), accessToken},
{QStringLiteral("clientToken"), token},
@@ -183,12 +184,12 @@ void AuthService::authenticate(const QString &login,
{
if (password.isEmpty()) {
AuthResult result;
result.error = tr("Укажите пароль Ely.by в настройках профиля");
result.error = Loc::text("auth.ely.error.noPassword");
callback(result);
return;
}
emit progress(tr("Авторизация на Ely.by…"));
emit progress(Loc::text("auth.ely.progress.signingIn"));
postJson(QStringLiteral("/auth/authenticate"),
{{QStringLiteral("username"), login},
{QStringLiteral("password"), password},
@@ -199,7 +200,7 @@ void AuthService::authenticate(const QString &login,
result.clientToken = clientToken;
if (!transportError.isEmpty()) {
result.error = AuthService::tr("Нет связи с authserver.ely.by: %1").arg(transportError);
result.error = Loc::text("auth.ely.error.unreachable").arg(transportError);
callback(result);
return;
}
@@ -210,7 +211,7 @@ void AuthService::authenticate(const QString &login,
result.twoFactorRequired = message.contains(QStringLiteral("two factor"),
Qt::CaseInsensitive);
result.error = result.twoFactorRequired
? AuthService::tr("Аккаунт защищён двухфакторной аутентификацией")
? Loc::text("auth.ely.error.twoFactor")
: message;
callback(result);
return;
@@ -223,7 +224,7 @@ void AuthService::authenticate(const QString &login,
result.userType = QStringLiteral("ELYBY");
result.ok = !result.accessToken.isEmpty() && !result.playerName.isEmpty();
if (!result.ok)
result.error = AuthService::tr("Ely.by не вернул игровой профиль для этого аккаунта");
result.error = Loc::text("auth.ely.error.noGameProfile");
callback(result);
});
}
@@ -239,7 +240,7 @@ void AuthService::ensureAuthlibInjector(
return;
}
emit progress(tr("Загрузка authlib-injector…"));
emit progress(Loc::text("auth.ely.progress.downloadingInjector"));
QNetworkRequest manifestRequest{QUrl(kInjectorManifest)};
manifestRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
@@ -248,7 +249,7 @@ void AuthService::ensureAuthlibInjector(
connect(manifestReply, &QNetworkReply::finished, this, [this, manifestReply, jarPath, callback]() {
manifestReply->deleteLater();
if (manifestReply->error() != QNetworkReply::NoError) {
callback({}, tr("Не удалось получить сведения об authlib-injector: %1")
callback({}, Loc::text("auth.ely.error.injectorInfoFailed")
.arg(manifestReply->errorString()));
return;
}
@@ -260,7 +261,7 @@ void AuthService::ensureAuthlibInjector(
.value(QStringLiteral("sha256"))
.toString();
if (url.isEmpty()) {
callback({}, tr("В манифесте authlib-injector нет ссылки на файл"));
callback({}, Loc::text("auth.ely.error.injectorNoUrl"));
return;
}
@@ -271,7 +272,7 @@ void AuthService::ensureAuthlibInjector(
connect(jarReply, &QNetworkReply::finished, this, [jarReply, jarPath, expectedSha256, callback]() {
jarReply->deleteLater();
if (jarReply->error() != QNetworkReply::NoError) {
callback({}, AuthService::tr("Не удалось скачать authlib-injector: %1").arg(jarReply->errorString()));
callback({}, Loc::text("auth.ely.error.injectorDownloadFailed").arg(jarReply->errorString()));
return;
}
@@ -280,14 +281,14 @@ void AuthService::ensureAuthlibInjector(
const QString actual = QString::fromLatin1(
QCryptographicHash::hash(payload, QCryptographicHash::Sha256).toHex());
if (actual != expectedSha256) {
callback({}, AuthService::tr("Контрольная сумма authlib-injector не совпала"));
callback({}, Loc::text("auth.ely.error.injectorChecksum"));
return;
}
}
QFile file(jarPath);
if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()) {
callback({}, AuthService::tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(jarPath)));
callback({}, Loc::text("common.error.saveFailed").arg(QDir::toNativeSeparators(jarPath)));
return;
}
file.close();
+23 -22
View File
@@ -1,4 +1,5 @@
#include "buildarchiveworker.h"
#include "localization.h"
#include <QDir>
#include <QDirIterator>
@@ -102,7 +103,7 @@ void BuildArchiveWorker::archive(const QString &gameDir,
{
const QDir root(gameDir);
if (!root.exists()) {
emit finished(false, tr("Папка игры не найдена: %1").arg(QDir::toNativeSeparators(gameDir)));
emit finished(false, Loc::text("switch.error.gameDirMissing").arg(QDir::toNativeSeparators(gameDir)));
return;
}
@@ -113,7 +114,7 @@ void BuildArchiveWorker::archive(const QString &gameDir,
QZipWriter writer(tempZipPath);
if (writer.status() != QZipWriter::NoError) {
emit finished(false,
tr("Не удалось создать архив %1")
Loc::text("switch.error.archiveCreateFailed")
.arg(QDir::toNativeSeparators(tempZipPath)));
return;
}
@@ -126,7 +127,7 @@ void BuildArchiveWorker::archive(const QString &gameDir,
if (canceled()) {
writer.close();
QFile::remove(tempZipPath);
emit finished(false, tr("Архивация отменена"));
emit finished(false, Loc::text("switch.status.archivingCancelled"));
return;
}
@@ -138,7 +139,7 @@ void BuildArchiveWorker::archive(const QString &gameDir,
writer.close();
QFile::remove(tempZipPath);
emit finished(false,
tr("Не удалось прочитать %1: %2")
Loc::text("common.error.readFailed")
.arg(QDir::toNativeSeparators(relative), file.errorString()));
return;
}
@@ -149,7 +150,7 @@ void BuildArchiveWorker::archive(const QString &gameDir,
writer.close();
QFile::remove(tempZipPath);
emit finished(false,
tr("Ошибка записи архива на файле %1")
Loc::text("switch.error.archiveWriteFailed")
.arg(QDir::toNativeSeparators(relative)));
return;
}
@@ -187,7 +188,7 @@ void BuildArchiveWorker::clear(const QString &gameDir, const QStringList &keepTo
: QFile::remove(entry.absoluteFilePath());
if (!ok) {
emit finished(false,
tr("Не удалось удалить %1")
Loc::text("switch.error.removeFailed")
.arg(QDir::toNativeSeparators(entry.absoluteFilePath())));
return;
}
@@ -202,7 +203,7 @@ void BuildArchiveWorker::restore(const QString &zipPath, const QString &gameDir)
QZipReader reader(zipPath);
if (!reader.isReadable()) {
emit finished(false,
tr("Не удалось открыть архив %1").arg(QDir::toNativeSeparators(zipPath)));
Loc::text("switch.error.archiveOpenFailed").arg(QDir::toNativeSeparators(zipPath)));
return;
}
@@ -212,14 +213,14 @@ void BuildArchiveWorker::restore(const QString &zipPath, const QString &gameDir)
const QDir root(gameDir);
if (!QDir().mkpath(gameDir)) {
emit finished(false,
tr("Не удалось создать %1").arg(QDir::toNativeSeparators(gameDir)));
Loc::text("common.error.createFailed").arg(QDir::toNativeSeparators(gameDir)));
return;
}
int done = 0;
for (const QZipReader::FileInfo &entry : entries) {
if (canceled()) {
emit finished(false, tr("Распаковка отменена"));
emit finished(false, Loc::text("switch.status.extractCancelled"));
return;
}
if (!entry.isFile) {
@@ -228,14 +229,14 @@ void BuildArchiveWorker::restore(const QString &zipPath, const QString &gameDir)
}
if (!isSafeRelativePath(entry.filePath)) {
emit finished(false,
tr("Архив повреждён: недопустимый путь «%1»").arg(entry.filePath));
Loc::text("switch.error.archiveBadPath").arg(entry.filePath));
return;
}
const QString target = root.absoluteFilePath(entry.filePath);
if (!QDir().mkpath(QFileInfo(target).absolutePath())) {
emit finished(false,
tr("Не удалось создать %1")
Loc::text("common.error.createFailed")
.arg(QDir::toNativeSeparators(QFileInfo(target).absolutePath())));
return;
}
@@ -244,7 +245,7 @@ void BuildArchiveWorker::restore(const QString &zipPath, const QString &gameDir)
const QByteArray data = reader.fileData(entry.filePath);
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size()) {
emit finished(false,
tr("Не удалось записать %1: %2")
Loc::text("switch.error.writeFailed")
.arg(QDir::toNativeSeparators(entry.filePath), file.errorString()));
return;
}
@@ -263,7 +264,7 @@ void BuildArchiveWorker::applyPack(const QString &zipPath,
QZipReader reader(zipPath);
if (!reader.isReadable()) {
emit finished(false,
tr("Не удалось открыть архив %1").arg(QDir::toNativeSeparators(zipPath)));
Loc::text("switch.error.archiveOpenFailed").arg(QDir::toNativeSeparators(zipPath)));
return;
}
@@ -277,12 +278,12 @@ void BuildArchiveWorker::applyPack(const QString &zipPath,
continue;
if (!isSafeRelativePath(entry.filePath)) {
emit finished(false,
tr("Архив сборки повреждён: недопустимый путь «%1»").arg(entry.filePath));
Loc::text("switch.error.packBadPath").arg(entry.filePath));
return;
}
if (forbiddenTop.contains(topSegment(entry.filePath))) {
emit finished(false,
tr("Архив сборки пытается изменить общие файлы лаунчера: «%1»")
Loc::text("switch.error.packTouchesLauncher")
.arg(entry.filePath));
return;
}
@@ -291,7 +292,7 @@ void BuildArchiveWorker::applyPack(const QString &zipPath,
const QDir root(gameDir);
if (!QDir().mkpath(gameDir)) {
emit finished(false, tr("Не удалось создать %1").arg(QDir::toNativeSeparators(gameDir)));
emit finished(false, Loc::text("common.error.createFailed").arg(QDir::toNativeSeparators(gameDir)));
return;
}
@@ -303,7 +304,7 @@ void BuildArchiveWorker::applyPack(const QString &zipPath,
QStringList touchedDirs;
for (const QString &relative : removeRelative) {
if (canceled()) {
emit finished(false, tr("Установка сборки отменена"));
emit finished(false, Loc::text("switch.status.installCancelled"));
return;
}
// Список пришёл из нашего же файла состояния, но он лежит на диске
@@ -321,7 +322,7 @@ void BuildArchiveWorker::applyPack(const QString &zipPath,
}
if (QFileInfo::exists(target) && !QFile::remove(target)) {
emit finished(false,
tr("Не удалось удалить %1").arg(QDir::toNativeSeparators(target)));
Loc::text("switch.error.removeFailed").arg(QDir::toNativeSeparators(target)));
return;
}
const QString parent = QFileInfo(relative).path();
@@ -334,20 +335,20 @@ void BuildArchiveWorker::applyPack(const QString &zipPath,
// ── Раскладываем новую ревизию поверх ────────────────────────────────
for (const QString &relative : std::as_const(applied)) {
if (canceled()) {
emit finished(false, tr("Установка сборки отменена"));
emit finished(false, Loc::text("switch.status.installCancelled"));
return;
}
const QString target = root.absoluteFilePath(relative);
if (!isInside(root, target)) {
emit finished(false,
tr("Архив сборки повреждён: путь «%1» ведёт за пределы папки игры")
Loc::text("switch.error.packEscapesGameDir")
.arg(relative));
return;
}
if (!QDir().mkpath(QFileInfo(target).absolutePath())) {
emit finished(false,
tr("Не удалось создать %1")
Loc::text("common.error.createFailed")
.arg(QDir::toNativeSeparators(QFileInfo(target).absolutePath())));
return;
}
@@ -359,7 +360,7 @@ void BuildArchiveWorker::applyPack(const QString &zipPath,
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)
|| file.write(data) != data.size()) {
emit finished(false,
tr("Не удалось записать %1: %2")
Loc::text("switch.error.writeFailed")
.arg(QDir::toNativeSeparators(relative), file.errorString()));
return;
}
+16 -15
View File
@@ -1,4 +1,5 @@
#include "buildswitcher.h"
#include "localization.h"
#include "buildarchiveworker.h"
#include "launcherpaths.h"
@@ -145,7 +146,7 @@ void BuildSwitcher::switchTo(int fromBuildId,
const QString &gameDir)
{
if (m_state != Idle) {
emit failed(toBuildId, tr("Смена сборки уже выполняется"), true);
emit failed(toBuildId, Loc::text("common.error.switchInProgress"), true);
return;
}
@@ -173,7 +174,7 @@ void BuildSwitcher::switchTo(int fromBuildId,
if (!QDir().mkpath(buildDir(fromBuildId))) {
emit failed(toBuildId,
tr("Не удалось создать %1")
Loc::text("common.error.createFailed")
.arg(QDir::toNativeSeparators(buildDir(fromBuildId))),
true);
return;
@@ -183,7 +184,7 @@ void BuildSwitcher::switchTo(int fromBuildId,
const QStorageInfo storage(LauncherPaths::buildStorageDir());
if (storage.isValid() && storage.bytesAvailable() < needed) {
emit failed(toBuildId,
tr("Недостаточно места для архива сборки «%1»: нужно около %2 МБ")
Loc::text("switch.error.notEnoughSpace")
.arg(fromName)
.arg(needed / (1024 * 1024)),
true);
@@ -194,7 +195,7 @@ void BuildSwitcher::switchTo(int fromBuildId,
+ QUuid::createUuid().toString(QUuid::Id128) + QStringLiteral(".zip");
m_state = Archiving;
setStage(tr("Сохранение сборки «%1»").arg(fromName));
setStage(Loc::text("switch.progress.saving").arg(fromName));
requestArchive(gameDir, m_tempZip, sharedTopLevel());
}
@@ -296,12 +297,12 @@ void BuildSwitcher::commitArchive()
// На сборку — один архив: новый встаёт на место, все прежние уходят.
if (QFile::exists(target) && !QFile::remove(target)) {
QFile::remove(m_tempZip);
fail(tr("Не удалось заменить архив %1").arg(QDir::toNativeSeparators(target)), true);
fail(Loc::text("switch.error.archiveReplaceFailed").arg(QDir::toNativeSeparators(target)), true);
return;
}
if (!QFile::rename(m_tempZip, target)) {
QFile::remove(m_tempZip);
fail(tr("Не удалось сохранить архив %1").arg(QDir::toNativeSeparators(target)), true);
fail(Loc::text("switch.error.archiveSaveFailed").arg(QDir::toNativeSeparators(target)), true);
return;
}
@@ -335,7 +336,7 @@ void BuildSwitcher::commitArchive()
void BuildSwitcher::beginClearing()
{
setStage(tr("Подготовка сборки «%1»").arg(m_toName));
setStage(Loc::text("switch.progress.preparing").arg(m_toName));
requestClear(m_gameDir, sharedTopLevel());
}
@@ -351,7 +352,7 @@ void BuildSwitcher::beginRestoring()
}
m_state = Restoring;
setStage(tr("Восстановление сборки «%1»").arg(m_toName));
setStage(Loc::text("switch.progress.restoring").arg(m_toName));
requestRestore(archive, m_gameDir);
}
@@ -365,7 +366,7 @@ void BuildSwitcher::applyPack(int buildId,
const QString &gameDir)
{
if (m_state != Idle) {
emit failed(buildId, tr("Операция со сборками уже выполняется"), true);
emit failed(buildId, Loc::text("switch.error.busy"), true);
return;
}
@@ -376,7 +377,7 @@ void BuildSwitcher::applyPack(int buildId,
}
if (!QFile::exists(packZipPath)) {
emit failed(buildId,
tr("Архив сборки не найден: %1")
Loc::text("switch.error.archiveMissing")
.arg(QDir::toNativeSeparators(packZipPath)),
true);
return;
@@ -412,7 +413,7 @@ void BuildSwitcher::applyPack(int buildId,
void BuildSwitcher::beginApplying()
{
setStage(tr("Установка сборки «%1»").arg(m_toName));
setStage(Loc::text("switch.progress.installing").arg(m_toName));
requestApplyPack(m_packPath, m_gameDir, m_packRemove);
}
@@ -507,8 +508,8 @@ QString BuildSwitcher::interruptedSwitchWarning() const
if (state == kStateApplying) {
const QString name = index.value(QStringLiteral("packName")).toString();
return tr("Установка сборки%1 не завершилась — докатываем её файлы.")
.arg(name.isEmpty() ? QString() : tr(" «%1»").arg(name));
return Loc::text("switch.status.applyInterrupted")
.arg(name.isEmpty() ? QString() : Loc::text("switch.status.nameSuffix").arg(name));
}
if (state != kStateSwitching)
return {};
@@ -519,8 +520,8 @@ QString BuildSwitcher::interruptedSwitchWarning() const
.toObject()
.value(QStringLiteral("name"))
.toString();
return tr("Прошлая смена сборки%1 не завершилась — восстанавливаем содержимое .minecraft.")
.arg(name.isEmpty() ? QString() : tr(" на «%1»").arg(name));
return Loc::text("switch.status.switchInterrupted")
.arg(name.isEmpty() ? QString() : Loc::text("switch.status.targetSuffix").arg(name));
}
void BuildSwitcher::resumeInterrupted(const QString &gameDir)
+10 -9
View File
@@ -1,4 +1,5 @@
#include "gamelauncher.h"
#include "localization.h"
#include "javalocator.h"
@@ -234,7 +235,7 @@ bool GameLauncher::extractNatives(const LaunchOptions &options,
{
if (!QDir().mkpath(nativesDir)) {
if (error)
*error = tr("Не удалось создать папку %1").arg(QDir::toNativeSeparators(nativesDir));
*error = Loc::text("launch.error.createFolderFailed").arg(QDir::toNativeSeparators(nativesDir));
return false;
}
@@ -246,7 +247,7 @@ bool GameLauncher::extractNatives(const LaunchOptions &options,
QZipReader reader(jar);
if (!reader.isReadable()) {
if (error)
*error = tr("Не удалось прочитать натив-библиотеку %1")
*error = Loc::text("launch.error.nativeReadFailed")
.arg(QDir::toNativeSeparators(jar));
return false;
}
@@ -282,7 +283,7 @@ bool GameLauncher::extractNatives(const LaunchOptions &options,
QFile file(target);
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size()) {
if (error)
*error = tr("Не удалось распаковать %1 в %2")
*error = Loc::text("launch.error.extractFailed")
.arg(fileName, QDir::toNativeSeparators(nativesDir));
return false;
}
@@ -298,27 +299,27 @@ bool GameLauncher::launch(const LaunchOptions &options,
{
if (isRunning()) {
if (error)
*error = tr("Игра уже запущена");
*error = Loc::text("common.error.gameRunning");
return false;
}
emit progress(tr("Проверка файлов игры…"));
emit progress(Loc::text("launch.progress.checkingFiles"));
const QStringList missing = missingFiles(options, version);
if (!missing.isEmpty()) {
if (error)
*error = tr("В .minecraft не хватает файлов (%1):\n%2")
*error = Loc::text("launch.error.filesMissing")
.arg(missing.size())
.arg(missing.join(u'\n'));
return false;
}
emit progress(tr("Распаковка нативных библиотек…"));
emit progress(Loc::text("launch.progress.extractingNatives"));
const QString nativesDir = options.gameDir + QStringLiteral("/versions/") + version.id
+ QStringLiteral("/natives");
if (!extractNatives(options, version, nativesDir, error))
return false;
emit progress(tr("Поиск Java %1…").arg(version.javaMajor));
emit progress(Loc::text("launch.progress.lookingForJava").arg(version.javaMajor));
const QString java = JavaLocator::select(options.gameDir, version.javaMajor, options.javaPath, error);
if (java.isEmpty())
return false;
@@ -352,7 +353,7 @@ bool GameLauncher::launch(const LaunchOptions &options,
m_process->start();
if (!m_process->waitForStarted(15000)) {
if (error)
*error = tr("Не удалось запустить %1: %2")
*error = Loc::text("launch.error.startFailed")
.arg(QDir::toNativeSeparators(executable), m_process->errorString());
m_process->deleteLater();
m_process = nullptr;
File diff suppressed because it is too large Load Diff
+34 -33
View File
@@ -1,4 +1,5 @@
#include "javainstaller.h"
#include "localization.h"
#include "launcherpaths.h"
@@ -114,21 +115,21 @@ void JavaInstaller::install(const JavaRuntimeEntry &entry)
if (m_running)
return;
if (!entry.isValid()) {
emit failed(entry.version, tr("Сборка Java недоступна для загрузки"));
emit failed(entry.version, Loc::text("java.error.buildUnavailable"));
return;
}
QString error;
if (!LauncherPaths::ensureRootExists(&error)) {
emit failed(entry.version, tr("Папка лаунчера недоступна: %1").arg(error));
emit failed(entry.version, Loc::text("java.error.launcherDirUnavailable").arg(error));
return;
}
m_entry = entry;
m_label = tr("%1 %2").arg(javaKindTitle(entry.kind), entry.version);
m_label = Loc::text("java.status.nameAndVersion").arg(javaKindTitle(entry.kind), entry.version);
m_targetDir = JavaRuntimeStore::dirFor(entry.id);
if (m_targetDir.isEmpty()) {
emit failed(m_label, tr("Недопустимый идентификатор сборки Java"));
emit failed(m_label, Loc::text("java.error.invalidBuildId"));
return;
}
@@ -139,7 +140,7 @@ void JavaInstaller::install(const JavaRuntimeEntry &entry)
return;
}
if (!QDir().mkpath(m_targetDir)) {
emit failed(m_label, tr("Не удалось создать %1").arg(QDir::toNativeSeparators(m_targetDir)));
emit failed(m_label, Loc::text("common.error.createFailed").arg(QDir::toNativeSeparators(m_targetDir)));
return;
}
@@ -191,7 +192,7 @@ void JavaInstaller::beginArchive()
{
if (m_entry.archive != QLatin1String("zip")
&& m_entry.archive != QLatin1String("tar.gz")) {
fail(tr("Неизвестный формат архива: %1").arg(m_entry.archive));
fail(Loc::text("java.error.unknownArchiveFormat").arg(m_entry.archive));
return;
}
@@ -199,7 +200,7 @@ void JavaInstaller::beginArchive()
m_archivePath = LauncherPaths::javaDownloadDir() + u'/'
+ (name.isEmpty() ? m_entry.id + QStringLiteral(".archive") : name);
m_bytesTotal = m_entry.size;
setStage(tr("Загрузка Java %1").arg(m_entry.version));
setStage(Loc::text("java.progress.downloadingBuild").arg(m_entry.version));
downloadArchive();
}
@@ -207,7 +208,7 @@ void JavaInstaller::downloadArchive()
{
m_archiveFile = std::make_unique<QSaveFile>(m_archivePath);
if (!m_archiveFile->open(QIODevice::WriteOnly)) {
fail(tr("Не удалось открыть на запись %1").arg(QDir::toNativeSeparators(m_archivePath)));
fail(Loc::text("common.error.openForWriteFailed").arg(QDir::toNativeSeparators(m_archivePath)));
return;
}
m_archiveHash.reset();
@@ -260,7 +261,7 @@ void JavaInstaller::handleArchiveFinished(QNetworkReply *reply)
if (reply->error() != QNetworkReply::NoError) {
m_archiveFile->cancelWriting();
m_archiveFile.reset();
fail(tr("Не удалось скачать Java %1: %2").arg(m_entry.version, reply->errorString()));
fail(Loc::text("java.error.downloadFailed").arg(m_entry.version, reply->errorString()));
return;
}
@@ -269,14 +270,14 @@ void JavaInstaller::handleArchiveFinished(QNetworkReply *reply)
if (actual.compare(m_entry.checksum, Qt::CaseInsensitive) != 0) {
m_archiveFile->cancelWriting();
m_archiveFile.reset();
fail(tr("Контрольная сумма архива Java не совпала — загрузка повреждена"));
fail(Loc::text("java.error.archiveChecksum"));
return;
}
}
if (!m_archiveFile->commit()) {
m_archiveFile.reset();
fail(tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(m_archivePath)));
fail(Loc::text("common.error.saveFailed").arg(QDir::toNativeSeparators(m_archivePath)));
return;
}
m_archiveFile.reset();
@@ -290,7 +291,7 @@ void JavaInstaller::startExtraction()
// показывать мегабайты, а проценты продолжит.
m_bytesTotal = 0;
m_bytesSettled = 0;
setStage(tr("Распаковка Java %1").arg(m_entry.version));
setStage(Loc::text("java.progress.extracting").arg(m_entry.version));
if (m_entry.archive == QLatin1String("tar.gz")) {
startTarExtraction();
@@ -300,7 +301,7 @@ void JavaInstaller::startExtraction()
m_zip = std::make_unique<ZipExtraction>(m_archivePath);
if (!m_zip->reader.isReadable()) {
m_zip.reset();
fail(tr("Не удалось прочитать архив %1").arg(QDir::toNativeSeparators(m_archivePath)));
fail(Loc::text("java.error.archiveReadFailed").arg(QDir::toNativeSeparators(m_archivePath)));
return;
}
@@ -324,7 +325,7 @@ void JavaInstaller::extractZipChunk()
const QString target = m_targetDir + u'/' + info.filePath;
if (!isInside(m_targetDir, target)) {
m_extractTimer->stop();
fail(tr("Архив Java содержит недопустимый путь: %1").arg(info.filePath));
fail(Loc::text("java.error.archiveBadPath").arg(info.filePath));
return;
}
@@ -338,7 +339,7 @@ void JavaInstaller::extractZipChunk()
if (!QDir().mkpath(QFileInfo(target).absolutePath())) {
m_extractTimer->stop();
fail(tr("Не удалось создать %1")
fail(Loc::text("common.error.createFailed")
.arg(QDir::toNativeSeparators(QFileInfo(target).absolutePath())));
return;
}
@@ -347,7 +348,7 @@ void JavaInstaller::extractZipChunk()
QFile file(target);
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size()) {
m_extractTimer->stop();
fail(tr("Не удалось распаковать %1").arg(info.filePath));
fail(Loc::text("java.error.extractEntryFailed").arg(info.filePath));
return;
}
file.close();
@@ -389,7 +390,7 @@ void JavaInstaller::startTarExtraction()
emit canceled(label);
return;
}
fail(tr("Не удалось распаковать архив Java: %1").arg(message));
fail(Loc::text("java.error.extractFailed").arg(message));
});
connect(m_tar, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) {
@@ -407,7 +408,7 @@ void JavaInstaller::startTarExtraction()
return;
}
if (status != QProcess::NormalExit || exitCode != 0) {
fail(tr("Распаковка архива Java не удалась: %1")
fail(Loc::text("java.error.extractCrashed")
.arg(QString::fromLocal8Bit(errorOutput).trimmed()));
return;
}
@@ -421,7 +422,7 @@ void JavaInstaller::startTarExtraction()
void JavaInstaller::beginMojang()
{
setStage(tr("Список файлов Java %1").arg(m_entry.version));
setStage(Loc::text("java.progress.fileList").arg(m_entry.version));
m_currentFile = QStringLiteral("manifest.json");
QNetworkRequest request{m_entry.url};
@@ -445,7 +446,7 @@ void JavaInstaller::beginMojang()
if (!m_running)
return;
if (reply->error() != QNetworkReply::NoError) {
fail(tr("Не удалось получить состав рантайма: %1").arg(reply->errorString()));
fail(Loc::text("java.error.manifestFetchFailed").arg(reply->errorString()));
return;
}
handleManifest(reply->readAll());
@@ -459,13 +460,13 @@ void JavaInstaller::handleManifest(const QByteArray &payload)
.value(QStringLiteral("files"))
.toObject();
if (files.isEmpty()) {
fail(tr("Состав рантайма Java не удалось разобрать"));
fail(Loc::text("java.error.manifestUnparsable"));
return;
}
if (!prepareTree(files))
return;
setStage(tr("Загрузка Java %1").arg(m_entry.version));
setStage(Loc::text("java.progress.downloadingBuild").arg(m_entry.version));
if (m_queue.isEmpty()) {
if (applyLinks())
complete();
@@ -481,7 +482,7 @@ bool JavaInstaller::prepareTree(const QJsonObject &files)
for (auto it = files.constBegin(); it != files.constEnd(); ++it) {
const QString target = m_targetDir + u'/' + it.key();
if (!isInside(m_targetDir, target)) {
fail(tr("Состав рантайма содержит недопустимый путь: %1").arg(it.key()));
fail(Loc::text("java.error.manifestBadPath").arg(it.key()));
return false;
}
@@ -490,7 +491,7 @@ bool JavaInstaller::prepareTree(const QJsonObject &files)
if (type == QLatin1String("directory")) {
if (!QDir().mkpath(target)) {
fail(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(target)));
fail(Loc::text("common.error.createFailed").arg(QDir::toNativeSeparators(target)));
return false;
}
continue;
@@ -546,7 +547,7 @@ void JavaInstaller::startTask(JavaFileTask task)
{
const QFileInfo info(task.path);
if (!QDir().mkpath(info.absolutePath())) {
fail(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(info.absolutePath())));
fail(Loc::text("common.error.createFailed").arg(QDir::toNativeSeparators(info.absolutePath())));
return;
}
@@ -554,7 +555,7 @@ void JavaInstaller::startTask(JavaFileTask task)
context->task = task;
context->file = std::make_unique<QSaveFile>(task.path);
if (!context->file->open(QIODevice::WriteOnly)) {
fail(tr("Не удалось открыть на запись %1").arg(QDir::toNativeSeparators(task.path)));
fail(Loc::text("common.error.openForWriteFailed").arg(QDir::toNativeSeparators(task.path)));
return;
}
@@ -614,7 +615,7 @@ void JavaInstaller::handleFileFinished(const std::shared_ptr<JavaActiveDownload>
if (!retryOrFail(context->task, reason))
return;
} else {
fail(tr("Не удалось скачать %1: %2")
fail(Loc::text("common.error.downloadFailed")
.arg(QDir(m_targetDir).relativeFilePath(context->task.path), reason));
return;
}
@@ -626,7 +627,7 @@ void JavaInstaller::handleFileFinished(const std::shared_ptr<JavaActiveDownload>
const QString actual = QString::fromLatin1(context->hash.result().toHex());
if (actual.compare(context->task.sha1, Qt::CaseInsensitive) != 0) {
context->file->cancelWriting();
if (!retryOrFail(context->task, tr("контрольная сумма не совпала")))
if (!retryOrFail(context->task, Loc::text("common.error.checksumMismatch")))
return;
pump();
return;
@@ -634,7 +635,7 @@ void JavaInstaller::handleFileFinished(const std::shared_ptr<JavaActiveDownload>
}
if (!context->file->commit()) {
fail(tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(context->task.path)));
fail(Loc::text("common.error.saveFailed").arg(QDir::toNativeSeparators(context->task.path)));
return;
}
if (context->task.executable)
@@ -649,7 +650,7 @@ void JavaInstaller::handleFileFinished(const std::shared_ptr<JavaActiveDownload>
bool JavaInstaller::retryOrFail(JavaFileTask task, const QString &reason)
{
if (++task.attempts >= kMaxAttempts) {
fail(tr("Не удалось скачать %1: %2")
fail(Loc::text("common.error.downloadFailed")
.arg(QDir(m_targetDir).relativeFilePath(task.path), reason));
return false;
}
@@ -673,7 +674,7 @@ bool JavaInstaller::applyLinks()
const QString source =
QDir::cleanPath(QFileInfo(linkPath).absolutePath() + u'/' + link.second);
if (!QFile::copy(source, linkPath)) {
fail(tr("Не удалось создать ссылку %1")
fail(Loc::text("java.error.linkFailed")
.arg(QDir(m_targetDir).relativeFilePath(linkPath)));
return false;
}
@@ -686,11 +687,11 @@ bool JavaInstaller::applyLinks()
void JavaInstaller::complete()
{
setStage(tr("Проверка сборки Java"));
setStage(Loc::text("java.progress.verifying"));
const QString javaPath = JavaRuntimeStore::locateBinary(m_targetDir);
if (javaPath.isEmpty()) {
fail(tr("В скачанной сборке не нашлось исполняемого файла java"));
fail(Loc::text("java.error.noExecutable"));
return;
}
#if !defined(Q_OS_WIN)
+5 -5
View File
@@ -1,4 +1,5 @@
#include "javalocator.h"
#include "localization.h"
#include "javaruntime.h"
@@ -173,7 +174,7 @@ QString JavaLocator::select(const QString &gameDir,
if (candidates.isEmpty()) {
if (error)
*error = QObject::tr("Java по указанному пути не найдена: %1")
*error = Loc::text("java.error.notAtPath")
.arg(QDir::toNativeSeparators(preferred));
return {};
}
@@ -182,7 +183,7 @@ QString JavaLocator::select(const QString &gameDir,
if (candidates.isEmpty()) {
if (error)
*error = QObject::tr("Java не найдена. Установите JDK %1 или укажите путь в настройках.")
*error = Loc::text("java.error.notFound")
.arg(requiredMajor);
return {};
}
@@ -201,12 +202,11 @@ QString JavaLocator::select(const QString &gameDir,
if (error) {
*error = bestBelowMajor > 0
? QObject::tr("Для этой версии нужна Java %1 или новее, найдена только Java %2 (%3).")
? Loc::text("java.error.tooOld")
.arg(requiredMajor)
.arg(bestBelowMajor)
.arg(QDir::toNativeSeparators(bestBelow))
: QObject::tr("Не удалось определить версию установленной Java. "
"Укажите путь к JDK %1 в настройках.")
: Loc::text("java.error.versionUnknown")
.arg(requiredMajor);
}
return {};
+14 -19
View File
@@ -1,8 +1,8 @@
#include "javaruntime.h"
#include "localization.h"
#include "launcherpaths.h"
#include <QCoreApplication>
#include <QDir>
#include <QDirIterator>
#include <QFile>
@@ -24,11 +24,6 @@ constexpr auto kExecutable = "java";
const QString kMetaFile = QStringLiteral("runtime.json");
QString translate(const char *text)
{
return QCoreApplication::translate("JavaRuntime", text);
}
} // namespace
QString javaKindKey(JavaRuntimeKind kind)
@@ -55,9 +50,9 @@ std::optional<JavaRuntimeKind> javaKindFromKey(const QString &key)
QString javaKindTitle(JavaRuntimeKind kind)
{
switch (kind) {
case JavaRuntimeKind::Mojang: return translate("Java");
case JavaRuntimeKind::Jdk: return translate("JDK");
case JavaRuntimeKind::Jre: return translate("JRE");
case JavaRuntimeKind::Mojang: return Loc::text("common.java.kind.java");
case JavaRuntimeKind::Jdk: return Loc::text("common.java.kind.jdk");
case JavaRuntimeKind::Jre: return Loc::text("common.java.kind.jre");
}
return {};
}
@@ -159,7 +154,7 @@ bool JavaRuntimeStore::writeMeta(const InstalledJavaRuntime &runtime, QString *e
const QString dir = dirFor(runtime.id);
if (dir.isEmpty()) {
if (error)
*error = translate("недопустимый идентификатор сборки Java");
*error = Loc::text("java.error.invalidRuntimeId");
return false;
}
@@ -174,14 +169,14 @@ bool JavaRuntimeStore::writeMeta(const InstalledJavaRuntime &runtime, QString *e
QSaveFile file(dir + u'/' + kMetaFile);
if (!file.open(QIODevice::WriteOnly)) {
if (error)
*error = translate("не удалось записать %1").arg(QDir::toNativeSeparators(file.fileName()));
*error = Loc::text("java.error.writeFailed").arg(QDir::toNativeSeparators(file.fileName()));
return false;
}
const QByteArray payload = QJsonDocument(object).toJson(QJsonDocument::Compact);
if (file.write(payload) != payload.size() || !file.commit()) {
file.cancelWriting();
if (error)
*error = translate("не удалось записать %1").arg(QDir::toNativeSeparators(file.fileName()));
*error = Loc::text("java.error.writeFailed").arg(QDir::toNativeSeparators(file.fileName()));
return false;
}
return true;
@@ -194,14 +189,14 @@ bool JavaRuntimeStore::remove(const QString &id, QString *error)
// проверка стоит одной строки, а цена ошибки — чужая папка.
if (dir.isEmpty() || !dir.startsWith(LauncherPaths::javaDir() + u'/')) {
if (error)
*error = translate("недопустимый идентификатор сборки Java");
*error = Loc::text("java.error.invalidRuntimeId");
return false;
}
if (!QDir(dir).exists())
return true;
if (!QDir(dir).removeRecursively()) {
if (error)
*error = translate("не удалось удалить %1").arg(QDir::toNativeSeparators(dir));
*error = Loc::text("common.error.removeFailed").arg(QDir::toNativeSeparators(dir));
return false;
}
return true;
@@ -257,14 +252,14 @@ int JavaRequirement::minimumFor(const QString &minecraftVersionId)
QString JavaRequirement::coverage(int javaMajor)
{
if (javaMajor >= 21)
return translate("Minecraft 1.20.5 и новее");
return Loc::text("java.range.modern");
if (javaMajor >= 17)
return translate("Minecraft 1.18 — 1.20.4");
return Loc::text("java.range.mid");
if (javaMajor == 16)
return translate("Minecraft 1.17 — 1.17.1");
return Loc::text("java.range.seventeen");
if (javaMajor >= 9)
return translate("для Minecraft не подходит");
return Loc::text("java.range.unsuitable");
if (javaMajor > 0)
return translate("Minecraft 1.16.5 и старше");
return Loc::text("java.range.legacy");
return {};
}
+6 -5
View File
@@ -1,4 +1,5 @@
#include "javaruntimeservice.h"
#include "localization.h"
#include "launcherpaths.h"
@@ -242,7 +243,7 @@ void JavaRuntimeService::requestAdoptiumReleases()
connect(reply, &QNetworkReply::finished, this, [this, reply] {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
requestFinished(tr("Не удалось получить список версий Java: %1")
requestFinished(Loc::text("java.error.versionsFetchFailed")
.arg(reply->errorString()));
return;
}
@@ -260,7 +261,7 @@ void JavaRuntimeService::requestAdoptiumReleases()
majors << major;
}
if (majors.isEmpty()) {
requestFinished(tr("Список версий Java не удалось разобрать"));
requestFinished(Loc::text("java.error.versionsUnparsable"));
return;
}
@@ -315,7 +316,7 @@ void JavaRuntimeService::requestFeatureRelease(int major,
QString warning;
if (reply->error() != QNetworkReply::NoError && status != 404) {
warning = tr("Не удалось получить сборки Java %1: %2")
warning = Loc::text("java.error.buildsFetchFailed")
.arg(major)
.arg(reply->errorString());
}
@@ -330,7 +331,7 @@ void JavaRuntimeService::requestMojangRuntimes()
connect(reply, &QNetworkReply::finished, this, [this, reply] {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
requestFinished(tr("Не удалось получить рантаймы Mojang: %1")
requestFinished(Loc::text("java.error.mojangRuntimesFailed")
.arg(reply->errorString()));
return;
}
@@ -370,7 +371,7 @@ void JavaRuntimeService::finishRefresh()
for (const Callback &callback : pending) {
// Сеть подвела, но старые данные есть — работаем на них.
if (!ok && hasData())
callback(true, warning.isEmpty() ? tr("Список сборок Java взят из кэша") : warning);
callback(true, warning.isEmpty() ? Loc::text("java.status.catalogFromCache") : warning);
else
callback(ok, warning);
}
+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},
+3 -8
View File
@@ -1,6 +1,6 @@
#include "launcherpaths.h"
#include "localization.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
@@ -8,11 +8,6 @@ namespace {
const QString kFolderName = QStringLiteral("galeonLauncher");
QString translate(const char *text)
{
return QCoreApplication::translate("LauncherPaths", text);
}
} // namespace
QString LauncherPaths::containerDir()
@@ -129,7 +124,7 @@ bool LauncherPaths::ensureRootExists(QString *error)
for (const QString &path : required) {
if (!QDir().mkpath(path)) {
if (error)
*error = translate("не удалось создать %1").arg(QDir::toNativeSeparators(path));
*error = Loc::text("storage.error.createFailed").arg(QDir::toNativeSeparators(path));
return false;
}
@@ -138,7 +133,7 @@ bool LauncherPaths::ensureRootExists(QString *error)
const QFileInfo info(path);
if (!info.isDir() || !info.isWritable()) {
if (error)
*error = translate("нет доступа на запись в %1").arg(QDir::toNativeSeparators(path));
*error = Loc::text("storage.error.notWritable").arg(QDir::toNativeSeparators(path));
return false;
}
}
+291
View File
@@ -0,0 +1,291 @@
#include "localization.h"
#include "launcherpaths.h"
#include <QCoreApplication>
#include <QFile>
#include <QJSEngine>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QLocale>
#include <QThread>
#include <QVariantMap>
#include <utility>
// ── Про потоки ──────────────────────────────────────────────────────────────
//
// Каталог (m_singles, m_lists, m_codes, m_displayNames) заполняется ровно один
// раз в load(), который отрабатывает в main() до того, как BuildSwitcher создаст
// свой QThread. Дальше эти поля только читаются, а копирование QString из хэша
// безопасно само по себе — поэтому Loc::text() зовётся из рабочего потока
// BuildArchiveWorker без единой блокировки.
//
// Единственное, что меняется на ходу, — m_index, и он QAtomicInt. В худшем
// случае сообщение, уже собиравшееся в момент переключения, уедет на прежнем
// языке; заметить это невозможно.
//
// setLanguage(), m_tree, rebuildTree() и languageChanged — только GUI-поток.
namespace {
const QString kCatalogPath =
QStringLiteral(":/qt/qml/Minecraft_launcher/i18n/translations.json");
const QString kSystem = QStringLiteral("system");
// Язык по системной локали. Промежуточных вариантов нет: языков в каталоге два.
QString systemLanguage()
{
return QLocale::system().language() == QLocale::Russian ? QStringLiteral("ru")
: QStringLiteral("en");
}
// Язык из settings.json. Читается напрямую, не через LauncherBackend: тот
// появляется только вместе с QML, а язык нужен раньше первой привязки.
// Обратной зависимости это не создаёт — файл только читается.
QString storedLanguage()
{
QFile file(LauncherPaths::settingsFile());
if (!file.open(QIODevice::ReadOnly))
return kSystem;
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isObject())
return kSystem;
const QJsonValue value = doc.object().value(QStringLiteral("language"));
return value.isString() ? value.toString() : kSystem;
}
} // namespace
Localization::Localization(QObject *parent)
: QObject(parent)
{
}
Localization &Localization::instance()
{
static Localization loc;
return loc;
}
Localization *Localization::create(QQmlEngine *, QJSEngine *jsEngine)
{
Localization &loc = instance();
loc.m_engine = jsEngine;
loc.rebuildTree();
// Синглтон живёт до конца процесса и движку не принадлежит — иначе сборщик
// мусора QML попытается удалить статический объект.
QJSEngine::setObjectOwnership(&loc, QJSEngine::CppOwnership);
return &loc;
}
bool Localization::load()
{
QFile file(kCatalogPath);
if (!file.open(QIODevice::ReadOnly)) {
qCritical("Каталог переводов не найден: %s", qPrintable(kCatalogPath));
return false;
}
QJsonParseError error{};
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qCritical("Каталог переводов испорчен: %s (смещение %d)",
qPrintable(error.errorString()),
error.offset);
return false;
}
if (!doc.isObject()) {
qCritical("Каталог переводов должен быть объектом JSON");
return false;
}
const QJsonObject root = doc.object();
const QJsonObject meta = root.value(QStringLiteral("_meta")).toObject();
const QJsonArray languages = meta.value(QStringLiteral("languages")).toArray();
m_codes.clear();
for (const QJsonValue &code : languages)
m_codes << code.toString();
if (m_codes.isEmpty()) {
qCritical("В каталоге переводов не перечислен ни один язык (_meta.languages)");
return false;
}
const QJsonObject names = meta.value(QStringLiteral("displayNames")).toObject();
m_displayNames.clear();
for (auto it = names.constBegin(); it != names.constEnd(); ++it)
m_displayNames.insert(it.key(), it.value().toString());
const QJsonObject strings = root.value(QStringLiteral("strings")).toObject();
if (strings.isEmpty()) {
qCritical("В каталоге переводов нет ни одной строки (strings)");
return false;
}
m_singles.clear();
m_lists.clear();
for (auto it = strings.constBegin(); it != strings.constEnd(); ++it) {
const QJsonObject entry = it.value().toObject();
// Тип значения определяется по первому языку и обязан совпадать у всех:
// за этим следит tools/check_translations.py.
const bool isList = entry.value(m_codes.first()).isArray();
if (isList) {
QList<QStringList> perLanguage;
perLanguage.reserve(m_codes.size());
for (const QString &code : std::as_const(m_codes)) {
QStringList values;
const QJsonArray array = entry.value(code).toArray();
for (const QJsonValue &value : array)
values << value.toString();
perLanguage << values;
}
m_lists.insert(it.key(), perLanguage);
} else {
QStringList perLanguage;
perLanguage.reserve(m_codes.size());
for (const QString &code : std::as_const(m_codes))
perLanguage << entry.value(code).toString();
m_singles.insert(it.key(), perLanguage);
}
}
m_language = storedLanguage();
if (m_language != kSystem && !m_codes.contains(m_language)) {
qWarning("Неизвестный язык в настройках: %s — берём системный",
qPrintable(m_language));
m_language = kSystem;
}
m_index.storeRelaxed(int(m_codes.indexOf(resolvedLanguage())));
#ifndef QT_NO_DEBUG
for (auto it = m_singles.constBegin(); it != m_singles.constEnd(); ++it) {
for (int i = 0; i < m_codes.size(); ++i) {
if (it.value().value(i).isEmpty()) {
qWarning("Пустой перевод: ключ %s, язык %s",
qPrintable(it.key()),
qPrintable(m_codes.at(i)));
}
}
}
#endif
return true;
}
QString Localization::resolvedLanguage() const
{
return m_language == kSystem ? systemLanguage() : m_language;
}
void Localization::setLanguage(const QString &code)
{
Q_ASSERT(QThread::currentThread() == qApp->thread());
QString wanted = code;
if (wanted != kSystem && !m_codes.contains(wanted)) {
qWarning("Неизвестный язык: %s — берём системный", qPrintable(wanted));
wanted = kSystem;
}
if (wanted == m_language)
return;
m_language = wanted;
m_index.storeRelaxed(int(m_codes.indexOf(resolvedLanguage())));
rebuildTree();
emit languageChanged();
}
QVariantList Localization::availableLanguages() const
{
QVariantList out;
out << QVariantMap{{QStringLiteral("code"), kSystem},
{QStringLiteral("name"), textFor("settings.language.system")}};
for (const QString &code : m_codes) {
out << QVariantMap{{QStringLiteral("code"), code},
{QStringLiteral("name"), m_displayNames.value(code, code)}};
}
return out;
}
QString Localization::textFor(const char *key) const
{
const QString id = QString::fromLatin1(key);
const auto it = m_singles.constFind(id);
if (it == m_singles.constEnd()) {
#ifndef QT_NO_DEBUG
qWarning("Localization: нет ключа %s", key);
#endif
return id;
}
return it.value().value(m_index.loadRelaxed());
}
QStringList Localization::listFor(const char *key) const
{
const QString id = QString::fromLatin1(key);
const auto it = m_lists.constFind(id);
if (it == m_lists.constEnd()) {
#ifndef QT_NO_DEBUG
qWarning("Localization: нет списка %s", key);
#endif
return {id};
}
return it.value().value(m_index.loadRelaxed());
}
namespace {
// Раскладывает плоский ключ "a.b.c" по вложенным картам. Рекурсия, а не спуск
// указателем: QVariant не отдаёт ссылку на вложенную карту, её приходится
// доставать копией и класть обратно.
void insertPath(QVariantMap &node, const QStringList &parts, int depth, const QVariant &value)
{
if (depth == parts.size() - 1) {
node[parts.at(depth)] = value;
return;
}
QVariantMap child = node.value(parts.at(depth)).toMap();
insertPath(child, parts, depth + 1, value);
node[parts.at(depth)] = child;
}
} // namespace
// Дерево для QML: плоские ключи "a.b.c" разбираются в вложенные объекты, чтобы
// в разметке писалось Loc.t.a.b.c. Строится один раз на смену языка.
void Localization::rebuildTree()
{
if (!m_engine)
return;
const int index = m_index.loadRelaxed();
QVariantMap root;
for (auto it = m_singles.constBegin(); it != m_singles.constEnd(); ++it)
insertPath(root, it.key().split(QLatin1Char('.')), 0, it.value().value(index));
for (auto it = m_lists.constBegin(); it != m_lists.constEnd(); ++it)
insertPath(root, it.key().split(QLatin1Char('.')), 0, it.value().value(index));
m_tree = m_engine->toScriptValue(root);
}
namespace Loc {
QString text(const char *key)
{
return Localization::instance().textFor(key);
}
QStringList list(const char *key)
{
return Localization::instance().listFor(key);
}
} // namespace Loc
+108
View File
@@ -0,0 +1,108 @@
#pragma once
#include <QAtomicInt>
#include <QHash>
#include <QJSValue>
#include <QList>
#include <QObject>
#include <QString>
#include <QStringList>
#include <QVariantList>
#include <QtQml/qqmlregistration.h>
class QQmlEngine;
class QJSEngine;
// Единственный источник всех текстов интерфейса. Каталог — i18n/translations.json,
// он лежит в ресурсах и правится руками; штатных .ts/.qm в проекте нет намеренно.
//
// Из QML читается свойством: text: Loc.t.settings.title
// Из C++ — свободной функцией: Loc::text("settings.title")
//
// Почему свойство, а не Q_INVOKABLE: вызов метода не регистрирует зависимость
// привязки, и text: Loc.t("ключ") никогда бы не обновился при смене языка.
// Чтение же Q_PROPERTY с сигналом NOTIFY зависимость регистрирует, поэтому
// languageChanged перевычисляет все привязки разом. Дальнейшие .settings.title —
// обычные обращения к членам JS-объекта, и отслеживать их не нужно: при смене
// языка дерево заменяется целиком.
//
// Тип свойства — QJSValue, а не QVariantMap: QVariantMap пересобирался бы в новый
// JS-объект при каждом чтении, а привязок в проекте полторы сотни. QJSValue
// строится один раз на смену языка.
class Localization : public QObject
{
Q_OBJECT
QML_NAMED_ELEMENT(Loc)
QML_SINGLETON
Q_PROPERTY(QJSValue t READ t NOTIFY languageChanged)
// Что выбрано пользователем: "system", "ru" или "en".
Q_PROPERTY(QString language READ language NOTIFY languageChanged)
// Во что это разрешилось: всегда "ru" или "en", никогда "system".
Q_PROPERTY(QString resolvedLanguage READ resolvedLanguage NOTIFY languageChanged)
public:
static Localization &instance();
// Фабрика синглтона для движка QML. Объект к этому моменту уже загружен
// из main(), здесь только запоминается движок и строится дерево.
static Localization *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine);
// Читает каталог из ресурсов и язык из settings.json. Вызывается ровно один
// раз из main() до создания QQmlApplicationEngine. false — каталога нет или
// он битый; это ошибка сборки, и запускаться дальше незачем.
bool load();
QJSValue t() const { return m_tree; }
QString language() const { return m_language; }
QString resolvedLanguage() const;
// Только из GUI-потока: меняет дерево и рассылает languageChanged.
void setLanguage(const QString &code);
// Для комбобокса в настройках: [{ "code": "ru", "name": "Русский" }, …],
// первым пунктом — "system".
Q_INVOKABLE QVariantList availableLanguages() const;
// Рабочие лошадки Loc::text/Loc::list. Потокобезопасны: см. комментарий к
// полям ниже.
[[nodiscard]] QString textFor(const char *key) const;
[[nodiscard]] QStringList listFor(const char *key) const;
signals:
void languageChanged();
private:
explicit Localization(QObject *parent = nullptr);
void rebuildTree();
// Каталог заполняется один раз в load() и дальше только читается — поэтому
// обращаться к нему можно из любого потока без блокировок. Единственное, что
// меняется на ходу, — m_index, и он атомарный.
QHash<QString, QStringList> m_singles; // ключ -> значения по языкам
QHash<QString, QList<QStringList>> m_lists; // ключ -> списки по языкам
QStringList m_codes; // ["ru", "en"] — порядок из _meta
QHash<QString, QString> m_displayNames;
QAtomicInt m_index{0}; // индекс текущего языка в m_codes
QString m_language{QStringLiteral("system")};
QJSEngine *m_engine = nullptr;
QJSValue m_tree;
};
// ── Доступ из C++ ───────────────────────────────────────────────────────────
//
// Свободные функции, а не методы: их вызывают и из namespace-обёрток
// (LauncherPaths, JavaLocator, ZlibReference), где никакого QObject нет.
namespace Loc {
// Перевод по ключу. Ключа нет — возвращается сам ключ, в отладочной сборке ещё
// и qWarning: строка вида "launch.error.gameRunning" в интерфейсе сразу видна.
[[nodiscard]] QString text(const char *key);
// Для ключей со значением-списком (модели комбобоксов).
[[nodiscard]] QStringList list(const char *key);
} // namespace Loc
+9
View File
@@ -1,3 +1,5 @@
#include "localization.h"
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickStyle>
@@ -17,6 +19,13 @@ int main(int argc, char *argv[])
QGuiApplication app(argc, argv);
// Язык интерфейса нужно знать до того, как QML вычислит первую привязку,
// поэтому каталог читается здесь, а не в LauncherBackend: тот появляется
// только вместе с движком. Каталог лежит в ресурсах — не прочитался, значит
// сломана сборка, и запускаться дальше незачем.
if (!Localization::instance().load())
return -1;
// Нативный стиль Windows игнорирует пользовательские contentItem/background
// и сыплет предупреждениями, а всё окно стилизовано вручную — берём Basic.
QQuickStyle::setStyle("Basic");
+9 -8
View File
@@ -1,4 +1,5 @@
#include "minecraftversion.h"
#include "localization.h"
#include <QDir>
#include <QDirIterator>
@@ -373,7 +374,7 @@ bool VersionLoader::remove(const QString &gameDir, const QString &versionId, QSt
const QString root = versionsRoot(gameDir);
if (gameDir.isEmpty() || !isSafeVersionId(versionId)) {
if (error)
*error = QObject::tr("недопустимый идентификатор версии");
*error = Loc::text("version.error.invalidId");
return false;
}
@@ -382,14 +383,14 @@ bool VersionLoader::remove(const QString &gameDir, const QString &versionId, QSt
const QString dir = QDir::cleanPath(root + u'/' + versionId);
if (!dir.startsWith(root + u'/')) {
if (error)
*error = QObject::tr("недопустимый идентификатор версии");
*error = Loc::text("version.error.invalidId");
return false;
}
if (!QDir(dir).exists())
return true;
if (!QDir(dir).removeRecursively()) {
if (error)
*error = QObject::tr("не удалось удалить %1").arg(QDir::toNativeSeparators(dir));
*error = Loc::text("common.error.removeFailed").arg(QDir::toNativeSeparators(dir));
return false;
}
return true;
@@ -410,12 +411,12 @@ MinecraftVersion VersionLoader::load(const QString &gameDir,
while (!currentId.isEmpty()) {
if (chainIds.contains(currentId)) {
if (error)
*error = QObject::tr("Циклическая зависимость inheritsFrom в версии «%1»").arg(currentId);
*error = Loc::text("version.error.inheritsCycle").arg(currentId);
return version;
}
if (chain.size() >= kMaxInheritanceDepth) {
if (error)
*error = QObject::tr("Слишком длинная цепочка inheritsFrom у версии «%1»").arg(versionId);
*error = Loc::text("common.error.inheritsChainTooLong").arg(versionId);
return version;
}
@@ -423,7 +424,7 @@ MinecraftVersion VersionLoader::load(const QString &gameDir,
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
if (error)
*error = QObject::tr("Не найден файл версии: %1").arg(QDir::toNativeSeparators(path));
*error = Loc::text("version.error.fileMissing").arg(QDir::toNativeSeparators(path));
return version;
}
@@ -431,7 +432,7 @@ MinecraftVersion VersionLoader::load(const QString &gameDir,
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
if (error)
*error = QObject::tr("Повреждён файл версии %1: %2")
*error = Loc::text("version.error.fileCorrupt")
.arg(QDir::toNativeSeparators(path), parseError.errorString());
return version;
}
@@ -544,7 +545,7 @@ MinecraftVersion VersionLoader::load(const QString &gameDir,
version.clientJarPath = versionJarPath(gameDir, chainIds.constLast());
if (version.mainClass.isEmpty() && error)
*error = QObject::tr("В файле версии «%1» не указан mainClass").arg(versionId);
*error = Loc::text("version.error.noMainClass").arg(versionId);
return version;
}
+33 -35
View File
@@ -1,4 +1,5 @@
#include "modloaderinstaller.h"
#include "localization.h"
#include "gamelauncher.h"
#include "javalocator.h"
@@ -106,11 +107,11 @@ void ModLoaderInstaller::install(const QString &gameDir,
const QString &javaPreference)
{
if (m_running) {
emit failed(loaderTitle(loader), tr("Установка модлоадера уже идёт"));
emit failed(loaderTitle(loader), Loc::text("loader.error.installInProgress"));
return;
}
if (gameVersion.trimmed().isEmpty() || loaderVersion.trimmed().isEmpty()) {
emit failed(loaderTitle(loader), tr("Не выбрана версия %1").arg(loaderTitle(loader)));
emit failed(loaderTitle(loader), Loc::text("loader.error.noVersionSelected").arg(loaderTitle(loader)));
return;
}
@@ -128,7 +129,7 @@ void ModLoaderInstaller::install(const QString &gameDir,
m_running = true;
emit started(m_label);
setStage(tr("Подготовка %1").arg(loaderTitle(loader)));
setStage(Loc::text("loader.progress.preparing").arg(loaderTitle(loader)));
// Список версий нужен и для ссылки на installer.jar, и для точного id
// профиля. После перезапуска лаунчера он ещё не прочитан с диска, поэтому
@@ -137,7 +138,7 @@ void ModLoaderInstaller::install(const QString &gameDir,
if (!m_running || m_canceling)
return;
if (!ok) {
fail(warning.isEmpty() ? tr("Список версий %1 недоступен").arg(loaderTitle(loader))
fail(warning.isEmpty() ? Loc::text("loader.error.listUnavailable").arg(loaderTitle(loader))
: warning);
return;
}
@@ -191,14 +192,14 @@ void ModLoaderInstaller::beginFabricLike()
+ m_loaderVersion + u'-' + m_gameVersion;
if (!isSafeSegment(expectedId)) {
fail(tr("Недопустимый идентификатор версии «%1»").arg(expectedId));
fail(Loc::text("loader.error.invalidVersionId").arg(expectedId));
return;
}
const QString base = m_loader == ModLoader::Fabric ? kFabricProfile : kQuiltProfile;
const QUrl url(base + m_gameVersion + u'/' + m_loaderVersion + QStringLiteral("/profile/json"));
setStage(tr("Описание версии %1").arg(loaderTitle(m_loader)));
setStage(Loc::text("loader.progress.description").arg(loaderTitle(m_loader)));
m_currentFile = expectedId + QStringLiteral(".json");
emit progressChanged();
@@ -216,7 +217,7 @@ void ModLoaderInstaller::beginFabricLike()
return;
if (reply->error() != QNetworkReply::NoError) {
fail(tr("Не удалось получить описание %1: %2")
fail(Loc::text("loader.error.descriptionFetchFailed")
.arg(loaderTitle(m_loader), reply->errorString()));
return;
}
@@ -229,21 +230,21 @@ void ModLoaderInstaller::beginFabricLike()
// Описание приходит из сети и превращается в путь на диске — проверяем
// и id, и то, что версия действительно наследует выбранную ванильную.
if (id.isEmpty() || !isSafeSegment(id)) {
fail(tr("Описание %1 испорчено: недопустимый идентификатор версии")
fail(Loc::text("loader.error.descriptionBadId")
.arg(loaderTitle(m_loader)));
return;
}
if (inherits != m_gameVersion) {
fail(tr("Описание %1 рассчитано на Minecraft %2, а не %3")
fail(Loc::text("loader.error.descriptionWrongMinecraft")
.arg(loaderTitle(m_loader), inherits, m_gameVersion));
return;
}
if (id != expectedId)
emit log(tr("Идентификатор версии: %1").arg(id));
emit log(Loc::text("loader.status.versionId").arg(id));
const QString directory = m_gameDir + QStringLiteral("/versions/") + id;
if (!QDir().mkpath(directory)) {
fail(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(directory)));
fail(Loc::text("common.error.createFailed").arg(QDir::toNativeSeparators(directory)));
return;
}
@@ -251,7 +252,7 @@ void ModLoaderInstaller::beginFabricLike()
if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()
|| !file.commit()) {
file.cancelWriting();
fail(tr("Не удалось сохранить описание версии «%1»").arg(id));
fail(Loc::text("common.error.versionSaveFailed").arg(id));
return;
}
@@ -265,12 +266,12 @@ void ModLoaderInstaller::beginInstallerJar()
{
const auto entry = m_meta->find(m_loader, m_gameVersion, m_loaderVersion);
if (!entry || !entry->installerUrl.isValid()) {
fail(tr("%1 %2 не подходит к Minecraft %3 — выберите версию из списка")
fail(Loc::text("loader.error.incompatible")
.arg(loaderTitle(m_loader), m_loaderVersion, m_gameVersion));
return;
}
if (entry->installerUrl.scheme() != QLatin1String("https")) {
fail(tr("Установщик %1 доступен только по недопустимой ссылке").arg(loaderTitle(m_loader)));
fail(Loc::text("loader.error.installerBadUrl").arg(loaderTitle(m_loader)));
return;
}
@@ -296,7 +297,7 @@ void ModLoaderInstaller::ensureBaseVersion(std::function<void()> next)
}
}
setStage(tr("Установка Minecraft %1").arg(m_gameVersion));
setStage(Loc::text("loader.progress.installingMinecraft").arg(m_gameVersion));
m_currentFile = m_gameVersion;
emit progressChanged();
@@ -329,7 +330,7 @@ void ModLoaderInstaller::ensureBaseVersion(std::function<void()> next)
void ModLoaderInstaller::downloadInstaller(const QUrl &url, std::function<void(const QString &)> next)
{
if (!LauncherPaths::ensureRootExists()) {
fail(tr("Нет доступа к папке лаунчера"));
fail(Loc::text("loader.error.launcherDirUnavailable"));
return;
}
@@ -337,7 +338,7 @@ void ModLoaderInstaller::downloadInstaller(const QUrl &url, std::function<void(c
+ QString(m_loaderVersion).replace(u'/', u'_')
+ QStringLiteral("-installer.jar");
setStage(tr("Загрузка установщика %1").arg(loaderTitle(m_loader)));
setStage(Loc::text("loader.progress.downloadingInstaller").arg(loaderTitle(m_loader)));
m_currentFile = QFileInfo(target).fileName();
emit progressChanged();
@@ -361,7 +362,7 @@ void ModLoaderInstaller::downloadInstaller(const QUrl &url, std::function<void(c
return;
if (reply->error() != QNetworkReply::NoError) {
fail(tr("Не удалось скачать установщик %1: %2")
fail(Loc::text("loader.error.installerDownloadFailed")
.arg(loaderTitle(m_loader), reply->errorString()));
return;
}
@@ -371,7 +372,7 @@ void ModLoaderInstaller::downloadInstaller(const QUrl &url, std::function<void(c
if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()
|| !file.commit()) {
file.cancelWriting();
fail(tr("Не удалось сохранить установщик %1").arg(loaderTitle(m_loader)));
fail(Loc::text("loader.error.installerSaveFailed").arg(loaderTitle(m_loader)));
return;
}
next(target);
@@ -413,7 +414,7 @@ bool ModLoaderInstaller::writeLauncherProfilesStub()
void ModLoaderInstaller::runInstaller(const QString &installerPath)
{
if (!writeLauncherProfilesStub()) {
fail(tr("Не удалось создать launcher_profiles.json — без него установщик %1 не работает")
fail(Loc::text("loader.error.noLauncherProfiles")
.arg(loaderTitle(m_loader)));
return;
}
@@ -427,7 +428,7 @@ void ModLoaderInstaller::runInstaller(const QString &installerPath)
QString javaError;
const QString java = JavaLocator::select(m_gameDir, requiredMajor, m_javaPreference, &javaError);
if (java.isEmpty()) {
fail(javaError.isEmpty() ? tr("Java не найдена — установщик %1 запустить нечем")
fail(javaError.isEmpty() ? Loc::text("loader.error.noJava")
.arg(loaderTitle(m_loader))
: javaError);
return;
@@ -448,8 +449,8 @@ void ModLoaderInstaller::runInstaller(const QString &installerPath)
m_installerLogPath.clear();
}
setStage(tr("Установка %1").arg(loaderTitle(m_loader)));
m_currentFile = tr("работает установщик, это может занять несколько минут");
setStage(Loc::text("loader.progress.installing").arg(loaderTitle(m_loader)));
m_currentFile = Loc::text("loader.progress.installerRunning");
// Прогресс установщика неизвестен: панель покажет «…».
m_bytesDone = 0;
m_bytesTotal = 0;
@@ -493,13 +494,13 @@ void ModLoaderInstaller::runInstaller(const QString &installerPath)
connect(m_process, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
if (m_canceling || error == QProcess::Crashed)
return; // Crashed придёт ещё и в finished
fail(tr("Не удалось запустить установщик %1: %2")
fail(Loc::text("loader.error.installerStartFailed")
.arg(loaderTitle(m_loader), m_process->errorString()));
});
m_process->start();
if (!m_process->waitForStarted(kTransferTimeoutMs)) {
fail(tr("Установщик %1 не запустился: %2")
fail(Loc::text("loader.error.installerFailedToStart")
.arg(loaderTitle(m_loader), m_process->errorString()));
return;
}
@@ -508,7 +509,7 @@ void ModLoaderInstaller::runInstaller(const QString &installerPath)
QTimer::singleShot(kInstallerTimeoutMs, this, [this] {
if (m_process && m_process->state() != QProcess::NotRunning) {
m_process->kill();
fail(tr("Установщик %1 не завершился за отведённое время").arg(loaderTitle(m_loader)));
fail(Loc::text("loader.error.installerTimeout").arg(loaderTitle(m_loader)));
}
});
}
@@ -525,7 +526,7 @@ void ModLoaderInstaller::handleInstallerFinished(int exitCode, bool crashed)
if (crashed || exitCode != 0) {
removeVersionsAddedByInstaller();
QString message = tr("Установщик %1 завершился с ошибкой (код %2).")
QString message = Loc::text("loader.error.installerExitCode")
.arg(loaderTitle(m_loader))
.arg(exitCode);
const QString hint = installerFailureHint();
@@ -535,7 +536,7 @@ void ModLoaderInstaller::handleInstallerFinished(int exitCode, bool crashed)
message += u'\n' + m_installerLog.join(u'\n');
if (!m_installerLogPath.isEmpty())
message += u'\n'
+ tr("Полный лог: %1").arg(QDir::toNativeSeparators(m_installerLogPath));
+ Loc::text("loader.error.fullLog").arg(QDir::toNativeSeparators(m_installerLogPath));
fail(message);
return;
}
@@ -555,7 +556,7 @@ void ModLoaderInstaller::handleInstallerFinished(int exitCode, bool crashed)
}
if (added.isEmpty()) {
QString message = tr("Установщик %1 отработал, но новая версия в %2 не появилась.")
QString message = Loc::text("loader.error.noVersionProduced")
.arg(loaderTitle(m_loader),
QDir::toNativeSeparators(m_gameDir
+ QStringLiteral("/versions")));
@@ -603,7 +604,7 @@ void ModLoaderInstaller::removeVersionsAddedByInstaller()
for (const QString &id : added) {
QString error;
if (!VersionLoader::remove(m_gameDir, id, &error))
emit log(tr("Не удалось убрать незавершённый профиль «%1»: %2").arg(id, error));
emit log(Loc::text("loader.error.cleanupFailed").arg(id, error));
}
m_versionsBefore.clear();
m_installerStarted = false;
@@ -622,10 +623,7 @@ QString ModLoaderInstaller::installerFailureHint() const
if (!checksumFailure)
return {};
return tr("Системный libz.so.1 — это zlib-ng: он сжимает иначе, чем обычный zlib, "
"поэтому sha1 собранных установщиком файлов не совпадает с эталоном. "
"Пересоберите лаунчер, чтобы рядом с ним появился эталонный libz.so.1, "
"либо замените zlib-ng в системе: sudo pacman -S zlib lib32-zlib");
return Loc::text("loader.error.zlibNg");
}
void ModLoaderInstaller::appendInstallerLog(const QString &line)
@@ -650,7 +648,7 @@ void ModLoaderInstaller::closeInstallerLog()
void ModLoaderInstaller::handOffToVersionInstaller(const QString &versionId)
{
m_awaitingVersionId = versionId;
setStage(tr("Загрузка файлов версии"));
setStage(Loc::text("loader.progress.downloadingVersionFiles"));
m_currentFile = versionId;
emit progressChanged();
m_versionInstaller->install(m_gameDir, versionId);
+5 -4
View File
@@ -1,4 +1,5 @@
#include "modloaderversionservice.h"
#include "localization.h"
#include "launcherpaths.h"
@@ -229,7 +230,7 @@ void ModLoaderVersionService::startRefresh(ModLoader loader, const QString &game
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
finishRefresh(loader, gameVersion, false,
tr("Не удалось получить список версий %1: %2")
Loc::text("loader.error.versionsFetchFailed")
.arg(loaderTitle(loader), reply->errorString()));
return;
}
@@ -240,7 +241,7 @@ void ModLoaderVersionService::startRefresh(ModLoader loader, const QString &game
const QJsonDocument document = QJsonDocument::fromJson(payload);
if (!document.isArray()) {
finishRefresh(loader, gameVersion, false,
tr("Список версий %1 не удалось разобрать").arg(loaderTitle(loader)));
Loc::text("loader.error.versionsUnparsable").arg(loaderTitle(loader)));
return;
}
@@ -296,14 +297,14 @@ void ModLoaderVersionService::startRefresh(ModLoader loader, const QString &game
connect(primary, &QNetworkReply::finished, this, [this, primary, state, loader, settle] {
primary->deleteLater();
if (primary->error() != QNetworkReply::NoError) {
state->warning = tr("Не удалось получить список версий %1: %2")
state->warning = Loc::text("loader.error.versionsFetchFailed")
.arg(loaderTitle(loader), primary->errorString());
settle();
return;
}
auto parsed = parseMavenMetadata(primary->readAll(), loader, /*legacyNeoForge=*/false);
if (parsed.isEmpty()) {
state->warning = tr("Список версий %1 не удалось разобрать").arg(loaderTitle(loader));
state->warning = Loc::text("loader.error.versionsUnparsable").arg(loaderTitle(loader));
settle();
return;
}
+31 -35
View File
@@ -1,4 +1,5 @@
#include "msaauthservice.h"
#include "localization.h"
#include <QDateTime>
#include <QJsonArray>
@@ -64,21 +65,19 @@ QString describeXstsError(const QJsonObject &response)
const qint64 code = static_cast<qint64>(response.value(QStringLiteral("XErr")).toDouble());
switch (code) {
case 2148916233:
return MsaAuthService::tr("К аккаунту Microsoft не привязан профиль Xbox. "
"Создайте его на xbox.com и повторите вход.");
return Loc::text("auth.msa.error.noXboxProfile");
case 2148916235:
return MsaAuthService::tr("Xbox Live недоступен в стране, указанной в аккаунте");
return Loc::text("auth.msa.error.countryBlocked");
case 2148916236:
case 2148916237:
return MsaAuthService::tr("Аккаунту нужна проверка возраста на xbox.com");
return Loc::text("auth.msa.error.ageVerification");
case 2148916238:
return MsaAuthService::tr("Детский аккаунт: добавьте его в семейную группу "
"Microsoft, иначе вход в Xbox Live невозможен");
return Loc::text("auth.msa.error.childAccount");
default:
break;
}
const QString message = response.value(QStringLiteral("Message")).toString();
return message.isEmpty() ? MsaAuthService::tr("Xbox Live отклонил вход (XErr %1)").arg(code)
return message.isEmpty() ? Loc::text("auth.msa.error.xboxRejected").arg(code)
: message;
}
@@ -90,7 +89,7 @@ QString describeOauthError(const QJsonObject &response, int status)
const QString error = response.value(QStringLiteral("error")).toString();
if (!error.isEmpty())
return error;
return MsaAuthService::tr("Microsoft вернул код %1").arg(status);
return Loc::text("auth.msa.error.microsoftCode").arg(status);
}
} // namespace
@@ -216,7 +215,7 @@ void MsaAuthService::loginWithCode(const QString &code, Callback callback)
{
if (code.isEmpty()) {
AuthResult result;
result.error = tr("Microsoft не вернул код авторизации");
result.error = Loc::text("auth.msa.error.noAuthCode");
callback(result);
return;
}
@@ -227,7 +226,7 @@ void MsaAuthService::loginWithCode(const QString &code, Callback callback)
form.addQueryItem(QStringLiteral("grant_type"), QStringLiteral("authorization_code"));
form.addQueryItem(QStringLiteral("redirect_uri"), kRedirectUri);
emit progress(tr("Обмен кода Microsoft на токен…"));
emit progress(Loc::text("auth.msa.progress.exchangingCode"));
exchangeToken(form.toString(QUrl::FullyEncoded).toUtf8(), std::move(callback));
}
@@ -235,7 +234,7 @@ void MsaAuthService::loginWithRefreshToken(const QString &refreshToken, Callback
{
if (refreshToken.isEmpty()) {
AuthResult result;
result.error = tr("Профиль не подключён к аккаунту Microsoft");
result.error = Loc::text("auth.msa.error.profileNotLinked");
callback(result);
return;
}
@@ -247,7 +246,7 @@ void MsaAuthService::loginWithRefreshToken(const QString &refreshToken, Callback
form.addQueryItem(QStringLiteral("redirect_uri"), kRedirectUri);
form.addQueryItem(QStringLiteral("scope"), kScope);
emit progress(tr("Обновление сессии Microsoft…"));
emit progress(Loc::text("auth.msa.progress.refreshingSession"));
exchangeToken(form.toString(QUrl::FullyEncoded).toUtf8(), std::move(callback));
}
@@ -257,7 +256,7 @@ void MsaAuthService::exchangeToken(const QByteArray &form, Callback callback)
[this, callback](int status, const QJsonObject &response, const QString &transportError) {
if (!transportError.isEmpty()) {
AuthResult result;
result.error = tr("Нет связи с сервером Microsoft: %1").arg(transportError);
result.error = Loc::text("auth.msa.error.microsoftUnreachable").arg(transportError);
callback(result);
return;
}
@@ -274,7 +273,7 @@ void MsaAuthService::exchangeToken(const QByteArray &form, Callback callback)
const QString refreshToken = response.value(QStringLiteral("refresh_token")).toString();
if (accessToken.isEmpty()) {
AuthResult result;
result.error = tr("Microsoft не вернул токен доступа");
result.error = Loc::text("auth.msa.error.noAccessToken");
callback(result);
return;
}
@@ -286,7 +285,7 @@ void MsaAuthService::authenticateXbox(const QString &microsoftToken,
const QString &refreshToken,
Callback callback)
{
emit progress(tr("Вход в Xbox Live…"));
emit progress(Loc::text("auth.msa.progress.xboxSignIn"));
QJsonObject properties{{QStringLiteral("AuthMethod"), QStringLiteral("RPS")},
{QStringLiteral("SiteName"), QStringLiteral("user.auth.xboxlive.com")},
@@ -300,19 +299,19 @@ void MsaAuthService::authenticateXbox(const QString &microsoftToken,
const QString &transportError) {
AuthResult result;
if (!transportError.isEmpty()) {
result.error = tr("Нет связи с Xbox Live: %1").arg(transportError);
result.error = Loc::text("auth.msa.error.xboxUnreachable").arg(transportError);
callback(result);
return;
}
if (status != 200) {
result.error = tr("Xbox Live отклонил токен Microsoft (код %1)").arg(status);
result.error = Loc::text("auth.msa.error.xboxTokenRejected").arg(status);
callback(result);
return;
}
const QString token = response.value(QStringLiteral("Token")).toString();
if (token.isEmpty()) {
result.error = tr("Xbox Live не вернул токен");
result.error = Loc::text("auth.msa.error.xboxNoToken");
callback(result);
return;
}
@@ -324,7 +323,7 @@ void MsaAuthService::authorizeXsts(const QString &xblToken,
const QString &refreshToken,
Callback callback)
{
emit progress(tr("Проверка прав XSTS…"));
emit progress(Loc::text("auth.msa.progress.xsts"));
const QJsonObject properties{{QStringLiteral("SandboxId"), QStringLiteral("RETAIL")},
{QStringLiteral("UserTokens"), QJsonArray{xblToken}}};
@@ -338,7 +337,7 @@ void MsaAuthService::authorizeXsts(const QString &xblToken,
const QString &transportError) {
AuthResult result;
if (!transportError.isEmpty()) {
result.error = tr("Нет связи с сервером XSTS: %1").arg(transportError);
result.error = Loc::text("auth.msa.error.xstsUnreachable").arg(transportError);
callback(result);
return;
}
@@ -348,7 +347,7 @@ void MsaAuthService::authorizeXsts(const QString &xblToken,
return;
}
if (status != 200) {
result.error = tr("XSTS отклонил запрос (код %1)").arg(status);
result.error = Loc::text("auth.msa.error.xstsRejected").arg(status);
callback(result);
return;
}
@@ -357,7 +356,7 @@ void MsaAuthService::authorizeXsts(const QString &xblToken,
const QJsonObject claim = displayClaim(response);
const QString userHash = claim.value(QStringLiteral("uhs")).toString();
if (token.isEmpty() || userHash.isEmpty()) {
result.error = tr("XSTS не вернул данные пользователя");
result.error = Loc::text("auth.msa.error.xstsNoUser");
callback(result);
return;
}
@@ -372,7 +371,7 @@ void MsaAuthService::loginWithXbox(const QString &userHash,
const QString &refreshToken,
Callback callback)
{
emit progress(tr("Авторизация в Minecraft Services…"));
emit progress(Loc::text("auth.msa.progress.minecraftServices"));
const QJsonObject body{{QStringLiteral("identityToken"),
QStringLiteral("XBL3.0 x=%1;%2").arg(userHash, xstsToken)}};
@@ -382,19 +381,19 @@ void MsaAuthService::loginWithXbox(const QString &userHash,
const QString &transportError) {
AuthResult result;
if (!transportError.isEmpty()) {
result.error = tr("Нет связи с api.minecraftservices.com: %1").arg(transportError);
result.error = Loc::text("auth.msa.error.servicesUnreachable").arg(transportError);
callback(result);
return;
}
if (status != 200) {
result.error = tr("Minecraft Services отклонили вход (код %1)").arg(status);
result.error = Loc::text("auth.msa.error.servicesRejected").arg(status);
callback(result);
return;
}
result.accessToken = response.value(QStringLiteral("access_token")).toString();
if (result.accessToken.isEmpty()) {
result.error = tr("Minecraft Services не вернули токен доступа");
result.error = Loc::text("auth.msa.error.servicesNoToken");
callback(result);
return;
}
@@ -411,7 +410,7 @@ void MsaAuthService::loginWithXbox(const QString &userHash,
void MsaAuthService::checkLicense(AuthResult result, Callback callback)
{
emit progress(tr("Проверка лицензии Minecraft…"));
emit progress(Loc::text("auth.msa.progress.checkingLicence"));
getJson(QUrl(kEntitlementsUrl), result.accessToken,
[this, result, callback](int status, const QJsonObject &response, const QString &) {
@@ -430,7 +429,7 @@ void MsaAuthService::fetchProfile(AuthResult result, bool storeEntitlement, Call
[result, storeEntitlement, callback](int status, const QJsonObject &response,
const QString &transportError) mutable {
if (!transportError.isEmpty()) {
result.error = MsaAuthService::tr("Нет связи с api.minecraftservices.com: %1")
result.error = Loc::text("auth.msa.error.servicesUnreachable")
.arg(transportError);
callback(result);
return;
@@ -442,17 +441,14 @@ void MsaAuthService::fetchProfile(AuthResult result, bool storeEntitlement, Call
result.licenseMissing = true;
result.error =
storeEntitlement
? MsaAuthService::tr("На аккаунте Microsoft есть покупка, но игровой "
"профиль не создан. Зайдите один раз в "
"официальный лаунчер и выберите ник.")
: MsaAuthService::tr("На этом аккаунте Microsoft нет копии "
"Minecraft: Java Edition");
? Loc::text("auth.msa.error.noProfileYet")
: Loc::text("auth.msa.error.noLicence");
callback(result);
return;
}
if (status != 200) {
result.error =
MsaAuthService::tr("Не удалось получить профиль Minecraft (код %1)").arg(status);
Loc::text("auth.msa.error.profileFetchFailed").arg(status);
callback(result);
return;
}
@@ -462,7 +458,7 @@ void MsaAuthService::fetchProfile(AuthResult result, bool storeEntitlement, Call
result.ok = !result.playerName.isEmpty() && !result.uuid.isEmpty();
if (!result.ok) {
result.licenseMissing = true;
result.error = MsaAuthService::tr("Minecraft Services не вернули игровой профиль");
result.error = Loc::text("auth.msa.error.servicesNoProfile");
}
callback(result);
});
+6 -5
View File
@@ -1,4 +1,5 @@
#include "seasonalbuildservice.h"
#include "localization.h"
#include "launcherpaths.h"
#include "modloader.h"
@@ -121,8 +122,8 @@ QList<SeasonalBuildEntry> SeasonalBuildService::parseCatalog(const QJsonObject &
if (format != kSupportedFormatVersion) {
if (error) {
*error = format > kSupportedFormatVersion
? tr("Сервер сборок отдаёт формат %1 — обновите лаунчер").arg(format)
: tr("Неизвестный формат каталога сборок (%1)").arg(format);
? Loc::text("seasonal.error.formatTooNew").arg(format)
: Loc::text("seasonal.error.formatUnknown").arg(format);
}
return {};
}
@@ -200,7 +201,7 @@ QList<SeasonalBuildEntry> SeasonalBuildService::parseCatalog(const QJsonObject &
void SeasonalBuildService::ensureLoaded(Callback callback, bool forceRefresh)
{
if (!m_baseUrl.isValid() || m_baseUrl.host().isEmpty()) {
m_lastError = tr("Адрес сервера сборок не задан");
m_lastError = Loc::text("seasonal.error.noServerUrl");
if (callback) {
const QString error = m_lastError;
QTimer::singleShot(0, this, [callback = std::move(callback), error] {
@@ -252,7 +253,7 @@ void SeasonalBuildService::startRefresh()
if (reply->error() != QNetworkReply::NoError) {
finishRefresh(false,
tr("Не удалось получить список сборок: %1").arg(reply->errorString()));
Loc::text("seasonal.error.catalogFetchFailed").arg(reply->errorString()));
return;
}
@@ -265,7 +266,7 @@ void SeasonalBuildService::startRefresh()
// остаётся прежним, а причина уходит в lastError.
if (parsed.isEmpty()) {
finishRefresh(false,
parseError.isEmpty() ? tr("Список сборок пуст или не разобран")
parseError.isEmpty() ? Loc::text("seasonal.error.catalogEmpty")
: parseError);
return;
}
+8 -7
View File
@@ -1,4 +1,5 @@
#include "seasonalpackdownloader.h"
#include "localization.h"
#include <QDir>
#include <QFile>
@@ -50,7 +51,7 @@ void SeasonalPackDownloader::download(const QUrl &url,
const QString &label)
{
if (m_running) {
emit failed(label, tr("Загрузка сборки уже идёт"));
emit failed(label, Loc::text("seasonal.error.downloadInProgress"));
return;
}
@@ -61,18 +62,18 @@ void SeasonalPackDownloader::download(const QUrl &url,
m_label = label;
m_bytesDone = 0;
m_bytesTotal = expectedSize;
m_stage = tr("Загрузка файлов сборки");
m_stage = Loc::text("seasonal.progress.downloadingFiles");
m_currentFile = QFileInfo(targetPath).fileName();
if (!QDir().mkpath(QFileInfo(targetPath).absolutePath())) {
fail(tr("Не удалось создать %1")
fail(Loc::text("common.error.createFailed")
.arg(QDir::toNativeSeparators(QFileInfo(targetPath).absolutePath())));
return;
}
m_file = std::make_unique<QSaveFile>(targetPath);
if (!m_file->open(QIODevice::WriteOnly)) {
fail(tr("Не удалось открыть на запись %1").arg(QDir::toNativeSeparators(targetPath)));
fail(Loc::text("common.error.openForWriteFailed").arg(QDir::toNativeSeparators(targetPath)));
return;
}
m_hash.reset();
@@ -120,7 +121,7 @@ void SeasonalPackDownloader::download(const QUrl &url,
if (reply->error() != QNetworkReply::NoError) {
m_file->cancelWriting();
m_file.reset();
fail(tr("Не удалось скачать сборку: %1").arg(reply->errorString()));
fail(Loc::text("seasonal.error.downloadFailed").arg(reply->errorString()));
return;
}
@@ -128,13 +129,13 @@ void SeasonalPackDownloader::download(const QUrl &url,
if (actual.compare(m_sha256, Qt::CaseInsensitive) != 0) {
m_file->cancelWriting();
m_file.reset();
fail(tr("Контрольная сумма архива сборки не совпала — загрузка повреждена"));
fail(Loc::text("seasonal.error.checksumMismatch"));
return;
}
if (!m_file->commit()) {
m_file.reset();
fail(tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(m_targetPath)));
fail(Loc::text("common.error.saveFailed").arg(QDir::toNativeSeparators(m_targetPath)));
return;
}
m_file.reset();
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Проверка каталога переводов. Только чтение; ненулевой код возврата при ошибке.
Запуск: python3 tools/check_translations.py
"""
import io, json, os, re, sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CATALOG = os.path.join(ROOT, "i18n", "translations.json")
CPP_TEXT = re.compile(r'Loc::text\("([^"]+)"\)')
CPP_LIST = re.compile(r'Loc::list\("([^"]+)"\)')
QML_PATH = re.compile(r'\bLoc\.t((?:\.[A-Za-z_][A-Za-z0-9_]*)+)')
STRAGGLER = re.compile(r'\b(tr|qsTr)\("|QCoreApplication::translate')
errors, warnings = [], []
def sources(*exts):
for name in sorted(os.listdir(ROOT)):
if name.endswith(exts):
yield name, io.open(os.path.join(ROOT, name), encoding="utf-8").read()
doc = json.load(io.open(CATALOG, encoding="utf-8"))
languages = doc["_meta"]["languages"]
strings = doc["strings"]
# 1. Языки: одинаковый набор ключей, ничего не пустует, типы совпадают.
for key, entry in strings.items():
missing = [c for c in languages if c not in entry]
if missing:
errors.append(f"{key}: нет языков {missing}")
continue
kinds = {c: type(entry[c]).__name__ for c in languages}
if len(set(kinds.values())) != 1:
errors.append(f"{key}: разный тип значения по языкам — {kinds}")
continue
if isinstance(entry[languages[0]], list):
sizes = {c: len(entry[c]) for c in languages}
if len(set(sizes.values())) != 1:
errors.append(f"{key}: разная длина списка — {sizes}")
for c in languages:
if any(not v.strip() for v in entry[c]):
errors.append(f"{key}: пустой элемент списка в «{c}»")
else:
for c in languages:
if not entry[c].strip():
errors.append(f"{key}: пустой перевод в «{c}»")
# 2. Набор %N обязан совпадать: порядок слов может отличаться, состав — нет.
holders = {c: set(re.findall(r"%\d", entry[c])) for c in languages}
if len(set(map(frozenset, holders.values()))) != 1:
errors.append(f"{key}: разный набор подстановок — {holders}")
lists = {k for k, e in strings.items() if isinstance(e[languages[0]], list)}
singles = set(strings) - lists
# 3. Каждый ключ из C++ существует и совпадает по типу.
used = set()
for name, src in sources(".cpp", ".h"):
for key in CPP_TEXT.findall(src):
if key in singles:
used.add(key)
else:
errors.append(f"{name}: Loc::text(\"{key}\") — нет такого ключа-строки")
for key in CPP_LIST.findall(src):
if key in lists:
used.add(key)
else:
errors.append(f"{name}: Loc::list(\"{key}\") — нет такого ключа-списка")
# 4. Каждый путь Loc.t.a.b.c из QML разворачивается в существующий ключ.
for name, src in sources(".qml"):
for path in QML_PATH.findall(src):
key = path.lstrip(".")
if key in strings:
used.add(key)
continue
# Loc.t.a.b.slice(0, 2) и подобное: последний сегмент — метод JS.
parent = key.rsplit(".", 1)[0]
if parent in strings:
used.add(parent)
continue
errors.append(f"{name}: Loc.t.{key} — нет такого ключа")
# 5. Ни одного оставшегося tr()/qsTr()/translate.
for name, src in sources(".cpp", ".h", ".qml"):
for i, line in enumerate(src.splitlines(), 1):
if STRAGGLER.search(line):
errors.append(f"{name}:{i}: остался вызов старой схемы перевода")
# 6. Ключи, которые никто не использует, — предупреждение.
for key in sorted(set(strings) - used):
warnings.append(f"ключ {key} нигде не используется")
print(f"ключей: {len(strings)} языки: {languages} использовано: {len(used)}")
for w in warnings:
print(" предупреждение:", w)
for e in errors:
print(" ОШИБКА:", e)
print("ОШИБОК НЕТ" if not errors else f"\nошибок: {len(errors)}")
sys.exit(1 if errors else 0)
+32 -32
View File
@@ -1,4 +1,5 @@
#include "versioninstaller.h"
#include "localization.h"
#include "gamelauncher.h"
#include "versionmanifestservice.h"
@@ -178,11 +179,11 @@ void VersionInstaller::install(const QString &gameDir, const QString &versionId)
return;
if (!isSafeRelativeSegment(id)) {
emit failed(id, tr("Недопустимое имя версии «%1»").arg(id));
emit failed(id, Loc::text("version.error.invalidName").arg(id));
return;
}
if (m_canceling) {
emit failed(id, tr("Идёт отмена предыдущей загрузки — попробуйте ещё раз"));
emit failed(id, Loc::text("version.error.cancelPending"));
return;
}
if (m_versionId == id || isQueued(id))
@@ -225,7 +226,7 @@ void VersionInstaller::startNext()
m_running = true;
if (!wasRunning)
emit started(m_versionId);
setStage(tr("Получение списка версий"));
setStage(Loc::text("version.progress.fetchingList"));
emit progressChanged();
resolveManifestEntry();
@@ -244,7 +245,7 @@ void VersionInstaller::resolveManifestEntry()
adoptVersionJson(local.readAll(), /*alreadySaved=*/true);
return;
}
failInstall(warning.isEmpty() ? tr("Список версий Mojang недоступен") : warning);
failInstall(warning.isEmpty() ? Loc::text("version.error.listUnavailable") : warning);
return;
}
@@ -264,15 +265,14 @@ void VersionInstaller::resolveManifestEntry()
// Иначе это вручную вписанный id сборки — сообщение не должно
// выглядеть как сетевая ошибка.
failInstall(tr("Версия «%1» отсутствует в манифесте Mojang — "
"установить её автоматически нельзя")
failInstall(Loc::text("version.error.notInManifest")
.arg(id));
});
}
void VersionInstaller::fetchVersionJson(const QUrl &url, const QString &expectedSha1)
{
setStage(tr("Описание версии"));
setStage(Loc::text("version.progress.description"));
m_currentLabel = m_versionId + QStringLiteral(".json");
QNetworkRequest request{url};
@@ -288,7 +288,7 @@ void VersionInstaller::fetchVersionJson(const QUrl &url, const QString &expected
return;
if (reply->error() != QNetworkReply::NoError) {
failInstall(tr("Не удалось получить описание версии: %1").arg(reply->errorString()));
failInstall(Loc::text("version.error.descriptionFetchFailed").arg(reply->errorString()));
return;
}
@@ -297,7 +297,7 @@ void VersionInstaller::fetchVersionJson(const QUrl &url, const QString &expected
const QString actual = QString::fromLatin1(
QCryptographicHash::hash(payload, QCryptographicHash::Sha1).toHex());
if (actual.compare(expectedSha1, Qt::CaseInsensitive) != 0) {
failInstall(tr("Контрольная сумма описания версии «%1» не совпала").arg(id));
failInstall(Loc::text("version.error.descriptionChecksum").arg(id));
return;
}
}
@@ -314,14 +314,14 @@ void VersionInstaller::adoptVersionJson(const QByteArray &payload, bool alreadyS
m_versionJson = QJsonDocument::fromJson(payload).object();
if (m_versionJson.isEmpty()) {
failInstall(tr("Описание версии «%1» не удалось разобрать").arg(id));
failInstall(Loc::text("version.error.descriptionUnparsable").arg(id));
return;
}
if (!alreadySaved) {
const QString directory = m_gameDir + QStringLiteral("/versions/") + id;
if (!QDir().mkpath(directory)) {
failInstall(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(directory)));
failInstall(Loc::text("common.error.createFailed").arg(QDir::toNativeSeparators(directory)));
return;
}
@@ -329,7 +329,7 @@ void VersionInstaller::adoptVersionJson(const QByteArray &payload, bool alreadyS
if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()
|| !file.commit()) {
file.cancelWriting();
failInstall(tr("Не удалось сохранить описание версии «%1»").arg(id));
failInstall(Loc::text("common.error.versionSaveFailed").arg(id));
return;
}
}
@@ -340,7 +340,7 @@ void VersionInstaller::adoptVersionJson(const QByteArray &payload, bool alreadyS
if (!parentId.isEmpty() && isSafeRelativeSegment(parentId)
&& !QFile::exists(versionJsonPath(m_gameDir, parentId))) {
if (m_inheritDepth >= kMaxInheritDepth) {
failInstall(tr("Слишком длинная цепочка inheritsFrom у версии «%1»").arg(id));
failInstall(Loc::text("common.error.inheritsChainTooLong").arg(id));
return;
}
++m_inheritDepth;
@@ -361,7 +361,7 @@ void VersionInstaller::enqueue(DownloadTask task)
{
if (task.url.scheme() != QLatin1String("https")) {
// Подменённое описание версии не должно уводить загрузку на чужой адрес.
m_warnings << tr("Пропущен %1: недопустимая ссылка").arg(task.label);
m_warnings << Loc::text("version.warning.skippedBadUrl").arg(task.label);
return;
}
m_bytesTotal += task.size;
@@ -370,14 +370,14 @@ void VersionInstaller::enqueue(DownloadTask task)
bool VersionInstaller::prepareTasks()
{
setStage(tr("Подготовка"));
setStage(Loc::text("version.progress.preparing"));
QString error;
// features пустые намеренно: Mojang отбирает библиотеки только по ОС,
// а feature-правила живут в arguments, которые установщику не нужны.
m_version = VersionLoader::load(m_gameDir, m_versionId, {}, &error);
if (!m_version.isValid()) {
failInstall(error.isEmpty() ? tr("Не удалось прочитать версию «%1»").arg(m_versionId)
failInstall(error.isEmpty() ? Loc::text("common.error.versionReadFailed").arg(m_versionId)
: error);
return false;
}
@@ -401,7 +401,7 @@ bool VersionInstaller::prepareTasks()
// описания загрузки, разбирать json заново не нужно.
for (const MinecraftLibrary &library : std::as_const(m_version.libraries)) {
if (library.url.isEmpty() || !isSafeRelativePath(library.path)) {
m_warnings << tr("Нет ссылки на библиотеку %1").arg(library.name);
m_warnings << Loc::text("version.error.libraryNoUrl").arg(library.name);
continue;
}
DownloadTask task;
@@ -423,7 +423,7 @@ bool VersionInstaller::prepareTasks()
+ QStringLiteral(".json");
task.sha1 = assetIndex.value(QStringLiteral("sha1")).toString();
task.size = assetIndex.value(QStringLiteral("size")).toInteger();
task.label = tr("индекс ресурсов");
task.label = Loc::text("version.progress.assetIndex");
enqueue(std::move(task));
// totalSize — суммарный вес всех ресурсов, известный ещё до скачивания
@@ -450,7 +450,7 @@ bool VersionInstaller::prepareTasks()
enqueue(std::move(task));
}
setStage(tr("Файлы игры"));
setStage(Loc::text("version.progress.gameFiles"));
return true;
}
@@ -507,7 +507,7 @@ void VersionInstaller::startTask(DownloadTask task)
// Временный файл QSaveFile ложится рядом с целевым, поэтому папка нужна
// даже до открытия.
if (!QDir().mkpath(info.absolutePath())) {
failInstall(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(info.absolutePath())));
failInstall(Loc::text("common.error.createFailed").arg(QDir::toNativeSeparators(info.absolutePath())));
return;
}
@@ -515,7 +515,7 @@ void VersionInstaller::startTask(DownloadTask task)
context->task = task;
context->file = std::make_unique<QSaveFile>(task.path);
if (!context->file->open(QIODevice::WriteOnly)) {
failInstall(tr("Не удалось открыть на запись %1").arg(QDir::toNativeSeparators(task.path)));
failInstall(Loc::text("common.error.openForWriteFailed").arg(QDir::toNativeSeparators(task.path)));
return;
}
@@ -572,7 +572,7 @@ void VersionInstaller::handleReplyFinished(const std::shared_ptr<ActiveDownload>
if (!retryOrFail(context->task, reason))
return;
} else {
failInstall(tr("Не удалось скачать %1: %2").arg(context->task.label, reason));
failInstall(Loc::text("common.error.downloadFailed").arg(context->task.label, reason));
return;
}
pump();
@@ -583,7 +583,7 @@ void VersionInstaller::handleReplyFinished(const std::shared_ptr<ActiveDownload>
const QString actual = QString::fromLatin1(context->hash.result().toHex());
if (actual.compare(context->task.sha1, Qt::CaseInsensitive) != 0) {
context->file->cancelWriting();
if (!retryOrFail(context->task, tr("контрольная сумма не совпала")))
if (!retryOrFail(context->task, Loc::text("common.error.checksumMismatch")))
return;
pump();
return;
@@ -591,7 +591,7 @@ void VersionInstaller::handleReplyFinished(const std::shared_ptr<ActiveDownload>
}
if (!context->file->commit()) {
failInstall(tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(context->task.path)));
failInstall(Loc::text("common.error.saveFailed").arg(QDir::toNativeSeparators(context->task.path)));
return;
}
@@ -608,7 +608,7 @@ void VersionInstaller::handleReplyFinished(const std::shared_ptr<ActiveDownload>
bool VersionInstaller::retryOrFail(DownloadTask task, const QString &reason)
{
if (task.attempts + 1 >= kMaxAttempts) {
failInstall(tr("Не удалось скачать %1: %2").arg(task.label, reason));
failInstall(Loc::text("common.error.downloadFailed").arg(task.label, reason));
return false;
}
@@ -628,7 +628,7 @@ void VersionInstaller::expandAssetIndex(const QString &indexPath)
{
QFile file(indexPath);
if (!file.open(QIODevice::ReadOnly)) {
failInstall(tr("Не удалось прочитать индекс ресурсов %1")
failInstall(Loc::text("version.error.assetIndexReadFailed")
.arg(QDir::toNativeSeparators(indexPath)));
return;
}
@@ -638,14 +638,14 @@ void VersionInstaller::expandAssetIndex(const QString &indexPath)
m_assetsVirtual = root.value(QStringLiteral("virtual")).toBool();
m_assetsMapToResources = root.value(QStringLiteral("map_to_resources")).toBool();
setStage(tr("Ресурсы"));
setStage(Loc::text("version.progress.assets"));
qint64 actualTotal = 0;
for (auto it = m_assetObjects.constBegin(); it != m_assetObjects.constEnd(); ++it) {
const QJsonObject object = it.value().toObject();
const QString hash = object.value(QStringLiteral("hash")).toString();
if (!isSha1Hex(hash)) {
m_warnings << tr("Пропущен ресурс %1: некорректный хэш").arg(it.key());
m_warnings << Loc::text("version.warning.skippedBadHash").arg(it.key());
continue;
}
@@ -684,7 +684,7 @@ void VersionInstaller::afterDownloads()
void VersionInstaller::startLegacyCopy()
{
setStage(tr("Подготовка ресурсов"));
setStage(Loc::text("version.progress.preparingAssets"));
const QString targetRoot = m_assetsMapToResources
? m_gameDir + QStringLiteral("/resources/")
@@ -728,10 +728,10 @@ void VersionInstaller::legacyTick()
QDir().mkpath(target.absolutePath());
QFile::remove(pair.second);
if (!QFile::copy(pair.first, pair.second))
m_warnings << tr("Не удалось подготовить ресурс %1").arg(target.fileName());
m_warnings << Loc::text("version.error.assetPrepareFailed").arg(target.fileName());
}
m_currentLabel = tr("%1 из %2").arg(m_legacyIndex).arg(m_legacyCopies.size());
m_currentLabel = Loc::text("version.progress.countOf").arg(m_legacyIndex).arg(m_legacyCopies.size());
scheduleProgress();
if (m_legacyIndex >= m_legacyCopies.size()) {
@@ -749,7 +749,7 @@ void VersionInstaller::complete()
probe.versionId = m_versionId;
const QStringList missing = GameLauncher::missingFiles(probe, m_version);
if (!missing.isEmpty()) {
failInstall(tr("После загрузки не хватает файлов (%1):\n%2")
failInstall(Loc::text("version.error.filesMissingAfterDownload")
.arg(missing.size())
.arg(missing.join(u'\n')));
return;
+3 -2
View File
@@ -1,4 +1,5 @@
#include "versionmanifestservice.h"
#include "localization.h"
#include "launcherpaths.h"
@@ -126,7 +127,7 @@ void VersionManifestService::startRefresh()
if (reply->error() != QNetworkReply::NoError) {
finishRefresh(false,
tr("Не удалось обновить список версий: %1").arg(reply->errorString()));
Loc::text("version.error.manifestFetchFailed").arg(reply->errorString()));
return;
}
@@ -134,7 +135,7 @@ void VersionManifestService::startRefresh()
parseManifest(QJsonDocument::fromJson(reply->readAll()).object());
// Пустой разбор — испорченный ответ; хороший кэш им не затираем.
if (parsed.isEmpty()) {
finishRefresh(false, tr("Манифест версий Mojang не удалось разобрать"));
finishRefresh(false, Loc::text("version.error.manifestUnparsable"));
return;
}
+3 -13
View File
@@ -1,6 +1,6 @@
#include "zlibreference.h"
#include "localization.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QProcessEnvironment>
@@ -12,15 +12,6 @@
#include <dlfcn.h>
#endif
namespace {
QString translate(const char *text)
{
return QCoreApplication::translate("ZlibReference", text);
}
} // namespace
bool ZlibReference::systemIsZlibNg()
{
#if defined(Q_OS_LINUX)
@@ -80,8 +71,7 @@ bool ZlibReference::applyTo(QProcessEnvironment &env, QString *note)
const QString path = bundledPath();
if (path.isEmpty()) {
if (note)
*note = translate("В системе zlib-ng, а эталонный libz.so.1 рядом с лаунчером "
"не найден — установщик почти наверняка не сойдётся по sha1.");
*note = Loc::text("zlib.warning.referenceMissing");
return false;
}
@@ -89,7 +79,7 @@ bool ZlibReference::applyTo(QProcessEnvironment &env, QString *note)
const QString existing = env.value(QStringLiteral("LD_PRELOAD"));
env.insert(QStringLiteral("LD_PRELOAD"), existing.isEmpty() ? path : path + u':' + existing);
if (note)
*note = translate("Системный zlib-ng подменён эталонным zlib: %1")
*note = Loc::text("zlib.status.preloaded")
.arg(QDir::toNativeSeparators(path));
return true;
#else