diff --git a/gui/translation_tab.py b/gui/translation_tab.py index 26588ed..0b2edd5 100644 --- a/gui/translation_tab.py +++ b/gui/translation_tab.py @@ -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,63 +1681,25 @@ 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 - - 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], + # 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 = [ + [ + display_name, + list(extensions), + _lazy_module_handler(module_name, handler_name), ] - - for module in self.modules: - extensions = ", ".join(module[1]) - self.module_combo.addItem(f"{module[0]} ({extensions})") - 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]] + for display_name, extensions, module_name, handler_name + in TRANSLATION_MODULE_SPECS + ] + + for module in self.modules: + extensions = ", ".join(module[1]) + self.module_combo.addItem(f"{module[0]} ({extensions})") + if self.module_combo.count(): self._on_module_changed(self.module_combo.currentText()) def _on_module_changed(self, text: str): diff --git a/tests/test_translation_engine_dropdown.py b/tests/test_translation_engine_dropdown.py new file mode 100644 index 0000000..dd4b9eb --- /dev/null +++ b/tests/test_translation_engine_dropdown.py @@ -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()