fix(gui): populate engine dropdown without environment config

- Defer engine imports until translation starts
- Add regression coverage for source downloads without environment settings
This commit is contained in:
DazedAnon 2026-07-24 12:12:10 -05:00
parent 3b13575a74
commit a371223189
2 changed files with 110 additions and 56 deletions

View file

@ -19,6 +19,7 @@ import traceback
import signal import signal
import multiprocessing import multiprocessing
import re import re
from importlib import import_module
from colorama import Fore from colorama import Fore
from tqdm import tqdm from tqdm import tqdm
from dotenv import load_dotenv from dotenv import load_dotenv
@ -75,6 +76,39 @@ BATCH_COLLECT_LIVE_CHARGE_NOTE = (
) )
TRANSLATION_MODULE_SPECS = (
("RPG Maker MV/MZ", (".json",), "modules.rpgmakermvmz", "handleMVMZ"),
("CSV", (".csv",), "modules.csv", "handleCSV"),
("Tyrano", (".ks",), "modules.tyrano", "handleTyrano"),
("Kirikiri", (".ks",), "modules.kirikiri", "handleKirikiri"),
("JSON", (".json",), "modules.json", "handleJSON"),
("Lune", (".l",), "modules.lune", "handleLune"),
("Yuris", (".json",), "modules.yuris", "handleYuris"),
("NScript", (".nscript",), "modules.nscript", "handleOnscripter"),
("Wolf RPG (WolfDawn)", (".json",), "modules.wolfdawn", "handleWolfDawn"),
("Wolf RPG", (".json",), "modules.wolf", "handleWOLF"),
("Wolf RPG 2", (".txt",), "modules.wolf2", "handleWOLF2"),
("Regex", (".txt", ".json", ".script", ".csv"), "modules.regex", "handleRegex"),
("Text", (".txt", ".srt"), "modules.text", "handleText"),
("RenPy", (".rpy",), "modules.renpy", "handleRenpy"),
("Unity", (".unity",), "modules.unity", "handleUnity"),
("Images", (".png", ".jpg", ".jpeg"), "modules.images", "handleImages"),
("RPG Maker Plugin", (".js",), "modules.rpgmakerplugin", "handlePlugin"),
("Aquedi4 Prepared JSON", (".json",), "modules.aquedi4", "handleAquedi4"),
("SRPG Studio", (".json",), "modules.srpg", "handleSRPG"),
)
def _lazy_module_handler(module_name, handler_name):
"""Return a handler that imports its engine only when translation starts."""
def run(*args, **kwargs):
module = import_module(module_name)
return getattr(module, handler_name)(*args, **kwargs)
return run
class _ShimLabel: class _ShimLabel:
"""Plain stand-in for QLabel used by Files-tab row helpers (no real Qt widget).""" """Plain stand-in for QLabel used by Files-tab row helpers (no real Qt widget)."""
@ -1647,63 +1681,25 @@ class TranslationTab(QWidget):
def setup_module_list(self): def setup_module_list(self):
"""Set up the module selection list.""" """Set up the module selection list."""
# Import modules to get the list # Engine modules read translation settings during import. A downloaded
try: # source archive intentionally has no .env yet, so importing every
sys.path.append(str(self.project_root)) # engine here used to abort discovery and leave only the fallback item.
from modules.rpgmakermvmz import handleMVMZ # Keep the UI registry independent and defer imports until a handler is
from modules.csv import handleCSV # actually used.
from modules.tyrano import handleTyrano self.modules = [
from modules.kirikiri import handleKirikiri [
from modules.json import handleJSON display_name,
from modules.lune import handleLune list(extensions),
from modules.yuris import handleYuris _lazy_module_handler(module_name, handler_name),
from modules.nscript import handleOnscripter
from modules.wolf import handleWOLF
from modules.wolf2 import handleWOLF2
from modules.wolfdawn import handleWolfDawn
from modules.regex import handleRegex
from modules.text import handleText
from modules.renpy import handleRenpy
from modules.unity import handleUnity
from modules.images import handleImages
from modules.rpgmakerplugin import handlePlugin
from modules.aquedi4 import handleAquedi4
from modules.srpg import handleSRPG
self.modules = [
["RPG Maker MV/MZ", [".json"], handleMVMZ],
["CSV", [".csv"], handleCSV],
["Tyrano", [".ks"], handleTyrano],
["Kirikiri", [".ks"], handleKirikiri],
["JSON", [".json"], handleJSON],
["Lune", [".l"], handleLune],
["Yuris", [".json"], handleYuris],
["NScript", [".nscript"], handleOnscripter],
["Wolf RPG (WolfDawn)", [".json"], handleWolfDawn],
["Wolf RPG", [".json"], handleWOLF],
["Wolf RPG 2", [".txt"], handleWOLF2],
["Regex", [".txt", ".json", ".script", ".csv"], handleRegex],
["Text", [".txt", ".srt"], handleText],
["RenPy", [".rpy"], handleRenpy],
["Unity", [".unity"], handleUnity],
["Images", [".png", ".jpg", ".jpeg"], handleImages],
["RPG Maker Plugin", [".js"], handlePlugin],
["Aquedi4 Prepared JSON", [".json"], handleAquedi4],
["SRPG Studio", [".json"], handleSRPG],
] ]
for display_name, extensions, module_name, handler_name
in TRANSLATION_MODULE_SPECS
]
for module in self.modules: for module in self.modules:
extensions = ", ".join(module[1]) extensions = ", ".join(module[1])
self.module_combo.addItem(f"{module[0]} ({extensions})") self.module_combo.addItem(f"{module[0]} ({extensions})")
if self.module_combo.count(): if self.module_combo.count():
self._on_module_changed(self.module_combo.currentText())
except Exception as e:
# Store error for later logging since log_display might not exist yet
self.module_load_error = f"Warning: Could not load modules: {str(e)}"
# Add a default option
self.module_combo.addItem("RPG Maker MV/MZ (.json)")
self.modules = [["RPG Maker MV/MZ", [".json"], None]]
self._on_module_changed(self.module_combo.currentText()) self._on_module_changed(self.module_combo.currentText())
def _on_module_changed(self, text: str): def _on_module_changed(self, text: str):

View file

@ -0,0 +1,58 @@
"""Regression tests for Translation-tab engine discovery."""
from __future__ import annotations
import os
import unittest
from unittest.mock import patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
try:
from PyQt5.QtWidgets import QApplication
from gui.translation_tab import TRANSLATION_MODULE_SPECS, TranslationTab
_HAS_QT = True
except Exception: # pragma: no cover - PyQt5 not installed
_HAS_QT = False
@unittest.skipUnless(_HAS_QT, "PyQt5 not available")
class TranslationEngineDropdownTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_all_engines_are_listed_without_environment_configuration(self) -> None:
required_settings = (
"model",
"language",
"timeout",
"width",
"listWidth",
"noteWidth",
)
without_settings = {
key: value
for key, value in os.environ.items()
if key not in required_settings
}
with patch.dict(os.environ, without_settings, clear=True):
tab = TranslationTab()
expected = [
f"{name} ({', '.join(extensions)})"
for name, extensions, _module, _handler in TRANSLATION_MODULE_SPECS
]
actual = [
tab.module_combo.itemText(index)
for index in range(tab.module_combo.count())
]
self.assertEqual(actual, expected)
self.assertTrue(all(callable(module[2]) for module in tab.modules))
if __name__ == "__main__":
unittest.main()