diff --git a/gui/main.py b/gui/main.py index 331cb2b..953d180 100644 --- a/gui/main.py +++ b/gui/main.py @@ -1180,6 +1180,10 @@ class DazedMTLGUI(QMainWindow): config = self.config_tab.get_config() font_scale = config.get("font_scale", 1.0) self.apply_font_scaling(font_scale) + for tab in (self.translation_tab, self.workflow_tab, self.wolf_workflow_tab): + refresh_mode = getattr(tab, "refresh_default_translation_mode", None) + if callable(refresh_mode): + refresh_mode() except Exception as e: print(f"Warning: Could not apply configuration changes: {e}") diff --git a/gui/translation_tab.py b/gui/translation_tab.py index a8e65e7..6cd5360 100644 --- a/gui/translation_tab.py +++ b/gui/translation_tab.py @@ -22,7 +22,7 @@ import re from importlib import import_module from colorama import Fore from tqdm import tqdm -from dotenv import load_dotenv +from dotenv import dotenv_values, load_dotenv from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QGroupBox, QTextEdit, QMessageBox, QListWidget, QListWidgetItem, @@ -34,7 +34,7 @@ from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, QMutex, QProcess, QEve from PyQt5.QtGui import QFont, QColor, QBrush from gui.log_viewer import LogViewer from gui import qt_icons -from util.paths import APP_NAME, ORG_NAME +from util.paths import APP_NAME, ORG_NAME, PROJECT_ROOT def _strip_ansi(text): @@ -75,6 +75,22 @@ BATCH_COLLECT_LIVE_CHARGE_NOTE = ( "only after you confirm the estimate." ) +_CONFIG_UNSET = object() + + +def default_translation_mode(model=_CONFIG_UNSET, api_url=_CONFIG_UNSET) -> str: + """Choose Batch only when the configured model supports native Claude batches.""" + if model is _CONFIG_UNSET or api_url is _CONFIG_UNSET: + env = dotenv_values(PROJECT_ROOT / ".env") if (PROJECT_ROOT / ".env").exists() else {} + if model is _CONFIG_UNSET: + model = env.get("model", os.getenv("model", "")) + if api_url is _CONFIG_UNSET: + api_url = env.get("api", os.getenv("api", "")) + + from util.translation import isClaudeNative + + return BATCH_MODE_LABEL if isClaudeNative(str(model or ""), api_url) else "Translate" + TRANSLATION_MODULE_SPECS = ( ("RPG Maker MV/MZ", (".json",), "modules.rpgmakermvmz", "handleMVMZ"), @@ -940,6 +956,8 @@ class TranslationTab(QWidget): self._batch_ui_phase = None self._batch_consume_started = False self._batch_tab_index = -1 + self._mode_user_selected = False + self._last_default_translation_mode = None self.setup_ui() self.setup_module_list() @@ -1627,6 +1645,7 @@ class TranslationTab(QWidget): self.mode_combo.addItem(BATCH_MODE_LABEL) self.mode_combo.setFixedWidth(300) self.mode_combo.currentTextChanged.connect(self._on_mode_changed) + self.mode_combo.activated.connect(self._mark_mode_user_selected) trans_form.addRow(mode_label, self.mode_combo) self.batch_mode_note = QLabel( @@ -1662,7 +1681,7 @@ class TranslationTab(QWidget): button_layout.addStretch() layout.addLayout(button_layout) - self.mode_combo.setCurrentIndex(self.mode_combo.findText(BATCH_MODE_LABEL)) + self.refresh_default_translation_mode(force=True) left_widget.setLayout(layout) # Right side - translation history log viewer @@ -1748,8 +1767,8 @@ class TranslationTab(QWidget): if index >= 0: self.mode_combo.setCurrentIndex(index) else: - batch_idx = self.mode_combo.findText(BATCH_MODE_LABEL) - self.mode_combo.setCurrentIndex(batch_idx if batch_idx >= 0 else 0) + default_idx = self.mode_combo.findText(default_translation_mode()) + self.mode_combo.setCurrentIndex(default_idx if default_idx >= 0 else 0) # Refresh file list to show only files matching the selected module's extensions self.refresh_file_lists() @@ -1843,6 +1862,20 @@ class TranslationTab(QWidget): elif mode_text == "Parse Speakers": self.translate_button.setText("Parse Speakers") + def _mark_mode_user_selected(self, _index: int): + self._mode_user_selected = True + + def refresh_default_translation_mode(self, force=False): + """Refresh the provider-aware default without overriding an active choice.""" + default_mode = default_translation_mode() + if not force and default_mode == self._last_default_translation_mode: + return + self._last_default_translation_mode = default_mode + if default_mode == "Translate" or force or not self._mode_user_selected: + index = self.mode_combo.findText(default_mode) + if index >= 0: + self.mode_combo.setCurrentIndex(index) + def _switch_progress_tab(self, index): """Switch Batch/Files views; index 0 = batch overview, 1 = per-file list.""" self._batch_tab_index = index if self.progress_tab_row.isVisible() else -1 diff --git a/gui/wolf_workflow_tab.py b/gui/wolf_workflow_tab.py index 9b8d369..657616d 100644 --- a/gui/wolf_workflow_tab.py +++ b/gui/wolf_workflow_tab.py @@ -83,6 +83,7 @@ from gui.translation_tab import ( BATCH_COLLECT_LIVE_CHARGE_NOTE, BATCH_MODE_BENEFIT_NOTE, BATCH_MODE_LABEL, + default_translation_mode, ) from gui.workflow_tab import ( _GAMEUPDATE_COPY_SKIP_NAMES, @@ -186,6 +187,9 @@ class WolfWorkflowTab(QWidget): self._pending_import_signature: tuple[str, ...] | None = None self._syncing_file_checks: bool = False self._gameupdate_path: str = "" + self._tl_mode_user_selected = False + self._last_default_translation_mode = None + self._tl_mode_combos = [] self._init_ui() @@ -2182,17 +2186,28 @@ class WolfWorkflowTab(QWidget): lbl = QLabel("Translation mode:") lbl.setStyleSheet("color:#cccccc;font-size:12px;font-weight:bold;background:transparent;") self._tl_mode_combo = QComboBox() + self._tl_mode_combos.append(self._tl_mode_combo) self._tl_mode_combo.addItem(_TL_NORMAL_LABEL) self._tl_mode_combo.addItem(BATCH_MODE_LABEL) self._tl_mode_combo.setFixedWidth(220) self._tl_mode_combo.setToolTip( "Normal translates live; Batch uses the Anthropic Batches API (~50% cheaper, Claude only)." ) - saved = self._setting("tl_mode", BATCH_MODE_LABEL) or BATCH_MODE_LABEL + default_mode = default_translation_mode() + self._last_default_translation_mode = default_mode + automatic = BATCH_MODE_LABEL if default_mode == BATCH_MODE_LABEL else _TL_NORMAL_LABEL + saved = self._setting("tl_mode", None) + self._tl_mode_user_selected = saved in (_TL_NORMAL_LABEL, BATCH_MODE_LABEL) + if saved not in (_TL_NORMAL_LABEL, BATCH_MODE_LABEL): + saved = automatic + elif saved == BATCH_MODE_LABEL and automatic != BATCH_MODE_LABEL: + saved = automatic + self._tl_mode_user_selected = False idx = self._tl_mode_combo.findText(str(saved)) if idx >= 0: self._tl_mode_combo.setCurrentIndex(idx) self._tl_mode_combo.currentTextChanged.connect(self._on_tl_mode_changed) + self._tl_mode_combo.activated.connect(self._mark_tl_mode_selected) row.addWidget(lbl) row.addWidget(self._tl_mode_combo) row.addStretch() @@ -2202,21 +2217,40 @@ class WolfWorkflowTab(QWidget): self._batch_note.setWordWrap(True) self._batch_note.setStyleSheet("color:#8fbc8f;font-size:11px;background:transparent;") layout.addWidget(self._batch_note) - self._on_tl_mode_changed(self._tl_mode_combo.currentText()) + self._on_tl_mode_changed(self._tl_mode_combo.currentText(), save=False) - def _on_tl_mode_changed(self, mode_text: str): + def _on_tl_mode_changed(self, mode_text: str, save=True): is_batch = mode_text == BATCH_MODE_LABEL if hasattr(self, "_batch_note"): self._batch_note.setVisible(is_batch) - self._save_setting("tl_mode", mode_text) + if save: + self._save_setting("tl_mode", mode_text) def _workflow_mode_text(self) -> str: """Map the selector to the Translation tab's own mode label.""" - saved = self._setting("tl_mode", BATCH_MODE_LABEL) or BATCH_MODE_LABEL - if saved == BATCH_MODE_LABEL: + automatic = BATCH_MODE_LABEL if default_translation_mode() == BATCH_MODE_LABEL else _TL_NORMAL_LABEL + selected = self._setting("tl_mode", automatic) or automatic + if selected == BATCH_MODE_LABEL and automatic == BATCH_MODE_LABEL: return BATCH_MODE_LABEL return "Translate" + def _mark_tl_mode_selected(self, _index: int): + self._tl_mode_user_selected = True + + def refresh_default_translation_mode(self, force=False): + """Apply the provider-aware mode after configuration changes.""" + default_mode = default_translation_mode() + if not force and default_mode == self._last_default_translation_mode: + return + self._last_default_translation_mode = default_mode + mode = BATCH_MODE_LABEL if default_mode == BATCH_MODE_LABEL else _TL_NORMAL_LABEL + if default_mode == "Translate" or force or not self._tl_mode_user_selected: + for combo in self._tl_mode_combos: + index = combo.findText(mode) + if index >= 0: + combo.setCurrentIndex(index) + self._save_setting("tl_mode", mode) + def _navigate_to_translation( self, only: str | None = None, diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index ed6d042..6bba9e0 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -63,6 +63,7 @@ from gui.translation_tab import ( BATCH_MODE_LABEL, BATCH_MODE_BENEFIT_NOTE, BATCH_COLLECT_LIVE_CHARGE_NOTE, + default_translation_mode, ) WORKFLOW_TL_NORMAL_LABEL = "Normal Translate" @@ -778,6 +779,8 @@ class WorkflowTab(QWidget): self._last_import_signature: tuple[str, ...] | None = None self._pending_import_signature: tuple[str, ...] | None = None self._release_zip_btn: QPushButton | None = None + self._tl_mode_user_selected = False + self._last_default_translation_mode = None self._init_ui() @@ -1654,6 +1657,7 @@ class WorkflowTab(QWidget): "Batch uses the Anthropic Batches API (50% off, Claude only)." ) self._tl_mode_combo.currentTextChanged.connect(self._on_workflow_tl_mode_changed) + self._tl_mode_combo.activated.connect(self._mark_workflow_tl_mode_selected) mode_row.addWidget(mode_lbl) mode_row.addWidget(self._tl_mode_combo) mode_row.addStretch() @@ -1672,9 +1676,7 @@ class WorkflowTab(QWidget): mode_inner.addWidget(self._batch_mode_warning) layout.addWidget(mode_box) - batch_idx = self._tl_mode_combo.findText(BATCH_MODE_LABEL) - if batch_idx >= 0: - self._tl_mode_combo.setCurrentIndex(batch_idx) + self.refresh_default_translation_mode(force=True) self._on_workflow_tl_mode_changed(self._tl_mode_combo.currentText()) def _on_workflow_tl_mode_changed(self, mode_text: str): @@ -1694,6 +1696,23 @@ class WorkflowTab(QWidget): combo = getattr(self, "_tl_mode_combo", None) return combo is not None and combo.currentText() == BATCH_MODE_LABEL + def _mark_workflow_tl_mode_selected(self, _index: int): + self._tl_mode_user_selected = True + + def refresh_default_translation_mode(self, force=False): + """Refresh the workflow mode when the configured provider changes.""" + default_mode = default_translation_mode() + if not force and default_mode == self._last_default_translation_mode: + return + self._last_default_translation_mode = default_mode + workflow_mode = ( + BATCH_MODE_LABEL if default_mode == BATCH_MODE_LABEL else WORKFLOW_TL_NORMAL_LABEL + ) + if default_mode == "Translate" or force or not self._tl_mode_user_selected: + index = self._tl_mode_combo.findText(workflow_mode) + if index >= 0: + self._tl_mode_combo.setCurrentIndex(index) + def _workflow_mode_text(self) -> str: return BATCH_MODE_LABEL if self._workflow_batch_mode() else "Translate" @@ -1853,7 +1872,9 @@ class WorkflowTab(QWidget): self._add_step_header(layout, "Step 4 — TL Phase 2", 4) - self._step5_mode_hint = QLabel(f"Translation mode: {BATCH_MODE_LABEL}") + initial_mode = getattr(self, "_tl_mode_combo", None) + initial_mode_text = initial_mode.currentText() if initial_mode is not None else WORKFLOW_TL_NORMAL_LABEL + self._step5_mode_hint = QLabel(f"Translation mode: {initial_mode_text}") self._step5_mode_hint.setStyleSheet("color:#8fbc8f;font-size:12px;margin-bottom:4px;") layout.addWidget(self._step5_mode_hint) if hasattr(self, "_tl_mode_combo"): diff --git a/modules/rpgmakermvmz.py b/modules/rpgmakermvmz.py index 8206026..e967db2 100644 --- a/modules/rpgmakermvmz.py +++ b/modules/rpgmakermvmz.py @@ -148,13 +148,13 @@ TLSYSTEMSWITCHES = False JOIN408 = False # Dialogue / Scroll / Choices (Main Codes) -CODE101 = False -CODE401 = False -CODE405 = False -CODE102 = False +CODE101 = True +CODE401 = True +CODE405 = True +CODE102 = True # Optional -CODE408 = False +CODE408 = True # Variables CODE122 = False diff --git a/tests/test_translation_engine_dropdown.py b/tests/test_translation_engine_dropdown.py index dd4b9eb..bf9f887 100644 --- a/tests/test_translation_engine_dropdown.py +++ b/tests/test_translation_engine_dropdown.py @@ -11,7 +11,12 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") try: from PyQt5.QtWidgets import QApplication - from gui.translation_tab import TRANSLATION_MODULE_SPECS, TranslationTab + from gui.translation_tab import ( + BATCH_MODE_LABEL, + TRANSLATION_MODULE_SPECS, + TranslationTab, + default_translation_mode, + ) _HAS_QT = True except Exception: # pragma: no cover - PyQt5 not installed @@ -53,6 +58,36 @@ class TranslationEngineDropdownTests(unittest.TestCase): self.assertEqual(actual, expected) self.assertTrue(all(callable(module[2]) for module in tab.modules)) + def test_normal_translate_is_default_for_non_claude_models(self) -> None: + self.assertEqual( + default_translation_mode("gpt-5.2", "https://api.openai.com/v1"), + "Translate", + ) + + def test_batch_translate_is_default_for_native_claude(self) -> None: + self.assertEqual( + default_translation_mode("claude-sonnet-4-6", "https://api.anthropic.com/v1"), + BATCH_MODE_LABEL, + ) + + def test_claude_through_custom_provider_defaults_to_normal(self) -> None: + self.assertEqual( + default_translation_mode("claude-sonnet-4-6", "https://openrouter.ai/api/v1"), + "Translate", + ) + + def test_translation_tab_applies_detected_default(self) -> None: + with patch("gui.translation_tab.default_translation_mode", return_value="Translate"): + normal_tab = TranslationTab() + self.assertEqual(normal_tab.mode_combo.currentText(), "Translate") + + with patch( + "gui.translation_tab.default_translation_mode", + return_value=BATCH_MODE_LABEL, + ): + claude_tab = TranslationTab() + self.assertEqual(claude_tab.mode_combo.currentText(), BATCH_MODE_LABEL) + if __name__ == "__main__": unittest.main() diff --git a/util/translation.py b/util/translation.py index 052f98d..f7fffcf 100644 --- a/util/translation.py +++ b/util/translation.py @@ -102,7 +102,7 @@ def isClaudeModel(model): return bool(model) and any(x in model.lower() for x in ("claude", "sonnet", "haiku", "opus")) -def isClaudeNative(model): +def isClaudeNative(model, api_url=None): """True when this model routes to the native Anthropic SDK. Mirrors the routing check in translateText: the model must look like Claude @@ -110,7 +110,7 @@ def isClaudeNative(model): other custom URL (e.g. DeepSeek, OpenAI proxy) uses the OpenAI-compatible path even for Claude-named models. """ - live_api = os.getenv("api", "").strip() + live_api = os.getenv("api", "").strip() if api_url is None else str(api_url).strip() return isClaudeModel(model) and (not live_api or "anthropic" in live_api.lower())