More changes and fixes
This commit is contained in:
parent
2c0bfddcd8
commit
e214a28d5d
8 changed files with 376 additions and 65 deletions
|
|
@ -331,9 +331,9 @@ Open the **Workflow** tab and choose **Wolf RPG (WolfDawn)** from the engine sel
|
||||||
| **4 Database** | Review the **discovery summary** to see where this game's text lives (standard RPG sheets vs custom dialogue tables). Database sheets are classified as foundation (items, skills, descriptions — translate first) or narrative (custom event/profile sheets — translate after foundation). Use **Translate foundation DB** then **Translate narrative DB**, or tick specific sheets and run **Translate checked sheets only**. Optional: copy the **DB structure prompt** for an AI audit and import the returned JSON into `wolf_json/db_profile.json`. |
|
| **4 Database** | Review the **discovery summary** to see where this game's text lives (standard RPG sheets vs custom dialogue tables). Database sheets are classified as foundation (items, skills, descriptions — translate first) or narrative (custom event/profile sheets — translate after foundation). Use **Translate foundation DB** then **Translate narrative DB**, or tick specific sheets and run **Translate checked sheets only**. Optional: copy the **DB structure prompt** for an AI audit and import the returned JSON into `wolf_json/db_profile.json`. |
|
||||||
| **5 Maps/Events** | Translate map scripts (`.mps`), common events (`CommonEvent.dat`), `Game.dat`, and Evtext. Run after Steps 3–4 so vocab and database terms are consistent. Configure speaker handling for low-confidence nameplate guesses. Batch mode is recommended for large `CommonEvent.dat` files. |
|
| **5 Maps/Events** | Translate map scripts (`.mps`), common events (`CommonEvent.dat`), `Game.dat`, and Evtext. Run after Steps 3–4 so vocab and database terms are consistent. Configure speaker handling for low-confidence nameplate guesses. Batch mode is recommended for large `CommonEvent.dat` files. |
|
||||||
| **6 Precheck** | Dry-run selected JSON for safety-guard skips; fix those lines before writing binaries. |
|
| **6 Precheck** | Dry-run selected JSON for safety-guard skips; fix those lines before writing binaries. |
|
||||||
| **7 Inject** | Tick translated JSON files from `translated/`, then **Inject selected** or **Inject all**. Each file is reported individually; failures open a dialog with details. Uses pristine originals from `wolf_json/originals/`. |
|
| **7 Inject** | Always **Inject all** translated JSON from `translated/` into Data/ in one pass (keeps `names.json` and DB/map files in sync). Each file is reported individually; failures open a dialog with details. Uses pristine originals from `wolf_json/originals/`, then preserves name-only fields after names-inject. Optional **Layout-restore** re-applies source whitespace pads. |
|
||||||
| **8 Package** | Run from loose `Data/` (backs up `Data.wolf` → `.bak`) or repack `Data.wolf` so you can playtest and see overflow in-game. Optionally rewrite existing `.sav` files so old Japanese saves load in the translated build. |
|
| **8 Package** | Run from loose `Data/` (backs up `Data.wolf` → `.bak`) or repack `Data.wolf` so you can playtest and see overflow in-game. Optionally rewrite existing `.sav` files so old Japanese saves load in the translated build. |
|
||||||
| **9 Fix wrap** | After packaging, paste overflowing in-game text to **search** `translated/` JSON and jump to the database sheet and row (sheet names match Step 4 and `names.json` notes, e.g. `├■街の噂(MOB)`). Edit the line, set wrap width, **Wrap this row** or **Wrap all overflowing rows in this sheet**, save, then **Inject this file** (usually `DataBase.project.json`) and re-package to verify. Per-sheet widths are remembered in `wolf_json/wrap_profile.json`. **Advanced:** **wolf relayout** for event message boxes (maps/CommonEvent only — not bulletin-style DB UI) and **wolf desc-relayout** for standard 説明 description fields. |
|
| **9 Fix wrap** | After packaging, paste overflowing in-game text to **search** `translated/` JSON and jump to the database sheet and row (sheet names match Step 4 and `names.json` notes, e.g. `├■街の噂(MOB)`). Edit the line, set wrap width, **Wrap this row** or **Wrap all overflowing rows in this sheet**, save, then **Inject all** (Step 7) and re-package to verify. Per-sheet widths are remembered in `wolf_json/wrap_profile.json`. **Advanced:** **wolf relayout** for event message boxes (maps/CommonEvent only — not bulletin-style DB UI) and **wolf desc-relayout** for standard 説明 description fields. |
|
||||||
|
|
||||||
### Recommended order by game layout
|
### Recommended order by game layout
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,14 @@ Mirrors the RPGMaker WorkflowTab, driven by the vendored WolfDawn ``wolf`` CLI
|
||||||
Step 5 Maps/Events - .mps maps, CommonEvent, Game.dat, Evtext; speaker handling
|
Step 5 Maps/Events - .mps maps, CommonEvent, Game.dat, Evtext; speaker handling
|
||||||
Step 6 Precheck - dry-run selected JSON for safety-guard skips; edit those
|
Step 6 Precheck - dry-run selected JSON for safety-guard skips; edit those
|
||||||
lines before writing binaries
|
lines before writing binaries
|
||||||
Step 7 Inject - inject translations into Data/ binaries and refresh
|
Step 7 Inject - always inject every translated JSON into Data/ and
|
||||||
wolf_json/; quick-inject picks a few files or all
|
refresh wolf_json/ (full pass avoids name/DB wipe bugs);
|
||||||
|
optional layout-restore re-applies source whitespace pads
|
||||||
Step 8 Package - run from a loose Data/ folder or repack Data.wolf;
|
Step 8 Package - run from a loose Data/ folder or repack Data.wolf;
|
||||||
optional save rewrite for existing .sav files
|
optional save rewrite for existing .sav files
|
||||||
Step 9 Fix wrap - search translated JSON by in-game text; Relayout
|
Step 9 Fix wrap - search translated JSON by in-game text; Relayout
|
||||||
(names-wrap / cell geometry) or Manual (width + font);
|
(names-wrap / cell geometry) or Manual (width + font);
|
||||||
inject the edited file, then re-package to verify
|
then Inject all from Step 7 and re-package to verify
|
||||||
|
|
||||||
names.json is staged into files/ but is NOT translated in the bulk phases - WolfDawn
|
names.json is staged into files/ but is NOT translated in the bulk phases - WolfDawn
|
||||||
tags each name safe / refs / verify and Phase 0 translates only safe entries
|
tags each name safe / refs / verify and Phase 0 translates only safe entries
|
||||||
|
|
@ -2523,9 +2524,10 @@ class WolfWorkflowTab(QWidget):
|
||||||
def _build_step7_inject(self, layout: QVBoxLayout):
|
def _build_step7_inject(self, layout: QVBoxLayout):
|
||||||
layout.addWidget(_make_section_label("Step 7 · Inject Translations"))
|
layout.addWidget(_make_section_label("Step 7 · Inject Translations"))
|
||||||
layout.addWidget(self._desc(
|
layout.addWidget(self._desc(
|
||||||
"Tick translated JSON files below, then inject them into the game's Data/ "
|
"Inject every translated JSON into the game's Data/ binaries in one pass. "
|
||||||
"binaries. Run Step 6 (Precheck) first if you want to catch safety-guard "
|
"A full inject keeps names.json and DB/map files in sync (partial injects "
|
||||||
"skips before writing. Each file is reported individually."
|
"can wipe name-only fields like rumor boards). Run Step 6 (Precheck) first "
|
||||||
|
"if you want to catch safety-guard skips before writing."
|
||||||
))
|
))
|
||||||
|
|
||||||
self.en_punct_cb = QCheckBox("Convert Japanese punctuation to ASCII (--en-punct)")
|
self.en_punct_cb = QCheckBox("Convert Japanese punctuation to ASCII (--en-punct)")
|
||||||
|
|
@ -2562,10 +2564,10 @@ class WolfWorkflowTab(QWidget):
|
||||||
layout.addLayout(check_row)
|
layout.addLayout(check_row)
|
||||||
|
|
||||||
layout.addWidget(_make_hr())
|
layout.addWidget(_make_hr())
|
||||||
layout.addWidget(self._subheading("Translated files"))
|
layout.addWidget(self._subheading("Files that will be injected"))
|
||||||
|
|
||||||
self.inject_list = QListWidget()
|
self.inject_list = QListWidget()
|
||||||
self.inject_list.setMaximumHeight(280)
|
self.inject_list.setMaximumHeight(220)
|
||||||
self.inject_list.setStyleSheet(
|
self.inject_list.setStyleSheet(
|
||||||
"QListWidget{background-color:#252526;border:1px solid #3a3a3a;border-radius:4px;"
|
"QListWidget{background-color:#252526;border:1px solid #3a3a3a;border-radius:4px;"
|
||||||
"color:#cccccc;font-size:12px;padding:2px;}"
|
"color:#cccccc;font-size:12px;padding:2px;}"
|
||||||
|
|
@ -2574,44 +2576,51 @@ class WolfWorkflowTab(QWidget):
|
||||||
)
|
)
|
||||||
layout.addWidget(self.inject_list)
|
layout.addWidget(self.inject_list)
|
||||||
|
|
||||||
|
self._inject_count_label = QLabel("")
|
||||||
|
self._inject_count_label.setStyleSheet("color:#9cdcfe;font-size:12px;")
|
||||||
|
layout.addWidget(self._inject_count_label)
|
||||||
|
|
||||||
list_row = QHBoxLayout()
|
list_row = QHBoxLayout()
|
||||||
refresh_btn = self._register(_make_btn("↻ Refresh", "#3a3a3a"))
|
refresh_btn = self._register(_make_btn("↻ Refresh", "#3a3a3a"))
|
||||||
refresh_btn.clicked.connect(self._refresh_inject_list)
|
refresh_btn.clicked.connect(self._refresh_inject_list)
|
||||||
sel_all_btn = self._register(_make_btn("Select all", "#3a3a3a"))
|
|
||||||
sel_all_btn.clicked.connect(lambda: self._set_checklist_checks(self.inject_list, True))
|
|
||||||
sel_none_btn = self._register(_make_btn("Select none", "#3a3a3a"))
|
|
||||||
sel_none_btn.clicked.connect(lambda: self._set_checklist_checks(self.inject_list, False))
|
|
||||||
list_row.addWidget(refresh_btn)
|
list_row.addWidget(refresh_btn)
|
||||||
list_row.addWidget(sel_all_btn)
|
|
||||||
list_row.addWidget(sel_none_btn)
|
|
||||||
list_row.addStretch()
|
list_row.addStretch()
|
||||||
layout.addLayout(list_row)
|
layout.addLayout(list_row)
|
||||||
|
|
||||||
btn_row = QHBoxLayout()
|
btn_row = QHBoxLayout()
|
||||||
inject_sel_btn = self._register(_make_btn("Inject selected", "#00a86b"))
|
|
||||||
inject_sel_btn.clicked.connect(self._inject_selected)
|
|
||||||
inject_all_btn = self._register(_make_btn("Inject all", "#00a86b"))
|
inject_all_btn = self._register(_make_btn("Inject all", "#00a86b"))
|
||||||
|
inject_all_btn.setToolTip(
|
||||||
|
"Always injects every file in translated/ that the manifest knows about. "
|
||||||
|
"names.json runs first; DB/map injects keep name-only fields."
|
||||||
|
)
|
||||||
inject_all_btn.clicked.connect(self._inject_all)
|
inject_all_btn.clicked.connect(self._inject_all)
|
||||||
btn_row.addWidget(inject_sel_btn)
|
layout_restore_btn = self._register(_make_btn("Layout-restore", "#3a3a3a"))
|
||||||
|
layout_restore_btn.setToolTip(
|
||||||
|
"Re-run wolf layout-restore on every injectable translated JSON. "
|
||||||
|
"Copies unambiguous positional whitespace pads from source onto text "
|
||||||
|
"(e.g. status-card <pad>\\nNAME). Safe to re-run; already-fixed lines are no-ops."
|
||||||
|
)
|
||||||
|
layout_restore_btn.clicked.connect(self._layout_restore_all)
|
||||||
btn_row.addWidget(inject_all_btn)
|
btn_row.addWidget(inject_all_btn)
|
||||||
btn_row.addStretch()
|
btn_row.addStretch()
|
||||||
|
btn_row.addWidget(layout_restore_btn)
|
||||||
layout.addLayout(btn_row)
|
layout.addLayout(btn_row)
|
||||||
|
|
||||||
layout.addWidget(_make_hr())
|
layout.addWidget(_make_hr())
|
||||||
layout.addWidget(self._desc(
|
layout.addWidget(self._desc(
|
||||||
"Next: Step 8 (Package) so you can playtest, then Step 9 (Fix wrapping) "
|
"Next: Step 8 (Package) so you can playtest, then Step 9 (Fix wrapping) "
|
||||||
"to paste overflowing in-game text, wrap, and re-inject."
|
"to paste overflowing in-game text, wrap, and Inject all again."
|
||||||
))
|
))
|
||||||
self._refresh_inject_list()
|
self._refresh_inject_list()
|
||||||
|
|
||||||
def _build_step9_relayout(self, layout: QVBoxLayout):
|
def _build_step9_relayout(self, layout: QVBoxLayout):
|
||||||
"""Search-first fix wrapping: find sheet by in-game text, wrap, re-inject."""
|
"""Search-first fix wrapping: find sheet by in-game text, wrap, Inject all."""
|
||||||
layout.addWidget(_make_section_label("Step 9 · Fix wrapping"))
|
layout.addWidget(_make_section_label("Step 9 · Fix wrapping"))
|
||||||
layout.addWidget(self._desc(
|
layout.addWidget(self._desc(
|
||||||
"After packaging and playtesting, paste overflowing in-game text to find "
|
"After packaging and playtesting, paste overflowing in-game text to find "
|
||||||
"it in translated JSON. Relayout uses cell width + max lines "
|
"it in translated JSON. Relayout uses cell width + max lines "
|
||||||
"(names.json → wolf names-wrap). Manual uses wrap width + body font; "
|
"(names.json → wolf names-wrap). Manual uses wrap width + body font; "
|
||||||
"emphasis \\f[N] scales with the body. Re-inject, then re-package to verify."
|
"emphasis \\f[N] scales with the body. Then Inject all (Step 7) and re-package."
|
||||||
))
|
))
|
||||||
|
|
||||||
search_row = QHBoxLayout()
|
search_row = QHBoxLayout()
|
||||||
|
|
@ -2777,8 +2786,12 @@ class WolfWorkflowTab(QWidget):
|
||||||
wrap_row_btn.clicked.connect(self._wrap_current_row)
|
wrap_row_btn.clicked.connect(self._wrap_current_row)
|
||||||
wrap_group_btn = _make_btn("Wrap all in group", "#00a86b")
|
wrap_group_btn = _make_btn("Wrap all in group", "#00a86b")
|
||||||
wrap_group_btn.clicked.connect(self._wrap_current_group)
|
wrap_group_btn.clicked.connect(self._wrap_current_group)
|
||||||
inject_btn = _make_btn("Inject this file", "#007acc")
|
inject_btn = _make_btn("Inject all", "#007acc")
|
||||||
inject_btn.clicked.connect(self._inject_current_wrap_file)
|
inject_btn.setToolTip(
|
||||||
|
"Always injects every translated file (same as Step 7). "
|
||||||
|
"Partial injects can wipe name-only DB fields."
|
||||||
|
)
|
||||||
|
inject_btn.clicked.connect(self._inject_all)
|
||||||
btn_row.addWidget(wrap_row_btn)
|
btn_row.addWidget(wrap_row_btn)
|
||||||
btn_row.addWidget(wrap_group_btn)
|
btn_row.addWidget(wrap_group_btn)
|
||||||
btn_row.addWidget(inject_btn)
|
btn_row.addWidget(inject_btn)
|
||||||
|
|
@ -3226,7 +3239,7 @@ class WolfWorkflowTab(QWidget):
|
||||||
else "Line already fits (manual)."
|
else "Line already fits (manual)."
|
||||||
)
|
)
|
||||||
self._wrap_status_label.setText(
|
self._wrap_status_label.setText(
|
||||||
msg + f" Inject {self._current_wrap_path.name} from Step 7."
|
msg + " Inject all from Step 7 when ready."
|
||||||
)
|
)
|
||||||
self._log(f"{'✅' if changed else 'ℹ'} {msg}")
|
self._log(f"{'✅' if changed else 'ℹ'} {msg}")
|
||||||
self._update_wrap_preview()
|
self._update_wrap_preview()
|
||||||
|
|
@ -3267,7 +3280,7 @@ class WolfWorkflowTab(QWidget):
|
||||||
if line is not None:
|
if line is not None:
|
||||||
self._current_wrap_text = str(line.get("text") or "")
|
self._current_wrap_text = str(line.get("text") or "")
|
||||||
msg = f"Wrapped line at width {width}." if changed else "Line already fits at this width."
|
msg = f"Wrapped line at width {width}." if changed else "Line already fits at this width."
|
||||||
self._wrap_status_label.setText(msg + f" Inject {self._current_wrap_path.name} from Step 7.")
|
self._wrap_status_label.setText(msg + " Inject all from Step 7 when ready.")
|
||||||
self._log(f"{'✅' if changed else 'ℹ'} {msg}")
|
self._log(f"{'✅' if changed else 'ℹ'} {msg}")
|
||||||
self._update_wrap_preview()
|
self._update_wrap_preview()
|
||||||
|
|
||||||
|
|
@ -3301,7 +3314,7 @@ class WolfWorkflowTab(QWidget):
|
||||||
f"Manual-wrapped {count} line(s) in {scope} "
|
f"Manual-wrapped {count} line(s) in {scope} "
|
||||||
f"(width {width}, body \\f[{font}])."
|
f"(width {width}, body \\f[{font}])."
|
||||||
)
|
)
|
||||||
self._wrap_status_label.setText(msg + f" Inject {self._current_wrap_path.name}.")
|
self._wrap_status_label.setText(msg + " Inject all from Step 7 when ready.")
|
||||||
self._log(f"✅ {msg}")
|
self._log(f"✅ {msg}")
|
||||||
self._reload_wrap_hit_editor(hit, width)
|
self._reload_wrap_hit_editor(hit, width)
|
||||||
return
|
return
|
||||||
|
|
@ -3339,7 +3352,7 @@ class WolfWorkflowTab(QWidget):
|
||||||
)
|
)
|
||||||
scope = wolf_ws.scope_label(hit)
|
scope = wolf_ws.scope_label(hit)
|
||||||
msg = f"Wrapped {count} line(s) in {scope} at width {width}."
|
msg = f"Wrapped {count} line(s) in {scope} at width {width}."
|
||||||
self._wrap_status_label.setText(msg + f" Inject {self._current_wrap_path.name}.")
|
self._wrap_status_label.setText(msg + " Inject all from Step 7 when ready.")
|
||||||
self._log(f"✅ {msg}")
|
self._log(f"✅ {msg}")
|
||||||
self._reload_wrap_hit_editor(hit, width)
|
self._reload_wrap_hit_editor(hit, width)
|
||||||
|
|
||||||
|
|
@ -3434,20 +3447,11 @@ class WolfWorkflowTab(QWidget):
|
||||||
self._reload_wrap_hit_editor(hit, self._active_wrap_width())
|
self._reload_wrap_hit_editor(hit, self._active_wrap_width())
|
||||||
if not quiet and hasattr(self, "_wrap_status_label"):
|
if not quiet and hasattr(self, "_wrap_status_label"):
|
||||||
self._wrap_status_label.setText(
|
self._wrap_status_label.setText(
|
||||||
summary + " Re-inject names.json from Step 7."
|
summary + " Inject all from Step 7 when ready."
|
||||||
)
|
)
|
||||||
|
|
||||||
self._run_task(task, on_done=_after)
|
self._run_task(task, on_done=_after)
|
||||||
|
|
||||||
def _inject_current_wrap_file(self):
|
|
||||||
hit = self._current_wrap_hit
|
|
||||||
if not hit:
|
|
||||||
QMessageBox.information(self, "Inject", "Select a search result first.")
|
|
||||||
return
|
|
||||||
if not self._require_manifest():
|
|
||||||
return
|
|
||||||
self._inject({hit.json_file})
|
|
||||||
|
|
||||||
def _tool_root(self) -> Path:
|
def _tool_root(self) -> Path:
|
||||||
"""DazedMTLTool project root (``files/``, ``translated/``, etc.)."""
|
"""DazedMTLTool project root (``files/``, ``translated/``, etc.)."""
|
||||||
return Path(__file__).resolve().parent.parent
|
return Path(__file__).resolve().parent.parent
|
||||||
|
|
@ -3626,7 +3630,7 @@ class WolfWorkflowTab(QWidget):
|
||||||
self._refresh_inject_list()
|
self._refresh_inject_list()
|
||||||
|
|
||||||
def _fill_injectable_checklist(self, list_widget: QListWidget):
|
def _fill_injectable_checklist(self, list_widget: QListWidget):
|
||||||
"""Populate a checkable file list from translated/ + manifest."""
|
"""Populate a checkable file list from translated/ + manifest (precheck)."""
|
||||||
list_widget.clear()
|
list_widget.clear()
|
||||||
if not self._read_manifest():
|
if not self._read_manifest():
|
||||||
item = QListWidgetItem("Run Step 0 (extract) first - no manifest found.")
|
item = QListWidgetItem("Run Step 0 (extract) first - no manifest found.")
|
||||||
|
|
@ -3652,9 +3656,31 @@ class WolfWorkflowTab(QWidget):
|
||||||
self._fill_injectable_checklist(self.precheck_list)
|
self._fill_injectable_checklist(self.precheck_list)
|
||||||
|
|
||||||
def _refresh_inject_list(self):
|
def _refresh_inject_list(self):
|
||||||
|
"""Show every injectable file (read-only). Inject always uses the full set."""
|
||||||
if not hasattr(self, "inject_list"):
|
if not hasattr(self, "inject_list"):
|
||||||
return
|
return
|
||||||
self._fill_injectable_checklist(self.inject_list)
|
self.inject_list.clear()
|
||||||
|
if hasattr(self, "_inject_count_label"):
|
||||||
|
self._inject_count_label.setText("")
|
||||||
|
if not self._read_manifest():
|
||||||
|
item = QListWidgetItem("Run Step 0 (extract) first - no manifest found.")
|
||||||
|
item.setFlags(Qt.ItemIsEnabled)
|
||||||
|
self.inject_list.addItem(item)
|
||||||
|
return
|
||||||
|
files = self._injectable_filenames()
|
||||||
|
if not files:
|
||||||
|
item = QListWidgetItem("No injectable files in translated/ yet.")
|
||||||
|
item.setFlags(Qt.ItemIsEnabled)
|
||||||
|
self.inject_list.addItem(item)
|
||||||
|
return
|
||||||
|
for json_name in files:
|
||||||
|
item = QListWidgetItem(json_name)
|
||||||
|
item.setFlags(Qt.ItemIsEnabled)
|
||||||
|
self.inject_list.addItem(item)
|
||||||
|
if hasattr(self, "_inject_count_label"):
|
||||||
|
self._inject_count_label.setText(
|
||||||
|
f"{len(files)} file(s) will be injected together."
|
||||||
|
)
|
||||||
|
|
||||||
def _selected_checklist_files(self, list_widget: QListWidget) -> set[str]:
|
def _selected_checklist_files(self, list_widget: QListWidget) -> set[str]:
|
||||||
selected: set[str] = set()
|
selected: set[str] = set()
|
||||||
|
|
@ -3669,9 +3695,6 @@ class WolfWorkflowTab(QWidget):
|
||||||
def _selected_precheck_files(self) -> set[str]:
|
def _selected_precheck_files(self) -> set[str]:
|
||||||
return self._selected_checklist_files(self.precheck_list)
|
return self._selected_checklist_files(self.precheck_list)
|
||||||
|
|
||||||
def _selected_inject_files(self) -> set[str]:
|
|
||||||
return self._selected_checklist_files(self.inject_list)
|
|
||||||
|
|
||||||
def _set_checklist_checks(self, list_widget: QListWidget, checked: bool):
|
def _set_checklist_checks(self, list_widget: QListWidget, checked: bool):
|
||||||
state = Qt.Checked if checked else Qt.Unchecked
|
state = Qt.Checked if checked else Qt.Unchecked
|
||||||
for i in range(list_widget.count()):
|
for i in range(list_widget.count()):
|
||||||
|
|
@ -3821,18 +3844,6 @@ class WolfWorkflowTab(QWidget):
|
||||||
"Saved. Re-run Precheck to confirm the safety skip is gone.",
|
"Saved. Re-run Precheck to confirm the safety skip is gone.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _inject_selected(self):
|
|
||||||
if not self._require_manifest():
|
|
||||||
return
|
|
||||||
selected = self._selected_inject_files()
|
|
||||||
if not selected:
|
|
||||||
QMessageBox.information(
|
|
||||||
self, "Nothing selected",
|
|
||||||
"Tick one or more files in the list, then click Inject selected.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
self._inject(selected)
|
|
||||||
|
|
||||||
def _inject_all(self):
|
def _inject_all(self):
|
||||||
if not self._require_manifest():
|
if not self._require_manifest():
|
||||||
return
|
return
|
||||||
|
|
@ -3845,6 +3856,49 @@ class WolfWorkflowTab(QWidget):
|
||||||
return
|
return
|
||||||
self._inject(set(files))
|
self._inject(set(files))
|
||||||
|
|
||||||
|
def _layout_restore_all(self):
|
||||||
|
"""Re-run wolf layout-restore on every injectable translated JSON."""
|
||||||
|
files = self._injectable_filenames()
|
||||||
|
if not files:
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Layout-restore",
|
||||||
|
"No injectable files found in translated/.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
translated = self._tool_root() / "translated"
|
||||||
|
paths = [translated / name for name in files if (translated / name).is_file()]
|
||||||
|
if not paths:
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Layout-restore",
|
||||||
|
"None of the injectable files exist under translated/.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
def task(log, progress=None):
|
||||||
|
from util import wolfdawn
|
||||||
|
|
||||||
|
log(f"layout-restore: {len(paths)} file(s)…")
|
||||||
|
res = wolfdawn.layout_restore(paths, log_fn=log)
|
||||||
|
for line in (res.stdout or "").splitlines():
|
||||||
|
if line.strip():
|
||||||
|
log(line)
|
||||||
|
fixed = wolfdawn.parse_layout_restore_counts(res.stdout, res.stderr)
|
||||||
|
if not res.ok:
|
||||||
|
err = (res.stderr or res.stdout or "").strip() or f"exit {res.returncode}"
|
||||||
|
return False, f"layout-restore failed: {err}"
|
||||||
|
n = fixed if fixed is not None else 0
|
||||||
|
return True, f"layout-restore fixed {n} line(s) across {len(paths)} file(s)."
|
||||||
|
|
||||||
|
def _after(ok: bool, msg: str):
|
||||||
|
if ok:
|
||||||
|
QMessageBox.information(self, "Layout-restore", msg)
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, "Layout-restore", msg or "layout-restore failed.")
|
||||||
|
|
||||||
|
self._run_task(task, on_done=_after)
|
||||||
|
|
||||||
def _names_check(self):
|
def _names_check(self):
|
||||||
if not self._require_manifest():
|
if not self._require_manifest():
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ to WOLF's native ``Speaker\nline`` layout on write-back. See ``util.speakers``.
|
||||||
Detection is WolfDawn's, so the reliable nameplate (``literal_line1``) is always
|
Detection is WolfDawn's, so the reliable nameplate (``literal_line1``) is always
|
||||||
reshaped; only the low-confidence guess (``literal_line1_lowconf``) is gated by a
|
reshaped; only the low-confidence guess (``literal_line1_lowconf``) is gated by a
|
||||||
per-game, AI-recommended setting from the workflow.
|
per-game, AI-recommended setting from the workflow.
|
||||||
|
|
||||||
|
Layout restore: after each file is written to ``translated/``, ``wolf
|
||||||
|
layout-restore`` copies unambiguous positional whitespace pads from ``source``
|
||||||
|
onto ``text`` (status-card ``<pad>\\nNAME`` fields the model often strips).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
@ -61,6 +65,7 @@ from util.translation import (
|
||||||
)
|
)
|
||||||
from util import speakers as wolf_speakers
|
from util import speakers as wolf_speakers
|
||||||
from util import vocab as wolf_vocab
|
from util import vocab as wolf_vocab
|
||||||
|
from util import wolfdawn
|
||||||
from util.wolfdawn import codes as wolf_codes
|
from util.wolfdawn import codes as wolf_codes
|
||||||
from util.wolfdawn import db_classify as wolf_db
|
from util.wolfdawn import db_classify as wolf_db
|
||||||
from util.wolfdawn import names as wolf_names
|
from util.wolfdawn import names as wolf_names
|
||||||
|
|
@ -149,12 +154,34 @@ def handleWolfDawn(filename, estimate):
|
||||||
# translated/ here would overwrite prior work with source echoed back.
|
# translated/ here would overwrite prior work with source echoed back.
|
||||||
# Real English is written on the consume pass (or a live non-batch run).
|
# Real English is written on the consume pass (or a live non-batch run).
|
||||||
if not estimate and _batch_phase() != "collect":
|
if not estimate and _batch_phase() != "collect":
|
||||||
|
out_path = "translated/" + filename
|
||||||
try:
|
try:
|
||||||
with open("translated/" + filename, "w", encoding="utf-8", newline="\n") as outFile:
|
with open(out_path, "w", encoding="utf-8", newline="\n") as outFile:
|
||||||
json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4)
|
json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4)
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return "Fail"
|
return "Fail"
|
||||||
|
# Restore positional whitespace pads the model dropped (status-card
|
||||||
|
# ``<pad>\nNAME`` fields, etc.). WolfDawn edits the JSON in place.
|
||||||
|
try:
|
||||||
|
res = wolfdawn.layout_restore(out_path)
|
||||||
|
if not res.ok:
|
||||||
|
tqdm.write(
|
||||||
|
Fore.YELLOW
|
||||||
|
+ f"{filename}: layout-restore exited {res.returncode}"
|
||||||
|
+ (f" ({res.stderr.strip()})" if res.stderr.strip() else "")
|
||||||
|
+ Fore.RESET
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fixed = wolfdawn.parse_layout_restore_counts(res.stdout, res.stderr)
|
||||||
|
if fixed:
|
||||||
|
tqdm.write(
|
||||||
|
Fore.CYAN
|
||||||
|
+ f"{filename}: layout-restore fixed {fixed} line(s)"
|
||||||
|
+ Fore.RESET
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
end = time.time()
|
end = time.time()
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ class ListInjectableTests(unittest.TestCase):
|
||||||
class InjectOrderTests(unittest.TestCase):
|
class InjectOrderTests(unittest.TestCase):
|
||||||
def test_names_runs_before_strings(self):
|
def test_names_runs_before_strings(self):
|
||||||
calls: list[str] = []
|
calls: list[str] = []
|
||||||
|
string_bases: list[str] = []
|
||||||
|
|
||||||
def fake_names(*_a, **_k):
|
def fake_names(*_a, **_k):
|
||||||
calls.append("names")
|
calls.append("names")
|
||||||
|
|
@ -49,8 +50,9 @@ class InjectOrderTests(unittest.TestCase):
|
||||||
stderr="",
|
stderr="",
|
||||||
)
|
)
|
||||||
|
|
||||||
def fake_strings(*_a, **_k):
|
def fake_strings(edited_json, base, output, **_k):
|
||||||
calls.append("strings")
|
calls.append("strings")
|
||||||
|
string_bases.append(str(base))
|
||||||
return mock.Mock(
|
return mock.Mock(
|
||||||
ok=True,
|
ok=True,
|
||||||
returncode=0,
|
returncode=0,
|
||||||
|
|
@ -105,6 +107,72 @@ class InjectOrderTests(unittest.TestCase):
|
||||||
self.assertEqual(calls, ["names", "strings"])
|
self.assertEqual(calls, ["names", "strings"])
|
||||||
self.assertTrue(report.ok)
|
self.assertTrue(report.ok)
|
||||||
self.assertEqual(len(report.succeeded), 2)
|
self.assertEqual(len(report.succeeded), 2)
|
||||||
|
# After names-inject, strings must use live Data/ as --base so
|
||||||
|
# name-only fields are not rebuilt from pristine Japanese originals.
|
||||||
|
self.assertEqual(string_bases, [str(map_live)])
|
||||||
|
|
||||||
|
def test_strings_alone_use_pristine_original_base(self):
|
||||||
|
string_bases: list[str] = []
|
||||||
|
|
||||||
|
def fake_strings(edited_json, base, output, **_k):
|
||||||
|
string_bases.append(str(base))
|
||||||
|
return mock.Mock(
|
||||||
|
ok=True,
|
||||||
|
returncode=0,
|
||||||
|
stdout="applied 1 translation(s) (0 drifted)",
|
||||||
|
stderr="",
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
translated = root / "translated"
|
||||||
|
originals = root / "originals"
|
||||||
|
data = root / "data"
|
||||||
|
translated.mkdir()
|
||||||
|
originals.mkdir()
|
||||||
|
data.mkdir()
|
||||||
|
(translated / "Map001.mps.json").write_text(
|
||||||
|
'{"kind":"map","scenes":[]}', encoding="utf-8"
|
||||||
|
)
|
||||||
|
map_live = data / "Map001.mps"
|
||||||
|
map_orig = originals / "Map001.mps"
|
||||||
|
map_live.write_bytes(b"live")
|
||||||
|
map_orig.write_bytes(b"orig")
|
||||||
|
entries = [
|
||||||
|
{"json": "Map001.mps.json", "kind": "map", "base": str(map_live)},
|
||||||
|
]
|
||||||
|
with mock.patch.object(wi.wolfdawn, "strings_inject", side_effect=fake_strings):
|
||||||
|
report = wi.inject_selected(
|
||||||
|
["Map001.mps.json"],
|
||||||
|
manifest_entries=entries,
|
||||||
|
data_dir=data,
|
||||||
|
originals_dir=originals,
|
||||||
|
translated_dir=translated,
|
||||||
|
game_root=data.parent,
|
||||||
|
)
|
||||||
|
self.assertTrue(report.ok)
|
||||||
|
self.assertEqual(string_bases, [str(map_orig)])
|
||||||
|
|
||||||
|
def test_names_result_mentions_safety_skips(self):
|
||||||
|
res = mock.Mock(
|
||||||
|
ok=True,
|
||||||
|
returncode=0,
|
||||||
|
stdout=(
|
||||||
|
"would apply 10 name change(s) (0 drifted/unmatched)\n"
|
||||||
|
"WARNING: 46 line(s) left UNTRANSLATED by a safety guard - "
|
||||||
|
"46 control-code mismatch, 0 not encodable\n"
|
||||||
|
),
|
||||||
|
stderr="",
|
||||||
|
)
|
||||||
|
# parse_names_inject_counts needs "applied N name change"
|
||||||
|
res.stdout = (
|
||||||
|
"applied 10 name change(s) (0 drifted/unmatched)\n"
|
||||||
|
"WARNING: 46 line(s) left UNTRANSLATED by a safety guard - "
|
||||||
|
"46 control-code mismatch, 0 not encodable\n"
|
||||||
|
)
|
||||||
|
result = wi._interpret_names_result("names.json", res)
|
||||||
|
self.assertTrue(result.success)
|
||||||
|
self.assertIn("46 skipped by safety guard", result.summary)
|
||||||
|
|
||||||
|
|
||||||
class ReportDialogTests(unittest.TestCase):
|
class ReportDialogTests(unittest.TestCase):
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,56 @@ class TestTranslationWriteback(unittest.TestCase):
|
||||||
else:
|
else:
|
||||||
os.environ["BATCH_PHASE"] = orig_phase
|
os.environ["BATCH_PHASE"] = orig_phase
|
||||||
|
|
||||||
|
def test_layout_restore_runs_after_write(self):
|
||||||
|
"""After writing translated/, wolf layout-restore restores pad skeletons."""
|
||||||
|
pad = " \n"
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
(root / "files").mkdir()
|
||||||
|
(root / "translated").mkdir()
|
||||||
|
# Use map kind so DB sheet filters from .env cannot skip the line.
|
||||||
|
doc = {
|
||||||
|
"kind": "map",
|
||||||
|
"file": "Map001.mps",
|
||||||
|
"scenes": [
|
||||||
|
{
|
||||||
|
"event": 1,
|
||||||
|
"name": "ev",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"cmd": 0,
|
||||||
|
"str": 0,
|
||||||
|
"source": pad + "なし",
|
||||||
|
"text": pad + "なし",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
(root / "files" / "Map001.mps.json").write_text(
|
||||||
|
json.dumps(doc, ensure_ascii=False), encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
def translate(text, history, history_ctx=None):
|
||||||
|
# Model drops the leading pad / newline - the bug layout-restore fixes.
|
||||||
|
if isinstance(text, list):
|
||||||
|
return [["None" for _ in text], [1, 1]]
|
||||||
|
return ["None", [1, 1]]
|
||||||
|
|
||||||
|
old_cwd = os.getcwd()
|
||||||
|
os.chdir(root)
|
||||||
|
orig_t = wd.translateAI
|
||||||
|
wd.translateAI = translate
|
||||||
|
try:
|
||||||
|
result = wd.handleWolfDawn("Map001.mps.json", estimate=False)
|
||||||
|
finally:
|
||||||
|
wd.translateAI = orig_t
|
||||||
|
os.chdir(old_cwd)
|
||||||
|
|
||||||
|
self.assertNotEqual(result, "Fail")
|
||||||
|
out = json.loads((root / "translated" / "Map001.mps.json").read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(out["scenes"][0]["lines"][0]["text"], pad + "None")
|
||||||
|
|
||||||
def test_echoed_source_does_not_overwrite_text(self):
|
def test_echoed_source_does_not_overwrite_text(self):
|
||||||
"""If the model/collect echoes JP source, leave text alone."""
|
"""If the model/collect echoes JP source, leave text alone."""
|
||||||
doc = {
|
doc = {
|
||||||
|
|
|
||||||
|
|
@ -97,5 +97,45 @@ class TestRelayoutWrappers(unittest.TestCase):
|
||||||
self.assertEqual(wolfdawn.parse_names_wrap_shrink_count(res.stdout), 2)
|
self.assertEqual(wolfdawn.parse_names_wrap_shrink_count(res.stdout), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLayoutRestoreWrapper(unittest.TestCase):
|
||||||
|
def test_layout_restore_flags(self):
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_run(args, log_fn=None):
|
||||||
|
captured["args"] = list(args)
|
||||||
|
return wolfdawn.WolfResult(
|
||||||
|
0,
|
||||||
|
"layout-restore: 1 translation(s) got back their source's whitespace skeleton\n",
|
||||||
|
"",
|
||||||
|
[str(a) for a in args],
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
||||||
|
res = wolfdawn.layout_restore("/tmp/db.json", dry_run=True)
|
||||||
|
self.assertEqual(
|
||||||
|
captured["args"],
|
||||||
|
["layout-restore", "/tmp/db.json", "--dry-run"],
|
||||||
|
)
|
||||||
|
self.assertEqual(wolfdawn.parse_layout_restore_counts(res.stdout), 1)
|
||||||
|
|
||||||
|
def test_layout_restore_multiple_files(self):
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_run(args, log_fn=None):
|
||||||
|
captured["args"] = list(args)
|
||||||
|
return wolfdawn.WolfResult(0, "", "", [str(a) for a in args])
|
||||||
|
|
||||||
|
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
||||||
|
wolfdawn.layout_restore(["/a.json", "/b.json"])
|
||||||
|
self.assertEqual(captured["args"], ["layout-restore", "/a.json", "/b.json"])
|
||||||
|
|
||||||
|
def test_parse_layout_restore_would_count(self):
|
||||||
|
out = (
|
||||||
|
"layout-restore: 3 translation(s) WOULD get back their "
|
||||||
|
"source's whitespace skeleton (dry run; no write)\n"
|
||||||
|
)
|
||||||
|
self.assertEqual(wolfdawn.parse_layout_restore_counts(out), 3)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ __all__ = [
|
||||||
"names_wrap",
|
"names_wrap",
|
||||||
"parse_names_wrap_counts",
|
"parse_names_wrap_counts",
|
||||||
"parse_names_wrap_shrink_count",
|
"parse_names_wrap_shrink_count",
|
||||||
|
"layout_restore",
|
||||||
|
"parse_layout_restore_counts",
|
||||||
"relayout",
|
"relayout",
|
||||||
"desc_relayout",
|
"desc_relayout",
|
||||||
]
|
]
|
||||||
|
|
@ -752,6 +754,47 @@ def names_wrap(
|
||||||
return _run(args, log_fn=log_fn)
|
return _run(args, log_fn=log_fn)
|
||||||
|
|
||||||
|
|
||||||
|
_LAYOUT_RESTORE_APPLIED_RE = re.compile(
|
||||||
|
r"(\d+)\s+translation\(s\)\s+got back their source's whitespace skeleton",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
_LAYOUT_RESTORE_WOULD_RE = re.compile(
|
||||||
|
r"(\d+)\s+translation\(s\)\s+WOULD get back their source's whitespace skeleton",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_layout_restore_counts(stdout: str, stderr: str = "") -> int | None:
|
||||||
|
"""Return how many translations layout-restore fixed (or would fix), or None."""
|
||||||
|
blob = f"{stdout}\n{stderr}"
|
||||||
|
for pattern in (_LAYOUT_RESTORE_APPLIED_RE, _LAYOUT_RESTORE_WOULD_RE):
|
||||||
|
match = pattern.search(blob)
|
||||||
|
if match:
|
||||||
|
return int(match.group(1))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def layout_restore(
|
||||||
|
json_files: Union[PathLike, Iterable[PathLike]],
|
||||||
|
*,
|
||||||
|
dry_run: bool = False,
|
||||||
|
log_fn=None,
|
||||||
|
) -> WolfResult:
|
||||||
|
"""``wolf layout-restore <extract.json>... [--dry-run]``.
|
||||||
|
|
||||||
|
Rebuilds translations that dropped their source's positional whitespace pad
|
||||||
|
skeleton (e.g. status-card ``<pad>\\nNAME`` fields). Edits JSON in place.
|
||||||
|
"""
|
||||||
|
if isinstance(json_files, (str, Path)):
|
||||||
|
paths = [json_files]
|
||||||
|
else:
|
||||||
|
paths = list(json_files)
|
||||||
|
args = ["layout-restore", *[_str(p) for p in paths]]
|
||||||
|
if dry_run:
|
||||||
|
args.append("--dry-run")
|
||||||
|
return _run(args, log_fn=log_fn)
|
||||||
|
|
||||||
|
|
||||||
def _relayout_metric_arg(value: int | str | None, *, zero_is_auto: bool = False) -> str | None:
|
def _relayout_metric_arg(value: int | str | None, *, zero_is_auto: bool = False) -> str | None:
|
||||||
"""Format one relayout geometry flag (``55``, ``auto``, or omit)."""
|
"""Format one relayout geometry flag (``55``, ``auto``, or omit)."""
|
||||||
if value is None:
|
if value is None:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
One list of files in ``translated/``, user picks some or all, each file gets a
|
One list of files in ``translated/``, user picks some or all, each file gets a
|
||||||
clear pass/fail result. ``names.json`` uses ``names-inject`` (must run before
|
clear pass/fail result. ``names.json`` uses ``names-inject`` (must run before
|
||||||
per-file ``strings-inject`` when both are selected).
|
per-file ``strings-inject`` when both are selected). After names-inject, each
|
||||||
|
``strings-inject`` uses the live post-names binary as ``--base`` so name-only
|
||||||
|
DB fields (rumor boards, etc.) are not rebuilt from pristine Japanese originals.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -170,18 +172,21 @@ def _interpret_strings_result(json_name: str, res: wolfdawn.WolfResult) -> FileI
|
||||||
def _interpret_names_result(json_name: str, res: wolfdawn.WolfResult) -> FileInjectResult:
|
def _interpret_names_result(json_name: str, res: wolfdawn.WolfResult) -> FileInjectResult:
|
||||||
cli_err = wolfdawn.parse_inject_cli_error(res.stdout, res.stderr)
|
cli_err = wolfdawn.parse_inject_cli_error(res.stdout, res.stderr)
|
||||||
applied, drifted = wolfdawn.parse_names_inject_counts(res.stdout, res.stderr)
|
applied, drifted = wolfdawn.parse_names_inject_counts(res.stdout, res.stderr)
|
||||||
|
safety = wolfdawn.parse_strings_inject_safety_count(res.stdout, res.stderr)
|
||||||
detail = _wolf_output_snippet(res.stdout, res.stderr)
|
detail = _wolf_output_snippet(res.stdout, res.stderr)
|
||||||
|
|
||||||
if not res.ok:
|
|
||||||
reason = cli_err or f"wolf exited {res.returncode}"
|
|
||||||
return FileInjectResult(json_name, False, reason, applied=applied, detail=detail)
|
|
||||||
|
|
||||||
if wolfdawn.inject_had_applied(applied):
|
if wolfdawn.inject_had_applied(applied):
|
||||||
msg = f"applied {applied} name change(s)"
|
msg = f"applied {applied} name change(s)"
|
||||||
if drifted:
|
if drifted:
|
||||||
msg += f" ({drifted} unmatched)"
|
msg += f" ({drifted} unmatched)"
|
||||||
|
if safety:
|
||||||
|
msg += f" ({safety} skipped by safety guard)"
|
||||||
return FileInjectResult(json_name, True, msg, applied=applied, detail=detail)
|
return FileInjectResult(json_name, True, msg, applied=applied, detail=detail)
|
||||||
|
|
||||||
|
if not res.ok:
|
||||||
|
reason = cli_err or f"wolf exited {res.returncode}"
|
||||||
|
return FileInjectResult(json_name, False, reason, applied=applied, detail=detail)
|
||||||
|
|
||||||
if res.ok and (drifted or 0) > 0:
|
if res.ok and (drifted or 0) > 0:
|
||||||
return FileInjectResult(
|
return FileInjectResult(
|
||||||
json_name,
|
json_name,
|
||||||
|
|
@ -320,9 +325,22 @@ def _inject_strings(
|
||||||
*,
|
*,
|
||||||
allow_code_drift: bool,
|
allow_code_drift: bool,
|
||||||
en_punct: bool,
|
en_punct: bool,
|
||||||
|
base_path: Path | None = None,
|
||||||
) -> FileInjectResult:
|
) -> FileInjectResult:
|
||||||
|
"""Inject one strings JSON.
|
||||||
|
|
||||||
|
``base_path`` defaults to the pristine original. Pass the live binary when
|
||||||
|
names-inject already ran in this batch so name-only fields are preserved.
|
||||||
|
"""
|
||||||
orig = orig_base_for(entry, data_dir, originals_dir)
|
orig = orig_base_for(entry, data_dir, originals_dir)
|
||||||
out = entry["base"]
|
out = entry["base"]
|
||||||
|
base = base_path if base_path is not None else orig
|
||||||
|
if not base.exists():
|
||||||
|
return FileInjectResult(
|
||||||
|
json_name,
|
||||||
|
False,
|
||||||
|
f"no inject base at {base} (re-run Step 0 extract)",
|
||||||
|
)
|
||||||
if not orig.exists():
|
if not orig.exists():
|
||||||
return FileInjectResult(
|
return FileInjectResult(
|
||||||
json_name,
|
json_name,
|
||||||
|
|
@ -330,16 +348,16 @@ def _inject_strings(
|
||||||
f"no pristine original at {orig} (re-run Step 0 extract)",
|
f"no pristine original at {orig} (re-run Step 0 extract)",
|
||||||
)
|
)
|
||||||
|
|
||||||
if entry.get("kind") == "db" and not db_dat_sibling(orig).is_file():
|
if entry.get("kind") == "db" and not db_dat_sibling(base).is_file():
|
||||||
return FileInjectResult(
|
return FileInjectResult(
|
||||||
json_name,
|
json_name,
|
||||||
False,
|
False,
|
||||||
"missing database .dat pair in originals (re-run Step 0 extract)",
|
"missing database .dat pair beside inject base (re-run Step 0 extract)",
|
||||||
)
|
)
|
||||||
|
|
||||||
res = wolfdawn.strings_inject(
|
res = wolfdawn.strings_inject(
|
||||||
str(inject_src),
|
str(inject_src),
|
||||||
str(orig),
|
str(base),
|
||||||
out,
|
out,
|
||||||
allow_code_drift=allow_code_drift,
|
allow_code_drift=allow_code_drift,
|
||||||
en_punct=en_punct,
|
en_punct=en_punct,
|
||||||
|
|
@ -410,6 +428,7 @@ def inject_selected(
|
||||||
if not todo:
|
if not todo:
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
names_applied = False
|
||||||
if NAMES_JSON in todo:
|
if NAMES_JSON in todo:
|
||||||
emit("Preparing live Data/ for names.json…")
|
emit("Preparing live Data/ for names.json…")
|
||||||
names_src = translated_dir / NAMES_JSON
|
names_src = translated_dir / NAMES_JSON
|
||||||
|
|
@ -452,15 +471,24 @@ def inject_selected(
|
||||||
)
|
)
|
||||||
report.files.append(result)
|
report.files.append(result)
|
||||||
emit((" ✓ " if result.success else " ✗ ") + f"{NAMES_JSON}: {result.summary}")
|
emit((" ✓ " if result.success else " ✗ ") + f"{NAMES_JSON}: {result.summary}")
|
||||||
|
names_applied = result.success
|
||||||
|
|
||||||
strings_todo = [n for n in todo if n != NAMES_JSON]
|
strings_todo = [n for n in todo if n != NAMES_JSON]
|
||||||
if strings_todo:
|
if strings_todo:
|
||||||
ensure_db_dat_snapshots(manifest_entries, data_dir, originals_dir)
|
ensure_db_dat_snapshots(manifest_entries, data_dir, originals_dir)
|
||||||
|
if names_applied:
|
||||||
|
emit(
|
||||||
|
" ℹ using live Data/ as strings-inject base "
|
||||||
|
"(preserve name-only fields from names-inject)"
|
||||||
|
)
|
||||||
|
|
||||||
for json_name in strings_todo:
|
for json_name in strings_todo:
|
||||||
entry = by_json[json_name]
|
entry = by_json[json_name]
|
||||||
inject_src = repair_inject_json(translated_dir / json_name)
|
inject_src = repair_inject_json(translated_dir / json_name)
|
||||||
emit(f"Injecting {json_name}…")
|
emit(f"Injecting {json_name}…")
|
||||||
|
# After names-inject, live binaries already hold EN name-only fields.
|
||||||
|
# Rebuilding from pristine JP originals would wipe those (rumor boards, etc.).
|
||||||
|
live_base = Path(entry["base"]) if names_applied else None
|
||||||
result = _inject_strings(
|
result = _inject_strings(
|
||||||
json_name,
|
json_name,
|
||||||
entry,
|
entry,
|
||||||
|
|
@ -469,6 +497,7 @@ def inject_selected(
|
||||||
originals_dir,
|
originals_dir,
|
||||||
allow_code_drift=allow_code_drift,
|
allow_code_drift=allow_code_drift,
|
||||||
en_punct=en_punct,
|
en_punct=en_punct,
|
||||||
|
base_path=live_base,
|
||||||
)
|
)
|
||||||
report.files.append(result)
|
report.files.append(result)
|
||||||
emit((" ✓ " if result.success else " ✗ ") + f"{json_name}: {result.summary}")
|
emit((" ✓ " if result.success else " ✗ ") + f"{json_name}: {result.summary}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue