diff --git a/tagstudio/resources/translations/en_us.ini b/tagstudio/resources/translations/en_us.ini new file mode 100644 index 000000000..90e2c560a --- /dev/null +++ b/tagstudio/resources/translations/en_us.ini @@ -0,0 +1,10 @@ +MainWindow.Title="Main Window" +MainWindow.ThumbnailSize="Thumbnail Size" +MainWindow.RecentLibraries="Recent Libraries" +MainWindow.Search.Search="Search" +MainWindow.Search.Entries="Search Entries" +MainWindow.Search.AND="And (Includes All Tags)" +MainWindow.Search.OR="Or (Includes Any Tag)" + +MenuBar.File.Title="&File" +MenuBar.File.OpenCreateLibrary="&Open/Create Library" \ No newline at end of file diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index ce6b1e338..2142879e1 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -196,7 +196,7 @@ def setupUi(self, MainWindow): def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QCoreApplication.translate( - "MainWindow", u"MainWindow", None)) + "MainWindow", u"Title", None)) # Navigation buttons self.backButton.setText( QCoreApplication.translate("MainWindow", u"<", None)) @@ -205,18 +205,18 @@ def retranslateUi(self, MainWindow): # Search field self.searchField.setPlaceholderText( - QCoreApplication.translate("MainWindow", u"Search Entries", None)) + QCoreApplication.translate("MainWindow.Search", u"Entries", None)) self.searchButton.setText( - QCoreApplication.translate("MainWindow", u"Search", None)) + QCoreApplication.translate("MainWindow.Search", u"Search", None)) # Search type selector - self.comboBox_2.setItemText(0, QCoreApplication.translate("MainWindow", "And (Includes All Tags)")) - self.comboBox_2.setItemText(1, QCoreApplication.translate("MainWindow", "Or (Includes Any Tag)")) + self.comboBox_2.setItemText(0, QCoreApplication.translate("MainWindow.Search", u"AND")) + self.comboBox_2.setItemText(1, QCoreApplication.translate("MainWindow.Search", u"OR")) self.comboBox.setCurrentText("") # Thumbnail size selector self.comboBox.setPlaceholderText( - QCoreApplication.translate("MainWindow", u"Thumbnail Size", None)) + QCoreApplication.translate("MainWindow", u"ThumbnailSize", None)) # retranslateUi def moveEvent(self, event) -> None: diff --git a/tagstudio/src/qt/translator.py b/tagstudio/src/qt/translator.py new file mode 100644 index 000000000..8026c9a17 --- /dev/null +++ b/tagstudio/src/qt/translator.py @@ -0,0 +1,34 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +from PySide6.QtCore import QObject, QTranslator +from pathlib import Path +from json import load as load_json + + +class TSTranslator(QTranslator): + def __init__(self, parent: QObject | None = None) -> None: + super().__init__(parent) + self.translations: dict[str, str] = {} + + def translate( + self, context, sourceText, disambiguation: str | None = None, n: int = -1 + ) -> str: + return self.translations.get(context + "." + sourceText.replace(" ", "")) + + def load(self, translationDir: Path, language: str, country: str = "") -> bool: + file = None + if (translationDir / (language + "_" + country + ".json")).exists(): + file = load_json( # noqa: SIM115 + translationDir / (language + "_" + country + ".json"), encoding="utf-8" + ) + elif (translationDir / (language + ".json")).exists(): + file = load_json(translationDir / (language + ".json"), encoding="utf-8") # noqa: SIM115 + if file is None: + return False + + self.translations = file + + file.close() + return True diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index d9ae4b7d8..25741b487 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -29,6 +29,8 @@ QThreadPool, QTimer, QSettings, + QLocale, + QCoreApplication, ) from PySide6.QtGui import ( QGuiApplication, @@ -90,6 +92,7 @@ from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal from src.qt.modals.fix_dupes import FixDupeFilesModal from src.qt.modals.folders_to_tags import FoldersToTagsModal +from src.qt.translator import TSTranslator # this import has side-effect of import PySide resources import src.qt.resources_rc # noqa: F401 @@ -146,7 +149,9 @@ def __init__(self, backend, args): self.spacing = None self.branch: str = (" (" + VERSION_BRANCH + ")") if VERSION_BRANCH else "" - self.base_title: str = f"TagStudio Alpha {VERSION}{self.branch}" + self.base_title: str = QCoreApplication.translate("home", "base_title").format( + version=VERSION, branch=self.branch + ) # self.title_text: str = self.base_title # self.buffer = {} self.thumb_job_queue: Queue = Queue() @@ -212,6 +217,16 @@ def start(self) -> None: sys.argv += ["-platform", "windows:darkmode=2"] app = QApplication(sys.argv) + + translator = TSTranslator() + path = Path(__file__).parents[2] / "resources/translations" + translator.load( + path, + QLocale.languageToCode(QLocale.system().language()).lower(), + QLocale.countryToCode(QLocale.system().country()).lower(), + ) + app.installTranslator(translator) + app.setStyle("Fusion") # pal: QPalette = app.palette() # pal.setColor(QPalette.ColorGroup.Active, @@ -265,18 +280,20 @@ def start(self) -> None: self.main_window.setMenuBar(menu_bar) menu_bar.setNativeMenuBar(True) - file_menu = QMenu("&File", menu_bar) - edit_menu = QMenu("&Edit", menu_bar) - tools_menu = QMenu("&Tools", menu_bar) - macros_menu = QMenu("&Macros", menu_bar) - window_menu = QMenu("&Window", menu_bar) - help_menu = QMenu("&Help", menu_bar) + file_menu = QMenu(QCoreApplication.translate("menu", "file"), menu_bar) + edit_menu = QMenu(QCoreApplication.translate("menu", "edit"), menu_bar) + tools_menu = QMenu(QCoreApplication.translate("menu", "tools"), menu_bar) + macros_menu = QMenu(QCoreApplication.translate("menu", "macros"), menu_bar) + window_menu = QMenu(QCoreApplication.translate("menu", "window"), menu_bar) + help_menu = QMenu(QCoreApplication.translate("menu", "help"), menu_bar) # File Menu ============================================================ # file_menu.addAction(QAction('&New Library', menu_bar)) # file_menu.addAction(QAction('&Open Library', menu_bar)) - open_library_action = QAction("&Open/Create Library", menu_bar) + open_library_action = QAction( + QCoreApplication.translate("dialog", "open_create_library"), menu_bar + ) open_library_action.triggered.connect(lambda: self.open_library_from_dialog()) open_library_action.setShortcut( QtCore.QKeyCombination( @@ -327,7 +344,7 @@ def start(self) -> None: file_menu.addAction(close_library_action) # Edit Menu ============================================================ - new_tag_action = QAction("New &Tag", menu_bar) + new_tag_action = QAction(QCoreApplication.translate("tag", "new"), menu_bar) new_tag_action.triggered.connect(lambda: self.add_tag_action_callback()) new_tag_action.setShortcut( QtCore.QKeyCombination( @@ -385,7 +402,9 @@ def create_fix_unlinked_entries_modal(): self.unlinked_modal = FixUnlinkedEntriesModal(self.lib, self) self.unlinked_modal.show() - fix_unlinked_entries_action = QAction("Fix &Unlinked Entries", menu_bar) + fix_unlinked_entries_action = QAction( + QCoreApplication.translate("fix_unlinked", "fix_unlinked"), menu_bar + ) fix_unlinked_entries_action.triggered.connect(create_fix_unlinked_entries_modal) tools_menu.addAction(fix_unlinked_entries_action) @@ -394,7 +413,9 @@ def create_dupe_files_modal(): self.dupe_modal = FixDupeFilesModal(self.lib, self) self.dupe_modal.show() - fix_dupe_files_action = QAction("Fix Duplicate &Files", menu_bar) + fix_dupe_files_action = QAction( + QCoreApplication.translate("fix_dupes", "fix_dupes"), menu_bar + ) fix_dupe_files_action.triggered.connect(create_dupe_files_modal) tools_menu.addAction(fix_dupe_files_action) @@ -480,7 +501,7 @@ def create_folders_tags_modal(): if lib: self.splash.showMessage( - f'Opening Library "{lib}"...', + QCoreApplication.translate("splash", "open_library").format(lib=lib), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), QColor("#9782ff"), ) @@ -571,7 +592,9 @@ def close_library(self, is_shutdown: bool = False): return logger.info("Closing Library...") - self.main_window.statusbar.showMessage("Closing Library...") + self.main_window.statusbar.showMessage( + QCoreApplication.translate("logging", "closing") + ) start_time = time.time() self.settings.setValue(SettingItems.LAST_LIBRARY, self.lib.library_dir) @@ -601,7 +624,9 @@ def close_library(self, is_shutdown: bool = False): def backup_library(self): logger.info("Backing Up Library...") - self.main_window.statusbar.showMessage("Saving Library...") + self.main_window.statusbar.showMessage( + QCoreApplication.translate("logging", "saving") + ) start_time = time.time() target_path = self.lib.save_library_backup_to_disk() end_time = time.time() @@ -612,8 +637,8 @@ def backup_library(self): def add_tag_action_callback(self): self.modal = PanelModal( BuildTagPanel(self.lib), - "New Tag", - "Add Tag", + QCoreApplication.translate("tag", "new"), + QCoreApplication.translate("tag", "add"), has_save=True, ) @@ -645,7 +670,10 @@ def clear_select_action_callback(self): def show_tag_database(self): self.modal = PanelModal( - TagDatabasePanel(self.lib), "Library Tags", "Library Tags", has_save=False + TagDatabasePanel(self.lib), + QCoreApplication.translate("tag", "library"), + QCoreApplication.translate("tag", "library"), + has_save=False, ) self.modal.show() @@ -653,8 +681,8 @@ def show_file_extension_modal(self): panel = FileExtensionModal(self.lib) self.modal = PanelModal( panel, - "File Extensions", - "File Extensions", + QCoreApplication.translate("generic", "file_extension"), + QCoreApplication.translate("generic", "file_extension"), has_save=True, ) @@ -667,8 +695,8 @@ def add_new_files_callback(self): tracker = RefreshDirTracker(self.lib) pw = ProgressWidget( - window_title="Refreshing Directories", - label_text="Scanning Directories for New Files...\nPreparing...", + window_title=QCoreApplication.translate("dialog", "refresh_directories"), + label_text=QCoreApplication.translate("dialog", "scan_directories"), cancel_button_text=None, minimum=0, maximum=0, @@ -680,7 +708,13 @@ def add_new_files_callback(self): lambda x: ( pw.update_progress(x + 1), pw.update_label( - f'Scanning Directories for New Files...\n{x + 1} File{"s" if x + 1 != 1 else ""} Searched, {tracker.files_count} New Files Found' + QCoreApplication.translate( + "dialog", "scan_directories.new_files" + ).format( + file_count=x + 1, + plural="s" if x + 1 != 1 else "", + new_files=tracker.files_count, + ) ), ) ) @@ -720,8 +754,12 @@ def add_new_files_runnable(self, tracker: RefreshDirTracker): # iterator = FunctionIterator(lambda: self.new_file_macros_runnable(tracker.files_not_in_library)) iterator = FunctionIterator(tracker.save_new_files) pw = ProgressWidget( - window_title="Running Macros on New Entries", - label_text=f"Running Configured Macros on 1/{files_count} New Entries", + window_title=QCoreApplication.translate( + "progression.running_macros", "running" + ), + label_text=QCoreApplication.translate( + "progression.running_macros", "new_entries" + ).format(done_files=1, new_files=files_count), cancel_button_text=None, minimum=0, maximum=files_count, @@ -731,7 +769,9 @@ def add_new_files_runnable(self, tracker: RefreshDirTracker): lambda x: ( pw.update_progress(x + 1), pw.update_label( - f"Running Configured Macros on {x + 1}/{files_count} New Entries" + QCoreApplication.translate( + "progression.running_macros", "new_entries" + ).format(done_files=x + 1, new_files=files_count) ), ) ) @@ -1004,7 +1044,9 @@ def filter_items(self, filter: FilterState | None = None) -> None: self.filter = dataclasses.replace(self.filter, **dataclasses.asdict(filter)) self.main_window.statusbar.showMessage( - f'Searching Library: "{self.filter.summary}"' + QCoreApplication.translate("status", "search_library_query").format( + query=self.filter.summary + ) ) self.main_window.statusbar.repaint() start_time = time.time() @@ -1016,11 +1058,17 @@ def filter_items(self, filter: FilterState | None = None) -> None: end_time = time.time() if self.filter.summary: self.main_window.statusbar.showMessage( - f'{query_count} Results Found for "{self.filter.summary}" ({format_timespan(end_time - start_time)})' + QCoreApplication.translate("status", "queried_results_found").format( + results=query_count, + query=self.filter.summary, + time=format_timespan(end_time - start_time), + ), ) else: self.main_window.statusbar.showMessage( - f"{query_count} Results ({format_timespan(end_time - start_time)})" + QCoreApplication.translate("status", "results_found").format( + results=query_count, time=format_timespan(end_time - start_time) + ) ) # update page content @@ -1075,7 +1123,9 @@ def update_libs_list(self, path: Path | str): def open_library(self, path: Path | str): """Opens a TagStudio library.""" - open_message: str = f'Opening Library "{str(path)}"...' + open_message: str = QCoreApplication.translate("splash", "open_library").format( + lib=str(path) + ) self.main_window.landing_widget.set_status_label(open_message) self.main_window.statusbar.showMessage(open_message, 3) self.main_window.repaint() @@ -1088,7 +1138,9 @@ def open_library(self, path: Path | str): self.add_new_files_callback() self.update_libs_list(path) - title_text = f"{self.base_title} - Library '{self.lib.library_dir}'" + title_text = QCoreApplication.translate("open_library", "title").format( + base_title=self.base_title, directory=self.lib.library_dir + ) self.main_window.setWindowTitle(title_text) self.selected.clear() diff --git a/tagstudio/src/qt/ui/home_ui.py b/tagstudio/src/qt/ui/home_ui.py index 0d986f6ad..d66d832de 100644 --- a/tagstudio/src/qt/ui/home_ui.py +++ b/tagstudio/src/qt/ui/home_ui.py @@ -146,15 +146,15 @@ def setupUi(self, MainWindow): # setupUi def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) - self.comboBox_2.setItemText(0, QCoreApplication.translate("MainWindow", u"And (includes all tags)", None)) - self.comboBox_2.setItemText(1, QCoreApplication.translate("MainWindow", u"Or (includes any tag)", None)) + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Title", None)) + self.comboBox_2.setItemText(0, QCoreApplication.translate("MainWindow.Search", u"AND")) + self.comboBox_2.setItemText(1, QCoreApplication.translate("MainWindow.Search", u"OR")) self.comboBox.setCurrentText("") - self.comboBox.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Thumbnail Size", None)) + self.comboBox.setPlaceholderText(QCoreApplication.translate("MainWindow", u"ThumbnailSize", None)) self.backButton.setText(QCoreApplication.translate("MainWindow", u"<", None)) self.forwardButton.setText(QCoreApplication.translate("MainWindow", u">", None)) - self.searchField.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Search Entries", None)) - self.searchButton.setText(QCoreApplication.translate("MainWindow", u"Search", None)) + self.searchField.setPlaceholderText(QCoreApplication.translate("MainWindow.Search", u"SearchEntries", None)) + self.searchButton.setText(QCoreApplication.translate("MainWindow.Search", u"Search", None)) # retranslateUi diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 386fa9ddc..3cf6f3e7b 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -13,7 +13,7 @@ import structlog from PIL import Image, UnidentifiedImageError from PIL.Image import DecompressionBombError -from PySide6.QtCore import Signal, Qt, QSize +from PySide6.QtCore import Signal, Qt, QSize, QCoreApplication from PySide6.QtGui import QResizeEvent, QAction from PySide6.QtWidgets import ( QWidget, @@ -293,8 +293,8 @@ def clear_layout(layout_item: QVBoxLayout): # remove any potential previous items clear_layout(layout) - label = QLabel("Recent Libraries") - label.setAlignment(Qt.AlignmentFlag.AlignCenter) + label = QLabel(QCoreApplication.translate("MainWindow", "RecentLibraries")) + label.setAlignment(Qt.AlignCenter) # type: ignore row_layout = QHBoxLayout() row_layout.addWidget(label)