diff --git a/app.py b/app.py
index 182d5cd..1cd37b6 100644
--- a/app.py
+++ b/app.py
@@ -1,15 +1,17 @@
import sys
+from _colorize import Theme
from pathlib import Path
from typing import Callable
from PySide6.QtCore import QSize
-from PySide6.QtGui import QIcon, QPixmap
-from PySide6.QtWidgets import QApplication, QStyle
+from PySide6.QtGui import QIcon, QPixmap, QPalette, QColor
+from PySide6.QtWidgets import QApplication, QStyle, QToolButton
from constant import LAUNCHER_VERSION
from core_lib.qt.hack import move_window_to_center, is_light_theme, \
recolor_icon
from core_lib.qt import messagebox
+from core_lib.i18n import TranslationManager
from core_lib.launcher.object import Launcher as LauncherObject
from manager.account import AccountManager
from manager.game import GameManager
@@ -22,6 +24,8 @@ from window import LauncherMainWindow
class Launcher(QApplication, LauncherObject):
def __init__(self, logger, debug: bool = False):
super().__init__()
+ self._system_palette = QPalette(self.palette()) # theme
+
# logger
self.logger = logger
self.debug = debug
@@ -41,6 +45,15 @@ class Launcher(QApplication, LauncherObject):
self.settings_manager.load()
else:
self.settings_manager.load_defaults().save()
+
+ general = self.settings_manager.get("general", GeneralSettings)
+ self.i18n = TranslationManager(
+ self.resources_dir / "translations",
+ general.language,
+ )
+
+ self.apply_theme()
+
self.profile_manager = ProfileManager(self, self.widget_manager)
self.account_manager = AccountManager(self, self.widget_manager)
self.game_manager = GameManager(self)
@@ -73,8 +86,8 @@ class Launcher(QApplication, LauncherObject):
if not path.exists() or not path.is_file():
messagebox.error(
self.main_window,
- "Resource Error",
- "Missing icon file: {}".format(path)
+ self.tr_text("Resource Error"),
+ self.tr_text("Missing icon file: {path}", path=path)
)
return icon
@@ -89,19 +102,58 @@ class Launcher(QApplication, LauncherObject):
except Exception as e:
messagebox.error(
self.main_window,
- "Resource Error",
- "Failed to load icon {}: {}".format(path, e)
+ self.tr_text("Resource Error"),
+ self.tr_text("Failed to load icon {path}: {error}", path=path, error=e)
)
return icon
+ def tr_text(self, key: str, /, **values) -> str:
+ """Translate a stable English source string for the active language."""
+ return self.i18n.translate(key, **values)
+
+ def apply_language(self, language: str) -> None:
+ if language != self.i18n.language:
+ self.i18n.set_language(language)
+ messagebox.info(None,"Launcher",
+ self.tr_text("To make all page apply new language."
+ " Is recommend to restart the launcher after change it."))
+
+ def set_reloadable_toolbutton_icon(
+ self,
+ button: QToolButton,
+ path: Path,
+ no_color_change: bool = False,
+ ) -> None:
+ """Set an icon and retain its source so theme changes can reload it."""
+ button.setProperty("themeIconPath", str(path))
+ button.setProperty("themeIconNoColorChange", no_color_change)
+ button.setIcon(self.get_icon(path, no_color_change))
+
+ def reload_toolbutton_icons(self) -> None:
+ for button in self.allWidgets():
+ if not isinstance(button, QToolButton):
+ continue
+
+ path = button.property("themeIconPath")
+ if not path:
+ continue
+
+ no_color_change = bool(
+ button.property("themeIconNoColorChange")
+ )
+ button.setIcon(
+ self.get_icon(Path(path), no_color_change)
+ )
+
def get_picture(self, path: Path, size: QSize, custom_error: str=None) -> QPixmap:
picture = self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning).pixmap(size)
if not path.exists() or not path.is_file():
messagebox.error(
self.main_window,
- "Resource Error",
- "Missing icon file: {}".format(path) if not custom_error else custom_error
+ self.tr_text("Resource Error"),
+ self.tr_text("Missing icon file: {path}", path=path)
+ if not custom_error else custom_error
)
return picture
@@ -111,8 +163,8 @@ class Launcher(QApplication, LauncherObject):
except Exception as e:
messagebox.error(
self.main_window,
- "Resource Error",
- "Failed to load icon {}: {}".format(path, e)
+ self.tr_text("Resource Error"),
+ self.tr_text("Failed to load icon {path}: {error}", path=path, error=e)
)
return picture
@@ -167,8 +219,8 @@ class Launcher(QApplication, LauncherObject):
if not full_path.exists():
messagebox.error(
None,
- "Resource Error",
- "Missing stylesheet file: {}".format(full_path),
+ self.tr_text("Resource Error"),
+ self.tr_text("Missing stylesheet file: {path}", path=full_path),
)
return ""
@@ -178,11 +230,86 @@ class Launcher(QApplication, LauncherObject):
except Exception as e:
messagebox.error(
self.main_window,
- "Resource Error",
- "Unable to apply QSS stylesheet {}: {}".format(full_path, e),
+ self.tr_text("Resource Error"),
+ self.tr_text(
+ "Unable to apply QSS stylesheet {path}: {error}",
+ path=full_path,
+ error=e,
+ ),
)
return ""
def get_main_window(self):
return self.main_window
+
+ def apply_theme(self, theme: Theme | None=None):
+ if not theme:
+ appearance = self.settings_manager.get("appearance", AppearanceSettings)
+
+ theme = appearance.theme
+
+ if theme == "system":
+ palette = self._system_palette
+ elif theme == "dark":
+ palette = self._dark_palette()
+ elif theme == "light":
+ palette = self._light_palette()
+ else:
+ raise ValueError(f"Unsupported theme: {theme}")
+
+ self.setPalette(palette)
+
+ for widget in self.allWidgets():
+ widget.style().unpolish(widget)
+ widget.style().polish(widget)
+ widget.update()
+
+ if hasattr(self, "main_window"):
+ self.reload_toolbutton_icons()
+ self.main_window.toolbar.update_all()
+
+ @staticmethod
+ def _light_palette() -> QPalette:
+ palette = QPalette()
+
+ palette.setColor(QPalette.ColorRole.Window, QColor("#f5f5f5"))
+ palette.setColor(QPalette.ColorRole.WindowText, QColor("#202020"))
+ palette.setColor(QPalette.ColorRole.Base, QColor("#ffffff"))
+ palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#eeeeee"))
+ palette.setColor(QPalette.ColorRole.Text, QColor("#202020"))
+ palette.setColor(QPalette.ColorRole.Button, QColor("#ffffff"))
+ palette.setColor(QPalette.ColorRole.ButtonText, QColor("#202020"))
+ palette.setColor(QPalette.ColorRole.Mid, QColor("#c5c5c5"))
+ palette.setColor(QPalette.ColorRole.Midlight, QColor("#e2e2e2"))
+ palette.setColor(QPalette.ColorRole.Dark, QColor("#aaaaaa"))
+ palette.setColor(QPalette.ColorRole.Highlight, QColor("#bf265e"))
+ palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
+ palette.setColor(
+ QPalette.ColorRole.PlaceholderText,
+ QColor("#707070"),
+ )
+
+ return palette
+ @staticmethod
+ def _dark_palette() -> QPalette:
+ palette = QPalette()
+
+ palette.setColor(QPalette.ColorRole.Window, QColor("#202124"))
+ palette.setColor(QPalette.ColorRole.WindowText, QColor("#f1f3f4"))
+ palette.setColor(QPalette.ColorRole.Base, QColor("#17181a"))
+ palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#292a2d"))
+ palette.setColor(QPalette.ColorRole.Text, QColor("#f1f3f4"))
+ palette.setColor(QPalette.ColorRole.Button, QColor("#292a2d"))
+ palette.setColor(QPalette.ColorRole.ButtonText, QColor("#f1f3f4"))
+ palette.setColor(QPalette.ColorRole.Mid, QColor("#4a4c50"))
+ palette.setColor(QPalette.ColorRole.Midlight, QColor("#383a3e"))
+ palette.setColor(QPalette.ColorRole.Dark, QColor("#121315"))
+ palette.setColor(QPalette.ColorRole.Highlight, QColor("#bf265e"))
+ palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
+ palette.setColor(
+ QPalette.ColorRole.PlaceholderText,
+ QColor("#a0a4aa"),
+ )
+
+ return palette
diff --git a/manager/account.py b/manager/account.py
index a9ffc24..1397bb1 100644
--- a/manager/account.py
+++ b/manager/account.py
@@ -739,8 +739,8 @@ class AccountManager(QObject):
# Ask user before actually delete
answer = QMessageBox.question(
self.app.main_window,
- "Delete all accounts",
- "Delete every saved account? This cannot be undone.",
+ self.app.tr_text("Delete all accounts"),
+ self.app.tr_text("Delete every saved account? This cannot be undone."),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
diff --git a/manager/game.py b/manager/game.py
index 998f720..9e465b0 100644
--- a/manager/game.py
+++ b/manager/game.py
@@ -1,5 +1,6 @@
import json
import os
+import re
import subprocess
import sys
import threading
@@ -15,7 +16,7 @@ from PySide6.QtCore import QObject, Signal
from constant import CLIENT_ID, LAUNCHER_VERSION
from core_lib.common.common import FileObject, download_multiple_files
-from core_lib.common.exception import VersionManifestFetchException, VersionManifestSaveException, \
+from core_lib.common.exception import JavaRuntimeException, VersionManifestFetchException, VersionManifestSaveException, \
NoSpecifiedVersionKeyException
from core_lib.game.argument import ArgumentMappings, generate_launch_command
from core_lib.game.asset import check_assets_exist, download_assets, find_correct_assets_dir
@@ -24,13 +25,22 @@ from core_lib.game.mod.mod_core import merge_manifest
from core_lib.game.version_info import fetch_and_save_version_manifest, get_specific_version_manifest_url_and_hash, \
save_version_manifest, find_useful_part_in_specific_version_manifest, download_core_file, get_latest_version, \
LATEST_VERSION_ALIASES
-from core_lib.runtime.java import find_java_installations
+from core_lib.runtime.java import find_java_installations, get_java_version
+from core_lib.runtime.mojang import download_java_runtimes, fetch_runtime_manifest, get_current_platform_runtime_key
from manager.account import AccountManager, LaunchAccount
from manager.profile import LaunchProfile
from manager.widgets import SelectPathRequest, AskForRequest, SignalTransferItem
MAX_DOWNLOAD_ATTEMPTS = 3
+@dataclass(frozen=True)
+class AvailableVersion:
+ id: str
+ display_name: str
+ version_type: str
+ source: str
+ inherits_from: str | None = None
+
class ManifestManager:
def __init__(self, app):
self.app = app
@@ -305,10 +315,112 @@ class ManifestManager:
is_snapshot=version_id == "latest-snapshot",
)
+ def list_available_profile_versions(self) -> list[AvailableVersion]:
+ result: list[AvailableVersion] = []
+ root_manifest = self.get_cached_version_manifest()
+ vanilla_ids: set[str] = set()
+
+ # For vanilla version
+ for version in (root_manifest or {}).get("versions", []):
+ version_id = version.get("id")
+ if not version_id:
+ continue
+
+ vanilla_ids.add(version_id)
+ result.append(
+ AvailableVersion(
+ id=version_id,
+ display_name=f"Minecraft {version_id}",
+ version_type=version.get("type") or "custom",
+ source="vanilla",
+ )
+ )
+
+ # Official launcher aliases are valid vanilla profile versions too.
+ for alias_id, alias in LATEST_VERSION_ALIASES.items():
+ result.append(
+ AvailableVersion(
+ id=alias_id,
+ display_name=alias.get("displayName", alias_id),
+ version_type=(
+ "snapshot" if alias_id == "latest-snapshot" else "release"
+ ),
+ source="vanilla",
+ )
+ )
+
+ # Locally created modded and custom versions.
+ versions_dir = Path(self.app.work_dir, "versions")
+
+ for manifest_path in versions_dir.glob("*/*.json"):
+ try:
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError) as exc:
+ self.app.logger.warning(
+ f"Unable to read local version manifest {manifest_path}: {exc}"
+ )
+ continue
+
+ version_id = manifest.get("id")
+ if not version_id or version_id in vanilla_ids:
+ continue
+
+ inherits_from = manifest.get("inheritsFrom")
+ result.append(
+ AvailableVersion(
+ id=version_id,
+ display_name=(
+ f"{version_id} - Minecraft {inherits_from}"
+ if inherits_from else version_id
+ ),
+ version_type=manifest.get("type") or "custom",
+ source="modded" if inherits_from else "custom",
+ inherits_from=inherits_from,
+ )
+ )
+
+ # Keep the first entry for each ID. The root manifest takes precedence
+ # over locally cached copies of vanilla manifests.
+ unique: dict[str, AvailableVersion] = {}
+ for version in result:
+ unique.setdefault(version.id, version)
+
+ return list(unique.values())
+
+@dataclass(frozen=True)
+class JavaRuntime:
+ id: str
+ major_version: int
+ raw_version: str
+ executable: Path
+ home_dir: Path
+ source: Literal["system", "managed", "custom"]
+ component: str | None = None
+ valid: bool = True
+
+ def to_dict(self) -> dict:
+ return {
+ "id": self.id,
+ "major": self.major_version,
+ "raw": self.raw_version,
+ "executable": self.executable.as_posix(),
+ "homeDir": self.home_dir.as_posix(),
+ "source": self.source,
+ "component": self.component,
+ }
+
+@dataclass(frozen=True)
+class InstallableJavaRuntime:
+ component: str
+ major_version: int | None
+ raw_version: str | None
+ released: str | None = None
+
class JavaManager:
def __init__(self, app):
self.app = app
self.widget_mgr = app.widget_manager
+ self.runtimes: list[JavaRuntime] = self._read_runtimes()
#
# JVM Management
@@ -317,81 +429,210 @@ class JavaManager:
def jvm_settings_path(self) -> Path:
return Path(self.app.work_dir) / "jvms.json"
- def _find_and_save_jvm_search_result(self):
- installations, errors = find_java_installations()
- if not installations:
- self.widget_mgr.signal.show_warning_message.emit(
- "There are no Java installations available in you system.\n"
- )
+ @staticmethod
+ def _runtime_from_dict(data: dict) -> JavaRuntime:
+ """
+ Convert runtime data to JavaRuntime object
+ :param data:
+ :return:
+ """
+ executable = Path(data["executable"])
+ home_dir = Path(data.get("homeDir") or data.get("home_dir") or executable.parent.parent)
+
+ return JavaRuntime(
+ id=str(data.get("id") or uuid4()),
+ major_version=int(data["major"]),
+ raw_version=str(data.get("raw") or data["major"]),
+ executable=executable,
+ home_dir=home_dir,
+ source=data.get("source") or "system",
+ component=data.get("component"),
+ valid=executable.is_file(),
+ )
+
+ def _read_runtimes(self) -> list[JavaRuntime]:
+ """
+ Read existing runtime data from jvms.json
+ :return:
+ """
+ if not self.jvm_settings_path.is_file():
+ return []
+ try:
+ data = json.loads(self.jvm_settings_path.read_text(encoding="utf-8"))
+
+ if not isinstance(data, list):
+ raise ValueError("JVM settings root must be a list")
+
+ return [self._runtime_from_dict(item) for item in data if isinstance(item, dict)]
+ except (OSError, ValueError, KeyError, TypeError) as exc:
+ self.app.logger.warning(f"Unable to read JVM settings: {exc}")
return []
- if errors:
- request = AskForRequest("Java Virtual Machine Finder", message = (
- "Unable to detect below java installation version:\n"
- ))
- request.details = "\n".join(path.as_posix() for path in errors)
- request.use_plain_text = True
-
- self.widget_mgr.signal.askyesno.emit(request)
-
+ def save_runtimes(self) -> None:
+ """
+ Save memory runtimes to jvms.json
+ :return:
+ """
self.jvm_settings_path.parent.mkdir(parents=True, exist_ok=True)
- try:
- with self.jvm_settings_path.open("w") as file:
- json.dump(installations, file, indent=4)
+ temporary = self.jvm_settings_path.with_suffix(".json.tmp")
+ temporary.write_text(
+ json.dumps([runtime.to_dict() for runtime in self.runtimes], indent=4),
+ encoding="utf-8",
+ )
- return installations
- except Exception as e:
- self.widget_mgr.signal.show_and_print_error_message.emit(
- {
- "error": traceback.format_exception(e),
- "title": "Settings Error",
- "message": "An error occurred while attempting to save jvm settings. Please try again.",
- }
- )
+ temporary.replace(self.jvm_settings_path)
- def _read_exist_jvm_settings(self):
- try:
- return json.loads(self.jvm_settings_path.read_text())
- except Exception as e:
- self.widget_mgr.signal.show_and_print_error_message.emit(
- {
- "error": traceback.format_exception(e),
- "title": "Settings Error",
- "message": "An error occurred while reading jvm settings.",
- }
- )
+ @staticmethod
+ def inspect_runtime(executable: str | Path,
+ *,
+ source: Literal["system", "managed", "custom"] = "custom",
+ component: str | None = None,
+ runtime_id: str | None = None,
+ ) -> JavaRuntime:
+ """
+ Check runtime version
+ :param executable:
+ :param source:
+ :param component:
+ :param runtime_id:
+ :return:
+ runtime: JavaRuntime
+ """
+ executable = Path(executable).expanduser().resolve()
+
+ if not executable.is_file():
+ raise JavaRuntimeException(f"Java executable does not exist: {executable}")
+
+ major, raw = get_java_version(executable)
+ return JavaRuntime(
+ id=runtime_id or uuid4().hex,
+ major_version=major,
+ raw_version=raw,
+ executable=executable,
+ home_dir=executable.parent.parent,
+ source=source,
+ component=component,
+ )
+
+ def discover_runtimes(self, extra_install_patterns: list | None = None) -> tuple[list[JavaRuntime], list[Path]]:
+ """
+ Find available runtimes
+ :param extra_install_patterns:
+ :return:
+ discovered: list[JavaRuntime]
+ errors: list[Path]
+ """
+ installations, errors = find_java_installations(extra_install_patterns)
+ known_ids = {runtime.executable.resolve(): runtime.id for runtime in self.runtimes}
+
+ discovered = []
+ for item in installations:
+ executable = Path(item["executable"]).resolve()
+
+ discovered.append(self._runtime_from_dict({
+ **item,
+ "id": known_ids.get(executable, uuid4().hex),
+ "source": "system",
+ }))
+
+ return discovered, errors
+
+ def refresh_runtimes(self, extra_install_patterns: list | None = None) -> tuple[list[JavaRuntime], list[Path]]:
+ """
+ Discover available runtimes and save
+ :param extra_install_patterns:
+ :return:
+ runtimes: list[JavaRuntime]
+ errors: list[Path]
+ """
+ discovered, errors = self.discover_runtimes(extra_install_patterns)
+ retained = [runtime for runtime in self.runtimes if runtime.source != "system"]
+ by_executable = {runtime.executable.resolve(): runtime for runtime in retained}
+
+ for runtime in discovered:
+ by_executable.setdefault(runtime.executable.resolve(), runtime)
+
+ self.runtimes = list(by_executable.values())
+ self.save_runtimes()
+ return list(self.runtimes), errors
+
+ def register_runtime(self, executable: str | Path) -> JavaRuntime:
+ """
+ Register runtime to manage's runtimes
+ :param executable:
+ :return:
+ """
+ candidate = Path(executable).expanduser().resolve()
+
+ for runtime in self.runtimes:
+ if runtime.executable.resolve() == candidate:
+ return runtime
+
+ runtime = self.inspect_runtime(candidate, source="custom")
+ self.runtimes.append(runtime)
+ self.save_runtimes()
+ return runtime
+
+ @staticmethod
+ def _runtime_major_version(raw_version: str | None) -> int | None:
+ if not raw_version:
return None
+ match = re.match(r"^(?:1\.)?(\d+)", raw_version)
+ return int(match.group(1)) if match else None
- def get_or_cache_jvm_settings(self) -> list:
- if self.jvm_settings_path.exists():
- exists = self._read_exist_jvm_settings()
+ def list_installable_runtimes(self) -> list[InstallableJavaRuntime]:
+ """Return Mojang components with the Java version carried by each one."""
+ manifest = fetch_runtime_manifest()
+ platform_key = get_current_platform_runtime_key()
+ if not platform_key:
+ return []
+ platform_runtimes = manifest.get(platform_key, {})
+ result = []
+ for component, versions in platform_runtimes.items():
+ if not isinstance(versions, list) or not versions:
+ continue
+ latest = versions[0] if isinstance(versions[0], dict) else {}
+ version = latest.get("version") or {}
+ raw_version = version.get("name")
+ result.append(InstallableJavaRuntime(
+ component=component,
+ major_version=self._runtime_major_version(raw_version),
+ raw_version=raw_version,
+ released=version.get("released"),
+ ))
+ return sorted(result, key=lambda item: (item.major_version or -1, item.component), reverse=True)
- if exists:
- return exists
+ def list_installable_components(self) -> list[str]:
+ """Compatibility API returning component names only."""
+ return [runtime.component for runtime in self.list_installable_runtimes()]
- return self._find_and_save_jvm_search_result()
+ def unregister_runtime(self, runtime_id: str) -> bool:
+ """
+ Remove runtime from manage's runtimes
+ :param runtime_id:
+ :return:
+ """
+ original_count = len(self.runtimes)
+ self.runtimes = [runtime for runtime in self.runtimes if runtime.id != runtime_id]
+
+ changed = len(self.runtimes) != original_count
+
+ if changed:
+ self.save_runtimes()
+
+ return changed
+
+ def get_or_cache_jvm_settings(self) -> list[dict]:
+ if not self.runtimes:
+ self.refresh_runtimes()
+
+ return [runtime.to_dict() for runtime in self.runtimes]
def get_support_runtime_jvm_executable(self, major_version: int | str, no_error: bool=False) -> str | None:
- result = self.get_or_cache_jvm_settings()
-
- if not result:
- return None
-
- for item in result:
- version = item.get("major", None)
- executable = item.get("executable", None)
-
- try:
- version = str(version)
- major_version = str(major_version)
- except Exception as e:
- self.app.logger.warning(
- f"Unable to convert version {version} to string: {e}"
- )
-
- if major_version == version and executable:
- return executable
+ runtime = self.resolve_runtime(major_version=int(major_version))
+ if runtime:
+ return runtime.executable.as_posix()
if not no_error:
self.widget_mgr.signal.show_error_message.emit(
@@ -400,6 +641,81 @@ class JavaManager:
return None
+ def list_runtimes(self, *, refresh: bool = False) -> list[JavaRuntime]:
+ if refresh:
+ self.refresh_runtimes()
+ return list(self.runtimes)
+
+ def resolve_runtime(self, *, manifest: dict | None = None, major_version: int | None = None,
+ preferred_runtime_id: str | None = None) -> JavaRuntime | None:
+ java_requirement = (manifest or {}).get("javaVersion", {})
+ required_major = major_version or java_requirement.get("majorVersion")
+
+ if required_major is None:
+ return None
+
+ runtimes = self.list_runtimes()
+ if not runtimes:
+ runtimes, _ = self.refresh_runtimes()
+
+ if preferred_runtime_id:
+ preferred = next((item for item in runtimes if item.id == preferred_runtime_id), None)
+ if preferred and preferred.valid and preferred.major_version == int(required_major):
+ return preferred
+
+ source_order = {"managed": 0, "custom": 1, "system": 2}
+ matches = [item for item in runtimes if item.valid and item.major_version == int(required_major)]
+ return min(matches, key=lambda item: source_order[item.source], default=None)
+
+ @property
+ def launcher_runtime_dir(self) -> Path:
+ return Path(self.app.work_dir) / "runtimes"
+
+ def install_runtime(self, component: str, *, progress_callback=None, redownload_callback=None) -> JavaRuntime:
+ destination = self.launcher_runtime_dir / component
+ download_java_runtimes(
+ component,
+ destination,
+ progress_callback=progress_callback,
+ redownload_callback=redownload_callback,
+ )
+ executable_name = "java.exe" if sys.platform == "win32" else "java"
+ candidates = sorted(destination.rglob(executable_name))
+ if not candidates:
+ raise JavaRuntimeException(f"Installed runtime has no {executable_name}: {destination}")
+ runtime = self.inspect_runtime(candidates[0], source="managed", component=component)
+ self.runtimes = [item for item in self.runtimes if item.executable.resolve() != runtime.executable.resolve()]
+ self.runtimes.append(runtime)
+ self.save_runtimes()
+ return runtime
+
+ def ensure_runtime(self, manifest: dict, *, auto_install: bool = False, progress_callback=None,
+ redownload_callback=None) -> JavaRuntime:
+ runtime = self.resolve_runtime(manifest=manifest)
+
+ if runtime:
+ return runtime
+
+ requirement = manifest.get("javaVersion") or {}
+ component = requirement.get("component")
+ major = requirement.get("majorVersion")
+
+ if not auto_install or not component:
+ raise JavaRuntimeException(f"No compatible Java runtime found (required major version: {major})")
+
+ runtime = self.install_runtime(
+ component,
+ progress_callback=progress_callback,
+ redownload_callback=redownload_callback,
+ )
+ if major is not None and runtime.major_version != int(major):
+ raise JavaRuntimeException(
+ f"Installed Java {runtime.major_version}, but Minecraft requires Java {major}"
+ )
+
+ return runtime
+
+
class GameFileDownloadRequest(SignalTransferItem):
def __init__(self, job_name: Literal["download_client", "download_libraries", "download_assets"],
args: tuple):
@@ -650,6 +966,7 @@ class LaunchResult:
process: subprocess.Popen | None = None
error_message: str | None = None
warnings: list[str] = field(default_factory=list)
+ fail_callback: Callable | None = None
@property
def success(self) -> bool:
@@ -657,6 +974,8 @@ class LaunchResult:
def fail(self, message: str) -> "LaunchResult":
self.error_message = message
+ if callable(self.fail_callback):
+ self.fail_callback()
return self
def add_warning(self, message: str) -> None:
@@ -680,6 +999,11 @@ class LaunchManager:
def launch(self, profile: LaunchProfile) -> LaunchResult:
result = LaunchResult()
+
+ def fail_callback():
+ self.profile_failed(profile.profile_id)
+
+ result.fail_callback = fail_callback
try:
version_id = profile.version_id
version_id = self.manifest_mgr.resolve_version_alias(version_id)
@@ -815,11 +1139,37 @@ class LaunchManager:
"Unable to parse arguments because data are invalid."
)
- # Use found java runtime (if available)
- java_path = self.java_mgr.get_support_runtime_jvm_executable(
- java_major_version,
- no_error=True
- )
+ # Resolve a compatible runtime, then offer the Mojang runtime from
+ # the version manifest before falling back to a custom executable.
+ runtime = self.java_mgr.resolve_runtime(manifest=version_manifest)
+ java_path = runtime.executable.as_posix() if runtime else None
+
+ if not java_path:
+ component = java_version.get("component")
+ if component:
+ install_request = AskForRequest(
+ "Java Runtime Required",
+ f"Minecraft {version_id} requires Java {java_major_version}. "
+ f"Download the Mojang runtime now?",
+ )
+ self.widget_mgr.signal.askyesno.emit(install_request)
+ if install_request.wait(120) and install_request.result():
+ callback, task_id = self.manifest_mgr.create_download_progress_callback(
+ f"Installing Java {java_major_version}"
+ )
+ try:
+ runtime = self.java_mgr.ensure_runtime(
+ version_manifest,
+ auto_install=True,
+ progress_callback=callback,
+ redownload_callback=self.game_file_mgr.redownload_files,
+ )
+ except Exception as exc:
+ self.widget_mgr.download_signal.failed.emit(task_id, str(exc))
+ self.app.logger.warning(f"Automatic Java installation failed: {exc}")
+ else:
+ self.widget_mgr.download_signal.finished.emit(task_id)
+ java_path = runtime.executable.as_posix()
if not java_path:
# Ask for a support java version. If not, use system environment java
@@ -840,10 +1190,19 @@ class LaunchManager:
java_path = request.result()
if java_path:
+ try:
+ runtime = self.java_mgr.register_runtime(java_path)
+ except JavaRuntimeException as exc:
+ return result.fail(f"The selected Java executable is invalid: {exc}")
+ if runtime.major_version != int(java_major_version):
+ return result.fail(
+ f"Minecraft requires Java {java_major_version}, but the selected executable is "
+ f"Java {runtime.major_version}."
+ )
+ java_path = runtime.executable.as_posix()
self.app.logger.debug(f"Use specified java executable path: {java_path}")
else:
- self.app.logger.debug("Use system environment java.")
- java_path = "java"
+ return result.fail(f"Java {java_major_version} is required to launch this version.")
self.app.logger.debug(f"Java executable path is: {java_path}")
@@ -913,12 +1272,32 @@ class LaunchManager:
or profile_id in self.launched_profiles
)
+ def profile_failed(self, profile_id: str) -> None:
+ try:
+ self.launched_profiles.remove(profile_id)
+ except ValueError:
+ pass
+
+ try:
+ self.launching_profiles.remove(profile_id)
+ except ValueError:
+ pass
+
+ self.app.logger.debug(
+ f"Profile {profile_id} exited."
+ )
+
def profile_finished(self, profile_id: str, exit_code: int) -> None:
try:
self.launched_profiles.remove(profile_id)
except ValueError:
pass
+ try:
+ self.launching_profiles.remove(profile_id)
+ except ValueError:
+ pass
+
self.app.logger.debug(
f"Profile {profile_id} exited with code {exit_code}."
)
diff --git a/pages/account.py b/pages/account.py
index b38469e..fbd5593 100644
--- a/pages/account.py
+++ b/pages/account.py
@@ -25,9 +25,10 @@ from manager.settings_models import AccountSettings
class AccountWidget(QWidget):
- def __init__(self, account_name: str, account_head: QPixmap, login_status: str, parent=None,
+ def __init__(self, app, account_name: str, account_head: QPixmap, login_status: str, parent=None,
/, delete_account_callback=None, check_account_callback=None, switch_account_callback=None,):
super().__init__(parent)
+ self.app = app
# Layout
self.layout = QHBoxLayout(self)
@@ -57,10 +58,10 @@ class AccountWidget(QWidget):
def show_right_click_menu(self, position):
menu = QMenu(self)
- switch_action = menu.addAction("Switch to this account")
- check_action = menu.addAction("Check login status")
+ switch_action = menu.addAction(self.app.tr_text("Switch to this account"))
+ check_action = menu.addAction(self.app.tr_text("Check login status"))
menu.addSeparator()
- delete_action = menu.addAction("Delete account")
+ delete_action = menu.addAction(self.app.tr_text("Delete account"))
selected = menu.exec(self.mapToGlobal(position))
if selected == delete_action and callable(self.delete_account_callback):
@@ -74,7 +75,7 @@ class LoginDialog(QDialog):
def __init__(self, app, parent=None, code="", error_message=""):
super().__init__(parent)
self.app = app
- self.setWindowTitle("Microsoft Login")
+ self.setWindowTitle(self.app.tr_text("Microsoft Login"))
self.setMinimumWidth(440)
self.setMinimumHeight(380)
self.resize(440, 420)
@@ -86,9 +87,9 @@ class LoginDialog(QDialog):
self.layout.setSpacing(14)
# Top widgets
- self.title_label = QLabel("Login Minecraft Account")
+ self.title_label = QLabel(self.app.tr_text("Login Minecraft Account"))
self.title_label.setObjectName("dialogTitle")
- self.help_label = QLabel("Copy the code below, then continue in your browser.")
+ self.help_label = QLabel(self.app.tr_text("Copy the code below, then continue in your browser."))
self.help_label.setObjectName("helpText")
self.help_label.setWordWrap(True)
@@ -106,9 +107,9 @@ class LoginDialog(QDialog):
self.error_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
# Button
- self.copy_code_button = QPushButton("Copy code")
+ self.copy_code_button = QPushButton(self.app.tr_text("Copy code"))
self.copy_code_button.setObjectName("copyCodeButton")
- self.open_browser_button = QPushButton("Open browser")
+ self.open_browser_button = QPushButton(self.app.tr_text("Open browser"))
self.open_browser_button.setObjectName("openBrowserButton")
self.center_layout = QVBoxLayout()
@@ -142,12 +143,12 @@ class LoginDialog(QDialog):
def copy_code(self):
QApplication.clipboard().setText(self.code)
- self.copy_code_button.setText("Copied!")
+ self.copy_code_button.setText(self.app.tr_text("Copied!"))
self.copy_code_button.setEnabled(False)
QTimer.singleShot(1200, self._reset_copy_button)
def _reset_copy_button(self):
- self.copy_code_button.setText("Copy code")
+ self.copy_code_button.setText(self.app.tr_text("Copy code"))
self.copy_code_button.setEnabled(True)
def set_error_message(self, message):
@@ -173,9 +174,9 @@ class AccountPage(QWidget):
# Widgets
self.account_list = QListWidget(self)
- self.add_account_button = QPushButton("Add Account")
- self.refresh_button = QPushButton("Refresh all")
- self.delete_all_button = QPushButton("Delete All")
+ self.add_account_button = QPushButton(self.app.tr_text("Add Account"))
+ self.refresh_button = QPushButton(self.app.tr_text("Refresh all"))
+ self.delete_all_button = QPushButton(self.app.tr_text("Delete All"))
# Button click handle
self.add_account_button.clicked.connect(self.start_login_page)
@@ -244,7 +245,7 @@ class AccountPage(QWidget):
# Add suggestion to account list if no account available
if not accounts:
- empty_item = QListWidgetItem("No accounts yet. Use Add Account to sign in.")
+ empty_item = QListWidgetItem(self.app.tr_text("No accounts yet. Use Add Account to sign in."))
empty_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
empty_item.setFlags(Qt.ItemFlag.NoItemFlags)
self.account_list.addItem(empty_item)
@@ -258,9 +259,10 @@ class AccountPage(QWidget):
head_size = QSize(32, 32)
widget = AccountWidget(
+ self.app,
account.display_name,
self.account_manager.get_head_pixmap(account.avatar, head_size),
- "Current" if is_current else "Available",
+ self.app.tr_text("Current" if is_current else "Available"),
self,
delete_account_callback=lambda: self._delete_account(account.id),
check_account_callback=lambda: self._show_account_status(account.id),
@@ -310,9 +312,14 @@ class AccountPage(QWidget):
QMessageBox.information(
self,
- "Account status",
- f"{account.display_name} — Minecraft profile "
- f"{'available' if account.minecraft_uuid else 'not available'}",
+ self.app.tr_text("Account status"),
+ self.app.tr_text(
+ "{account} — Minecraft profile {status}",
+ account=account.display_name,
+ status=self.app.tr_text(
+ "available" if account.minecraft_uuid else "not available"
+ ),
+ ),
)
def _switch_account(self, account_id: str):
@@ -336,7 +343,7 @@ class AccountPage(QWidget):
# Create login dialog
self.add_account_button.setEnabled(False)
self.login_dialog = LoginDialog(parent=self, app=self.app)
- self.login_dialog.help_label.setText("Requesting a Microsoft sign-in code…")
+ self.login_dialog.help_label.setText(self.app.tr_text("Requesting a Microsoft sign-in code…"))
self.login_dialog.copy_code_button.setEnabled(False)
self.login_dialog.open_browser_button.setEnabled(False)
self.login_dialog.open_browser_button.clicked.connect(self.open_sign_in_page)
@@ -351,7 +358,7 @@ class AccountPage(QWidget):
if self.login_dialog is not None:
self.login_dialog.help_label.setText(
- device.get("message") or "Copy the code below, then continue in your browser."
+ device.get("message") or self.app.tr_text("Copy the code below, then continue in your browser.")
)
self.login_dialog.set_code(device.get("user_code", ""))
self.login_dialog.copy_code_button.setEnabled(True)
@@ -378,11 +385,11 @@ class AccountPage(QWidget):
def _login_failed(self, message: str):
if self.login_dialog is not None:
self.login_dialog.set_error_message(message)
- self.login_dialog.help_label.setText("Microsoft sign-in could not be completed.")
+ self.login_dialog.help_label.setText(self.app.tr_text("Microsoft sign-in could not be completed."))
self.login_dialog.copy_code_button.setEnabled(False)
self.login_dialog.open_browser_button.setEnabled(False)
else:
- QMessageBox.critical(self, "Microsoft sign-in failed", message)
+ QMessageBox.critical(self, self.app.tr_text("Microsoft sign-in failed"), message)
def _login_finished(self):
self.add_account_button.setEnabled(True)
diff --git a/pages/add_modded_version.py b/pages/add_modded_version.py
new file mode 100644
index 0000000..567d3d1
--- /dev/null
+++ b/pages/add_modded_version.py
@@ -0,0 +1,315 @@
+import threading
+
+from PySide6.QtCore import QObject, Signal
+from PySide6.QtWidgets import (
+ QComboBox, QDialog, QFormLayout, QHBoxLayout, QLabel, QPushButton,
+ QVBoxLayout,
+)
+
+from core_lib.game.mod import fabric
+from core_lib.game.mod.mod_core import find_useful_part_in_mod_version_manifest
+from core_lib.game.version_info import get_available_version_types
+
+
+class AddModdedVersionDialog(QDialog):
+ """
+ Create a local mod-loader manifest without creating a profile.
+
+ This dialog is vibed.
+ """
+
+ version_created = Signal(str)
+
+ class WorkerSignals(QObject):
+ manifest_loaded = Signal(object, object)
+ loader_versions_loaded = Signal(str, object, object)
+ version_installed = Signal(object, object)
+
+ def __init__(self, app, parent=None):
+ super().__init__(parent)
+ self.app = app
+ self.manifest_manager = app.game_manager.manifest
+ self.root_manifest: dict = {}
+ self.worker_signals = self.WorkerSignals(self)
+
+ self.setWindowTitle(self.app.tr_text("Add Modded Version"))
+ self.setMinimumWidth(480)
+
+ root = QVBoxLayout(self)
+ root.setContentsMargins(20, 18, 20, 18)
+ root.setSpacing(14)
+
+ title = QLabel(self.app.tr_text("Add Modded Version"))
+ title.setObjectName("dialogTitle")
+ description = QLabel(self.app.tr_text(
+ "Create a local mod-loader version manifest without creating a profile."
+ ))
+ description.setWordWrap(True)
+
+ form = QFormLayout()
+ form.setVerticalSpacing(10)
+
+ self.loader_dropdown = QComboBox()
+ self.loader_dropdown.setObjectName("Dropdown")
+ self.loader_dropdown.addItem(self.app.tr_text("Fabric"), "fabric")
+
+ self.minecraft_type_dropdown = QComboBox()
+ self.minecraft_type_dropdown.setObjectName("Dropdown")
+ self.minecraft_version_dropdown = QComboBox()
+ self.minecraft_version_dropdown.setObjectName("Dropdown")
+ self.loader_version_dropdown = QComboBox()
+ self.loader_version_dropdown.setObjectName("Dropdown")
+
+ form.addRow(self.app.tr_text("Mod loader"), self.loader_dropdown)
+ form.addRow(self.app.tr_text("Minecraft type"), self.minecraft_type_dropdown)
+ form.addRow(self.app.tr_text("Minecraft version"), self.minecraft_version_dropdown)
+ form.addRow(self.app.tr_text("Loader version"), self.loader_version_dropdown)
+
+ self.status_label = QLabel()
+ self.status_label.setWordWrap(True)
+
+ buttons = QHBoxLayout()
+ self.refresh_button = QPushButton(self.app.tr_text("Refresh"))
+ self.cancel_button = QPushButton(self.app.tr_text("Cancel"))
+ self.add_button = QPushButton(self.app.tr_text("Add Version"))
+ self.add_button.setObjectName("saveProfileButton")
+ buttons.addWidget(self.refresh_button)
+ buttons.addStretch()
+ buttons.addWidget(self.cancel_button)
+ buttons.addWidget(self.add_button)
+
+ root.addWidget(title)
+ root.addWidget(description)
+ root.addLayout(form)
+ root.addWidget(self.status_label)
+ root.addLayout(buttons)
+
+ self.minecraft_type_dropdown.currentIndexChanged.connect(
+ self._update_minecraft_versions
+ )
+ self.minecraft_version_dropdown.currentIndexChanged.connect(
+ self._start_load_loader_versions
+ )
+ self.refresh_button.clicked.connect(self.load_versions)
+ self.cancel_button.clicked.connect(self.reject)
+ self.add_button.clicked.connect(self.install_selected_version)
+
+ self.worker_signals.manifest_loaded.connect(self._manifest_loaded)
+ self.worker_signals.loader_versions_loaded.connect(
+ self._loader_versions_loaded
+ )
+ self.worker_signals.version_installed.connect(self._version_installed)
+
+ self.load_versions()
+
+ @staticmethod
+ def _error_text(error: Exception) -> str:
+ return str(getattr(error, "user_message", None) or error)
+
+ def _set_loading(self, loading: bool, status: str = "") -> None:
+ self.refresh_button.setEnabled(not loading)
+ self.add_button.setEnabled(not loading)
+ self.status_label.setText(status)
+
+ def load_versions(self) -> None:
+ self._set_loading(True, self.app.tr_text("Loading Minecraft versions..."))
+ self.minecraft_type_dropdown.setEnabled(False)
+ self.minecraft_version_dropdown.setEnabled(False)
+ self.loader_version_dropdown.setEnabled(False)
+
+ def worker():
+ try:
+ manifest = self.manifest_manager.get_cached_version_manifest()
+ if not manifest:
+ raise RuntimeError("Unable to load the Minecraft version manifest.")
+ self.worker_signals.manifest_loaded.emit(manifest, None)
+ except Exception as error:
+ self.worker_signals.manifest_loaded.emit(None, error)
+
+ threading.Thread(target=worker, daemon=True).start()
+
+ def _manifest_loaded(self, manifest, error) -> None:
+ if error is not None:
+ self._set_loading(False, self.app.tr_text(
+ "Unable to load versions: {error}", error=self._error_text(error)
+ ))
+ return
+
+ self.root_manifest = manifest
+ try:
+ types = get_available_version_types(manifest)
+ except Exception as exc:
+ self._set_loading(False, self.app.tr_text(
+ "Unable to read version types: {error}", error=self._error_text(exc)
+ ))
+ return
+
+ types = [item for item in types if isinstance(item, str) and item]
+ if "release" in types:
+ types.remove("release")
+ types.insert(0, "release")
+
+ self.minecraft_type_dropdown.blockSignals(True)
+ self.minecraft_type_dropdown.clear()
+ self.minecraft_type_dropdown.addItems(types)
+ self.minecraft_type_dropdown.blockSignals(False)
+ self.minecraft_type_dropdown.setEnabled(True)
+ self.minecraft_version_dropdown.setEnabled(True)
+ self._update_minecraft_versions()
+
+ def _update_minecraft_versions(self, _index: int = -1) -> None:
+ version_type = self.minecraft_type_dropdown.currentText()
+ self.minecraft_version_dropdown.blockSignals(True)
+ self.minecraft_version_dropdown.clear()
+
+ for version in self.root_manifest.get("versions", []):
+ if version.get("type") != version_type:
+ continue
+ version_id = version.get("id")
+ if version_id:
+ self.minecraft_version_dropdown.addItem(version_id, version_id)
+
+ self.minecraft_version_dropdown.blockSignals(False)
+ self._start_load_loader_versions()
+
+ def _start_load_loader_versions(self, _index: int = -1) -> None:
+ minecraft_version = self.minecraft_version_dropdown.currentData()
+ self.loader_version_dropdown.clear()
+ self.loader_version_dropdown.setEnabled(False)
+
+ if not minecraft_version:
+ self._set_loading(False, self.app.tr_text("No Minecraft version is available for this type."))
+ return
+
+ self._set_loading(True, self.app.tr_text(
+ "Loading Fabric versions for {version}...", version=minecraft_version
+ ))
+
+ def worker():
+ try:
+ loaders = fabric.get_version_support_loader_list(minecraft_version)
+ self.worker_signals.loader_versions_loaded.emit(
+ minecraft_version, loaders, None
+ )
+ except Exception as error:
+ self.worker_signals.loader_versions_loaded.emit(
+ minecraft_version, None, error
+ )
+
+ threading.Thread(target=worker, daemon=True).start()
+
+ def _loader_versions_loaded(self, minecraft_version, loaders, error) -> None:
+ # Ignore an obsolete response after the user selected another version.
+ if minecraft_version != self.minecraft_version_dropdown.currentData():
+ return
+
+ if error is not None:
+ self._set_loading(
+ False,
+ self.app.tr_text(
+ "Unable to load Fabric versions: {error}",
+ error=self._error_text(error),
+ ),
+ )
+ return
+
+ self.loader_version_dropdown.clear()
+ for loader in loaders:
+ loader_version = loader.get("version")
+ if not loader_version:
+ continue
+ display_name = loader_version
+ if loader.get("stable") is True:
+ display_name += " (stable)"
+ self.loader_version_dropdown.addItem(display_name, loader)
+
+ self.loader_version_dropdown.setEnabled(
+ self.loader_version_dropdown.count() > 0
+ )
+ self._set_loading(False, self.app.tr_text("Select a loader version to continue."))
+
+ def install_selected_version(self) -> None:
+ minecraft_version = self.minecraft_version_dropdown.currentData()
+ loader_data = self.loader_version_dropdown.currentData()
+
+ if not minecraft_version or not isinstance(loader_data, dict):
+ self.status_label.setText(self.app.tr_text("Select a Minecraft and loader version."))
+ return
+
+ loader_version = loader_data.get("version")
+ if not loader_version:
+ self.status_label.setText(self.app.tr_text("The selected loader version is invalid."))
+ return
+
+ self._set_loading(True, self.app.tr_text("Creating the Fabric version manifest..."))
+ self.loader_dropdown.setEnabled(False)
+ self.minecraft_type_dropdown.setEnabled(False)
+ self.minecraft_version_dropdown.setEnabled(False)
+ self.loader_version_dropdown.setEnabled(False)
+
+ def worker():
+ try:
+ # Cache the parent manifest as well, so the new modded version
+ # can be resolved immediately when it is selected for launch.
+ parent = self.manifest_manager.get_cached_version_manifest(
+ minecraft_version
+ )
+ if not parent:
+ raise RuntimeError(
+ f"Unable to cache Minecraft {minecraft_version}."
+ )
+
+ version_id = fabric.get_full_version_string(
+ version=minecraft_version,
+ loader_version=loader_version,
+ )
+
+ def fetch_manifest(_version_id: str):
+ return (
+ fabric.get_loader_manifest_from_loader_data(
+ minecraft_version, loader_data
+ ),
+ None,
+ )
+
+ manifest = self.manifest_manager.get_cached_custom_version_manifest(
+ version_id,
+ fetch_manifest_handler=fetch_manifest,
+ )
+ if not manifest:
+ raise RuntimeError("Unable to create the Fabric version manifest.")
+
+ loader_info = find_useful_part_in_mod_version_manifest(manifest)
+ if loader_info["id"] != version_id:
+ raise ValueError(
+ f"Manifest ID mismatch: expected {version_id}, "
+ f"got {loader_info['id']}."
+ )
+ if loader_info["inheritsFrom"] != minecraft_version:
+ raise ValueError(
+ "The created manifest inherits from an unexpected "
+ "Minecraft version."
+ )
+
+ self.worker_signals.version_installed.emit(version_id, None)
+ except Exception as error:
+ self.worker_signals.version_installed.emit(None, error)
+
+ threading.Thread(target=worker, daemon=True).start()
+
+ def _version_installed(self, version_id, error) -> None:
+ if error is not None:
+ self.loader_dropdown.setEnabled(True)
+ self.minecraft_type_dropdown.setEnabled(True)
+ self.minecraft_version_dropdown.setEnabled(True)
+ self.loader_version_dropdown.setEnabled(True)
+ self._set_loading(False, self.app.tr_text(
+ "Unable to add version: {error}", error=self._error_text(error)
+ ))
+ return
+
+ self.status_label.setText(self.app.tr_text(
+ "Added version {version_id}.", version_id=version_id
+ ))
+ self.version_created.emit(version_id)
+ self.accept()
diff --git a/pages/create_profile.py b/pages/create_profile.py
index 156aac3..32942a1 100644
--- a/pages/create_profile.py
+++ b/pages/create_profile.py
@@ -13,6 +13,7 @@ from core_lib.game.version_info import get_all_versions, get_available_version_t
from manager.game import ManifestManager, GameManager
from manager.profile import CreateProfileRequest
from manager.widgets import SignalTransferItem, WidgetManager
+from pages.install_java_dialog import InstallJavaDialog
class CreateProfilePage(QWidget):
def __init__(self, app, parent=None):
@@ -33,7 +34,7 @@ class CreateProfilePage(QWidget):
self.operate_layout = QHBoxLayout()
# Some items
- self.version_type_label = QLabel("Version Type")
+ self.version_type_label = QLabel(self.app.tr_text("Version Type"))
self.version_type_dropdown = QComboBox()
self.version_type_dropdown.setObjectName("Dropdown")
@@ -42,7 +43,7 @@ class CreateProfilePage(QWidget):
icon = self.app.get_icon(Path(self.app.icon_dir, "mod_loader_temp.png"))
pixmap = icon.pixmap(QSize(100, 100))
self.mod_loader_icon_label.setPixmap(pixmap)
- self.mod_loader_label = QLabel("Mod Loader")
+ self.mod_loader_label = QLabel(self.app.tr_text("Mod Loader"))
self.mod_loader_dropdown = QComboBox()
self.mod_loader_dropdown.setObjectName("Dropdown")
@@ -50,9 +51,7 @@ class CreateProfilePage(QWidget):
self.mod_config_layout = QHBoxLayout()
self.mod_config_fields_layout = QVBoxLayout()
- self.mod_loader_version_label = QLabel(
- "Mod Loader Version"
- )
+ self.mod_loader_version_label = QLabel(self.app.tr_text("Mod Loader Version"))
self.mod_loader_version_list = QListWidget()
self.mod_loader_version_list.setObjectName(
@@ -60,23 +59,27 @@ class CreateProfilePage(QWidget):
)
# Version list
- self.version_list_type_label = QLabel("Version: Vanilla")
- self.version_list_label = QLabel("Select a version to continue:")
+ self.version_list_type_label = QLabel(self.app.tr_text("Version: Vanilla"))
+ self.version_list_label = QLabel(self.app.tr_text("Select a version to continue:"))
self.version_list = QListWidget()
self.version_list.setObjectName("ListWidget")
# Button
- self.create_profile_button = QPushButton("Create Profile")
+ self.create_profile_button = QPushButton(self.app.tr_text("Create Profile"))
self.create_profile_button.setObjectName("Button")
- self.refresh_button = QPushButton("Refresh")
+ self.refresh_button = QPushButton(self.app.tr_text("Refresh"))
self.refresh_button.setObjectName("Button")
+ self.install_java_button = QPushButton(self.app.tr_text("Install Java"))
+ self.install_java_button.setObjectName("Button")
+
# Bind widget
self.main_layout.addWidget(self.version_list_label)
self.main_layout.addWidget(self.version_list)
self.operate_layout.addWidget(self.version_list_type_label)
self.operate_layout.addStretch()
+ self.operate_layout.addWidget(self.install_java_button)
self.operate_layout.addWidget(self.refresh_button)
self.operate_layout.addWidget(self.create_profile_button)
@@ -142,6 +145,7 @@ class CreateProfilePage(QWidget):
# Button event
self.create_profile_button.clicked.connect(self.create_profile)
+ self.install_java_button.clicked.connect(self.create_install_jvm_dialog)
# Update version list while current type is changed
self.version_type_dropdown.currentTextChanged.connect(
@@ -406,7 +410,7 @@ class CreateProfilePage(QWidget):
if not request.wait(10):
self.widget_mgr.signal.show_error_message.emit(
- "Timeout while waiting for selected version.",
+ self.app.tr_text("Timeout while waiting for selected version."),
)
return
@@ -414,7 +418,7 @@ class CreateProfilePage(QWidget):
if not minecraft_version:
self.widget_mgr.signal.show_warning_message.emit(
- "Selected a version before creating profile.",
+ self.app.tr_text("Selected a version before creating profile."),
)
return
@@ -425,7 +429,7 @@ class CreateProfilePage(QWidget):
if not loader_request.wait(10):
self.widget_mgr.signal.show_error_message.emit(
- "Timeout while waiting for selected mod loader.",
+ self.app.tr_text("Timeout while waiting for selected mod loader."),
)
return
@@ -433,7 +437,7 @@ class CreateProfilePage(QWidget):
if loader_name not in self.loader_list:
self.widget_mgr.signal.show_error_message.emit(
- "Loader {} not found.".format(loader_name),
+ self.app.tr_text("Loader {loader} not found.", loader=loader_name),
)
return
@@ -442,7 +446,10 @@ class CreateProfilePage(QWidget):
if profile_create_handler is None:
self.widget_mgr.signal.show_warning_message.emit(
- "Profile creation for loader {} is not implemented.".format(loader_name),
+ self.app.tr_text(
+ "Profile creation for loader {loader} is not implemented.",
+ loader=loader_name,
+ ),
)
return
@@ -453,7 +460,7 @@ class CreateProfilePage(QWidget):
if not loader_version_request.wait(10):
self.widget_mgr.signal.show_error_message.emit(
- "Timeout while waiting for selected mod loader version.",
+ self.app.tr_text("Timeout while waiting for selected mod loader version."),
)
return
@@ -464,8 +471,8 @@ class CreateProfilePage(QWidget):
profile_name, accepted = QInputDialog.getText(
self,
- "Create Profile",
- "Profile name:",
+ self.app.tr_text("Create Profile"),
+ self.app.tr_text("Profile name:"),
text=f"Minecraft {minecraft_version}",
)
@@ -475,7 +482,7 @@ class CreateProfilePage(QWidget):
profile_name = profile_name.strip()
if not profile_name:
self.widget_mgr.signal.show_warning_message.emit(
- "Profile name cannot be empty.",
+ self.app.tr_text("Profile name cannot be empty."),
)
return
@@ -496,7 +503,7 @@ class CreateProfilePage(QWidget):
if not manifest:
self.widget_mgr.signal.show_warning_message.emit(
- "Unable to create profile due to manifest fetch failure.",
+ self.app.tr_text("Unable to create profile due to manifest fetch failure."),
)
return
@@ -508,7 +515,7 @@ class CreateProfilePage(QWidget):
if version_id is None:
self.widget_mgr.signal.show_error_message.emit(
- "Unable to create profile due to version ID is missing in manifest.",
+ self.app.tr_text("Unable to create profile due to version ID is missing in manifest."),
)
return
@@ -531,7 +538,7 @@ class CreateProfilePage(QWidget):
return
self.widget_mgr.signal.show_info_message.emit(
- f"Profile {profile_id} successfully created.",
+ self.app.tr_text("Profile {profile_id} successfully created.", profile_id=profile_id),
)
def handle_fabric_profile_create(self, profile_name: str, minecraft_version: str, loader_version: str | None, switch_to_current=False):
@@ -545,7 +552,7 @@ class CreateProfilePage(QWidget):
"""
if not loader_version:
self.widget_mgr.signal.show_warning_message.emit(
- "Selected a fabric loader version before creating profile.",
+ self.app.tr_text("Selected a fabric loader version before creating profile."),
)
return
@@ -555,7 +562,7 @@ class CreateProfilePage(QWidget):
)
if not vanilla_manifest:
self.widget_mgr.signal.show_error_message.emit(
- "Unable to create profile due to vanilla manifest fetch failure.",
+ self.app.tr_text("Unable to create profile due to vanilla manifest fetch failure."),
)
return
@@ -573,7 +580,7 @@ class CreateProfilePage(QWidget):
if not loader_manifest:
self.widget_mgr.signal.show_error_message.emit(
- "Unable to create profile due to mod loader manifest fetch failure.",
+ self.app.tr_text("Unable to create profile due to mod loader manifest fetch failure."),
)
return
@@ -585,7 +592,7 @@ class CreateProfilePage(QWidget):
if loader_info["id"] != version_id:
self.app.logger.error(f"Fabric manifest ID mismatch: expected {version_id}, got {loader_info["id"]}.")
self.widget_mgr.signal.show_error_message.emit(
- "Unable to create profile. Exising loader version ID mismatch.",
+ self.app.tr_text("Unable to create profile. Exising loader version ID mismatch."),
)
return
@@ -595,7 +602,7 @@ class CreateProfilePage(QWidget):
f'{loader_info["inheritsFrom"]}, not {minecraft_version}.'
)
self.widget_mgr.signal.show_error_message.emit(
- "Unable to create profile. Inherits version is mismatched as expected.",
+ self.app.tr_text("Unable to create profile. Inherits version is mismatched as expected."),
)
return
@@ -620,20 +627,23 @@ class CreateProfilePage(QWidget):
return
self.widget_mgr.signal.show_info_message.emit(
- f"Profile {profile_id} with fabric loader successfully created.",
+ self.app.tr_text(
+ "Profile {profile_id} with fabric loader successfully created.",
+ profile_id=profile_id,
+ ),
)
def handle_forge_profile_create(self, profile_name: str, minecraft_version: str, loader_version: str | None):
if not loader_version:
self.widget_mgr.signal.show_warning_message.emit(
- "Selected a forge loader version before creating profile.",
+ self.app.tr_text("Selected a forge loader version before creating profile."),
)
def handle_neoforge_profile_create(self, profile_name: str, minecraft_version: str, loader_version: str | None):
if not loader_version:
self.widget_mgr.signal.show_warning_message.emit(
- "Selected a neoforge loader version before creating profile.",
+ self.app.tr_text("Selected a neoforge loader version before creating profile."),
)
#
@@ -666,7 +676,7 @@ class CreateProfilePage(QWidget):
if not request.wait(10):
self.widget_mgr.signal.show_error_message.emit(
- "Timeout while waiting for selected type.",
+ self.app.tr_text("Timeout while waiting for selected type."),
)
return
@@ -674,7 +684,7 @@ class CreateProfilePage(QWidget):
if not selected_type:
self.widget_mgr.signal.show_warning_message.emit(
- "No type selected.",
+ self.app.tr_text("No type selected."),
)
return
@@ -715,14 +725,14 @@ class CreateProfilePage(QWidget):
if not request.wait(10):
self.widget_mgr.signal.show_error_message.emit(
- "Timeout while waiting for selected version.",
+ self.app.tr_text("Timeout while waiting for selected version."),
)
selected_version = request.result()
if not selected_version:
self.widget_mgr.signal.show_warning_message.emit(
- "No version selected.",
+ self.app.tr_text("No version selected."),
)
try:
@@ -731,12 +741,17 @@ class CreateProfilePage(QWidget):
)
except (VersionManifestFetchException, VersionManifestException) as e:
self.widget_mgr.signal.show_error_message.emit(
- "Failed to fetch version manifest: {}".format(e.user_message),
+ self.app.tr_text(
+ "Failed to fetch version manifest: {error}", error=e.user_message
+ ),
)
return
except Exception as e:
self.widget_mgr.signal.show_error_message.emit(
- "An unexpected error occurred while fetching version manifest: {}".format(e),
+ self.app.tr_text(
+ "An unexpected error occurred while fetching version manifest: {error}",
+ error=e,
+ ),
)
return
@@ -768,7 +783,7 @@ class CreateProfilePage(QWidget):
if not request.wait(10):
self.widget_mgr.signal.show_error_message.emit(
- "Timeout while waiting for selected loader.",
+ self.app.tr_text("Timeout while waiting for selected loader."),
)
return
@@ -782,7 +797,10 @@ class CreateProfilePage(QWidget):
if not version_handler:
self.widget_mgr.signal.show_warning_message.emit(
- "Selected loader \"{}\" is not implemented".format(loader_name),
+ self.app.tr_text(
+ 'Selected loader "{loader}" is not implemented',
+ loader=loader_name,
+ ),
)
return
@@ -791,4 +809,22 @@ class CreateProfilePage(QWidget):
else:
self.app.logger.warning(
"Select mod loader \"loadVersionMethod\" is not callable."
- )
\ No newline at end of file
+ )
+
+ def create_install_jvm_dialog(self):
+ manifest = None
+ selected = self.version_list.currentItem()
+ if selected:
+ version_id = (
+ selected.raw_value
+ if isinstance(selected, self.DisplayListItem)
+ else selected.text()
+ )
+ try:
+ resolved = self.manifest_mgr.resolve_manifest(version_id)
+ manifest = resolved.manifest if resolved else None
+ except Exception as exc:
+ self.app.logger.warning(f"Unable to resolve Java requirement for {version_id}: {exc}")
+ self.dialog = InstallJavaDialog(self.app, manifest=manifest, parent=self)
+ self.dialog.setModal(False)
+ self.dialog.show()
diff --git a/pages/install_java_dialog.py b/pages/install_java_dialog.py
new file mode 100644
index 0000000..c2f1f99
--- /dev/null
+++ b/pages/install_java_dialog.py
@@ -0,0 +1,150 @@
+import threading
+
+from PySide6.QtCore import QObject, Signal
+from PySide6.QtWidgets import (
+ QComboBox,
+ QDialog,
+ QDialogButtonBox,
+ QLabel,
+ QListWidget,
+ QPushButton,
+ QVBoxLayout,
+)
+
+from manager.game import InstallableJavaRuntime, JavaManager, JavaRuntime
+
+
+class InstallJavaDialog(QDialog):
+ """Install and register launcher-managed Mojang Java runtimes."""
+
+ class WorkerSignals(QObject):
+ components_loaded = Signal(object, object)
+ installed = Signal(object, object)
+
+ runtime_installed = Signal(object)
+
+ def __init__(self, app, manifest: dict | None = None, parent=None):
+ super().__init__(parent)
+ self.app = app
+ self.java_manager: JavaManager = app.game_manager.java
+ self.manifest = manifest or {}
+ self.worker_signals = self.WorkerSignals(self)
+ self.worker_signals.components_loaded.connect(self._components_loaded)
+ self.worker_signals.installed.connect(self._installed)
+
+ self.setWindowTitle(self.app.tr_text("Install Java"))
+ self.setMinimumWidth(480)
+ layout = QVBoxLayout(self)
+ self.requirement_label = QLabel(self._requirement_text())
+ self.runtime_list = QListWidget()
+ self.component_dropdown = QComboBox()
+ self.component_dropdown.setEditable(False)
+ self.status_label = QLabel(self.app.tr_text("Loading available Mojang runtimes..."))
+ self.install_button = QPushButton(self.app.tr_text("Install"))
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
+
+ layout.addWidget(self.requirement_label)
+ layout.addWidget(QLabel(self.app.tr_text("Detected Java runtimes")))
+ layout.addWidget(self.runtime_list)
+ layout.addWidget(QLabel(self.app.tr_text("Mojang runtime component")))
+ layout.addWidget(self.component_dropdown)
+ layout.addWidget(self.status_label)
+ layout.addWidget(self.install_button)
+ layout.addWidget(buttons)
+
+ buttons.rejected.connect(self.reject)
+ self.install_button.clicked.connect(self.install_selected)
+ self._refresh_runtime_list()
+ threading.Thread(target=self._load_components, daemon=True).start()
+
+ def _requirement_text(self) -> str:
+ requirement = self.manifest.get("javaVersion") or {}
+ major = requirement.get("majorVersion")
+ component = requirement.get("component")
+ if major:
+ return self.app.tr_text(
+ "This version requires Java {version}.", version=major
+ ) + f" ({component or 'component unknown'})"
+ return self.app.tr_text("Choose a Mojang Java runtime component to install.")
+
+ def _refresh_runtime_list(self) -> None:
+ self.runtime_list.clear()
+ for runtime in self.java_manager.list_runtimes():
+ state = "available" if runtime.valid else "missing"
+ self.runtime_list.addItem(
+ f"Java {runtime.major_version} | {runtime.source} | {state}\n{runtime.executable}"
+ )
+
+ def _load_components(self) -> None:
+ try:
+ runtimes = self.java_manager.list_installable_runtimes()
+ self.worker_signals.components_loaded.emit(runtimes, None)
+ except Exception as exc:
+ self.worker_signals.components_loaded.emit([], exc)
+
+ def _components_loaded(self, runtimes: list[InstallableJavaRuntime], error) -> None:
+ requirement = self.manifest.get("javaVersion") or {}
+ required = requirement.get("component")
+ required_major = requirement.get("majorVersion")
+ self.component_dropdown.clear()
+ for runtime in runtimes:
+ major = runtime.major_version
+ if runtime.component == required and required_major is not None:
+ # The Minecraft version manifest is authoritative for its own
+ # runtime requirement if Mojang's version name is unusual.
+ major = int(required_major)
+ label = f"Java {major} | {runtime.component}" if major is not None else runtime.component
+ if runtime.raw_version:
+ label += f" ({runtime.raw_version})"
+ self.component_dropdown.addItem(label, runtime.component)
+ if required:
+ index = self.component_dropdown.findData(required)
+ if index < 0:
+ label = f"Java {required_major} | {required}" if required_major else required
+ self.component_dropdown.addItem(label, required)
+ index = self.component_dropdown.count() - 1
+ self.component_dropdown.setCurrentIndex(index)
+ self.install_button.setEnabled(self.component_dropdown.count() > 0)
+ self.status_label.setText(
+ self.app.tr_text("Unable to load component list: {error}", error=error)
+ if error else self.app.tr_text("Ready to install.")
+ )
+
+ def install_selected(self) -> None:
+ component = self.component_dropdown.currentData()
+ if not component:
+ self.status_label.setText(self.app.tr_text("Select a runtime component."))
+ return
+ self.install_button.setEnabled(False)
+ self.component_dropdown.setEnabled(False)
+ self.status_label.setText(self.app.tr_text("Installing {component}...", component=component))
+ callback, task_id = self.app.game_manager.manifest.create_download_progress_callback(
+ self.app.tr_text("Installing Java ({component})", component=component)
+ )
+
+ def worker():
+ try:
+ runtime = self.java_manager.install_runtime(
+ component,
+ progress_callback=callback,
+ redownload_callback=self.app.game_manager.game_file.redownload_files,
+ )
+ self.app.widget_manager.download_signal.finished.emit(task_id)
+ self.worker_signals.installed.emit(runtime, None)
+ except Exception as exc:
+ self.app.widget_manager.download_signal.failed.emit(task_id, str(exc))
+ self.worker_signals.installed.emit(None, exc)
+
+ threading.Thread(target=worker, daemon=True).start()
+
+ def _installed(self, runtime: JavaRuntime | None, error) -> None:
+ self.install_button.setEnabled(True)
+ self.component_dropdown.setEnabled(True)
+ if error:
+ self.status_label.setText(self.app.tr_text("Installation failed: {error}", error=error))
+ return
+ self._refresh_runtime_list()
+ self.status_label.setText(self.app.tr_text(
+ "Java {version} installed successfully.", version=runtime.major_version
+ ))
+ self.runtime_installed.emit(runtime)
diff --git a/pages/launch_profile.py b/pages/launch_profile.py
index d2c4a78..227be08 100644
--- a/pages/launch_profile.py
+++ b/pages/launch_profile.py
@@ -208,7 +208,7 @@ class LaunchProfilePage(QWidget):
self.bottom_layout = QHBoxLayout()
# Widgets
- self.status_label = QLabel("Select a profile to launch")
+ self.status_label = QLabel(self.app.tr_text("Select a profile to launch"))
self.status_label.setStyleSheet("font-size: 12px; font-weight: 600;")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -221,7 +221,7 @@ class LaunchProfilePage(QWidget):
self.profile_popup.profile_selected.connect(self.select_profile)
self.profile_popup.closed.connect(self.refresh_profile_button)
- launch_button = QPushButton("Launch Game")
+ launch_button = QPushButton(self.app.tr_text("Launch Game"))
launch_button.setObjectName("launchButton")
launch_button.setMinimumWidth(120)
launch_button.setMinimumHeight(42)
@@ -258,7 +258,7 @@ class LaunchProfilePage(QWidget):
# self.profile_frame.setGraphicsEffect(shadow)
# Header
- self.header_label = QLabel("Launch Profile")
+ self.header_label = QLabel(self.app.tr_text("Launch Profile"))
self.header_label.setObjectName("headerLabel")
self.header_layout = QHBoxLayout(self.profile_overlay)
@@ -322,16 +322,20 @@ class LaunchProfilePage(QWidget):
:return:
"""
if not result.success:
- self.status_label.setText("Launch Failed")
+ self.status_label.setText(self.app.tr_text("Launch Failed"))
self.widget_mgr.signal.show_error_message.emit(
- f"Unable to launch profile "
- f"{launch_profile.display_name}: "
- f"{result.error_message}"
+ self.app.tr_text(
+ "Unable to launch profile {profile}: {error}",
+ profile=launch_profile.display_name,
+ error=result.error_message,
+ )
)
+ self.status_label_cleanup()
return
# Create log view
log_window = ProcessLogWindow(
+ self.app,
result.process,
launch_profile.profile_id,
title=(
@@ -350,12 +354,17 @@ class LaunchProfilePage(QWidget):
self.process_log_windows.append(log_window)
log_window.show()
- self.status_label.setText("Launched!")
+ self.status_label.setText(self.app.tr_text("Launched!"))
# Cleanup for status label
+ self.status_label_cleanup()
+
+ def status_label_cleanup(self):
timer = QTimer(self)
timer.setSingleShot(True)
- timer.timeout.connect(lambda: self.status_label.setText("Select a profile to launch"))
+ timer.timeout.connect(lambda: self.status_label.setText(
+ self.app.tr_text("Select a profile to launch")
+ ))
timer.start(3000)
def load_profiles(self):
@@ -364,7 +373,7 @@ class LaunchProfilePage(QWidget):
if not profiles:
self.profile_button.setProperty("profile_id", None)
- self.profile_button.name_label.setText("No profiles available")
+ self.profile_button.name_label.setText(self.app.tr_text("No profiles available"))
self.profile_button.version_label.clear()
return
@@ -441,30 +450,30 @@ class LaunchProfilePage(QWidget):
if profile_id is None:
self.widget_mgr.signal.show_warning_message.emit(
- "There is no profile is selected."
- " Please create one first."
+ self.app.tr_text("There is no profile is selected. Please create one first.")
)
return
- self.status_label.setText("Preparing to launch...")
+ self.status_label.setText(self.app.tr_text("Preparing to launch..."))
launch_profile = self.app.profile_manager.get_launch_profile(profile_id)
if launch_profile is None:
- self.status_label.setText("Unable to load the selected profile.")
+ self.status_label.setText(self.app.tr_text("Unable to load the selected profile."))
return
if self.launch_manager.is_profile_running(launch_profile.profile_id):
request = AskForRequest(
- "Launch Request",
- "Target profile {} is running. Are you sure you want to launch it again?".format(
- launch_profile.display_name
+ self.app.tr_text("Launch Request"),
+ self.app.tr_text(
+ "Target profile {profile} is running. Are you sure you want to launch it again?",
+ profile=launch_profile.display_name,
),
)
self.widget_mgr.signal.askyesno.emit(request)
if not request.wait(60):
self.widget_mgr.signal.show_error_message.emit(
- "Time out waiting for request for launch."
+ self.app.tr_text("Time out waiting for request for launch.")
)
return
diff --git a/pages/manage_profile.py b/pages/manage_profile.py
index 29c4d4b..1f0b87f 100644
--- a/pages/manage_profile.py
+++ b/pages/manage_profile.py
@@ -6,23 +6,33 @@ from PySide6.QtGui import QDesktopServices, QIcon
from PySide6.QtWidgets import (
QFileDialog, QFormLayout, QFrame, QHBoxLayout, QLabel, QLineEdit,
QMessageBox, QPushButton, QScrollArea, QSizePolicy, QSpinBox,
- QVBoxLayout, QWidget,
+ QVBoxLayout, QWidget, QMenu, QComboBox,
)
from manager.profile import ProfileDetail, ProfileManager, ProfileSummary, UpdateProfileRequest
+from manager.game import AvailableVersion
+from pages.add_modded_version import AddModdedVersionDialog
class ManageProfileItem(QPushButton):
selected = Signal(object)
- def __init__(self, app, profile: ProfileSummary, parent=None):
+ def __init__(self, app, profile: ProfileSummary,
+ parent=None,
+ delete_callback=None,
+ duplicate_callback=None):
super().__init__(parent)
+ self.app = app
self.profile = profile
self.setObjectName("manageProfileItem")
self.setCheckable(True)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.setMinimumHeight(64)
+ # Callback
+ self.delete_callback = delete_callback
+ self.duplicate_callback = duplicate_callback
+
layout = QHBoxLayout(self)
layout.setContentsMargins(12, 8, 12, 8)
layout.setSpacing(12)
@@ -48,19 +58,57 @@ class ManageProfileItem(QPushButton):
layout.addLayout(texts, 1)
self.clicked.connect(lambda: self.selected.emit(self.profile))
+ # Enable right click menu
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+ self.customContextMenuRequested.connect(self.show_right_click_menu)
+
+ def show_right_click_menu(self, position):
+ menu = QMenu(self)
+
+ duplicate_action = menu.addAction(self.app.tr_text("Duplicate"))
+ remove_action = menu.addAction(self.app.tr_text("Delete"))
+
+ menu.addAction(duplicate_action)
+ menu.addAction(remove_action)
+
+ def duplicate_callback():
+ self.duplicate_callback(self.profile)
+
+ def delete_callback():
+ self.delete_callback(self.profile)
+
+ if callable(self.duplicate_callback):
+ duplicate_action.triggered.connect(duplicate_callback)
+
+ if callable(self.delete_callback):
+ remove_action.triggered.connect(delete_callback)
+
+ global_position = self.mapToGlobal(position)
+ menu.exec(global_position)
+
class ManageProfilePage(QWidget):
- """Select a profile on the left and edit its launcher settings on the right."""
+ """
+ Profile manage page
+ """
+
+ VERSION_CATEGORIES = (
+ ("Vanilla", "vanilla"),
+ ("Modded", "modded"),
+ ("Custom", "custom"),
+ )
def __init__(self, app, parent=None):
super().__init__(parent)
self.app = app
self.profile_manager: ProfileManager = app.profile_manager
+ self.manifest_manager = app.game_manager.manifest
self.selected_profile_id: str | None = None
self.profile_items: dict[str, ManageProfileItem] = {}
+ self.version_options: list[AvailableVersion] = []
root = QVBoxLayout(self)
- title = QLabel("Manage Profiles (Experimental)")
+ title = QLabel(self.app.tr_text("Manage Profiles"))
title.setObjectName("manageTitle")
root.addWidget(title)
@@ -68,6 +116,7 @@ class ManageProfilePage(QWidget):
columns.setSpacing(12)
root.addLayout(columns, 1)
+ # Widgets
self.list_scroll = self._scroll_area("manageProfileListScroll")
self.list_content = QWidget()
self.list_layout = QVBoxLayout(self.list_content)
@@ -87,7 +136,7 @@ class ManageProfilePage(QWidget):
self.options_scroll.setWidget(self.options_content)
columns.addWidget(self.options_scroll, 3)
- self.empty_label = QLabel("Select a profile from the list to edit its settings.")
+ self.empty_label = QLabel(self.app.tr_text("Select a profile from the list to edit its settings."))
self.empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.empty_label.setWordWrap(True)
self.options_layout.addWidget(self.empty_label, 1)
@@ -101,45 +150,58 @@ class ManageProfilePage(QWidget):
form.setVerticalSpacing(12)
self.name_edit = QLineEdit()
- self.version_edit = QLineEdit()
- self.type_edit = QLineEdit()
+ self.version_dropdown = QComboBox()
+ self.version_dropdown.setObjectName("Dropdown")
+ self.version_dropdown.setSizePolicy(
+ QSizePolicy.Policy.Expanding,
+ QSizePolicy.Policy.Fixed,
+ )
+ self.version_dropdown.setSizeAdjustPolicy(
+ QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon
+ )
+ self.version_dropdown.setMinimumContentsLength(24)
+ self.type_dropdown = QComboBox()
+ self.type_dropdown.setObjectName("Dropdown")
self.game_dir_edit = QLineEdit()
self.java_args_edit = QLineEdit()
self.icon_edit = QLineEdit()
self.width_spin = self._dimension_spin()
self.height_spin = self._dimension_spin()
- form.addRow("Profile name", self.name_edit)
- form.addRow("Version ID", self.version_edit)
- form.addRow("Profile type", self.type_edit)
- form.addRow("Game directory", self.game_dir_edit)
- form.addRow("Java arguments", self.java_args_edit)
- form.addRow("Icon", self.icon_edit)
+ form.addRow(self.app.tr_text("Profile name"), self.name_edit)
+ form.addRow(self.app.tr_text("Type"), self.type_dropdown)
+ form.addRow(self.app.tr_text("Version"), self.version_dropdown)
+ form.addRow(self.app.tr_text("Game directory"), self.game_dir_edit)
+ form.addRow(self.app.tr_text("Java arguments"), self.java_args_edit)
+ form.addRow(self.app.tr_text("Icon"), self.icon_edit)
resolution = QHBoxLayout()
resolution.addWidget(self.width_spin)
resolution.addWidget(QLabel("×"))
resolution.addWidget(self.height_spin)
- form.addRow("Resolution", resolution)
+ form.addRow(self.app.tr_text("Resolution"), resolution)
editor_layout.addLayout(form)
file_actions = QHBoxLayout()
- open_button = QPushButton("Open Profile Folder")
+ add_version_button = QPushButton(self.app.tr_text("Add Modded Version"))
+ add_version_button.clicked.connect(self.open_add_modded_version_dialog)
+ open_button = QPushButton(self.app.tr_text("Open Profile Folder"))
open_button.clicked.connect(self.open_profile_folder)
- import_button = QPushButton("Import Mods…")
+ import_button = QPushButton(self.app.tr_text("Import Mods…"))
import_button.clicked.connect(self.import_mods)
+ file_actions.addWidget(add_version_button)
file_actions.addWidget(open_button)
file_actions.addWidget(import_button)
file_actions.addStretch()
editor_layout.addLayout(file_actions)
actions = QHBoxLayout()
- duplicate_button = QPushButton("Duplicate")
+ duplicate_button = QPushButton(self.app.tr_text("Duplicate"))
duplicate_button.clicked.connect(self.duplicate_profile)
- delete_button = QPushButton("Delete")
+ delete_button = QPushButton(self.app.tr_text("Delete"))
delete_button.setObjectName("dangerButton")
delete_button.clicked.connect(self.delete_profile)
- save_button = QPushButton("Save Changes")
+ save_button = QPushButton(self.app.tr_text("Save Changes"))
save_button.setObjectName("saveProfileButton")
save_button.clicked.connect(self.save_profile)
actions.addWidget(duplicate_button)
@@ -152,9 +214,11 @@ class ManageProfilePage(QWidget):
self.options_layout.addStretch()
self.editor.hide()
- self.profile_manager.profiles_changed.connect(self.load_profiles)
+ self.type_dropdown.currentIndexChanged.connect(self._filter_versions)
+ self.profile_manager.profiles_changed.connect(self.refresh_profiles)
self.app.apply_qss(Path("manage_profile.qss"), self.setStyleSheet)
- self.load_profiles()
+ self._load_type_dropdown()
+ self.refresh_profiles()
@staticmethod
def _scroll_area(name: str) -> QScrollArea:
@@ -165,31 +229,68 @@ class ManageProfilePage(QWidget):
area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
return area
- @staticmethod
- def _dimension_spin() -> QSpinBox:
+ def _dimension_spin(self) -> QSpinBox:
spin = QSpinBox()
spin.setRange(0, 16384)
- spin.setSpecialValueText("Default")
+ spin.setSpecialValueText(self.app.tr_text("Default"))
spin.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
return spin
+ def _load_type_dropdown(self) -> None:
+ self.type_dropdown.blockSignals(True)
+ self.type_dropdown.clear()
+ for display_name, category in self.VERSION_CATEGORIES:
+ self.type_dropdown.addItem(self.app.tr_text(display_name), category)
+ self.type_dropdown.blockSignals(False)
+
+ def refresh_profiles(self) -> None:
+ self.version_options = self.manifest_manager.list_available_profile_versions()
+ self.load_profiles()
+
+ def _filter_versions(self, _index: int = -1) -> None:
+ selected_category = self.type_dropdown.currentData()
+ selected_option = self.version_dropdown.currentData()
+ selected_id = (
+ selected_option.id
+ if isinstance(selected_option, AvailableVersion)
+ else None
+ )
+
+ self.version_dropdown.blockSignals(True)
+ self.version_dropdown.clear()
+
+ for option in self.version_options:
+ if option.source == selected_category:
+ self.version_dropdown.addItem(option.display_name, option)
+
+ if selected_id is not None:
+ self._select_version(selected_id)
+
+ self.version_dropdown.blockSignals(False)
+
def load_profiles(self) -> None:
while self.list_layout.count():
item = self.list_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
+
self.profile_items.clear()
profiles = self.profile_manager.list_profiles()
for profile in profiles:
- item = ManageProfileItem(self.app, profile, self.list_content)
+ item = ManageProfileItem(self.app, profile,
+ self.list_content,
+ duplicate_callback=self.duplicate_profile,
+ delete_callback=self.delete_profile)
item.selected.connect(self.select_profile)
self.profile_items[profile.id] = item
self.list_layout.addWidget(item)
available_ids = {profile.id for profile in profiles}
+
if self.selected_profile_id not in available_ids:
self.selected_profile_id = None
+
if self.selected_profile_id is None and profiles:
current = self.profile_manager.get_current_profile_id()
selected = next((p for p in profiles if p.id == current), profiles[0])
@@ -203,19 +304,53 @@ class ManageProfilePage(QWidget):
def select_profile(self, profile: ProfileSummary) -> None:
detail = self.profile_manager.get_profile(profile.id)
+
if detail is None:
return
+
self.selected_profile_id = profile.id
+
for profile_id, item in self.profile_items.items():
item.setChecked(profile_id == profile.id)
+
self._set_detail(detail)
self.empty_label.hide()
self.editor.show()
+ def _select_version(self, version_id: str) -> None:
+ for index in range(self.version_dropdown.count()):
+ data = self.version_dropdown.itemData(index)
+
+ if isinstance(data, AvailableVersion) and data.id == version_id:
+ self.version_dropdown.setCurrentIndex(index)
+ return
+
+ def _select_type(self, category: str) -> None:
+ index = self.type_dropdown.findData(category)
+ if index >= 0:
+ self.type_dropdown.setCurrentIndex(index)
+
def _set_detail(self, profile: ProfileDetail) -> None:
self.name_edit.setText(profile.name)
- self.version_edit.setText(profile.version_id)
- self.type_edit.setText(profile.version_type)
+ option = next(
+ (item for item in self.version_options if item.id == profile.version_id),
+ None,
+ )
+
+ # Preserve profiles imported from another launcher, even when their
+ # version manifest is not available locally anymore.
+ if option is None:
+ option = AvailableVersion(
+ id=profile.version_id,
+ display_name=f"{profile.version_id} (Unavailable)",
+ version_type=profile.version_type or "custom",
+ source="custom",
+ )
+ self.version_options.append(option)
+
+ self._select_type(option.source)
+ self._filter_versions()
+ self._select_version(profile.version_id)
self.game_dir_edit.setText(str(profile.game_dir or ""))
self.java_args_edit.setText(profile.java_args)
self.icon_edit.setText(profile.icon)
@@ -226,87 +361,139 @@ class ManageProfilePage(QWidget):
def _profile_dir(self) -> Path | None:
if self.selected_profile_id is None:
return None
+
detail = self.profile_manager.get_profile(self.selected_profile_id)
+
if detail is None:
return None
+
return detail.game_dir or Path(self.app.default_game_dir)
def save_profile(self) -> None:
if self.selected_profile_id is None:
return
+
width, height = self.width_spin.value(), self.height_spin.value()
+
if (width == 0) != (height == 0):
self.app.widget_manager.signal.show_warning_message.emit(
- "Set both resolution values, or set both to Default."
+ self.app.tr_text("Set both resolution values, or set both to Default.")
)
return
+
game_dir_text = self.game_dir_edit.text().strip()
+ version = self.version_dropdown.currentData()
+
+ if not isinstance(version, AvailableVersion):
+ self.app.widget_manager.signal.show_warning_message.emit(
+ self.app.tr_text("Select a version.")
+ )
+ return
+
request = UpdateProfileRequest(
name=self.name_edit.text().strip(),
- version_id=self.version_edit.text().strip(),
- version_type=self.type_edit.text().strip(),
+ version_id=version.id,
+ version_type=version.version_type,
game_dir=Path(game_dir_text) if game_dir_text else None,
java_args=self.java_args_edit.text(),
icon=self.icon_edit.text().strip(),
resolution=(width, height) if width and height else None,
)
+
if self.profile_manager.validate_update(request).is_valid and self.profile_manager.update_profile(
self.selected_profile_id, request
):
self.profile_manager.save()
- self.app.widget_manager.signal.show_info_message.emit("Profile settings saved.")
+ self.app.widget_manager.signal.show_info_message.emit(
+ self.app.tr_text("Profile settings saved.")
+ )
else:
errors = self.profile_manager.validate_update(request).errors
if errors:
self.app.widget_manager.signal.show_warning_message.emit("\n".join(errors.values()))
+ def open_add_modded_version_dialog(self) -> None:
+ dialog = AddModdedVersionDialog(self.app, self)
+ dialog.version_created.connect(self._modded_version_created)
+ dialog.exec()
+
+ def _modded_version_created(self, version_id: str) -> None:
+ self.version_options = self.manifest_manager.list_available_profile_versions()
+ # self._select_type("modded")
+ # self._filter_versions()
+ # self._select_version(version_id)
+ self.app.widget_manager.signal.show_info_message.emit(
+ self.app.tr_text("Modded version {version_id} added.", version_id=version_id)
+ )
+
def open_profile_folder(self) -> None:
profile_dir = self._profile_dir()
+
if profile_dir is None:
return
+
profile_dir.mkdir(parents=True, exist_ok=True)
QDesktopServices.openUrl(QUrl.fromLocalFile(str(profile_dir.resolve())))
def import_mods(self) -> None:
profile_dir = self._profile_dir()
+
if profile_dir is None:
return
+
files, _ = QFileDialog.getOpenFileNames(
- self, "Import Mods", "", "Minecraft mods (*.jar *.zip);;All files (*)"
+ self, self.app.tr_text("Import Mods"), "", "Minecraft mods (*.jar *.zip);;All files (*)"
)
+
if not files:
return
+
mods_dir = profile_dir / "mods"
mods_dir.mkdir(parents=True, exist_ok=True)
+
try:
for source in files:
shutil.copy2(source, mods_dir / Path(source).name)
except OSError as error:
- self.app.widget_manager.signal.show_error_message.emit(f"Unable to import mod: {error}")
+ self.app.widget_manager.signal.show_error_message.emit(
+ self.app.tr_text("Unable to import mod: {error}", error=error)
+ )
return
+
self.app.widget_manager.signal.show_info_message.emit(
- f"Imported {len(files)} mod(s) into {mods_dir}."
+ self.app.tr_text(
+ "Imported {count} mod(s) into {directory}.",
+ count=len(files), directory=mods_dir,
+ )
)
- def duplicate_profile(self) -> None:
- if self.selected_profile_id is None:
+ def duplicate_profile(self, profile: ProfileSummary | None=None) -> None:
+ if self.selected_profile_id is None and profile is None:
return
- new_id = self.profile_manager.duplicate_profile(self.selected_profile_id)
+
+ profile_id = self.selected_profile_id
+
+ new_id = self.profile_manager.duplicate_profile(profile_id)
+
if new_id:
self.selected_profile_id = new_id
self.load_profiles()
self.profile_manager.save()
- def delete_profile(self) -> None:
- if self.selected_profile_id is None:
+ def delete_profile(self, profile: ProfileSummary | None=None) -> None:
+ if self.selected_profile_id is None and profile is None:
return
+
+ profile_id = self.selected_profile_id
+
answer = QMessageBox.question(
- self, "Delete Profile", "Delete this profile? Game files will not be removed.",
+ self,
+ self.app.tr_text("Delete Profile"),
+ self.app.tr_text("Delete this profile? Game files will not be removed."),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if answer == QMessageBox.StandardButton.Yes:
- profile_id = self.selected_profile_id
- self.selected_profile_id = None
if self.profile_manager.remove_profile(profile_id):
+ self.selected_profile_id = None
self.profile_manager.save()
diff --git a/pages/process_log.py b/pages/process_log.py
index e6dd595..8f3856b 100644
--- a/pages/process_log.py
+++ b/pages/process_log.py
@@ -31,24 +31,25 @@ class ProcessLogWindow(QMainWindow):
A Simple client log view
"""
- def __init__(self, process: subprocess.Popen, profile_id: str, title: str = "Game Log",
+ def __init__(self, app, process: subprocess.Popen, profile_id: str, title: str = "Game Log",
parent: QWidget | None = None, finished_callback: Callable[[str, int], None]=None) -> None:
super().__init__(parent)
+ self.app = app
self.process = process
self.profile_id = profile_id
self._signals = _ProcessSignals(self)
self._last_error_message: str | None = None
self._stderr_lines: deque[str] = deque(maxlen=500)
- self.setWindowTitle(title)
+ self.setWindowTitle(self.app.tr_text(title))
self.resize(700, 720)
container = QWidget(self)
layout = QVBoxLayout(container)
controls = QHBoxLayout()
- self.status_label = QLabel(f"Running (PID {process.pid})")
- self.kill_button = QPushButton("Kill Process")
+ self.status_label = QLabel(self.app.tr_text("Running (PID {pid})", pid=process.pid))
+ self.kill_button = QPushButton(self.app.tr_text("Kill Process"))
self.kill_button.clicked.connect(self.kill_process)
self.log_output = QPlainTextEdit()
@@ -135,18 +136,20 @@ class ProcessLogWindow(QMainWindow):
return
self.kill_button.setEnabled(False)
- self.status_label.setText("Killing process...")
+ self.status_label.setText(self.app.tr_text("Killing process..."))
try:
self.process.kill()
except Exception as exc:
self.kill_button.setEnabled(True)
- self.status_label.setText("Unable to kill process")
+ self.status_label.setText(self.app.tr_text("Unable to kill process"))
self.append_message(f"[launcher] Unable to kill process: {exc}")
def _process_finished(self, exit_code: int) -> None:
self.kill_button.setEnabled(False)
- self.status_label.setText(f"Process exited with code {exit_code}")
+ self.status_label.setText(self.app.tr_text(
+ "Process exited with code {exit_code}", exit_code=exit_code
+ ))
self.append_message(f"[launcher] Process exited with code {exit_code}.")
if exit_code != 0 and self._last_error_message:
@@ -164,8 +167,10 @@ class ProcessLogWindow(QMainWindow):
messagebox.error_with_plain_text(
self,
- title="Minecraft Client Error",
- message=f"Minecraft exited with code {exit_code}.",
+ title=self.app.tr_text("Minecraft Client Error"),
+ message=self.app.tr_text(
+ "Minecraft exited with code {exit_code}.", exit_code=exit_code
+ ),
content=error_content,
)
diff --git a/pages/settings.py b/pages/settings.py
index 5bb6b30..cba9700 100644
--- a/pages/settings.py
+++ b/pages/settings.py
@@ -111,9 +111,44 @@ class SettingsPage(QWidget):
root_layout.addWidget(self.stack)
self.app.apply_qss(Path("settings.qss"), self.setStyleSheet)
self.language_combo.currentIndexChanged.connect(self._mark_settings_dirty)
- self.theme_combo.currentIndexChanged.connect(self._mark_settings_dirty)
self.client_id_edit.textEdited.connect(self._mark_settings_dirty)
+ self.theme_combo.currentIndexChanged.connect(
+ self._apply_selected_theme
+ )
+ self.app.i18n.language_changed.connect(self.retranslate_ui)
+
+ def _apply_selected_theme(self) -> None:
+ theme = self.theme_combo.currentData()
+ self.app.apply_theme(theme)
+ self._mark_settings_dirty()
+
+ def retranslate_ui(self, *_args) -> None:
+ """Rebuild this page so every label uses the active catalog."""
+ current_page = self.stack.currentIndex()
+ while self.stack.count():
+ old_page = self.stack.widget(0)
+ self.stack.removeWidget(old_page)
+ old_page.deleteLater()
+
+ self.main_page = self._create_main_page()
+ self.general_page = self._create_general_page()
+ self.appearance_page = self._create_appearance_page()
+ self.account_page = self._create_account_page()
+ self.about_page = self._create_about_page()
+ for page in (
+ self.main_page,
+ self.general_page,
+ self.appearance_page,
+ self.account_page,
+ self.about_page,
+ ):
+ self.stack.addWidget(page)
+ self.stack.setCurrentIndex(min(current_page, self.stack.count() - 1))
+ self.language_combo.currentIndexChanged.connect(self._mark_settings_dirty)
+ self.client_id_edit.textEdited.connect(self._mark_settings_dirty)
+ self.theme_combo.currentIndexChanged.connect(self._apply_selected_theme)
+
def _create_main_page(self) -> QWidget:
page = QWidget()
page.setObjectName("settingsMainPage")
@@ -132,16 +167,17 @@ class SettingsPage(QWidget):
scroll_layout.setContentsMargins(24, 20, 24, 24)
scroll_layout.setSpacing(12)
- title = QLabel("Settings")
+ tr = self.app.tr_text
+ title = QLabel(tr("Settings"))
title.setObjectName("settingsPageTitle")
scroll_layout.addWidget(title)
scroll_layout.addSpacing(8)
pages = (
- ("General", "Language and launcher directories", "general_page"),
- ("Appearance", "Launcher theme", "appearance_page"),
- ("Account", "Microsoft authentication settings", "account_page"),
- ("About", "Version and project information", "about_page"),
+ (tr("General"), tr("Language and launcher directories"), "general_page"),
+ (tr("Appearance"), tr("Launcher theme"), "appearance_page"),
+ (tr("Account"), tr("Microsoft authentication settings"), "account_page"),
+ (tr("About"), tr("Version and project information"), "about_page"),
)
for title_text, description, page_name in pages:
button = SettingsItem(title_text, description)
@@ -157,21 +193,22 @@ class SettingsPage(QWidget):
return page
def _create_general_page(self) -> QWidget:
- page = SettingsSubPage("General", self._go_back)
+ tr = self.app.tr_text
+ page = SettingsSubPage(tr("General"), self._go_back)
current = self.settings.get("general", GeneralSettings)
- page.content_layout.addWidget(QLabel("Current launcher working directory"))
+ page.content_layout.addWidget(QLabel(tr("Current launcher working directory")))
launcher_dir = QLineEdit(self.app.work_dir.as_posix())
launcher_dir.setReadOnly(True)
page.content_layout.addWidget(launcher_dir)
- page.content_layout.addWidget(QLabel("Current program directory"))
+ page.content_layout.addWidget(QLabel(tr("Current program directory")))
program_dir = QLineEdit(self.app.program_dir.as_posix())
program_dir.setReadOnly(True)
page.content_layout.addWidget(program_dir)
page.content_layout.addSpacing(8)
- page.content_layout.addWidget(QLabel("Language"))
+ page.content_layout.addWidget(QLabel(tr("Language")))
self.language_combo = self._choice_combo(
"general", "language", LANGUAGE_LABELS, current.language
)
@@ -179,10 +216,11 @@ class SettingsPage(QWidget):
return page
def _create_appearance_page(self) -> QWidget:
- page = SettingsSubPage("Appearance", self._go_back)
+ tr = self.app.tr_text
+ page = SettingsSubPage(tr("Appearance"), self._go_back)
current = self.settings.get("appearance", AppearanceSettings)
- page.content_layout.addWidget(QLabel("Theme"))
+ page.content_layout.addWidget(QLabel(tr("Theme")))
self.theme_combo = self._choice_combo(
"appearance", "theme", THEME_LABELS, current.theme
)
@@ -190,30 +228,32 @@ class SettingsPage(QWidget):
return page
def _create_account_page(self) -> QWidget:
- page = SettingsSubPage("Account", self._go_back)
+ tr = self.app.tr_text
+ page = SettingsSubPage(tr("Account"), self._go_back)
current = self.settings.get("account", AccountSettings)
- page.content_layout.addWidget(QLabel("Microsoft OAuth Client ID"))
+ page.content_layout.addWidget(QLabel(tr("Microsoft OAuth Client ID")))
self.client_id_edit = QLineEdit(str(current.client_id))
self.client_id_edit.setPlaceholderText(
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
)
page.content_layout.addWidget(self.client_id_edit)
- help_label = QLabel(
+ help_label = QLabel(tr(
"The Client ID must belong to an Azure application configured "
"for Microsoft device-code authentication."
- )
+ ))
help_label.setObjectName("settingsHelpText")
help_label.setWordWrap(True)
page.content_layout.addWidget(help_label)
return page
def _create_about_page(self) -> QWidget:
- page = SettingsSubPage("About", self._go_back)
+ tr = self.app.tr_text
+ page = SettingsSubPage(tr("About"), self._go_back)
brand_label = QLabel(LAUNCHER_NAME)
brand_label.setObjectName("settingsBrand")
- version_label = QLabel(f"Version: {LAUNCHER_VERSION}")
+ version_label = QLabel(tr("Version: {version}", version=LAUNCHER_VERSION))
version_label.setObjectName("settingsVersion")
description = QPlainTextEdit(LAUNCHER_DESCRIPTION)
description.setObjectName("settingsDescription")
@@ -233,7 +273,10 @@ class SettingsPage(QWidget):
) -> QComboBox:
combo = QComboBox()
for value in self.settings.choices(section, field):
- combo.addItem(labels.get(value, str(value)), value)
+ combo.addItem(
+ self.app.tr_text(labels.get(value, str(value))),
+ value,
+ )
index = combo.findData(current)
combo.setCurrentIndex(max(index, 0))
return combo
@@ -257,10 +300,12 @@ class SettingsPage(QWidget):
self.settings.save()
except Exception as error:
if show_error:
- QMessageBox.critical(self, "Settings error", str(error))
+ QMessageBox.critical(self, self.app.tr_text("Settings error"), str(error))
return False
self._settings_dirty = False
+ selected_language = data["general"]["language"]
+ self.app.apply_language(selected_language)
return True
def _open_page(self, page: QWidget) -> None:
diff --git a/pages/test.py b/pages/test.py
index b73d96b..4abad3f 100644
--- a/pages/test.py
+++ b/pages/test.py
@@ -43,31 +43,31 @@ class TestPage(QWidget):
self.resize(800, 600)
self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint)
- self.button = QPushButton("Fetch Version Data")
+ self.button = QPushButton(self.app.tr_text("Fetch Version Data"))
self.button.setObjectName("ActionButton")
self.button.clicked.connect(self.run_test)
- self.button_2 = QPushButton("Download client")
+ self.button_2 = QPushButton(self.app.tr_text("Download client"))
self.button_2.setObjectName("ActionButton")
self.button_2.clicked.connect(self.run_download_client)
- self.button_3 = QPushButton("Download libraries")
+ self.button_3 = QPushButton(self.app.tr_text("Download libraries"))
self.button_3.setObjectName("ActionButton")
self.button_3.clicked.connect(self.run_download_libraries)
- self.button_4 = QPushButton("Refresh versions")
+ self.button_4 = QPushButton(self.app.tr_text("Refresh versions"))
self.button_4.setObjectName("ActionButton")
self.button_4.clicked.connect(self.load_all_version)
- self.button_5 = QPushButton("Download assets")
+ self.button_5 = QPushButton(self.app.tr_text("Download assets"))
self.button_5.setObjectName("ActionButton")
self.button_5.clicked.connect(self.download_assets)
- self.button_6 = QPushButton("Parse Arguments")
+ self.button_6 = QPushButton(self.app.tr_text("Parse Arguments"))
self.button_6.setObjectName("ActionButton")
self.button_6.clicked.connect(self.parse_argument)
- self.button_7 = QPushButton("Launch Game! (Offline)")
+ self.button_7 = QPushButton(self.app.tr_text("Launch Game! (Offline)"))
self.button_7.setObjectName("ActionButton")
self.button_7.clicked.connect(self.launch_offline_game)
diff --git a/resources/styles/settings.qss b/resources/styles/settings.qss
index cbc8780..58d00fd 100644
--- a/resources/styles/settings.qss
+++ b/resources/styles/settings.qss
@@ -82,3 +82,38 @@ QLabel#settingsVersion {
QPlainTextEdit#settingsDescription {
font-size: 13px;
}
+
+QWidget#settingsPage QComboBox {
+ min-height: 22px;
+ padding: 6px 32px 6px 10px;
+ border: 1px solid palette(mid);
+ border-radius: 6px;
+ background-color: palette(base);
+ color: palette(text);
+ selection-background-color: palette(highlight);
+ selection-color: palette(highlighted-text);
+}
+
+QWidget#settingsPage QComboBox:hover {
+ border-color: palette(highlight);
+}
+
+QWidget#settingsPage QComboBox:focus {
+ border: 2px solid palette(highlight);
+ padding: 5px 31px 5px 9px;
+}
+
+/*QWidget#settingsPage QComboBox::drop-down {*/
+/* width: 28px;*/
+/* border: none;*/
+/* border-left: 1px solid palette(mid);*/
+/*}*/
+
+QWidget#settingsPage QComboBox QAbstractItemView {
+ border: 1px solid palette(mid);
+ background-color: palette(base);
+ color: palette(text);
+ selection-background-color: palette(highlight);
+ selection-color: palette(highlighted-text);
+ outline: none;
+}
diff --git a/resources/translations/en.json b/resources/translations/en.json
new file mode 100644
index 0000000..9956ed4
--- /dev/null
+++ b/resources/translations/en.json
@@ -0,0 +1,275 @@
+{
+ "About": "About",
+ "Account": "Account",
+ "Account status": "Account status",
+ "Accounts": "Accounts",
+ "Add Account": "Add Account",
+ "Add Modded Version": "Add Modded Version",
+ "Add Version": "Add Version",
+ "Added version {version_id}.": "Added version {version_id}.",
+ "Added {version_id}.": "Added {version_id}.",
+ "All assets downloaded successfully.": "All assets downloaded successfully.",
+ "All libraries downloaded successfully.": "All libraries downloaded successfully.",
+ "Appearance": "Appearance",
+ "Authentication failed.": "Authentication failed.",
+ "Authentication failed. Session may expire.": "Authentication failed. Session may expire.",
+ "Available": "Available",
+ "Cancel": "Cancel",
+ "Check login status": "Check login status",
+ "Checking Minecraft ownership": "Checking Minecraft ownership",
+ "Choose a Mojang Java runtime component to install.": "Choose a Mojang Java runtime component to install.",
+ "Close": "Close",
+ "Completed": "Completed",
+ "Copied!": "Copied!",
+ "Copy code": "Copy code",
+ "Copy the code below, then continue in your browser.": "Copy the code below, then continue in your browser.",
+ "Create a local mod-loader version manifest without creating a profile.": "Create a local mod-loader version manifest without creating a profile.",
+ "Create Profile": "Create Profile",
+ "Creating the Fabric version manifest...": "Creating the Fabric version manifest...",
+ "Current": "Current",
+ "Current launcher working directory": "Current launcher working directory",
+ "Current program directory": "Current program directory",
+ "Current version: {version}": "Current version: {version}",
+ "Dark": "Dark",
+ "Default": "Default",
+ "Delete": "Delete",
+ "Delete account": "Delete account",
+ "Delete All": "Delete All",
+ "Delete Profile": "Delete Profile",
+ "Delete this profile? Game files will not be removed.": "Delete this profile? Game files will not be removed.",
+ "Detected Java runtimes": "Detected Java runtimes",
+ "Don't Install": "Don't Install",
+ "Download assets": "Download assets",
+ "Download client": "Download client",
+ "Download Error": "Download Error",
+ "Download libraries": "Download libraries",
+ "Downloaded": "Downloaded",
+ "Downloading": "Downloading",
+ "Downloading assets for version {version}": "Downloading assets for version {version}",
+ "Downloading libraries for version {version}": "Downloading libraries for version {version}",
+ "Downloading missing assets": "Downloading missing assets",
+ "Downloads": "Downloads",
+ "Duplicate": "Duplicate",
+ "English": "English",
+ "Error": "Error",
+ "Fabric": "Fabric",
+ "Failed: {error}": "Failed: {error}",
+ "Fetch Error": "Fetch Error",
+ "Fetch Version Data": "Fetch Version Data",
+ "Follow system settings": "Follow system settings",
+ "Game directory": "Game directory",
+ "Game Log": "Game Log",
+ "General": "General",
+ "Home": "Home",
+ "Icon": "Icon",
+ "Import Mods": "Import Mods",
+ "Import Mods…": "Import Mods…",
+ "Imported {count} mod(s).": "Imported {count} mod(s).",
+ "Info": "Info",
+ "Install": "Install",
+ "Install Java": "Install Java",
+ "Installation failed: {error}": "Installation failed: {error}",
+ "Installing {component}...": "Installing {component}...",
+ "Java {version} installed successfully.": "Java {version} installed successfully.",
+ "Java arguments": "Java arguments",
+ "Java Version:": "Java Version:",
+ "Kill Process": "Kill Process",
+ "Killing process...": "Killing process...",
+ "Language": "Language",
+ "Language and launcher directories": "Language and launcher directories",
+ "Launch Failed": "Launch Failed",
+ "Launch Game": "Launch Game",
+ "Launch Game! (Offline)": "Launch Game! (Offline)",
+ "Launch Profile": "Launch Profile",
+ "Launch Request": "Launch Request",
+ "Launched!": "Launched!",
+ "Launcher error occurred.": "Launcher error occurred.",
+ "Launcher theme": "Launcher theme",
+ "Light": "Light",
+ "Loading all types...": "Loading all types...",
+ "Loading all versions...": "Loading all versions...",
+ "Loading available Mojang runtimes...": "Loading available Mojang runtimes...",
+ "Loading Fabric versions for {version}...": "Loading Fabric versions for {version}...",
+ "Loading Minecraft versions...": "Loading Minecraft versions...",
+ "Login Minecraft Account": "Login Minecraft Account",
+ "Main repo": "Main repo",
+ "Manage Profiles": "Manage Profiles",
+ "Microsoft authentication settings": "Microsoft authentication settings",
+ "Microsoft Client ID": "Microsoft Client ID",
+ "Microsoft Login": "Microsoft Login",
+ "Microsoft OAuth Client ID": "Microsoft OAuth Client ID",
+ "Microsoft sign-in could not be completed.": "Microsoft sign-in could not be completed.",
+ "Microsoft sign-in failed": "Microsoft sign-in failed",
+ "Minecraft Client Error": "Minecraft Client Error",
+ "Minecraft exited with code {exit_code}.": "Minecraft exited with code {exit_code}.",
+ "Minecraft type": "Minecraft type",
+ "Minecraft version": "Minecraft version",
+ "Mod Loader": "Mod Loader",
+ "Mod Loader Version": "Mod Loader Version",
+ "Mod loader": "Mod loader",
+ "Modded": "Modded",
+ "Mojang runtime component": "Mojang runtime component",
+ "No accounts yet. Use Add Account to sign in.": "No accounts yet. Use Add Account to sign in.",
+ "No content": "No content",
+ "No Minecraft version is available for this type.": "No Minecraft version is available for this type.",
+ "No profiles available": "No profiles available",
+ "No type selected.": "No type selected.",
+ "No version selected.": "No version selected.",
+ "Open browser": "Open browser",
+ "Open Profile Folder": "Open Profile Folder",
+ "Ouch. The icon is missing.": "Ouch. The icon is missing.",
+ "Parse Arguments": "Parse Arguments",
+ "Parse Error": "Parse Error",
+ "Preparing...": "Preparing...",
+ "Preparing to launch...": "Preparing to launch...",
+ "Process exited with code {exit_code}": "Process exited with code {exit_code}",
+ "Profile name": "Profile name",
+ "Profile name:": "Profile name:",
+ "Profile name cannot be empty.": "Profile name cannot be empty.",
+ "Profile settings saved.": "Profile settings saved.",
+ "Ready to install.": "Ready to install.",
+ "Redownload": "Redownload",
+ "Refresh": "Refresh",
+ "Refresh all": "Refresh all",
+ "Refresh versions": "Refresh versions",
+ "Requesting a Microsoft sign-in code…": "Requesting a Microsoft sign-in code…",
+ "Resolution": "Resolution",
+ "Resource Error": "Resource Error",
+ "Running (PID {pid})": "Running (PID {pid})",
+ "Save Changes": "Save Changes",
+ "Select a loader version to continue.": "Select a loader version to continue.",
+ "Select a Minecraft and loader version.": "Select a Minecraft and loader version.",
+ "Select a profile from the list to edit its settings.": "Select a profile from the list to edit its settings.",
+ "Select a profile to launch": "Select a profile to launch",
+ "Select a runtime component.": "Select a runtime component.",
+ "Select a version.": "Select a version.",
+ "Select a version before continuing.": "Select a version before continuing.",
+ "Select a version to continue:": "Select a version to continue:",
+ "Set both resolution values, or set both to Default.": "Set both resolution values, or set both to Default.",
+ "Settings": "Settings",
+ "Settings error": "Settings error",
+ "Signing in to Minecraft services": "Signing in to Minecraft services",
+ "Signing in to Xbox Live": "Signing in to Xbox Live",
+ "Some assets could not be downloaded. They are not required to launch the game, but missing assets may cause texture issues while you play.": "Some assets could not be downloaded. They are not required to launch the game, but missing assets may cause texture issues while you play.",
+ "Some files failed to download.": "Some files failed to download.",
+ "Some files failed to download. Would you like to retry downloading them?": "Some files failed to download. Would you like to retry downloading them?",
+ "Still digging!": "Still digging!",
+ "Switch to this account": "Switch to this account",
+ "Test Error": "Test Error",
+ "Tests": "Tests",
+ "The Client ID must belong to an Azure application configured for Microsoft device-code authentication.": "The Client ID must belong to an Azure application configured for Microsoft device-code authentication.",
+ "The selected loader version is invalid.": "The selected loader version is invalid.",
+ "Theme": "Theme",
+ "There is no profile is selected. Please create one first.": "There is no profile is selected. Please create one first.",
+ "This version requires Java {version}.": "This version requires Java {version}.",
+ "Type": "Type",
+ "Unable to kill process": "Unable to kill process",
+ "Unable to launch profile": "Unable to launch profile",
+ "Unable to load the Minecraft version manifest.": "Unable to load the Minecraft version manifest.",
+ "Unable to load the selected profile.": "Unable to load the selected profile.",
+ "Version": "Version",
+ "Version and project information": "Version and project information",
+ "Version Type": "Version Type",
+ "Version: Vanilla": "Version: Vanilla",
+ "Version: {version}": "Version: {version}",
+ "Waiting for Microsoft sign-in approval": "Waiting for Microsoft sign-in approval",
+ "Warning": "Warning",
+ "If you find an issue, you can report it in the main repository. Suggestions are always welcome!": "If you find an issue, you can report it in the main repository. Suggestions are always welcome!",
+ "An error occurred while connecting to authentication server.": "An error occurred while connecting to authentication server.",
+ "An error occurred while executing Java runtime.": "An error occurred while executing Java runtime.",
+ "An error occurred while fetching third party API.": "An error occurred while fetching third party API.",
+ "An error occurred while processing account data.": "An error occurred while processing account data.",
+ "An error occurred while processing profile.": "An error occurred while processing profile.",
+ "An settings error occurred.": "An settings error occurred.",
+ "Current platform or architecture is not supported.": "Current platform or architecture is not supported.",
+ "Delete all accounts": "Delete all accounts",
+ "Delete every saved account? This cannot be undone.": "Delete every saved account? This cannot be undone.",
+ "Microsoft approved the sign-in but did not return an access token. Please sign in again.": "Microsoft approved the sign-in but did not return an access token. Please sign in again.",
+ "Microsoft could not refresh this account. Please sign in again.": "Microsoft could not refresh this account. Please sign in again.",
+ "Microsoft did not return a refresh token. Ensure the offline_access permission is enabled, then sign in again.": "Microsoft did not return a refresh token. Ensure the offline_access permission is enabled, then sign in again.",
+ "Microsoft did not return a sign-in code. Check the client ID or update the launcher.": "Microsoft did not return a sign-in code. Check the client ID or update the launcher.",
+ "Microsoft sign-in was rejected (HTTP {status}): {description}": "Microsoft sign-in was rejected (HTTP {status}): {description}",
+ "Minecraft services accepted the Xbox session but did not return a game access token. Please sign in again.": "Minecraft services accepted the Xbox session but did not return a game access token. Please sign in again.",
+ "Minecraft services did not return a valid player profile. Ensure this account owns Minecraft and has created a profile.": "Minecraft services did not return a valid player profile. Ensure this account owns Minecraft and has created a profile.",
+ "Minecraft services returned an invalid ownership response. Please sign in again or try later.": "Minecraft services returned an invalid ownership response. Please sign in again or try later.",
+ "Modded version {version_id} added.": "Modded version {version_id} added.",
+ "No compatible native library was found for this platform.": "No compatible native library was found for this platform.",
+ "No version found. (This should never happen)": "No version found. (This should never happen)",
+ "Runtime manifest is missing or corrupted.": "Runtime manifest is missing or corrupted.",
+ "Settings section could not be registered.": "Settings section could not be registered.",
+ "Settings section could not be validated.": "Settings section could not be validated.",
+ "Specified account was not found.": "Specified account was not found.",
+ "Specified asset data section (key) not found.": "Specified asset data section (key) not found.",
+ "Specified profile key not found. Profile may corrupted.": "Specified profile key not found. Profile may corrupted.",
+ "Specified version data section (key) not found.": "Specified version data section (key) not found.",
+ "Specified version or type not found.": "Specified version or type not found.",
+ "The authentication request or session was rejected.": "The authentication request or session was rejected.",
+ "The authentication service received too many requests. Wait a moment and try again.": "The authentication service received too many requests. Wait a moment and try again.",
+ "The authentication service returned an unexpected response. Try again later.": "The authentication service returned an unexpected response. Try again later.",
+ "The login session is invalid or expired. Sign in again.": "The login session is invalid or expired. Sign in again.",
+ "Unable to extract native libraries.": "Unable to extract native libraries.",
+ "Unable to fetch asset manifest.": "Unable to fetch asset manifest.",
+ "Unable to fetch runtime manifest.": "Unable to fetch runtime manifest.",
+ "Unable to fetch third party API. Provider may no longer exist. Try updating the launcher.": "Unable to fetch third party API. Provider may no longer exist. Try updating the launcher.",
+ "Unable to fetch third party API. Try updating the launcher.": "Unable to fetch third party API. Try updating the launcher.",
+ "Unable to fetch version manifest.": "Unable to fetch version manifest.",
+ "Unable to find dependency.": "Unable to find dependency.",
+ "Unable to load account data. The account file may be corrupted.": "Unable to load account data. The account file may be corrupted.",
+ "Unable to load asset manifest.": "Unable to load asset manifest.",
+ "Unable to load profile. Profile may corrupted": "Unable to load profile. Profile may corrupted",
+ "Unable to load version manifest.": "Unable to load version manifest.",
+ "Unable to parse Java runtime details.": "Unable to parse Java runtime details.",
+ "Unable to resolve native libraries.": "Unable to resolve native libraries.",
+ "Unable to save account data. Try again later.": "Unable to save account data. Try again later.",
+ "Unable to save asset manifest.": "Unable to save asset manifest.",
+ "Unable to save profile. Try again later.": "Unable to save profile. Try again later.",
+ "Unable to save version manifest.": "Unable to save version manifest.",
+ "Unable to verify Minecraft ownership because the ownership list is missing or invalid.": "Unable to verify Minecraft ownership because the ownership list is missing or invalid.",
+ "Unable to verify Minecraft ownership because the ownership response is invalid.": "Unable to verify Minecraft ownership because the ownership response is invalid.",
+ "Xbox Live accepted the sign-in but did not return an authentication token. Please sign in again.": "Xbox Live accepted the sign-in but did not return an authentication token. Please sign in again.",
+ "Xbox Live did not return the account identifier. Check the account's Xbox profile and try again.": "Xbox Live did not return the account identifier. Check the account's Xbox profile and try again.",
+ "Xbox security authorization did not return a token. The account may need an Xbox profile or may be restricted.": "Xbox security authorization did not return a token. The account may need an Xbox profile or may be restricted.",
+ "Chinese (Traditional)": "Chinese (Traditional)",
+ "Installing Java ({component})": "Installing Java ({component})",
+ "Unable to load component list: {error}": "Unable to load component list: {error}",
+ "Target profile {profile} is running. Are you sure you want to launch it again?": "Target profile {profile} is running. Are you sure you want to launch it again?",
+ "Time out waiting for request for launch.": "Time out waiting for request for launch.",
+ "Imported {count} mod(s) into {directory}.": "Imported {count} mod(s) into {directory}.",
+ "Unable to import mod: {error}": "Unable to import mod: {error}",
+ "Loader version": "Loader version",
+ "Unable to load versions: {error}": "Unable to load versions: {error}",
+ "Unable to load Fabric versions: {error}": "Unable to load Fabric versions: {error}",
+ "Unable to add version: {error}": "Unable to add version: {error}",
+ "Unable to read version types: {error}": "Unable to read version types: {error}",
+ "Missing icon file: {path}": "Missing icon file: {path}",
+ "Missing stylesheet file: {path}": "Missing stylesheet file: {path}",
+ "Failed to load icon {path}: {error}": "Failed to load icon {path}: {error}",
+ "Unable to apply QSS stylesheet {path}: {error}": "Unable to apply QSS stylesheet {path}: {error}",
+ "{account} — Minecraft profile {status}": "{account} — Minecraft profile {status}",
+ "available": "available",
+ "not available": "not available",
+ "Profile {profile_id} successfully created.": "Profile {profile_id} successfully created.",
+ "Profile {profile_id} with fabric loader successfully created.": "Profile {profile_id} with fabric loader successfully created.",
+ "Timeout while waiting for selected version.": "Timeout while waiting for selected version.",
+ "Selected a version before creating profile.": "Selected a version before creating profile.",
+ "Timeout while waiting for selected mod loader.": "Timeout while waiting for selected mod loader.",
+ "Loader {loader} not found.": "Loader {loader} not found.",
+ "Profile creation for loader {loader} is not implemented.": "Profile creation for loader {loader} is not implemented.",
+ "Timeout while waiting for selected mod loader version.": "Timeout while waiting for selected mod loader version.",
+ "Unable to create profile due to manifest fetch failure.": "Unable to create profile due to manifest fetch failure.",
+ "Unable to create profile due to version ID is missing in manifest.": "Unable to create profile due to version ID is missing in manifest.",
+ "Selected a fabric loader version before creating profile.": "Selected a fabric loader version before creating profile.",
+ "Unable to create profile due to vanilla manifest fetch failure.": "Unable to create profile due to vanilla manifest fetch failure.",
+ "Unable to create profile due to mod loader manifest fetch failure.": "Unable to create profile due to mod loader manifest fetch failure.",
+ "Unable to create profile. Exising loader version ID mismatch.": "Unable to create profile. Exising loader version ID mismatch.",
+ "Unable to create profile. Inherits version is mismatched as expected.": "Unable to create profile. Inherits version is mismatched as expected.",
+ "Selected a forge loader version before creating profile.": "Selected a forge loader version before creating profile.",
+ "Selected a neoforge loader version before creating profile.": "Selected a neoforge loader version before creating profile.",
+ "Timeout while waiting for selected type.": "Timeout while waiting for selected type.",
+ "Timeout while waiting for selected loader.": "Timeout while waiting for selected loader.",
+ "Selected loader \"{loader}\" is not implemented": "Selected loader \"{loader}\" is not implemented",
+ "Failed to fetch version manifest: {error}": "Failed to fetch version manifest: {error}",
+ "An unexpected error occurred while fetching version manifest: {error}": "An unexpected error occurred while fetching version manifest: {error}",
+ "Unable to launch profile {profile}: {error}": "Unable to launch profile {profile}: {error}",
+ "To make all page apply new language. Is recommend to restart the launcher after change it.": "To make all page apply new language. Is recommend to restart the launcher after change it."
+}
diff --git a/resources/translations/zh-TW.json b/resources/translations/zh-TW.json
new file mode 100644
index 0000000..8ab9371
--- /dev/null
+++ b/resources/translations/zh-TW.json
@@ -0,0 +1,275 @@
+{
+ "About": "關於",
+ "Account": "帳號",
+ "Account status": "帳號狀態",
+ "Accounts": "帳號",
+ "Add Account": "新增帳號",
+ "Add Modded Version": "新增模組載入器版本",
+ "Add Version": "新增版本",
+ "Added version {version_id}.": "新增版本 {version_id} 。",
+ "All assets downloaded successfully.": "所有資源皆已成功下載。",
+ "All libraries downloaded successfully.": "所有程式庫皆已成功下載。",
+ "Appearance": "外觀",
+ "Authentication failed.": "驗證失敗。",
+ "Authentication failed. Session may expire.": "驗證失敗,登入工作階段可能已過期。",
+ "Available": "可用",
+ "Cancel": "取消",
+ "Check login status": "檢查登入狀態",
+ "Checking Minecraft ownership": "正在檢查 Minecraft 擁有權",
+ "Choose a Mojang Java runtime component to install.": "選擇要安裝的 Mojang Java 執行環境元件。",
+ "Close": "關閉",
+ "Completed": "已完成",
+ "Copied!": "已複製!",
+ "Copy code": "複製代碼",
+ "Copy the code below, then continue in your browser.": "複製下方代碼,然後在瀏覽器中繼續。",
+ "Create a local mod-loader version manifest without creating a profile.": "建立本機模組載入器版本資訊,而不建立設定檔。",
+ "Create Profile": "建立設定檔",
+ "Creating the Fabric version manifest...": "正在建立 Fabric 版本資訊……",
+ "Current": "目前使用",
+ "Current launcher working directory": "目前啟動器工作目錄",
+ "Current program directory": "目前程式目錄",
+ "Current version: {version}": "目前版本:{version}",
+ "Dark": "深色",
+ "Default": "預設",
+ "Delete": "刪除",
+ "Delete account": "刪除帳號",
+ "Delete All": "全部刪除",
+ "Delete Profile": "刪除設定檔",
+ "Delete this profile? Game files will not be removed.": "確定要刪除此設定檔嗎?遊戲檔案不會被移除。",
+ "Detected Java runtimes": "偵測到的 Java 執行環境",
+ "Don't Install": "不要安裝",
+ "Download assets": "下載資源",
+ "Download client": "下載用戶端",
+ "Download Error": "下載錯誤",
+ "Download libraries": "下載程式庫",
+ "Downloaded": "已下載",
+ "Downloading": "正在下載",
+ "Downloading assets for version {version}": "正在下載版本 {version} 的資源",
+ "Downloading libraries for version {version}": "正在下載版本 {version} 的程式庫",
+ "Downloading missing assets": "正在下載缺少的資源",
+ "Downloads": "下載項目",
+ "Duplicate": "建立副本",
+ "English": "English",
+ "Error": "錯誤",
+ "Fabric": "Fabric",
+ "Failed: {error}": "失敗:{error}",
+ "Fetch Error": "取得資料時發生錯誤",
+ "Fetch Version Data": "取得版本資料",
+ "Follow system settings": "跟隨系統設定",
+ "Game directory": "遊戲目錄",
+ "Game Log": "遊戲記錄",
+ "General": "一般",
+ "Home": "首頁",
+ "Icon": "圖示",
+ "Import Mods": "匯入模組",
+ "Import Mods…": "匯入模組……",
+ "Imported {count} mod(s).": "已匯入 {count} 個模組。",
+ "Info": "資訊",
+ "Install": "安裝",
+ "Install Java": "安裝 Java",
+ "Installation failed: {error}": "安裝失敗:{error}",
+ "Installing {component}...": "正在安裝 {component}……",
+ "Java {version} installed successfully.": "Java {version} 已成功安裝。",
+ "Java arguments": "Java 引數",
+ "Java Version:": "Java 版本:",
+ "Kill Process": "終止處理程序",
+ "Killing process...": "正在終止處理程序……",
+ "Language": "語言",
+ "Language and launcher directories": "語言與啟動器目錄",
+ "Launch Failed": "啟動失敗",
+ "Launch Game": "啟動遊戲",
+ "Launch Game! (Offline)": "啟動遊戲!(離線)",
+ "Launch Profile": "啟動設定檔",
+ "Launch Request": "啟動要求",
+ "Launched!": "已啟動!",
+ "Launcher error occurred.": "啟動器發生錯誤。",
+ "Launcher theme": "啟動器佈景主題",
+ "Light": "淺色",
+ "Loading all types...": "正在載入所有類型……",
+ "Loading all versions...": "正在載入所有版本……",
+ "Loading available Mojang runtimes...": "正在載入可用的 Mojang 執行環境……",
+ "Loading Fabric versions for {version}...": "正在載入適用於 {version} 的 Fabric 版本……",
+ "Loading Minecraft versions...": "正在載入 Minecraft 版本……",
+ "Login Minecraft Account": "登入 Minecraft 帳號",
+ "Main repo": "主要程式碼儲存庫",
+ "Manage Profiles": "管理設定檔",
+ "Microsoft authentication settings": "Microsoft 驗證設定",
+ "Microsoft Client ID": "Microsoft 用戶端 ID",
+ "Microsoft Login": "Microsoft 登入",
+ "Microsoft OAuth Client ID": "Microsoft OAuth 用戶端 ID",
+ "Microsoft sign-in could not be completed.": "無法完成 Microsoft 登入。",
+ "Microsoft sign-in failed": "Microsoft 登入失敗",
+ "Minecraft Client Error": "Minecraft 用戶端錯誤",
+ "Minecraft exited with code {exit_code}.": "Minecraft 已結束,結束代碼為 {exit_code}。",
+ "Minecraft type": "Minecraft 類型",
+ "Minecraft version": "Minecraft 版本",
+ "Mod Loader": "模組載入器",
+ "Mod Loader Version": "模組載入器版本",
+ "Mod loader": "模組載入器",
+ "Modded": "模組版",
+ "Mojang runtime component": "Mojang 執行環境元件",
+ "No accounts yet. Use Add Account to sign in.": "尚未新增帳號。請使用「新增帳號」登入。",
+ "No content": "沒有內容",
+ "No Minecraft version is available for this type.": "此類型沒有可用的 Minecraft 版本。",
+ "No profiles available": "沒有可用的設定檔",
+ "No type selected.": "尚未選擇類型。",
+ "No version selected.": "尚未選擇版本。",
+ "Open browser": "開啟瀏覽器",
+ "Open Profile Folder": "開啟設定檔資料夾",
+ "Ouch. The icon is missing.": "糟糕,找不到圖示。",
+ "Parse Arguments": "解析引數",
+ "Parse Error": "解析錯誤",
+ "Preparing...": "正在準備……",
+ "Preparing to launch...": "正在準備啟動……",
+ "Process exited with code {exit_code}": "處理程序已結束,結束代碼為 {exit_code}",
+ "Profile name": "設定檔名稱",
+ "Profile name:": "設定檔名稱:",
+ "Profile name cannot be empty.": "設定檔名稱不可為空白。",
+ "Profile settings saved.": "設定檔設定已儲存。",
+ "Ready to install.": "已準備好安裝。",
+ "Redownload": "重新下載",
+ "Refresh": "重新整理",
+ "Refresh all": "全部重新整理",
+ "Refresh versions": "重新整理版本",
+ "Requesting a Microsoft sign-in code…": "正在要求 Microsoft 登入代碼……",
+ "Resolution": "解析度",
+ "Resource Error": "資源錯誤",
+ "Running (PID {pid})": "執行中(PID {pid})",
+ "Save Changes": "儲存變更",
+ "Select a loader version to continue.": "選擇載入器版本以繼續。",
+ "Select a Minecraft and loader version.": "請選擇 Minecraft 與載入器版本。",
+ "Select a profile from the list to edit its settings.": "從清單選擇設定檔以編輯其設定。",
+ "Select a profile to launch": "選擇要啟動的設定檔",
+ "Select a runtime component.": "請選擇執行環境元件。",
+ "Select a version.": "請選擇版本。",
+ "Select a version before continuing.": "請先選擇版本再繼續。",
+ "Select a version to continue:": "選擇版本以繼續:",
+ "Set both resolution values, or set both to Default.": "請同時設定寬度與高度,或將兩者都設為預設值。",
+ "Settings": "設定",
+ "Settings error": "設定錯誤",
+ "Signing in to Minecraft services": "正在登入 Minecraft 服務",
+ "Signing in to Xbox Live": "正在登入 Xbox Live",
+ "Some assets could not be downloaded. They are not required to launch the game, but missing assets may cause texture issues while you play.": "部分資源無法下載。這些資源不是啟動遊戲的必要項目,但缺少資源可能造成遊戲中的材質問題。",
+ "Some files failed to download.": "部分檔案下載失敗。",
+ "Some files failed to download. Would you like to retry downloading them?": "部分檔案下載失敗。要重試下載嗎?",
+ "Still digging!": "仍在努力開發中!",
+ "Switch to this account": "切換至此帳號",
+ "Test Error": "測試錯誤",
+ "Tests": "測試",
+ "The Client ID must belong to an Azure application configured for Microsoft device-code authentication.": "用戶端 ID 必須屬於已設定 Microsoft 裝置代碼驗證的 Azure 應用程式。",
+ "The selected loader version is invalid.": "選取的載入器版本無效。",
+ "Theme": "佈景主題",
+ "There is no profile is selected. Please create one first.": "尚未選擇設定檔,請先建立一個設定檔。",
+ "This version requires Java {version}.": "此版本需要 Java {version}。",
+ "Type": "類型",
+ "Unable to kill process": "無法終止處理程序",
+ "Unable to launch profile": "無法啟動設定檔",
+ "Unable to load the Minecraft version manifest.": "無法載入 Minecraft 版本資訊。",
+ "Unable to load the selected profile.": "無法載入選取的設定檔。",
+ "Version": "版本",
+ "Version and project information": "版本與專案資訊",
+ "Version Type": "版本類型",
+ "Version: Vanilla": "版本:原版",
+ "Version: {version}": "版本:{version}",
+ "Waiting for Microsoft sign-in approval": "正在等待 Microsoft 登入核准",
+ "Warning": "警告",
+ "If you find an issue, you can report it in the main repository. Suggestions are always welcome!": "如果發現問題,可以在主要程式碼儲存庫中回報。也歡迎提供任何建議!",
+ "An error occurred while connecting to authentication server.": "連線至驗證伺服器時發生錯誤。",
+ "An error occurred while executing Java runtime.": "執行 Java 執行環境時發生錯誤。",
+ "An error occurred while fetching third party API.": "取得第三方 API 資料時發生錯誤。",
+ "An error occurred while processing account data.": "處理帳號資料時發生錯誤。",
+ "An error occurred while processing profile.": "處理設定檔時發生錯誤。",
+ "An settings error occurred.": "發生設定錯誤。",
+ "Current platform or architecture is not supported.": "不支援目前的平台或處理器架構。",
+ "Delete all accounts": "刪除所有帳號",
+ "Delete every saved account? This cannot be undone.": "確定要刪除所有已儲存的帳號嗎?此操作無法復原。",
+ "Microsoft approved the sign-in but did not return an access token. Please sign in again.": "Microsoft 已核准登入,但未傳回存取權杖。請重新登入。",
+ "Microsoft could not refresh this account. Please sign in again.": "Microsoft 無法重新整理此帳號。請重新登入。",
+ "Microsoft did not return a refresh token. Ensure the offline_access permission is enabled, then sign in again.": "Microsoft 未傳回重新整理權杖。請確認已啟用 offline_access 權限,然後重新登入。",
+ "Microsoft did not return a sign-in code. Check the client ID or update the launcher.": "Microsoft 未傳回登入代碼。請檢查用戶端 ID 或更新啟動器。",
+ "Microsoft sign-in was rejected (HTTP {status}): {description}": "Microsoft 登入遭拒絕(HTTP {status}):{description}",
+ "Minecraft services accepted the Xbox session but did not return a game access token. Please sign in again.": "Minecraft 服務已接受 Xbox 工作階段,但未傳回遊戲存取權杖。請重新登入。",
+ "Minecraft services did not return a valid player profile. Ensure this account owns Minecraft and has created a profile.": "Minecraft 服務未傳回有效的玩家資料。請確認此帳號擁有 Minecraft 且已建立玩家資料。",
+ "Minecraft services returned an invalid ownership response. Please sign in again or try later.": "Minecraft 服務傳回無效的擁有權回應。請重新登入或稍後再試。",
+ "Modded version {version_id} added.": "已新增模組版本 {version_id}。",
+ "No compatible native library was found for this platform.": "找不到適用於此平台的原生程式庫。",
+ "No version found. (This should never happen)": "找不到任何版本。(正常情況下不應發生)",
+ "Runtime manifest is missing or corrupted.": "執行環境資訊遺失或損毀。",
+ "Settings section could not be registered.": "無法註冊設定區段。",
+ "Settings section could not be validated.": "無法驗證設定區段。",
+ "Specified account was not found.": "找不到指定的帳號。",
+ "Specified asset data section (key) not found.": "找不到指定的資源資料區段(鍵)。",
+ "Specified profile key not found. Profile may corrupted.": "找不到指定的設定檔鍵,設定檔可能已損毀。",
+ "Specified version data section (key) not found.": "找不到指定的版本資料區段(鍵)。",
+ "Specified version or type not found.": "找不到指定的版本或類型。",
+ "The authentication request or session was rejected.": "驗證要求或工作階段遭到拒絕。",
+ "The authentication service received too many requests. Wait a moment and try again.": "驗證服務收到過多要求。請稍候再試。",
+ "The authentication service returned an unexpected response. Try again later.": "驗證服務傳回非預期的回應。請稍後再試。",
+ "The login session is invalid or expired. Sign in again.": "登入工作階段無效或已過期。請重新登入。",
+ "Unable to extract native libraries.": "無法解壓縮原生程式庫。",
+ "Unable to fetch asset manifest.": "無法取得資源資訊。",
+ "Unable to fetch runtime manifest.": "無法取得執行環境資訊。",
+ "Unable to fetch third party API. Provider may no longer exist. Try updating the launcher.": "無法取得第三方 API 資料。服務提供者可能已不存在,請嘗試更新啟動器。",
+ "Unable to fetch third party API. Try updating the launcher.": "無法取得第三方 API 資料。請嘗試更新啟動器。",
+ "Unable to fetch version manifest.": "無法取得版本資訊。",
+ "Unable to find dependency.": "找不到相依項目。",
+ "Unable to load account data. The account file may be corrupted.": "無法載入帳號資料,帳號檔案可能已損毀。",
+ "Unable to load asset manifest.": "無法載入資源資訊。",
+ "Unable to load profile. Profile may corrupted": "無法載入設定檔,設定檔可能已損毀。",
+ "Unable to load version manifest.": "無法載入版本資訊。",
+ "Unable to parse Java runtime details.": "無法解析 Java 執行環境詳細資料。",
+ "Unable to resolve native libraries.": "無法解析原生程式庫。",
+ "Unable to save account data. Try again later.": "無法儲存帳號資料,請稍後再試。",
+ "Unable to save asset manifest.": "無法儲存資源資訊。",
+ "Unable to save profile. Try again later.": "無法儲存設定檔,請稍後再試。",
+ "Unable to save version manifest.": "無法儲存版本資訊。",
+ "Unable to verify Minecraft ownership because the ownership list is missing or invalid.": "無法驗證 Minecraft 擁有權,因為擁有權清單遺失或無效。",
+ "Unable to verify Minecraft ownership because the ownership response is invalid.": "無法驗證 Minecraft 擁有權,因為擁有權回應無效。",
+ "Xbox Live accepted the sign-in but did not return an authentication token. Please sign in again.": "Xbox Live 已接受登入,但未傳回驗證權杖。請重新登入。",
+ "Xbox Live did not return the account identifier. Check the account's Xbox profile and try again.": "Xbox Live 未傳回帳號識別碼。請檢查帳號的 Xbox 個人檔案後再試。",
+ "Xbox security authorization did not return a token. The account may need an Xbox profile or may be restricted.": "Xbox 安全性授權未傳回權杖。此帳號可能需要建立 Xbox 個人檔案,或可能受到限制。",
+ "Added {version_id}.": "已新增 {version_id}。",
+ "Chinese (Traditional)": "繁體中文",
+ "Installing Java ({component})": "正在安裝 Java({component})",
+ "Unable to load component list: {error}": "無法載入元件清單:{error}",
+ "Target profile {profile} is running. Are you sure you want to launch it again?": "設定檔 {profile} 正在執行。確定要再次啟動嗎?",
+ "Time out waiting for request for launch.": "等待啟動確認逾時。",
+ "Imported {count} mod(s) into {directory}.": "已將 {count} 個模組匯入 {directory}。",
+ "Unable to import mod: {error}": "無法匯入模組:{error}",
+ "Loader version": "載入器版本",
+ "Unable to load versions: {error}": "無法載入版本:{error}",
+ "Unable to load Fabric versions: {error}": "無法載入 Fabric 版本:{error}",
+ "Unable to add version: {error}": "無法新增版本:{error}",
+ "Unable to read version types: {error}": "無法讀取版本類型:{error}",
+ "Missing icon file: {path}": "找不到圖示檔案:{path}",
+ "Missing stylesheet file: {path}": "找不到樣式表檔案:{path}",
+ "Failed to load icon {path}: {error}": "無法載入圖示 {path}:{error}",
+ "Unable to apply QSS stylesheet {path}: {error}": "無法套用 QSS 樣式表 {path}:{error}",
+ "{account} — Minecraft profile {status}": "{account} — Minecraft 玩家資料{status}",
+ "available": "可用",
+ "not available": "不可用",
+ "Profile {profile_id} successfully created.": "設定檔 {profile_id} 已成功建立。",
+ "Profile {profile_id} with fabric loader successfully created.": "已成功建立含 Fabric 載入器的設定檔 {profile_id}。",
+ "Timeout while waiting for selected version.": "等待選取版本逾時。",
+ "Selected a version before creating profile.": "請先選擇版本再建立設定檔。",
+ "Timeout while waiting for selected mod loader.": "等待選取模組載入器逾時。",
+ "Loader {loader} not found.": "找不到載入器 {loader}。",
+ "Profile creation for loader {loader} is not implemented.": "尚未實作載入器 {loader} 的設定檔建立功能。",
+ "Timeout while waiting for selected mod loader version.": "等待選取模組載入器版本逾時。",
+ "Unable to create profile due to manifest fetch failure.": "因無法取得版本資訊,所以無法建立設定檔。",
+ "Unable to create profile due to version ID is missing in manifest.": "版本資訊缺少版本 ID,無法建立設定檔。",
+ "Selected a fabric loader version before creating profile.": "請先選擇 Fabric 載入器版本再建立設定檔。",
+ "Unable to create profile due to vanilla manifest fetch failure.": "因無法取得原版版本資訊,所以無法建立設定檔。",
+ "Unable to create profile due to mod loader manifest fetch failure.": "因無法取得模組載入器版本資訊,所以無法建立設定檔。",
+ "Unable to create profile. Exising loader version ID mismatch.": "載入器版本 ID 不一致,無法建立設定檔。",
+ "Unable to create profile. Inherits version is mismatched as expected.": "繼承的版本不符合預期,無法建立設定檔。",
+ "Selected a forge loader version before creating profile.": "請先選擇 Forge 載入器版本再建立設定檔。",
+ "Selected a neoforge loader version before creating profile.": "請先選擇 NeoForge 載入器版本再建立設定檔。",
+ "Timeout while waiting for selected type.": "等待選取類型逾時。",
+ "Timeout while waiting for selected loader.": "等待選取載入器逾時。",
+ "Selected loader \"{loader}\" is not implemented": "尚未實作選取的載入器「{loader}」",
+ "Failed to fetch version manifest: {error}": "無法取得版本資訊:{error}",
+ "An unexpected error occurred while fetching version manifest: {error}": "取得版本資訊時發生非預期錯誤:{error}",
+ "Unable to launch profile {profile}: {error}": "無法啟動設定檔 {profile}:{error}",
+ "To make all page apply new language. Is recommend to restart the launcher after change it.": "為了讓所有頁面套用語言更新,建議在修改後重新啟動啟動器。"
+}
diff --git a/window.py b/window.py
index 40de299..43e795e 100644
--- a/window.py
+++ b/window.py
@@ -116,13 +116,17 @@ class LauncherMainWindow(QMainWindow):
"Suggestions are always welcome!")
self.icon_label = QLabel()
self.icon_label.setObjectName("icon_label")
- self.dev_info_box = QLabel(content)
+ self.dev_info_box = QLabel(self.app.tr_text(content))
self.dev_info_box.setObjectName("dev_info_box")
- self.dev_info_2 = QLabel(message)
+ self.dev_info_2 = QLabel(self.app.tr_text(message))
self.dev_info_2.setObjectName("dev_info_message")
- self.version_label = QLabel("Current version: {}".format(self.app.launcher_version))
+ self.version_label = QLabel(self.app.tr_text(
+ "Current version: {version}", version=self.app.launcher_version
+ ))
self.version_label.setObjectName("version_label")
- self.repo_url_label = QLabel(f"Main repo")
+ self.repo_url_label = QLabel(
+ f'{self.app.tr_text("Main repo")}'
+ )
self.repo_url_label.setObjectName("repo_url_label")
self.repo_url_label.setOpenExternalLinks(True)
@@ -130,7 +134,7 @@ class LauncherMainWindow(QMainWindow):
image = QPixmap(Path(self.app.resources_dir, "pictures", "in_progress.png"))
self.icon_label.setPixmap(image.scaled(300, 300))
else:
- self.icon_label.setText("Ouch. The icon is missing.")
+ self.icon_label.setText(self.app.tr_text("Ouch. The icon is missing."))
# Dialog
self.download_dialog = ProgressDialog(self)
@@ -146,12 +150,13 @@ class LauncherMainWindow(QMainWindow):
self.home_layout.addWidget(self.repo_url_label, alignment=Qt.AlignmentFlag.AlignHCenter)
self.home_layout.addStretch()
- # test page
- self.test_page = TestPage(
- app=self.app,
- parent=self,
- widget_manager=self.app.widget_manager
- )
+ if self.app.debug:
+ # test page
+ self.test_page = TestPage(
+ app=self.app,
+ parent=self,
+ widget_manager=self.app.widget_manager
+ )
self.launch_profile_page = LaunchProfilePage(
app=self.app,
@@ -172,7 +177,8 @@ class LauncherMainWindow(QMainWindow):
# Add page
self.central.addWidget(self.home_page)
- self.central.addWidget(self.test_page)
+ if self.app.debug:
+ self.central.addWidget(self.test_page)
self.central.addWidget(self.account_page)
self.central.addWidget(self.settings_page)
self.central.addWidget(self.create_profile_page)
@@ -190,18 +196,31 @@ class LauncherMainWindow(QMainWindow):
# Toolbar
# Test Window btn
- self.open_test_page = self.toolbar.add_button(" Tests", icon=self.app.get_icon(Path(self.app.icon_dir, "debug.png")))
- self.open_test_page.setObjectName("open_test_page")
+ if self.app.debug:
+ self.open_test_page = self.toolbar.add_button(" Tests")
+ self.app.set_reloadable_toolbutton_icon(
+ self.open_test_page,
+ Path(self.app.icon_dir, "debug.png"),
+ )
+ self.open_test_page.setObjectName("open_test_page")
# Launch profile btn
self.open_launch_profile_page = self.toolbar.add_button(" Launch Profile",
icon=self.app.get_icon(Path(self.app.icon_dir, "profile_default.png"), True))
# Home btn
- self.home_button = self.toolbar.add_button(" Home", icon=self.app.get_icon(Path(self.app.icon_dir, "home.png")))
+ self.home_button = self.toolbar.add_button(" Home")
+ self.app.set_reloadable_toolbutton_icon(
+ self.home_button,
+ Path(self.app.icon_dir, "home.png"),
+ )
# Settings btn (will be moved to the bottom of the toolbar)
- self.open_settings_page = self.toolbar.add_button(" Settings", icon=self.app.get_icon(Path(self.app.icon_dir, "settings.png")))
+ self.open_settings_page = self.toolbar.add_button(" Settings")
+ self.app.set_reloadable_toolbutton_icon(
+ self.open_settings_page,
+ Path(self.app.icon_dir, "settings.png"),
+ )
# Account btn (will be moved to the bottom (or last one item) of the toolbar
self.open_account_page = self.toolbar.add_button(" Accounts", icon=self.app.get_icon(Path(self.app.icon_dir, "head.png"), True))
@@ -209,8 +228,11 @@ class LauncherMainWindow(QMainWindow):
# current account's avatar
# Profile btn
- self.open_create_profile_page = self.toolbar.add_button("Create Profile",
- icon=self.app.get_icon(Path(self.app.icon_dir, "add.png")))
+ self.open_create_profile_page = self.toolbar.add_button("Create Profile")
+ self.app.set_reloadable_toolbutton_icon(
+ self.open_create_profile_page,
+ Path(self.app.icon_dir, "add.png"),
+ )
# manage profile
self.open_manage_profile_page = self.toolbar.add_button(
@@ -220,7 +242,8 @@ class LauncherMainWindow(QMainWindow):
# Bind tool buttons
self.toolbar.addWidget(self.home_button)
- self.toolbar.addWidget(self.open_test_page)
+ if self.app.debug:
+ self.toolbar.addWidget(self.open_test_page)
self.toolbar.addWidget(self.open_create_profile_page)
self.toolbar.addWidget(self.open_launch_profile_page)
self.toolbar.addWidget(self.open_manage_profile_page)
@@ -238,9 +261,10 @@ class LauncherMainWindow(QMainWindow):
lambda: self.set_current_page(self.home_page, self.home_button)
)
- self.open_test_page.clicked.connect(
- lambda: self.set_current_page(self.test_page, self.open_test_page)
- )
+ if self.app.debug:
+ self.open_test_page.clicked.connect(
+ lambda: self.set_current_page(self.test_page, self.open_test_page)
+ )
self.open_launch_profile_page.clicked.connect(
lambda: self.set_current_page(self.launch_profile_page, self.open_launch_profile_page)
@@ -266,6 +290,31 @@ class LauncherMainWindow(QMainWindow):
self.bind_event()
self.set_current_page(self.home_page, self.home_button)
+ self.app.i18n.language_changed.connect(self.retranslate_ui)
+ self.retranslate_ui()
+
+ def retranslate_ui(self, *_args):
+ tr = self.app.tr_text
+ self.dev_info_box.setText(tr("Still digging!"))
+ self.dev_info_2.setText(tr(
+ "If you find an issue, you can report it in the main repository. "
+ "Suggestions are always welcome!"
+ ))
+ self.version_label.setText(
+ tr("Current version: {version}", version=self.app.launcher_version)
+ )
+ self.repo_url_label.setText(
+ f'{tr("Main repo")}'
+ )
+ self.home_button.setText(" " + tr("Home"))
+ self.open_launch_profile_page.setText(" " + tr("Launch Profile"))
+ self.open_settings_page.setText(" " + tr("Settings"))
+ self.open_account_page.setText(" " + tr("Accounts"))
+ self.open_create_profile_page.setText(" " + tr("Create Profile"))
+ self.open_manage_profile_page.setText(" " + tr("Manage Profiles"))
+ if self.app.debug:
+ self.open_test_page.setText(" " + tr("Tests"))
+ self.toolbar.update_all()
@Slot(object)
def ask_request(self, context: AskForRequest):
@@ -309,8 +358,8 @@ class LauncherMainWindow(QMainWindow):
@Slot(dict)
def show_and_print_error(self, context: dict):
error = context.get("error", None)
- title = context.get("title", "Error")
- message = context.get("message", "No content")
+ title = context.get("title", self.app.tr_text("Error"))
+ message = context.get("message", self.app.tr_text("No content"))
if isinstance(error, list):
self.app.logger.error("".join(error))
@@ -335,15 +384,15 @@ class LauncherMainWindow(QMainWindow):
@Slot(str)
def show_error(self, message):
- messagebox.error(self, "Error", message=message)
+ messagebox.error(self, self.app.tr_text("Error"), message=message)
@Slot(str)
def show_warning(self, widget):
- messagebox.warning(self, "Warning", message=widget)
+ messagebox.warning(self, self.app.tr_text("Warning"), message=widget)
@Slot(str)
def show_info(self, widget):
- messagebox.info(self, "Info", message=widget)
+ messagebox.info(self, self.app.tr_text("Info"), message=widget)
def set_current_page(self, page: QWidget, related_button: QToolButton):
self.central.setCurrentWidget(page)