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(
+ "
"
+ "- Run this after exporting translations so the game files are up to date.
"
+ "- Overlay save to file reloads instantly; VSCode saves reload once the file is stable on disk
"
+ "- F9 — force reload database + current map
"
+ "- F10 — toggle the inspector panel
"
+ "- Click a source location to open in VSCode; click edit to change text in-game
"
+ "- Remove the plugin before shipping a release build
"
+ "
"
+ )
+ 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."