diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index 3a2cebf..8d2e183 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -1,3714 +1,3802 @@ -""" -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), Phase 2 (risky) - Step 5 – Translate visible strings in js/plugins.js - Step 6 – Export translated/ back to the game folder -""" - -from __future__ import annotations - -import json -import os -import sys -import threading -from pathlib import Path - -import jsbeautifier - -from PyQt5.QtCore import Qt, QSettings, QThread, QTimer, pyqtSignal -from PyQt5.QtGui import QFont -from PyQt5.QtWidgets import ( - QApplication, - QCheckBox, - QFileDialog, - QFrame, - QGridLayout, - QGroupBox, - QHBoxLayout, - QLabel, - QLineEdit, - QListWidget, - QListWidgetItem, - QMessageBox, - QPushButton, - QScrollArea, - QSizePolicy, - QSpinBox, - QSplitter, - QStyle, - QTabWidget, - QTextEdit, - QVBoxLayout, - QWidget, -) - -# --------------------------------------------------------------------------- -# 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: - 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.is_file() and fp.name != ".gitkeep": - try: - fp.unlink() - 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_toggle_btn(text: str) -> QPushButton: - """Checkable Select All / Deselect All button, styled to match the rest of the workflow UI.""" - btn = QPushButton(text) - btn.setCheckable(True) - btn.setStyleSheet( - "QPushButton{background-color:#2d2d30;color:#aaaaaa;" - "border:1px solid #555555;padding:4px 12px;" - "border-radius:4px;font-size:12px;" - "font-family:'Segoe UI','Segoe UI Emoji','Apple Color Emoji',sans-serif;}" - "QPushButton:hover{background-color:#3e3e42;color:#cccccc;}" - "QPushButton:checked{background-color:#1a3a5a;color:#7ab8d4;border-color:#2a6a9a;}" - "QPushButton:checked:hover{background-color:#1e4268;}" - ) - 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._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), - ] - - 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 _on_step_tab_changed(self, index: int): - """Refresh config-backed controls when their workflow page is shown.""" - if index == 5: - self._populate_p2_checkboxes() - - 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_btn(" Browse…") - browse_btn.setIcon(browse_btn.style().standardIcon(QStyle.SP_DirOpenIcon)) - 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.setMaximumHeight(180) - 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:hover{background-color:#2d2d30;" - "border-left:2px solid #007acc;}" - ) - layout.addWidget(self.file_list) - - # Action row - row1 = QHBoxLayout() - row1.setSpacing(6) - self.sel_all_btn = _make_btn("Select All", "#555") - self.sel_all_btn.setFixedWidth(110) - self.sel_all_btn.clicked.connect(self._select_all_files) - row1.addWidget(self.sel_all_btn) - - sel_core = _make_btn("Core Only", "#555") - sel_core.setFixedWidth(110) - sel_core.setToolTip("Select only core database files; deselect map files") - sel_core.clicked.connect(self._select_core_only) - row1.addWidget(sel_core) - - 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: