Simplify Fix Wrap
This commit is contained in:
parent
2b4a0e257f
commit
7079efc639
10 changed files with 553 additions and 824 deletions
10
.env.example
10
.env.example
|
|
@ -65,20 +65,10 @@ listWidth="100"
|
||||||
#The wordwrap of items and help text
|
#The wordwrap of items and help text
|
||||||
noteWidth="75"
|
noteWidth="75"
|
||||||
|
|
||||||
# WolfDawn names.json (Step 3): re-wrap selected note categories with dazedwrap
|
|
||||||
#wolfNameWrap="false"
|
|
||||||
#wolfNameWrapWidth="60"
|
|
||||||
#wolfNameWrapNotes='["├■プロフィール","説明"]'
|
|
||||||
|
|
||||||
# WolfDawn database (Step 4): limit which DB sheets are translated (JSON array of group keys)
|
# WolfDawn database (Step 4): limit which DB sheets are translated (JSON array of group keys)
|
||||||
#wolfDbIncludeGroups='["DataBase.project.json|Skill · 技能"]'
|
#wolfDbIncludeGroups='["DataBase.project.json|Skill · 技能"]'
|
||||||
#wolfDbIncludeTiers='["foundation","system"]'
|
#wolfDbIncludeTiers='["foundation","system"]'
|
||||||
|
|
||||||
# WolfDawn dialogue wrap at translate time is optional / advanced; normal workflow
|
|
||||||
# leaves line breaks alone; optional `wolf relayout` after inject (Step 6, off by default).
|
|
||||||
#wolfWrap="false"
|
|
||||||
#wolfWidth="55"
|
|
||||||
|
|
||||||
#CSV delimiter character (comma, semicolon, or tab)
|
#CSV delimiter character (comma, semicolon, or tab)
|
||||||
csvDelimiter=","
|
csvDelimiter=","
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,14 @@ Mirrors the RPGMaker WorkflowTab, driven by the vendored WolfDawn ``wolf`` CLI
|
||||||
JSON; quick-inject picks just a few translated files for
|
JSON; quick-inject picks just a few translated files for
|
||||||
fast in-game / git iteration, or inject everything at once.
|
fast in-game / git iteration, or inject everything at once.
|
||||||
Step 7 Fix wrap - search translated JSON by in-game text; wrap DB sheet rows;
|
Step 7 Fix wrap - search translated JSON by in-game text; wrap DB sheet rows;
|
||||||
re-inject (Advanced: wolf relayout for event messages)
|
wolf names-wrap, relayout, and desc-relayout
|
||||||
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
|
||||||
Step 9 Saves - fix baked strings in existing .sav files (optional)
|
Step 9 Saves - fix baked strings in existing .sav files (optional)
|
||||||
|
|
||||||
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
|
||||||
(per-name, not per-category), harvests them into vocab.txt, and Step 6 injects the
|
(per-name, not per-category), harvests them into vocab.txt, and Step 6 injects the
|
||||||
result.
|
result. All text wrapping (names-wrap, relayout, manual sheet wrap) lives in Step 7.
|
||||||
|
|
||||||
The extracted JSON is staged in ``<game_root>/wolf_json/`` (a manifest maps each
|
The extracted JSON is staged in ``<game_root>/wolf_json/`` (a manifest maps each
|
||||||
file back to its base binary), then imported into ``files/`` for the shared
|
file back to its base binary), then imported into ``files/`` for the shared
|
||||||
|
|
@ -88,7 +88,6 @@ from gui.workflow_tab import (
|
||||||
from util.wolfdawn import names as wolf_names
|
from util.wolfdawn import names as wolf_names
|
||||||
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 selective_wrap as wolf_selwrap
|
|
||||||
from util.wolfdawn import wrap_search as wolf_ws
|
from util.wolfdawn import wrap_search as wolf_ws
|
||||||
|
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
|
|
@ -2337,8 +2336,6 @@ class WolfWorkflowTab(QWidget):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
layout.addWidget(tl_btn)
|
layout.addWidget(tl_btn)
|
||||||
|
|
||||||
self._add_name_wrap_options(layout)
|
|
||||||
self._refresh_names_summary()
|
self._refresh_names_summary()
|
||||||
|
|
||||||
def _load_names_document(self) -> tuple[dict | None, Path | None]:
|
def _load_names_document(self) -> tuple[dict | None, Path | None]:
|
||||||
|
|
@ -2369,157 +2366,9 @@ class WolfWorkflowTab(QWidget):
|
||||||
self.names_summary_label.setText(
|
self.names_summary_label.setText(
|
||||||
f"{summary}\n\n{categories}\n\nSource: {where}/{NAMES_JSON}"
|
f"{summary}\n\n{categories}\n\nSource: {where}/{NAMES_JSON}"
|
||||||
)
|
)
|
||||||
self._refresh_name_wrap_categories()
|
|
||||||
|
|
||||||
def _add_name_wrap_options(self, layout: QVBoxLayout):
|
|
||||||
"""Per-category dazedwrap for names.json (Step 3)."""
|
|
||||||
layout.addWidget(_make_hr())
|
|
||||||
layout.addWidget(self._subheading("Category word wrap (dazedwrap)"))
|
|
||||||
layout.addWidget(self._desc(
|
|
||||||
"Re-wrap translated name values in selected categories (profile blurbs, "
|
|
||||||
"descriptions, and other multi-line fields) to a character width so English "
|
|
||||||
"fits in-game UI. Short row labels are usually left alone. Applies on the "
|
|
||||||
"next Phase 0 run."
|
|
||||||
))
|
|
||||||
|
|
||||||
wrap_env = self._read_env_values(["wolfNameWrap", "wolfNameWrapWidth", "wolfNameWrapNotes"])
|
|
||||||
self._name_wrap_enable_cb = QCheckBox("Apply dazedwrap to selected categories")
|
|
||||||
enabled = str(wrap_env.get("wolfNameWrap", "false")).strip().lower() == "true"
|
|
||||||
self._name_wrap_enable_cb.setChecked(enabled)
|
|
||||||
self._name_wrap_enable_cb.stateChanged.connect(self._apply_name_wrap_config)
|
|
||||||
layout.addWidget(self._name_wrap_enable_cb)
|
|
||||||
|
|
||||||
width_row = QHBoxLayout()
|
|
||||||
width_lbl = QLabel("Wrap width (characters):")
|
|
||||||
width_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
|
||||||
self._name_wrap_width_spin = QSpinBox()
|
|
||||||
self._name_wrap_width_spin.setRange(20, 300)
|
|
||||||
try:
|
|
||||||
self._name_wrap_width_spin.setValue(int(wrap_env.get("wolfNameWrapWidth") or 60))
|
|
||||||
except ValueError:
|
|
||||||
self._name_wrap_width_spin.setValue(60)
|
|
||||||
self._name_wrap_width_spin.setFixedWidth(70)
|
|
||||||
self._name_wrap_width_spin.valueChanged.connect(self._apply_name_wrap_config)
|
|
||||||
width_row.addWidget(width_lbl)
|
|
||||||
width_row.addWidget(self._name_wrap_width_spin)
|
|
||||||
width_row.addStretch()
|
|
||||||
layout.addLayout(width_row)
|
|
||||||
|
|
||||||
self._name_wrap_notes_list = QListWidget()
|
|
||||||
self._name_wrap_notes_list.setMaximumHeight(180)
|
|
||||||
self._name_wrap_notes_list.setStyleSheet(
|
|
||||||
"QListWidget{background-color:#252526;border:1px solid #3a3a3a;border-radius:4px;"
|
|
||||||
"color:#cccccc;font-size:12px;padding:2px;}"
|
|
||||||
"QListWidget::item{padding:3px 4px;}"
|
|
||||||
"QListWidget::item:selected{background:#264f78;color:#ffffff;}"
|
|
||||||
)
|
|
||||||
self._name_wrap_notes_list.itemChanged.connect(self._apply_name_wrap_config)
|
|
||||||
layout.addWidget(self._name_wrap_notes_list)
|
|
||||||
|
|
||||||
list_row = QHBoxLayout()
|
|
||||||
refresh_cats_btn = _make_btn("↻ Refresh categories", "#3a3a3a")
|
|
||||||
refresh_cats_btn.clicked.connect(self._refresh_name_wrap_categories)
|
|
||||||
sel_all_btn = _make_btn("Select all", "#3a3a3a")
|
|
||||||
sel_all_btn.clicked.connect(lambda: self._set_name_wrap_checks(True))
|
|
||||||
sel_none_btn = _make_btn("Select none", "#3a3a3a")
|
|
||||||
sel_none_btn.clicked.connect(lambda: self._set_name_wrap_checks(False))
|
|
||||||
list_row.addWidget(refresh_cats_btn)
|
|
||||||
list_row.addWidget(sel_all_btn)
|
|
||||||
list_row.addWidget(sel_none_btn)
|
|
||||||
list_row.addStretch()
|
|
||||||
layout.addLayout(list_row)
|
|
||||||
|
|
||||||
self._saved_name_wrap_notes = wolf_names.parse_name_wrap_notes(
|
|
||||||
wrap_env.get("wolfNameWrapNotes", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
def _set_name_wrap_checks(self, checked: bool):
|
|
||||||
if not hasattr(self, "_name_wrap_notes_list"):
|
|
||||||
return
|
|
||||||
state = Qt.Checked if checked else Qt.Unchecked
|
|
||||||
self._name_wrap_notes_list.blockSignals(True)
|
|
||||||
for i in range(self._name_wrap_notes_list.count()):
|
|
||||||
item = self._name_wrap_notes_list.item(i)
|
|
||||||
if item.flags() & Qt.ItemIsUserCheckable:
|
|
||||||
item.setCheckState(state)
|
|
||||||
self._name_wrap_notes_list.blockSignals(False)
|
|
||||||
self._apply_name_wrap_config()
|
|
||||||
|
|
||||||
def _refresh_name_wrap_categories(self):
|
|
||||||
if not hasattr(self, "_name_wrap_notes_list"):
|
|
||||||
return
|
|
||||||
saved = getattr(self, "_saved_name_wrap_notes", frozenset())
|
|
||||||
wrap_env = self._read_env_values(["wolfNameWrapNotes"])
|
|
||||||
if wrap_env.get("wolfNameWrapNotes"):
|
|
||||||
saved = wolf_names.parse_name_wrap_notes(wrap_env["wolfNameWrapNotes"])
|
|
||||||
self._saved_name_wrap_notes = saved
|
|
||||||
|
|
||||||
self._name_wrap_notes_list.blockSignals(True)
|
|
||||||
self._name_wrap_notes_list.clear()
|
|
||||||
data, _src = self._load_names_document()
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
item = QListWidgetItem("Run Step 0 (Extract text) first to list categories.")
|
|
||||||
item.setFlags(Qt.ItemIsEnabled)
|
|
||||||
self._name_wrap_notes_list.addItem(item)
|
|
||||||
self._name_wrap_notes_list.blockSignals(False)
|
|
||||||
return
|
|
||||||
|
|
||||||
notes = wolf_names.collect_name_notes(data)
|
|
||||||
if not notes:
|
|
||||||
item = QListWidgetItem("No categories found in names.json.")
|
|
||||||
item.setFlags(Qt.ItemIsEnabled)
|
|
||||||
self._name_wrap_notes_list.addItem(item)
|
|
||||||
self._name_wrap_notes_list.blockSignals(False)
|
|
||||||
return
|
|
||||||
|
|
||||||
db_labels = wolf_names.derive_db_labels("files")
|
|
||||||
for note in notes:
|
|
||||||
label = wolf_names.note_header(note, db_labels)
|
|
||||||
item = QListWidgetItem(label)
|
|
||||||
item.setData(Qt.UserRole, note)
|
|
||||||
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
|
||||||
item.setCheckState(Qt.Checked if note in saved else Qt.Unchecked)
|
|
||||||
self._name_wrap_notes_list.addItem(item)
|
|
||||||
self._name_wrap_notes_list.blockSignals(False)
|
|
||||||
|
|
||||||
def _selected_name_wrap_notes(self) -> list[str]:
|
|
||||||
if not hasattr(self, "_name_wrap_notes_list"):
|
|
||||||
return []
|
|
||||||
notes: list[str] = []
|
|
||||||
for i in range(self._name_wrap_notes_list.count()):
|
|
||||||
item = self._name_wrap_notes_list.item(i)
|
|
||||||
if not (item.flags() & Qt.ItemIsUserCheckable):
|
|
||||||
continue
|
|
||||||
if item.checkState() == Qt.Checked:
|
|
||||||
note = item.data(Qt.UserRole)
|
|
||||||
if note:
|
|
||||||
notes.append(str(note))
|
|
||||||
return notes
|
|
||||||
|
|
||||||
def _apply_name_wrap_config(self, *args):
|
|
||||||
"""Persist names.json category wrap settings to .env."""
|
|
||||||
if not hasattr(self, "_name_wrap_enable_cb"):
|
|
||||||
return
|
|
||||||
notes = self._selected_name_wrap_notes()
|
|
||||||
enabled = self._name_wrap_enable_cb.isChecked() and bool(notes)
|
|
||||||
updates = {
|
|
||||||
"wolfNameWrap": "true" if enabled else "false",
|
|
||||||
"wolfNameWrapWidth": str(self._name_wrap_width_spin.value()),
|
|
||||||
"wolfNameWrapNotes": json.dumps(notes, ensure_ascii=False),
|
|
||||||
}
|
|
||||||
self._saved_name_wrap_notes = frozenset(notes)
|
|
||||||
if self._write_env_values(updates):
|
|
||||||
if enabled:
|
|
||||||
self._log(
|
|
||||||
"Name category wrap updated: "
|
|
||||||
f"width={updates['wolfNameWrapWidth']}, "
|
|
||||||
f"categories={len(notes)}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._log("Name category wrap disabled.")
|
|
||||||
|
|
||||||
def _names_json_path(self) -> Path | None:
|
def _names_json_path(self) -> Path | None:
|
||||||
"""Locate names.json for reading its note categories (files/ then wolf_json/)."""
|
"""Locate names.json for reading (files/ then wolf_json/)."""
|
||||||
for base in (Path("files"), self._work_dir()):
|
for base in (Path("files"), self._work_dir()):
|
||||||
p = base / NAMES_JSON
|
p = base / NAMES_JSON
|
||||||
if p.is_file():
|
if p.is_file():
|
||||||
|
|
@ -2603,9 +2452,9 @@ class WolfWorkflowTab(QWidget):
|
||||||
"""Search-first fix wrapping: find sheet by in-game text, wrap, re-inject."""
|
"""Search-first fix wrapping: find sheet by in-game text, wrap, re-inject."""
|
||||||
layout.addWidget(_make_section_label("Step 7 · Fix wrapping"))
|
layout.addWidget(_make_section_label("Step 7 · Fix wrapping"))
|
||||||
layout.addWidget(self._desc(
|
layout.addWidget(self._desc(
|
||||||
"Paste text you see overflowing in the game. Search covers database sheets "
|
"Paste overflowing in-game text to find it in translated JSON. Adjust width "
|
||||||
"(Step 4), names.json profile blurbs (Step 3), maps, and common events. "
|
"for one line or wrap every overflowing line in the same group (database "
|
||||||
"Wrap at the right width, then re-inject that file from Step 6 or below."
|
"sheet, names category, map, CommonEvent, or Game.dat), then inject."
|
||||||
))
|
))
|
||||||
|
|
||||||
search_row = QHBoxLayout()
|
search_row = QHBoxLayout()
|
||||||
|
|
@ -2702,21 +2551,40 @@ class WolfWorkflowTab(QWidget):
|
||||||
layout.addWidget(self._wrap_preview)
|
layout.addWidget(self._wrap_preview)
|
||||||
|
|
||||||
btn_row = QHBoxLayout()
|
btn_row = QHBoxLayout()
|
||||||
wrap_row_btn = _make_btn("Wrap this row", "#00a86b")
|
wrap_row_btn = _make_btn("Wrap this line", "#00a86b")
|
||||||
wrap_row_btn.clicked.connect(self._wrap_current_row)
|
wrap_row_btn.clicked.connect(self._wrap_current_row)
|
||||||
wrap_sheet_btn = _make_btn("Wrap all overflowing in sheet", "#00a86b")
|
wrap_group_btn = _make_btn("Wrap all in group", "#00a86b")
|
||||||
wrap_sheet_btn.clicked.connect(self._wrap_current_sheet)
|
wrap_group_btn.clicked.connect(self._wrap_current_group)
|
||||||
save_row_btn = _make_btn("Save text", "#3a3a3a")
|
save_row_btn = _make_btn("Save text", "#3a3a3a")
|
||||||
save_row_btn.clicked.connect(self._save_wrap_row_text)
|
save_row_btn.clicked.connect(self._save_wrap_row_text)
|
||||||
inject_btn = _make_btn("Inject this file", "#007acc")
|
inject_btn = _make_btn("Inject this file", "#007acc")
|
||||||
inject_btn.clicked.connect(self._inject_current_wrap_file)
|
inject_btn.clicked.connect(self._inject_current_wrap_file)
|
||||||
btn_row.addWidget(wrap_row_btn)
|
btn_row.addWidget(wrap_row_btn)
|
||||||
btn_row.addWidget(wrap_sheet_btn)
|
btn_row.addWidget(wrap_group_btn)
|
||||||
btn_row.addWidget(save_row_btn)
|
btn_row.addWidget(save_row_btn)
|
||||||
btn_row.addWidget(inject_btn)
|
btn_row.addWidget(inject_btn)
|
||||||
btn_row.addStretch()
|
btn_row.addStretch()
|
||||||
layout.addLayout(btn_row)
|
layout.addLayout(btn_row)
|
||||||
|
|
||||||
|
context_row = QHBoxLayout()
|
||||||
|
self._wrap_relayout_btn = _make_btn("Relayout messages on Data/ (wolf)", "#3a3a3a")
|
||||||
|
self._wrap_relayout_btn.clicked.connect(lambda: self._run_relayout(manual=True))
|
||||||
|
self._wrap_relayout_btn.setVisible(False)
|
||||||
|
self._wrap_relayout_btn.setToolTip(
|
||||||
|
"Reflow message boxes in live Data/ using JP-measured geometry. "
|
||||||
|
"Run after inject for map and CommonEvent dialogue."
|
||||||
|
)
|
||||||
|
self._wrap_names_smart_btn = _make_btn("Smart wrap names.json (wolf)", "#3a3a3a")
|
||||||
|
self._wrap_names_smart_btn.clicked.connect(lambda: self._run_names_wrap(manual=True))
|
||||||
|
self._wrap_names_smart_btn.setVisible(False)
|
||||||
|
self._wrap_names_smart_btn.setToolTip(
|
||||||
|
"Re-wrap multi-line name fields to each category's JP-measured width."
|
||||||
|
)
|
||||||
|
context_row.addWidget(self._wrap_relayout_btn)
|
||||||
|
context_row.addWidget(self._wrap_names_smart_btn)
|
||||||
|
context_row.addStretch()
|
||||||
|
layout.addLayout(context_row)
|
||||||
|
|
||||||
self._wrap_status_label = QLabel("")
|
self._wrap_status_label = QLabel("")
|
||||||
self._wrap_status_label.setWordWrap(True)
|
self._wrap_status_label.setWordWrap(True)
|
||||||
self._wrap_status_label.setStyleSheet(
|
self._wrap_status_label.setStyleSheet(
|
||||||
|
|
@ -2724,208 +2592,11 @@ class WolfWorkflowTab(QWidget):
|
||||||
)
|
)
|
||||||
layout.addWidget(self._wrap_status_label)
|
layout.addWidget(self._wrap_status_label)
|
||||||
|
|
||||||
layout.addWidget(_make_hr())
|
|
||||||
sheets_toggle = QPushButton("▸ Browse sheets & name categories (overflow counts)")
|
|
||||||
sheets_toggle.setCheckable(True)
|
|
||||||
sheets_toggle.setChecked(False)
|
|
||||||
sheets_toggle.setStyleSheet(
|
|
||||||
"QPushButton{text-align:left;color:#cccccc;background:transparent;border:none;"
|
|
||||||
"font-size:12px;padding:4px 0;}"
|
|
||||||
"QPushButton:hover{color:#ffffff;}"
|
|
||||||
)
|
|
||||||
self._wrap_sheets_toggle = sheets_toggle
|
|
||||||
sheets_toggle.toggled.connect(self._toggle_wrap_sheets_panel)
|
|
||||||
layout.addWidget(sheets_toggle)
|
|
||||||
|
|
||||||
self._wrap_sheets_panel = QWidget()
|
|
||||||
sheets_panel_layout = QVBoxLayout(self._wrap_sheets_panel)
|
|
||||||
sheets_panel_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
sheets_panel_layout.addWidget(self._desc(
|
|
||||||
"Database sheets and names.json categories (``note``) with lines longer than "
|
|
||||||
"the wrap width above. Double-click to open the first overflowing entry."
|
|
||||||
))
|
|
||||||
self._wrap_sheets_list = QListWidget()
|
|
||||||
self._wrap_sheets_list.setMaximumHeight(140)
|
|
||||||
self._wrap_sheets_list.setStyleSheet(
|
|
||||||
"QListWidget{background-color:#252526;border:1px solid #3a3a3a;border-radius:4px;"
|
|
||||||
"color:#cccccc;font-size:12px;}"
|
|
||||||
)
|
|
||||||
self._wrap_sheets_list.itemDoubleClicked.connect(self._on_wrap_sheet_double_clicked)
|
|
||||||
sheets_panel_layout.addWidget(self._wrap_sheets_list)
|
|
||||||
self._wrap_sheets_panel.setVisible(False)
|
|
||||||
layout.addWidget(self._wrap_sheets_panel)
|
|
||||||
|
|
||||||
layout.addWidget(_make_hr())
|
|
||||||
adv_toggle = QPushButton("▸ Advanced (event message windows & item descriptions)")
|
|
||||||
adv_toggle.setCheckable(True)
|
|
||||||
adv_toggle.setChecked(False)
|
|
||||||
adv_toggle.setStyleSheet(sheets_toggle.styleSheet())
|
|
||||||
adv_toggle.toggled.connect(self._toggle_wrap_advanced_panel)
|
|
||||||
layout.addWidget(adv_toggle)
|
|
||||||
self._wrap_advanced_toggle = adv_toggle
|
|
||||||
|
|
||||||
self._wrap_advanced_panel = QWidget()
|
|
||||||
adv_layout = QVBoxLayout(self._wrap_advanced_panel)
|
|
||||||
adv_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self._build_wrap_advanced_panel(adv_layout)
|
|
||||||
self._wrap_advanced_panel.setVisible(False)
|
|
||||||
layout.addWidget(self._wrap_advanced_panel)
|
|
||||||
|
|
||||||
self._current_wrap_hit: wolf_ws.WrapHit | None = None
|
self._current_wrap_hit: wolf_ws.WrapHit | None = None
|
||||||
self._current_wrap_hit_id: dict | None = None
|
self._current_wrap_hit_id: dict | None = None
|
||||||
self._current_wrap_path: Path | None = None
|
self._current_wrap_path: Path | None = None
|
||||||
self._current_wrap_doc: dict | None = None
|
self._current_wrap_doc: dict | None = None
|
||||||
|
|
||||||
def _build_wrap_advanced_panel(self, layout: QVBoxLayout):
|
|
||||||
"""wolf relayout / desc-relayout for non-DB-UI text."""
|
|
||||||
layout.addWidget(self._desc(
|
|
||||||
"Use these only for standard event message windows (maps / CommonEvent) and "
|
|
||||||
"item description boxes (説明 fields). Bulletin boards and custom DB UI "
|
|
||||||
"use the search workflow above."
|
|
||||||
))
|
|
||||||
self._relayout_after_cb = QCheckBox("Automatically relayout after Step 6 inject")
|
|
||||||
self._relayout_after_cb.setChecked(
|
|
||||||
self._setting("relayout_after_inject", "false") == "true"
|
|
||||||
)
|
|
||||||
self._relayout_after_cb.stateChanged.connect(
|
|
||||||
lambda: self._save_setting(
|
|
||||||
"relayout_after_inject",
|
|
||||||
"true" if self._relayout_after_cb.isChecked() else "false",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
layout.addWidget(self._relayout_after_cb)
|
|
||||||
|
|
||||||
msg_row = QHBoxLayout()
|
|
||||||
width_lbl = QLabel("Message width:")
|
|
||||||
width_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
|
||||||
self._relayout_width_spin = QSpinBox()
|
|
||||||
self._relayout_width_spin.setRange(20, 300)
|
|
||||||
try:
|
|
||||||
self._relayout_width_spin.setValue(int(self._setting("relayout_width", 55) or 55))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
self._relayout_width_spin.setValue(55)
|
|
||||||
self._relayout_width_spin.setFixedWidth(70)
|
|
||||||
self._relayout_width_spin.valueChanged.connect(
|
|
||||||
lambda v: self._save_setting("relayout_width", v)
|
|
||||||
)
|
|
||||||
max_rows_lbl = QLabel("Max rows (0=auto):")
|
|
||||||
max_rows_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
|
||||||
self._relayout_max_rows_spin = QSpinBox()
|
|
||||||
self._relayout_max_rows_spin.setRange(0, 20)
|
|
||||||
try:
|
|
||||||
self._relayout_max_rows_spin.setValue(int(self._setting("relayout_max_rows", 0) or 0))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
self._relayout_max_rows_spin.setValue(0)
|
|
||||||
self._relayout_max_rows_spin.setFixedWidth(70)
|
|
||||||
self._relayout_max_rows_spin.valueChanged.connect(
|
|
||||||
lambda v: self._save_setting("relayout_max_rows", v)
|
|
||||||
)
|
|
||||||
msg_row.addWidget(width_lbl)
|
|
||||||
msg_row.addWidget(self._relayout_width_spin)
|
|
||||||
msg_row.addWidget(max_rows_lbl)
|
|
||||||
msg_row.addWidget(self._relayout_max_rows_spin)
|
|
||||||
msg_row.addStretch()
|
|
||||||
layout.addLayout(msg_row)
|
|
||||||
|
|
||||||
relayout_btn = _make_btn("Relayout event messages (wolf relayout)", "#3a3a3a")
|
|
||||||
relayout_btn.clicked.connect(lambda: self._run_relayout(manual=True))
|
|
||||||
layout.addWidget(relayout_btn)
|
|
||||||
|
|
||||||
self._relayout_desc_cb = QCheckBox("Also refit item description boxes (desc-relayout)")
|
|
||||||
self._relayout_desc_cb.setChecked(
|
|
||||||
str(self._setting("relayout_desc", "true")).lower() != "false"
|
|
||||||
)
|
|
||||||
self._relayout_desc_cb.stateChanged.connect(
|
|
||||||
lambda: self._save_setting(
|
|
||||||
"relayout_desc",
|
|
||||||
"true" if self._relayout_desc_cb.isChecked() else "false",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
layout.addWidget(self._relayout_desc_cb)
|
|
||||||
|
|
||||||
desc_row = QHBoxLayout()
|
|
||||||
desc_width_lbl = QLabel("Desc width:")
|
|
||||||
desc_width_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
|
||||||
self._relayout_desc_width_spin = QSpinBox()
|
|
||||||
self._relayout_desc_width_spin.setRange(20, 300)
|
|
||||||
try:
|
|
||||||
self._relayout_desc_width_spin.setValue(
|
|
||||||
int(self._setting("relayout_desc_width", 75) or 75)
|
|
||||||
)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
self._relayout_desc_width_spin.setValue(75)
|
|
||||||
self._relayout_desc_width_spin.setFixedWidth(70)
|
|
||||||
self._relayout_desc_width_spin.valueChanged.connect(
|
|
||||||
lambda v: self._save_setting("relayout_desc_width", v)
|
|
||||||
)
|
|
||||||
desc_lines_lbl = QLabel("Desc max lines (0=auto):")
|
|
||||||
desc_lines_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
|
||||||
self._relayout_desc_lines_spin = QSpinBox()
|
|
||||||
self._relayout_desc_lines_spin.setRange(0, 20)
|
|
||||||
try:
|
|
||||||
self._relayout_desc_lines_spin.setValue(
|
|
||||||
int(self._setting("relayout_desc_max_lines", 0) or 0)
|
|
||||||
)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
self._relayout_desc_lines_spin.setValue(0)
|
|
||||||
self._relayout_desc_lines_spin.setFixedWidth(70)
|
|
||||||
self._relayout_desc_lines_spin.valueChanged.connect(
|
|
||||||
lambda v: self._save_setting("relayout_desc_max_lines", v)
|
|
||||||
)
|
|
||||||
desc_row.addWidget(desc_width_lbl)
|
|
||||||
desc_row.addWidget(self._relayout_desc_width_spin)
|
|
||||||
desc_row.addWidget(desc_lines_lbl)
|
|
||||||
desc_row.addWidget(self._relayout_desc_lines_spin)
|
|
||||||
desc_row.addStretch()
|
|
||||||
layout.addLayout(desc_row)
|
|
||||||
|
|
||||||
wrap_env = self._read_env_values(["wolfWrap", "wolfWidth"])
|
|
||||||
self._wolf_wrap_cb = QCheckBox("Wrap dialogue at translate time (wolfWrap, rare)")
|
|
||||||
self._wolf_wrap_cb.setChecked(
|
|
||||||
str(wrap_env.get("wolfWrap", "false")).strip().lower() == "true"
|
|
||||||
)
|
|
||||||
self._wolf_wrap_cb.stateChanged.connect(self._apply_wolf_wrap_config)
|
|
||||||
layout.addWidget(self._wolf_wrap_cb)
|
|
||||||
tw_row = QHBoxLayout()
|
|
||||||
tw_lbl = QLabel("wolfWrap width:")
|
|
||||||
tw_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
|
||||||
self._wolf_wrap_width_spin = QSpinBox()
|
|
||||||
self._wolf_wrap_width_spin.setRange(20, 300)
|
|
||||||
try:
|
|
||||||
self._wolf_wrap_width_spin.setValue(int(wrap_env.get("wolfWidth") or 55))
|
|
||||||
except ValueError:
|
|
||||||
self._wolf_wrap_width_spin.setValue(55)
|
|
||||||
self._wolf_wrap_width_spin.setFixedWidth(70)
|
|
||||||
self._wolf_wrap_width_spin.valueChanged.connect(self._apply_wolf_wrap_config)
|
|
||||||
tw_row.addWidget(tw_lbl)
|
|
||||||
tw_row.addWidget(self._wolf_wrap_width_spin)
|
|
||||||
tw_row.addStretch()
|
|
||||||
layout.addLayout(tw_row)
|
|
||||||
|
|
||||||
def _toggle_wrap_sheets_panel(self, expanded: bool):
|
|
||||||
if hasattr(self, "_wrap_sheets_panel"):
|
|
||||||
self._wrap_sheets_panel.setVisible(expanded)
|
|
||||||
btn = getattr(self, "_wrap_sheets_toggle", None)
|
|
||||||
if btn is not None:
|
|
||||||
label = "▾ Browse database sheets (overflow counts)" if expanded else (
|
|
||||||
"▸ Browse database sheets (overflow counts)"
|
|
||||||
)
|
|
||||||
btn.setText(label)
|
|
||||||
if expanded:
|
|
||||||
self._refresh_wrap_sheets_list()
|
|
||||||
|
|
||||||
def _toggle_wrap_advanced_panel(self, expanded: bool):
|
|
||||||
if hasattr(self, "_wrap_advanced_panel"):
|
|
||||||
self._wrap_advanced_panel.setVisible(expanded)
|
|
||||||
btn = getattr(self, "_wrap_advanced_toggle", None)
|
|
||||||
if btn is not None:
|
|
||||||
label = (
|
|
||||||
"▾ Advanced (event message windows & item descriptions)"
|
|
||||||
if expanded else
|
|
||||||
"▸ Advanced (event message windows & item descriptions)"
|
|
||||||
)
|
|
||||||
btn.setText(label)
|
|
||||||
|
|
||||||
def _translated_and_files_dirs(self) -> tuple[Path, Path]:
|
def _translated_and_files_dirs(self) -> tuple[Path, Path]:
|
||||||
root = self._tool_root()
|
root = self._tool_root()
|
||||||
return root / "translated", root / "files"
|
return root / "translated", root / "files"
|
||||||
|
|
@ -2956,6 +2627,51 @@ class WolfWorkflowTab(QWidget):
|
||||||
def _on_wrap_width_changed(self, value: int):
|
def _on_wrap_width_changed(self, value: int):
|
||||||
self._save_setting("wrap_fix_width", value)
|
self._save_setting("wrap_fix_width", value)
|
||||||
self._update_wrap_preview()
|
self._update_wrap_preview()
|
||||||
|
hit = self._current_wrap_hit
|
||||||
|
doc = self._current_wrap_doc
|
||||||
|
if hit is not None and doc is not None and hasattr(self, "_wrap_detail_label"):
|
||||||
|
text = self._wrap_text_editor.toPlainText() if self._wrap_text_editor.isEnabled() else hit.text
|
||||||
|
self._wrap_detail_label.setText(self._format_wrap_detail(hit, doc, text, value))
|
||||||
|
|
||||||
|
def _format_wrap_detail(
|
||||||
|
self,
|
||||||
|
hit: wolf_ws.WrapHit,
|
||||||
|
doc: dict | None,
|
||||||
|
text: str,
|
||||||
|
width: int,
|
||||||
|
) -> str:
|
||||||
|
kind_labels = {
|
||||||
|
"db": "Database sheet",
|
||||||
|
"names": "Names category",
|
||||||
|
"gamedat": "Game.dat",
|
||||||
|
"map": "Map dialogue",
|
||||||
|
"common": "CommonEvent dialogue",
|
||||||
|
}
|
||||||
|
kind_lbl = kind_labels.get(hit.kind, hit.kind)
|
||||||
|
scope = (
|
||||||
|
wolf_ws.scope_stats(doc, hit, width)
|
||||||
|
if doc is not None
|
||||||
|
else wolf_ws.ScopeStats(label=wolf_ws.scope_label(hit), total=0, overflow=0)
|
||||||
|
)
|
||||||
|
overflow_note = (
|
||||||
|
f" · {scope.overflow} of {scope.total} lines overflow at width {width}"
|
||||||
|
if scope.total
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
if hit.kind == "names":
|
||||||
|
return (
|
||||||
|
f"{kind_lbl}: {hit.sheet_name} · {hit.json_file} · "
|
||||||
|
f"entry #{hit.row}{overflow_note}"
|
||||||
|
)
|
||||||
|
if hit.kind == "db":
|
||||||
|
return (
|
||||||
|
f"{kind_lbl}: {hit.sheet_name} · {hit.json_file} · "
|
||||||
|
f"row {hit.row} · {hit.field_name}{overflow_note}"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"{kind_lbl}: {hit.sheet_name} · {hit.json_file} · "
|
||||||
|
f"{hit.field_name}{overflow_note}"
|
||||||
|
)
|
||||||
|
|
||||||
def _update_wrap_preview(self):
|
def _update_wrap_preview(self):
|
||||||
if not hasattr(self, "_wrap_preview"):
|
if not hasattr(self, "_wrap_preview"):
|
||||||
|
|
@ -3090,30 +2806,14 @@ class WolfWorkflowTab(QWidget):
|
||||||
text = hit.text
|
text = hit.text
|
||||||
self._wrap_text_editor.setEnabled(True)
|
self._wrap_text_editor.setEnabled(True)
|
||||||
self._wrap_text_editor.setPlainText(text)
|
self._wrap_text_editor.setPlainText(text)
|
||||||
kind_labels = {
|
self._wrap_detail_label.setText(
|
||||||
"db": "Database sheet",
|
self._format_wrap_detail(hit, doc, text, self._wrap_width_spin.value())
|
||||||
"names": "Name / profile text",
|
)
|
||||||
"gamedat": "Game.dat",
|
if hasattr(self, "_wrap_relayout_btn"):
|
||||||
"map": "Event dialogue",
|
show_relayout = hit.kind in ("map", "common")
|
||||||
"common": "Event dialogue",
|
self._wrap_relayout_btn.setVisible(show_relayout)
|
||||||
}
|
if hasattr(self, "_wrap_names_smart_btn"):
|
||||||
kind_lbl = kind_labels.get(hit.kind, hit.kind)
|
self._wrap_names_smart_btn.setVisible(hit.kind == "names")
|
||||||
if hit.kind == "names":
|
|
||||||
detail = (
|
|
||||||
f"{kind_lbl}: {hit.sheet_name} · {hit.json_file} · "
|
|
||||||
f"entry #{hit.row} · longest line {hit.max_line_len} chars"
|
|
||||||
)
|
|
||||||
elif hit.kind == "db":
|
|
||||||
detail = (
|
|
||||||
f"{kind_lbl}: {hit.sheet_name} · {hit.json_file} · "
|
|
||||||
f"row {hit.row} · {hit.field_name} · longest line {hit.max_line_len} chars"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
detail = (
|
|
||||||
f"{kind_lbl}: {hit.sheet_name} · {hit.json_file} · "
|
|
||||||
f"{hit.field_name} · longest line {hit.max_line_len} chars"
|
|
||||||
)
|
|
||||||
self._wrap_detail_label.setText(detail)
|
|
||||||
self._sync_wrap_font_controls(text)
|
self._sync_wrap_font_controls(text)
|
||||||
self._update_wrap_preview()
|
self._update_wrap_preview()
|
||||||
|
|
||||||
|
|
@ -3196,37 +2896,43 @@ class WolfWorkflowTab(QWidget):
|
||||||
)
|
)
|
||||||
if line is not None:
|
if line is not None:
|
||||||
self._wrap_text_editor.setPlainText(str(line.get("text") or ""))
|
self._wrap_text_editor.setPlainText(str(line.get("text") or ""))
|
||||||
msg = f"Wrapped row at width {width}." if changed else "Row 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 6.")
|
self._wrap_status_label.setText(msg + f" Inject {self._current_wrap_path.name} from Step 6.")
|
||||||
self._log(f"{'✅' if changed else 'ℹ'} {msg}")
|
self._log(f"{'✅' if changed else 'ℹ'} {msg}")
|
||||||
self._update_wrap_preview()
|
self._update_wrap_preview()
|
||||||
|
|
||||||
def _wrap_current_sheet(self):
|
def _wrap_current_group(self):
|
||||||
hit = self._current_wrap_hit
|
hit = self._current_wrap_hit
|
||||||
if not hit or hit.kind not in ("db", "names"):
|
if not hit or not self._current_wrap_path or not self._current_wrap_doc:
|
||||||
QMessageBox.information(
|
QMessageBox.information(self, "Fix wrapping", "Select a search result first.")
|
||||||
self, "Fix wrapping",
|
|
||||||
"Select a database sheet or names.json category result first.",
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
tdir, fdir = self._translated_and_files_dirs()
|
|
||||||
path = tdir / hit.json_file
|
|
||||||
if not path.is_file():
|
|
||||||
path = fdir / hit.json_file
|
|
||||||
if not path.is_file():
|
|
||||||
QMessageBox.warning(self, "Fix wrapping", f"{hit.json_file} not found.")
|
|
||||||
return
|
|
||||||
doc = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
||||||
width = self._wrap_width_spin.value()
|
width = self._wrap_width_spin.value()
|
||||||
count = wolf_ws.wrap_overflow_in_sheet(
|
count = wolf_ws.wrap_overflow_in_scope(
|
||||||
path, doc, hit.sheet_name, width, kind=hit.kind,
|
self._current_wrap_path,
|
||||||
|
self._current_wrap_doc,
|
||||||
|
hit,
|
||||||
|
width,
|
||||||
)
|
)
|
||||||
self._remember_sheet_width(hit.sheet_name, width, hit.json_file)
|
self._remember_sheet_width(hit.sheet_name, width, hit.json_file)
|
||||||
self._refresh_wrap_sheets_list()
|
scope = wolf_ws.scope_label(hit)
|
||||||
label = "category" if hit.kind == "names" else "sheet"
|
msg = f"Wrapped {count} line(s) in {scope} at width {width}."
|
||||||
msg = f"Wrapped {count} overflowing line(s) in {label} {hit.sheet_name} at width {width}."
|
self._wrap_status_label.setText(msg + f" Inject {self._current_wrap_path.name}.")
|
||||||
self._wrap_status_label.setText(msg + f" Inject {hit.json_file}.")
|
|
||||||
self._log(f"✅ {msg}")
|
self._log(f"✅ {msg}")
|
||||||
|
path, doc, line = wolf_ws.load_hit_from_id(
|
||||||
|
self._translated_and_files_dirs()[0],
|
||||||
|
hit.hit_id,
|
||||||
|
files_dir=self._translated_and_files_dirs()[1],
|
||||||
|
extra_dirs=self._wrap_search_extra_dirs(),
|
||||||
|
)
|
||||||
|
self._current_wrap_path = path
|
||||||
|
self._current_wrap_doc = doc
|
||||||
|
if line is not None:
|
||||||
|
self._wrap_text_editor.setPlainText(str(line.get("text") or ""))
|
||||||
|
if doc is not None:
|
||||||
|
self._wrap_detail_label.setText(
|
||||||
|
self._format_wrap_detail(hit, doc, self._wrap_text_editor.toPlainText(), width)
|
||||||
|
)
|
||||||
|
self._update_wrap_preview()
|
||||||
|
|
||||||
def _inject_current_wrap_file(self):
|
def _inject_current_wrap_file(self):
|
||||||
hit = self._current_wrap_hit
|
hit = self._current_wrap_hit
|
||||||
|
|
@ -3237,101 +2943,6 @@ class WolfWorkflowTab(QWidget):
|
||||||
return
|
return
|
||||||
self._inject({hit.json_file})
|
self._inject({hit.json_file})
|
||||||
|
|
||||||
def _refresh_wrap_sheets_list(self):
|
|
||||||
if not hasattr(self, "_wrap_sheets_list"):
|
|
||||||
return
|
|
||||||
tdir, fdir = self._translated_and_files_dirs()
|
|
||||||
width = self._wrap_width_spin.value() if hasattr(self, "_wrap_width_spin") else 36
|
|
||||||
summaries = wolf_ws.sheet_overflow_summaries(tdir, width, files_dir=fdir)
|
|
||||||
self._wrap_sheets_list.clear()
|
|
||||||
if not summaries:
|
|
||||||
item = QListWidgetItem("No database or names.json data found.")
|
|
||||||
item.setFlags(Qt.ItemIsEnabled)
|
|
||||||
self._wrap_sheets_list.addItem(item)
|
|
||||||
return
|
|
||||||
for s in sorted(
|
|
||||||
summaries,
|
|
||||||
key=lambda x: (x.kind != "names", -x.overflow_count, x.sheet_name),
|
|
||||||
):
|
|
||||||
kind_tag = "names" if s.kind == "names" else "DB"
|
|
||||||
flag = f" ⚠ {s.overflow_count} overflow" if s.overflow_count else " ok"
|
|
||||||
item = QListWidgetItem(
|
|
||||||
f"[{kind_tag}] {s.sheet_name} ({s.line_count} lines){flag} — {s.json_file}"
|
|
||||||
)
|
|
||||||
item.setData(Qt.UserRole, s.sheet_name)
|
|
||||||
item.setData(Qt.UserRole + 1, s.json_file)
|
|
||||||
item.setData(Qt.UserRole + 2, s.kind)
|
|
||||||
self._wrap_sheets_list.addItem(item)
|
|
||||||
|
|
||||||
def _on_wrap_sheet_double_clicked(self, item: QListWidgetItem):
|
|
||||||
sheet = item.data(Qt.UserRole)
|
|
||||||
jf = item.data(Qt.UserRole + 1)
|
|
||||||
kind = item.data(Qt.UserRole + 2) or "db"
|
|
||||||
if not sheet or not jf:
|
|
||||||
return
|
|
||||||
tdir, fdir = self._translated_and_files_dirs()
|
|
||||||
width = self._wrap_width_spin.value()
|
|
||||||
path = tdir / str(jf)
|
|
||||||
if not path.is_file():
|
|
||||||
path = fdir / str(jf)
|
|
||||||
if not path.is_file():
|
|
||||||
return
|
|
||||||
doc = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
||||||
if kind == "names":
|
|
||||||
for idx, entry in enumerate(doc.get("names") or []):
|
|
||||||
if not isinstance(entry, dict):
|
|
||||||
continue
|
|
||||||
if str(entry.get("note") or "") != sheet:
|
|
||||||
continue
|
|
||||||
text = entry.get("text")
|
|
||||||
if isinstance(text, str) and wolf_selwrap.line_needs_wrap(text, width):
|
|
||||||
hit = wolf_ws.WrapHit(
|
|
||||||
json_file=str(jf),
|
|
||||||
kind="names",
|
|
||||||
sheet_name=str(sheet),
|
|
||||||
row=idx,
|
|
||||||
field_name=str(entry.get("source") or "")[:80] or f"entry {idx}",
|
|
||||||
text=str(text),
|
|
||||||
max_line_len=dazedwrap.max_line_visible_length(str(text)),
|
|
||||||
)
|
|
||||||
self._show_wrap_hit_in_editor(hit, width)
|
|
||||||
return
|
|
||||||
return
|
|
||||||
for group in doc.get("groups") or []:
|
|
||||||
if str(group.get("typeName") or "") != sheet:
|
|
||||||
continue
|
|
||||||
for line in group.get("lines") or []:
|
|
||||||
text = line.get("text")
|
|
||||||
if isinstance(text, str) and wolf_selwrap.line_needs_wrap(text, width):
|
|
||||||
hit = wolf_ws.WrapHit(
|
|
||||||
json_file=str(jf),
|
|
||||||
kind="db",
|
|
||||||
sheet_name=str(sheet),
|
|
||||||
row=line.get("row"),
|
|
||||||
field_name=str(line.get("fieldName") or ""),
|
|
||||||
text=str(text),
|
|
||||||
max_line_len=dazedwrap.max_line_visible_length(str(text)),
|
|
||||||
)
|
|
||||||
self._show_wrap_hit_in_editor(hit, width)
|
|
||||||
return
|
|
||||||
|
|
||||||
def _show_wrap_hit_in_editor(self, hit: wolf_ws.WrapHit, width: int):
|
|
||||||
self._wrap_search_edit.setText(str(hit.text)[:40])
|
|
||||||
self._wrap_results_list.clear()
|
|
||||||
item_r = QListWidgetItem(hit.summary(width))
|
|
||||||
item_r.setData(Qt.UserRole, hit.hit_id)
|
|
||||||
item_r.setData(Qt.UserRole + 1, hit)
|
|
||||||
self._wrap_results_list.addItem(item_r)
|
|
||||||
self._wrap_results_list.setCurrentRow(0)
|
|
||||||
|
|
||||||
def _apply_wolf_wrap_config(self):
|
|
||||||
enabled = self._wolf_wrap_cb.isChecked() if hasattr(self, "_wolf_wrap_cb") else False
|
|
||||||
width = self._wolf_wrap_width_spin.value() if hasattr(self, "_wolf_wrap_width_spin") else 55
|
|
||||||
self._write_env_values({
|
|
||||||
"wolfWrap": "true" if enabled else "false",
|
|
||||||
"wolfWidth": str(width),
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -3437,29 +3048,9 @@ class WolfWorkflowTab(QWidget):
|
||||||
elif body:
|
elif body:
|
||||||
QMessageBox.information(self, title, body)
|
QMessageBox.information(self, title, body)
|
||||||
|
|
||||||
selection = inject_state.get("selected") or set()
|
|
||||||
names_only = selection and selection.issubset({NAMES_JSON})
|
|
||||||
if names_only or not self._relayout_after_enabled():
|
|
||||||
self._log("Relayout: not run")
|
|
||||||
return
|
|
||||||
if not report or not any(
|
|
||||||
r.success and r.json_name != NAMES_JSON for r in report.files
|
|
||||||
):
|
|
||||||
self._log("Relayout: not run")
|
|
||||||
return
|
|
||||||
QTimer.singleShot(
|
|
||||||
0, lambda: self._run_relayout(manual=False, quiet=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
inject_state: dict = {}
|
inject_state: dict = {}
|
||||||
self._run_task(task, on_done=_after_inject)
|
self._run_task(task, on_done=_after_inject)
|
||||||
|
|
||||||
def _relayout_after_enabled(self) -> bool:
|
|
||||||
cb = getattr(self, "_relayout_after_cb", None)
|
|
||||||
if cb is not None:
|
|
||||||
return cb.isChecked()
|
|
||||||
return self._setting("relayout_after_inject", "false") == "true"
|
|
||||||
|
|
||||||
def _on_step_changed(self, idx: int):
|
def _on_step_changed(self, idx: int):
|
||||||
previous = self._current_step_index
|
previous = self._current_step_index
|
||||||
self._current_step_index = idx
|
self._current_step_index = idx
|
||||||
|
|
@ -3468,17 +3059,12 @@ class WolfWorkflowTab(QWidget):
|
||||||
# Index 3 == Names: refresh the per-name safety summary.
|
# Index 3 == Names: refresh the per-name safety summary.
|
||||||
if idx == 3:
|
if idx == 3:
|
||||||
self._refresh_names_summary()
|
self._refresh_names_summary()
|
||||||
self._refresh_name_wrap_categories()
|
|
||||||
# Index 4 == Database: refresh discovery report and sheet list.
|
# Index 4 == Database: refresh discovery report and sheet list.
|
||||||
elif idx == 4:
|
elif idx == 4:
|
||||||
self._refresh_db_discovery()
|
self._refresh_db_discovery()
|
||||||
# Index 6 == Inject: keep the file list in sync with translated/.
|
# Index 6 == Inject: keep the file list in sync with translated/.
|
||||||
elif idx == 6:
|
elif idx == 6:
|
||||||
self._refresh_inject_list()
|
self._refresh_inject_list()
|
||||||
# Index 7 == Fix wrap: refresh sheet overflow list.
|
|
||||||
elif idx == 7:
|
|
||||||
if getattr(self, "_wrap_sheets_panel", None) and self._wrap_sheets_panel.isVisible():
|
|
||||||
self._refresh_wrap_sheets_list()
|
|
||||||
|
|
||||||
def _refresh_inject_list(self):
|
def _refresh_inject_list(self):
|
||||||
if not hasattr(self, "inject_list"):
|
if not hasattr(self, "inject_list"):
|
||||||
|
|
@ -3568,28 +3154,25 @@ class WolfWorkflowTab(QWidget):
|
||||||
self._run_task(task)
|
self._run_task(task)
|
||||||
|
|
||||||
def _relayout_desc_enabled(self) -> bool:
|
def _relayout_desc_enabled(self) -> bool:
|
||||||
cb = getattr(self, "_relayout_desc_cb", None)
|
|
||||||
if cb is not None:
|
|
||||||
return cb.isChecked()
|
|
||||||
return str(self._setting("relayout_desc", "true")).lower() != "false"
|
return str(self._setting("relayout_desc", "true")).lower() != "false"
|
||||||
|
|
||||||
def _relayout_settings(self) -> dict:
|
def _relayout_settings(self) -> dict:
|
||||||
def _spin(attr: str, key: str, default: int) -> int:
|
def _int_setting(key: str, default: int) -> int:
|
||||||
w = getattr(self, attr, None)
|
|
||||||
if w is not None:
|
|
||||||
return int(w.value())
|
|
||||||
try:
|
try:
|
||||||
return int(self._setting(key, default) or default)
|
return int(self._setting(key, default) or default)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
def _width(key: str) -> int | str:
|
||||||
|
if str(self._setting(f"{key}_auto", "true")).lower() != "false":
|
||||||
|
return "auto"
|
||||||
|
return _int_setting(key, 55 if key == "relayout_width" else 75)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"width": _spin("_relayout_width_spin", "relayout_width", 55),
|
"width": _width("relayout_width"),
|
||||||
"max_rows": _spin("_relayout_max_rows_spin", "relayout_max_rows", 0),
|
"max_rows": _int_setting("relayout_max_rows", 0),
|
||||||
"desc_width": _spin("_relayout_desc_width_spin", "relayout_desc_width", 75),
|
"desc_width": _width("relayout_desc_width"),
|
||||||
"desc_max_lines": _spin(
|
"desc_max_lines": _int_setting("relayout_desc_max_lines", 0),
|
||||||
"_relayout_desc_lines_spin", "relayout_desc_max_lines", 0
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _find_db_projects(self, data_dir: Path) -> list[Path]:
|
def _find_db_projects(self, data_dir: Path) -> list[Path]:
|
||||||
|
|
@ -3643,7 +3226,7 @@ class WolfWorkflowTab(QWidget):
|
||||||
data_dir,
|
data_dir,
|
||||||
out_dir=out_dir,
|
out_dir=out_dir,
|
||||||
width=opts["width"],
|
width=opts["width"],
|
||||||
max_rows=opts["max_rows"] or None,
|
max_rows=opts["max_rows"],
|
||||||
log_fn=None,
|
log_fn=None,
|
||||||
)
|
)
|
||||||
if not res.ok and res.returncode not in (0, 2):
|
if not res.ok and res.returncode not in (0, 2):
|
||||||
|
|
@ -3690,6 +3273,35 @@ class WolfWorkflowTab(QWidget):
|
||||||
|
|
||||||
self._run_task(task)
|
self._run_task(task)
|
||||||
|
|
||||||
|
def _run_names_wrap(self, *, manual: bool = False, dry_run: bool = False):
|
||||||
|
"""Run ``wolf names-wrap`` on translated/names.json."""
|
||||||
|
names_path = self._tool_root() / "translated" / NAMES_JSON
|
||||||
|
if not names_path.is_file():
|
||||||
|
names_path = self._work_dir() / NAMES_JSON
|
||||||
|
if not names_path.is_file():
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"names-wrap",
|
||||||
|
"No names.json found in translated/ or wolf_json/. Run Step 0 first.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
def task(log, progress=None):
|
||||||
|
ok, summary = wolf_names.apply_names_wrap(
|
||||||
|
names_path,
|
||||||
|
dry_run=dry_run,
|
||||||
|
log_fn=log,
|
||||||
|
)
|
||||||
|
if not ok:
|
||||||
|
return False, f"names-wrap failed — {summary}"
|
||||||
|
if manual:
|
||||||
|
self._sync_translated_json_to_wolf_json(NAMES_JSON, self._work_dir())
|
||||||
|
return True, summary + (
|
||||||
|
"" if dry_run else " Re-inject names.json from Step 6 if needed."
|
||||||
|
)
|
||||||
|
|
||||||
|
self._run_task(task)
|
||||||
|
|
||||||
# ── Step 5: Package ────────────────────────────────────────────────────────
|
# ── Step 5: Package ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _build_step5_package(self, layout: QVBoxLayout):
|
def _build_step5_package(self, layout: QVBoxLayout):
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,6 @@ names.json, translatable entries are harvested into ``vocab.txt`` (grouped by
|
||||||
``note``, with bilingual headers) so later phases keep item / skill / term names
|
``note``, with bilingual headers) so later phases keep item / skill / term names
|
||||||
consistent.
|
consistent.
|
||||||
|
|
||||||
Text wrapping: optional advanced ``wolfWrap`` / ``wolfWidth`` (via :mod:`util.dazedwrap`)
|
|
||||||
can still re-wrap dialogue bodies at translate time, but the normal Wolf workflow
|
|
||||||
leaves JSON line breaks alone and runs WolfDawn ``relayout`` after inject (Step 6)
|
|
||||||
to fit the message box. Defaults to off (``wolfWrap=false``).
|
|
||||||
|
|
||||||
names.json category wrap (Step 3): selected ``note`` categories can be re-wrapped
|
|
||||||
with dazedwrap at a dedicated width (``.env``: ``wolfNameWrap``,
|
|
||||||
``wolfNameWrapWidth``, ``wolfNameWrapNotes``). Other name categories keep the
|
|
||||||
model output verbatim.
|
|
||||||
|
|
||||||
Database sheet filter (Step 4): optional ``wolfDbIncludeGroups`` or
|
Database sheet filter (Step 4): optional ``wolfDbIncludeGroups`` or
|
||||||
``wolfDbIncludeTiers`` in ``.env`` limit which ``kind: db`` lines are collected.
|
``wolfDbIncludeTiers`` in ``.env`` limit which ``kind: db`` lines are collected.
|
||||||
When unset, all database lines are translated.
|
When unset, all database lines are translated.
|
||||||
|
|
@ -61,8 +51,6 @@ import traceback
|
||||||
|
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
import util.dazedwrap as dazedwrap
|
|
||||||
from util.paths import PROMPT_PATH, VOCAB_PATH
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
from util.translation import (
|
from util.translation import (
|
||||||
TranslationConfig,
|
TranslationConfig,
|
||||||
|
|
@ -110,25 +98,6 @@ _WOLF_CODE_RE = re.compile(
|
||||||
# configurable from the workflow (data/wolf_speakers.json).
|
# configurable from the workflow (data/wolf_speakers.json).
|
||||||
SPEAKER_CONFIG = wolf_speakers.load_config()
|
SPEAKER_CONFIG = wolf_speakers.load_config()
|
||||||
|
|
||||||
# names.json: translate per-entry safe badges only (see util.wolfdawn.names).
|
|
||||||
|
|
||||||
# Text wrapping: optional advanced rewrap via dazedwrap (wolfWrap/wolfWidth). Defaults
|
|
||||||
# off - the workflow uses WolfDawn relayout after inject instead.
|
|
||||||
WRAP = (os.getenv("wolfWrap", "false").strip().lower() == "true")
|
|
||||||
try:
|
|
||||||
WRAPWIDTH = int(os.getenv("wolfWidth") or 0)
|
|
||||||
except ValueError:
|
|
||||||
WRAPWIDTH = 0
|
|
||||||
|
|
||||||
# names.json: optional per-category dazedwrap (Step 3 workflow UI).
|
|
||||||
NAME_WRAP_ENABLED, NAME_WRAP_WIDTH, NAME_WRAP_NOTES = wolf_names.load_name_wrap_config()
|
|
||||||
|
|
||||||
|
|
||||||
def reload_name_wrap_config() -> None:
|
|
||||||
"""Re-read names.json category wrap settings from ``.env``."""
|
|
||||||
global NAME_WRAP_ENABLED, NAME_WRAP_WIDTH, NAME_WRAP_NOTES
|
|
||||||
NAME_WRAP_ENABLED, NAME_WRAP_WIDTH, NAME_WRAP_NOTES = wolf_names.load_name_wrap_config()
|
|
||||||
|
|
||||||
# Pricing / batching from the configured model
|
# Pricing / batching from the configured model
|
||||||
PRICING_CONFIG = getPricingConfig(MODEL)
|
PRICING_CONFIG = getPricingConfig(MODEL)
|
||||||
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
||||||
|
|
@ -167,7 +136,6 @@ def handleWolfDawn(filename, estimate):
|
||||||
# Re-read workflow-configured settings so edits made this session take effect
|
# Re-read workflow-configured settings so edits made this session take effect
|
||||||
# even when translation runs in-process (the module import is cached).
|
# even when translation runs in-process (the module import is cached).
|
||||||
SPEAKER_CONFIG = wolf_speakers.load_config()
|
SPEAKER_CONFIG = wolf_speakers.load_config()
|
||||||
reload_name_wrap_config()
|
|
||||||
# Reload the glossary so a later phase (DB text / dialogue) picks up names
|
# Reload the glossary so a later phase (DB text / dialogue) picks up names
|
||||||
# that an earlier Phase 0 (names) harvested into vocab.txt.
|
# that an earlier Phase 0 (names) harvested into vocab.txt.
|
||||||
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
|
|
@ -177,8 +145,7 @@ def handleWolfDawn(filename, estimate):
|
||||||
translatedData = openFiles(filename)
|
translatedData = openFiles(filename)
|
||||||
|
|
||||||
# Batch collect only queues API requests; text is still Japanese. Writing
|
# Batch collect only queues API requests; text is still Japanese. Writing
|
||||||
# translated/ here would overwrite prior work with rewrapped source (and with
|
# translated/ here would overwrite prior work with source echoed back.
|
||||||
# wolfWrap on, the git diff looks like "translations" that never changed).
|
|
||||||
# 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":
|
||||||
try:
|
try:
|
||||||
|
|
@ -252,42 +219,6 @@ def collectEntries(data):
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def _wrap_body(body):
|
|
||||||
"""Word-wrap a dialogue body to WRAPWIDTH (no-op when wrapping is disabled)."""
|
|
||||||
if not WRAP or WRAPWIDTH <= 0 or not isinstance(body, str) or not body:
|
|
||||||
return body
|
|
||||||
return dazedwrap.wrapText(body, WRAPWIDTH)
|
|
||||||
|
|
||||||
|
|
||||||
def _wrap_names_entry(text, entry):
|
|
||||||
"""Apply dazedwrap to a names.json entry when its ``note`` is selected in Step 3."""
|
|
||||||
if not NAME_WRAP_ENABLED or NAME_WRAP_WIDTH <= 0:
|
|
||||||
return text
|
|
||||||
if not isinstance(text, str) or not text:
|
|
||||||
return text
|
|
||||||
note = str(entry.get("note", ""))
|
|
||||||
if note not in NAME_WRAP_NOTES:
|
|
||||||
return text
|
|
||||||
return dazedwrap.wrapText(text, NAME_WRAP_WIDTH)
|
|
||||||
|
|
||||||
|
|
||||||
def _wrap_plain(text, is_firstline):
|
|
||||||
"""Wrap a non-reshaped entry, protecting a nameplate first line if present.
|
|
||||||
|
|
||||||
``is_firstline`` is True for first-line-speaker formats that are turned off in
|
|
||||||
the config: their model output is still ``Speaker\\nbody``, so line 1 (the
|
|
||||||
name) is kept intact and only the body is wrapped. Any leading ``@<option>``
|
|
||||||
window prefix is also preserved.
|
|
||||||
"""
|
|
||||||
if not WRAP or WRAPWIDTH <= 0 or not isinstance(text, str) or not text:
|
|
||||||
return text
|
|
||||||
prefix, rest = wolf_speakers.split_window_prefix(text)
|
|
||||||
if is_firstline and "\n" in rest:
|
|
||||||
name, body = rest.split("\n", 1)
|
|
||||||
return prefix + name + "\n" + _wrap_body(body)
|
|
||||||
return prefix + _wrap_body(rest)
|
|
||||||
|
|
||||||
|
|
||||||
def _text_check_body(text: str, speaker_src: str = "") -> str:
|
def _text_check_body(text: str, speaker_src: str = "") -> str:
|
||||||
"""Body text used for skip-translated checks (nameplates / codes ignored).
|
"""Body text used for skip-translated checks (nameplates / codes ignored).
|
||||||
|
|
||||||
|
|
@ -404,20 +335,16 @@ def parseDocument(data, filename):
|
||||||
if text == src:
|
if text == src:
|
||||||
continue
|
continue
|
||||||
text = wolf_codes.restore_wolf_code_placeholders(text, code_map)
|
text = wolf_codes.restore_wolf_code_placeholders(text, code_map)
|
||||||
if is_names:
|
if has_speaker:
|
||||||
entry["text"] = _wrap_names_entry(text, entry)
|
|
||||||
elif has_speaker:
|
|
||||||
speaker_en, body_en = wolf_speakers.parse_prefixed(text)
|
speaker_en, body_en = wolf_speakers.parse_prefixed(text)
|
||||||
if speaker_en is not None:
|
if speaker_en is not None:
|
||||||
# Wrap only the body; the name stays on its own line.
|
|
||||||
entry["text"] = wolf_speakers.restore_source(
|
entry["text"] = wolf_speakers.restore_source(
|
||||||
prefix, speaker_en, _wrap_body(body_en)
|
prefix, speaker_en, body_en
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Model dropped the [Speaker]: prefix; keep its output as-is.
|
entry["text"] = prefix + text
|
||||||
entry["text"] = prefix + _wrap_body(text)
|
|
||||||
else:
|
else:
|
||||||
entry["text"] = _wrap_plain(text, is_firstline)
|
entry["text"] = text
|
||||||
wolf_codes.repair_entry(entry)
|
wolf_codes.repair_entry(entry)
|
||||||
|
|
||||||
# Phase 0 feeds the glossary: harvest safe name values into vocab.txt
|
# Phase 0 feeds the glossary: harvest safe name values into vocab.txt
|
||||||
|
|
|
||||||
|
|
@ -101,19 +101,6 @@ class TestCollectNameNotes(unittest.TestCase):
|
||||||
def test_collects_unique_sorted_notes(self):
|
def test_collects_unique_sorted_notes(self):
|
||||||
self.assertEqual(wn.collect_name_notes(self.DOC), ["技能", "武器"])
|
self.assertEqual(wn.collect_name_notes(self.DOC), ["技能", "武器"])
|
||||||
|
|
||||||
def test_parse_json_notes(self):
|
|
||||||
self.assertEqual(
|
|
||||||
wn.parse_name_wrap_notes('["武器","├■プロフィール"]'),
|
|
||||||
frozenset({"武器", "├■プロフィール"}),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_parse_comma_separated_notes(self):
|
|
||||||
self.assertEqual(
|
|
||||||
wn.parse_name_wrap_notes("武器,技能"),
|
|
||||||
frozenset({"武器", "技能"}),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDeriveDbLabels(unittest.TestCase):
|
class TestDeriveDbLabels(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self._tmp = tempfile.TemporaryDirectory()
|
self._tmp = tempfile.TemporaryDirectory()
|
||||||
|
|
|
||||||
|
|
@ -263,5 +263,64 @@ class TestNamesCategoryWrap(unittest.TestCase):
|
||||||
self.assertIn("\n", long_entry)
|
self.assertIn("\n", long_entry)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEventScopeWrap(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.translated = Path(self.tmp.name) / "translated"
|
||||||
|
self.translated.mkdir()
|
||||||
|
self.path = self.translated / "Map001.mps.json"
|
||||||
|
self.path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"kind": "map",
|
||||||
|
"file": "Map001.mps",
|
||||||
|
"scenes": [
|
||||||
|
{
|
||||||
|
"event": 1,
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"text": (
|
||||||
|
"This is a very long line of dialogue that "
|
||||||
|
"should wrap at thirty four visible chars."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{"text": "Short line."},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=4,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
self.hit = ws.WrapHit(
|
||||||
|
json_file="Map001.mps.json",
|
||||||
|
kind="map",
|
||||||
|
sheet_name="Map001.mps",
|
||||||
|
row=1,
|
||||||
|
field_name="scene 0 line 0",
|
||||||
|
text="very long line",
|
||||||
|
max_line_len=80,
|
||||||
|
scene_index=0,
|
||||||
|
line_index=0,
|
||||||
|
map_file="Map001.mps",
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.tmp.cleanup()
|
||||||
|
|
||||||
|
def test_scope_stats_for_map_file(self):
|
||||||
|
doc = json.loads(self.path.read_text(encoding="utf-8"))
|
||||||
|
stats = ws.scope_stats(doc, self.hit, 34)
|
||||||
|
self.assertEqual(stats.total, 2)
|
||||||
|
self.assertEqual(stats.overflow, 1)
|
||||||
|
|
||||||
|
def test_wrap_all_in_map_file(self):
|
||||||
|
doc = json.loads(self.path.read_text(encoding="utf-8"))
|
||||||
|
changed = ws.wrap_overflow_in_scope(self.path, doc, self.hit, 34)
|
||||||
|
self.assertEqual(changed, 1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -493,10 +493,8 @@ SPEAKER_MAP_DOC = {
|
||||||
class _SpeakerHarness:
|
class _SpeakerHarness:
|
||||||
"""Run parseDocument with the speaker-aware mock and a chosen speaker config."""
|
"""Run parseDocument with the speaker-aware mock and a chosen speaker config."""
|
||||||
|
|
||||||
def __init__(self, config, wrap=False, width=0):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.wrap = wrap
|
|
||||||
self.width = width
|
|
||||||
self.captured = []
|
self.captured = []
|
||||||
|
|
||||||
def run(self, data, filename="OP.mps.json"):
|
def run(self, data, filename="OP.mps.json"):
|
||||||
|
|
@ -504,17 +502,15 @@ class _SpeakerHarness:
|
||||||
self.captured.append(copy.deepcopy(text))
|
self.captured.append(copy.deepcopy(text))
|
||||||
return _mock_translate_speaker(text, history, history_ctx)
|
return _mock_translate_speaker(text, history, history_ctx)
|
||||||
|
|
||||||
orig = (wd.translateAI, wd.ESTIMATE, wd.SPEAKER_CONFIG, wd.WRAP, wd.WRAPWIDTH)
|
orig = (wd.translateAI, wd.ESTIMATE, wd.SPEAKER_CONFIG)
|
||||||
wd.translateAI = translate
|
wd.translateAI = translate
|
||||||
wd.ESTIMATE = False
|
wd.ESTIMATE = False
|
||||||
wd.SPEAKER_CONFIG = self.config
|
wd.SPEAKER_CONFIG = self.config
|
||||||
wd.WRAP = self.wrap
|
|
||||||
wd.WRAPWIDTH = self.width
|
|
||||||
try:
|
try:
|
||||||
result = wd.parseDocument(copy.deepcopy(data), filename)
|
result = wd.parseDocument(copy.deepcopy(data), filename)
|
||||||
return result, self.captured
|
return result, self.captured
|
||||||
finally:
|
finally:
|
||||||
(wd.translateAI, wd.ESTIMATE, wd.SPEAKER_CONFIG, wd.WRAP, wd.WRAPWIDTH) = orig
|
(wd.translateAI, wd.ESTIMATE, wd.SPEAKER_CONFIG) = orig
|
||||||
|
|
||||||
|
|
||||||
class TestSpeakerReshaping(unittest.TestCase):
|
class TestSpeakerReshaping(unittest.TestCase):
|
||||||
|
|
@ -549,132 +545,6 @@ class TestSpeakerReshaping(unittest.TestCase):
|
||||||
self.assertEqual(lines[0]["text"], "EN_市民\nおはよう\n元気?")
|
self.assertEqual(lines[0]["text"], "EN_市民\nおはよう\n元気?")
|
||||||
|
|
||||||
|
|
||||||
class TestWrapping(unittest.TestCase):
|
|
||||||
"""dazedwrap-style re-wrapping, with the speaker name kept on its own line."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self._orig = (wd.WRAP, wd.WRAPWIDTH)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
wd.WRAP, wd.WRAPWIDTH = self._orig
|
|
||||||
|
|
||||||
def _set(self, enabled, width):
|
|
||||||
wd.WRAP, wd.WRAPWIDTH = enabled, width
|
|
||||||
|
|
||||||
def test_wrap_body_respects_width_and_keeps_words(self):
|
|
||||||
self._set(True, 10)
|
|
||||||
out = wd._wrap_body("alpha beta gamma delta")
|
|
||||||
self.assertGreater(out.count("\n"), 0) # actually wrapped
|
|
||||||
for line in out.split("\n"):
|
|
||||||
self.assertLessEqual(len(line), 10)
|
|
||||||
self.assertEqual(out.replace("\n", " "), "alpha beta gamma delta")
|
|
||||||
|
|
||||||
def test_wrap_disabled_is_noop(self):
|
|
||||||
self._set(False, 10)
|
|
||||||
self.assertEqual(wd._wrap_body("alpha beta gamma delta"), "alpha beta gamma delta")
|
|
||||||
|
|
||||||
def test_zero_width_is_noop(self):
|
|
||||||
self._set(True, 0)
|
|
||||||
self.assertEqual(wd._wrap_body("alpha beta gamma delta"), "alpha beta gamma delta")
|
|
||||||
|
|
||||||
def test_wrap_plain_preserves_nameplate_line(self):
|
|
||||||
self._set(True, 12)
|
|
||||||
out = wd._wrap_plain("Name\nalpha beta gamma delta epsilon", is_firstline=True)
|
|
||||||
self.assertEqual(out.split("\n", 1)[0], "Name") # name never merged into body
|
|
||||||
for line in out.split("\n")[1:]:
|
|
||||||
self.assertLessEqual(len(line), 12)
|
|
||||||
|
|
||||||
def test_wrap_plain_preserves_window_prefix(self):
|
|
||||||
self._set(True, 12)
|
|
||||||
out = wd._wrap_plain("@1\nName\nalpha beta gamma delta", is_firstline=True)
|
|
||||||
self.assertTrue(out.startswith("@1\nName\n"))
|
|
||||||
|
|
||||||
def test_speaker_body_wrapped_but_name_kept(self):
|
|
||||||
# Integration: even at a tiny width the (translated) name stays on line 1.
|
|
||||||
cfg = {"literal_line1": True, "literal_line1_lowconf": True}
|
|
||||||
(data, _t, err), _c = _SpeakerHarness(cfg, wrap=True, width=6).run(SPEAKER_MAP_DOC)
|
|
||||||
self.assertIsNone(err)
|
|
||||||
line0 = data["scenes"][0]["lines"][0]["text"]
|
|
||||||
self.assertEqual(line0.split("\n", 1)[0], "EN_市民")
|
|
||||||
|
|
||||||
|
|
||||||
class TestNameCategoryWrap(unittest.TestCase):
|
|
||||||
"""dazedwrap for selected names.json note categories (Step 3)."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self._orig = (wd.NAME_WRAP_ENABLED, wd.NAME_WRAP_WIDTH, wd.NAME_WRAP_NOTES)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
wd.NAME_WRAP_ENABLED, wd.NAME_WRAP_WIDTH, wd.NAME_WRAP_NOTES = self._orig
|
|
||||||
|
|
||||||
def _set(self, enabled, width, notes):
|
|
||||||
wd.NAME_WRAP_ENABLED = enabled
|
|
||||||
wd.NAME_WRAP_WIDTH = width
|
|
||||||
wd.NAME_WRAP_NOTES = frozenset(notes)
|
|
||||||
|
|
||||||
def test_wrap_names_entry_only_selected_notes(self):
|
|
||||||
self._set(True, 10, {"├■プロフィール"})
|
|
||||||
long_text = "alpha beta gamma delta epsilon"
|
|
||||||
entry = {"note": "├■プロフィール"}
|
|
||||||
wrapped = wd._wrap_names_entry(long_text, entry)
|
|
||||||
self.assertGreater(wrapped.count("\n"), 0)
|
|
||||||
entry_other = {"note": "武器"}
|
|
||||||
self.assertEqual(wd._wrap_names_entry(long_text, entry_other), long_text)
|
|
||||||
|
|
||||||
def test_wrap_names_entry_disabled_is_noop(self):
|
|
||||||
self._set(False, 10, {"├■プロフィール"})
|
|
||||||
long_text = "alpha beta gamma delta epsilon"
|
|
||||||
entry = {"note": "├■プロフィール"}
|
|
||||||
self.assertEqual(wd._wrap_names_entry(long_text, entry), long_text)
|
|
||||||
|
|
||||||
def test_names_translation_wraps_selected_category(self):
|
|
||||||
# Narrow JP source (8 cells of CJK) so LANGREGEX matches; mock returns long EN.
|
|
||||||
profile_src = "あいうえ" # 4 CJK → matches LANGREGEX
|
|
||||||
doc = {
|
|
||||||
"kind": "names",
|
|
||||||
"names": [
|
|
||||||
{"source": "剣", "text": "剣", "note": "武器", "safety": "safe"},
|
|
||||||
{
|
|
||||||
"source": profile_src,
|
|
||||||
"text": profile_src,
|
|
||||||
"note": "├■プロフィール",
|
|
||||||
"safety": "safe",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
self._set(True, 8, {"├■プロフィール"})
|
|
||||||
long_en = "alpha beta gamma delta epsilon zeta eta theta"
|
|
||||||
orig_t = wd.translateAI
|
|
||||||
orig_estimate = wd.ESTIMATE
|
|
||||||
orig_wrap = wd.WRAP
|
|
||||||
orig_update = wd.wolf_vocab.update_vocab_section
|
|
||||||
orig_labels = wd.wolf_names.derive_db_labels
|
|
||||||
|
|
||||||
def translate(text, history, history_ctx=None):
|
|
||||||
out = []
|
|
||||||
for t in text:
|
|
||||||
out.append(long_en if t == profile_src else f"EN_{t}")
|
|
||||||
return [out, [1, 1]]
|
|
||||||
|
|
||||||
wd.translateAI = translate
|
|
||||||
wd.ESTIMATE = False
|
|
||||||
wd.WRAP = False
|
|
||||||
wd.wolf_vocab.update_vocab_section = lambda *_a, **_k: None
|
|
||||||
wd.wolf_names.derive_db_labels = lambda _p: {}
|
|
||||||
try:
|
|
||||||
data, _t, err = wd.parseDocument(copy.deepcopy(doc), "names.json")
|
|
||||||
finally:
|
|
||||||
wd.translateAI = orig_t
|
|
||||||
wd.ESTIMATE = orig_estimate
|
|
||||||
wd.WRAP = orig_wrap
|
|
||||||
wd.wolf_vocab.update_vocab_section = orig_update
|
|
||||||
wd.wolf_names.derive_db_labels = orig_labels
|
|
||||||
self.assertIsNone(err)
|
|
||||||
names = {n["note"]: n["text"] for n in data["names"]}
|
|
||||||
self.assertEqual(names["武器"], "EN_剣")
|
|
||||||
self.assertGreater(names["├■プロフィール"].count("\n"), 0)
|
|
||||||
|
|
||||||
|
|
||||||
class TestOpenFiles(unittest.TestCase):
|
class TestOpenFiles(unittest.TestCase):
|
||||||
def test_rejects_unknown_kind(self):
|
def test_rejects_unknown_kind(self):
|
||||||
with tempfile.TemporaryDirectory() as td:
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ class TestRelayoutWrappers(unittest.TestCase):
|
||||||
self.assertIn("--width", captured["args"])
|
self.assertIn("--width", captured["args"])
|
||||||
self.assertIn("55", captured["args"])
|
self.assertIn("55", captured["args"])
|
||||||
self.assertNotIn("-o", captured["args"])
|
self.assertNotIn("-o", captured["args"])
|
||||||
self.assertNotIn("--max-rows", captured["args"]) # 0 omitted
|
self.assertIn("--max-rows", captured["args"])
|
||||||
|
self.assertIn("auto", captured["args"])
|
||||||
|
|
||||||
def test_relayout_write_args(self):
|
def test_relayout_write_args(self):
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
@ -79,9 +80,56 @@ class TestRelayoutWrappers(unittest.TestCase):
|
||||||
self.assertIn("--width", args)
|
self.assertIn("--width", args)
|
||||||
self.assertIn("75", args)
|
self.assertIn("75", args)
|
||||||
self.assertIn("--max-lines", args)
|
self.assertIn("--max-lines", args)
|
||||||
self.assertIn("0", args)
|
self.assertIn("auto", args)
|
||||||
self.assertIn("--keep-breaks", args)
|
self.assertIn("--keep-breaks", args)
|
||||||
|
|
||||||
|
def test_relayout_auto_width(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.relayout("/game/Data", width="auto", max_rows="auto")
|
||||||
|
args = captured["args"]
|
||||||
|
self.assertIn("--width", args)
|
||||||
|
self.assertIn("auto", args)
|
||||||
|
self.assertIn("--max-rows", args)
|
||||||
|
|
||||||
|
def test_names_wrap_args(self):
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_run(args, log_fn=None):
|
||||||
|
captured["args"] = list(args)
|
||||||
|
return wolfdawn.WolfResult(
|
||||||
|
0,
|
||||||
|
"names-wrap: 3 entry(ies) re-wrapped",
|
||||||
|
"",
|
||||||
|
[str(a) for a in args],
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
||||||
|
res = wolfdawn.names_wrap("/tmp/names.json")
|
||||||
|
self.assertEqual(captured["args"], ["names-wrap", "/tmp/names.json"])
|
||||||
|
self.assertEqual(wolfdawn.parse_names_wrap_counts(res.stdout), 3)
|
||||||
|
|
||||||
|
def test_names_wrap_dry_run(self):
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_run(args, log_fn=None):
|
||||||
|
captured["args"] = list(args)
|
||||||
|
return wolfdawn.WolfResult(
|
||||||
|
0,
|
||||||
|
"names-wrap: 2 entry(ies) WOULD be re-wrapped",
|
||||||
|
"",
|
||||||
|
[str(a) for a in args],
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
||||||
|
wolfdawn.names_wrap("/tmp/names.json", dry_run=True)
|
||||||
|
self.assertEqual(captured["args"], ["names-wrap", "/tmp/names.json", "--dry-run"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ __all__ = [
|
||||||
"pack",
|
"pack",
|
||||||
"save_update",
|
"save_update",
|
||||||
"names_check",
|
"names_check",
|
||||||
|
"names_wrap",
|
||||||
|
"parse_names_wrap_counts",
|
||||||
"relayout",
|
"relayout",
|
||||||
"desc_relayout",
|
"desc_relayout",
|
||||||
]
|
]
|
||||||
|
|
@ -655,6 +657,58 @@ def names_check(json_files: Iterable[PathLike], log_fn=None) -> WolfResult:
|
||||||
return _run(args, log_fn=log_fn)
|
return _run(args, log_fn=log_fn)
|
||||||
|
|
||||||
|
|
||||||
|
_NAMES_WRAP_WOULD_RE = re.compile(
|
||||||
|
r"(\d+)\s+entry\(ies\)\s+WOULD\s+be\s+re-wrapped", re.IGNORECASE
|
||||||
|
)
|
||||||
|
_NAMES_WRAP_APPLIED_RE = re.compile(
|
||||||
|
r"(\d+)\s+entry\(ies\)\s+re-wrapped", re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_names_wrap_counts(stdout: str, stderr: str = "") -> int | None:
|
||||||
|
"""Return how many entries names-wrap would change or did change, or None."""
|
||||||
|
blob = f"{stdout}\n{stderr}"
|
||||||
|
for pattern in (_NAMES_WRAP_APPLIED_RE, _NAMES_WRAP_WOULD_RE):
|
||||||
|
match = pattern.search(blob)
|
||||||
|
if match:
|
||||||
|
return int(match.group(1))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def names_wrap(
|
||||||
|
names_json: PathLike,
|
||||||
|
*,
|
||||||
|
dry_run: bool = False,
|
||||||
|
log_fn=None,
|
||||||
|
) -> WolfResult:
|
||||||
|
"""``wolf names-wrap <names.json> [--dry-run]``.
|
||||||
|
|
||||||
|
Re-wraps translated multi-line glossary entries to each category's
|
||||||
|
JP-measured width and line count. One-line names are never wrapped.
|
||||||
|
"""
|
||||||
|
args = ["names-wrap", _str(names_json)]
|
||||||
|
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:
|
||||||
|
"""Format one relayout geometry flag (``55``, ``auto``, or omit)."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, str):
|
||||||
|
token = value.strip().lower()
|
||||||
|
if not token or token == "auto":
|
||||||
|
return "auto"
|
||||||
|
try:
|
||||||
|
value = int(token)
|
||||||
|
except ValueError:
|
||||||
|
return token
|
||||||
|
if zero_is_auto and int(value) == 0:
|
||||||
|
return "auto"
|
||||||
|
return str(int(value))
|
||||||
|
|
||||||
|
|
||||||
def pack(
|
def pack(
|
||||||
directory: PathLike,
|
directory: PathLike,
|
||||||
output: PathLike,
|
output: PathLike,
|
||||||
|
|
@ -700,12 +754,12 @@ def relayout(
|
||||||
data_dir: PathLike,
|
data_dir: PathLike,
|
||||||
out_dir: Optional[PathLike] = None,
|
out_dir: Optional[PathLike] = None,
|
||||||
*,
|
*,
|
||||||
width: Optional[int] = None,
|
width: int | str | None = None,
|
||||||
width_face: Optional[int] = None,
|
width_face: int | str | None = None,
|
||||||
max_rows: Optional[int] = None,
|
max_rows: int | str | None = None,
|
||||||
sub_width: Optional[int] = None,
|
sub_width: Optional[int] = None,
|
||||||
base_font: Optional[int] = None,
|
base_font: int | str | None = None,
|
||||||
no_formup: bool = False,
|
no_fixup: bool = False,
|
||||||
no_resolve: bool = False,
|
no_resolve: bool = False,
|
||||||
log_fn=None,
|
log_fn=None,
|
||||||
) -> WolfResult:
|
) -> WolfResult:
|
||||||
|
|
@ -713,22 +767,26 @@ def relayout(
|
||||||
|
|
||||||
Without ``-o`` this is a dry run. With ``-o``, only changed map / CommonEvent
|
Without ``-o`` this is a dry run. With ``-o``, only changed map / CommonEvent
|
||||||
files are written under ``out_dir`` (paths relative to ``data_dir``).
|
files are written under ``out_dir`` (paths relative to ``data_dir``).
|
||||||
|
Pass ``width="auto"`` or ``max_rows=0`` to measure geometry from JP text.
|
||||||
"""
|
"""
|
||||||
args: list[str] = ["relayout", _str(data_dir)]
|
args: list[str] = ["relayout", _str(data_dir)]
|
||||||
if out_dir is not None:
|
if out_dir is not None:
|
||||||
args += ["-o", _str(out_dir)]
|
args += ["-o", _str(out_dir)]
|
||||||
if width is not None:
|
width_arg = _relayout_metric_arg(width)
|
||||||
args += ["--width", str(int(width))]
|
if width_arg is not None:
|
||||||
if width_face is not None:
|
args += ["--width", width_arg]
|
||||||
args += ["--width-face", str(int(width_face))]
|
width_face_arg = _relayout_metric_arg(width_face)
|
||||||
if max_rows is not None and int(max_rows) > 0:
|
if width_face_arg is not None:
|
||||||
args += ["--max-rows", str(int(max_rows))]
|
args += ["--width-face", width_face_arg]
|
||||||
|
if max_rows is not None:
|
||||||
|
args += ["--max-rows", _relayout_metric_arg(max_rows, zero_is_auto=True) or "auto"]
|
||||||
if sub_width is not None:
|
if sub_width is not None:
|
||||||
args += ["--sub-width", str(int(sub_width))]
|
args += ["--sub-width", str(int(sub_width))]
|
||||||
if base_font is not None:
|
if base_font is not None:
|
||||||
args += ["--base-font", str(int(base_font))]
|
base_arg = _relayout_metric_arg(base_font, zero_is_auto=True)
|
||||||
if no_formup:
|
args += ["--base-font", base_arg if base_arg is not None else "0"]
|
||||||
args.append("--no-formup")
|
if no_fixup:
|
||||||
|
args.append("--no-fixup")
|
||||||
if no_resolve:
|
if no_resolve:
|
||||||
args.append("--no-resolve")
|
args.append("--no-resolve")
|
||||||
return _run(args, log_fn=log_fn)
|
return _run(args, log_fn=log_fn)
|
||||||
|
|
@ -738,33 +796,36 @@ def desc_relayout(
|
||||||
project: PathLike,
|
project: PathLike,
|
||||||
output: PathLike,
|
output: PathLike,
|
||||||
*,
|
*,
|
||||||
width: Optional[int] = None,
|
width: int | str | None = None,
|
||||||
max_lines: Optional[int] = None,
|
max_lines: int | str | None = None,
|
||||||
font: Optional[int] = None,
|
font: int | str | None = None,
|
||||||
min_font: Optional[int] = None,
|
min_font: Optional[int] = None,
|
||||||
types: Optional[str] = None,
|
types: Optional[str] = None,
|
||||||
keep_breaks: bool = False,
|
keep_breaks: bool = False,
|
||||||
no_formup: bool = False,
|
no_fixup: bool = False,
|
||||||
log_fn=None,
|
log_fn=None,
|
||||||
) -> WolfResult:
|
) -> WolfResult:
|
||||||
"""``wolf desc-relayout <X.project> -o <out.project> [flags]``.
|
"""``wolf desc-relayout <X.project> -o <out.project> [flags]``.
|
||||||
|
|
||||||
``width`` may be omitted when ``wolfdawn-roles.json`` defines ``descBoxes``.
|
``width`` may be omitted or set to ``auto`` to measure from JP text.
|
||||||
``max_lines=0`` means auto (read each field's ``[N行]`` hint).
|
``max_lines=0`` means auto (read each field's ``[N行]`` hint).
|
||||||
"""
|
"""
|
||||||
args: list[str] = ["desc-relayout", _str(project), "-o", _str(output)]
|
args: list[str] = ["desc-relayout", _str(project), "-o", _str(output)]
|
||||||
if width is not None:
|
width_arg = _relayout_metric_arg(width)
|
||||||
args += ["--width", str(int(width))]
|
if width_arg is not None:
|
||||||
|
args += ["--width", width_arg]
|
||||||
if max_lines is not None:
|
if max_lines is not None:
|
||||||
args += ["--max-lines", str(int(max_lines))]
|
max_arg = _relayout_metric_arg(max_lines, zero_is_auto=True)
|
||||||
|
args += ["--max-lines", max_arg if max_arg is not None else "0"]
|
||||||
if font is not None:
|
if font is not None:
|
||||||
args += ["--font", str(int(font))]
|
font_arg = _relayout_metric_arg(font, zero_is_auto=True)
|
||||||
|
args += ["--font", font_arg if font_arg is not None else "0"]
|
||||||
if min_font is not None:
|
if min_font is not None:
|
||||||
args += ["--min-font", str(int(min_font))]
|
args += ["--min-font", str(int(min_font))]
|
||||||
if types:
|
if types:
|
||||||
args += ["--types", str(types)]
|
args += ["--types", str(types)]
|
||||||
if keep_breaks:
|
if keep_breaks:
|
||||||
args.append("--keep-breaks")
|
args.append("--keep-breaks")
|
||||||
if no_formup:
|
if no_fixup:
|
||||||
args.append("--no-formup")
|
args.append("--no-fixup")
|
||||||
return _run(args, log_fn=log_fn)
|
return _run(args, log_fn=log_fn)
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,10 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from pathlib import Path
|
||||||
|
from typing import Any, Union
|
||||||
|
|
||||||
|
PathLike = Union[str, Path]
|
||||||
|
|
||||||
SAFETY_SAFE = "safe"
|
SAFETY_SAFE = "safe"
|
||||||
SAFETY_REFS = "refs"
|
SAFETY_REFS = "refs"
|
||||||
|
|
@ -227,28 +230,46 @@ def collect_name_notes(data: dict[str, Any]) -> list[str]:
|
||||||
return sorted(notes)
|
return sorted(notes)
|
||||||
|
|
||||||
|
|
||||||
def parse_name_wrap_notes(raw: str) -> frozenset[str]:
|
def apply_names_wrap(
|
||||||
"""Parse ``wolfNameWrapNotes`` from ``.env`` (JSON array or comma-separated)."""
|
names_path: PathLike,
|
||||||
text = (raw or "").strip()
|
*,
|
||||||
if not text:
|
dry_run: bool = False,
|
||||||
return frozenset()
|
log_fn=None,
|
||||||
try:
|
) -> tuple[bool, str]:
|
||||||
parsed = json.loads(text)
|
"""Run ``wolf names-wrap`` on one names.json file.
|
||||||
if isinstance(parsed, list):
|
|
||||||
return frozenset(str(item).strip() for item in parsed if str(item).strip())
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
pass
|
|
||||||
return frozenset(part.strip() for part in text.split(",") if part.strip())
|
|
||||||
|
|
||||||
|
Returns ``(ok, summary)``. *ok* is False only when the CLI exits with a hard
|
||||||
|
error; overflow warnings still count as success.
|
||||||
|
"""
|
||||||
|
from util import wolfdawn
|
||||||
|
|
||||||
def load_name_wrap_config() -> tuple[bool, int, frozenset[str]]:
|
emit = log_fn or (lambda _msg: None)
|
||||||
"""Return ``(enabled, width, notes)`` from workflow ``.env`` settings."""
|
path = Path(names_path)
|
||||||
import os
|
if not path.is_file():
|
||||||
|
return False, f"names.json not found: {path}"
|
||||||
|
|
||||||
enabled = os.getenv("wolfNameWrap", "false").strip().lower() == "true"
|
res = wolfdawn.names_wrap(path, dry_run=dry_run, log_fn=None)
|
||||||
try:
|
for line in (res.stdout or "").splitlines():
|
||||||
width = int(os.getenv("wolfNameWrapWidth") or 0)
|
line = line.strip()
|
||||||
except ValueError:
|
if line:
|
||||||
width = 0
|
emit(line)
|
||||||
notes = parse_name_wrap_notes(os.getenv("wolfNameWrapNotes", ""))
|
for line in (res.stderr or "").splitlines():
|
||||||
return enabled, width, notes
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
emit(line)
|
||||||
|
|
||||||
|
if not res.ok and res.returncode not in (0, 2):
|
||||||
|
return False, f"names-wrap exited {res.returncode}"
|
||||||
|
|
||||||
|
count = wolfdawn.parse_names_wrap_counts(res.stdout, res.stderr)
|
||||||
|
if dry_run:
|
||||||
|
summary = (
|
||||||
|
f"dry run: {count} entr{'y' if count == 1 else 'ies'} would be re-wrapped"
|
||||||
|
if count is not None
|
||||||
|
else "dry run complete"
|
||||||
|
)
|
||||||
|
elif count is not None:
|
||||||
|
summary = f"re-wrapped {count} entr{'y' if count == 1 else 'ies'}"
|
||||||
|
else:
|
||||||
|
summary = "names-wrap complete"
|
||||||
|
return True, summary
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"""Search translated WolfDawn JSON for text to fix wrapping.
|
"""Search translated WolfDawn JSON for text to fix wrapping.
|
||||||
|
|
||||||
Primary workflow: paste in-game text, find the database sheet and row, wrap at
|
Paste in-game text, open the matching line, then wrap that line or every
|
||||||
the right width, re-inject. Sheet names match ``typeName`` in DB JSON and
|
overflowing line in the same group (database sheet, names category, map /
|
||||||
``note`` in names.json (e.g. ``├■街の噂(MOB)``).
|
CommonEvent file, or Game.dat).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -493,6 +493,160 @@ def wrap_line_in_doc(line: dict[str, Any], width: int) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ScopeStats:
|
||||||
|
"""Line counts for the wrap group containing one search hit."""
|
||||||
|
|
||||||
|
label: str
|
||||||
|
total: int
|
||||||
|
overflow: int
|
||||||
|
|
||||||
|
|
||||||
|
def scope_label(hit: WrapHit | dict[str, Any]) -> str:
|
||||||
|
"""Human-readable group name for status messages."""
|
||||||
|
if isinstance(hit, WrapHit):
|
||||||
|
kind, sheet, jf = hit.kind, hit.sheet_name, hit.json_file
|
||||||
|
else:
|
||||||
|
kind = str(hit.get("kind") or "")
|
||||||
|
sheet = str(hit.get("sheet_name") or "")
|
||||||
|
jf = str(hit.get("json_file") or "")
|
||||||
|
if kind == "db":
|
||||||
|
return f"sheet {sheet}"
|
||||||
|
if kind == "names":
|
||||||
|
return f"category {sheet}"
|
||||||
|
if kind == "gamedat":
|
||||||
|
return "Game.dat"
|
||||||
|
if kind == "common":
|
||||||
|
return "CommonEvent"
|
||||||
|
if kind == "map":
|
||||||
|
return sheet or jf
|
||||||
|
return sheet or jf
|
||||||
|
|
||||||
|
|
||||||
|
def scope_stats(
|
||||||
|
doc: dict[str, Any],
|
||||||
|
hit: WrapHit | dict[str, Any],
|
||||||
|
width: int,
|
||||||
|
) -> ScopeStats:
|
||||||
|
"""Count lines and overflows in the same group as *hit*."""
|
||||||
|
if isinstance(hit, WrapHit):
|
||||||
|
kind, sheet_name = hit.kind, hit.sheet_name
|
||||||
|
else:
|
||||||
|
kind = str(hit.get("kind") or doc.get("kind") or "")
|
||||||
|
sheet_name = str(hit.get("sheet_name") or "")
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
overflow = 0
|
||||||
|
|
||||||
|
if kind == "db":
|
||||||
|
for group in doc.get("groups") or []:
|
||||||
|
if str(group.get("typeName") or "") != sheet_name:
|
||||||
|
continue
|
||||||
|
for line in group.get("lines") or []:
|
||||||
|
text = line.get("text")
|
||||||
|
if not isinstance(text, str) or not text.strip():
|
||||||
|
continue
|
||||||
|
total += 1
|
||||||
|
if line_needs_wrap(text, width):
|
||||||
|
overflow += 1
|
||||||
|
break
|
||||||
|
elif kind == "names":
|
||||||
|
for entry in doc.get("names") or []:
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
if str(entry.get("note") or "") != sheet_name:
|
||||||
|
continue
|
||||||
|
text = entry.get("text")
|
||||||
|
if not isinstance(text, str) or not text.strip():
|
||||||
|
continue
|
||||||
|
total += 1
|
||||||
|
if line_needs_wrap(text, width):
|
||||||
|
overflow += 1
|
||||||
|
elif kind in ("map", "common"):
|
||||||
|
for scene in doc.get("scenes") or []:
|
||||||
|
for line in scene.get("lines") or []:
|
||||||
|
text = line.get("text")
|
||||||
|
if not isinstance(text, str) or not text.strip():
|
||||||
|
continue
|
||||||
|
total += 1
|
||||||
|
if line_needs_wrap(text, width):
|
||||||
|
overflow += 1
|
||||||
|
elif kind == "gamedat":
|
||||||
|
for line in doc.get("lines") or []:
|
||||||
|
if not isinstance(line, dict):
|
||||||
|
continue
|
||||||
|
text = line.get("text")
|
||||||
|
if not isinstance(text, str) or not text.strip():
|
||||||
|
continue
|
||||||
|
total += 1
|
||||||
|
if line_needs_wrap(text, width):
|
||||||
|
overflow += 1
|
||||||
|
|
||||||
|
return ScopeStats(label=scope_label(hit), total=total, overflow=overflow)
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_overflow_in_event_file(path: Path, doc: dict[str, Any], width: int) -> int:
|
||||||
|
"""Wrap overflowing dialogue lines in one map or CommonEvent JSON file."""
|
||||||
|
if doc.get("kind") not in ("map", "common"):
|
||||||
|
return 0
|
||||||
|
changed = 0
|
||||||
|
for scene in doc.get("scenes") or []:
|
||||||
|
for line in scene.get("lines") or []:
|
||||||
|
text = line.get("text")
|
||||||
|
if not isinstance(text, str) or not text.strip():
|
||||||
|
continue
|
||||||
|
if not line_needs_wrap(text, width):
|
||||||
|
continue
|
||||||
|
if wrap_line_in_doc(line, width):
|
||||||
|
changed += 1
|
||||||
|
if changed:
|
||||||
|
save_document(path, doc)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_overflow_in_gamedat(path: Path, doc: dict[str, Any], width: int) -> int:
|
||||||
|
if doc.get("kind") != "gamedat":
|
||||||
|
return 0
|
||||||
|
changed = 0
|
||||||
|
for line in doc.get("lines") or []:
|
||||||
|
if not isinstance(line, dict):
|
||||||
|
continue
|
||||||
|
text = line.get("text")
|
||||||
|
if not isinstance(text, str) or not text.strip():
|
||||||
|
continue
|
||||||
|
if not line_needs_wrap(text, width):
|
||||||
|
continue
|
||||||
|
if wrap_line_in_doc(line, width):
|
||||||
|
changed += 1
|
||||||
|
if changed:
|
||||||
|
save_document(path, doc)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_overflow_in_scope(
|
||||||
|
path: Path,
|
||||||
|
doc: dict[str, Any],
|
||||||
|
hit: WrapHit | dict[str, Any],
|
||||||
|
width: int,
|
||||||
|
) -> int:
|
||||||
|
"""Wrap every overflowing line in the same group as *hit*."""
|
||||||
|
if isinstance(hit, WrapHit):
|
||||||
|
kind, sheet_name = hit.kind, hit.sheet_name
|
||||||
|
else:
|
||||||
|
kind = str(hit.get("kind") or doc.get("kind") or "")
|
||||||
|
sheet_name = str(hit.get("sheet_name") or "")
|
||||||
|
|
||||||
|
if kind == "names":
|
||||||
|
return _wrap_overflow_in_names_category(path, doc, sheet_name, width)
|
||||||
|
if kind == "db":
|
||||||
|
return wrap_overflow_in_sheet(path, doc, sheet_name, width, kind="db")
|
||||||
|
if kind in ("map", "common"):
|
||||||
|
return _wrap_overflow_in_event_file(path, doc, width)
|
||||||
|
if kind == "gamedat":
|
||||||
|
return _wrap_overflow_in_gamedat(path, doc, width)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def wrap_hit_in_file(
|
def wrap_hit_in_file(
|
||||||
path: Path,
|
path: Path,
|
||||||
doc: dict[str, Any],
|
doc: dict[str, Any],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue