Update CSV to work better with GUI and more compact
This commit is contained in:
parent
c18c2a257c
commit
3c771a2b32
4 changed files with 656 additions and 267 deletions
|
|
@ -16,6 +16,7 @@ from dotenv import load_dotenv, set_key
|
|||
|
||||
from gui.rpgmaker_tab import RPGMakerTab
|
||||
from gui.wolf_tab import WolfTab
|
||||
from gui.csv_tab import CSVTab
|
||||
|
||||
|
||||
def create_section_header(title):
|
||||
|
|
@ -104,6 +105,12 @@ class ConfigTab(QWidget):
|
|||
nav_layout.addWidget(btn_wolf)
|
||||
self.nav_buttons.append(btn_wolf)
|
||||
|
||||
# CSV button
|
||||
btn_csv = self.create_nav_button("📄", "CSV")
|
||||
btn_csv.clicked.connect(lambda: self.switch_page(4))
|
||||
nav_layout.addWidget(btn_csv)
|
||||
self.nav_buttons.append(btn_csv)
|
||||
|
||||
nav_layout.addStretch()
|
||||
nav_bar.setLayout(nav_layout)
|
||||
|
||||
|
|
@ -126,6 +133,10 @@ class ConfigTab(QWidget):
|
|||
self.wolf_tab = WolfTab()
|
||||
self.content_stack.addWidget(self.wolf_tab)
|
||||
|
||||
# Page 5: CSV Settings
|
||||
self.csv_tab = CSVTab()
|
||||
self.content_stack.addWidget(self.csv_tab)
|
||||
|
||||
# Add navigation bar and content to main layout
|
||||
main_layout.addWidget(nav_bar)
|
||||
main_layout.addWidget(self.content_stack)
|
||||
|
|
@ -589,6 +600,7 @@ class ConfigTab(QWidget):
|
|||
self.mvmz_tab.reset_to_defaults()
|
||||
self.ace_tab.reset_to_defaults()
|
||||
self.wolf_tab.reset_to_defaults()
|
||||
self.csv_tab.reset_to_defaults()
|
||||
|
||||
def get_config(self):
|
||||
"""Get current configuration as dictionary."""
|
||||
|
|
|
|||
498
gui/csv_tab.py
Normal file
498
gui/csv_tab.py
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
"""
|
||||
CSV Tab - Configuration for CSV translation settings
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QCheckBox,
|
||||
QPushButton, QLabel, QMessageBox, QSpinBox, QFrame, QComboBox
|
||||
)
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
|
||||
try:
|
||||
from .config_integration import ConfigIntegration
|
||||
except ImportError:
|
||||
ConfigIntegration = None
|
||||
|
||||
|
||||
def create_section_label(text):
|
||||
"""Create a section label for grouping settings."""
|
||||
label = QLabel(text)
|
||||
label.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #007acc;
|
||||
padding: 5px 0px 3px 0px;
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
return label
|
||||
|
||||
|
||||
def create_horizontal_line():
|
||||
"""Create a horizontal separator line."""
|
||||
line = QFrame()
|
||||
line.setFrameShape(QFrame.HLine)
|
||||
line.setFrameShadow(QFrame.Sunken)
|
||||
line.setStyleSheet("QFrame { color: #555555; margin: 5px 0px; }")
|
||||
return line
|
||||
|
||||
|
||||
class CSVTab(QWidget):
|
||||
"""CSV configuration tab for managing CSV translation settings."""
|
||||
|
||||
# Default configuration values (0-indexed internally)
|
||||
DEFAULT_CONFIG = {
|
||||
# Column settings (stored as 0-indexed)
|
||||
"SOURCE_COLUMN": 0,
|
||||
"TARGET_COLUMN": 1,
|
||||
"SPEAKER_COLUMN": -1, # -1 means no speaker column
|
||||
|
||||
# Row settings
|
||||
"SKIP_HEADER_ROW": True,
|
||||
"USE_TARGET_IF_NOT_EMPTY": False,
|
||||
|
||||
# Output settings
|
||||
"WRITE_TO_NEXT_COLUMN": False,
|
||||
|
||||
# Special parsing
|
||||
"PARSE_NAME_TAGS": False,
|
||||
"PARSE_M_MARKERS": False,
|
||||
"REMOVE_FURIGANA": False,
|
||||
"SKIP_COMMENT_ROWS": False,
|
||||
}
|
||||
|
||||
config_changed = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.config_integration = ConfigIntegration() if ConfigIntegration else None
|
||||
self.init_ui()
|
||||
|
||||
# Load configuration from module
|
||||
try:
|
||||
self.disconnect_auto_apply()
|
||||
loaded_config = None
|
||||
if self.config_integration:
|
||||
module_path = Path("modules") / "csv.py"
|
||||
loaded_config = self.read_csv_config(module_path)
|
||||
|
||||
if loaded_config:
|
||||
self.set_config(loaded_config)
|
||||
else:
|
||||
self.set_config(self.DEFAULT_CONFIG)
|
||||
except Exception:
|
||||
self.set_config(self.DEFAULT_CONFIG)
|
||||
|
||||
self.connect_auto_apply()
|
||||
|
||||
def init_ui(self):
|
||||
"""Initialize the user interface."""
|
||||
main_layout = QVBoxLayout()
|
||||
main_layout.setContentsMargins(15, 15, 15, 15)
|
||||
main_layout.setSpacing(10)
|
||||
|
||||
# Title and description
|
||||
title_label = QLabel("CSV Translation Settings")
|
||||
title_label.setStyleSheet("font-size: 15px; font-weight: bold; color: #007acc;")
|
||||
main_layout.addWidget(title_label)
|
||||
|
||||
description_label = QLabel(
|
||||
"Configure how CSV files are parsed and translated. These settings replace the old format presets."
|
||||
)
|
||||
description_label.setWordWrap(True)
|
||||
description_label.setStyleSheet("color: #888888; font-size: 10px; margin-bottom: 5px;")
|
||||
main_layout.addWidget(description_label)
|
||||
|
||||
# Two-column layout
|
||||
columns_layout = QHBoxLayout()
|
||||
columns_layout.setSpacing(40)
|
||||
|
||||
# LEFT COLUMN
|
||||
left_column = QVBoxLayout()
|
||||
left_column.setSpacing(5)
|
||||
|
||||
# Column Settings
|
||||
left_column.addWidget(create_section_label("📊 Column Settings"))
|
||||
|
||||
column_form = QFormLayout()
|
||||
column_form.setSpacing(8)
|
||||
column_form.setContentsMargins(0, 0, 0, 10)
|
||||
|
||||
# Source Column
|
||||
source_label = QLabel("Source Column:")
|
||||
source_label.setToolTip("Which column contains the source text to translate (1 = first column)")
|
||||
self.source_column_spin = QSpinBox()
|
||||
self.source_column_spin.setRange(1, 100)
|
||||
self.source_column_spin.setValue(1)
|
||||
self.source_column_spin.setFixedWidth(80)
|
||||
column_form.addRow(source_label, self.source_column_spin)
|
||||
|
||||
# Target Column
|
||||
target_label = QLabel("Target Column:")
|
||||
target_label.setToolTip("Which column to write translations to (1 = first column)")
|
||||
self.target_column_spin = QSpinBox()
|
||||
self.target_column_spin.setRange(1, 100)
|
||||
self.target_column_spin.setValue(2)
|
||||
self.target_column_spin.setFixedWidth(80)
|
||||
column_form.addRow(target_label, self.target_column_spin)
|
||||
|
||||
# Speaker Column
|
||||
speaker_label = QLabel("Speaker Column:")
|
||||
speaker_label.setToolTip("Which column contains speaker names (0 = none)")
|
||||
self.speaker_column_spin = QSpinBox()
|
||||
self.speaker_column_spin.setRange(0, 100)
|
||||
self.speaker_column_spin.setValue(0)
|
||||
self.speaker_column_spin.setSpecialValueText("None")
|
||||
self.speaker_column_spin.setFixedWidth(80)
|
||||
column_form.addRow(speaker_label, self.speaker_column_spin)
|
||||
|
||||
left_column.addLayout(column_form)
|
||||
left_column.addWidget(create_horizontal_line())
|
||||
|
||||
# Row Settings
|
||||
left_column.addWidget(create_section_label("📝 Row Settings"))
|
||||
|
||||
self.skip_header_cb = QCheckBox("Skip Header Row")
|
||||
self.skip_header_cb.setToolTip("Skip the first row (usually contains column headers)")
|
||||
left_column.addWidget(self.skip_header_cb)
|
||||
|
||||
self.use_target_if_not_empty_cb = QCheckBox("Use Target if Not Empty")
|
||||
self.use_target_if_not_empty_cb.setToolTip("If target column already has text, use that instead of source (T++ style)")
|
||||
left_column.addWidget(self.use_target_if_not_empty_cb)
|
||||
|
||||
left_column.addStretch()
|
||||
|
||||
# RIGHT COLUMN
|
||||
right_column = QVBoxLayout()
|
||||
right_column.setSpacing(5)
|
||||
|
||||
# Output Settings
|
||||
right_column.addWidget(create_section_label("📤 Output Settings"))
|
||||
|
||||
self.write_next_column_cb = QCheckBox("Write to Next Column")
|
||||
self.write_next_column_cb.setToolTip("Write translation to the column after target instead of overwriting")
|
||||
right_column.addWidget(self.write_next_column_cb)
|
||||
|
||||
right_column.addWidget(create_horizontal_line())
|
||||
|
||||
# Special Parsing
|
||||
right_column.addWidget(create_section_label("🔧 Special Parsing"))
|
||||
|
||||
self.parse_name_tags_cb = QCheckBox("Parse :name[] Tags")
|
||||
self.parse_name_tags_cb.setToolTip("Extract speaker names from :name[Speaker] tags in text")
|
||||
right_column.addWidget(self.parse_name_tags_cb)
|
||||
|
||||
self.parse_m_markers_cb = QCheckBox("Parse \\M Markers")
|
||||
self.parse_m_markers_cb.setToolTip("Handle \\M escape sequences in text")
|
||||
right_column.addWidget(self.parse_m_markers_cb)
|
||||
|
||||
self.remove_furigana_cb = QCheckBox("Remove Furigana <=>")
|
||||
self.remove_furigana_cb.setToolTip("Remove furigana annotations like <漢字=かんじ>")
|
||||
right_column.addWidget(self.remove_furigana_cb)
|
||||
|
||||
self.skip_comment_rows_cb = QCheckBox("Skip Comment Rows")
|
||||
self.skip_comment_rows_cb.setToolTip("Skip rows where first column contains 'comment'")
|
||||
right_column.addWidget(self.skip_comment_rows_cb)
|
||||
|
||||
right_column.addStretch()
|
||||
|
||||
# Add columns
|
||||
columns_layout.addLayout(left_column, 1)
|
||||
columns_layout.addLayout(right_column, 1)
|
||||
main_layout.addLayout(columns_layout)
|
||||
|
||||
# Preset buttons
|
||||
main_layout.addWidget(create_horizontal_line())
|
||||
main_layout.addWidget(create_section_label("⚡ Quick Presets"))
|
||||
|
||||
preset_layout = QHBoxLayout()
|
||||
preset_layout.setSpacing(10)
|
||||
|
||||
preset_tpp = QPushButton("Translator++")
|
||||
preset_tpp.setToolTip("Source col 1, Target col 2, skip header, use target if not empty")
|
||||
preset_tpp.clicked.connect(self.apply_preset_tpp)
|
||||
preset_tpp.setMaximumWidth(120)
|
||||
preset_layout.addWidget(preset_tpp)
|
||||
|
||||
preset_simple = QPushButton("Simple")
|
||||
preset_simple.setToolTip("Source col 1, Target col 2, no special processing")
|
||||
preset_simple.clicked.connect(self.apply_preset_simple)
|
||||
preset_simple.setMaximumWidth(120)
|
||||
preset_layout.addWidget(preset_simple)
|
||||
|
||||
preset_speaker = QPushButton("Speaker & Text")
|
||||
preset_speaker.setToolTip("Speaker col 3, Text col 10, with furigana removal")
|
||||
preset_speaker.clicked.connect(self.apply_preset_speaker)
|
||||
preset_speaker.setMaximumWidth(120)
|
||||
preset_layout.addWidget(preset_speaker)
|
||||
|
||||
preset_layout.addStretch()
|
||||
main_layout.addLayout(preset_layout)
|
||||
|
||||
# Bottom buttons
|
||||
main_layout.addSpacing(12)
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
|
||||
self.reset_button = QPushButton("🔄 Reset to Defaults")
|
||||
self.reset_button.clicked.connect(self.reset_to_defaults_with_message)
|
||||
self.reset_button.setMaximumWidth(180)
|
||||
self.reset_button.setMinimumHeight(32)
|
||||
|
||||
button_layout.addWidget(self.reset_button)
|
||||
button_layout.addStretch()
|
||||
|
||||
main_layout.addLayout(button_layout)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
def apply_preset_tpp(self):
|
||||
"""Apply Translator++ preset."""
|
||||
self.disconnect_auto_apply()
|
||||
self.source_column_spin.setValue(1) # Display as 1-based
|
||||
self.target_column_spin.setValue(2)
|
||||
self.speaker_column_spin.setValue(0) # 0 = None
|
||||
self.skip_header_cb.setChecked(True)
|
||||
self.use_target_if_not_empty_cb.setChecked(True)
|
||||
self.write_next_column_cb.setChecked(False)
|
||||
self.parse_name_tags_cb.setChecked(False)
|
||||
self.parse_m_markers_cb.setChecked(False)
|
||||
self.remove_furigana_cb.setChecked(False)
|
||||
self.skip_comment_rows_cb.setChecked(False)
|
||||
self.connect_auto_apply()
|
||||
self.apply_to_module(show_messages=False)
|
||||
|
||||
def apply_preset_simple(self):
|
||||
"""Apply simple two-column preset."""
|
||||
self.disconnect_auto_apply()
|
||||
self.source_column_spin.setValue(1) # Display as 1-based
|
||||
self.target_column_spin.setValue(2)
|
||||
self.speaker_column_spin.setValue(0) # 0 = None
|
||||
self.skip_header_cb.setChecked(False)
|
||||
self.use_target_if_not_empty_cb.setChecked(False)
|
||||
self.write_next_column_cb.setChecked(False)
|
||||
self.parse_name_tags_cb.setChecked(False)
|
||||
self.parse_m_markers_cb.setChecked(False)
|
||||
self.remove_furigana_cb.setChecked(False)
|
||||
self.skip_comment_rows_cb.setChecked(False)
|
||||
self.connect_auto_apply()
|
||||
self.apply_to_module(show_messages=False)
|
||||
|
||||
def apply_preset_speaker(self):
|
||||
"""Apply speaker & text preset (like old format 4)."""
|
||||
self.disconnect_auto_apply()
|
||||
self.source_column_spin.setValue(10) # Display as 1-based (was 9)
|
||||
self.target_column_spin.setValue(10)
|
||||
self.speaker_column_spin.setValue(3) # Display as 1-based (was 2, 0=None so 3=col2)
|
||||
self.skip_header_cb.setChecked(False)
|
||||
self.use_target_if_not_empty_cb.setChecked(False)
|
||||
self.write_next_column_cb.setChecked(False)
|
||||
self.parse_name_tags_cb.setChecked(False)
|
||||
self.parse_m_markers_cb.setChecked(False)
|
||||
self.remove_furigana_cb.setChecked(True)
|
||||
self.skip_comment_rows_cb.setChecked(False)
|
||||
self.connect_auto_apply()
|
||||
self.apply_to_module(show_messages=False)
|
||||
|
||||
def reset_to_defaults(self):
|
||||
"""Reset all settings to default values."""
|
||||
self.disconnect_auto_apply()
|
||||
self.set_config(self.DEFAULT_CONFIG)
|
||||
self.connect_auto_apply()
|
||||
self.apply_to_module(show_messages=False)
|
||||
|
||||
def reset_to_defaults_with_message(self):
|
||||
"""Reset to defaults and show confirmation."""
|
||||
self.reset_to_defaults()
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Reset Complete",
|
||||
"All CSV settings have been reset to their default values."
|
||||
)
|
||||
|
||||
def disconnect_auto_apply(self):
|
||||
"""Disconnect all widgets from auto-apply."""
|
||||
try:
|
||||
self.source_column_spin.valueChanged.disconnect()
|
||||
self.target_column_spin.valueChanged.disconnect()
|
||||
self.speaker_column_spin.valueChanged.disconnect()
|
||||
self.skip_header_cb.stateChanged.disconnect()
|
||||
self.use_target_if_not_empty_cb.stateChanged.disconnect()
|
||||
self.write_next_column_cb.stateChanged.disconnect()
|
||||
self.parse_name_tags_cb.stateChanged.disconnect()
|
||||
self.parse_m_markers_cb.stateChanged.disconnect()
|
||||
self.remove_furigana_cb.stateChanged.disconnect()
|
||||
self.skip_comment_rows_cb.stateChanged.disconnect()
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def connect_auto_apply(self):
|
||||
"""Connect all widgets to auto-apply changes."""
|
||||
self.source_column_spin.valueChanged.connect(lambda: self.apply_to_module(show_messages=False))
|
||||
self.target_column_spin.valueChanged.connect(lambda: self.apply_to_module(show_messages=False))
|
||||
self.speaker_column_spin.valueChanged.connect(lambda: self.apply_to_module(show_messages=False))
|
||||
self.skip_header_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False))
|
||||
self.use_target_if_not_empty_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False))
|
||||
self.write_next_column_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False))
|
||||
self.parse_name_tags_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False))
|
||||
self.parse_m_markers_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False))
|
||||
self.remove_furigana_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False))
|
||||
self.skip_comment_rows_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False))
|
||||
|
||||
def get_config(self):
|
||||
"""Get current configuration as dictionary (converts UI 1-based to internal 0-based)."""
|
||||
# Convert from 1-based UI to 0-based internal
|
||||
# Source/Target: UI shows 1-100, internal uses 0-99
|
||||
# Speaker: UI shows 0 (None) or 1-100, internal uses -1 (None) or 0-99
|
||||
speaker_ui = self.speaker_column_spin.value()
|
||||
speaker_internal = speaker_ui - 1 if speaker_ui > 0 else -1
|
||||
|
||||
return {
|
||||
"SOURCE_COLUMN": self.source_column_spin.value() - 1,
|
||||
"TARGET_COLUMN": self.target_column_spin.value() - 1,
|
||||
"SPEAKER_COLUMN": speaker_internal,
|
||||
"SKIP_HEADER_ROW": self.skip_header_cb.isChecked(),
|
||||
"USE_TARGET_IF_NOT_EMPTY": self.use_target_if_not_empty_cb.isChecked(),
|
||||
"WRITE_TO_NEXT_COLUMN": self.write_next_column_cb.isChecked(),
|
||||
"PARSE_NAME_TAGS": self.parse_name_tags_cb.isChecked(),
|
||||
"PARSE_M_MARKERS": self.parse_m_markers_cb.isChecked(),
|
||||
"REMOVE_FURIGANA": self.remove_furigana_cb.isChecked(),
|
||||
"SKIP_COMMENT_ROWS": self.skip_comment_rows_cb.isChecked(),
|
||||
}
|
||||
|
||||
def set_config(self, config):
|
||||
"""Set configuration from dictionary (converts internal 0-based to UI 1-based)."""
|
||||
# Convert from 0-based internal to 1-based UI
|
||||
# Source/Target: internal uses 0-99, UI shows 1-100
|
||||
# Speaker: internal uses -1 (None) or 0-99, UI shows 0 (None) or 1-100
|
||||
speaker_internal = config.get("SPEAKER_COLUMN", -1)
|
||||
speaker_ui = speaker_internal + 1 if speaker_internal >= 0 else 0
|
||||
|
||||
self.source_column_spin.setValue(config.get("SOURCE_COLUMN", 0) + 1)
|
||||
self.target_column_spin.setValue(config.get("TARGET_COLUMN", 1) + 1)
|
||||
self.speaker_column_spin.setValue(speaker_ui)
|
||||
self.skip_header_cb.setChecked(config.get("SKIP_HEADER_ROW", True))
|
||||
self.use_target_if_not_empty_cb.setChecked(config.get("USE_TARGET_IF_NOT_EMPTY", False))
|
||||
self.write_next_column_cb.setChecked(config.get("WRITE_TO_NEXT_COLUMN", False))
|
||||
self.parse_name_tags_cb.setChecked(config.get("PARSE_NAME_TAGS", False))
|
||||
self.parse_m_markers_cb.setChecked(config.get("PARSE_M_MARKERS", False))
|
||||
self.remove_furigana_cb.setChecked(config.get("REMOVE_FURIGANA", False))
|
||||
self.skip_comment_rows_cb.setChecked(config.get("SKIP_COMMENT_ROWS", False))
|
||||
|
||||
def read_csv_config(self, module_path: Path) -> dict:
|
||||
"""Read current configuration from csv.py module file."""
|
||||
if not module_path.exists():
|
||||
return {}
|
||||
|
||||
config = {}
|
||||
|
||||
try:
|
||||
with open(module_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
import re
|
||||
|
||||
# Read integer values
|
||||
int_patterns = {
|
||||
"SOURCE_COLUMN": r'^SOURCE_COLUMN\s*=\s*(-?\d+)',
|
||||
"TARGET_COLUMN": r'^TARGET_COLUMN\s*=\s*(-?\d+)',
|
||||
"SPEAKER_COLUMN": r'^SPEAKER_COLUMN\s*=\s*(-?\d+)',
|
||||
}
|
||||
|
||||
# Read boolean values
|
||||
bool_patterns = {
|
||||
"SKIP_HEADER_ROW": r'^SKIP_HEADER_ROW\s*=\s*(True|False)',
|
||||
"USE_TARGET_IF_NOT_EMPTY": r'^USE_TARGET_IF_NOT_EMPTY\s*=\s*(True|False)',
|
||||
"WRITE_TO_NEXT_COLUMN": r'^WRITE_TO_NEXT_COLUMN\s*=\s*(True|False)',
|
||||
"PARSE_NAME_TAGS": r'^PARSE_NAME_TAGS\s*=\s*(True|False)',
|
||||
"PARSE_M_MARKERS": r'^PARSE_M_MARKERS\s*=\s*(True|False)',
|
||||
"REMOVE_FURIGANA": r'^REMOVE_FURIGANA\s*=\s*(True|False)',
|
||||
"SKIP_COMMENT_ROWS": r'^SKIP_COMMENT_ROWS\s*=\s*(True|False)',
|
||||
}
|
||||
|
||||
for line in content.split('\n'):
|
||||
line = line.strip()
|
||||
|
||||
for key, pattern in int_patterns.items():
|
||||
match = re.match(pattern, line)
|
||||
if match:
|
||||
config[key] = int(match.group(1))
|
||||
|
||||
for key, pattern in bool_patterns.items():
|
||||
match = re.match(pattern, line)
|
||||
if match:
|
||||
config[key] = match.group(1) == 'True'
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading CSV config: {e}")
|
||||
|
||||
return config
|
||||
|
||||
def apply_to_module(self, show_messages=True):
|
||||
"""Apply current configuration to the csv.py module file."""
|
||||
if not self.config_integration:
|
||||
if show_messages:
|
||||
QMessageBox.warning(self, "Error", "Config integration not available")
|
||||
return
|
||||
|
||||
module_path = Path("modules") / "csv.py"
|
||||
|
||||
if not module_path.exists():
|
||||
if show_messages:
|
||||
QMessageBox.warning(self, "Error", f"Module file not found: {module_path}")
|
||||
return
|
||||
|
||||
try:
|
||||
config = self.get_config()
|
||||
|
||||
with open(module_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Update configuration values
|
||||
updated_content = self._update_csv_config(content, config)
|
||||
|
||||
with open(module_path, 'w', encoding='utf-8') as f:
|
||||
f.write(updated_content)
|
||||
|
||||
self.config_changed.emit()
|
||||
|
||||
if show_messages:
|
||||
QMessageBox.information(self, "Success", "CSV configuration applied successfully!")
|
||||
|
||||
except Exception as e:
|
||||
if show_messages:
|
||||
QMessageBox.critical(self, "Error", f"Failed to apply configuration:\n{str(e)}")
|
||||
|
||||
def _update_csv_config(self, content: str, config: dict) -> str:
|
||||
"""Update configuration values in the module content."""
|
||||
import re
|
||||
|
||||
lines = content.split('\n')
|
||||
updated_lines = []
|
||||
|
||||
for line in lines:
|
||||
updated_line = line
|
||||
|
||||
for key, value in config.items():
|
||||
# Match lines like: CONFIG_NAME = value # comment
|
||||
pattern = rf'^({re.escape(key)})\s*=\s*.*?(#.*)?$'
|
||||
match = re.match(pattern, line.strip())
|
||||
|
||||
if match:
|
||||
comment = match.group(2) if match.group(2) else ""
|
||||
if comment:
|
||||
comment = " " + comment
|
||||
|
||||
# Format value appropriately
|
||||
if isinstance(value, bool):
|
||||
value_str = str(value)
|
||||
else:
|
||||
value_str = str(value)
|
||||
|
||||
updated_line = f"{key} = {value_str}{comment}"
|
||||
break
|
||||
|
||||
updated_lines.append(updated_line)
|
||||
|
||||
return '\n'.join(updated_lines)
|
||||
|
|
@ -986,21 +986,6 @@ class TranslationTab(QWidget):
|
|||
self.mode_combo.currentTextChanged.connect(self._on_mode_changed)
|
||||
trans_form.addRow(mode_label, self.mode_combo)
|
||||
|
||||
# CSV Format dropdown (only visible when CSV engine is selected)
|
||||
self.csv_format_label = QLabel("CSV Format:")
|
||||
self.csv_format_label.setFixedWidth(100)
|
||||
self.csv_format_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
self.csv_format_combo = QComboBox()
|
||||
self.csv_format_combo.addItem("Translator++")
|
||||
self.csv_format_combo.addItem("Single")
|
||||
self.csv_format_combo.addItem("Multiple")
|
||||
self.csv_format_combo.addItem("Speaker&Text")
|
||||
self.csv_format_combo.setFixedWidth(300)
|
||||
trans_form.addRow(self.csv_format_label, self.csv_format_combo)
|
||||
# Hide by default (shown when CSV is selected)
|
||||
self.csv_format_label.setVisible(False)
|
||||
self.csv_format_combo.setVisible(False)
|
||||
|
||||
layout.addLayout(trans_form)
|
||||
layout.addWidget(create_horizontal_line())
|
||||
|
||||
|
|
@ -1141,11 +1126,6 @@ class TranslationTab(QWidget):
|
|||
else:
|
||||
self.mode_combo.setCurrentIndex(0)
|
||||
|
||||
# Show/hide CSV format dropdown based on engine selection
|
||||
is_csv = "csv" in lowered
|
||||
self.csv_format_label.setVisible(is_csv)
|
||||
self.csv_format_combo.setVisible(is_csv)
|
||||
|
||||
# Refresh file list to show only files matching the selected module's extensions
|
||||
self.refresh_file_lists()
|
||||
|
||||
|
|
@ -1627,13 +1607,6 @@ class TranslationTab(QWidget):
|
|||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
# Write CSV format to csv.tmp if CSV module is selected
|
||||
if "csv" in selected_module[0].lower():
|
||||
csv_format_index = self.csv_format_combo.currentIndex() + 1 # 1-based (1-4)
|
||||
csv_tmp_path = self.project_root / "csv.tmp"
|
||||
with open(csv_tmp_path, "w", encoding="utf-8") as f:
|
||||
f.write(str(csv_format_index))
|
||||
|
||||
# Switch to progress view
|
||||
self.file_stack.setCurrentIndex(1)
|
||||
|
||||
|
|
|
|||
386
modules/csv.py
386
modules/csv.py
|
|
@ -39,6 +39,18 @@ MISMATCH = [] # Lists files that thdata a mismatch error (Length of GPT list re
|
|||
FILENAME = None
|
||||
BRACKETNAMES = False
|
||||
|
||||
# CSV Configuration Settings (configurable via GUI)
|
||||
SOURCE_COLUMN = 2 # Which column has the source text to translate
|
||||
TARGET_COLUMN = 3 # Which column to write translations to
|
||||
SPEAKER_COLUMN = -1 # Which column has speaker names (-1 = none)
|
||||
SKIP_HEADER_ROW = True # Skip the first row (header)
|
||||
USE_TARGET_IF_NOT_EMPTY = False # Use target column text if not empty (T++ style)
|
||||
WRITE_TO_NEXT_COLUMN = False # Write to column after target instead of overwriting
|
||||
PARSE_NAME_TAGS = False # Parse :name[] tags in text
|
||||
PARSE_M_MARKERS = False # Parse \M markers in text
|
||||
REMOVE_FURIGANA = False # Remove furigana annotations <=>
|
||||
SKIP_COMMENT_ROWS = False # Skip rows starting with 'comment'
|
||||
|
||||
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
||||
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
||||
|
||||
|
|
@ -152,30 +164,6 @@ def parseCSV(readFile, writeFile, filename):
|
|||
totalLines = 0
|
||||
global LOCK
|
||||
|
||||
# Read from tmp files
|
||||
if os.path.isfile("csv.tmp"):
|
||||
with open("csv.tmp") as tmpFile:
|
||||
format = tmpFile.readline()
|
||||
else:
|
||||
format = ""
|
||||
|
||||
# Choices
|
||||
while format not in ["1", "2", "3", "4"]:
|
||||
format = input("\n\nSelect the CSV Format:\n\n1. Translator++\n2. Single\n3. Multiple\n4. Speaker&Text\n")
|
||||
match format:
|
||||
case "1":
|
||||
format = "1"
|
||||
case "2":
|
||||
format = "2"
|
||||
case "3":
|
||||
format = "3"
|
||||
case "4":
|
||||
format = "4"
|
||||
|
||||
# Write to file for later use
|
||||
with open("csv.tmp", "w", encoding="utf-8") as tmpFile:
|
||||
tmpFile.write(f"{format}")
|
||||
|
||||
# Get total for progress bar
|
||||
totalLines = len(readFile.readlines())
|
||||
readFile.seek(0)
|
||||
|
|
@ -199,7 +187,7 @@ def parseCSV(readFile, writeFile, filename):
|
|||
data.append(row)
|
||||
|
||||
try:
|
||||
response = translateCSV(data, pbar, writeFile, writer, filename, None, format)
|
||||
response = translateCSV(data, pbar, writeFile, writer, filename, None)
|
||||
totalTokens[0] = response[0]
|
||||
totalTokens[1] = response[1]
|
||||
except Exception:
|
||||
|
|
@ -224,7 +212,22 @@ def flush_progress_csv(writeFile, writer, rows):
|
|||
traceback.print_exc()
|
||||
|
||||
|
||||
def translateCSV(data, pbar, writeFile, writer, filename, translatedList, format):
|
||||
def translateCSV(data, pbar, writeFile, writer, filename, translatedList):
|
||||
"""
|
||||
Unified CSV translation function using configurable settings.
|
||||
|
||||
Uses global settings:
|
||||
- SOURCE_COLUMN: column index for source text
|
||||
- TARGET_COLUMN: column index to write translations
|
||||
- SPEAKER_COLUMN: column index for speaker names (-1 = none)
|
||||
- SKIP_HEADER_ROW: whether to skip first row
|
||||
- USE_TARGET_IF_NOT_EMPTY: use existing target text if present (T++ style)
|
||||
- WRITE_TO_NEXT_COLUMN: write to column after target
|
||||
- PARSE_NAME_TAGS: parse :name[] tags
|
||||
- PARSE_M_MARKERS: parse \\M markers
|
||||
- REMOVE_FURIGANA: remove furigana
|
||||
- SKIP_COMMENT_ROWS: skip rows with 'comment' in first column
|
||||
"""
|
||||
global LOCK, ESTIMATE, PBAR
|
||||
PBAR = pbar
|
||||
translatedText = ""
|
||||
|
|
@ -235,219 +238,123 @@ def translateCSV(data, pbar, writeFile, writer, filename, translatedList, format
|
|||
try:
|
||||
# Translate
|
||||
while i < len(data):
|
||||
match format:
|
||||
# T++ Format: Source Text on column 1. TL Target on Column 2
|
||||
case "1":
|
||||
# Get String
|
||||
if i != 0:
|
||||
if data[i][1] == "":
|
||||
jaString = data[i][0]
|
||||
else:
|
||||
jaString = data[i][1]
|
||||
# Skip header row if configured
|
||||
if SKIP_HEADER_ROW and i == 0:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Skip comment rows if configured
|
||||
if SKIP_COMMENT_ROWS and len(data[i]) > 0 and 'comment' in str(data[i][0]).lower():
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Check if row has enough columns
|
||||
if len(data[i]) <= SOURCE_COLUMN:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Get source text
|
||||
jaString = ""
|
||||
speaker = ""
|
||||
|
||||
# If USE_TARGET_IF_NOT_EMPTY is enabled (T++ style), check target column first
|
||||
if USE_TARGET_IF_NOT_EMPTY and len(data[i]) > TARGET_COLUMN and data[i][TARGET_COLUMN]:
|
||||
jaString = data[i][TARGET_COLUMN]
|
||||
else:
|
||||
jaString = data[i][SOURCE_COLUMN] if data[i][SOURCE_COLUMN] else ""
|
||||
|
||||
# Skip empty strings
|
||||
if not jaString:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Handle speaker column if configured
|
||||
if SPEAKER_COLUMN >= 0 and len(data[i]) > SPEAKER_COLUMN and data[i][SPEAKER_COLUMN]:
|
||||
speakerResponse = getSpeaker(data[i][SPEAKER_COLUMN])
|
||||
totalTokens[0] += speakerResponse[1][0]
|
||||
totalTokens[1] += speakerResponse[1][1]
|
||||
speaker = speakerResponse[0]
|
||||
data[i][SPEAKER_COLUMN] = speaker
|
||||
|
||||
# Parse :name[] tags if configured
|
||||
if PARSE_NAME_TAGS and ':name' in jaString:
|
||||
match = re.search(r":name\[([^\]]+?)\]\n([\w\W]*)", jaString)
|
||||
if match:
|
||||
# Translate speaker name
|
||||
response = getSpeaker(match.group(1))
|
||||
speaker = response[0]
|
||||
totalTokens[0] += response[1][0]
|
||||
totalTokens[1] += response[1][1]
|
||||
data[i][SOURCE_COLUMN] = data[i][SOURCE_COLUMN].replace(match.group(1), speaker)
|
||||
|
||||
# Extract text portion
|
||||
jaString = match.group(2)
|
||||
# Remove voice markers
|
||||
voMatch = re.search(r"\\[vfF]+\[.+]", jaString)
|
||||
if voMatch:
|
||||
jaString = jaString.replace(voMatch.group(0), "")
|
||||
|
||||
# Parse \M markers if configured
|
||||
if PARSE_M_MARKERS and '\\M' in jaString:
|
||||
match = re.search(r"\\M.+\n([\w\W]*)", jaString)
|
||||
if match:
|
||||
jaString = match.group(1)
|
||||
voMatch = re.search(r"\\[vfF]+\[.+]", jaString)
|
||||
if voMatch:
|
||||
jaString = jaString.replace(voMatch.group(0), "")
|
||||
|
||||
# Remove furigana if configured
|
||||
if REMOVE_FURIGANA:
|
||||
jaString = re.sub(r"<(.*)=.*>", r"\1", jaString)
|
||||
|
||||
# Store original for replacement
|
||||
ojaString = jaString
|
||||
|
||||
# Remove textwrap
|
||||
jaString = jaString.replace("\\n", " ")
|
||||
jaString = jaString.replace("\n", " ")
|
||||
|
||||
# Pass 1: Collect strings
|
||||
if not translatedList:
|
||||
if speaker:
|
||||
stringList.append(f"[{speaker}]: {jaString}")
|
||||
else:
|
||||
stringList.append(jaString)
|
||||
|
||||
# Pass 2: Apply translations
|
||||
else:
|
||||
# Grab and pop translation
|
||||
translatedText = translatedList[0]
|
||||
translatedList.pop(0)
|
||||
|
||||
# Remove speaker prefix from translation if present
|
||||
if speaker:
|
||||
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
|
||||
|
||||
# Add wordwrap
|
||||
translatedText = dazedwrap.wrapText(translatedText, WIDTH)
|
||||
translatedText = translatedText.replace("\n", "\\n")
|
||||
|
||||
# Determine target column
|
||||
actual_target = TARGET_COLUMN + 1 if WRITE_TO_NEXT_COLUMN else TARGET_COLUMN
|
||||
|
||||
# Ensure row has enough columns
|
||||
while len(data[i]) <= actual_target:
|
||||
data[i].append("")
|
||||
|
||||
# Set data
|
||||
if PARSE_NAME_TAGS or PARSE_M_MARKERS:
|
||||
# Replace original text portion
|
||||
data[i][actual_target] = data[i][actual_target].replace(ojaString, translatedText) if data[i][actual_target] else translatedText
|
||||
else:
|
||||
data[i][actual_target] = translatedText
|
||||
|
||||
flush_progress_csv(writeFile, writer, data)
|
||||
|
||||
# Iterate
|
||||
i += 1
|
||||
|
||||
# Remove Textwrap
|
||||
jaString = jaString.replace("\\n", " ")
|
||||
|
||||
# Pass 1
|
||||
if not translatedList:
|
||||
stringList.append(jaString)
|
||||
|
||||
# Pass 2
|
||||
else:
|
||||
# Grab and Pop
|
||||
translatedText = translatedList[0]
|
||||
translatedList.pop(0)
|
||||
|
||||
# Add Wordwrap
|
||||
translatedText = dazedwrap.wrapText(translatedText, WIDTH)
|
||||
translatedText = translatedText.replace("\n", "\\n")
|
||||
|
||||
# Set Data
|
||||
data[i][1] = translatedText
|
||||
flush_progress_csv(writeFile, writer, data)
|
||||
|
||||
# Iterate
|
||||
i += 1
|
||||
|
||||
# Target Format
|
||||
case "2":
|
||||
# Set Values
|
||||
sourceColumn = 0
|
||||
targetColumn = 1
|
||||
|
||||
# Check if Translated
|
||||
jaString = data[i][sourceColumn]
|
||||
|
||||
# Remove Textwrap
|
||||
jaString = jaString.replace("\\n", " ")
|
||||
|
||||
# Pass 1
|
||||
if not translatedList:
|
||||
stringList.append(jaString)
|
||||
|
||||
# Pass 2
|
||||
else:
|
||||
# Grab and Pop
|
||||
translatedText = translatedList[0]
|
||||
translatedList.pop(0)
|
||||
|
||||
# Add Wordwrap
|
||||
translatedText = dazedwrap.wrapText(translatedText, WIDTH)
|
||||
translatedText = translatedText.replace("\n", "\\n")
|
||||
|
||||
# Set Data
|
||||
data[i][targetColumn] = translatedText
|
||||
flush_progress_csv(writeFile, writer, data)
|
||||
|
||||
# Iterate
|
||||
i += 1
|
||||
|
||||
# In Place Format
|
||||
case "3":
|
||||
# Set columns to translate. Leave empty to translate all.
|
||||
targetColumns = [1]
|
||||
|
||||
# False - Place translation in source column
|
||||
# True - Place translation in next column
|
||||
targetNextRow = False
|
||||
|
||||
# Skip 1st Row
|
||||
skipFirstRow = True
|
||||
|
||||
for j in range(len(data[i])):
|
||||
if skipFirstRow and i == 0:
|
||||
continue
|
||||
if j in targetColumns and data[i][j]:
|
||||
# Check if Translated
|
||||
jaString = data[i][j]
|
||||
speaker = ""
|
||||
vo = ""
|
||||
|
||||
if ':name' in jaString:
|
||||
match = re.search(r":name\[([^\]]+?)\]\n([\w\W]*)", jaString)
|
||||
if match:
|
||||
# TL Speaker
|
||||
response = getSpeaker(match.group(1))
|
||||
speaker = response[0]
|
||||
totalTokens[0] += response[1][0]
|
||||
totalTokens[1] += response[1][1]
|
||||
data[i][j] = data[i][j].replace(match.group(1), speaker)
|
||||
|
||||
# TL Text
|
||||
jaString = match.group(2)
|
||||
voMatch = re.search(r"\\[vfF]+\[.+]", jaString)
|
||||
if voMatch:
|
||||
vo = voMatch.group(0)
|
||||
jaString = jaString.replace(vo, "")
|
||||
jaString = jaString.replace(vo, "")
|
||||
elif '\\M' in jaString:
|
||||
match = re.search(r"\\M.+\n([\w\W]*)", jaString)
|
||||
if match:
|
||||
jaString = match.group(1)
|
||||
voMatch = re.search(r"\\[vfF]+\[.+]", jaString)
|
||||
if voMatch:
|
||||
vo = voMatch.group(0)
|
||||
jaString = jaString.replace(vo, "")
|
||||
elif 'comment' in data[i][0]:
|
||||
# Skip comments
|
||||
continue
|
||||
|
||||
# Remove Textwrap
|
||||
ojaString = jaString
|
||||
jaString = jaString.replace("\n", " ")
|
||||
|
||||
# Pass 1
|
||||
if not translatedList:
|
||||
if speaker:
|
||||
stringList.append(f"[{speaker}]: {jaString}")
|
||||
else:
|
||||
stringList.append(jaString)
|
||||
|
||||
# Pass 2
|
||||
else:
|
||||
# Grab and Pop
|
||||
translatedText = translatedList[0]
|
||||
translatedList.pop(0)
|
||||
|
||||
# Remove speaker
|
||||
if speaker:
|
||||
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
|
||||
|
||||
# Add Wordwrap
|
||||
translatedText = dazedwrap.wrapText(translatedText, WIDTH)
|
||||
|
||||
# Set Data
|
||||
if targetNextRow:
|
||||
data[i][j + 1] = data[i][j + 1].replace(ojaString, f"{translatedText}")
|
||||
else:
|
||||
data[i][j] = data[i][j].replace(ojaString, f"{translatedText}")
|
||||
flush_progress_csv(writeFile, writer, data)
|
||||
|
||||
# Iterate
|
||||
i += 1
|
||||
|
||||
# Speaker & Text Format
|
||||
case "4":
|
||||
# Set columns to translate. Leave empty to translate all.
|
||||
speakerColumn = 2
|
||||
textColumn = 9
|
||||
speaker = ""
|
||||
|
||||
if len(data[i]) > textColumn and data[i][textColumn]:
|
||||
# Speaker
|
||||
if data[i][speakerColumn]:
|
||||
speakerResponse = getSpeaker(data[i][speakerColumn])
|
||||
totalTokens[0] += speakerResponse[1][0]
|
||||
totalTokens[1] += speakerResponse[1][1]
|
||||
speaker = speakerResponse[0]
|
||||
data[i][speakerColumn] = speaker
|
||||
|
||||
# Get Text
|
||||
jaString = data[i][textColumn]
|
||||
|
||||
# Remove Textwrap
|
||||
jaString = jaString.replace("\\n", " ")
|
||||
|
||||
# Remove Furigana
|
||||
jaString = re.sub(r"<(.*)=.*>", r"\1", jaString)
|
||||
|
||||
# Pass 1
|
||||
if not translatedList:
|
||||
# Append Speaker
|
||||
if speaker:
|
||||
jaString = f"[{speaker}]: {jaString}"
|
||||
|
||||
# Append to List
|
||||
stringList.append(jaString)
|
||||
|
||||
# Pass 2
|
||||
else:
|
||||
# Grab and Pop
|
||||
translatedText = translatedList[0]
|
||||
translatedList.pop(0)
|
||||
|
||||
# Add Wordwrap
|
||||
translatedText = dazedwrap.wrapText(translatedText, WIDTH)
|
||||
translatedText = translatedText.replace("\n", "\\n")
|
||||
|
||||
# Check for more than 3 newlines (Shoujo Ramune)
|
||||
newline_count = translatedText.count("\\n")
|
||||
if newline_count >= 3:
|
||||
parts = translatedText.split("\\n", 3)
|
||||
data[i][textColumn] = parts[0] + "\\n" + parts[1] + "\\n" + parts[2]
|
||||
new_row = data[i].copy()
|
||||
new_row[textColumn] = parts[3]
|
||||
new_row[0] = str(int(new_row[0]) + 1) # Add 1 to the line number
|
||||
data.insert(i + 1, new_row)
|
||||
i += 1
|
||||
else:
|
||||
data[i][textColumn] = translatedText
|
||||
flush_progress_csv(writeFile, writer, data)
|
||||
|
||||
# Iterate
|
||||
i += 1
|
||||
|
||||
# EOF
|
||||
# EOF - Process collected strings
|
||||
if len(stringList) > 0:
|
||||
# Set Progress
|
||||
pbar.total = len(stringList)
|
||||
|
|
@ -461,8 +368,7 @@ def translateCSV(data, pbar, writeFile, writer, filename, translatedList, format
|
|||
|
||||
# Set Strings
|
||||
if len(stringList) == len(translatedList):
|
||||
translateCSV(data, pbar, writer, filename, translatedList, format)
|
||||
|
||||
translateCSV(data, pbar, writeFile, writer, filename, translatedList)
|
||||
# Mismatch
|
||||
else:
|
||||
with LOCK:
|
||||
|
|
|
|||
Loading…
Reference in a new issue