diff --git a/README.md b/README.md index af86efb..6ed629c 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ An AI-powered game translation tool with a GUI. Translate RPG Maker, Ren'Py, Tyr ## Credits - **[Sinflower](https://github.com/Sinflower)** — [RV2JSON](https://github.com/Sinflower/RV2JSON) — enables RPGMaker Ace games to be translated the same way as MV/MZ by converting rvdata2 files to JSON and back. +- **Sakura & kaoss** — TL Inspector (`util/tl_inspector/`) — in-game translation source inspector and live-edit plugin for RPG Maker MV/MZ playtesting. ## Table of Contents diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index e4500e5..ce45ea3 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -7,9 +7,11 @@ Provides a guided, step-by-step interface: 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 + 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 @@ -560,6 +562,7 @@ class WorkflowTab(QWidget): ("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: @@ -714,6 +717,8 @@ class WorkflowTab(QWidget): if index == 5: self._populate_p2_checkboxes() + if index == 8: + self._refresh_tl_inspector_status() def _register_import_button(self, button: QPushButton) -> None: self._import_buttons.append(button) @@ -2398,6 +2403,145 @@ class WorkflowTab(QWidget): row.addStretch() layout.addLayout(row) + # ── Step 8: Playtest (TL Inspector) ───────────────────────────────────── + + def _build_step8_playtest(self, layout: QVBoxLayout): + self._step8_section_label = _make_section_label("Step 8 — Playtest with TL Inspector") + layout.addWidget(self._step8_section_label) + + hint = QLabel( + "Install TL Inspector into your RPG Maker MV/MZ game for playtesting. " + "Press F10 in-game to see which source file each line of text comes from, " + "open it in VSCode, or edit the text live and save directly to the JSON file." + ) + hint.setWordWrap(True) + hint.setTextFormat(Qt.RichText) + hint.setStyleSheet("color:#9d9d9d;font-size:13px;padding-bottom:4px;") + layout.addWidget(hint) + + credits = QLabel("Idea by Sakura · Plugin by kaoss") + credits.setStyleSheet("color:#6a6a6a;font-size:11px;font-style:italic;padding-bottom:6px;") + layout.addWidget(credits) + + box = QWidget() + box.setObjectName("tbox") + box.setStyleSheet(self._task_box_style()) + inner = QVBoxLayout(box) + inner.setContentsMargins(10, 8, 10, 8) + inner.setSpacing(6) + + self._tli_status_label = QLabel("Status: (detect a project folder first)") + self._tli_status_label.setWordWrap(True) + self._tli_status_label.setStyleSheet("color:#7a7a7a;font-size:13px;") + inner.addWidget(self._tli_status_label) + + tips = QLabel( + "" + ) + tips.setWordWrap(True) + tips.setTextFormat(Qt.RichText) + inner.addWidget(tips) + + btn_row = QHBoxLayout() + btn_row.setSpacing(8) + _BTN_W = 200 + self._tli_install_btn = _make_btn("⬇ Install TL Inspector", "#3a7a3a") + self._tli_install_btn.setFixedWidth(_BTN_W) + self._tli_install_btn.clicked.connect(self._install_tl_inspector) + btn_row.addWidget(self._tli_install_btn) + + self._tli_uninstall_btn = _make_btn("⬆ Uninstall TL Inspector", "#7a3a3a") + self._tli_uninstall_btn.setFixedWidth(_BTN_W) + self._tli_uninstall_btn.clicked.connect(self._uninstall_tl_inspector) + btn_row.addWidget(self._tli_uninstall_btn) + btn_row.addStretch() + inner.addLayout(btn_row) + + layout.addWidget(box) + self._step8_playtest_box = box + + def _refresh_tl_inspector_status(self): + """Update Step 8 status label from the current game folder.""" + label = getattr(self, "_tli_status_label", None) + if label is None: + return + game_root = self.folder_edit.text().strip() + if not game_root: + label.setText("Status: no game folder set — complete Step 0 first.") + label.setStyleSheet("color:#7a7a7a;font-size:13px;") + return + try: + from util.tl_inspector.installer import status + st = status(Path(game_root)) + except Exception as exc: + label.setText(f"Status: error — {exc}") + label.setStyleSheet("color:#f48771;font-size:13px;") + return + + if not st.get("ok"): + label.setText(f"Status: {st.get('message', 'unsupported')}") + label.setStyleSheet("color:#e9a12a;font-size:13px;") + return + + engine = st.get("engine", "?") + msg = st.get("message", "") + parts = [f"RPG Maker {engine}", msg] + if st.get("declared") and st.get("plugin_file"): + detail = "plugin declared in plugins.js and file present" + elif st.get("declared"): + detail = "declared in plugins.js (plugin file missing)" + elif st.get("plugin_file"): + detail = "plugin file present (not declared in plugins.js)" + else: + detail = "not installed" + label.setText(f"Status: {' · '.join(parts)} — {detail}") + color = "#6a9a6a" if st.get("declared") and st.get("plugin_file") else "#9d9d9d" + label.setStyleSheet(f"color:{color};font-size:13px;") + + def _install_tl_inspector(self): + game_root = self.folder_edit.text().strip() + if not game_root: + self._log("⚠ No game folder set. Complete Step 0 first.") + return + try: + from util.tl_inspector.installer import install + ok, msg = install(Path(game_root)) + except Exception as exc: + self._log(f"❌ TL Inspector install failed: {exc}") + return + self._log(("✅ " if ok else "❌ ") + msg) + self._refresh_tl_inspector_status() + + def _uninstall_tl_inspector(self): + game_root = self.folder_edit.text().strip() + if not game_root: + self._log("⚠ No game folder set. Complete Step 0 first.") + return + reply = QMessageBox.question( + self, + "Uninstall TL Inspector", + "Remove TLInspector from plugins.js and delete the plugin file?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + if reply != QMessageBox.Yes: + return + try: + from util.tl_inspector.installer import uninstall + ok, msg = uninstall(Path(game_root)) + except Exception as exc: + self._log(f"❌ TL Inspector uninstall failed: {exc}") + return + self._log(("✅ " if ok else "❌ ") + msg) + self._refresh_tl_inspector_status() + # ───────────────────────────────────────────────────────────────────────── # Step 0 – Project Folder logic # ───────────────────────────────────────────────────────────────────────── @@ -2501,6 +2645,13 @@ class WorkflowTab(QWidget): "Copy a prompt that instructs Copilot/Cursor to translate only " "visible player-facing strings in plugins.js, using vocab.txt as a glossary." ) + # Step 8 — TL Inspector (MV/MZ only) + if hasattr(self, "_step_tabs") and self._step_tabs.count() > 8: + self._step_tabs.setTabEnabled(8, not is_ace) + box = getattr(self, "_step8_playtest_box", None) + if box is not None: + box.setEnabled(not is_ace) + self._refresh_tl_inspector_status() def _detect_folder(self): folder = self.folder_edit.text().strip() diff --git a/util/tl_inspector/TLInspector.bat b/util/tl_inspector/TLInspector.bat new file mode 100644 index 0000000..c8b4747 --- /dev/null +++ b/util/tl_inspector/TLInspector.bat @@ -0,0 +1,112 @@ +@echo off +rem ============================================================================ +rem TLInspector installer / uninstaller (RPG Maker MV & MZ) +rem Idea by Sakura · Plugin by kaoss +rem Put this file and TLInspector.js in the GAME ROOT (next to the .exe / +rem index.html), then double-click this file. +rem - Not installed yet -> installs it (moves the plugin into the plugins +rem folder and declares it at the end of plugins.js). +rem - Already installed -> asks whether to remove it. +rem ============================================================================ +chcp 65001 >nul +setlocal +set "TLI_ROOT=%~dp0" +powershell -NoProfile -ExecutionPolicy Bypass -Command "$l=Get-Content -LiteralPath '%~f0'; $i=[Array]::IndexOf($l,'#:PSSTART'); Invoke-Expression (($l[($i+1)..($l.Count-1)]) -join [char]10)" +endlocal +exit /b +#:PSSTART +$ErrorActionPreference = 'Stop' +try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch {} +function PauseExit { [void](Read-Host "`nPress Enter to exit"); exit } + +$root = $env:TLI_ROOT +Write-Host "============================================" +Write-Host " TLInspector installer" +Write-Host " Idea by Sakura · Plugin by kaoss" +Write-Host "============================================" + +# --- Detect engine / locate plugins.js (MV uses www\, MZ does not) ---------- +$mvJs = Join-Path $root 'www\js\plugins.js' +$mzJs = Join-Path $root 'js\plugins.js' +if (Test-Path -LiteralPath $mvJs) { + $engine = 'MV'; $pluginsJs = $mvJs; $pluginsDir = Join-Path $root 'www\js\plugins' +} elseif (Test-Path -LiteralPath $mzJs) { + $engine = 'MZ'; $pluginsJs = $mzJs; $pluginsDir = Join-Path $root 'js\plugins' +} else { + Write-Host "" + Write-Host "ERROR: No RPG Maker MV/MZ game found here." -ForegroundColor Red + Write-Host "Could not find 'www\js\plugins.js' (MV) or 'js\plugins.js' (MZ)." + Write-Host "Place this installer and TLInspector.js in the game ROOT folder" + Write-Host "(the one containing the .exe / index.html) and run it again." + PauseExit +} +Write-Host ("Detected RPG Maker {0}" -f $engine) +Write-Host ("plugins list: {0}" -f $pluginsJs) + +$target = Join-Path $pluginsDir 'TLInspector.js' +$srcRoot = Join-Path $root 'TLInspector.js' +$utf8 = New-Object System.Text.UTF8Encoding($false) # no BOM +$content = [IO.File]::ReadAllText($pluginsJs) +$nl = if ($content -match "`r`n") { "`r`n" } else { "`n" } +$declared = [regex]::IsMatch($content, '"name"\s*:\s*"TLInspector"') +$fileThere = Test-Path -LiteralPath $target + +# --- Already installed -> offer to remove ----------------------------------- +if ($declared -or $fileThere) { + Write-Host "" + Write-Host "TLInspector is currently INSTALLED." -ForegroundColor Yellow + $ans = Read-Host "Remove it? (Y/N)" + if ($ans -match '^(y|yes)$') { + if ($declared) { + $kept = ($content -split "`r?`n") | Where-Object { $_ -notmatch '"name"\s*:\s*"TLInspector"' } + $newText = ($kept -join $nl) + # drop a now-dangling comma before the closing ]; + $newText = [regex]::Replace($newText, ',(\s*)\];', '$1];') + [IO.File]::WriteAllText($pluginsJs, $newText, $utf8) + Write-Host "Removed the TLInspector entry from plugins.js" + } + if ($fileThere) { + Remove-Item -LiteralPath $target -Force + Write-Host ("Deleted {0}" -f $target) + } + Write-Host "" + Write-Host "Uninstalled." -ForegroundColor Green + } else { + Write-Host "Cancelled - nothing changed." + } + PauseExit +} + +# --- Not installed -> install ----------------------------------------------- +if (-not (Test-Path -LiteralPath $srcRoot)) { + Write-Host "" + Write-Host "ERROR: TLInspector.js was not found next to this installer." -ForegroundColor Red + Write-Host ("Expected: {0}" -f $srcRoot) + Write-Host "Copy TLInspector.js into the game root and run this again." + PauseExit +} +if (-not (Test-Path -LiteralPath $pluginsDir)) { + New-Item -ItemType Directory -Path $pluginsDir -Force | Out-Null +} +Move-Item -LiteralPath $srcRoot -Destination $target -Force +Write-Host ("Moved plugin -> {0}" -f $target) + +# Insert the declaration as the last entry, just before the closing ]; +$entry = ' { "name": "TLInspector", "status": true, "description": "TL source inspector", "parameters": {} }' +$idx = $content.LastIndexOf('];') +if ($idx -lt 0) { + Write-Host "" + Write-Host "ERROR: could not find the end of the plugin list ( ]; ) in plugins.js." -ForegroundColor Red + Write-Host "The plugin file was moved; please add this line before the ']' yourself:" + Write-Host $entry + PauseExit +} +$before = $content.Substring(0, $idx).TrimEnd() +$after = $content.Substring($idx) # starts at ]; +$sep = if ($before.EndsWith(',')) { $nl } else { ',' + $nl } +$newText = $before + $sep + $entry + $nl + ' ' + $after +[IO.File]::WriteAllText($pluginsJs, $newText, $utf8) +Write-Host "Declared TLInspector at the end of plugins.js" +Write-Host "" +Write-Host "Installed! Start the game, press F10 to open the inspector, and use edit to save text changes in-game." -ForegroundColor Green +PauseExit diff --git a/util/tl_inspector/__init__.py b/util/tl_inspector/__init__.py new file mode 100644 index 0000000..8338591 --- /dev/null +++ b/util/tl_inspector/__init__.py @@ -0,0 +1,5 @@ +"""TL Inspector install helpers for RPG Maker MV/MZ (Idea: Sakura · Plugin: kaoss).""" + +from util.tl_inspector.installer import bundled_plugin_path, detect_engine, install, status, uninstall + +__all__ = ["bundled_plugin_path", "detect_engine", "install", "status", "uninstall"] diff --git a/util/tl_inspector/installer.py b/util/tl_inspector/installer.py new file mode 100644 index 0000000..409e6b7 --- /dev/null +++ b/util/tl_inspector/installer.py @@ -0,0 +1,135 @@ +"""Install / uninstall TLInspector into an RPG Maker MV or MZ game folder. + +Credits: Idea by Sakura · Plugin by kaoss +""" + +from __future__ import annotations + +import re +import shutil +from pathlib import Path + +PLUGIN_NAME = "TLInspector" +PLUGIN_ENTRY = ( + ' { "name": "TLInspector", "status": true, ' + '"description": "TL source inspector", "parameters": {} }' +) + +_PKG_ROOT = Path(__file__).resolve().parent +DEFAULT_PLUGIN_SRC = _PKG_ROOT / "TLInspector.js" + + +def bundled_plugin_path() -> Path: + return DEFAULT_PLUGIN_SRC + + +def detect_engine(game_root: Path) -> tuple[str, Path, Path] | None: + """Return (engine, plugins_js, plugins_dir) or None if not MV/MZ.""" + root = Path(game_root) + mv_js = root / "www" / "js" / "plugins.js" + mz_js = root / "js" / "plugins.js" + if mv_js.is_file(): + return "MV", mv_js, root / "www" / "js" / "plugins" + if mz_js.is_file(): + return "MZ", mz_js, root / "js" / "plugins" + return None + + +def _read_plugins_js(plugins_js: Path) -> tuple[str, str]: + content = plugins_js.read_text(encoding="utf-8") + nl = "\r\n" if "\r\n" in content else "\n" + return content, nl + + +def _is_declared(content: str) -> bool: + return bool(re.search(r'"name"\s*:\s*"TLInspector"', content)) + + +def status(game_root: Path) -> dict: + """Return install state for the game folder.""" + info = detect_engine(game_root) + if info is None: + return { + "ok": False, + "engine": None, + "installed": False, + "declared": False, + "plugin_file": None, + "message": "No RPG Maker MV/MZ game found (missing plugins.js).", + } + engine, plugins_js, plugins_dir = info + target = plugins_dir / f"{PLUGIN_NAME}.js" + content, _ = _read_plugins_js(plugins_js) + declared = _is_declared(content) + file_there = target.is_file() + return { + "ok": True, + "engine": engine, + "installed": declared or file_there, + "declared": declared, + "plugin_file": file_there, + "plugins_js": str(plugins_js), + "target": str(target), + "message": ( + "Installed" + if declared and file_there + else "Partially installed" + if declared or file_there + else "Not installed" + ), + } + + +def install(game_root: Path, source_js: Path | None = None) -> tuple[bool, str]: + """Copy TLInspector.js into the game and declare it in plugins.js.""" + info = detect_engine(game_root) + if info is None: + return False, "No RPG Maker MV/MZ game found at that path." + + engine, plugins_js, plugins_dir = info + src = Path(source_js) if source_js else DEFAULT_PLUGIN_SRC + if not src.is_file(): + return False, f"TLInspector.js not found: {src}" + + target = plugins_dir / f"{PLUGIN_NAME}.js" + content, nl = _read_plugins_js(plugins_js) + + plugins_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, target) + + if not _is_declared(content): + idx = content.rfind("];") + if idx < 0: + return False, "Could not find plugin list end ( ]; ) in plugins.js." + before = content[:idx].rstrip() + after = content[idx:] + sep = nl if before.endswith(",") else "," + nl + content = before + sep + PLUGIN_ENTRY + nl + " " + after + plugins_js.write_text(content, encoding="utf-8", newline="") + + return True, f"TLInspector installed for RPG Maker {engine}. Press F10 in-game to open." + + +def uninstall(game_root: Path) -> tuple[bool, str]: + """Remove TLInspector from plugins.js and delete the plugin file.""" + info = detect_engine(game_root) + if info is None: + return False, "No RPG Maker MV/MZ game found at that path." + + _, plugins_js, plugins_dir = info + target = plugins_dir / f"{PLUGIN_NAME}.js" + content, nl = _read_plugins_js(plugins_js) + + if _is_declared(content): + kept = [ + line for line in re.split(r"\r?\n", content) + if not re.search(r'"name"\s*:\s*"TLInspector"', line) + ] + new_text = nl.join(kept) + new_text = re.sub(r",(\s*)\];", r"\1];", new_text) + plugins_js.write_text(new_text, encoding="utf-8", newline="") + + if target.is_file(): + target.unlink() + + return True, "TLInspector uninstalled."