""" 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 # ───────────────────────────────────────────────────────────────────────────── # 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._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) 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 _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 helping me configure a Japanese RPGMaker MV/MZ translation tool.\n" "\n" "Look at a sample of the game's event files (Map*.json, CommonEvents.json, " "Troops.json) and determine which speaker name format the game uses.\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 — output SKIP for every flag.\n" "\n" " • 101 param[4] name — { \"code\": 101, \"parameters\": [\"\", 0, 2, 2, \"るな\"] }\n" " • \\\\n or \\\\k escape code anywhere in a 401 line\n" " • 【Name】 alone on a line, or 【Name】dialogue on the same line\n" " • [Name] alone on a line (square brackets), 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" "---\n" "\n" "## Flags that MAY need to be enabled\n" "\n" "Only set these if the game does NOT already use an always-on format above.\n" "\n" "### INLINE401SPEAKERS\n" "The speaker name is embedded at the start of the 401 text directly before 「 " "with NO intervening markup or brackets.\n" "Example:\n" " { \"code\": 401, \"parameters\": [\"エレナ「今日は晴れですね。\"] }\n" "→ ENABLE only if the game has no always-on format. " "Do NOT enable if names come via 101 param[4], \\\\n<>, 【】, or [] — those are already handled.\n" "\n" "### FIRSTLINESPEAKERS\n" "The very first 401 in a message group is a short standalone name (<40 chars), " "and the following 401 starts with 「 \" ( ( * [.\n" "Example:\n" " { \"code\": 401, \"parameters\": [\"アリス\"] }\n" " { \"code\": 401, \"parameters\": [\"「こんにちは。\"] }\n" "→ ENABLE only if the game has no always-on format and names appear this way.\n" "\n" "### FACENAME101 ⚠ LAST RESORT ONLY\n" "Enable this ONLY when:\n" " (a) the game has NO always-on speaker format, AND\n" " (b) neither INLINE401SPEAKERS nor FIRSTLINESPEAKERS applies, AND\n" " (c) the 101 code has a face image filename in parameters[0] while parameters[4] is empty.\n" "Example:\n" " { \"code\": 101, \"parameters\": [\"face_alice\", 0, 0, 2, \"\"] }\n" "→ ENABLE only as a last resort. If you recommend this, you MUST also list every " "unique face filename found in parameters[0] of code 101 across the attached files, " "so the user knows which names to map.\n" "\n" "---\n" "\n" "## Decision process\n" "\n" "Step 1 — Check for always-on formats in the 401/101 blocks.\n" " If found → output SKIP for all three flags and stop.\n" "Step 2 — Check for INLINE401SPEAKERS or FIRSTLINESPEAKERS.\n" " If found → ENABLE the matching flag(s). Stop — do not also enable FACENAME101.\n" "Step 3 — Only if steps 1 and 2 both found nothing → consider FACENAME101.\n" "\n" "---\n" "\n" "Please examine a sample of the event commands in the attached files and output:\n" "\n" " 1. Which format(s) were detected and which step of the decision process applied\n" " 2. For each flag — ENABLE or SKIP, with a one-line reason:\n" " INLINE401SPEAKERS : ENABLE / SKIP — \n" " FIRSTLINESPEAKERS : ENABLE / SKIP — \n" " FACENAME101 : ENABLE / SKIP — \n" " 3. A short concrete example from the files confirming the dominant pattern\n" " 4. If FACENAME101 is ENABLE: list every unique face filename found in " "code 101 parameters[0] so the user can build the name mapping\n" ) _PROMPT_GLOSSARY = ( "You are helping me build a complete translation glossary for a Japanese RPGMaker game.\n" "\n" "⚠️ FILE SIZE WARNING: Map files and CommonEvents.json can be extremely large " "(hundreds of thousands of lines). Do NOT attempt to read them in full — you will " "hit context limits and miss content. Instead, use this strategy:\n" "\n" " 1. Read the small structured DB files IN FULL first — these are the richest " "source of names and are always small:\n" " Actors.json, Classes.json, Troops.json, Skills.json, Items.json, " "Armors.json, Weapons.json, States.json, System.json\n" "\n" " 2. For large files (CommonEvents.json, Map*.json), do NOT read sequentially. " "Instead, SEARCH (grep) for:\n" " - Named character patterns: 【, \\\\n<, \\\\k< at the start of dialogue lines\n" " - Unique proper nouns: capitalised katakana clusters or kanji compound nouns " "in \"message\" or \"parameters\" fields\n" " Scan Map001.json through Map010.json at most — early maps have the most " "story-critical dialogue.\n" "\n" " 3. Stop adding entries once you stop finding new names/terms — do not pad.\n" "\n" "Produce TWO sections of output.\n" "\n" "Output the results EXACTLY in the format shown below, including the category headers " "starting with #. Do not add any other text, preamble, or explanation outside the entries.\n" "\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "# Game Characters\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "Identify every NAMED CHARACTER that appears in dialogue or descriptions.\n" "For each character provide:\n" " - Japanese name (katakana/kanji as it appears in-game)\n" " - English transliteration or translation\n" " - Gender (Male / Female / Unknown) — infer from speech patterns or pronouns\n" " - Role (protagonist, antagonist, NPC, etc.)\n" " - Speech register and personality notes — how they speak, their tone, any nicknames, " "whether their name is player-chosen, etc.\n" "\n" "⚠️ IMPORTANT: In all descriptions, ALWAYS refer to other characters by their " "ENGLISH name, never their Japanese name. " "For example, write \"her sister Ruin was kidnapped\" not " "\"her sister ルイン was kidnapped\". " "The description text must be entirely in English with no Japanese characters " "except inside the leading Japanese (English) name tag.\n" "\n" "Format — the header line, then one entry per line:\n" "# Game Characters\n" "\u30b7\u30ed (Shiro) - Female; protagonist; player-controlled (Actors.json ID 1); " "speaks in a flustered, cute register with feminine speech markers; " "her partner is Kurone\n" "\u30af\u30ed\u30cd (Kurone) - Female; antagonist; cold and terse; speaks in short cutting sentences; " "pursues Shiro throughout the story\n" "\n" "Rules:\n" " - Named characters only — no generic enemy types or unnamed NPCs.\n" " - If a character has a player-chosen name (e.g. Actors.json id 1), note it explicitly.\n" "\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "# Worldbuilding Terms\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "Identify lore-specific terms that appear in dialogue, descriptions, or narration " "but do NOT have a dedicated database file — i.e. terms the translation tool will NOT " "auto-populate from Skills.json, Items.json, Armors.json, Weapons.json, etc.\n" "\n" "Target specifically:\n" " - Faction / organisation names (kingdoms, guilds, cults, nations)\n" " - Location names mentioned in dialogue but not map titles\n" " - Unique magic systems, schools of magic, or power classifications\n" " - Lore titles and honorifics unique to this setting\n" " - Recurring in-universe concepts or proper nouns with no English equivalent\n" "\n" "Format — the header line, then one entry per line:\n" "# Worldbuilding Terms\n" "\u9b54\u4e16\u754c (Demon World) - The demonic realm referenced in NPC dialogue; not a named map\n" "\u8056\u5263\u6559\u56e3 (Holy Blade Order) - Antagonist faction controlling the eastern territories\n" "\n" "Rules:\n" " - Do NOT list skill names, item names, weapon names, armour names — the tool handles those.\n" " - Skip generic RPG words (\u30dd\u30fc\u30b7\u30e7\u30f3, \u30ec\u30d9\u30eb, \u30b9\u30c6\u30fc\u30bf\u30b9, etc.).\n" " - Do NOT repeat character names here.\n" "\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "COMPLETE EXAMPLE OUTPUT\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "Below is what a correct, well-formed response looks like.\n" "Your output should follow this structure exactly:\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" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "GLOBAL RULES (apply to both sections)\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" " - NEVER give two options for any term (e.g. 'Sylfia / Sylphia' is wrong). " "Always commit to a single best translation. If multiple transliterations exist, " "pick the most etymologically accurate or natural-sounding one and use only that.\n" " - Use a plain hyphen-minus (-) as the separator between the Japanese entry and " "its description. Never use an em dash (\u2014) or en dash (\u2013) \u2014 the " "translation tool only recognises the plain hyphen." ) _WRAP_PROMPT = ( "You are helping me configure text-wrap widths for a Japanese RPGMaker MV/MZ translation tool.\n" "\n" "The tool wraps translated English text using a character-count limit (not pixels).\n" "I need three values:\n" "\n" " width — main dialogue / message box (Show Text)\n" " listWidth — item / skill / help description windows\n" " noteWidth — database note fields (item, weapon, armour, skill descriptions)\n" "\n" "To calculate them:\n" " 1. Read screenWidth and fontSize from System.json.\n" " Check js/plugins.js for any MessageCore or Window plugin that overrides these.\n" " 2. For each window type, estimate its pixel width, subtract ~48px padding, then:\n" " chars = floor(content_px / (font_size × 0.58))\n" " listWidth window is usually full or half screen width.\n" " noteWidth window is usually the narrowest description pane (~40–50% screen width).\n" " 3. If the font size is above 26px and reducing it would meaningfully increase\n" " characters per line, note where to change it (System.json or plugin parameter).\n" "\n" "Do not show calculations. Just give me the final answer in this exact format:\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" ) _PLUGINS_JS_TRANSLATE_PROMPT = ( "You are helping me translate a Japanese RPGMaker MV/MZ game.\n" "I need you to translate visible Japanese strings inside js/plugins.js\n" "without breaking any game logic or plugin functionality.\n" "\n" "A vocab.txt glossary file has been attached. Use it as your primary reference\n" "for character names, worldbuilding terms, and their approved English translations.\n" "Any Japanese name or term that appears in the glossary must be translated\n" "exactly as specified there — do not invent alternative spellings.\n" "\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "## WHAT TO TRANSLATE\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "Only translate string values that are directly shown to the player at runtime.\n" "These are typically plugin parameters with Japanese text such as:\n" " - Menu/button labels displayed 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" "## WHAT MUST NOT BE TRANSLATED\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "CRITICAL — translating the following will break the game:\n" "\n" " 1. Plugin parameter KEYS (object property names).\n" " Example: { \"CommandName\": \"セーブ\" }\n" " → translate \"セーブ\" but NOT \"CommandName\"\n" "\n" " 2. Strings used as internal identifiers or lookup keys:\n" " - Switch/variable names that match System.json entries\n" " - Actor, skill, item, weapon, armour names if they are used\n" " as keys inside other plugin parameters (e.g. skill filtering lists)\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" " Example: \"img/faces/Actor1\" or \"#ffffff\" — do NOT translate.\n" "\n" " 4. Plugin names and script identifiers:\n" " - The \"name\" property at the top of each plugin block\n" " (e.g. { \"name\": \"YEP_CoreEngine\" }) — never translate\n" " - Function identifiers, class names, JS event names\n" "\n" " 5. Any string that also appears in the game data files (Actors.json,\n" " Skills.json, Items.json, etc.) AND is used as a lookup key in the\n" " plugin — changing it here would desync it from the data file.\n" "\n" " 6. Boolean strings, numeric strings, regex patterns.\n" "\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "## HOW TO IDENTIFY SAFE STRINGS\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "Before translating a string, ask yourself:\n" " ✔ Is this value ever displayed directly to the player as text?\n" " ✔ Is it purely a UI label, not a key used anywhere else?\n" " ✔ Does nothing in the codebase compare this exact string to another value?\n" "\n" "If all three are YES, it is safe to translate.\n" "When in doubt, SKIP IT — untranslated Japanese is better than a broken game.\n" "\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "## OUTPUT FORMAT\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "Provide the translated file as a complete replacement of js/plugins.js.\n" "Only change the string values identified above.\n" "Preserve all original formatting, indentation, comments, and structure.\n" "\n" "After the file, output a translation summary:\n" "\n" "### Translations Made\n" "List each change in this format:\n" " Plugin: \n" " Parameter: \n" " Before: \n" " After: \n" "\n" "### Skipped (Ambiguous or Internal)\n" "List any Japanese strings you detected but chose NOT to translate, with a one-line reason.\n" ) _ACE_SCRIPTS_TRANSLATE_PROMPT = ( "You are helping me translate a Japanese RPGMaker Ace (VX Ace) game.\n" "The game's scripts are in the ace_json/scripts/ folder as .rb files.\n" "I need you to translate visible Japanese strings inside these Ruby scripts\n" "without breaking any game logic or script functionality.\n" "\n" "A vocab.txt glossary file has been attached. Use it as your primary reference\n" "for character names, worldbuilding terms, and their approved English translations.\n" "Any Japanese name or term that appears in the glossary must be translated\n" "exactly as specified there — do not invent alternative spellings.\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" "## HOW TO IDENTIFY SAFE STRINGS\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "Before translating a string, ask yourself:\n" " \u2714 Is this value ever displayed directly to the player as text?\n" " \u2714 Is it purely a display string, not compared or looked up anywhere?\n" " \u2714 Would changing it break no conditional logic or data lookup?\n" "\n" "If all three are YES, it is safe to translate.\n" "When in doubt, SKIP IT — untranslated Japanese is better than a broken game.\n" "\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "## OUTPUT FORMAT\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "For each .rb file that needed changes, provide the full translated file content.\n" "Only change the string values identified as safe above.\n" "Preserve all Ruby syntax, indentation, comments, and structure exactly.\n" "\n" "After all files, output a translation summary:\n" "\n" "### Translations Made\n" "List each change in this format:\n" " File: