Credits
This commit is contained in:
parent
7d79b8a3ec
commit
8e5ae829b3
5 changed files with 407 additions and 3 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <b>TL Inspector</b> into your RPG Maker MV/MZ game for playtesting. "
|
||||
"Press <b>F10</b> in-game to see which source file each line of text comes from, "
|
||||
"open it in VSCode, or <b>edit the text live</b> 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(
|
||||
"<ul style='margin:4px 0;padding-left:18px;color:#9d9d9d;font-size:12px;'>"
|
||||
"<li>Run this <b>after exporting</b> translations so the game files are up to date.</li>"
|
||||
"<li>Overlay <b>save to file</b> reloads instantly; VSCode saves reload once the file is stable on disk</li>"
|
||||
"<li><b>F9</b> — force reload database + current map</li>"
|
||||
"<li><b>F10</b> — toggle the inspector panel</li>"
|
||||
"<li>Click a source location to open in VSCode; click <b>edit</b> to change text in-game</li>"
|
||||
"<li>Remove the plugin before shipping a release build</li>"
|
||||
"</ul>"
|
||||
)
|
||||
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()
|
||||
|
|
|
|||
112
util/tl_inspector/TLInspector.bat
Normal file
112
util/tl_inspector/TLInspector.bat
Normal file
|
|
@ -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
|
||||
5
util/tl_inspector/__init__.py
Normal file
5
util/tl_inspector/__init__.py
Normal file
|
|
@ -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"]
|
||||
135
util/tl_inspector/installer.py
Normal file
135
util/tl_inspector/installer.py
Normal file
|
|
@ -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."
|
||||
Loading…
Reference in a new issue