moremoremoremore
This commit is contained in:
parent
9b5915888a
commit
174fbcad98
1 changed files with 339 additions and 74 deletions
|
|
@ -123,6 +123,23 @@ class _ImportWorker(QThread):
|
|||
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)
|
||||
|
|
@ -134,15 +151,23 @@ class _ExportWorker(QThread):
|
|||
done = pyqtSignal(int, list)
|
||||
log = pyqtSignal(str)
|
||||
|
||||
def __init__(self, game_data_path: 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
|
||||
self.log.emit(f"Exporting translated/ → {self.game_data_path} …")
|
||||
count, errors = export_to_game("translated", self.game_data_path)
|
||||
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)])
|
||||
|
|
@ -456,9 +481,9 @@ class WorkflowTab(QWidget):
|
|||
|
||||
# Action row
|
||||
row1 = QHBoxLayout()
|
||||
sel_all = _make_btn("Select All", "#555")
|
||||
sel_all.clicked.connect(self._select_all_files)
|
||||
row1.addWidget(sel_all)
|
||||
self.sel_all_btn = _make_btn("Select All", "#555")
|
||||
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.setToolTip("Select only core database files; deselect map files")
|
||||
|
|
@ -654,6 +679,82 @@ class WorkflowTab(QWidget):
|
|||
"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 — so\n"
|
||||
"I need to know how many English characters fit comfortably on one line for three\n"
|
||||
"different window types:\n"
|
||||
"\n"
|
||||
" width — main dialogue / message box (Show Text / code 401)\n"
|
||||
" listWidth — item / skill / help description windows\n"
|
||||
" noteWidth — database note fields (item, weapon, armour, skill descriptions)\n"
|
||||
"\n"
|
||||
"## Step 1 — Gather screen and font settings\n"
|
||||
"\n"
|
||||
"Read System.json and report:\n"
|
||||
" - screenWidth and screenHeight (or window_width / window_height for older MV)\n"
|
||||
" - fontSize (default 26 for MV; may be absent in MZ if using default)\n"
|
||||
" - advanced.uiAreaWidth / advanced.uiAreaHeight if present\n"
|
||||
"\n"
|
||||
"## Step 2 — Check plugins for window overrides\n"
|
||||
"\n"
|
||||
"Search plugins.js (or js/plugins.js) for any of the following and report their\n"
|
||||
"relevant parameters if present:\n"
|
||||
"\n"
|
||||
" VisuMZ_1_MessageCore → MessageWidth, DefaultRows, FontSize\n"
|
||||
" YEP_MessageCore → messageWidth, messageRows\n"
|
||||
" MOG_MessageSystem → message_width, message_height\n"
|
||||
" Any other plugin whose name contains 'Message' or 'Window' with size config\n"
|
||||
"\n"
|
||||
"## Step 3 — Calculate character widths\n"
|
||||
"\n"
|
||||
"Use this formula for each window type:\n"
|
||||
"\n"
|
||||
" content_px = window_pixel_width - (horizontal_padding × 2)\n"
|
||||
" assume 24px padding per side if unknown\n"
|
||||
" avg_char_width_px = font_size_px × 0.58 (empirical average for English sans-serif)\n"
|
||||
" chars_per_line = floor(content_px / avg_char_width_px)\n"
|
||||
"\n"
|
||||
"For listWidth, the relevant window is the help/description pane in the menu\n"
|
||||
"(usually full screen width or half, depending on the game's menu layout).\n"
|
||||
"For noteWidth, use the narrowest description area you can find — often an item\n"
|
||||
"detail window that is roughly 40–50% of screen width.\n"
|
||||
"\n"
|
||||
"Show your working for each of the three values.\n"
|
||||
"\n"
|
||||
"## Step 4 — Font size advice\n"
|
||||
"\n"
|
||||
"If the game's font size is larger than 26px, consider whether reducing it would\n"
|
||||
"let more translated text fit without excessive wrapping. Only recommend a change\n"
|
||||
"if the gain is meaningful (e.g. 30px → 26px adds ~6 characters per line).\n"
|
||||
"\n"
|
||||
"If a font-size change is advisable, identify exactly where to make it:\n"
|
||||
" a) System.json 'fontSize' field\n"
|
||||
" b) VisuMZ_1_MessageCore or YEP_MessageCore 'FontSize' parameter in plugins.js\n"
|
||||
" c) A CSS / font override file if the game uses a custom web font\n"
|
||||
"\n"
|
||||
"## Required output format\n"
|
||||
"\n"
|
||||
"Reply with EXACTLY this block — fill in the numbers from your analysis:\n"
|
||||
"\n"
|
||||
"```\n"
|
||||
"# Text wrap recommendations\n"
|
||||
"\n"
|
||||
"Screen resolution : <W>x<H> (source: System.json / plugin override)\n"
|
||||
"Font size : <N>px (source: System.json / plugin override)\n"
|
||||
"\n"
|
||||
"Dialogue window : <pixel_width>px wide → width=<N>\n"
|
||||
"List/help window : <pixel_width>px wide → listWidth=<N>\n"
|
||||
"Note window : <pixel_width>px wide → noteWidth=<N>\n"
|
||||
"\n"
|
||||
"Font size change : <'No change needed' OR 'Change Xpx → Ypx in <location>'>\n"
|
||||
"```\n"
|
||||
"\n"
|
||||
"After the block, include a short paragraph explaining any assumptions you made\n"
|
||||
"(e.g. padding estimates, windows whose pixel width could not be determined directly).\n"
|
||||
)
|
||||
|
||||
# ── Step 1 (Optional): Pre-process ────────────────────────────────
|
||||
|
||||
def _build_preprocess(self, layout: QVBoxLayout):
|
||||
|
|
@ -823,7 +924,7 @@ class WorkflowTab(QWidget):
|
|||
layout.addWidget(hint)
|
||||
|
||||
# ---- Parse Speakers -------------------------------------------------
|
||||
spk_box = QGroupBox("2a — Parse Speakers (Auto-detect Names)")
|
||||
spk_box = QGroupBox("3a — Parse Speakers (Auto-detect Names)")
|
||||
spk_box.setStyleSheet(
|
||||
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
|
||||
"margin-top:8px;font-size:11px;}"
|
||||
|
|
@ -858,7 +959,7 @@ class WorkflowTab(QWidget):
|
|||
layout.addWidget(spk_box)
|
||||
|
||||
# ---- Copilot / Cursor prompt helpers --------------------------------
|
||||
prompt_box = QGroupBox("2b — AI Prompt Helpers (Copilot / Cursor)")
|
||||
prompt_box = QGroupBox("3b — AI Prompt Helpers (Copilot / Cursor)")
|
||||
prompt_box.setStyleSheet(
|
||||
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
|
||||
"margin-top:8px;font-size:11px;}"
|
||||
|
|
@ -992,6 +1093,71 @@ class WorkflowTab(QWidget):
|
|||
def _build_step4(self, layout: QVBoxLayout):
|
||||
layout.addWidget(_make_section_label("Step 4 — Translation"))
|
||||
|
||||
# ---- Pre-flight: text wrap configuration ----------------------------
|
||||
wrap_box = QGroupBox("Pre-flight — Text Wrap Width")
|
||||
wrap_box.setStyleSheet(
|
||||
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
|
||||
"margin-top:4px;font-size:11px;}"
|
||||
"QGroupBox::title{padding:0 6px;}"
|
||||
)
|
||||
wrap_inner = QVBoxLayout(wrap_box)
|
||||
wrap_inner.setSpacing(6)
|
||||
|
||||
wrap_hint = QLabel(
|
||||
"Copy the prompt, paste it into Copilot with System.json and plugins.js attached. "
|
||||
"It will calculate how many English characters fit per line for each window type. "
|
||||
"Enter the recommended values and click Apply to update .env."
|
||||
)
|
||||
wrap_hint.setWordWrap(True)
|
||||
wrap_hint.setStyleSheet("color:#888;font-size:10px;")
|
||||
wrap_inner.addWidget(wrap_hint)
|
||||
|
||||
copy_wrap_row = QHBoxLayout()
|
||||
copy_wrap_btn = _make_btn("📋 Copy Wrap Prompt", "#555")
|
||||
copy_wrap_btn.setToolTip("Copy the text-wrap analysis prompt to clipboard")
|
||||
copy_wrap_btn.clicked.connect(self._copy_wrap_prompt)
|
||||
copy_wrap_row.addWidget(copy_wrap_btn)
|
||||
copy_wrap_row.addStretch()
|
||||
wrap_inner.addLayout(copy_wrap_row)
|
||||
|
||||
def _spin_pair(label_text: str, default: int):
|
||||
lbl = QLabel(label_text)
|
||||
lbl.setStyleSheet("color:#aaa;font-size:10px;")
|
||||
sp = QSpinBox()
|
||||
sp.setRange(20, 300)
|
||||
sp.setValue(default)
|
||||
sp.setStyleSheet(
|
||||
"QSpinBox{background:#2d2d30;color:#ccc;border:1px solid #555;"
|
||||
"padding:2px 4px;font-size:10px;border-radius:2px;}"
|
||||
)
|
||||
sp.setFixedWidth(68)
|
||||
return lbl, sp
|
||||
|
||||
lbl_w, self.wrap_width_spin = _spin_pair("Dialogue (width)", 60)
|
||||
lbl_lw, self.wrap_list_spin = _spin_pair("List/Help (listWidth)", 80)
|
||||
lbl_nw, self.wrap_note_spin = _spin_pair("Notes (noteWidth)", 60)
|
||||
|
||||
for lbl, sp in [(lbl_w, self.wrap_width_spin),
|
||||
(lbl_lw, self.wrap_list_spin),
|
||||
(lbl_nw, self.wrap_note_spin)]:
|
||||
row = QHBoxLayout()
|
||||
row.setSpacing(6)
|
||||
lbl.setFixedWidth(160)
|
||||
row.addWidget(lbl)
|
||||
row.addWidget(sp)
|
||||
row.addStretch()
|
||||
wrap_inner.addLayout(row)
|
||||
|
||||
apply_wrap_row = QHBoxLayout()
|
||||
apply_wrap_btn = _make_btn("✔ Apply to .env", "#3a7a3a")
|
||||
apply_wrap_btn.setToolTip("Write width / listWidth / noteWidth into .env")
|
||||
apply_wrap_btn.clicked.connect(self._apply_wrap_config)
|
||||
apply_wrap_row.addStretch()
|
||||
apply_wrap_row.addWidget(apply_wrap_btn)
|
||||
wrap_inner.addLayout(apply_wrap_row)
|
||||
|
||||
layout.addWidget(wrap_box)
|
||||
|
||||
p1_box = QGroupBox("Phase 1 – Safe Codes (dialogue + choices)")
|
||||
p1_box.setStyleSheet(
|
||||
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
|
||||
|
|
@ -999,13 +1165,17 @@ class WorkflowTab(QWidget):
|
|||
"QGroupBox::title{padding:0 6px;}"
|
||||
)
|
||||
p1_inner = QVBoxLayout(p1_box)
|
||||
p1_desc = QLabel("Codes: 401 (Show Text), 405 (continued), 102 (Choices), 408 (extra lines)\n"
|
||||
"These rarely break game logic — safe to translate first.")
|
||||
p1_desc = QLabel(
|
||||
"Codes ON: 101 (Name), 401 (Show Text), 405 (continued), 102 (Choices), 408 (extra lines)\n"
|
||||
"Clicking the button below applies these codes, then takes you to the Translation tab.\n"
|
||||
"While translating, watch the log — speaker lines should look like [Speaker]: Dialogue."
|
||||
)
|
||||
p1_desc.setStyleSheet("color:#888;font-size:10px;")
|
||||
p1_desc.setWordWrap(True)
|
||||
p1_inner.addWidget(p1_desc)
|
||||
p1_row = QHBoxLayout()
|
||||
run_p1 = _make_btn("▶ Run Phase 1", "#007acc")
|
||||
run_p1 = _make_btn("⚙ Set Codes \u2192 Go to Translation", "#007acc")
|
||||
run_p1.setToolTip("Applies Phase 1 code settings and switches to the Translation tab")
|
||||
run_p1.clicked.connect(lambda: self._run_phase(1))
|
||||
p1_row.addWidget(run_p1)
|
||||
p1_row.addStretch()
|
||||
|
|
@ -1020,9 +1190,9 @@ class WorkflowTab(QWidget):
|
|||
)
|
||||
p2_inner = QVBoxLayout(p2_box)
|
||||
p2_desc = QLabel(
|
||||
"Codes: 122 (Control Variables), 357 (Plugin Commands), 111 (Conditional Branch)\n"
|
||||
"Strings in these codes are often used in script comparisons — mistranslations "
|
||||
"can break game logic. Scan first, then translate carefully."
|
||||
"Codes ON: 122 (Control Variables), 357 (Plugin Commands), 111 (Conditional Branch)\n"
|
||||
"Strings here are often used in script comparisons — mistranslations can break game "
|
||||
"logic. Use Scan first to preview what will be translated, then apply and go translate."
|
||||
)
|
||||
p2_desc.setStyleSheet("color:#888;font-size:10px;")
|
||||
p2_desc.setWordWrap(True)
|
||||
|
|
@ -1037,7 +1207,8 @@ class WorkflowTab(QWidget):
|
|||
scan_p2.clicked.connect(self._scan_risky_strings)
|
||||
p2_row.addWidget(scan_p2)
|
||||
|
||||
run_p2 = _make_btn("▶ Run Phase 2", "#7a4a00")
|
||||
run_p2 = _make_btn("⚙ Set Codes \u2192 Go to Translation", "#7a4a00")
|
||||
run_p2.setToolTip("Applies Phase 2 code settings and switches to the Translation tab")
|
||||
run_p2.clicked.connect(lambda: self._run_phase(2))
|
||||
p2_row.addWidget(run_p2)
|
||||
p2_row.addStretch()
|
||||
|
|
@ -1049,17 +1220,25 @@ class WorkflowTab(QWidget):
|
|||
def _build_step6(self, layout: QVBoxLayout):
|
||||
layout.addWidget(_make_section_label("Step 5 — Export to Game"))
|
||||
hint = QLabel(
|
||||
"Copy all files from translated/ back into the game's data folder "
|
||||
"to patch the game in-place."
|
||||
"Copy translated files back into the game's data folder to patch the game in-place."
|
||||
)
|
||||
hint.setWordWrap(True)
|
||||
hint.setStyleSheet("color:#888;font-size:10px;padding-bottom:4px;")
|
||||
layout.addWidget(hint)
|
||||
|
||||
row = QHBoxLayout()
|
||||
export_btn = _make_btn("📤 Export translated/ → Game Folder", "#3a7a3a")
|
||||
export_btn.clicked.connect(self._export_to_game)
|
||||
row.addWidget(export_btn)
|
||||
export_active_btn = _make_btn("📤 Export Active Files → Game Folder", "#3a7a3a")
|
||||
export_active_btn.setToolTip(
|
||||
"Only export files whose names match those currently in files/\n"
|
||||
"(i.e. the files you imported for this project)"
|
||||
)
|
||||
export_active_btn.clicked.connect(self._export_active_files)
|
||||
row.addWidget(export_active_btn)
|
||||
|
||||
export_all_btn = _make_btn("📤 Export ALL translated/ → Game Folder", "#555")
|
||||
export_all_btn.setToolTip("Export every file in translated/ regardless of what is in files/")
|
||||
export_all_btn.clicked.connect(self._export_to_game)
|
||||
row.addWidget(export_all_btn)
|
||||
row.addStretch()
|
||||
layout.addLayout(row)
|
||||
|
||||
|
|
@ -1130,15 +1309,27 @@ class WorkflowTab(QWidget):
|
|||
self.file_list.addItem(lw)
|
||||
|
||||
self.import_btn.setEnabled(len(items) > 0)
|
||||
self.sel_all_btn.setText("Select All")
|
||||
self._log(f"Found {len(items)} importable file(s).")
|
||||
self._populate_preprocess_paths()
|
||||
|
||||
def _select_all_files(self):
|
||||
count = self.file_list.count()
|
||||
if not count:
|
||||
return
|
||||
all_checked = all(
|
||||
self.file_list.item(i).checkState() == Qt.Checked
|
||||
for i in range(count)
|
||||
)
|
||||
new_state = Qt.Unchecked if all_checked else Qt.Checked
|
||||
for i in range(count):
|
||||
self.file_list.item(i).setCheckState(Qt.Checked)
|
||||
if count:
|
||||
self.file_list.item(i).setCheckState(new_state)
|
||||
if new_state == Qt.Checked:
|
||||
self.sel_all_btn.setText("Deselect All")
|
||||
self._log(f"✔ Selected all {count} file(s).")
|
||||
else:
|
||||
self.sel_all_btn.setText("Select All")
|
||||
self._log(f"✔ Deselected all {count} file(s).")
|
||||
|
||||
def _select_core_only(self):
|
||||
core = other = 0
|
||||
|
|
@ -1316,6 +1507,38 @@ class WorkflowTab(QWidget):
|
|||
QApplication.clipboard().setText(self._SPEAKER_PROMPT)
|
||||
self._log("Speaker format prompt copied to clipboard.")
|
||||
|
||||
def _copy_wrap_prompt(self):
|
||||
QApplication.clipboard().setText(self._WRAP_PROMPT)
|
||||
self._log("Text-wrap analysis prompt copied to clipboard.")
|
||||
|
||||
def _apply_wrap_config(self):
|
||||
"""Write width / listWidth / noteWidth back into .env."""
|
||||
import re as _re
|
||||
updates = {
|
||||
"width": str(self.wrap_width_spin.value()),
|
||||
"listWidth": str(self.wrap_list_spin.value()),
|
||||
"noteWidth": str(self.wrap_note_spin.value()),
|
||||
}
|
||||
env_path = Path(".env")
|
||||
try:
|
||||
text = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
|
||||
for key, val in updates.items():
|
||||
text, n = _re.subn(
|
||||
rf"^({_re.escape(key)}\s*=\s*')[^']*(')",
|
||||
rf"\g<1>{val}\2",
|
||||
text,
|
||||
flags=_re.MULTILINE,
|
||||
)
|
||||
if n == 0:
|
||||
text = text.rstrip("\n") + f"\n{key}='{val}'\n"
|
||||
env_path.write_text(text, encoding="utf-8")
|
||||
self._log(
|
||||
"✅ .env updated — "
|
||||
+ ", ".join(f"{k}={v}" for k, v in updates.items())
|
||||
)
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not update .env: {exc}")
|
||||
|
||||
def _apply_speaker_flags(self):
|
||||
cfg = {
|
||||
"INLINE401SPEAKERS": self.spk_inline_cb.isChecked(),
|
||||
|
|
@ -1347,57 +1570,61 @@ class WorkflowTab(QWidget):
|
|||
config = PHASE1_CONFIG if phase == 1 else PHASE2_CONFIG
|
||||
label = "Phase 1 (safe codes)" if phase == 1 else "Phase 2 (risky codes)"
|
||||
|
||||
# Apply config profile
|
||||
# Apply config profile so the Translation tab uses the right codes
|
||||
try:
|
||||
from gui.config_integration import ConfigIntegration
|
||||
ci = ConfigIntegration()
|
||||
ci.update_rpgmaker_config(config)
|
||||
self._log(f"Applied {label} config profile.")
|
||||
# Sync the live Settings tab if it is open
|
||||
try:
|
||||
if self.parent_window and hasattr(self.parent_window, "config_tab"):
|
||||
ct = self.parent_window.config_tab
|
||||
if hasattr(ct, "rpgmaker_tab"):
|
||||
ct.rpgmaker_tab.set_config(
|
||||
ct.rpgmaker_tab.get_config() | config
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
self._log(f"✅ {label} config applied — codes set:")
|
||||
on = [k for k, v in config.items() if v]
|
||||
off = [k for k, v in config.items() if not v]
|
||||
if on:
|
||||
self._log(" ON : " + " ".join(on))
|
||||
if off:
|
||||
self._log(" OFF: " + " ".join(off))
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not apply phase config: {exc}")
|
||||
return
|
||||
|
||||
# Launch translation worker
|
||||
# Phase-specific guidance
|
||||
if phase == 1:
|
||||
self._log("")
|
||||
self._log("─" * 54)
|
||||
self._log("👉 Switch to the Translation tab and start the run.")
|
||||
self._log("")
|
||||
self._log("⚠ While translating, watch the log for speaker lines.")
|
||||
self._log(" They should look like: [Speaker]: Dialogue text")
|
||||
self._log(" If names are missing or garbled, stop the run and")
|
||||
self._log(" revisit Step 2 (speaker flags) before continuing.")
|
||||
self._log("─" * 54)
|
||||
else:
|
||||
self._log("")
|
||||
self._log("─" * 54)
|
||||
self._log("👉 Switch to the Translation tab and start the run.")
|
||||
self._log(" Phase 2 targets script/variable strings — review")
|
||||
self._log(" the Risky String scan output before proceeding.")
|
||||
self._log("─" * 54)
|
||||
|
||||
# Navigate to the Translation tab (index 0 in content_stack)
|
||||
try:
|
||||
from gui.translation_tab import TranslationWorker
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not import TranslationWorker: {exc}")
|
||||
return
|
||||
|
||||
project_root = Path(__file__).parent.parent
|
||||
module_info = ["RPG Maker MV/MZ", [".json"], None]
|
||||
|
||||
# Build file order: core DB files first (they populate the glossary),
|
||||
# then map files. Within each group, sort alphabetically.
|
||||
files_dir = project_root / "files"
|
||||
all_json = sorted(
|
||||
p.name for p in files_dir.glob("*.json") if p.name != ".gitkeep"
|
||||
) if files_dir.exists() else []
|
||||
core_files = [f for f in all_json if not f.lower().startswith("map")]
|
||||
map_files = [f for f in all_json if f.lower().startswith("map")]
|
||||
ordered_files = core_files + map_files
|
||||
if ordered_files:
|
||||
self._log(
|
||||
f"File order: {len(core_files)} core file(s) first, "
|
||||
f"then {len(map_files)} map file(s)."
|
||||
)
|
||||
|
||||
self._log(f"Starting {label} translation…")
|
||||
worker = TranslationWorker(
|
||||
project_root, module_info, estimate_only=False,
|
||||
selected_files=ordered_files if ordered_files else None,
|
||||
)
|
||||
worker.log_signal.connect(self._log)
|
||||
worker.progress_signal.connect(
|
||||
lambda cur, tot, fn: self._log(f" [{cur}/{tot}] {fn}")
|
||||
)
|
||||
worker.finished_signal.connect(
|
||||
lambda ok, msg: self._log(
|
||||
f"{'✅' if ok else '❌'} {label} finished: {msg}"
|
||||
)
|
||||
)
|
||||
self._worker = worker
|
||||
worker.start()
|
||||
if self.parent_window and hasattr(self.parent_window, "content_stack"):
|
||||
self.parent_window.content_stack.setCurrentIndex(0)
|
||||
# Keep nav buttons in sync
|
||||
if hasattr(self.parent_window, "nav_buttons"):
|
||||
for i, btn in enumerate(self.parent_window.nav_buttons):
|
||||
btn.setChecked(i == 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Risky string scanner (preview mode)
|
||||
|
|
@ -1447,21 +1674,47 @@ class WorkflowTab(QWidget):
|
|||
# Step 5 – Export to game
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _export_to_game(self):
|
||||
game_data = self._data_path
|
||||
def _export_active_files(self):
|
||||
"""Export only translated files whose names match what is in files/."""
|
||||
files_dir = Path("files")
|
||||
active = sorted(
|
||||
fp.name for fp in files_dir.glob("*.json") if fp.name != ".gitkeep"
|
||||
) if files_dir.exists() else []
|
||||
|
||||
if not active:
|
||||
self._log("⚠ No files found in files/ — run Step 0 (Import) first.")
|
||||
return
|
||||
|
||||
game_data = self._resolve_export_path()
|
||||
if not game_data:
|
||||
# Prompt the user
|
||||
game_data = QFileDialog.getExistingDirectory(
|
||||
self, "Select Game Data Folder to Export Into"
|
||||
)
|
||||
if not game_data:
|
||||
return
|
||||
self._data_path = game_data
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Export Active Files to Game",
|
||||
f"Export {len(active)} file(s) matching files/ contents into:\n{game_data}\n\n"
|
||||
"Make a backup first if needed. Continue?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
if reply != QMessageBox.Yes:
|
||||
return
|
||||
|
||||
w = _ExportWorker(game_data, filter_names=active)
|
||||
w.log.connect(self._log)
|
||||
w.done.connect(self._on_export_done)
|
||||
self._worker = w
|
||||
w.start()
|
||||
|
||||
def _export_to_game(self):
|
||||
game_data = self._resolve_export_path()
|
||||
if not game_data:
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Export to Game",
|
||||
f"This will overwrite files in:\n{game_data}\n\n"
|
||||
f"This will overwrite ALL translated files in:\n{game_data}\n\n"
|
||||
"Make a backup first if needed. Continue?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
|
|
@ -1475,6 +1728,18 @@ class WorkflowTab(QWidget):
|
|||
self._worker = w
|
||||
w.start()
|
||||
|
||||
def _resolve_export_path(self) -> str | None:
|
||||
"""Return the game data path, prompting if not yet set."""
|
||||
game_data = self._data_path
|
||||
if not game_data:
|
||||
game_data = QFileDialog.getExistingDirectory(
|
||||
self, "Select Game Data Folder to Export Into"
|
||||
)
|
||||
if not game_data:
|
||||
return None
|
||||
self._data_path = game_data
|
||||
return game_data
|
||||
|
||||
def _on_export_done(self, count: int, errors: list):
|
||||
if errors:
|
||||
self._log(f"⚠ {len(errors)} error(s) during export:")
|
||||
|
|
|
|||
Loading…
Reference in a new issue