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 multiprocessing
import re
from importlib import import_module
from colorama import Fore
from tqdm import tqdm
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:
"""Plain stand-in for QLabel used by Files-tab row helpers (no real Qt widget)."""
@ -1647,49 +1681,19 @@ class TranslationTab(QWidget):
def setup_module_list(self):
"""Set up the module selection list."""
# Import modules to get the list
try:
sys.path.append(str(self.project_root))
from modules.rpgmakermvmz import handleMVMZ
from modules.csv import handleCSV
from modules.tyrano import handleTyrano
from modules.kirikiri import handleKirikiri
from modules.json import handleJSON
from modules.lune import handleLune
from modules.yuris import handleYuris
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
# Engine modules read translation settings during import. A downloaded
# source archive intentionally has no .env yet, so importing every
# engine here used to abort discovery and leave only the fallback item.
# Keep the UI registry independent and defer imports until a handler is
# actually used.
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],
[
display_name,
list(extensions),
_lazy_module_handler(module_name, handler_name),
]
for display_name, extensions, module_name, handler_name
in TRANSLATION_MODULE_SPECS
]
for module in self.modules:
@ -1698,14 +1702,6 @@ class TranslationTab(QWidget):
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())
def _on_module_changed(self, text: str):
lowered = text.lower()
if "wolf" in lowered and "wolf rpg 2" not in lowered:

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()