Update CSV to work better with GUI and more compact

This commit is contained in:
dazedanon 2026-01-06 12:15:25 -06:00
parent c18c2a257c
commit 3c771a2b32
4 changed files with 656 additions and 267 deletions

View file

@ -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
View 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)

View file

@ -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)

View file

@ -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---\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: