#!/usr/bin/env python3 from PyQt5.QtWidgets import QMainWindow, QApplication, QAction, QMenuBar, QMessageBox, QFileDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QTableWidget, QTableWidgetItem, QHeaderView, QPushButton, QTabWidget, QFormLayout from PyQt5.QtCore import Qt import sys import os import polib class LangToolWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Dusk Language Editor") self.setGeometry(100, 100, 800, 600) self.current_file = None self.dirty = False self.init_menu() self.init_ui() def init_ui(self): central = QWidget() layout = QVBoxLayout(central) tabs = QTabWidget() # Header Tab header_tab = QWidget() header_layout = QFormLayout(header_tab) self.language_edit = QLineEdit() self.language_edit.setMaximumWidth(220) header_layout.addRow(QLabel("Language:"), self.language_edit) self.plural_edit = QLineEdit() self.plural_edit.setMaximumWidth(320) header_layout.addRow(QLabel("Plural-Forms:"), self.plural_edit) self.content_type_edit = QLineEdit("text/plain; charset=UTF-8") self.content_type_edit.setMaximumWidth(320) header_layout.addRow(QLabel("Content-Type:"), self.content_type_edit) tabs.addTab(header_tab, "Header") # Strings Tab strings_tab = QWidget() strings_layout = QVBoxLayout(strings_tab) self.po_table = QTableWidget() self.po_table.setColumnCount(2) self.po_table.setHorizontalHeaderLabels(["msgid", "msgstr"]) self.po_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.po_table.verticalHeader().setMinimumWidth(22) self.po_table.verticalHeader().setDefaultAlignment(Qt.AlignRight | Qt.AlignVCenter) strings_layout.addWidget(self.po_table) row_btn_layout = QHBoxLayout() add_row_btn = QPushButton("Add Row") remove_row_btn = QPushButton("Remove Row") row_btn_layout.addWidget(add_row_btn) row_btn_layout.addWidget(remove_row_btn) strings_layout.addLayout(row_btn_layout) tabs.addTab(strings_tab, "Strings") layout.addWidget(tabs) add_row_btn.clicked.connect(self.add_row) remove_row_btn.clicked.connect(self.remove_row) self.add_row_btn = add_row_btn self.remove_row_btn = remove_row_btn self.setCentralWidget(central) # Connect edits to dirty flag self.language_edit.textChanged.connect(self.set_dirty) self.plural_edit.textChanged.connect(self.set_dirty) self.content_type_edit.textChanged.connect(self.set_dirty) self.po_table.itemChanged.connect(self.set_dirty) def set_dirty(self): self.dirty = True self.update_save_action() def init_menu(self): menubar = self.menuBar() file_menu = menubar.addMenu("File") new_action = QAction("New", self) open_action = QAction("Open", self) save_action = QAction("Save", self) save_as_action = QAction("Save As", self) new_action.triggered.connect(lambda: self.handle_file_action("new")) open_action.triggered.connect(lambda: self.handle_file_action("open")) save_action.triggered.connect(self.handle_save) save_as_action.triggered.connect(self.handle_save_as) file_menu.addAction(new_action) file_menu.addAction(open_action) file_menu.addAction(save_action) file_menu.addAction(save_as_action) self.save_action = save_action # Store reference for enabling/disabling self.update_save_action() def update_save_action(self): self.save_action.setEnabled(self.current_file is not None) def handle_file_action(self, action): if self.dirty: reply = QMessageBox.question(self, "Save Changes?", "Do you want to save changes before proceeding?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) if reply == QMessageBox.Cancel: return elif reply == QMessageBox.Yes: self.handle_save() if action == "new": self.new_file() elif action == "open": default_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../assets/locale")) file_path, _ = QFileDialog.getOpenFileName(self, "Open Language File", default_dir, "PO Files (*.po)") if file_path: self.open_file(file_path) def new_file(self): self.current_file = None self.dirty = False self.language_edit.setText("") self.plural_edit.setText("") self.po_table.setRowCount(0) self.update_save_action() def open_file(self, file_path): self.current_file = file_path self.dirty = False self.update_save_action() self.load_po_file(file_path) def load_po_file(self, file_path): po = polib.pofile(file_path) language = po.metadata.get('Language', '') plural = po.metadata.get('Plural-Forms', '') content_type = po.metadata.get('Content-Type', 'text/plain; charset=UTF-8') self.language_edit.setText(language) self.plural_edit.setText(plural) self.content_type_edit.setText(content_type) self.po_table.setRowCount(len(po)) for row, entry in enumerate(po): self.po_table.setItem(row, 0, QTableWidgetItem(entry.msgid)) self.po_table.setItem(row, 1, QTableWidgetItem(entry.msgstr)) def save_file(self, file_path): po = polib.POFile() po.metadata = { 'Language': self.language_edit.text(), 'Content-Type': self.content_type_edit.text(), 'Plural-Forms': self.plural_edit.text(), } for row in range(self.po_table.rowCount()): msgid_item = self.po_table.item(row, 0) msgstr_item = self.po_table.item(row, 1) msgid = msgid_item.text() if msgid_item else '' msgstr = msgstr_item.text() if msgstr_item else '' if msgid or msgstr: entry = polib.POEntry(msgid=msgid, msgstr=msgstr) po.append(entry) po.save(file_path) self.dirty = False self.update_save_action() def handle_save(self): if self.current_file: self.save_file(self.current_file) else: self.handle_save_as() def handle_save_as(self): default_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../assets/locale")) file_path, _ = QFileDialog.getSaveFileName(self, "Save Language File As", default_dir, "PO Files (*.po)") if file_path: self.current_file = file_path self.update_save_action() self.save_file(file_path) def add_row(self): row = self.po_table.rowCount() self.po_table.insertRow(row) self.po_table.setItem(row, 0, QTableWidgetItem("")) self.po_table.setItem(row, 1, QTableWidgetItem("")) self.po_table.setCurrentCell(row, 0) self.set_dirty() def remove_row(self): row = self.po_table.currentRow() if row >= 0: self.po_table.removeRow(row) self.set_dirty() def closeEvent(self, event): if self.dirty: reply = QMessageBox.question(self, "Save Changes?", "Do you want to save changes before exiting?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) if reply == QMessageBox.Cancel: event.ignore() return elif reply == QMessageBox.Yes: self.handle_save() event.accept() if __name__ == "__main__": app = QApplication(sys.argv) window = LangToolWindow() window.show() sys.exit(app.exec_())