""" RPGMaker Workflow Tab - Automation hub for the full translation pipeline. Provides a guided, step-by-step interface: Step 0 – Select game project folder and import data files into files/ Step 1 – (Optional) Pre-process game files Step 2 – Auto-detect speaker format and apply to module settings Step 3 – Build glossary: parse speakers, then enrich with AI prompt Step 4 – Translation: Phase 0 (DB), Phase 1 (dialogue), Phase 1b (111 cache) Step 5 – Translation Phase 2 (risky codes) Step 6 – Translate visible strings in js/plugins.js (or Ace scripts) Step 7 – Export translated/ back to the game folder Step 8 – Install TL Inspector for playtesting and live in-game edits (MV/MZ) """ from __future__ import annotations import json import os import sys import threading from pathlib import Path import jsbeautifier from PyQt5.QtCore import Qt, QEvent, QSettings, QThread, QTimer, pyqtSignal from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( QApplication, QCheckBox, QComboBox, QFileDialog, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMessageBox, QPushButton, QScrollArea, QSizePolicy, QSpinBox, QSplitter, QStyle, QTabWidget, QTextEdit, QVBoxLayout, QWidget, QAbstractItemView, ) from gui.translation_tab import BATCH_MODE_LABEL, BATCH_COLLECT_LIVE_CHARGE_NOTE # --------------------------------------------------------------------------- # Phase profiles applied to rpgmakermvmz.py before each translation run # --------------------------------------------------------------------------- # Core database files — translated first (names/descriptions) _DB_FILES = { "Actors.json", "Armors.json", "Classes.json", "Enemies.json", "Items.json", "MapInfos.json", "Skills.json", "States.json", "System.json", "Weapons.json", } # Event files — translated in phases 1 / 1b / 2 _EVENT_FILES_EXACT = {"CommonEvents.json", "Troops.json"} # Any Map????.json is also an event file (matched by prefix below) PHASE0_CONFIG = { # All event codes OFF — DB files use top-level name/description fields "CODE101": False, "CODE401": False, "CODE405": False, "CODE102": False, "CODE408": False, "CODE111": False, "CODE122": False, "CODE357": False, "CODE355655": False, "CODE657": False, "CODE356": False, "CODE320": False, "CODE324": False, "CODE325": False, "CODE108": False, } PHASE1_CONFIG = { # Safe dialogue / choices "CODE101": True, "CODE401": True, "CODE405": True, "CODE102": True, "CODE408": True, # Risky codes OFF "CODE122": False, "CODE355655": False, "CODE357": False, "CODE657": False, "CODE356": False, "CODE320": False, "CODE324": False, "CODE325": False, "CODE111": False, "CODE108": False, } PHASE1B_CONFIG = { # Dialogue OFF (handled by Phase 1) "CODE101": False, "CODE401": False, "CODE405": False, "CODE102": False, "CODE408": False, # Only 111 ON — build the var-translation cache from string comparisons "CODE111": True, "CODE122": False, "CODE357": False, "CODE355655": False, "CODE657": False, "CODE356": False, "CODE320": False, "CODE324": False, "CODE325": False, "CODE108": False, } PHASE2_CONFIG = { # Dialogue OFF (already handled by Phase 1) "CODE101": False, "CODE401": False, "CODE405": False, "CODE102": False, "CODE408": False, # Risky codes ON (111 OFF — cache already built by Phase 1b) "CODE122": True, "CODE357": True, "CODE111": False, "CODE356": False, # plugin cmd — user can enable manually if needed "CODE108": False, # comment — rarely needed } # ───────────────────────────────────────────────────────────────────────────── # Background workers # ───────────────────────────────────────────────────────────────────────────── class _ScanWorker(QThread): """Run project_scanner.list_data_files in a thread.""" done = pyqtSignal(object) # list[dict] error = pyqtSignal(str) def __init__(self, data_path: str, engine: str): super().__init__() self.data_path = data_path self.engine = engine def run(self): try: from util.project_scanner import list_data_files result = list_data_files(self.data_path, self.engine) self.done.emit(result) except Exception as exc: self.error.emit(str(exc)) class _ImportWorker(QThread): """Copy selected files into files/ directory.""" done = pyqtSignal(int, list) # count_copied, errors log = pyqtSignal(str) def __init__(self, file_items: list[dict], dest_dir: str): super().__init__() self.file_items = file_items self.dest_dir = dest_dir def run(self): try: import shutil from util.project_scanner import import_to_files # Clear existing files/ contents before importing so stale files # from a previous game don't linger. translated/ is intentionally # left untouched. dest = Path(self.dest_dir) if dest.exists(): removed = 0 for fp in dest.iterdir(): if fp.name == ".gitkeep": continue if fp.is_file(): try: fp.unlink() removed += 1 except Exception as e: self.log.emit(f" ⚠ Could not remove {fp.name}: {e}") elif fp.is_dir(): try: shutil.rmtree(fp) removed += 1 except Exception as e: self.log.emit(f" ⚠ Could not remove {fp.name}: {e}") if removed: self.log.emit(f"Cleared {removed} existing file(s) from {dest.name}/") self.log.emit(f"Importing {len(self.file_items)} file(s) into files/ …") count, errors = import_to_files(self.file_items, self.dest_dir) self.done.emit(count, errors) except Exception as exc: self.done.emit(0, [str(exc)]) class _ExportWorker(QThread): done = pyqtSignal(int, list) log = pyqtSignal(str) def __init__(self, game_data_path: str, filter_names: list[str] | None = None): super().__init__() self.game_data_path = game_data_path self.filter_names = filter_names # if set, only export these filenames def run(self): try: from util.project_scanner import export_to_game if self.filter_names: self.log.emit( f"Exporting {len(self.filter_names)} active file(s) → {self.game_data_path} …" ) else: self.log.emit(f"Exporting translated/ → {self.game_data_path} …") count, errors = export_to_game( "translated", self.game_data_path, filenames=self.filter_names ) self.done.emit(count, errors) except Exception as exc: self.done.emit(0, [str(exc)]) class _SubprocessWorker(QThread): """Run an arbitrary shell command in a given working directory, streaming output.""" done = pyqtSignal(bool, str) # success, final message log = pyqtSignal(str) def __init__(self, cmd: list, cwd: str, label: str = ""): super().__init__() self.cmd = cmd self.cwd = cwd self.label = label or cmd[0] def run(self): import subprocess import shutil as _shutil try: exe = _shutil.which(self.cmd[0]) if exe is None: self.done.emit( False, f"'{self.cmd[0]}' not found on PATH. " "Make sure it is installed and accessible from the terminal.", ) return self.log.emit(f"$ {' '.join(str(c) for c in self.cmd)} — cwd: {self.cwd}") proc = subprocess.Popen( self.cmd, cwd=self.cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace", ) for line in proc.stdout: stripped = line.rstrip("\n") if stripped: self.log.emit(stripped) proc.wait() if proc.returncode == 0: self.done.emit(True, f"{self.label}: finished successfully.") else: self.done.emit(False, f"{self.label}: exited with code {proc.returncode}.") except Exception as exc: self.done.emit(False, f"{self.label}: {exc}") class _JsonFormatWorker(QThread): """Format all JSON files in a directory using the bundled dazedformat utility.""" done = pyqtSignal(bool, str) log = pyqtSignal(str) def __init__(self, data_path: str): super().__init__() self.data_path = data_path def run(self): try: from util.dazedformat import format_json_files self.log.emit(f"Formatting JSON files in {self.data_path} …") count, errors = format_json_files(self.data_path, log=self.log.emit) for e in errors: self.log.emit(f" ⚠ {e}") if errors: self.done.emit(False, f"dazedformat: {count} formatted, {len(errors)} error(s).") else: self.done.emit(True, f"dazedformat: {count} file(s) formatted successfully.") except Exception as exc: self.done.emit(False, f"dazedformat error: {exc}") class _FileCopyWorker(QThread): """Recursively copy a source folder into a destination folder.""" done = pyqtSignal(int, list) # count_copied, errors log = pyqtSignal(str) def __init__(self, src: str, dst: str): super().__init__() self.src = src self.dst = dst def run(self): import shutil src = Path(self.src) dst = Path(self.dst) if not src.is_dir(): self.done.emit(0, [f"Source folder not found: {src}"]) return dst.mkdir(parents=True, exist_ok=True) copied = 0 errors: list[str] = [] self.log.emit(f"Copying {src} → {dst} …") for fp in src.rglob("*"): if not fp.is_file(): continue rel = fp.relative_to(src) target = dst / rel try: target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(fp, target) copied += 1 self.log.emit(f" copied {rel}") except Exception as exc: errors.append(f"{rel}: {exc}") self.done.emit(copied, errors) class _JsFormatWorker(QThread): """Format a JavaScript file using jsbeautifier (pure Python, no Node required).""" done = pyqtSignal(bool, str) log = pyqtSignal(str) def __init__(self, js_path: str): super().__init__() self.js_path = js_path def run(self): try: p = Path(self.js_path) self.log.emit(f"Formatting {p.name} …") original = p.read_text(encoding="utf-8") opts = jsbeautifier.default_options() opts.indent_size = 2 opts.indent_char = " " opts.max_preserve_newlines = 2 opts.preserve_newlines = True opts.end_with_newline = True formatted = jsbeautifier.beautify(original, opts) p.write_text(formatted, encoding="utf-8") self.done.emit(True, f"plugins.js formatted successfully ({len(formatted):,} chars).") except Exception as exc: self.done.emit(False, f"Format error: {exc}") def _make_section_label(text: str) -> QLabel: lbl = QLabel(text) lbl.setStyleSheet( "font-size: 14px; font-weight: bold; color: #e0e0e0;" "padding: 7px 0px 6px 10px; background-color: transparent;" "border-left: 3px solid #007acc; margin-top: 6px;" ) return lbl def _make_hr() -> QFrame: hr = QFrame() hr.setFrameShape(QFrame.HLine) hr.setFrameShadow(QFrame.Plain) hr.setStyleSheet("QFrame { color: #333333; margin: 10px 0px 4px 0px; }") return hr def _make_btn(text: str, color: str = "#007acc") -> QPushButton: """Button styled to match the translation tab. Dark utility colours (max channel < 115) use the flat sidebar style. Action colours use flat dark bg + coloured outline + coloured text. """ btn = QPushButton(text) try: c = color.lstrip("#") if len(c) == 3: c = c[0] * 2 + c[1] * 2 + c[2] * 2 r, g, b = int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16) is_flat = max(r, g, b) < 115 except Exception: r = g = b = 0 is_flat = False _PAD = "padding:6px 14px;" if is_flat: btn.setStyleSheet( f"QPushButton{{background-color:#2d2d30;color:#cccccc;" f"border:1px solid #555555;{_PAD}" f"border-radius:4px;font-size:12px;font-weight:bold;" f"font-family:'Segoe UI','Segoe UI Emoji','Apple Color Emoji',sans-serif;}}" f"QPushButton:hover{{background-color:#3e3e42;}}" f"QPushButton:pressed{{background-color:#007acc;color:white;}}" f"QPushButton:disabled{{background-color:#404040;color:#666666;border-color:#444444;}}" ) else: # Flat dark bg + coloured outline + lightened text rt = min(255, r + 80) gt = min(255, g + 80) bt = min(255, b + 80) text_color = f"#{rt:02x}{gt:02x}{bt:02x}" base = 0x2d rh = min(255, int(base + (r - base) * 0.18)) gh = min(255, int(base + (g - base) * 0.18)) bh = min(255, int(base + (b - base) * 0.18)) hover_bg = f"#{rh:02x}{gh:02x}{bh:02x}" rb = min(255, r + 35) gb = min(255, g + 35) bb = min(255, b + 35) hover_accent = f"#{rb:02x}{gb:02x}{bb:02x}" btn.setStyleSheet( f"QPushButton{{background-color:#2d2d30;color:{text_color};" f"border:1px solid {color};{_PAD}" f"border-radius:4px;font-size:12px;font-weight:bold;" f"font-family:'Segoe UI','Segoe UI Emoji','Apple Color Emoji',sans-serif;}}" f"QPushButton:hover{{background-color:{hover_bg};border-color:{hover_accent};color:{hover_accent};}}" f"QPushButton:pressed{{background-color:#1a1a1a;}}" f"QPushButton:disabled{{background-color:#2d2d30;color:#555555;border-color:#444444;}}" ) return btn def _make_icon_btn(icon_text: str, tooltip: str = "") -> QPushButton: """Compact icon-only button matching the Translation tab action buttons.""" btn = QPushButton(icon_text) btn.setToolTip(tooltip) btn.setFont(QFont("Segoe UI", 12)) btn.setFixedSize(40, 36) btn.setStyleSheet( "QPushButton{background-color:#2d2d30;color:white;" "font-weight:bold;font-size:16px;border:1px solid #555555;" "border-radius:4px;min-width:40px;max-width:40px;" "min-height:36px;max-height:36px;}" "QPushButton:hover{background-color:#3e3e42;border-left-color:#007acc;}" "QPushButton:pressed{background-color:#007acc;}" "QPushButton:disabled{background-color:#2d2d30;color:#555555;border-color:#444444;}" ) return btn # ───────────────────────────────────────────────────────────────────────────── # Helpers # ───────────────────────────────────────────────────────────────────────────── class _PlainPasteTextEdit(QTextEdit): """QTextEdit that always pastes as plain text to avoid spurious newlines.""" def insertFromMimeData(self, source): # noqa: N802 self.insertPlainText(source.text()) # ───────────────────────────────────────────────────────────────────────────── # Main widget # ───────────────────────────────────────────────────────────────────────────── class WorkflowTab(QWidget): """Guided automation tab for the full RPGMaker translation workflow.""" def __init__(self, parent=None): super().__init__(parent) self.parent_window = parent try: self.settings = QSettings("DazedTranslations", "DazedMTLTool") except Exception: self.settings = None # State self._data_path: str | None = None self._engine: str = "MVMZ" self._file_items: list[dict] = [] self._worker = None # active background QThread # Pre-process paths (auto-populated after folder detection) self._plugins_js_path: str = "" self._gameupdate_path: str = "" # RPGMaker Ace state self._ace_encrypted: bool = False self._ace_json_dir: str = "" # /JSON/ — used as _data_path for Ace self._ace_rvdata_dir: str = "" # /Data/ with rvdata2 files self._p2_loading_config: bool = False self._p2_auto_apply_timer: QTimer | None = None self._syncing_file_checks: bool = False self._import_buttons: list[QPushButton] = [] self._current_step_index: int = 0 self._last_import_signature: tuple[str, ...] | None = None self._pending_import_signature: tuple[str, ...] | None = None self._init_ui() # ───────────────────────────────── UI setup ────────────────────────────── def _init_ui(self): root = QVBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) root.setSpacing(0) # Splitter: left=steps, right=log splitter = QSplitter(Qt.Horizontal) splitter.setHandleWidth(1) splitter.setStyleSheet("QSplitter::handle{background:#3a3a3a;}") # ---- Left: tabbed step panels ---- _SCROLL_STYLE = ( "QScrollArea{border:none;background-color:transparent;}" "QScrollBar:vertical{background:#252526;width:10px;border:none;}" "QScrollBar::handle:vertical{background:#555555;border-radius:5px;min-height:20px;}" "QScrollBar::handle:vertical:hover{background:#007acc;}" "QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical{height:0;}" ) self._step_tabs = QTabWidget() self._step_tabs.setStyleSheet(""" QTabWidget::pane { border: none; background-color: #1e1e1e; } QTabWidget::tab-bar { alignment: left; } QTabBar { background-color: #252526; } QTabBar::tab { background-color: #252526; color: #7a7a7a; padding: 9px 18px; border: none; border-right: 1px solid #3a3a3a; font-size: 12px; min-width: 90px; } QTabBar::tab:selected { background-color: #1e1e1e; color: #e0e0e0; font-weight: bold; border-top: 2px solid #007acc; } QTabBar::tab:hover:!selected { background-color: #2d2d30; color: #cccccc; } """) _tab_defs = [ ("0 Project", self._build_step0), ("1 Pre-process", self._build_step1_preprocess), ("2 Speaker", self._build_step2_speaker), ("3 Glossary", self._build_step3_glossary), ("4 TL Phase 1", self._build_step4_translation), ("5 TL Phase 2", self._build_step5_tl_phase2), ("6 Plugins.js", self._build_step6_plugins_js), ("7 Export", self._build_step7_export), ("8 Playtest", self._build_step8_playtest), ] for tab_label, builder in _tab_defs: # Each tab: outer page → scroll area → inner content widget page = QWidget() page_layout = QVBoxLayout(page) page_layout.setContentsMargins(0, 0, 0, 0) page_layout.setSpacing(0) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setStyleSheet(_SCROLL_STYLE) inner = QWidget() vbox = QVBoxLayout(inner) vbox.setContentsMargins(24, 18, 24, 12) vbox.setSpacing(10) builder(vbox) vbox.addStretch() scroll.setWidget(inner) page_layout.addWidget(scroll, 1) # ── Navigation footer ────────────────────────────────────────── nav = QWidget() nav.setStyleSheet( "QWidget{background-color:#252526;" "border-top:1px solid #3a3a3a;}" ) nav_layout = QHBoxLayout(nav) nav_layout.setContentsMargins(16, 6, 16, 6) nav_layout.setSpacing(8) tab_idx = len(self._step_tabs) # current tab index (before addTab) if tab_idx > 0: back_btn = _make_btn("← Back", "#3a3a3a") back_btn.setFixedWidth(100) _idx = tab_idx # capture for lambda back_btn.clicked.connect( lambda _checked, i=_idx: self._step_tabs.setCurrentIndex(i - 1) ) nav_layout.addWidget(back_btn) nav_layout.addStretch() if tab_idx < len(_tab_defs) - 1: next_btn = _make_btn("Next →", "#007acc") next_btn.setFixedWidth(100) _idx = tab_idx # capture for lambda _lbl = tab_label # original label without checkmark next_btn.clicked.connect( lambda _checked, i=_idx, lbl=_lbl: ( self._step_tabs.setTabText(i, "✓ " + lbl), self._step_tabs.setCurrentIndex(i + 1), ) ) nav_layout.addWidget(next_btn) page_layout.addWidget(nav) self._step_tabs.addTab(page, tab_label) self._step_tabs.currentChanged.connect(self._on_step_tab_changed) splitter.addWidget(self._step_tabs) # ---- Right: log area ---- log_panel = QWidget() lp_layout = QVBoxLayout(log_panel) lp_layout.setContentsMargins(0, 0, 0, 0) lp_layout.setSpacing(0) log_header = QLabel(" ▸ Workflow Log") log_header.setStyleSheet( "background-color:#252526;color:#9d9d9d;font-size:11px;font-weight:bold;" "padding:7px 10px;border-bottom:1px solid #3a3a3a;" "letter-spacing:0.5px;" ) lp_layout.addWidget(log_header) self.log_area = QTextEdit() self.log_area.setReadOnly(True) self.log_area.setFont(QFont("Consolas", 9)) self.log_area.setStyleSheet( "QTextEdit{background-color:#1e1e1e;color:#c8c8c8;" "border:none;padding:10px;" "selection-background-color:#264f78;}" ) lp_layout.addWidget(self.log_area) clear_btn = _make_btn("Clear Log", "#3a3a3a") clear_btn.setStyleSheet( "QPushButton{background-color:#252526;color:#6a6a6a;" "border:none;border-top:1px solid #3a3a3a;" "padding:5px 12px;font-size:11px;font-weight:normal;" "font-family:'Segoe UI','Arial',sans-serif;}" "QPushButton:hover{background-color:#2d2d30;color:#9d9d9d;}" "QPushButton:pressed{color:#cccccc;}" ) clear_btn.clicked.connect(self.log_area.clear) lp_layout.addWidget(clear_btn) splitter.addWidget(log_panel) splitter.setSizes([900, 200]) root.addWidget(splitter) self.setLayout(root) self._apply_theme() self._detected_on_show: bool = False # guard: only auto-detect once per new folder # ── Tab visibility ────────────────────────────────────────────────────── def showEvent(self, event): """Trigger folder detection the first time this tab is shown (or after a new folder is set).""" super().showEvent(event) if not self._detected_on_show and self._setting("last_game_folder", ""): self._detected_on_show = True QTimer.singleShot(100, self._detect_folder) def eventFilter(self, obj, event): """Toggle workflow file checks when clicking a row outside the checkbox.""" try: if ( obj is self.file_list.viewport() and event.type() == QEvent.MouseButtonRelease and event.button() == Qt.LeftButton ): item = self.file_list.itemAt(event.pos()) if item is None: return False # Let native checkbox clicks handle their own checked state. item_rect = self.file_list.visualItemRect(item) if event.pos().x() <= item_rect.left() + 26: return False item.setCheckState( Qt.Unchecked if item.checkState() == Qt.Checked else Qt.Checked ) return False except Exception: pass return super().eventFilter(obj, event) def _on_step_tab_changed(self, index: int): """Refresh config-backed controls when their workflow page is shown.""" previous_index = self._current_step_index self._current_step_index = index if previous_index == 0 and index != 0: self._auto_import_if_needed() if index == 5: self._populate_p2_checkboxes() if index == 8: self._refresh_tl_inspector_status() self._load_tli_editor_settings() def _register_import_button(self, button: QPushButton) -> None: self._import_buttons.append(button) def _set_import_buttons_enabled(self, enabled: bool) -> None: for button in self._import_buttons: button.setEnabled(enabled) def _apply_theme(self): """Apply a unified dark-theme stylesheet to all standard controls.""" self.setStyleSheet(""" QLineEdit { background-color: #3c3c3c; border: 1px solid #555555; border-radius: 4px; padding: 4px 8px; color: #cccccc; font-size: 13px; } QLineEdit:focus { border-color: #007acc; } QLineEdit:disabled { background-color: #2d2d30; color: #666666; } QLineEdit::placeholder { color: #606060; } QSpinBox { background-color: #3c3c3c; border: 1px solid #555555; border-radius: 4px; padding: 3px 6px; color: #cccccc; font-size: 13px; } QSpinBox:focus { border-color: #007acc; } QSpinBox::up-button, QSpinBox::down-button { background-color: #2d2d30; border: none; width: 18px; } QSpinBox::up-button:hover, QSpinBox::down-button:hover { background-color: #007acc; } QCheckBox { color: #cccccc; font-size: 13px; spacing: 7px; } QCheckBox::indicator { width: 14px; height: 14px; border: 1px solid #555555; border-radius: 3px; background-color: #3c3c3c; } QCheckBox::indicator:checked { background-color: #007acc; border-color: #007acc; } QCheckBox::indicator:hover { border-color: #007acc; } QCheckBox::indicator:disabled { background-color: #2d2d30; border-color: #444444; } """) # ── Step 0: Project Folder ────────────────────────────────────────────── def _build_step0(self, layout: QVBoxLayout): layout.addWidget(_make_section_label("Step 0 — Project Folder")) # Folder picker row row0 = QHBoxLayout() self.folder_edit = QLineEdit() self.folder_edit.setPlaceholderText("Path to game root folder…") saved = self._setting("last_game_folder", "") if saved: self.folder_edit.setText(saved) row0.addWidget(self.folder_edit, 1) self.folder_edit.returnPressed.connect(self._detect_folder) browse_btn = _make_icon_btn("📁", "Browse for a game project folder") browse_btn.clicked.connect(self._browse_folder) row0.addWidget(browse_btn) layout.addLayout(row0) # Detected info self.detected_label = QLabel("No folder detected yet.") self.detected_label.setStyleSheet( "color:#6a9a6a;font-size:13px;padding:4px 8px;" "background-color:#1f2b1f;border:1px solid #2a4a2a;" "border-radius:4px;margin:4px 0;" ) layout.addWidget(self.detected_label) # File list self.file_list = QListWidget() self.file_list.setMinimumHeight(320) self.file_list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.file_list.setSelectionMode(QAbstractItemView.ExtendedSelection) self.file_list.itemChanged.connect(self._sync_selected_file_checks) self.file_list.viewport().installEventFilter(self) self.file_list.setStyleSheet( "QListWidget{outline:none;border:1px solid #3c3c3c;" "background-color:#252526;border-radius:4px;}" "QListWidget::item{border:none;outline:none;padding:2px 6px;" "color:#c8c8c8;}" "QListWidget::item:selected{background-color:#252526;color:#c8c8c8;}" "QListWidget::item:hover{background-color:#2d2d30;" "border-left:2px solid #007acc;}" ) layout.addWidget(self.file_list, 1) # Action row row1 = QHBoxLayout() row1.setSpacing(6) select_all_btn = _make_icon_btn("✓", "Select all importable files") select_all_btn.clicked.connect(self._select_all_files) row1.addWidget(select_all_btn) deselect_all_btn = _make_icon_btn("✗", "Deselect all files") deselect_all_btn.clicked.connect(self._deselect_all_files) row1.addWidget(deselect_all_btn) sel_core = _make_icon_btn("◆", "Core Only: select database files and deselect maps") sel_core.setToolTip("Select only core database files; deselect map files") sel_core.clicked.connect(self._select_core_only) row1.addWidget(sel_core) import_btn = _make_icon_btn("📥", "Import selected files into files/") import_btn.setEnabled(False) import_btn.setToolTip("Replace files/ with exactly the checked files above") import_btn.clicked.connect(lambda _checked=False: self._import_files()) self._register_import_button(import_btn) row1.addWidget(import_btn) row1.addStretch() layout.addLayout(row1) # ── Step 3: Vocab / Glossary ──────────────────────────────────────────── # ── Copilot prompt templates ──────────────────────────────────────────── _SPEAKER_PROMPT = ( "You are an expert RPGMaker MV/MZ analyst helping configure a Japanese game translation tool.\n" "\n" "\n" "Examine the game's event files (Map*.json, CommonEvents.json, Troops.json) and determine " "which speaker name format(s) the game uses. Output flag recommendations based on what you find.\n" "\n" "\n" "\n" "Code 101 opens the text window. Code 401 is a dialogue line. Multiple 401s in a row form " "one message box. The translation tool needs to know how character names appear in the event " "data so it can extract and translate them correctly.\n" "\n" "\n" "--- attach your game files here before continuing ---\n" "\n" "\n" "The following formats are detected and translated automatically — no flag is needed. " "If the game uses any of these, do NOT enable a flag for lines matching that exact pattern. " "However, continue scanning to check whether other speakers use a flag-requiring pattern.\n" "\n" "## How dialogue commands work\n" "\n" "Code 101 opens the text window. Code 401 is a dialogue line. " "Multiple 401s in a row form one message box.\n" "\n" "## IMPORTANT — Always-on formats (NO FLAG NEEDED, never enable any flag for these)\n" "\n" "The following formats are detected and translated automatically. " "If the game uses ANY of these, do NOT enable any flag for lines matching that pattern. " "However, **DO NOT STOP** — continue checking whether other speakers in the same game " "use a different format that requires a flag.\n" "\n" "\n" " 101 param[4] name { \"code\": 101, \"parameters\": [\"\", 0, 2, 2, \"るな\"] }\n" " \\\\n or \\\\k escape code with ANGLE BRACKETS anywhere inside a 401 line\n" " 【Name】 alone on a line, or 【Name】dialogue on the same line\n" " [Name] alone on a line, or [Name]dialogue on the same line\n" " \\\\c[N]Name\\\\c[0] color-wrapped name on its own 401 line\n" " Name: line ending with a full-width colon\n" "\n" "## CRITICAL — \\\\N[X] / \\\\n[X] (square bracket + number) is NOT a speaker format\n" "\n" " \\\\N[ActorID] and \\\\n[ActorID] (e.g. \\\\N[1], \\\\n[2]) are RPGMaker actor variable\n" " substitution codes. They expand to an actor's name at runtime, but the translation tool\n" " handles them purely as text substitution during translation — they are NEVER treated as\n" " a speaker name tag.\n" "\n" " A 401 line containing ONLY \\\\N[X], or narration text that embeds \\\\N[X] (e.g.\n" " \"ローターが\\\\N[1]の乳首に装着される!\"), does NOT match any always-on speaker format.\n" " Do NOT count \\\\N[X] / \\\\n[X] codes as always-on speaker detection hits.\n" "\n" " If a game's ONLY speaker indicator is a standalone \\\\N[X] line followed by dialogue,\n" " check whether FIRSTLINESPEAKERS would catch it (the standalone line must be < 40 chars\n" " AND contain at least one Japanese character — a bare \\\\N[1] with no Japanese text does\n" " NOT qualify for FIRSTLINESPEAKERS either).\n" "\n" "\n" "\n" "Only enable these when at least some speakers do NOT use an always-on format.\n" "\n" "\n" "The speaker name is embedded at the start of a 401 line directly before 「, " "with no intervening markup or brackets.\n" "\n" "{ \"code\": 401, \"parameters\": [\"エレナ「今日は晴れですね。\"] }\n" "\n" "Enable if the game has dialogue lines matching this pattern.\n" "\n" "\n" "\n" "The very first 401 in a message group is a short standalone name (under 40 chars), " "and the following 401 starts with 「 \" ( ( * [. This commonly appears for NPCs even " "in games where the protagonist uses an always-on format. The 101 command for these " "lines typically has an empty face image (parameters[0] == \"\").\n" "\n" "{ \"code\": 101, \"parameters\": [\"\", 0, 0, 2] }\n" "{ \"code\": 401, \"parameters\": [\"衛兵さん\"] } ← plain name, no color or brackets\n" "{ \"code\": 401, \"parameters\": [\"「……起きたか」\"] }\n" "\n" "Enable if ANY speakers in the game use this pattern, even if others use an always-on format.\n" "\n" "\n" "\n" "Enable ONLY when: (a) the game has no always-on format, AND (b) neither INLINE401SPEAKERS " "nor FIRSTLINESPEAKERS applies, AND (c) the 101 code has a face image filename in " "parameters[0] while parameters[4] is empty.\n" "\n" "{ \"code\": 101, \"parameters\": [\"face_alice\", 0, 0, 2, \"\"] }\n" "\n" "If you recommend this, you MUST list every unique face filename found in parameters[0] " "of code 101 across all attached files.\n" "\n" "\n" "\n" "\n" "Follow these steps in order:\n" "\n" "1. Scan ALL 101→401 blocks across the sample files.\n" " (a) List every always-on pattern found — these need no flag.\n" " (b) List every flag-requiring pattern found separately.\n" " A game may use both simultaneously (e.g. protagonist via \\\\c[N]Name\\\\c[0], " "NPCs via plain standalone name lines). Do not stop after finding one pattern.\n" "\n" "2. For each flag-requiring pattern from step 1:\n" " - INLINE401SPEAKERS → ENABLE if name「 inline pattern exists\n" " - FIRSTLINESPEAKERS → ENABLE if plain standalone name line exists\n" " Both may be enabled together if both patterns exist.\n" "\n" "3. Only if steps 1–2 found no flag-requiring patterns AND no always-on format " "→ consider FACENAME101.\n" "\n" "\n" "\n" "Provide exactly four sections:\n" "\n" "1. Patterns detected — one entry per speaker type (protagonist, NPCs, signs/narration)\n" "2. Flag decisions:\n" " INLINE401SPEAKERS : ENABLE / SKIP — \n" " FIRSTLINESPEAKERS : ENABLE / SKIP — \n" " FACENAME101 : ENABLE / SKIP — \n" "3. A short concrete example from the actual files for each detected pattern\n" "4. (Only if FACENAME101 is ENABLE) Every unique face filename from parameters[0] of code 101\n" "\n" ) _PROMPT_GLOSSARY = ( "You are an expert Japanese RPGMaker game analyst building a translation glossary.\n" "\n" "\n" "Extract named characters and lore-specific worldbuilding terms from this game's data files. " "Produce a structured glossary in the exact format specified below. It will be loaded directly " "into a translation tool, so strict format compliance is required.\n" "\n" "\n" "--- attach your game data files here before continuing ---\n" "\n" "\n" "Map files and CommonEvents.json can be extremely large. Do NOT read them sequentially — " "you will hit context limits. Use this strategy:\n" "\n" "1. Read these small DB files IN FULL first — richest source of names, always small:\n" " Actors.json is mandatory for major character vocab. Use it as the canonical source " "for actor IDs, Japanese names, nicknames, profiles, and \\N[n] name mappings.\n" " Then read Classes.json, Troops.json, Skills.json, Items.json,\n" " Armors.json, Weapons.json, States.json, System.json\n" "\n" "2. For large files (CommonEvents.json, Map*.json), SEARCH (grep) instead of reading " "sequentially. Prioritise dialogue commands because they are the best evidence for " "character voice:\n" " - Code 401 dialogue lines, plus nearby code 101 speaker/name parameters\n" " - Code 405 scrolling text when present\n" " - Speaker patterns such as 【Name】, [Name], Name:, \\\\n, and \\\\k\n" " - Capitalised katakana clusters or kanji compound proper nouns in dialogue/parameters fields\n" " Scan Map001.json through Map010.json at most — early maps have the most story content.\n" "\n" "3. Stop once you stop finding new names or terms. Do not pad the output.\n" "\n" "\n" "\n" "Apply to both sections:\n" "- Separator: use a plain hyphen-minus (-). Never use — or –. " "The translation tool only recognises the plain hyphen.\n" "- Descriptions must be entirely in English. Refer to other characters by English name only " "(write 'her sister Ruin was kidnapped', not 'her sister ルイン was kidnapped').\n" "- Never give two spelling options (e.g. 'Sylfia / Sylphia' is wrong). Commit to one translation.\n" "\n" "# Game Characters — rules:\n" "- If a list was provided, output entries for those names, then cross-check " "Actors.json for major named actors that should also be included. Skip unnamed NPCs, generic " "enemies, and narration-only entries.\n" "- If no list was provided, discover named characters from the files, but still skip " "unnamed NPCs and generic enemy types.\n" "- For each character include: gender, role, speech register, personality, and whether " "the name is player-chosen (check Actors.json ID 1).\n" "- Any actor in Actors.json with a real name should get a full # Game Characters entry, " "not only a \\N[n] placeholder mapping. If events reference \\N[3] or [EnglishName], " "resolve it through Actors.json ID 3 and preserve the full character context.\n" "- Event references are supporting evidence for personality/speech, but they must not " "replace actor-database discovery. Do not miss major characters just because they appear " "primarily in Actors.json.\n" "\n" "# Worldbuilding Terms — rules:\n" "- Include: faction/organisation names, locations mentioned in dialogue but not on maps, " "unique magic systems, lore titles, recurring in-universe concepts.\n" "- Exclude: skill names, item names, weapon names, armour names (the tool handles those). " "Skip generic RPG words (\u30dd\u30fc\u30b7\u30e7\u30f3, \u30ec\u30d9\u30eb, \u30b9\u30c6\u30fc\u30bf\u30b9, etc.). " "Do not repeat character names here.\n" "\n" "\n" "\n" "Output EXACTLY two sections with these headers. Do not add any preamble, explanation, " "or text outside the entries.\n" "\n" "# Game Characters\n" "# Worldbuilding Terms\n" "\n" "Entry format: Japanese (English) - description\n" "\n" "\n" "# Game Characters\n" "アリア (Aria) - Female; protagonist; player-chosen name (Actors.json ID 1); " "speaks cheerfully in casual feminine speech; nicknamed アリアちゃん by her sister\n" "ゼクス (Zex) - Male; antagonist; cold and commanding; addresses others with contempt; " "uses archaic formal register\n" "カナエ (Kanae) - Female; NPC shopkeeper; warm and motherly; ends sentences with わね\n" "\n" "# Worldbuilding Terms\n" "虚無の穴 (Void Rift) - Dimensional tear referenced repeatedly in Act 2 NPC dialogue; " "not a named map location\n" "鋼の誓約 (Iron Vow) - Sacred oath-binding ritual unique to the knightly order; " "appears in story cutscenes\n" "裁定者 (Arbiter) - Title held by the ruling council; lore-specific rank with no " "real-world equivalent\n" "\n" "\n" ) _WRAP_PROMPT = ( "You are an expert RPGMaker MV/MZ configuration analyst.\n" "\n" "\n" "Calculate the correct text-wrap width settings for a Japanese-to-English RPGMaker MV/MZ " "translation tool. The tool wraps translated English using character-count limits (not pixels). " "I need three values: width, listWidth, and noteWidth.\n" "\n" "\n" "--- attach System.json and js/plugins.js here before continuing ---\n" "\n" "\n" "1. Read screenWidth and fontSize from System.json.\n" " Check js/plugins.js for any MessageCore or Window plugin that overrides these values.\n" "2. For each window type, estimate its pixel width, subtract ~48px padding, then calculate:\n" " chars = floor(content_px / (font_size × 0.58))\n" " - width: main dialogue/message box (Show Text) — typically full screen width\n" " - listWidth: item/skill/help description windows — typically full or half screen width\n" " - noteWidth: database note fields — typically the narrowest pane (~40–50% screen width)\n" "3. If font size is above 26px and reducing it would meaningfully increase characters per line, " "note where to change it (System.json or the relevant plugin parameter).\n" "\n" "\n" "\n" "Output only the final values — do not show calculations:\n" "\n" "```\n" "width=\n" "listWidth=\n" "noteWidth=\n" "fontSize= # or: no change needed\n" "```\n" "\n" "Followed by one sentence of assumptions if anything was estimated.\n" "\n" ) _PLUGINS_JS_TRANSLATE_PROMPT = ( "You are an expert RPGMaker MV/MZ localisation engineer.\n" "\n" "\n" "Translate visible Japanese strings inside js/plugins.js without breaking any game logic " "or plugin functionality. A vocab.txt glossary is attached — use it as your primary reference. " "Any name or term that appears in the glossary must be translated exactly as shown there.\n" "\n" "\n" "--- attach js/plugins.js and vocab.txt here before continuing ---\n" "\n" "\n" "\n" "Only translate string values directly shown to the player at runtime:\n" "- Menu/button labels in-game UI (e.g. 決定, キャンセル)\n" "- Scene/window title text (e.g. アイテム, スキル, 装備)\n" "- In-game popup, tooltip, or notification messages\n" "- Default NPC names or pronouns used in UI display\n" "- Help or description text shown in help windows\n" "- Battle log messages or status effect messages\n" "\n" "\n" "\n" "Translating the following WILL BREAK THE GAME:\n" "\n" "1. Plugin parameter keys (object property names).\n" " { \"CommandName\": \"セーブ\" } → translate セーブ but NOT CommandName\n" "\n" "2. Strings used as internal identifiers or lookup keys:\n" " - Switch/variable names matching System.json entries\n" " - Actor, skill, item, weapon, armour names used as keys inside other plugin parameters\n" " - Strings passed as plugin command arguments that the engine looks up\n" "\n" "3. File paths, filenames, URLs, colour codes, font names, icon indices.\n" " (e.g. img/faces/Actor1 or #ffffff)\n" "\n" "4. Plugin names and script identifiers:\n" " - The \"name\" property at the top of each plugin block (e.g. { \"name\": \"YEP_CoreEngine\" })\n" " - Function identifiers, class names, JS event names\n" "\n" "5. Any string in the game data files (Actors.json, Skills.json, Items.json, etc.) that is " "also used as a lookup key in the plugin — changing it here would desync it from the data file.\n" "\n" "6. Boolean strings, numeric strings, regex patterns.\n" "\n" "\n" "\n" "Before translating any string, confirm all three are true:\n" "- It is displayed directly to the player as visible text\n" "- It is purely a UI label, not used as a key anywhere else\n" "- Nothing in the codebase compares this exact string to another value\n" "When in doubt, skip it — untranslated Japanese is better than a broken game.\n" "\n" "\n" "\n" "Provide the translated file as a complete replacement of js/plugins.js. Only change the " "string values identified above. Preserve all original formatting, indentation, comments, and structure.\n" "\n" "After the file, output a summary:\n" "\n" "### Translations Made\n" " Plugin: \n" " Parameter: \n" " Before: \n" " After: \n" "\n" "### Skipped (Ambiguous or Internal)\n" "List any Japanese strings you detected but did not translate, with a one-line reason.\n" "\n" ) _ACE_SCRIPTS_TRANSLATE_PROMPT = ( "You are an expert RPGMaker VX Ace (Ruby) localisation engineer.\n" "\n" "\n" "Translate visible Japanese strings inside the game's Ruby scripts (ace_json/scripts/*.rb) " "without breaking any game logic or script functionality. A vocab.txt glossary is attached — " "use it as your primary reference. Any name or term in the glossary must be translated exactly as shown.\n" "\n" "\n" "--- attach the .rb script files and vocab.txt here before continuing ---\n" "\n" "\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "## WHAT TO TRANSLATE\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "Only translate string literals that are directly shown to the player at runtime.\n" "These typically appear as:\n" " - Strings passed to msgbox, msgbox_p, print, p\n" " - Labels and text in Window or Scene classes rendered to screen\n" " - draw_text / draw_item calls with a Japanese string literal\n" " - Default UI label text (menu names, button labels, status window text)\n" " - Battle log messages, notifications, popup strings\n" " - Help or description text shown in help windows\n" "\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "## WHAT MUST NOT BE TRANSLATED\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "CRITICAL — translating the following will break the game:\n" "\n" " 1. Strings used as hash keys, method names, or symbol equivalents.\n" " Example: vocab[\"HP\"] = \"体力\" \u2192 translate \"体力\" but NOT the key \"HP\"\n" "\n" " 2. Strings used as internal identifiers compared with == or used in case/when:\n" " - Actor/class/skill/item names used as lookup strings\n" " - Script-internal state names or flag strings\n" " Example: if type == \"スキル\" \u2192 do NOT translate \"スキル\"\n" "\n" " 3. File paths, filenames, font names, colour strings, URLs.\n" "\n" " 4. Regular expressions, format strings used with sprintf or % operator\n" " where the placeholders must stay in the same position.\n" " (You may translate the human-readable parts but keep %s / %d / %1 etc intact.)\n" "\n" " 5. Script class names, module names, method names, constants.\n" "\n" " 6. Any string that is read back elsewhere in the scripts with an exact match.\n" "\n" "\n" "Before translating any string, confirm all three are true:\n" "- It is displayed directly to the player as visible text\n" "- It is purely a display string, not compared or looked up anywhere\n" "- Changing it would break no conditional logic or data lookup\n" "When in doubt, skip it — untranslated Japanese is better than a broken game.\n" "\n" "\n" "\n" "For each .rb file that needed changes, provide the full translated file content. " "Only change string values identified as safe. " "Preserve all Ruby syntax, indentation, comments, and structure exactly.\n" "\n" "After all files, output a summary:\n" "\n" "### Translations Made\n" " File: