diff --git a/gui/config_tab.py b/gui/config_tab.py index 392103b..148156d 100644 --- a/gui/config_tab.py +++ b/gui/config_tab.py @@ -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.""" diff --git a/gui/csv_tab.py b/gui/csv_tab.py new file mode 100644 index 0000000..8cf5368 --- /dev/null +++ b/gui/csv_tab.py @@ -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) diff --git a/gui/translation_tab.py b/gui/translation_tab.py index 922551d..2e7ecd6 100644 --- a/gui/translation_tab.py +++ b/gui/translation_tab.py @@ -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) diff --git a/modules/csv.py b/modules/csv.py index a73165c..fe2e590 100644 --- a/modules/csv.py +++ b/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: