translation of all project, and new translation mechanism
This commit is contained in:
+28
-28
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 µsoftToken,
|
||||
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 µsoftToken,
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user