489 lines
19 KiB
Python
489 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Translation Execution Tab for DazedMTLTool GUI
|
|
|
|
Handles running the translation process with configured settings.
|
|
Each module has its own default settings in its folder without backups.
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
from pathlib import Path
|
|
from PyQt5.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QGroupBox,
|
|
QComboBox, QTextEdit, QProgressBar, QMessageBox, QListWidget,
|
|
QListWidgetItem, QSplitter, QCheckBox
|
|
)
|
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
|
|
from PyQt5.QtGui import QFont, QColor
|
|
|
|
|
|
class TranslationWorker(QThread):
|
|
"""Worker thread for translation process."""
|
|
|
|
progress_updated = pyqtSignal(int)
|
|
status_updated = pyqtSignal(str)
|
|
log_updated = pyqtSignal(str)
|
|
finished = pyqtSignal(bool) # True if successful
|
|
|
|
def __init__(self, config, files_to_translate):
|
|
super().__init__()
|
|
self.config = config
|
|
self.files_to_translate = files_to_translate
|
|
self.is_cancelled = False
|
|
|
|
def run(self):
|
|
"""Run the translation process."""
|
|
try:
|
|
total_files = len(self.files_to_translate)
|
|
if total_files == 0:
|
|
self.status_updated.emit("No files to translate")
|
|
self.finished.emit(False)
|
|
return
|
|
|
|
self.status_updated.emit(f"Starting translation of {total_files} files...")
|
|
|
|
for i, file_path in enumerate(self.files_to_translate):
|
|
if self.is_cancelled:
|
|
self.status_updated.emit("Translation cancelled")
|
|
self.finished.emit(False)
|
|
return
|
|
|
|
progress = int((i / total_files) * 100)
|
|
self.progress_updated.emit(progress)
|
|
self.status_updated.emit(f"Translating: {file_path.name}")
|
|
self.log_updated.emit(f"Processing file: {file_path}")
|
|
|
|
# Here we would call the actual translation module
|
|
success = self.translate_file(file_path)
|
|
|
|
if not success:
|
|
self.log_updated.emit(f"Failed to translate: {file_path.name}")
|
|
self.status_updated.emit(f"Error translating {file_path.name}")
|
|
self.finished.emit(False)
|
|
return
|
|
else:
|
|
self.log_updated.emit(f"Successfully translated: {file_path.name}")
|
|
|
|
self.progress_updated.emit(100)
|
|
self.status_updated.emit(f"Translation completed successfully!")
|
|
self.finished.emit(True)
|
|
|
|
except Exception as e:
|
|
self.log_updated.emit(f"Translation error: {str(e)}")
|
|
self.status_updated.emit(f"Translation failed: {str(e)}")
|
|
self.finished.emit(False)
|
|
|
|
def translate_file(self, file_path):
|
|
"""Translate a single file using the appropriate module."""
|
|
try:
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Add project root to path
|
|
project_root = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
# Import the main translation module
|
|
from modules.main import translate_file
|
|
|
|
# Create module-specific config for this file
|
|
self.create_module_config(file_path)
|
|
|
|
# Call the translation function
|
|
result = translate_file(str(file_path))
|
|
return result
|
|
|
|
except Exception as e:
|
|
self.log_updated.emit(f"Error in translate_file: {str(e)}")
|
|
return False
|
|
|
|
def create_module_config(self, file_path):
|
|
"""Create module-specific configuration without backups."""
|
|
try:
|
|
# Determine the module based on file extension and content
|
|
module_name = self.determine_module(file_path)
|
|
|
|
if module_name:
|
|
# Create module directory if it doesn't exist
|
|
module_dir = Path(__file__).parent.parent / "modules" / module_name
|
|
module_dir.mkdir(exist_ok=True)
|
|
|
|
# Create default settings file
|
|
settings_file = module_dir / "settings.json"
|
|
|
|
# Only create if it doesn't exist (no overwriting)
|
|
if not settings_file.exists():
|
|
default_settings = self.get_default_settings(module_name)
|
|
with open(settings_file, 'w', encoding='utf-8') as f:
|
|
json.dump(default_settings, f, indent=2, ensure_ascii=False)
|
|
|
|
self.log_updated.emit(f"Created default settings for {module_name}")
|
|
|
|
except Exception as e:
|
|
self.log_updated.emit(f"Error creating module config: {str(e)}")
|
|
|
|
def determine_module(self, file_path):
|
|
"""Determine which module to use based on file type."""
|
|
suffix = file_path.suffix.lower()
|
|
|
|
if suffix == '.json':
|
|
# Check if it's RPG Maker format
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
if '"code":' in content or '"parameters":' in content:
|
|
return "rpgmakermvmz"
|
|
else:
|
|
return "json"
|
|
except:
|
|
return "json"
|
|
elif suffix == '.txt':
|
|
return "text"
|
|
elif suffix == '.csv':
|
|
return "csv"
|
|
elif suffix in ['.js', '.py']:
|
|
return "regex"
|
|
else:
|
|
return "text" # Default fallback
|
|
|
|
def get_default_settings(self, module_name):
|
|
"""Get default settings for a module."""
|
|
base_settings = {
|
|
"enabled": True,
|
|
"backup": False, # No backups as requested
|
|
"output_directory": "translated",
|
|
"preserve_formatting": True
|
|
}
|
|
|
|
if module_name == "rpgmakermvmz":
|
|
# Use the same defaults as defined in RPG Maker tab
|
|
rpg_defaults = {
|
|
# Main dialogue codes (enabled by default)
|
|
"CODE401": True, # Show Text
|
|
"CODE405": True, # Show Text (line 4+)
|
|
"CODE102": True, # Show Choices
|
|
|
|
# Optional codes (disabled by default)
|
|
"CODE101": False, # Show Text (face)
|
|
"CODE408": False, # Show Text (line continuation)
|
|
|
|
# Variable codes (disabled by default)
|
|
"CODE122": False, # Control Variables
|
|
|
|
# General settings
|
|
"FIRSTLINESPEAKERS": False,
|
|
"FACENAME101": False,
|
|
"NAMES": False,
|
|
"BRFLAG": False,
|
|
"FIXTEXTWRAP": True,
|
|
"IGNORETLTEXT": False,
|
|
|
|
# All other codes disabled by default
|
|
"translate_dialogue": True,
|
|
"translate_choices": True,
|
|
"translate_items": False,
|
|
"translate_skills": False,
|
|
"translate_states": False,
|
|
"translate_actors": False,
|
|
"translate_classes": False,
|
|
"translate_weapons": False,
|
|
"translate_armors": False,
|
|
"translate_enemies": False,
|
|
"event_codes": ["401", "405", "102"] # Only main dialogue codes enabled
|
|
}
|
|
base_settings.update(rpg_defaults)
|
|
elif module_name == "json":
|
|
base_settings.update({
|
|
"translate_strings": True,
|
|
"skip_keys": ["id", "index", "type"],
|
|
"nested_translation": True
|
|
})
|
|
elif module_name == "text":
|
|
base_settings.update({
|
|
"line_by_line": True,
|
|
"skip_empty_lines": True,
|
|
"preserve_whitespace": True
|
|
})
|
|
elif module_name == "csv":
|
|
base_settings.update({
|
|
"has_header": True,
|
|
"delimiter": ",",
|
|
"columns_to_translate": [] # Empty means translate all text columns
|
|
})
|
|
|
|
return base_settings
|
|
|
|
def cancel(self):
|
|
"""Cancel the translation process."""
|
|
self.is_cancelled = True
|
|
|
|
|
|
class TranslationTab(QWidget):
|
|
"""Translation execution tab."""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.parent_window = parent
|
|
self.worker = None
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
"""Set up the user interface."""
|
|
layout = QVBoxLayout()
|
|
|
|
# Title
|
|
title_label = QLabel("Translation Execution")
|
|
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #007acc;")
|
|
layout.addWidget(title_label)
|
|
|
|
# Description
|
|
desc_label = QLabel(
|
|
"Execute the translation process on selected files. "
|
|
"Each module will use its default settings stored in its own folder."
|
|
)
|
|
desc_label.setWordWrap(True)
|
|
desc_label.setStyleSheet("color: #cccccc; margin-bottom: 10px;")
|
|
layout.addWidget(desc_label)
|
|
|
|
# Main content
|
|
content_splitter = QSplitter(Qt.Vertical)
|
|
|
|
# File selection and controls
|
|
controls_group = self.create_controls_section()
|
|
content_splitter.addWidget(controls_group)
|
|
|
|
# Progress and log section
|
|
progress_group = self.create_progress_section()
|
|
content_splitter.addWidget(progress_group)
|
|
|
|
content_splitter.setSizes([300, 200])
|
|
layout.addWidget(content_splitter)
|
|
|
|
# Status bar
|
|
self.status_label = QLabel("Ready to translate")
|
|
self.status_label.setStyleSheet("color: #cccccc; padding: 5px;")
|
|
layout.addWidget(self.status_label)
|
|
|
|
self.setLayout(layout)
|
|
|
|
def create_controls_section(self):
|
|
"""Create the controls section."""
|
|
group = QGroupBox("Translation Controls")
|
|
layout = QVBoxLayout()
|
|
|
|
# File selection
|
|
file_layout = QHBoxLayout()
|
|
|
|
file_label = QLabel("Files to translate:")
|
|
file_label.setStyleSheet("color: #ffffff;")
|
|
file_layout.addWidget(file_label)
|
|
|
|
self.refresh_files_btn = QPushButton("Refresh File List")
|
|
self.refresh_files_btn.clicked.connect(self.refresh_file_list)
|
|
file_layout.addWidget(self.refresh_files_btn)
|
|
|
|
file_layout.addStretch()
|
|
layout.addLayout(file_layout)
|
|
|
|
# File list
|
|
self.file_list = QListWidget()
|
|
self.file_list.setSelectionMode(QListWidget.ExtendedSelection)
|
|
layout.addWidget(self.file_list)
|
|
|
|
# Options
|
|
options_layout = QHBoxLayout()
|
|
|
|
self.select_all_cb = QCheckBox("Select All Files")
|
|
self.select_all_cb.stateChanged.connect(self.toggle_select_all)
|
|
options_layout.addWidget(self.select_all_cb)
|
|
|
|
self.dry_run_cb = QCheckBox("Dry Run (test without translating)")
|
|
options_layout.addWidget(self.dry_run_cb)
|
|
|
|
options_layout.addStretch()
|
|
layout.addLayout(options_layout)
|
|
|
|
# Action buttons
|
|
button_layout = QHBoxLayout()
|
|
|
|
self.start_btn = QPushButton("Start Translation")
|
|
self.start_btn.clicked.connect(self.start_translation)
|
|
self.start_btn.setStyleSheet("QPushButton { background-color: #0a7a0a; }")
|
|
button_layout.addWidget(self.start_btn)
|
|
|
|
self.stop_btn = QPushButton("Stop Translation")
|
|
self.stop_btn.clicked.connect(self.stop_translation)
|
|
self.stop_btn.setEnabled(False)
|
|
self.stop_btn.setStyleSheet("QPushButton { background-color: #aa0000; }")
|
|
button_layout.addWidget(self.stop_btn)
|
|
|
|
button_layout.addStretch()
|
|
layout.addLayout(button_layout)
|
|
|
|
group.setLayout(layout)
|
|
return group
|
|
|
|
def create_progress_section(self):
|
|
"""Create the progress and log section."""
|
|
group = QGroupBox("Translation Progress")
|
|
layout = QVBoxLayout()
|
|
|
|
# Progress bar
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setRange(0, 100)
|
|
self.progress_bar.setValue(0)
|
|
layout.addWidget(self.progress_bar)
|
|
|
|
# Log output
|
|
log_label = QLabel("Translation Log:")
|
|
log_label.setStyleSheet("color: #ffffff; margin-top: 10px;")
|
|
layout.addWidget(log_label)
|
|
|
|
self.log_output = QTextEdit()
|
|
self.log_output.setReadOnly(True)
|
|
self.log_output.setMaximumHeight(150)
|
|
layout.addWidget(self.log_output)
|
|
|
|
# Clear log button
|
|
clear_layout = QHBoxLayout()
|
|
clear_layout.addStretch()
|
|
|
|
self.clear_log_btn = QPushButton("Clear Log")
|
|
self.clear_log_btn.clicked.connect(self.clear_log)
|
|
clear_layout.addWidget(self.clear_log_btn)
|
|
|
|
layout.addLayout(clear_layout)
|
|
|
|
group.setLayout(layout)
|
|
return group
|
|
|
|
def refresh_file_list(self):
|
|
"""Refresh the list of available files."""
|
|
self.file_list.clear()
|
|
|
|
try:
|
|
# Get files from the file manager tab
|
|
if hasattr(self.parent_window, 'file_manager_tab'):
|
|
input_files = self.parent_window.file_manager_tab.get_input_files()
|
|
|
|
for file_path in input_files:
|
|
item = QListWidgetItem(file_path.name)
|
|
item.setData(Qt.UserRole, str(file_path))
|
|
item.setCheckState(Qt.Checked) # Default to checked
|
|
self.file_list.addItem(item)
|
|
|
|
self.status_label.setText(f"Found {len(input_files)} files ready for translation")
|
|
else:
|
|
self.status_label.setText("File manager not available")
|
|
|
|
except Exception as e:
|
|
self.status_label.setText(f"Error loading files: {str(e)}")
|
|
|
|
def toggle_select_all(self, state):
|
|
"""Toggle selection of all files."""
|
|
check_state = Qt.Checked if state == Qt.Checked else Qt.Unchecked
|
|
|
|
for i in range(self.file_list.count()):
|
|
item = self.file_list.item(i)
|
|
item.setCheckState(check_state)
|
|
|
|
def get_selected_files(self):
|
|
"""Get list of selected files."""
|
|
selected_files = []
|
|
|
|
for i in range(self.file_list.count()):
|
|
item = self.file_list.item(i)
|
|
if item.checkState() == Qt.Checked:
|
|
file_path = Path(item.data(Qt.UserRole))
|
|
selected_files.append(file_path)
|
|
|
|
return selected_files
|
|
|
|
def start_translation(self):
|
|
"""Start the translation process."""
|
|
selected_files = self.get_selected_files()
|
|
|
|
if not selected_files:
|
|
QMessageBox.warning(self, "No Files Selected", "Please select at least one file to translate.")
|
|
return
|
|
|
|
# Get configuration from other tabs
|
|
try:
|
|
config = {}
|
|
if hasattr(self.parent_window, 'config_tab'):
|
|
config.update(self.parent_window.config_tab.get_config())
|
|
if hasattr(self.parent_window, 'rpgmaker_tab'):
|
|
config.update(self.parent_window.rpgmaker_tab.get_config())
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Configuration Error", f"Failed to get configuration:\n{str(e)}")
|
|
return
|
|
|
|
# Confirm start
|
|
if not self.dry_run_cb.isChecked():
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Confirm Translation",
|
|
f"Start translation of {len(selected_files)} files?\n\nThis will create translated files in the 'translated' directory.",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
if reply != QMessageBox.Yes:
|
|
return
|
|
|
|
# Start worker thread
|
|
self.worker = TranslationWorker(config, selected_files)
|
|
self.worker.progress_updated.connect(self.progress_bar.setValue)
|
|
self.worker.status_updated.connect(self.status_label.setText)
|
|
self.worker.log_updated.connect(self.add_log_message)
|
|
self.worker.finished.connect(self.on_translation_finished)
|
|
|
|
# Update UI state
|
|
self.start_btn.setEnabled(False)
|
|
self.stop_btn.setEnabled(True)
|
|
self.progress_bar.setValue(0)
|
|
|
|
# Start translation
|
|
self.worker.start()
|
|
self.add_log_message(f"Starting translation of {len(selected_files)} files...")
|
|
|
|
def stop_translation(self):
|
|
"""Stop the translation process."""
|
|
if self.worker and self.worker.isRunning():
|
|
self.worker.cancel()
|
|
self.add_log_message("Cancelling translation...")
|
|
self.status_label.setText("Stopping translation...")
|
|
|
|
def on_translation_finished(self, success):
|
|
"""Handle translation completion."""
|
|
# Update UI state
|
|
self.start_btn.setEnabled(True)
|
|
self.stop_btn.setEnabled(False)
|
|
|
|
if success:
|
|
self.add_log_message("Translation completed successfully!")
|
|
QMessageBox.information(self, "Translation Complete", "All files have been translated successfully!")
|
|
|
|
# Refresh file manager if available
|
|
if hasattr(self.parent_window, 'file_manager_tab'):
|
|
self.parent_window.file_manager_tab.refresh_file_lists()
|
|
else:
|
|
self.add_log_message("Translation failed or was cancelled.")
|
|
|
|
self.worker = None
|
|
|
|
def add_log_message(self, message):
|
|
"""Add a message to the log output."""
|
|
import datetime
|
|
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
|
formatted_message = f"[{timestamp}] {message}"
|
|
self.log_output.append(formatted_message)
|
|
|
|
# Auto-scroll to bottom
|
|
scrollbar = self.log_output.verticalScrollBar()
|
|
scrollbar.setValue(scrollbar.maximum())
|
|
|
|
def clear_log(self):
|
|
"""Clear the log output."""
|
|
self.log_output.clear()
|
|
self.add_log_message("Log cleared")
|