translation of all project, and new translation mechanism

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