Some fixes to workflow wolf
This commit is contained in:
parent
014278b339
commit
4114cfec93
4 changed files with 359 additions and 35 deletions
|
|
@ -324,8 +324,8 @@ Open the **Workflow** tab and choose **Wolf RPG (WolfDawn)** from the engine sel
|
|||
|
||||
| Step | Action |
|
||||
|------|--------|
|
||||
| **0 Project** | Select the game folder, **Unpack** the `.wolf` archives into a loose `Data/` folder, then **Extract text** (maps, common events, databases, `Game.dat`, external event text, and the project-wide name glossary). Everything is staged into `files/` for translation. |
|
||||
| **1 Glossary** | Translate the name glossary (`names.json`) first. WOLF references item/skill/enemy names by value across many files, so doing names first keeps them consistent. |
|
||||
| **0 Project** | Select the game folder, **Unpack** the `.wolf` archives into a loose `Data/` folder, then **Extract text** (maps, common events, databases, `Game.dat`, external event text, and the project-wide name glossary). Everything is staged into `files/` for translation. Finally, **Set up git tracking** by copying the `gameupdate/` folder (updater scripts, `patch.sh`/`patch.ps1`, and a `.gitignore` that excludes the original game files) into the game root, so the translation project can be tracked with git and later shipped as a git-based patch players apply themselves. |
|
||||
| **1 Glossary** | Build both glossaries before translating. **1a - Character & Worldbuilding Glossary (`vocab.txt`):** copy the WOLF-tailored prompt into Cursor/Copilot with the extracted `files/` JSON, let it discover character names, speech registers, and lore terms, then paste the result into the in-tab editor and save (the shared `vocab.txt` is used by every translation batch to keep names and voice consistent). **1b - Name Value Glossary (`names.json`):** translate the name glossary first, since WOLF references item/skill/enemy names by value across many files. |
|
||||
| **2 Translate** | Run the `Wolf RPG (WolfDawn)` module over `files/`. Only the `text` fields are filled in; `source` is preserved so injection can verify each line. |
|
||||
| **3 Inject** | Write the translations back into the `Data/` binaries. The name glossary is applied consistently across every file. Toggle `--en-punct` (convert Japanese punctuation to ASCII) and `--allow-code-drift` (relax the inline-code guard) as needed. Use **Check name consistency** to catch names translated differently across files. |
|
||||
| **4 Package** | Either run from the loose `Data/` folder (backs up `Data.wolf` → `Data.wolf.bak`), or **Repack** a fresh `Data.wolf`, inheriting the original archive's encryption. |
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ Mirrors the RPGMaker WorkflowTab, driven by the vendored WolfDawn ``wolf`` CLI
|
|||
(see util/wolfdawn):
|
||||
|
||||
Step 0 Project - select game folder, unpack .wolf archives, extract text
|
||||
(strings-extract per file + names-extract) into files/
|
||||
Step 1 Glossary - translate the project-wide name glossary first
|
||||
(strings-extract per file + names-extract) into files/, and
|
||||
copy the gameupdate/ patch files (incl. .gitignore) for git tracking
|
||||
Step 1 Glossary - build vocab.txt (characters/terms) and translate the
|
||||
project-wide name-value glossary first
|
||||
Step 2 Translate - run the "Wolf RPG (WolfDawn)" module over files/
|
||||
Step 3 Inject - inject translations + names back into the Data/ binaries
|
||||
Step 4 Package - run from a loose Data/ folder, or repack Data.wolf
|
||||
|
|
@ -26,7 +28,9 @@ from pathlib import Path
|
|||
from PyQt5.QtCore import Qt, QSettings, QThread, QTimer, pyqtSignal
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QFileDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
|
|
@ -41,13 +45,109 @@ from PyQt5.QtWidgets import (
|
|||
QWidget,
|
||||
)
|
||||
|
||||
from gui.translation_tab import (
|
||||
BATCH_COLLECT_LIVE_CHARGE_NOTE,
|
||||
BATCH_MODE_BENEFIT_NOTE,
|
||||
BATCH_MODE_LABEL,
|
||||
)
|
||||
from gui.workflow_tab import _make_btn, _make_hr, _make_section_label
|
||||
from util.project_scanner import detect_wolf_layout
|
||||
from util.vocab import read_game_vocab, write_game_vocab
|
||||
|
||||
# Workflow-level label for the non-batch (live) translation path. The Translation
|
||||
# tab's own mode is called "Translate"; _workflow_mode_text() maps to that.
|
||||
_TL_NORMAL_LABEL = "Normal Translate"
|
||||
|
||||
MANIFEST_NAME = "manifest.json"
|
||||
NAMES_JSON = "names.json"
|
||||
WORK_DIR_NAME = "wolf_json"
|
||||
|
||||
# Glossary-discovery prompt for Copilot / Cursor, tailored to WolfDawn's extracted
|
||||
# JSON (source/text pairs staged in files/). Produces the same two-section format
|
||||
# the shared vocab.txt expects, so the AI output pastes straight into the editor.
|
||||
_WOLF_GLOSSARY_PROMPT = (
|
||||
"You are an expert Japanese WOLF RPG Editor game analyst building a translation glossary.\n"
|
||||
"\n"
|
||||
"<task>\n"
|
||||
"Extract named characters and lore-specific worldbuilding terms from this game's extracted "
|
||||
"text. Produce a structured glossary in the exact format specified below. It will be loaded "
|
||||
"directly into a translation tool, so strict format compliance is required.\n"
|
||||
"</task>\n"
|
||||
"\n"
|
||||
"--- attach the extracted JSON in files/ here before continuing ---\n"
|
||||
"\n"
|
||||
"<data_format>\n"
|
||||
"The files/ folder holds WolfDawn extractions as JSON. Each file is a list of entries with a\n"
|
||||
"'source' field (the original Japanese) and a 'text' field (initially empty/identical).\n"
|
||||
"Analyse the 'source' fields only. The files are:\n"
|
||||
" - DataBase.project.json, CDataBase.project.json, SysDatabase.project.json — databases:\n"
|
||||
" richest, small source of character/actor names, classes, factions, and lore titles.\n"
|
||||
" - CommonEvent.dat.json — common events: dialogue and system text.\n"
|
||||
" - Game.dat.json — game/system strings (title, terms).\n"
|
||||
" - <Map>.mps.json — per-map events: the main story dialogue (can be large).\n"
|
||||
" - Evtext.json — external event text, when present.\n"
|
||||
" - names.json — item/skill/enemy value names (the tool handles these; do NOT list them).\n"
|
||||
"</data_format>\n"
|
||||
"\n"
|
||||
"<file_strategy>\n"
|
||||
"Map files can be extremely large. Do NOT read them sequentially — you will hit context "
|
||||
"limits. Instead:\n"
|
||||
"1. Read the DataBase.project.json files IN FULL first — small and the best source of names.\n"
|
||||
"2. For large map files, SEARCH (grep) the 'source' fields instead of reading sequentially. "
|
||||
"Prioritise dialogue because it is the best evidence for character voice:\n"
|
||||
" - Speaker patterns such as 【Name】, [Name], Name「…」, Name:, and <Name> at the start of a line\n"
|
||||
" - Katakana clusters or kanji compound proper nouns that recur across dialogue\n"
|
||||
" Scan the lowest-numbered maps first — early maps have the most story content.\n"
|
||||
"3. Stop once you stop finding new names or terms. Do not pad the output.\n"
|
||||
"</file_strategy>\n"
|
||||
"\n"
|
||||
"<rules>\n"
|
||||
"Apply to both sections:\n"
|
||||
"- Separator: use a plain hyphen-minus (-). Never use an em dash or en dash. "
|
||||
"The translation tool only recognises the plain hyphen.\n"
|
||||
"- Descriptions must be entirely in English. Refer to other characters by English name only.\n"
|
||||
"- Never give two spelling options (e.g. 'Sylfia / Sylphia' is wrong). Commit to one translation.\n"
|
||||
"\n"
|
||||
"# Game Characters — rules:\n"
|
||||
"- Discover named characters from the databases and dialogue. Skip unnamed NPCs, generic "
|
||||
"enemies, and narration-only entries.\n"
|
||||
"- For each character include: gender, role, speech register, and personality. Note if the "
|
||||
"name is player-chosen (a hero/actor whose name comes from a variable or input prompt).\n"
|
||||
"\n"
|
||||
"# Worldbuilding Terms — rules:\n"
|
||||
"- Include: faction/organisation names, locations mentioned in dialogue but not on maps, "
|
||||
"unique magic systems, lore titles, recurring in-universe concepts.\n"
|
||||
"- Exclude: skill names, item names, weapon/armour names (the tool handles those via "
|
||||
"names.json). Skip generic RPG words. Do not repeat character names here.\n"
|
||||
"</rules>\n"
|
||||
"\n"
|
||||
"<output_format>\n"
|
||||
"Return the ENTIRE glossary inside ONE fenced code block (a ``` block), with nothing before "
|
||||
"or after it, so it can be copied in a single click and pasted straight into the tool.\n"
|
||||
"Inside the code block, output EXACTLY two sections with these headers. Do not add any "
|
||||
"preamble, explanation, or text outside the entries.\n"
|
||||
"\n"
|
||||
"# Game Characters\n"
|
||||
"# Worldbuilding Terms\n"
|
||||
"\n"
|
||||
"Entry format: Japanese (English) - description\n"
|
||||
"\n"
|
||||
"<example>\n"
|
||||
"# Game Characters\n"
|
||||
"シロ (Shiro) - Female; protagonist; player-named; speaks in a flustered, cute register "
|
||||
"with feminine speech markers\n"
|
||||
"ゼクス (Zex) - Male; antagonist; cold and commanding; uses an 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 late-game dialogue; "
|
||||
"not a named map location\n"
|
||||
"裁定者 (Arbiter) - Title held by the ruling council; lore-specific rank with no "
|
||||
"real-world equivalent\n"
|
||||
"</example>\n"
|
||||
"</output_format>\n"
|
||||
)
|
||||
|
||||
|
||||
class _WolfTaskWorker(QThread):
|
||||
"""Run a blocking WolfDawn task callable off the UI thread.
|
||||
|
|
@ -270,6 +370,13 @@ class WolfWorkflowTab(QWidget):
|
|||
lbl.setStyleSheet("color:#9d9d9d;font-size:12px;background:transparent;")
|
||||
return lbl
|
||||
|
||||
def _subheading(self, text: str) -> QLabel:
|
||||
lbl = QLabel(text)
|
||||
lbl.setStyleSheet(
|
||||
"color:#4ec9b0;font-size:13px;font-weight:bold;background:transparent;padding-top:4px;"
|
||||
)
|
||||
return lbl
|
||||
|
||||
def _register(self, btn: QPushButton) -> QPushButton:
|
||||
self._buttons.append(btn)
|
||||
return btn
|
||||
|
|
@ -375,6 +482,17 @@ class WolfWorkflowTab(QWidget):
|
|||
"project-wide name glossary, then imports them into files/ for translation."
|
||||
))
|
||||
|
||||
gu_btn = self._register(_make_btn("③ Set up git tracking (copy gameupdate/ files)", "#3a3a3a"))
|
||||
gu_btn.clicked.connect(self._apply_gameupdate)
|
||||
layout.addWidget(gu_btn)
|
||||
layout.addWidget(self._desc(
|
||||
"Copies the tool's gameupdate/ folder into the game's root folder: the updater "
|
||||
"scripts (GameUpdate.bat / GameUpdate_linux.sh), the patch scripts (patch.sh / "
|
||||
"patch.ps1), and a .gitignore that excludes the original game files. Do this early so "
|
||||
"you can track the translation project with git and later ship it as a git-based patch "
|
||||
"players apply themselves. Existing files are overwritten."
|
||||
))
|
||||
|
||||
def _browse_folder(self):
|
||||
start = self.folder_edit.text() or str(Path.home())
|
||||
folder = QFileDialog.getExistingDirectory(self, "Select WOLF Game Folder", start)
|
||||
|
|
@ -547,20 +665,91 @@ class WolfWorkflowTab(QWidget):
|
|||
# ── Step 1: Glossary / names ───────────────────────────────────────────────
|
||||
|
||||
def _build_step1_glossary(self, layout: QVBoxLayout):
|
||||
layout.addWidget(_make_section_label("Step 1 · Name Glossary (recommended first)"))
|
||||
layout.addWidget(_make_section_label("Step 1 · Glossary (build before translating)"))
|
||||
layout.addWidget(self._desc(
|
||||
"Two glossaries keep the translation consistent. Build them before Step 2 so the AI "
|
||||
"already knows every character and term while translating."
|
||||
))
|
||||
|
||||
# ── 1a: Character & worldbuilding glossary (vocab.txt) ──
|
||||
layout.addWidget(self._subheading("1a · Character & Worldbuilding Glossary (vocab.txt)"))
|
||||
layout.addWidget(self._desc(
|
||||
"vocab.txt is the project-wide glossary used by every translation batch to keep "
|
||||
"character names, speech register, honorifics, and lore terms consistent. Copy the "
|
||||
"prompt below into Cursor or Copilot Chat with the extracted files/ JSON open, let it "
|
||||
"analyse the game, then paste its output into the editor and save."
|
||||
))
|
||||
prompt_btn = _make_btn("📋 Copy glossary prompt for Copilot / Cursor", "#5a3a7a")
|
||||
prompt_btn.clicked.connect(self._copy_wolf_glossary_prompt)
|
||||
layout.addWidget(prompt_btn)
|
||||
|
||||
fmt = QLabel(
|
||||
"Format: Japanese (English) - description\n"
|
||||
"Example: シロ (Shiro) - Female; protagonist; flustered, cute register"
|
||||
)
|
||||
fmt.setFont(QFont("Consolas", 9))
|
||||
fmt.setWordWrap(True)
|
||||
fmt.setStyleSheet(
|
||||
"color:#569cd6;background-color:#1a1e2a;border:1px solid #2a3a5a;"
|
||||
"padding:5px 10px;border-radius:4px;font-size:12px;border-left:3px solid #2a6a9a;"
|
||||
)
|
||||
layout.addWidget(fmt)
|
||||
|
||||
self.vocab_editor = QTextEdit()
|
||||
self.vocab_editor.setMinimumHeight(120)
|
||||
self.vocab_editor.setFont(QFont("Consolas", 9))
|
||||
self.vocab_editor.setStyleSheet(
|
||||
"QTextEdit{background-color:#252526;color:#d4d4d4;border:1px solid #3c3c3c;"
|
||||
"border-radius:4px;padding:8px;selection-background-color:#264f78;}"
|
||||
)
|
||||
self._reload_vocab()
|
||||
layout.addWidget(self.vocab_editor, 1)
|
||||
|
||||
vrow = QHBoxLayout()
|
||||
save_btn = _make_btn("💾 Save vocab.txt", "#3a7a3a")
|
||||
save_btn.clicked.connect(self._save_vocab)
|
||||
vrow.addWidget(save_btn)
|
||||
reload_btn = _make_btn("↺ Reload", "#555")
|
||||
reload_btn.clicked.connect(self._reload_vocab)
|
||||
vrow.addWidget(reload_btn)
|
||||
vrow.addStretch()
|
||||
layout.addLayout(vrow)
|
||||
|
||||
layout.addWidget(_make_hr())
|
||||
|
||||
# ── 1b: Name-value glossary (names.json) ──
|
||||
layout.addWidget(self._subheading("1b · Name Value Glossary (item / skill / enemy names)"))
|
||||
layout.addWidget(self._desc(
|
||||
"WOLF databases reference item/skill/enemy names by value across many files, so "
|
||||
"translating the glossary first keeps them consistent. This translates only "
|
||||
f"{NAMES_JSON} using the Wolf RPG (WolfDawn) module, then you can inject it in Step 3."
|
||||
f"translating {NAMES_JSON} first keeps those consistent. This translates only "
|
||||
f"{NAMES_JSON} using the Wolf RPG (WolfDawn) module; it is injected in Step 3. "
|
||||
"You can also translate it together with everything else in Step 2, but doing it "
|
||||
"first is the higher-quality path. It uses the translation mode (Normal / Batch) "
|
||||
"selected in Step 2."
|
||||
))
|
||||
btn = self._register(_make_btn("Translate name glossary now", "#007acc"))
|
||||
btn.clicked.connect(lambda: self._navigate_to_translation(only=NAMES_JSON, auto_start=True))
|
||||
layout.addWidget(btn)
|
||||
layout.addWidget(_make_hr())
|
||||
layout.addWidget(self._desc(
|
||||
"Optional: you can also open the glossary in Step 2 together with everything else. "
|
||||
"Doing names first is just the higher-quality path."
|
||||
))
|
||||
|
||||
def _copy_wolf_glossary_prompt(self):
|
||||
try:
|
||||
QApplication.clipboard().setText(_WOLF_GLOSSARY_PROMPT)
|
||||
self._log("📋 Glossary prompt copied. Paste it into Cursor/Copilot with files/ open.")
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not copy glossary prompt: {exc}")
|
||||
|
||||
def _reload_vocab(self):
|
||||
try:
|
||||
self.vocab_editor.setPlainText(read_game_vocab())
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not load vocab.txt: {exc}")
|
||||
|
||||
def _save_vocab(self):
|
||||
try:
|
||||
write_game_vocab(self.vocab_editor.toPlainText())
|
||||
self._log("✅ vocab.txt saved (base terms from vocab_base.txt appended).")
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not save vocab.txt: {exc}")
|
||||
|
||||
# ── Step 2: Translate ──────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -571,6 +760,9 @@ class WolfWorkflowTab(QWidget):
|
|||
"Wolf RPG (WolfDawn) module and starts translating. Only the 'text' fields are "
|
||||
"filled in; 'source' is preserved so injection can verify each line."
|
||||
))
|
||||
|
||||
self._add_tl_mode_selector(layout)
|
||||
|
||||
btn = self._register(_make_btn("Translate all files now", "#00a86b"))
|
||||
btn.clicked.connect(lambda: self._navigate_to_translation(auto_start=True))
|
||||
layout.addWidget(btn)
|
||||
|
|
@ -579,6 +771,48 @@ class WolfWorkflowTab(QWidget):
|
|||
open_btn.clicked.connect(lambda: self._navigate_to_translation(auto_start=False))
|
||||
layout.addWidget(open_btn)
|
||||
|
||||
def _add_tl_mode_selector(self, layout: QVBoxLayout):
|
||||
"""Normal vs Batch selector; applies to the name glossary (1b) and full runs."""
|
||||
row = QHBoxLayout()
|
||||
lbl = QLabel("Translation mode:")
|
||||
lbl.setStyleSheet("color:#cccccc;font-size:12px;font-weight:bold;background:transparent;")
|
||||
self._tl_mode_combo = QComboBox()
|
||||
self._tl_mode_combo.addItem(_TL_NORMAL_LABEL)
|
||||
self._tl_mode_combo.addItem(BATCH_MODE_LABEL)
|
||||
self._tl_mode_combo.setFixedWidth(220)
|
||||
self._tl_mode_combo.setToolTip(
|
||||
"Applies to both the Step 1 name glossary and the full run here.\n"
|
||||
"Normal translates live; Batch uses the Anthropic Batches API (~50% cheaper, Claude only)."
|
||||
)
|
||||
saved = self._setting("tl_mode", BATCH_MODE_LABEL) or BATCH_MODE_LABEL
|
||||
idx = self._tl_mode_combo.findText(str(saved))
|
||||
if idx >= 0:
|
||||
self._tl_mode_combo.setCurrentIndex(idx)
|
||||
self._tl_mode_combo.currentTextChanged.connect(self._on_tl_mode_changed)
|
||||
row.addWidget(lbl)
|
||||
row.addWidget(self._tl_mode_combo)
|
||||
row.addStretch()
|
||||
layout.addLayout(row)
|
||||
|
||||
self._batch_note = QLabel(BATCH_MODE_BENEFIT_NOTE + "\n" + BATCH_COLLECT_LIVE_CHARGE_NOTE)
|
||||
self._batch_note.setWordWrap(True)
|
||||
self._batch_note.setStyleSheet("color:#8fbc8f;font-size:11px;background:transparent;")
|
||||
layout.addWidget(self._batch_note)
|
||||
self._on_tl_mode_changed(self._tl_mode_combo.currentText())
|
||||
|
||||
def _on_tl_mode_changed(self, mode_text: str):
|
||||
is_batch = mode_text == BATCH_MODE_LABEL
|
||||
if hasattr(self, "_batch_note"):
|
||||
self._batch_note.setVisible(is_batch)
|
||||
self._save_setting("tl_mode", mode_text)
|
||||
|
||||
def _workflow_mode_text(self) -> str:
|
||||
"""Map the selector to the Translation tab's own mode label."""
|
||||
combo = getattr(self, "_tl_mode_combo", None)
|
||||
if combo is not None and combo.currentText() == BATCH_MODE_LABEL:
|
||||
return BATCH_MODE_LABEL
|
||||
return "Translate"
|
||||
|
||||
def _navigate_to_translation(self, only: str | None = None, auto_start: bool = False):
|
||||
"""Switch to the Translation tab, select the WolfDawn module, and check files."""
|
||||
pw = self.parent_window
|
||||
|
|
@ -595,6 +829,16 @@ class WolfWorkflowTab(QWidget):
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# Apply the chosen translation mode (after the module, since changing the
|
||||
# module can refresh the mode list).
|
||||
try:
|
||||
mode_combo = tt.mode_combo
|
||||
mode_idx = mode_combo.findText(self._workflow_mode_text())
|
||||
if mode_idx >= 0:
|
||||
mode_combo.setCurrentIndex(mode_idx)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
tt.refresh_file_lists()
|
||||
fl = tt.file_list
|
||||
|
|
@ -769,6 +1013,46 @@ class WolfWorkflowTab(QWidget):
|
|||
"archive's encryption where possible (--like the backed-up original)."
|
||||
))
|
||||
|
||||
def _apply_gameupdate(self):
|
||||
if not self._require_root():
|
||||
return
|
||||
from util.paths import PROJECT_ROOT
|
||||
|
||||
src = PROJECT_ROOT / "gameupdate"
|
||||
dst = Path(self._game_root)
|
||||
if not src.is_dir():
|
||||
QMessageBox.warning(
|
||||
self, "gameupdate",
|
||||
f"The tool's gameupdate/ folder was not found at {src}.",
|
||||
)
|
||||
return
|
||||
|
||||
def task(log):
|
||||
import shutil
|
||||
|
||||
copied = 0
|
||||
errors: list[str] = []
|
||||
log(f"Copying gameupdate patch files: {src} → {dst} …")
|
||||
for fp in sorted(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
|
||||
log(f" copied {rel}")
|
||||
except Exception as exc:
|
||||
errors.append(f"{rel}: {exc}")
|
||||
for e in errors:
|
||||
log(f" ⚠ {e}")
|
||||
if errors:
|
||||
return False, f"Copied {copied} file(s); {len(errors)} failed (see log)."
|
||||
return True, f"Copied {copied} gameupdate file(s) into {dst.name}."
|
||||
|
||||
self._run_task(task)
|
||||
|
||||
def _package_loose(self):
|
||||
if not self._require_root():
|
||||
return
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ import sys
|
|||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from util.paths import VOCAB_BASE_PATH, VOCAB_PATH
|
||||
from util.paths import VOCAB_PATH
|
||||
from util.vocab import BASE_SEPARATOR as _SHARED_BASE_SEPARATOR
|
||||
from util.vocab import read_game_vocab, write_game_vocab
|
||||
|
||||
import jsbeautifier
|
||||
|
||||
|
|
@ -1075,8 +1077,10 @@ class WorkflowTab(QWidget):
|
|||
"</rules>\n"
|
||||
"\n"
|
||||
"<output_format>\n"
|
||||
"Output EXACTLY two sections with these headers. Do not add any preamble, explanation, "
|
||||
"or text outside the entries.\n"
|
||||
"Return the ENTIRE glossary inside ONE fenced code block (a ``` block), with nothing before "
|
||||
"or after it, so it can be copied in a single click and pasted straight into the tool.\n"
|
||||
"Inside the code block, output EXACTLY two sections with these headers. Do not add any "
|
||||
"preamble, explanation, or text outside the entries.\n"
|
||||
"\n"
|
||||
"# Game Characters\n"
|
||||
"# Worldbuilding Terms\n"
|
||||
|
|
@ -1194,10 +1198,11 @@ class WorkflowTab(QWidget):
|
|||
"</safety_check>\n"
|
||||
"\n"
|
||||
"<output_format>\n"
|
||||
"Provide the translated file as a complete replacement of js/plugins.js. Only change the "
|
||||
"Provide the translated file as a complete replacement of js/plugins.js, inside a single "
|
||||
"fenced code block (```js ... ```) so it can be copied in one click. Only change the "
|
||||
"string values identified above. Preserve all original formatting, indentation, comments, and structure.\n"
|
||||
"\n"
|
||||
"After the file, output a summary:\n"
|
||||
"After the code block, output a summary:\n"
|
||||
"\n"
|
||||
"### Translations Made\n"
|
||||
" Plugin: <plugin name>\n"
|
||||
|
|
@ -1268,8 +1273,9 @@ class WorkflowTab(QWidget):
|
|||
"</safety_check>\n"
|
||||
"\n"
|
||||
"<output_format>\n"
|
||||
"For each .rb file that needed changes, provide the full translated file content. "
|
||||
"Only change string values identified as safe. "
|
||||
"For each .rb file that needed changes, provide the full translated file content inside its "
|
||||
"own fenced code block (```ruby ... ```), preceded by the filename, so each file can be "
|
||||
"copied in one click. Only change string values identified as safe. "
|
||||
"Preserve all Ruby syntax, indentation, comments, and structure exactly.\n"
|
||||
"\n"
|
||||
"After all files, output a summary:\n"
|
||||
|
|
@ -3555,7 +3561,7 @@ class WorkflowTab(QWidget):
|
|||
# Step 1 – Vocab
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
_BASE_SEPARATOR = "# ── Base Vocabulary (auto-appended from vocab_base.txt — do not edit below) ──\n"
|
||||
_BASE_SEPARATOR = _SHARED_BASE_SEPARATOR
|
||||
|
||||
def _copy_glossary_prompt(self):
|
||||
"""Copy the glossary prompt to clipboard, injecting known speakers from vocab.txt."""
|
||||
|
|
@ -3608,27 +3614,14 @@ class WorkflowTab(QWidget):
|
|||
return results
|
||||
|
||||
def _reload_vocab(self):
|
||||
vocab_path = VOCAB_PATH
|
||||
try:
|
||||
if vocab_path.exists():
|
||||
text = vocab_path.read_text(encoding="utf-8")
|
||||
# Strip the auto-appended base section so editor shows only game-specific content
|
||||
sep_idx = text.find(self._BASE_SEPARATOR)
|
||||
if sep_idx != -1:
|
||||
text = text[:sep_idx].rstrip("\n")
|
||||
self.vocab_editor.setPlainText(text)
|
||||
else:
|
||||
self.vocab_editor.setPlainText("# Add character glossary entries here\n")
|
||||
self.vocab_editor.setPlainText(read_game_vocab())
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not load vocab.txt: {exc}")
|
||||
|
||||
def _save_vocab(self):
|
||||
try:
|
||||
game_text = self.vocab_editor.toPlainText().rstrip("\n")
|
||||
base_path = VOCAB_BASE_PATH
|
||||
base_text = base_path.read_text(encoding="utf-8") if base_path.exists() else ""
|
||||
combined = game_text + "\n\n" + self._BASE_SEPARATOR + base_text
|
||||
VOCAB_PATH.write_text(combined, encoding="utf-8")
|
||||
write_game_vocab(self.vocab_editor.toPlainText())
|
||||
self._log("✅ vocab.txt saved (base terms from vocab_base.txt appended).")
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not save vocab.txt: {exc}")
|
||||
|
|
|
|||
47
util/vocab.py
Normal file
47
util/vocab.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"""Shared helpers for the game-specific translation glossary (``data/vocab.txt``).
|
||||
|
||||
``vocab.txt`` is loaded by the shared translation layer (:mod:`util.translation`)
|
||||
and applied to every engine, so a good glossary keeps character names, honorifics,
|
||||
and worldbuilding terms consistent across the whole translation.
|
||||
|
||||
The file has two parts:
|
||||
|
||||
* the game-specific entries (characters, worldbuilding terms) edited per project,
|
||||
* a base vocabulary that is auto-appended from ``data/vocab_base.txt``.
|
||||
|
||||
``BASE_SEPARATOR`` marks where the auto-appended base section begins so the
|
||||
workflow editors can show and save only the game-specific portion. It must stay
|
||||
byte-identical to what has already been written into users' ``vocab.txt`` files,
|
||||
otherwise the base section would not be stripped on reload.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from util.paths import VOCAB_BASE_PATH, VOCAB_PATH
|
||||
|
||||
BASE_SEPARATOR = (
|
||||
"# ── Base Vocabulary (auto-appended from vocab_base.txt — do not edit below) ──\n"
|
||||
)
|
||||
|
||||
_EMPTY_PLACEHOLDER = "# Add character glossary entries here\n"
|
||||
|
||||
|
||||
def read_game_vocab() -> str:
|
||||
"""Return the game-specific portion of ``vocab.txt`` (base section stripped)."""
|
||||
if VOCAB_PATH.is_file():
|
||||
text = VOCAB_PATH.read_text(encoding="utf-8")
|
||||
idx = text.find(BASE_SEPARATOR)
|
||||
if idx != -1:
|
||||
text = text[:idx].rstrip("\n")
|
||||
return text
|
||||
return _EMPTY_PLACEHOLDER
|
||||
|
||||
|
||||
def write_game_vocab(game_text: str) -> None:
|
||||
"""Write the game-specific vocab and re-append the base vocabulary."""
|
||||
game_text = (game_text or "").rstrip("\n")
|
||||
base_text = (
|
||||
VOCAB_BASE_PATH.read_text(encoding="utf-8") if VOCAB_BASE_PATH.is_file() else ""
|
||||
)
|
||||
combined = game_text + "\n\n" + BASE_SEPARATOR + base_text
|
||||
VOCAB_PATH.write_text(combined, encoding="utf-8")
|
||||
Loading…
Reference in a new issue