From 69aed92f2421924f71498d23dac84c0fed79f7f4 Mon Sep 17 00:00:00 2001 From: Jann Stute <46534683+Computerdores@users.noreply.github.com> Date: Tue, 31 Dec 2024 04:15:39 +0100 Subject: [PATCH] feat: translations (#662) * feat: implement Translator class * feat: add translate_with_setter and implement formatting of translations * feat: extend PanelModal to allow for translation * feat: extend ProgressWidget to allow for translation * feat: translation in ts_qt.py * debug: set default lang to DE and show "Not Translated" when replacing untranslated stuff * add translation todos * feat: translate modals * feat: translate more stuff * fix: UI test wasn't comparing to translated strings * feat: translations for most of the remaining stuff * fix: replace debug changes with simpler one * uncomment debug change * fix: missing parameter in call * fix: various review feedback * fix: don't translate json migration discrepancies list * fix: typo * fix: various PR feedback * fix: correctly read non-ascii characters * fix: add missing new line at eof * fix: comment out line of debug code * fix: address latest review comment * fix: KeyError that occurred when formatting translations * fix: regression of d594e84 * fix: add newline to en.json * fix: organize en.json, fix typo --------- Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> --- tagstudio/resources/translations/en.json | 146 +++++++++++--- tagstudio/src/qt/main_window.py | 34 +--- tagstudio/src/qt/modals/add_field.py | 9 +- tagstudio/src/qt/modals/build_tag.py | 21 +- tagstudio/src/qt/modals/delete_unlinked.py | 32 ++-- tagstudio/src/qt/modals/drop_import.py | 35 ++-- tagstudio/src/qt/modals/file_extension.py | 17 +- tagstudio/src/qt/modals/fix_dupes.py | 46 ++--- tagstudio/src/qt/modals/fix_unlinked.py | 33 ++-- tagstudio/src/qt/modals/folders_to_tags.py | 11 +- tagstudio/src/qt/modals/merge_dupe_entries.py | 5 +- tagstudio/src/qt/modals/mirror_entities.py | 29 +-- tagstudio/src/qt/modals/relink_unlinked.py | 12 +- tagstudio/src/qt/modals/tag_database.py | 19 +- tagstudio/src/qt/modals/tag_search.py | 3 +- tagstudio/src/qt/platform_strings.py | 8 +- tagstudio/src/qt/translations.py | 95 +++++++++ tagstudio/src/qt/ts_qt.py | 181 ++++++++++++------ tagstudio/src/qt/widgets/item_thumb.py | 7 +- tagstudio/src/qt/widgets/landing.py | 5 +- tagstudio/src/qt/widgets/migration_modal.py | 100 +++++----- tagstudio/src/qt/widgets/panel.py | 16 +- tagstudio/src/qt/widgets/preview_panel.py | 62 +++--- tagstudio/src/qt/widgets/progress.py | 5 +- tagstudio/src/qt/widgets/tag.py | 10 +- tagstudio/src/qt/widgets/tag_box.py | 8 +- tagstudio/src/qt/widgets/video_player.py | 7 +- tagstudio/tests/qt/test_build_tag_panel.py | 3 +- 28 files changed, 619 insertions(+), 340 deletions(-) create mode 100644 tagstudio/src/qt/translations.py diff --git a/tagstudio/resources/translations/en.json b/tagstudio/resources/translations/en.json index 32cae501..e9247b71 100644 --- a/tagstudio/resources/translations/en.json +++ b/tagstudio/resources/translations/en.json @@ -1,27 +1,40 @@ { "app.git": "Git Commit", "app.pre_release": "Pre-Release", + "app.title": "{base_title} - Library '{library_dir}'", + "drop_import.description": "The following files have filenames already exist in the library", + "drop_import.duplicates_choice.plural": "The following {count} files have filenames that already exist in the library.", + "drop_import.duplicates_choice.singular": "The following file has a filename that already exists in the library.", + "drop_import.progress.label.initial": "Importing New Files...", + "drop_import.progress.label.plural": "Importing New Files...\n{count} Files Imported.{suffix}", + "drop_import.progress.label.singular": "Importing New Files...\n1 File imported.{suffix}", + "drop_import.progress.window_title": "Import Files", + "drop_import.title": "Conflicting File(s)", "edit.tag_manager": "Manage Tags", - "entries.duplicate.merge.label": "Merging Duplicate Entries", + "entries.duplicate.merge.label": "Merging Duplicate Entries...", "entries.duplicate.merge": "Merge Duplicate Entries", "entries.duplicate.refresh": "Refresh Duplicate Entries", "entries.duplicates.description": "Duplicate entries are defined as multiple entries which point to the same file on disk. Merging these will combine the tags and metadata from all duplicates into a single consolidated entry. These are not to be confused with \"duplicate files\", which are duplicates of your files themselves outside of TagStudio.", - "entries.mirror.confirmation": "Are you sure you want to mirror the following %{len(self.lib.dupe_files)} Entries?", - "entries.mirror.label": "Mirroring 1/%{count} Entries...", + "entries.mirror.confirmation": "Are you sure you want to mirror the following {count} Entries?", + "entries.mirror.label": "Mirroring {idx}/{total} Entries...", "entries.mirror.title": "Mirroring Entries", - "entries.mirror": "Mirror", + "entries.mirror.window_title": "Mirror Entries", + "entries.mirror": "&Mirror", "entries.tags": "Tags", - "entries.unlinked.delete.confirm": "Are you sure you want to delete the following %{len(self.lib.missing_files)} entries?", - "entries.unlinked.delete.deleting_count": "Deleting %{x[0]+1}/{len(self.lib.missing_files)} Unlinked Entries", + "entries.unlinked.delete_alt": "De&lete Unlinked Entries", + "entries.unlinked.delete.confirm": "Are you sure you want to delete the following {count} entries?", + "entries.unlinked.delete.deleting_count": "Deleting {idx}/{count} Unlinked Entries", "entries.unlinked.delete.deleting": "Deleting Entries", "entries.unlinked.delete": "Delete Unlinked Entries", - "entries.unlinked.description": "Each library entry is linked to a file in one of your directories. If a file linked to an entry is moved or deleted outside of TagStudio, it is then considered unlinked. Unlinked entries may be automatically relinked via searching your directories or deleted if desired.", - "entries.unlinked.refresh_all": "Refresh All", - "entries.unlinked.relink.attempting": "Attempting to Relink %{x[0]+1}/%{len(self.lib.missing_files)} Entries, %{self.fixed} Successfully Relinked", - "entries.unlinked.relink.manual": "Manual Relink", + "entries.unlinked.description": "Each library entry is linked to a file in one of your directories. If a file linked to an entry is moved or deleted outside of TagStudio, it is then considered unlinked.

Unlinked entries may be automatically relinked via searching your directories or deleted if desired.", + "entries.unlinked.missing_count.none": "Unlinked Entries: N/A", + "entries.unlinked.missing_count.some": "Unlinked Entries: {count}", + "entries.unlinked.refresh_all": "&Refresh All", + "entries.unlinked.relink.attempting": "Attempting to Relink {idx}/{missing_count} Entries, {fixed_count} Successfully Relinked", + "entries.unlinked.relink.manual": "&Manual Relink", "entries.unlinked.relink.title": "Relinking Entries", "entries.unlinked.scanning": "Scanning Library for Unlinked Entries...", - "entries.unlinked.search_and_relink": "Search && Relink", + "entries.unlinked.search_and_relink": "&Search && Relink", "entries.unlinked.title": "Fix Unlinked Entries", "field.copy": "Copy Field", "field.edit": "Edit Field", @@ -30,21 +43,22 @@ "file.date_created": "Date Created", "file.date_modified": "Date Modified", "file.dimensions": "Dimensions", + "file.duplicates.description": "TagStudio supports importing DupeGuru results to manage duplicate files.", "file.duplicates.dupeguru.advice": "After mirroring, you're free to use DupeGuru to delete the unwanted files. Afterwards, use TagStudio's \"Fix Unlinked Entries\" feature in the Tools menu in order to delete the unlinked Entries.", "file.duplicates.dupeguru.file_extension": "DupeGuru Files (*.dupeguru)", - "file.duplicates.dupeguru.load_file": "Load DupeGuru File", + "file.duplicates.dupeguru.load_file": "&Load DupeGuru File", "file.duplicates.dupeguru.no_file": "No DupeGuru File Selected", "file.duplicates.dupeguru.open_file": "Open DupeGuru Results File", "file.duplicates.fix": "Fix Duplicate Files", "file.duplicates.matches_uninitialized": "Duplicate File Matches: N/A", - "file.duplicates.matches": "Duplicate File Matches: %{count}", - "file.duplicates.mirror_entries": "Mirror Entries", + "file.duplicates.matches": "Duplicate File Matches: {count}", + "file.duplicates.mirror_entries": "&Mirror Entries", "file.duplicates.mirror.description": "Mirror the Entry data across each duplicate match set, combining all data while not removing or duplicating fields. This operation will not delete any files or data.", "file.duration": "Length", "file.not_found": "File Not Found", "file.open_file_with": "Open file with", "file.open_file": "Open file", - "file.open_location.generic": "Show file in explorer", + "file.open_location.generic": "Show file in file explorer", "file.open_location.mac": "Reveal in Finder", "file.open_location.windows": "Show in File Explorer", "folders_to_tags.close_all": "Close All", @@ -53,52 +67,110 @@ "folders_to_tags.open_all": "Open All", "folders_to_tags.title": "Create Tags From Folders", "generic.add": "Add", + "generic.apply_alt": "&Apply", "generic.apply": "Apply", + "generic.cancel_alt": "&Cancel", "generic.cancel": "Cancel", + "generic.close": "Close", + "generic.continue": "Continue", "generic.copy": "Copy", "generic.cut": "Cut", + "generic.delete_alt": "&Delete", "generic.delete": "Delete", + "generic.done_alt": "&Done", "generic.done": "Done", + "generic.edit_alt": "&Edit", "generic.edit": "Edit", + "generic.filename": "Filename", "generic.navigation.back": "Back", "generic.navigation.next": "Next", + "generic.overwrite_alt": "&Overwrite", + "generic.overwrite": "Overwrite", "generic.paste": "Paste", "generic.recent_libraries": "Recent Libraries", + "generic.rename_alt": "&Rename", + "generic.rename": "Rename", + "generic.save": "Save", + "generic.skip_alt": "&Skip", + "generic.skip": "Skip", "help.visit_github": "Visit GitHub Repository", "home.search_entries": "Search Entries", "home.search_library": "Search Library", "home.search_tags": "Search Tags", "home.search": "Search", + "home.thumbnail_size.extra_large": "Extra Large Thumbnails", + "home.thumbnail_size.large": "Large Thumbnails", + "home.thumbnail_size.medium": "Medium Thumbnails", + "home.thumbnail_size.mini": "Mini Thumbnails", + "home.thumbnail_size.small": "Small Thumbnails", "home.thumbnail_size": "Thumbnail Size", - "ignore_list.add_extension": "Add Extension", + "ignore_list.add_extension": "&Add Extension", "ignore_list.mode.exclude": "Exclude", "ignore_list.mode.include": "Include", - "ignore_list.mode.label": "List Mode", + "ignore_list.mode.label": "List Mode:", "ignore_list.title": "File Extensions", + "json_migration.checking_for_parity": "Checking for Parity...", + "json_migration.creating_database_tables": "Creating SQL Database Tables...", + "json_migration.description": "
Start and preview the results of the library migration process. The converted library will not be used unless you click \"Finish Migration\".

Library data should either have matching values or feature a \"Matched\" label. Values that do not match will be displayed in red and feature a \"(!)\" symbol next to them.
This process may take up to several minutes for larger libraries.
", + "json_migration.discrepancies_found.description": "Discrepancies were found between the original and converted library formats. Please review and choose to whether continue with the migration or to cancel.", + "json_migration.discrepancies_found": "Library Discrepancies Found", + "json_migration.finish_migration": "Finish Migration", + "json_migration.heading.aliases": "Aliases:", + "json_migration.heading.colors": "Colors:", + "json_migration.heading.differ": "Discrepancy", + "json_migration.heading.entires": "Entries:", + "json_migration.heading.extension_list_type": "Extension List Type:", + "json_migration.heading.fields": "Fields:", + "json_migration.heading.file_extension_list": "File Extension List:", + "json_migration.heading.match": "Matched", + "json_migration.heading.parent_tags": "Parent Tags:", + "json_migration.heading.paths": "Paths:", + "json_migration.heading.shorthands": "Shorthands:", + "json_migration.heading.tags": "Tags:", + "json_migration.info.description": "Library save files created with TagStudio versions 9.4 and below will need to be migrated to the new v9.5+ format.

What you need to know:

", + "json_migration.migrating_files_entries": "Migrating {entries:,d} File Entries...", + "json_migration.migration_complete_with_discrepancies": "Migration Complete, Discrepancies Found", + "json_migration.migration_complete": "Migration Complete!", + "json_migration.start_and_preview": "Start and Preview", + "json_migration.title.new_lib": "

v9.5+ Library

", + "json_migration.title.old_lib": "

v9.4 Library

", + "json_migration.title": "Save Format Migration: \"{path}\"", + "landing.open_create_library": "Open/Create Library {shortcut}", "library.field.add": "Add Field", - "library.field.confirm_remove": "Are you sure you want to remove this \"%{self.lib.get_field_attr(field, \"name\")}\" field?", + "library.field.confirm_remove": "Are you sure you want to remove this \"{name}\" field?", "library.field.mixed_data": "Mixed Data", "library.field.remove": "Remove Field", "library.missing": "Library Location is Missing", "library.name": "Library", "library.refresh.scanning_preparing": "Scanning Directories for New Files...\nPreparing...", - "library.refresh.scanning": "Scanning Directories for New Files...\n%{x + 1} File%{\"s\" if x + 1 != 1 else \"\"} Searched, %{len(self.lib.files_not_in_library)} New Files Found", + "library.refresh.scanning.plural": "Scanning Directories for New Files...\n{searched_count} Files Searched, {found_count} New Files Found", + "library.refresh.scanning.singular": "Scanning Directories for New Files...\n{searched_count} File Searched, {found_count} New Files Found", "library.refresh.title": "Refreshing Directories", "library.scan_library.title": "Scanning Library", - "macros.running.dialog.new_entries": "Running Configured Macros on %{x + 1}/%{len(new_ids)} New Entries", + "macros.running.dialog.new_entries": "Running Configured Macros on {count}/{total} New Entries", "macros.running.dialog.title": "Running Macros on New Entries", + "media_player.autoplay": "Autoplay", "menu.edit.ignore_list": "Ignore Files and Folders", + "menu.edit.manage_file_extensions": "Manage File Extensions", + "menu.edit.manage_tags": "Manage Tags", + "menu.edit.new_tag": "New &Tag", "menu.edit": "Edit", + "menu.file.close_library": "&Close Library", "menu.file.new_library": "New Library", - "menu.file.open_create_library": "Open/Create Library", + "menu.file.open_create_library": "&Open/Create Library", "menu.file.open_library": "Open Library", + "menu.file.refresh_directories": "&Refresh Directories", + "menu.file.save_backup": "&Save Library Backup", "menu.file.save_library": "Save Library", - "menu.file": "File", - "menu.help": "Help", - "menu.macros": "Macros", + "menu.file": "&File", + "menu.help": "&Help", + "menu.macros.folders_to_tags": "Folders to Tags", + "menu.macros": "&Macros", "menu.select": "Select", - "menu.tools": "Tools", - "menu.view": "View", + "menu.tools.fix_duplicate_files": "Fix Duplicate &Files", + "menu.tools.fix_unlinked_entries": "Fix &Unlinked Entries", + "menu.tools": "&Tools", + "menu.view": "&View", "menu.window": "Window", "preview.no_selection": "No Items Selected", "select.all": "Select All", @@ -106,27 +178,39 @@ "settings.open_library_on_start": "Open Library on Start", "settings.show_filenames_in_grid": "Show Filenames in Grid", "settings.show_recent_libraries": "Show Recent Libraries", - "splash.opening_library": "Opening Library", - "status.library_backup_success": "Library Backup Saved at:", + "splash.opening_library": "Opening Library \"{library_path}\"...", + "status.library_backup_in_progress": "Saving Library Backup...", + "status.library_backup_success": "Library Backup Saved at: \"{path}\" ({time_span})", + "status.library_closed": "Library Closed ({time_span})", + "status.library_closing": "Closing Library...", "status.library_save_success": "Library Saved and Closed!", - "status.library_search_query": "Searching Library for", - "status.results_found": "{results.total_count} Results Found", + "status.library_search_query": "Searching Library...", + "status.results_found": "{count} Results Found ({time_span})", "status.results": "Results", "tag_manager.title": "Library Tags", "tag.add_to_search": "Add to Search", + "tag.add.plural": "Add Tags", "tag.add": "Add Tag", "tag.aliases": "Aliases", "tag.color": "Color", + "tag.confirm_delete": "Are you sure you want to delete the tag \"{tag_name}\"?", + "tag.create": "Create Tag", + "tag.edit": "Edit Tag", "tag.name": "Name", "tag.new": "New Tag", "tag.parent_tags.add": "Add Parent Tag(s)", "tag.parent_tags.description": "This tag can be treated as a substitute for any of these Parent Tags in searches.", "tag.parent_tags": "Parent Tags", + "tag.remove": "Remove Tag", "tag.search_for_tag": "Search for Tag", "tag.shorthand": "Shorthand", + "tag.tag_name_required": "Tag Name (Required)", "view.size.0": "Mini", "view.size.1": "Small", "view.size.2": "Medium", "view.size.3": "Large", - "view.size.4": "Extra Large" + "view.size.4": "Extra Large", + "window.message.error_opening_library": "Error opening library.", + "window.title.error": "Error", + "window.title.open_create_library": "Open/Create Library" } diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index 3de0118d..d1125ede 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -25,6 +25,8 @@ from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout, from src.qt.pagination import Pagination from src.qt.widgets.landing import LandingWidget +from src.qt.translations import Translations + # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -77,6 +79,8 @@ class Ui_MainWindow(QMainWindow): # Thumbnail Size placeholder self.thumb_size_combobox = QComboBox(self.centralwidget) self.thumb_size_combobox.setObjectName(u"thumbSizeComboBox") + Translations.translate_with_setter(self.thumb_size_combobox.setPlaceholderText, "home.thumbnail_size") + self.thumb_size_combobox.setCurrentText("") sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -128,7 +132,7 @@ class Ui_MainWindow(QMainWindow): self.horizontalLayout_2 = QHBoxLayout() self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") self.horizontalLayout_2.setSizeConstraint(QLayout.SetMinimumSize) - self.backButton = QPushButton(self.centralwidget) + self.backButton = QPushButton("<", self.centralwidget) self.backButton.setObjectName(u"backButton") self.backButton.setMinimumSize(QSize(0, 32)) self.backButton.setMaximumSize(QSize(32, 16777215)) @@ -139,7 +143,7 @@ class Ui_MainWindow(QMainWindow): self.horizontalLayout_2.addWidget(self.backButton) - self.forwardButton = QPushButton(self.centralwidget) + self.forwardButton = QPushButton(">", self.centralwidget) self.forwardButton.setObjectName(u"forwardButton") self.forwardButton.setMinimumSize(QSize(0, 32)) self.forwardButton.setMaximumSize(QSize(32, 16777215)) @@ -152,6 +156,7 @@ class Ui_MainWindow(QMainWindow): self.horizontalLayout_2.addWidget(self.forwardButton) self.searchField = QLineEdit(self.centralwidget) + Translations.translate_with_setter(self.searchField.setPlaceholderText, "home.search_entries") self.searchField.setObjectName(u"searchField") self.searchField.setMinimumSize(QSize(0, 32)) font2 = QFont() @@ -167,6 +172,7 @@ class Ui_MainWindow(QMainWindow): self.horizontalLayout_2.addWidget(self.searchField) self.searchButton = QPushButton(self.centralwidget) + Translations.translate_qobject(self.searchButton, "home.search") self.searchButton.setObjectName(u"searchButton") self.searchButton.setMinimumSize(QSize(0, 32)) self.searchButton.setFont(font2) @@ -186,33 +192,9 @@ class Ui_MainWindow(QMainWindow): self.statusbar.setSizePolicy(sizePolicy1) MainWindow.setStatusBar(self.statusbar) - self.retranslateUi(MainWindow) - QMetaObject.connectSlotsByName(MainWindow) # setupUi - def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QCoreApplication.translate( - "MainWindow", u"MainWindow", None)) - # Navigation buttons - self.backButton.setText( - QCoreApplication.translate("MainWindow", u"<", None)) - self.forwardButton.setText( - QCoreApplication.translate("MainWindow", u">", None)) - - # Search field - self.searchField.setPlaceholderText( - QCoreApplication.translate("MainWindow", u"Search Entries", None)) - self.searchButton.setText( - QCoreApplication.translate("MainWindow", u"Search", None)) - - self.thumb_size_combobox.setCurrentText("") - - # Thumbnail size selector - self.thumb_size_combobox.setPlaceholderText( - QCoreApplication.translate("MainWindow", u"Thumbnail Size", None)) - # retranslateUi - def moveEvent(self, event) -> None: # time.sleep(0.02) # sleep for 20ms pass diff --git a/tagstudio/src/qt/modals/add_field.py b/tagstudio/src/qt/modals/add_field.py index fd0e0c99..e948e511 100644 --- a/tagstudio/src/qt/modals/add_field.py +++ b/tagstudio/src/qt/modals/add_field.py @@ -14,6 +14,7 @@ from PySide6.QtWidgets import ( QWidget, ) from src.core.library import Library +from src.qt.translations import Translations class AddFieldModal(QWidget): @@ -26,7 +27,7 @@ class AddFieldModal(QWidget): super().__init__() self.is_connected = False self.lib = library - self.setWindowTitle("Add Field") + Translations.translate_with_setter(self.setWindowTitle, "library.field.add") self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setMinimumSize(400, 300) self.root_layout = QVBoxLayout(self) @@ -40,7 +41,7 @@ class AddFieldModal(QWidget): # 'text-align:center;' "font-weight:bold;" "font-size:14px;" "padding-top: 6px" "" ) - self.title_widget.setText("Add Field") + Translations.translate_qobject(self.title_widget, "library.field.add") self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) self.list_widget = QListWidget() @@ -54,13 +55,13 @@ class AddFieldModal(QWidget): # self.cancel_button.setText('Cancel') self.cancel_button = QPushButton() - self.cancel_button.setText("Cancel") + Translations.translate_qobject(self.cancel_button, "generic.cancel") self.cancel_button.clicked.connect(self.hide) # self.cancel_button.clicked.connect(widget.reset) self.button_layout.addWidget(self.cancel_button) self.save_button = QPushButton() - self.save_button.setText("Add") + Translations.translate_qobject(self.save_button, "generic.add") # self.save_button.setAutoDefault(True) self.save_button.setDefault(True) self.save_button.clicked.connect(self.hide) diff --git a/tagstudio/src/qt/modals/build_tag.py b/tagstudio/src/qt/modals/build_tag.py index 15de641e..e0ce6615 100644 --- a/tagstudio/src/qt/modals/build_tag.py +++ b/tagstudio/src/qt/modals/build_tag.py @@ -24,6 +24,7 @@ from src.core.library import Library, Tag from src.core.library.alchemy.enums import TagColor from src.core.palette import ColorType, UiColor, get_tag_color, get_ui_color from src.qt.modals.tag_search import TagSearchPanel +from src.qt.translations import Translations from src.qt.widgets.panel import PanelModal, PanelWidget from src.qt.widgets.tag import TagWidget @@ -69,12 +70,14 @@ class BuildTagPanel(PanelWidget): self.name_layout.setSpacing(0) self.name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) self.name_title = QLabel() - self.name_title.setText("Name") + Translations.translate_qobject(self.name_title, "tag.name") self.name_layout.addWidget(self.name_title) self.name_field = QLineEdit() self.name_field.setFixedHeight(24) self.name_field.textChanged.connect(self.on_name_changed) - self.name_field.setPlaceholderText("Tag Name (Required)") + Translations.translate_with_setter( + self.name_field.setPlaceholderText, "tag.tag_name_required" + ) self.name_layout.addWidget(self.name_field) # Shorthand ------------------------------------------------------------ @@ -85,7 +88,7 @@ class BuildTagPanel(PanelWidget): self.shorthand_layout.setSpacing(0) self.shorthand_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) self.shorthand_title = QLabel() - self.shorthand_title.setText("Shorthand") + Translations.translate_qobject(self.shorthand_title, "tag.shorthand") self.shorthand_layout.addWidget(self.shorthand_title) self.shorthand_field = QLineEdit() self.shorthand_layout.addWidget(self.shorthand_field) @@ -98,7 +101,7 @@ class BuildTagPanel(PanelWidget): self.aliases_layout.setSpacing(0) self.aliases_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) self.aliases_title = QLabel() - self.aliases_title.setText("Aliases") + Translations.translate_qobject(self.aliases_title, "tag.aliases") self.aliases_layout.addWidget(self.aliases_title) self.aliases_table = QTableWidget(0, 2) @@ -122,7 +125,7 @@ class BuildTagPanel(PanelWidget): self.subtags_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) self.subtags_title = QLabel() - self.subtags_title.setText("Parent Tags") + Translations.translate_qobject(self.subtags_title, "tag.parent_tags") self.subtags_layout.addWidget(self.subtags_title) self.scroll_contents = QWidget() @@ -151,7 +154,9 @@ class BuildTagPanel(PanelWidget): tsp = TagSearchPanel(self.lib, exclude_ids) tsp.tag_chosen.connect(lambda x: self.add_subtag_callback(x)) - self.add_tag_modal = PanelModal(tsp, "Add Parent Tags", "Add Parent Tags") + self.add_tag_modal = PanelModal(tsp) + Translations.translate_with_setter(self.add_tag_modal.setTitle, "tag.parent_tags.add") + Translations.translate_with_setter(self.add_tag_modal.setWindowTitle, "tag.parent_tags.add") self.subtags_add_button.clicked.connect(self.add_tag_modal.show) # Shorthand ------------------------------------------------------------ @@ -162,7 +167,7 @@ class BuildTagPanel(PanelWidget): self.color_layout.setSpacing(0) self.color_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) self.color_title = QLabel() - self.color_title.setText("Color") + Translations.translate_qobject(self.color_title, "tag.color") self.color_layout.addWidget(self.color_title) self.color_field = QComboBox() self.color_field.setEditable(False) @@ -200,7 +205,7 @@ class BuildTagPanel(PanelWidget): self.new_alias_names: dict = {} self.new_item_id = sys.maxsize - self.set_tag(tag or Tag(name="New Tag")) + self.set_tag(tag or Tag(name=Translations["tag.new"])) if tag is None: self.name_field.selectAll() diff --git a/tagstudio/src/qt/modals/delete_unlinked.py b/tagstudio/src/qt/modals/delete_unlinked.py index b382d083..da229c99 100644 --- a/tagstudio/src/qt/modals/delete_unlinked.py +++ b/tagstudio/src/qt/modals/delete_unlinked.py @@ -15,6 +15,7 @@ from PySide6.QtWidgets import ( QWidget, ) from src.core.utils.missing_files import MissingRegistry +from src.qt.translations import Translations from src.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. @@ -29,7 +30,7 @@ class DeleteUnlinkedEntriesModal(QWidget): super().__init__() self.driver = driver self.tracker = tracker - self.setWindowTitle("Delete Unlinked Entries") + Translations.translate_with_setter(self.setWindowTitle, "entries.unlinked.delete") self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setMinimumSize(500, 400) self.root_layout = QVBoxLayout(self) @@ -38,9 +39,11 @@ class DeleteUnlinkedEntriesModal(QWidget): self.desc_widget = QLabel() self.desc_widget.setObjectName("descriptionLabel") self.desc_widget.setWordWrap(True) - self.desc_widget.setText(f""" - Are you sure you want to delete the following {self.tracker.missing_files_count} entries? - """) + Translations.translate_qobject( + self.desc_widget, + "entries.unlinked.delete.confirm", + count=self.tracker.missing_files_count, + ) self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) self.list_view = QListView() @@ -53,13 +56,13 @@ class DeleteUnlinkedEntriesModal(QWidget): self.button_layout.addStretch(1) self.cancel_button = QPushButton() - self.cancel_button.setText("&Cancel") + Translations.translate_qobject(self.cancel_button, "generic.cancel_alt") self.cancel_button.setDefault(True) self.cancel_button.clicked.connect(self.hide) self.button_layout.addWidget(self.cancel_button) self.delete_button = QPushButton() - self.delete_button.setText("&Delete") + Translations.translate_qobject(self.delete_button, "generic.delete_alt") self.delete_button.clicked.connect(self.hide) self.delete_button.clicked.connect(lambda: self.delete_entries()) self.button_layout.addWidget(self.delete_button) @@ -69,9 +72,11 @@ class DeleteUnlinkedEntriesModal(QWidget): self.root_layout.addWidget(self.button_container) def refresh_list(self): - self.desc_widget.setText(f""" - Are you sure you want to delete the following {self.tracker.missing_files_count} entries? - """) + self.desc_widget.setText( + Translations.translate_formatted( + "entries.unlinked.delete.confirm", count=self.tracker.missing_files_count + ) + ) self.model.clear() for i in self.tracker.missing_files: @@ -81,14 +86,17 @@ class DeleteUnlinkedEntriesModal(QWidget): def delete_entries(self): def displayed_text(x): - return f"Deleting {x}/{self.tracker.missing_files_count} Unlinked Entries" + return Translations.translate_formatted( + "entries.unlinked.delete.deleting_count", + idx=x, + count=self.tracker.missing_files_count, + ) pw = ProgressWidget( - window_title="Deleting Entries", - label_text="", cancel_button_text=None, minimum=0, maximum=self.tracker.missing_files_count, ) + Translations.translate_with_setter(pw.setWindowTitle, "entries.unlinked.delete.deleting") pw.from_iterable_function(self.tracker.execute_deletion, displayed_text, self.done.emit) diff --git a/tagstudio/src/qt/modals/drop_import.py b/tagstudio/src/qt/modals/drop_import.py index ec94a3e5..9b13b3d1 100644 --- a/tagstudio/src/qt/modals/drop_import.py +++ b/tagstudio/src/qt/modals/drop_import.py @@ -17,6 +17,7 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) +from src.qt.translations import Translations from src.qt.widgets.progress import ProgressWidget if TYPE_CHECKING: @@ -41,7 +42,7 @@ class DropImportModal(QWidget): self.driver: QtDriver = driver # Widget ====================== - self.setWindowTitle("Conflicting File(s)") + Translations.translate_with_setter(self.setWindowTitle, "drop_import.title") self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setMinimumSize(500, 400) self.root_layout = QVBoxLayout(self) @@ -50,7 +51,7 @@ class DropImportModal(QWidget): self.desc_widget = QLabel() self.desc_widget.setObjectName("descriptionLabel") self.desc_widget.setWordWrap(True) - self.desc_widget.setText("The following files have filenames already exist in the library") + self.desc_widget.setText(Translations["drop_import.description"]) self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) # Duplicate File List ======== @@ -65,25 +66,25 @@ class DropImportModal(QWidget): self.button_layout.addStretch(1) self.skip_button = QPushButton() - self.skip_button.setText("&Skip") + Translations.translate_qobject(self.skip_button, "generic.skip_alt") self.skip_button.setDefault(True) self.skip_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.SKIP)) self.button_layout.addWidget(self.skip_button) self.overwrite_button = QPushButton() - self.overwrite_button.setText("&Overwrite") + Translations.translate_qobject(self.overwrite_button, "generic.overwrite_alt") self.overwrite_button.clicked.connect( lambda: self.begin_transfer(DuplicateChoice.OVERWRITE) ) self.button_layout.addWidget(self.overwrite_button) self.rename_button = QPushButton() - self.rename_button.setText("&Rename") + Translations.translate_qobject(self.rename_button, "generic.rename_alt") self.rename_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.RENAME)) self.button_layout.addWidget(self.rename_button) self.cancel_button = QPushButton() - self.cancel_button.setText("&Cancel") + Translations.translate_qobject(self.cancel_button, "generic.cancel_alt") self.cancel_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.CANCEL)) self.button_layout.addWidget(self.cancel_button) @@ -137,7 +138,11 @@ class DropImportModal(QWidget): def ask_duplicates_choice(self): """Display the message widgeth with a list of the duplicated files.""" self.desc_widget.setText( - f"The following {len(self.duplicate_files)} file(s) have filenames already exist in the library." # noqa: E501 + Translations["drop_import.duplicates_choice.singular"] + if len(self.duplicate_files) == 1 + else Translations.translate_formatted( + "drop_import.duplicates_choice.plural", count=len(self.duplicate_files) + ) ) self.model.clear() @@ -158,21 +163,21 @@ class DropImportModal(QWidget): return def displayed_text(x): - text = ( - f"Importing New Files...\n{x[0] + 1} File{'s' if x[0] + 1 != 1 else ''} Imported." + return Translations.translate_formatted( + "drop_import.progress.label.singular" + if x[0] + 1 == 1 + else "drop_import.progress.label.plural", + count=x[0] + 1, + suffix=f" {x[1]} {self.choice.value}" if self.choice else "", ) - if self.choice: - text += f" {x[1]} {self.choice.value}" - - return text pw = ProgressWidget( - window_title="Import Files", - label_text="Importing New Files...", cancel_button_text=None, minimum=0, maximum=len(self.files), ) + Translations.translate_with_setter(pw.setWindowTitle, "drop_import.progress.window_title") + Translations.translate_with_setter(pw.update_label, "drop_import.progress.label.initial") pw.from_iterable_function( self.copy_files, diff --git a/tagstudio/src/qt/modals/file_extension.py b/tagstudio/src/qt/modals/file_extension.py index 224638ca..49b5b147 100644 --- a/tagstudio/src/qt/modals/file_extension.py +++ b/tagstudio/src/qt/modals/file_extension.py @@ -18,6 +18,7 @@ from PySide6.QtWidgets import ( ) from src.core.enums import LibraryPrefs from src.core.library import Library +from src.qt.translations import Translations from src.qt.widgets.panel import PanelWidget @@ -35,7 +36,7 @@ class FileExtensionModal(PanelWidget): super().__init__() # Initialize Modal ===================================================== self.lib = library - self.setWindowTitle("File Extensions") + Translations.translate_with_setter(self.setWindowTitle, "ignore_list.title") self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setMinimumSize(240, 400) self.root_layout = QVBoxLayout(self) @@ -50,7 +51,7 @@ class FileExtensionModal(PanelWidget): # Create "Add Button" Widget ------------------------------------------- self.add_button = QPushButton() - self.add_button.setText("&Add Extension") + Translations.translate_qobject(self.add_button, "ignore_list.add_extension") self.add_button.clicked.connect(self.add_item) self.add_button.setDefault(True) self.add_button.setMinimumWidth(100) @@ -61,11 +62,17 @@ class FileExtensionModal(PanelWidget): self.mode_layout.setContentsMargins(0, 0, 0, 0) self.mode_layout.setSpacing(12) self.mode_label = QLabel() - self.mode_label.setText("List Mode:") + Translations.translate_qobject(self.mode_label, "ignore_list.mode.label") self.mode_combobox = QComboBox() self.mode_combobox.setEditable(False) - self.mode_combobox.addItem("Include") - self.mode_combobox.addItem("Exclude") + self.mode_combobox.addItem("") + self.mode_combobox.addItem("") + Translations.translate_with_setter( + lambda text: self.mode_combobox.setItemText(0, text), "ignore_list.mode.include" + ) + Translations.translate_with_setter( + lambda text: self.mode_combobox.setItemText(1, text), "ignore_list.mode.exclude" + ) is_exclude_list = int(bool(self.lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST))) diff --git a/tagstudio/src/qt/modals/fix_dupes.py b/tagstudio/src/qt/modals/fix_dupes.py index fccdb5bc..76261039 100644 --- a/tagstudio/src/qt/modals/fix_dupes.py +++ b/tagstudio/src/qt/modals/fix_dupes.py @@ -17,6 +17,7 @@ from PySide6.QtWidgets import ( from src.core.library import Library from src.core.utils.dupe_files import DupeRegistry from src.qt.modals.mirror_entities import MirrorEntriesModal +from src.qt.translations import Translations # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: @@ -30,7 +31,7 @@ class FixDupeFilesModal(QWidget): self.driver = driver self.count = -1 self.filename = "" - self.setWindowTitle("Fix Duplicate Files") + Translations.translate_with_setter(self.setWindowTitle, "file.duplicates.fix") self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setMinimumSize(400, 300) self.root_layout = QVBoxLayout(self) @@ -42,9 +43,7 @@ class FixDupeFilesModal(QWidget): self.desc_widget.setObjectName("descriptionLabel") self.desc_widget.setWordWrap(True) self.desc_widget.setStyleSheet("text-align:left;") - self.desc_widget.setText( - "TagStudio supports importing DupeGuru results to manage duplicate files." - ) + Translations.translate_qobject(self.desc_widget, "file.duplicates.description") self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) self.dupe_count = QLabel() @@ -54,34 +53,25 @@ class FixDupeFilesModal(QWidget): self.file_label = QLabel() self.file_label.setObjectName("fileLabel") - self.file_label.setText("No DupeGuru File Selected") + Translations.translate_qobject(self.file_label, "file.duplicates.dupeguru.no_file") self.open_button = QPushButton() - self.open_button.setText("&Load DupeGuru File") + Translations.translate_qobject(self.open_button, "file.duplicates.dupeguru.load_file") self.open_button.clicked.connect(self.select_file) self.mirror_modal = MirrorEntriesModal(self.driver, self.tracker) self.mirror_modal.done.connect(self.refresh_dupes) self.mirror_button = QPushButton() - self.mirror_button.setText("&Mirror Entries") + Translations.translate_qobject(self.mirror_button, "file.duplicates.mirror_entries") self.mirror_button.clicked.connect(self.mirror_modal.show) self.mirror_desc = QLabel() self.mirror_desc.setWordWrap(True) - self.mirror_desc.setText( - "Mirror the Entry data across each duplicate match set, combining all data while not " - "removing or duplicating fields. This operation will not delete any files or data." - ) + Translations.translate_qobject(self.mirror_desc, "file.duplicates.mirror.description") self.advice_label = QLabel() self.advice_label.setWordWrap(True) - # fmt: off - self.advice_label.setText( - "After mirroring, you're free to use DupeGuru to delete the unwanted files. " - "Afterwards, use TagStudio's \"Fix Unlinked Entries\" feature in the " - "Tools menu in order to delete the unlinked Entries." - ) - # fmt: on + Translations.translate_qobject(self.advice_label, "file.duplicates.dupeguru.advice") self.button_container = QWidget() self.button_layout = QHBoxLayout(self.button_container) @@ -89,7 +79,7 @@ class FixDupeFilesModal(QWidget): self.button_layout.addStretch(1) self.done_button = QPushButton() - self.done_button.setText("&Done") + Translations.translate_qobject(self.done_button, "generic.done_alt") self.done_button.setDefault(True) self.done_button.clicked.connect(self.hide) self.button_layout.addWidget(self.done_button) @@ -108,9 +98,11 @@ class FixDupeFilesModal(QWidget): self.set_dupe_count(-1) def select_file(self): - qfd = QFileDialog(self, "Open DupeGuru Results File", str(self.lib.library_dir)) + qfd = QFileDialog( + self, Translations["file.duplicates.dupeguru.open_file"], str(self.lib.library_dir) + ) qfd.setFileMode(QFileDialog.FileMode.ExistingFile) - qfd.setNameFilter("DupeGuru Files (*.dupeguru)") + qfd.setNameFilter(Translations["file.duplicates.dupeguru.file_extension"]) if qfd.exec_(): filename = qfd.selectedFiles() if filename: @@ -120,7 +112,7 @@ class FixDupeFilesModal(QWidget): if filename: self.file_label.setText(filename) else: - self.file_label.setText("No DupeGuru File Selected") + self.file_label.setText(Translations["file.duplicates.dupeguru.no_file"]) self.filename = filename self.refresh_dupes() self.mirror_modal.refresh_list() @@ -132,10 +124,14 @@ class FixDupeFilesModal(QWidget): def set_dupe_count(self, count: int): if count < 0: self.mirror_button.setDisabled(True) - self.dupe_count.setText("Duplicate File Matches: N/A") + self.dupe_count.setText(Translations["file.duplicates.matches_uninitialized"]) elif count == 0: self.mirror_button.setDisabled(True) - self.dupe_count.setText(f"Duplicate File Matches: {count}") + self.dupe_count.setText( + Translations.translate_formatted("file.duplicates.matches", count=count) + ) else: self.mirror_button.setDisabled(False) - self.dupe_count.setText(f"Duplicate File Matches: {count}") + self.dupe_count.setText( + Translations.translate_formatted("file.duplicates.matches", count=count) + ) diff --git a/tagstudio/src/qt/modals/fix_unlinked.py b/tagstudio/src/qt/modals/fix_unlinked.py index 7c580d3f..eafe3297 100644 --- a/tagstudio/src/qt/modals/fix_unlinked.py +++ b/tagstudio/src/qt/modals/fix_unlinked.py @@ -12,6 +12,7 @@ from src.core.utils.missing_files import MissingRegistry from src.qt.modals.delete_unlinked import DeleteUnlinkedEntriesModal from src.qt.modals.merge_dupe_entries import MergeDuplicateEntries from src.qt.modals.relink_unlinked import RelinkUnlinkedEntries +from src.qt.translations import Translations from src.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. @@ -29,7 +30,7 @@ class FixUnlinkedEntriesModal(QWidget): self.missing_count = -1 self.dupe_count = -1 - self.setWindowTitle("Fix Unlinked Entries") + Translations.translate_with_setter(self.setWindowTitle, "entries.unlinked.title") self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setMinimumSize(400, 300) self.root_layout = QVBoxLayout(self) @@ -39,13 +40,7 @@ class FixUnlinkedEntriesModal(QWidget): self.unlinked_desc_widget.setObjectName("unlinkedDescriptionLabel") self.unlinked_desc_widget.setWordWrap(True) self.unlinked_desc_widget.setStyleSheet("text-align:left;") - self.unlinked_desc_widget.setText( - "Each library entry is linked to a file in one of your directories. " - "If a file linked to an entry is moved or deleted outside of TagStudio, " - "it is then considered unlinked.\n\n" - "Unlinked entries may be automatically relinked via searching your directories, " - "manually relinked by the user, or deleted if desired." - ) + Translations.translate_qobject(self.unlinked_desc_widget, "entries.unlinked.description") self.missing_count_label = QLabel() self.missing_count_label.setObjectName("missingCountLabel") @@ -58,14 +53,14 @@ class FixUnlinkedEntriesModal(QWidget): self.dupe_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.refresh_unlinked_button = QPushButton() - self.refresh_unlinked_button.setText("&Refresh All") + Translations.translate_qobject(self.refresh_unlinked_button, "entries.unlinked.refresh_all") self.refresh_unlinked_button.clicked.connect(self.refresh_missing_files) self.merge_class = MergeDuplicateEntries(self.lib, self.driver) self.relink_class = RelinkUnlinkedEntries(self.tracker) self.search_button = QPushButton() - self.search_button.setText("&Search && Relink") + Translations.translate_qobject(self.search_button, "entries.unlinked.search_and_relink") self.relink_class.done.connect( # refresh the grid lambda: ( @@ -76,7 +71,7 @@ class FixUnlinkedEntriesModal(QWidget): self.search_button.clicked.connect(self.relink_class.repair_entries) self.manual_button = QPushButton() - self.manual_button.setText("&Manual Relink") + Translations.translate_qobject(self.manual_button, "entries.unlinked.relink.manual") self.manual_button.setHidden(True) self.delete_button = QPushButton() @@ -88,7 +83,7 @@ class FixUnlinkedEntriesModal(QWidget): self.driver.filter_items(), ) ) - self.delete_button.setText("De&lete Unlinked Entries") + Translations.translate_qobject(self.delete_button, "entries.unlinked.delete_alt") self.delete_button.clicked.connect(self.delete_modal.show) self.button_container = QWidget() @@ -97,7 +92,7 @@ class FixUnlinkedEntriesModal(QWidget): self.button_layout.addStretch(1) self.done_button = QPushButton() - self.done_button.setText("&Done") + Translations.translate_qobject(self.done_button, "generic.done_alt") self.done_button.setDefault(True) self.done_button.clicked.connect(self.hide) self.button_layout.addWidget(self.done_button) @@ -116,12 +111,12 @@ class FixUnlinkedEntriesModal(QWidget): def refresh_missing_files(self): pw = ProgressWidget( - window_title="Scanning Library", - label_text="Scanning Library for Unlinked Entries...", cancel_button_text=None, minimum=0, maximum=self.lib.entries_count, ) + Translations.translate_with_setter(pw.setWindowTitle, "library.scan_library.title") + Translations.translate_with_setter(pw.update_label, "entries.unlinked.scanning") pw.from_iterable_function( self.tracker.refresh_missing_files, @@ -139,9 +134,13 @@ class FixUnlinkedEntriesModal(QWidget): if self.missing_count < 0: self.search_button.setDisabled(True) self.delete_button.setDisabled(True) - self.missing_count_label.setText("Unlinked Entries: N/A") + self.missing_count_label.setText(Translations["entries.unlinked.missing_count.none"]) else: # disable buttons if there are no files to fix self.search_button.setDisabled(self.missing_count == 0) self.delete_button.setDisabled(self.missing_count == 0) - self.missing_count_label.setText(f"Unlinked Entries: {self.missing_count}") + self.missing_count_label.setText( + Translations.translate_formatted( + "entries.unlinked.missing_count.some", count=self.missing_count + ) + ) diff --git a/tagstudio/src/qt/modals/folders_to_tags.py b/tagstudio/src/qt/modals/folders_to_tags.py index f9962a0e..f680fdc1 100644 --- a/tagstudio/src/qt/modals/folders_to_tags.py +++ b/tagstudio/src/qt/modals/folders_to_tags.py @@ -23,6 +23,7 @@ from src.core.library import Library, Tag from src.core.library.alchemy.fields import _FieldID from src.core.palette import ColorType, get_tag_color from src.qt.flowlayout import FlowLayout +from src.qt.translations import Translations if typing.TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -164,7 +165,7 @@ class FoldersToTagsModal(QWidget): self.count = -1 self.filename = "" - self.setWindowTitle("Create Tags From Folders") + Translations.translate_with_setter(self.setWindowTitle, "folders_to_tags.title") self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setMinimumSize(640, 640) self.root_layout = QVBoxLayout(self) @@ -174,7 +175,7 @@ class FoldersToTagsModal(QWidget): self.title_widget.setObjectName("title") self.title_widget.setWordWrap(True) self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px") - self.title_widget.setText("Create Tags From Folders") + Translations.translate_qobject(self.title_widget, "folders_to_tags.title") self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) self.desc_widget = QLabel() @@ -190,10 +191,10 @@ class FoldersToTagsModal(QWidget): self.open_close_button_layout = QHBoxLayout(self.open_close_button_w) self.open_all_button = QPushButton() - self.open_all_button.setText("Open All") + Translations.translate_qobject(self.open_all_button, "folders_to_tags.open_all") self.open_all_button.clicked.connect(lambda: self.set_all_branches(False)) self.close_all_button = QPushButton() - self.close_all_button.setText("Close All") + Translations.translate_qobject(self.close_all_button, "folders_to_tags.close_all") self.close_all_button.clicked.connect(lambda: self.set_all_branches(True)) self.open_close_button_layout.addWidget(self.open_all_button) @@ -212,7 +213,7 @@ class FoldersToTagsModal(QWidget): self.scroll_area.setWidget(self.scroll_contents) self.apply_button = QPushButton() - self.apply_button.setText("&Apply") + Translations.translate_qobject(self.apply_button, "generic.apply_alt") self.apply_button.setMinimumWidth(100) self.apply_button.clicked.connect(self.on_apply) diff --git a/tagstudio/src/qt/modals/merge_dupe_entries.py b/tagstudio/src/qt/modals/merge_dupe_entries.py index 66c94dda..35bdf7c1 100644 --- a/tagstudio/src/qt/modals/merge_dupe_entries.py +++ b/tagstudio/src/qt/modals/merge_dupe_entries.py @@ -7,6 +7,7 @@ import typing from PySide6.QtCore import QObject, Signal from src.core.library import Library from src.core.utils.dupe_files import DupeRegistry +from src.qt.translations import Translations from src.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. @@ -25,11 +26,11 @@ class MergeDuplicateEntries(QObject): def merge_entries(self): pw = ProgressWidget( - window_title="Merging Duplicate Entries", - label_text="Merging Duplicate Entries...", cancel_button_text=None, minimum=0, maximum=self.tracker.groups_count, ) + Translations.translate_with_setter(pw.setWindowTitle, "entries.duplicate.merge.label") + Translations.translate_with_setter(pw.update_label, "entries.duplicate.merge.label") pw.from_iterable_function(self.tracker.merge_dupe_entries, None, self.done.emit) diff --git a/tagstudio/src/qt/modals/mirror_entities.py b/tagstudio/src/qt/modals/mirror_entities.py index 6ab19957..4cb37a1c 100644 --- a/tagstudio/src/qt/modals/mirror_entities.py +++ b/tagstudio/src/qt/modals/mirror_entities.py @@ -17,6 +17,7 @@ from PySide6.QtWidgets import ( QWidget, ) from src.core.utils.dupe_files import DupeRegistry +from src.qt.translations import Translations from src.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. @@ -30,7 +31,7 @@ class MirrorEntriesModal(QWidget): def __init__(self, driver: "QtDriver", tracker: DupeRegistry): super().__init__() self.driver = driver - self.setWindowTitle("Mirror Entries") + Translations.translate_with_setter(self.setWindowTitle, "entries.mirror.window_title") self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setMinimumSize(500, 400) self.root_layout = QVBoxLayout(self) @@ -40,10 +41,9 @@ class MirrorEntriesModal(QWidget): self.desc_widget = QLabel() self.desc_widget.setObjectName("descriptionLabel") self.desc_widget.setWordWrap(True) - - self.desc_widget.setText(f""" - Are you sure you want to mirror the following {self.tracker.groups_count} Entries? - """) + Translations.translate_qobject( + self.desc_widget, "entries.mirror.confirmation", count=self.tracker.groups_count + ) self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) self.list_view = QListView() @@ -56,13 +56,13 @@ class MirrorEntriesModal(QWidget): self.button_layout.addStretch(1) self.cancel_button = QPushButton() - self.cancel_button.setText("&Cancel") + Translations.translate_qobject(self.cancel_button, "generic.cancel_alt") self.cancel_button.setDefault(True) self.cancel_button.clicked.connect(self.hide) self.button_layout.addWidget(self.cancel_button) self.mirror_button = QPushButton() - self.mirror_button.setText("&Mirror") + Translations.translate_qobject(self.mirror_button, "entries.mirror") self.mirror_button.clicked.connect(self.hide) self.mirror_button.clicked.connect(self.mirror_entries) self.button_layout.addWidget(self.mirror_button) @@ -72,9 +72,11 @@ class MirrorEntriesModal(QWidget): self.root_layout.addWidget(self.button_container) def refresh_list(self): - self.desc_widget.setText(f""" - Are you sure you want to mirror the following {self.tracker.groups_count} Entries? - """) + self.desc_widget.setText( + Translations.translate_formatted( + "entries.mirror.confirmation", count=self.tracker.groups_count + ) + ) self.model.clear() for i in self.tracker.groups: @@ -82,15 +84,16 @@ class MirrorEntriesModal(QWidget): def mirror_entries(self): def displayed_text(x): - return f"Mirroring {x + 1}/{self.tracker.groups_count} Entries..." + return Translations.translate_formatted( + "entries.mirror.label", idx=x + 1, count=self.tracker.groups_count + ) pw = ProgressWidget( - window_title="Mirroring Entries", - label_text="", cancel_button_text=None, minimum=0, maximum=self.tracker.groups_count, ) + Translations.translate_with_setter(pw.setWindowTitle, "entries.mirror.title") pw.from_iterable_function( self.mirror_entries_runnable, diff --git a/tagstudio/src/qt/modals/relink_unlinked.py b/tagstudio/src/qt/modals/relink_unlinked.py index ab2a1d40..0363c848 100644 --- a/tagstudio/src/qt/modals/relink_unlinked.py +++ b/tagstudio/src/qt/modals/relink_unlinked.py @@ -5,6 +5,7 @@ from PySide6.QtCore import QObject, Signal from src.core.utils.missing_files import MissingRegistry +from src.qt.translations import Translations from src.qt.widgets.progress import ProgressWidget @@ -17,16 +18,19 @@ class RelinkUnlinkedEntries(QObject): def repair_entries(self): def displayed_text(x): - text = f"Attempting to Relink {x}/{self.tracker.missing_files_count} Entries. \n" - text += f"{self.tracker.files_fixed_count} Successfully Relinked." - return text + return Translations.translate_formatted( + "entries.unlinked.relink.attempting", + idx=x, + missing_count=self.tracker.missing_files_count, + fixed_count=self.tracker.files_fixed_count, + ) pw = ProgressWidget( - window_title="Relinking Entries", label_text="", cancel_button_text=None, minimum=0, maximum=self.tracker.missing_files_count, ) + Translations.translate_with_setter(pw.setWindowTitle, "entries.unlinked.relink.title") pw.from_iterable_function(self.tracker.fix_missing_files, displayed_text, self.done.emit) diff --git a/tagstudio/src/qt/modals/tag_database.py b/tagstudio/src/qt/modals/tag_database.py index bc5cfce9..560611d5 100644 --- a/tagstudio/src/qt/modals/tag_database.py +++ b/tagstudio/src/qt/modals/tag_database.py @@ -18,6 +18,7 @@ from PySide6.QtWidgets import ( from src.core.constants import RESERVED_TAG_END, RESERVED_TAG_START from src.core.library import Library, Tag from src.qt.modals.build_tag import BuildTagPanel +from src.qt.translations import Translations from src.qt.widgets.panel import PanelModal, PanelWidget from src.qt.widgets.tag import TagWidget @@ -44,7 +45,7 @@ class TagDatabasePanel(PanelWidget): self.search_field = QLineEdit() self.search_field.setObjectName("searchField") self.search_field.setMinimumSize(QSize(0, 32)) - self.search_field.setPlaceholderText("Search Tags") + Translations.translate_with_setter(self.search_field.setPlaceholderText, "home.search_tags") self.search_field.textEdited.connect(lambda: self.update_tags(self.search_field.text())) self.search_field.returnPressed.connect( lambda checked=False: self.on_return(self.search_field.text()) @@ -63,7 +64,7 @@ class TagDatabasePanel(PanelWidget): self.scroll_area.setWidget(self.scroll_contents) self.create_tag_button = QPushButton() - self.create_tag_button.setText("Create Tag") + Translations.translate_qobject(self.create_tag_button, "tag.create") self.create_tag_button.clicked.connect(self.build_tag) self.root_layout.addWidget(self.search_field) @@ -72,14 +73,14 @@ class TagDatabasePanel(PanelWidget): self.update_tags() def build_tag(self): + panel = BuildTagPanel(self.lib) self.modal = PanelModal( - BuildTagPanel(self.lib), - "New Tag", - "Add Tag", + panel, has_save=True, ) + Translations.translate_with_setter(self.modal.setTitle, "tag.new") + Translations.translate_with_setter(self.modal.setWindowTitle, "tag.add") - panel: BuildTagPanel = self.modal.widget self.modal.saved.connect( lambda: ( self.lib.add_tag( @@ -134,8 +135,8 @@ class TagDatabasePanel(PanelWidget): return message_box = QMessageBox() - message_box.setWindowTitle("Remove Tag") - message_box.setText(f'Are you sure you want to delete the tag "{tag.name}"?') + Translations.translate_with_setter(message_box.setWindowTitle, "tag.remove") + Translations.translate_qobject(message_box, "tag.confirm_delete", tag_name=tag.name) message_box.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) # type: ignore message_box.setIcon(QMessageBox.Question) # type: ignore @@ -156,10 +157,10 @@ class TagDatabasePanel(PanelWidget): self.edit_modal = PanelModal( build_tag_panel, tag.name, - "Edit Tag", done_callback=(self.update_tags(self.search_field.text())), has_save=True, ) + Translations.translate_with_setter(self.edit_modal.setWindowTitle, "tag.edit") # TODO Check Warning: Expected type 'BuildTagPanel', got 'PanelWidget' instead self.edit_modal.saved.connect(lambda: self.edit_tag_callback(build_tag_panel)) self.edit_modal.show() diff --git a/tagstudio/src/qt/modals/tag_search.py b/tagstudio/src/qt/modals/tag_search.py index 72f3650b..90932b0d 100644 --- a/tagstudio/src/qt/modals/tag_search.py +++ b/tagstudio/src/qt/modals/tag_search.py @@ -19,6 +19,7 @@ from PySide6.QtWidgets import ( ) from src.core.library import Library from src.core.palette import ColorType, get_tag_color +from src.qt.translations import Translations from src.qt.widgets.panel import PanelWidget from src.qt.widgets.tag import TagWidget @@ -41,7 +42,7 @@ class TagSearchPanel(PanelWidget): self.search_field = QLineEdit() self.search_field.setObjectName("searchField") self.search_field.setMinimumSize(QSize(0, 32)) - self.search_field.setPlaceholderText("Search Tags") + Translations.translate_with_setter(self.search_field.setPlaceholderText, "home.search_tags") self.search_field.textEdited.connect(lambda: self.update_tags(self.search_field.text())) self.search_field.returnPressed.connect( lambda checked=False: self.on_return(self.search_field.text()) diff --git a/tagstudio/src/qt/platform_strings.py b/tagstudio/src/qt/platform_strings.py index 9eda3ef8..23851dd4 100644 --- a/tagstudio/src/qt/platform_strings.py +++ b/tagstudio/src/qt/platform_strings.py @@ -6,11 +6,13 @@ import platform +from src.qt.translations import Translations + class PlatformStrings: - open_file_str: str = "Open in file explorer" + open_file_str: str = Translations["file.open_location.generic"] if platform.system() == "Windows": - open_file_str = "Open in Explorer" + open_file_str = Translations["file.open_location.windows"] elif platform.system() == "Darwin": - open_file_str = "Reveal in Finder" + open_file_str = Translations["file.open_location.mac"] diff --git a/tagstudio/src/qt/translations.py b/tagstudio/src/qt/translations.py new file mode 100644 index 00000000..5d2dcea0 --- /dev/null +++ b/tagstudio/src/qt/translations.py @@ -0,0 +1,95 @@ +from pathlib import Path +from typing import Callable + +import structlog +import ujson +from PySide6.QtCore import QObject, Signal +from PySide6.QtGui import QAction +from PySide6.QtWidgets import QLabel, QMenu, QMessageBox, QPushButton + +from .helpers.qbutton_wrapper import QPushButtonWrapper + +logger = structlog.get_logger(__name__) + +DEFAULT_TRANSLATION = "en" + + +class TranslatedString(QObject): + changed = Signal(str) + + __default_value: str + __value: str | None = None + + def __init__(self, value: str): + super().__init__() + self.__default_value = value + + @property + def value(self) -> str: + return self.__value or self.__default_value + + @value.setter + def value(self, value: str): + if self.__value != value: + self.__value = value + self.changed.emit(self.__value) + + +class Translator: + _strings: dict[str, TranslatedString] = {} + _lang: str = DEFAULT_TRANSLATION + + def __init__(self): + for k, v in self.__get_translation_dict(DEFAULT_TRANSLATION).items(): + self._strings[k] = TranslatedString(v) + + def __get_translation_dict(self, lang: str) -> dict[str, str]: + with open( + Path(__file__).parents[2] / "resources" / "translations" / f"{lang}.json", + encoding="utf-8", + ) as f: + return ujson.loads(f.read()) + + def change_language(self, lang: str): + self._lang = lang + translated = self.__get_translation_dict(lang) + for k in self._strings: + self._strings[k].value = translated.get(k, None) + + def translate_qobject(self, widget: QObject, key: str, **kwargs): + """Translates the text of the QObject using :func:`translate_with_setter`.""" + if isinstance(widget, (QLabel, QAction, QPushButton, QMessageBox, QPushButtonWrapper)): + self.translate_with_setter(widget.setText, key, **kwargs) + elif isinstance(widget, (QMenu)): + self.translate_with_setter(widget.setTitle, key, **kwargs) + else: + raise RuntimeError + + def translate_with_setter(self, setter: Callable[[str], None], key: str, **kwargs): + """Calls `setter` everytime the language changes and passes the translated string for `key`. + + Also formats the translation with the given keyword arguments. + """ + if key in self._strings: + self._strings[key].changed.connect(lambda text: setter(self.__format(text, **kwargs))) + setter(self.translate_formatted(key, **kwargs)) + + def __format(self, text: str, **kwargs) -> str: + try: + return text.format(**kwargs) + except KeyError: + logger.warning( + "Error while formatting translation.", text=text, kwargs=kwargs, language=self._lang + ) + return text + + def translate_formatted(self, key: str, **kwargs) -> str: + return self.__format(self[key], **kwargs) + + def __getitem__(self, key: str) -> str: + # return "???" + return self._strings[key].value if key in self._strings else "Not Translated" + + +Translations = Translator() +# Translations.change_language("de") diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 2c238807..06c136dc 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -84,6 +84,7 @@ from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal from src.qt.modals.folders_to_tags import FoldersToTagsModal from src.qt.modals.tag_database import TagDatabasePanel from src.qt.resource_manager import ResourceManager +from src.qt.translations import Translations from src.qt.widgets.item_thumb import BadgeType, ItemThumb from src.qt.widgets.migration_modal import JsonMigrationModal from src.qt.widgets.panel import PanelModal @@ -186,10 +187,10 @@ class QtDriver(DriverMixin, QObject): def open_library_from_dialog(self): dir = QFileDialog.getExistingDirectory( - None, - "Open/Create Library", - "/", - QFileDialog.Option.ShowDirsOnly, + parent=None, + caption=Translations["window.title.open_create_library"], + dir="/", + options=QFileDialog.Option.ShowDirsOnly, ) if dir not in (None, ""): self.open_library(Path(dir)) @@ -254,15 +255,22 @@ class QtDriver(DriverMixin, QObject): self.main_window.setMenuBar(menu_bar) menu_bar.setNativeMenuBar(True) - file_menu = QMenu("&File", menu_bar) - edit_menu = QMenu("&Edit", menu_bar) - view_menu = QMenu("&View", menu_bar) - tools_menu = QMenu("&Tools", menu_bar) - macros_menu = QMenu("&Macros", menu_bar) - help_menu = QMenu("&Help", menu_bar) + file_menu = QMenu(menu_bar) + Translations.translate_qobject(file_menu, "menu.file") + edit_menu = QMenu(menu_bar) + Translations.translate_qobject(edit_menu, "generic.edit_alt") + view_menu = QMenu(menu_bar) + Translations.translate_qobject(view_menu, "menu.view") + tools_menu = QMenu(menu_bar) + Translations.translate_qobject(tools_menu, "menu.tools") + macros_menu = QMenu(menu_bar) + Translations.translate_qobject(macros_menu, "menu.macros") + help_menu = QMenu(menu_bar) + Translations.translate_qobject(help_menu, "menu.help") # File Menu ============================================================ - open_library_action = QAction("&Open/Create Library", menu_bar) + open_library_action = QAction(menu_bar) + Translations.translate_qobject(open_library_action, "menu.file.open_create_library") open_library_action.triggered.connect(lambda: self.open_library_from_dialog()) open_library_action.setShortcut( QtCore.QKeyCombination( @@ -273,7 +281,8 @@ class QtDriver(DriverMixin, QObject): open_library_action.setToolTip("Ctrl+O") file_menu.addAction(open_library_action) - save_library_backup_action = QAction("&Save Library Backup", menu_bar) + save_library_backup_action = QAction(menu_bar) + Translations.translate_qobject(save_library_backup_action, "menu.file.save_backup") save_library_backup_action.triggered.connect( lambda: self.callback_library_needed_check(self.backup_library) ) @@ -291,7 +300,8 @@ class QtDriver(DriverMixin, QObject): file_menu.addSeparator() - add_new_files_action = QAction("&Refresh Directories", menu_bar) + add_new_files_action = QAction(menu_bar) + Translations.translate_qobject(add_new_files_action, "menu.file.refresh_directories") add_new_files_action.triggered.connect( lambda: self.callback_library_needed_check(self.add_new_files_callback) ) @@ -305,12 +315,14 @@ class QtDriver(DriverMixin, QObject): file_menu.addAction(add_new_files_action) file_menu.addSeparator() - close_library_action = QAction("&Close Library", menu_bar) + close_library_action = QAction(menu_bar) + Translations.translate_qobject(close_library_action, "menu.file.close_library") close_library_action.triggered.connect(self.close_library) file_menu.addAction(close_library_action) file_menu.addSeparator() - open_on_start_action = QAction("Open Library on Start", self) + open_on_start_action = QAction(self) + Translations.translate_qobject(open_on_start_action, "settings.open_library_on_start") open_on_start_action.setCheckable(True) open_on_start_action.setChecked( bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool)) @@ -321,7 +333,8 @@ class QtDriver(DriverMixin, QObject): file_menu.addAction(open_on_start_action) # Edit Menu ============================================================ - new_tag_action = QAction("New &Tag", menu_bar) + new_tag_action = QAction(menu_bar) + Translations.translate_qobject(new_tag_action, "menu.edit.new_tag") new_tag_action.triggered.connect(lambda: self.add_tag_action_callback()) new_tag_action.setShortcut( QtCore.QKeyCombination( @@ -334,7 +347,8 @@ class QtDriver(DriverMixin, QObject): edit_menu.addSeparator() - select_all_action = QAction("Select All", menu_bar) + select_all_action = QAction(menu_bar) + Translations.translate_qobject(select_all_action, "select.all") select_all_action.triggered.connect(self.select_all_action_callback) select_all_action.setShortcut( QtCore.QKeyCombination( @@ -345,7 +359,8 @@ class QtDriver(DriverMixin, QObject): select_all_action.setToolTip("Ctrl+A") edit_menu.addAction(select_all_action) - clear_select_action = QAction("Clear Selection", menu_bar) + clear_select_action = QAction(menu_bar) + Translations.translate_qobject(clear_select_action, "select.clear") clear_select_action.triggered.connect(self.clear_select_action_callback) clear_select_action.setShortcut(QtCore.Qt.Key.Key_Escape) clear_select_action.setToolTip("Esc") @@ -353,16 +368,21 @@ class QtDriver(DriverMixin, QObject): edit_menu.addSeparator() - manage_file_extensions_action = QAction("Manage File Extensions", menu_bar) + manage_file_extensions_action = QAction(menu_bar) + Translations.translate_qobject( + manage_file_extensions_action, "menu.edit.manage_file_extensions" + ) manage_file_extensions_action.triggered.connect(self.show_file_extension_modal) edit_menu.addAction(manage_file_extensions_action) - tag_database_action = QAction("Manage Tags", menu_bar) + tag_database_action = QAction(menu_bar) + Translations.translate_qobject(tag_database_action, "menu.edit.manage_tags") tag_database_action.triggered.connect(lambda: self.show_tag_database()) edit_menu.addAction(tag_database_action) # View Menu ============================================================ - show_libs_list_action = QAction("Show Recent Libraries", menu_bar) + show_libs_list_action = QAction(menu_bar) + Translations.translate_qobject(show_libs_list_action, "settings.show_recent_libraries") show_libs_list_action.setCheckable(True) show_libs_list_action.setChecked( bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool)) @@ -375,7 +395,8 @@ class QtDriver(DriverMixin, QObject): ) view_menu.addAction(show_libs_list_action) - show_filenames_action = QAction("Show Filenames in Grid", menu_bar) + show_filenames_action = QAction(menu_bar) + Translations.translate_qobject(show_filenames_action, "settings.show_filenames_in_grid") show_filenames_action.setCheckable(True) show_filenames_action.setChecked( bool(self.settings.value(SettingItems.SHOW_FILENAMES, defaultValue=True, type=bool)) @@ -394,7 +415,10 @@ class QtDriver(DriverMixin, QObject): 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(menu_bar) + Translations.translate_qobject( + fix_unlinked_entries_action, "menu.tools.fix_unlinked_entries" + ) fix_unlinked_entries_action.triggered.connect(create_fix_unlinked_entries_modal) tools_menu.addAction(fix_unlinked_entries_action) @@ -403,7 +427,8 @@ class QtDriver(DriverMixin, QObject): 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(menu_bar) + Translations.translate_qobject(fix_dupe_files_action, "menu.tools.fix_duplicate_files") fix_dupe_files_action.triggered.connect(create_dupe_files_modal) tools_menu.addAction(fix_dupe_files_action) @@ -426,12 +451,14 @@ class QtDriver(DriverMixin, QObject): self.folders_modal = FoldersToTagsModal(self.lib, self) self.folders_modal.show() - folders_to_tags_action = QAction("Folders to Tags", menu_bar) + folders_to_tags_action = QAction(menu_bar) + Translations.translate_qobject(folders_to_tags_action, "menu.macros.folders_to_tags") folders_to_tags_action.triggered.connect(create_folders_tags_modal) macros_menu.addAction(folders_to_tags_action) # Help Menu ============================================================ - self.repo_action = QAction("Visit GitHub Repository", menu_bar) + self.repo_action = QAction(menu_bar) + Translations.translate_qobject(self.repo_action, "help.visit_github") self.repo_action.triggered.connect( lambda: webbrowser.open("https://github.com/TagStudioDev/TagStudio") ) @@ -455,12 +482,13 @@ class QtDriver(DriverMixin, QObject): str(Path(__file__).parents[2] / "resources/qt/fonts/Oxanium-Bold.ttf") ) + # TODO this doesn't update when the language is changed self.thumb_sizes: list[tuple[str, int]] = [ - ("Extra Large Thumbnails", 256), - ("Large Thumbnails", 192), - ("Medium Thumbnails", 128), - ("Small Thumbnails", 96), - ("Mini Thumbnails", 76), + (Translations["home.thumbnail_size.extra_large"], 256), + (Translations["home.thumbnail_size.large"], 192), + (Translations["home.thumbnail_size.medium"], 128), + (Translations["home.thumbnail_size.small"], 96), + (Translations["home.thumbnail_size.mini"], 76), ] self.item_thumbs: list[ItemThumb] = [] self.thumb_renderers: list[ThumbRenderer] = [] @@ -472,7 +500,9 @@ class QtDriver(DriverMixin, QObject): # check status of library path evaluating if path_result.success and path_result.library_path: self.splash.showMessage( - f'Opening Library "{path_result.library_path}"...', + Translations.translate_formatted( + "splash.opening_library", library_path=path_result.library_path + ), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), QColor("#9782ff"), ) @@ -489,8 +519,8 @@ class QtDriver(DriverMixin, QObject): msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Icon.Critical) msg_box.setText(message) - msg_box.setWindowTitle("Error") - msg_box.addButton("Close", QMessageBox.ButtonRole.AcceptRole) + msg_box.setWindowTitle(Translations["window.title.error"]) + msg_box.addButton(Translations["generic.close"], QMessageBox.ButtonRole.AcceptRole) # Show the message box msg_box.exec() @@ -584,7 +614,7 @@ class QtDriver(DriverMixin, QObject): return logger.info("Closing Library...") - self.main_window.statusbar.showMessage("Closing Library...") + self.main_window.statusbar.showMessage(Translations["status.library_closing"]) start_time = time.time() self.settings.setValue(SettingItems.LAST_LIBRARY, str(self.lib.library_dir)) @@ -610,26 +640,32 @@ class QtDriver(DriverMixin, QObject): end_time = time.time() self.main_window.statusbar.showMessage( - f"Library Closed ({format_timespan(end_time - start_time)})" + Translations.translate_formatted( + "status.library_closed", time_span=format_timespan(end_time - start_time) + ) ) def backup_library(self): logger.info("Backing Up Library...") - self.main_window.statusbar.showMessage("Saving Library...") + self.main_window.statusbar.showMessage(Translations["status.library_backup_in_progress"]) start_time = time.time() target_path = self.lib.save_library_backup_to_disk() end_time = time.time() self.main_window.statusbar.showMessage( - f'Library Backup Saved at: "{target_path}" ({format_timespan(end_time - start_time)})' + Translations.translate_formatted( + "status.library_backup_success", + path=target_path, + time_span=format_timespan(end_time - start_time), + ) ) def add_tag_action_callback(self): self.modal = PanelModal( BuildTagPanel(self.lib), - "New Tag", - "Add Tag", has_save=True, ) + Translations.translate_with_setter(self.modal.setTitle, "tag.new") + Translations.translate_with_setter(self.modal.setWindowTitle, "tag.add") panel: BuildTagPanel = self.modal.widget self.modal.saved.connect( @@ -665,21 +701,21 @@ class QtDriver(DriverMixin, QObject): def show_tag_database(self): self.modal = PanelModal( widget=TagDatabasePanel(self.lib), - title="Library Tags", - window_title="Library Tags", done_callback=self.preview_panel.update_widgets, has_save=False, ) + Translations.translate_with_setter(self.modal.setTitle, "tag_manager.title") + Translations.translate_with_setter(self.modal.setWindowTitle, "tag_manager.title") self.modal.show() def show_file_extension_modal(self): panel = FileExtensionModal(self.lib) self.modal = PanelModal( panel, - "File Extensions", - "File Extensions", has_save=True, ) + Translations.translate_with_setter(self.modal.setTitle, "ignore_list.title") + Translations.translate_with_setter(self.modal.setWindowTitle, "ignore_list.title") self.modal.saved.connect(lambda: (panel.save(), self.filter_items())) self.modal.show() @@ -689,12 +725,13 @@ class QtDriver(DriverMixin, QObject): tracker = RefreshDirTracker(self.lib) pw = ProgressWidget( - window_title="Refreshing Directories", - label_text="Scanning Directories for New Files...\nPreparing...", cancel_button_text=None, minimum=0, maximum=0, ) + Translations.translate_with_setter(pw.setWindowTitle, "library.refresh.title") + Translations.translate_with_setter(pw.update_label, "library.refresh.scanning_preparing") + pw.show() iterator = FunctionIterator(lambda: tracker.refresh_dir(self.lib.library_dir)) @@ -702,9 +739,13 @@ class QtDriver(DriverMixin, QObject): lambda x: ( pw.update_progress(x + 1), pw.update_label( - f"Scanning Directories for New Files...\n{x + 1}" - f' File{"s" if x + 1 != 1 else ""} Searched,' - f" {tracker.files_count} New Files Found" + Translations.translate_formatted( + "library.refresh.scanning.plural" + if x + 1 != 1 + else "library.refresh.scanning.singular", + searched_count=x + 1, + found_count=tracker.files_count, + ) ), ) ) @@ -727,18 +768,24 @@ class QtDriver(DriverMixin, QObject): 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", cancel_button_text=None, minimum=0, maximum=files_count, ) + Translations.translate_with_setter(pw.setWindowTitle, "macros.running.dialog.title") + Translations.translate_with_setter( + pw.update_label, "macros.running.dialog.new_entries", count=1, total=files_count + ) pw.show() iterator.value.connect( lambda x: ( pw.update_progress(x + 1), - pw.update_label(f"Running Configured Macros on {x + 1}/{files_count} New Entries"), + pw.update_label( + Translations.translate_formatted( + "macros.running.dialog.new_entries", count=x + 1, total=files_count + ) + ), ) ) r = CustomRunnable(iterator.run) @@ -1134,7 +1181,7 @@ class QtDriver(DriverMixin, QObject): self.filter = dataclasses.replace(self.filter, **dataclasses.asdict(filter)) # inform user about running search - self.main_window.statusbar.showMessage("Searching Library...") + self.main_window.statusbar.showMessage(Translations["status.library_search_query"]) self.main_window.statusbar.repaint() # search the library @@ -1149,7 +1196,11 @@ class QtDriver(DriverMixin, QObject): # inform user about completed search self.main_window.statusbar.showMessage( - f"{results.total_count} Results Found ({format_timespan(end_time - start_time)})" + Translations.translate_formatted( + "status.results_found", + count=results.total_count, + time_span=format_timespan(end_time - start_time), + ) ) # update page content @@ -1196,9 +1247,13 @@ class QtDriver(DriverMixin, QObject): def open_library(self, path: Path) -> None: """Open a TagStudio library.""" - open_message: str = f'Opening Library "{str(path)}"...' - self.main_window.landing_widget.set_status_label(open_message) - self.main_window.statusbar.showMessage(open_message, 3) + translation_params = {"key": "splash.opening_library", "library_path": str(path)} + Translations.translate_with_setter( + self.main_window.landing_widget.set_status_label, **translation_params + ) + self.main_window.statusbar.showMessage( + Translations.translate_formatted(**translation_params), 3 + ) self.main_window.repaint() open_status: LibraryStatus = self.lib.open_library(path) @@ -1216,7 +1271,9 @@ class QtDriver(DriverMixin, QObject): def init_library(self, path: Path, open_status: LibraryStatus): if not open_status.success: - self.show_error_message(open_status.message or "Error opening library.") + self.show_error_message( + open_status.message or Translations["window.message.error_opening_library"] + ) return open_status self.init_workers() @@ -1228,8 +1285,12 @@ class QtDriver(DriverMixin, QObject): self.add_new_files_callback() self.update_libs_list(path) - title_text = f"{self.base_title} - Library '{self.lib.library_dir}'" - self.main_window.setWindowTitle(title_text) + Translations.translate_with_setter( + self.main_window.setWindowTitle, + "app.title", + base_title=self.base_title, + library_dir=self.lib.library_dir, + ) self.main_window.setAcceptDrops(True) self.selected.clear() diff --git a/tagstudio/src/qt/widgets/item_thumb.py b/tagstudio/src/qt/widgets/item_thumb.py index 9935fe4e..f79708ad 100644 --- a/tagstudio/src/qt/widgets/item_thumb.py +++ b/tagstudio/src/qt/widgets/item_thumb.py @@ -30,6 +30,7 @@ from src.core.media_types import MediaCategories, MediaType from src.qt.flowlayout import FlowWidget from src.qt.helpers.file_opener import FileOpenerHelper from src.qt.platform_strings import PlatformStrings +from src.qt.translations import Translations from src.qt.widgets.thumb_button import ThumbButton from src.qt.widgets.thumb_renderer import ThumbRenderer @@ -217,7 +218,8 @@ class ItemThumb(FlowWidget): self.thumb_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) self.opener = FileOpenerHelper("") - open_file_action = QAction("Open file", self) + open_file_action = QAction(self) + Translations.translate_qobject(open_file_action, "file.open_file") open_file_action.triggered.connect(self.opener.open_file) open_explorer_action = QAction(PlatformStrings.open_file_str, self) open_explorer_action.triggered.connect(self.opener.open_explorer) @@ -306,7 +308,8 @@ class ItemThumb(FlowWidget): self.cb_layout.addWidget(badge) # Filename Label ======================================================= - self.file_label = QLabel(text="Filename") + self.file_label = QLabel() + Translations.translate_qobject(self.file_label, "generic.filename") self.file_label.setStyleSheet(ItemThumb.filename_style) self.file_label.setMaximumHeight(self.label_height) if not show_filename_label: diff --git a/tagstudio/src/qt/widgets/landing.py b/tagstudio/src/qt/widgets/landing.py index c1a5a7d8..3f6b1594 100644 --- a/tagstudio/src/qt/widgets/landing.py +++ b/tagstudio/src/qt/widgets/landing.py @@ -13,6 +13,7 @@ from PySide6.QtCore import QEasingCurve, QPoint, QPropertyAnimation, Qt from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget from src.qt.helpers.color_overlay import gradient_overlay, theme_fg_overlay +from src.qt.translations import Translations from src.qt.widgets.clickable_label import ClickableLabel # Only import for type checking/autocompletion, will not be imported at runtime. @@ -62,7 +63,9 @@ class LandingWidget(QWidget): open_shortcut_text = "(Ctrl+O)" self.open_button: QPushButton = QPushButton() self.open_button.setMinimumWidth(200) - self.open_button.setText(f"Open/Create Library {open_shortcut_text}") + Translations.translate_qobject( + self.open_button, "landing.open_create_library", shortcut=open_shortcut_text + ) self.open_button.clicked.connect(self.driver.open_library_from_dialog) # Create status label -------------------------------------------------- diff --git a/tagstudio/src/qt/widgets/migration_modal.py b/tagstudio/src/qt/widgets/migration_modal.py index a2978f56..6e8aeb06 100644 --- a/tagstudio/src/qt/widgets/migration_modal.py +++ b/tagstudio/src/qt/widgets/migration_modal.py @@ -30,6 +30,7 @@ from src.core.library.json.library import Library as JsonLibrary # type: ignore from src.qt.helpers.custom_runnable import CustomRunnable from src.qt.helpers.function_iterator import FunctionIterator from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper +from src.qt.translations import Translations from src.qt.widgets.paged_panel.paged_body_wrapper import PagedBodyWrapper from src.qt.widgets.paged_panel.paged_panel import PagedPanel from src.qt.widgets.paged_panel.paged_panel_state import PagedPanelState @@ -54,7 +55,7 @@ class JsonMigrationModal(QObject): self.is_migration_initialized: bool = False self.discrepancies: list[str] = [] - self.title: str = f'Save Format Migration: "{self.path}"' + self.title: str = Translations.translate_formatted("json_migration.title", path=self.path) self.warning: str = "(!)" self.old_entry_count: int = 0 @@ -77,24 +78,17 @@ class JsonMigrationModal(QObject): def init_page_info(self) -> None: """Initialize the migration info page.""" body_wrapper: PagedBodyWrapper = PagedBodyWrapper() - body_label: QLabel = QLabel( - "Library save files created with TagStudio versions 9.4 and below will " - "need to be migrated to the new v9.5+ format." - "
" - "

What you need to know:

" - "" - ) + body_label = QLabel() + Translations.translate_qobject(body_label, "json_migration.info.description") body_label.setWordWrap(True) body_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) body_wrapper.layout().addWidget(body_label) body_wrapper.layout().setContentsMargins(0, 36, 0, 0) - cancel_button: QPushButtonWrapper = QPushButtonWrapper("Cancel") - next_button: QPushButtonWrapper = QPushButtonWrapper("Continue") + cancel_button = QPushButtonWrapper() + Translations.translate_qobject(cancel_button, "generic.cancel") + next_button = QPushButtonWrapper() + Translations.translate_qobject(next_button, "generic.continue") cancel_button.clicked.connect(self.migration_cancelled.emit) self.stack.append( @@ -115,30 +109,20 @@ class JsonMigrationModal(QObject): body_container_layout.setContentsMargins(0, 0, 0, 0) tab: str = " " - self.match_text: str = "Matched" - self.differ_text: str = "Discrepancy" + self.match_text: str = Translations["json_migration.heading.match"] + self.differ_text: str = Translations["json_migration.heading.differ"] - entries_text: str = "Entries:" - tags_text: str = "Tags:" - shorthand_text: str = tab + "Shorthands:" - subtags_text: str = tab + "Parent Tags:" - aliases_text: str = tab + "Aliases:" - colors_text: str = tab + "Colors:" - ext_text: str = "File Extension List:" - ext_type_text: str = "Extension List Type:" - desc_text: str = ( - "
Start and preview the results of the library migration process. " - 'The converted library will not be used unless you click "Finish Migration". ' - "

" - 'Library data should either have matching values or a feature a "Matched" label. ' - 'Values that do not match will be displayed in red and feature a "(!)" ' - "symbol next to them." - "
" - "This process may take up to several minutes for larger libraries." - "
" - ) - path_parity_text: str = tab + "Paths:" - field_parity_text: str = tab + "Fields:" + entries_text: str = Translations["json_migration.heading.entires"] + tags_text: str = Translations["json_migration.heading.tags"] + shorthand_text: str = tab + Translations["json_migration.heading.shorthands"] + subtags_text: str = tab + Translations["json_migration.heading.parent_tags"] + aliases_text: str = tab + Translations["json_migration.heading.aliases"] + colors_text: str = tab + Translations["json_migration.heading.colors"] + ext_text: str = Translations["json_migration.heading.file_extension_list"] + ext_type_text: str = Translations["json_migration.heading.extension_list_type"] + desc_text: str = Translations["json_migration.description"] + path_parity_text: str = tab + Translations["json_migration.heading.paths"] + field_parity_text: str = tab + Translations["json_migration.heading.fields"] self.entries_row: int = 0 self.path_row: int = 1 @@ -153,7 +137,8 @@ class JsonMigrationModal(QObject): old_lib_container: QWidget = QWidget() old_lib_layout: QVBoxLayout = QVBoxLayout(old_lib_container) - old_lib_title: QLabel = QLabel("

v9.4 Library

") + old_lib_title = QLabel() + Translations.translate_qobject(old_lib_title, "json_migration.title.old_lib") old_lib_title.setAlignment(Qt.AlignmentFlag.AlignCenter) old_lib_layout.addWidget(old_lib_title) @@ -215,7 +200,8 @@ class JsonMigrationModal(QObject): new_lib_container: QWidget = QWidget() new_lib_layout: QVBoxLayout = QVBoxLayout(new_lib_container) - new_lib_title: QLabel = QLabel("

v9.5+ Library

") + new_lib_title = QLabel() + Translations.translate_qobject(new_lib_title, "json_migration.title.new_lib") new_lib_title.setAlignment(Qt.AlignmentFlag.AlignCenter) new_lib_layout.addWidget(new_lib_title) @@ -291,13 +277,16 @@ class JsonMigrationModal(QObject): self.body_wrapper_01.layout().addWidget(desc_label) self.body_wrapper_01.layout().setSpacing(12) - back_button: QPushButtonWrapper = QPushButtonWrapper("Back") - start_button: QPushButtonWrapper = QPushButtonWrapper("Start and Preview") + back_button = QPushButtonWrapper() + Translations.translate_qobject(back_button, "generic.navigation.back") + start_button = QPushButtonWrapper() + Translations.translate_qobject(start_button, "json_migration.start_and_preview") start_button.setMinimumWidth(120) start_button.clicked.connect(self.migrate) start_button.clicked.connect(lambda: finish_button.setDisabled(False)) start_button.clicked.connect(lambda: start_button.setDisabled(True)) - finish_button: QPushButtonWrapper = QPushButtonWrapper("Finish Migration") + finish_button: QPushButtonWrapper = QPushButtonWrapper() + Translations.translate_qobject(finish_button, "json_migration.finish_migration") finish_button.setMinimumWidth(120) finish_button.setDisabled(True) finish_button.clicked.connect(self.finish_migration) @@ -348,9 +337,11 @@ class JsonMigrationModal(QObject): lambda x: ( pb.setLabelText(f"

{x}

"), self.update_sql_value_ui(show_msg_box=False) - if x == "Checking for Parity..." + if x == Translations["json_migration.checking_for_parity"] + else (), + self.update_parity_ui() + if x == Translations["json_migration.checking_for_parity"] else (), - self.update_parity_ui() if x == "Checking for Parity..." else (), ) ) r = CustomRunnable(iterator.run) @@ -367,7 +358,7 @@ class JsonMigrationModal(QObject): """Iterate over the library migration process.""" try: # Convert JSON Library to SQLite - yield "Creating SQL Database Tables..." + yield Translations["json_migration.creating_database_tables"] self.sql_lib = SqliteLibrary() self.temp_path: Path = ( self.json_lib.library_dir / TS_FOLDER_NAME / "migration_ts_library.sqlite" @@ -379,9 +370,11 @@ class JsonMigrationModal(QObject): self.sql_lib.open_sqlite_library( self.json_lib.library_dir, is_new=True, add_default_data=False ) - yield f"Migrating {len(self.json_lib.entries):,d} File Entries..." + yield Translations.translate_formatted( + "json_migration.migrating_files_entries", entries=len(self.json_lib.entries) + ) self.sql_lib.migrate_json_to_sqlite(self.json_lib) - yield "Checking for Parity..." + yield Translations["json_migration.checking_for_parity"] check_set = set() check_set.add(self.check_field_parity()) check_set.add(self.check_path_parity()) @@ -391,9 +384,9 @@ class JsonMigrationModal(QObject): check_set.add(self.check_color_parity()) self.update_parity_ui() if False not in check_set: - yield "Migration Complete!" + yield Translations["json_migration.migration_complete"] else: - yield "Migration Complete, Discrepancies Found" + yield Translations["json_migration.migration_complete_with_discrepancies"] self.done = True except Exception as e: @@ -440,10 +433,11 @@ class JsonMigrationModal(QObject): if not show_msg_box: return msg_box = QMessageBox() - msg_box.setWindowTitle("Library Discrepancies Found") - msg_box.setText( - "Discrepancies were found between the original and converted library formats. " - "Please review and choose to whether continue with the migration or to cancel." + Translations.translate_with_setter( + msg_box.setWindowTitle, "json_migration.discrepancies_found" + ) + Translations.translate_qobject( + msg_box, "json_migration.discrepancies_found.description" ) msg_box.setDetailedText("\n".join(self.discrepancies)) msg_box.setIcon(QMessageBox.Icon.Warning) diff --git a/tagstudio/src/qt/widgets/panel.py b/tagstudio/src/qt/widgets/panel.py index b164acc9..573f6f48 100755 --- a/tagstudio/src/qt/widgets/panel.py +++ b/tagstudio/src/qt/widgets/panel.py @@ -6,6 +6,7 @@ from typing import Callable from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget +from src.qt.translations import Translations class PanelModal(QWidget): @@ -16,8 +17,8 @@ class PanelModal(QWidget): def __init__( self, widget, - title: str, - window_title: str, + title: str = "", + window_title: str = "", done_callback: Callable | None = None, save_callback: Callable | None = None, has_save: bool = False, @@ -36,7 +37,7 @@ class PanelModal(QWidget): self.title_widget.setObjectName("fieldTitle") self.title_widget.setWordWrap(True) self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px") - self.title_widget.setText(title) + self.setTitle(title) self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) self.button_container = QWidget() @@ -49,7 +50,7 @@ class PanelModal(QWidget): if not (save_callback or has_save): self.done_button = QPushButton() - self.done_button.setText("Done") + Translations.translate_qobject(self.done_button, "generic.done") self.done_button.setAutoDefault(True) self.done_button.clicked.connect(self.hide) if done_callback: @@ -59,7 +60,7 @@ class PanelModal(QWidget): if save_callback or has_save: self.cancel_button = QPushButton() - self.cancel_button.setText("Cancel") + Translations.translate_qobject(self.cancel_button, "generic.cancel") self.cancel_button.clicked.connect(self.hide) self.cancel_button.clicked.connect(widget.reset) # self.cancel_button.clicked.connect(cancel_callback) @@ -67,7 +68,7 @@ class PanelModal(QWidget): self.button_layout.addWidget(self.cancel_button) self.save_button = QPushButton() - self.save_button.setText("Save") + Translations.translate_qobject(self.save_button, "generic.save") self.save_button.setAutoDefault(True) self.save_button.clicked.connect(self.hide) self.save_button.clicked.connect(self.saved.emit) @@ -95,6 +96,9 @@ class PanelModal(QWidget): self.done_button.click() event.accept() + def setTitle(self, title: str): # noqa: N802 + self.title_widget.setText(title) + class PanelWidget(QWidget): """Used for widgets that go in a modal panel, ex. for editing or searching.""" diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 7e8e0c8b..0bb1c181 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -52,6 +52,7 @@ from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper from src.qt.helpers.rounded_pixmap_style import RoundedPixmapStyle from src.qt.modals.add_field import AddFieldModal from src.qt.platform_strings import PlatformStrings +from src.qt.translations import Translations from src.qt.widgets.fields import FieldContainer from src.qt.widgets.media_player import MediaPlayer from src.qt.widgets.panel import PanelModal @@ -119,7 +120,8 @@ class PreviewPanel(QWidget): ) date_style = "font-size:12px;" - self.open_file_action = QAction("Open file", self) + self.open_file_action = QAction(self) + Translations.translate_qobject(self.open_file_action, "file.open_file") self.open_explorer_action = QAction(PlatformStrings.open_file_str, self) self.preview_img = QPushButtonWrapper() @@ -279,7 +281,7 @@ class PreviewPanel(QWidget): self.add_field_button.setCursor(Qt.CursorShape.PointingHandCursor) self.add_field_button.setMinimumSize(96, 28) self.add_field_button.setMaximumSize(96, 28) - self.add_field_button.setText("Add Field") + Translations.translate_qobject(self.add_field_button, "library.field.add") self.afb_layout.addWidget(self.add_field_button) self.add_field_modal = AddFieldModal(self.lib) self.place_add_field_button() @@ -303,7 +305,7 @@ class PreviewPanel(QWidget): self.driver.frame_content[grid_idx] = result def remove_field_prompt(self, name: str) -> str: - return f'Are you sure you want to remove field "{name}"?' + return Translations.translate_formatted("library.field.confirm_remove", name=name) def fill_libs_widget(self, layout: QVBoxLayout): settings = self.driver.settings @@ -341,7 +343,8 @@ class PreviewPanel(QWidget): # remove any potential previous items clear_layout(layout) - label = QLabel("Recent Libraries") + label = QLabel() + Translations.translate_qobject(label, "generic.recent_libraries") label.setAlignment(Qt.AlignmentFlag.AlignCenter) row_layout = QHBoxLayout() @@ -379,7 +382,7 @@ class PreviewPanel(QWidget): lib = Path(full_val) if not lib.exists() or not (lib / TS_FOLDER_NAME).exists(): button.setDisabled(True) - button.setToolTip("Location is missing") + Translations.translate_with_setter(button.setToolTip, "library.missing") def open_library_button_clicked(path): return lambda: self.driver.open_library(Path(path)) @@ -492,16 +495,16 @@ class PreviewPanel(QWidget): created = dt.fromtimestamp(filepath.stat().st_ctime) modified: dt = dt.fromtimestamp(filepath.stat().st_mtime) self.date_created_label.setText( - f"Date Created: {dt.strftime(created, "%a, %x, %X")}" + f"Date Created: {dt.strftime(created, "%a, %x, %X")}" # TODO translate ) self.date_modified_label.setText( - f"Date Modified: {dt.strftime(modified, "%a, %x, %X")}" + f"Date Modified: {dt.strftime(modified, "%a, %x, %X")}" # TODO translate ) self.date_created_label.setHidden(False) self.date_modified_label.setHidden(False) elif filepath: - self.date_created_label.setText("Date Created: N/A") - self.date_modified_label.setText("Date Modified: N/A") + self.date_created_label.setText("Date Created: N/A") # TODO translate + self.date_modified_label.setText("Date Modified: N/A") # TODO translate self.date_created_label.setHidden(False) self.date_modified_label.setHidden(False) else: @@ -520,7 +523,7 @@ class PreviewPanel(QWidget): if not self.driver.selected: if self.selected or not self.initialized: - self.file_label.setText("No Items Selected") + self.file_label.setText("No Items Selected") # TODO translate self.file_label.set_file_path("") self.file_label.setCursor(Qt.CursorShape.ArrowCursor) @@ -777,7 +780,9 @@ class PreviewPanel(QWidget): self.media_player.hide() self.update_date_label() if self.selected != self.driver.selected: - self.file_label.setText(f"{len(self.driver.selected)} Items Selected") + self.file_label.setText( + f"{len(self.driver.selected)} Items Selected" + ) # TODO translate self.file_label.setCursor(Qt.CursorShape.ArrowCursor) self.file_label.set_file_path("") self.dimensions_label.setText("") @@ -872,10 +877,11 @@ class PreviewPanel(QWidget): else: container = self.containers[index] + # TODO this is in severe need of refactoring due to exessive code duplication if isinstance(field, TagBoxField): container.set_title(field.type.name) container.set_inline(False) - title = f"{field.type.name} (Tag Box)" + title = f"{field.type.name} (Tag Box)" # TODO translate if not is_mixed: inner_container = container.get_inner_widget() @@ -916,8 +922,8 @@ class PreviewPanel(QWidget): ) ) else: - text = "Mixed Data" - title = f"{field.type.name} (Wacky Tag Box)" + text = "Mixed Data" # TODO translate + title = f"{field.type.name} (Wacky Tag Box)" # TODO translate inner_container = TextWidget(title, text) container.set_inner_widget(inner_container) @@ -932,7 +938,7 @@ class PreviewPanel(QWidget): assert isinstance(field.value, (str, type(None))) text = field.value or "" else: - text = "Mixed Data" + text = "Mixed Data" # TODO translate title = f"{field.type.name} ({field.type.type.value})" inner_container = TextWidget(title, text) @@ -941,7 +947,7 @@ class PreviewPanel(QWidget): modal = PanelModal( EditTextLine(field.value), title=title, - window_title=f"Edit {field.type.type.value}", + window_title=f"Edit {field.type.type.value}", # TODO translate save_callback=( lambda content: ( self.update_field(field, content), @@ -973,15 +979,15 @@ class PreviewPanel(QWidget): assert isinstance(field.value, (str, type(None))) text = (field.value or "").replace("\r", "\n") else: - text = "Mixed Data" - title = f"{field.type.name} (Text Box)" + text = "Mixed Data" # TODO translate + title = f"{field.type.name} (Text Box)" # TODO translate inner_container = TextWidget(title, text) container.set_inner_widget(inner_container) if not is_mixed: modal = PanelModal( EditTextBox(field.value), title=title, - window_title=f"Edit {field.type.name}", + window_title=f"Edit {field.type.name}", # TODO translate save_callback=( lambda content: ( self.update_field(field, content), @@ -1008,14 +1014,14 @@ class PreviewPanel(QWidget): container.set_inline(False) # TODO: Localize this and/or add preferences. date = dt.strptime(field.value, "%Y-%m-%d %H:%M:%S") - title = f"{field.type.name} (Date)" + title = f"{field.type.name} (Date)" # TODO translate inner_container = TextWidget(title, date.strftime("%D - %r")) container.set_inner_widget(inner_container) except Exception: container.set_title(field.type.name) # container.set_editable(False) container.set_inline(False) - title = f"{field.type.name} (Date) (Unknown Format)" + title = f"{field.type.name} (Date) (Unknown Format)" # TODO translate inner_container = TextWidget(title, str(field.value)) container.set_inner_widget(inner_container) @@ -1029,15 +1035,15 @@ class PreviewPanel(QWidget): ) ) else: - text = "Mixed Data" - title = f"{field.type.name} (Wacky Date)" + text = "Mixed Data" # TODO translate + title = f"{field.type.name} (Wacky Date)" # TODO translate inner_container = TextWidget(title, text) container.set_inner_widget(inner_container) else: logger.warning("write_container - unknown field", field=field) container.set_title(field.type.name) container.set_inline(False) - title = f"{field.type.name} (Unknown Field Type)" + title = f"{field.type.name} (Unknown Field Type)" # TODO translate inner_container = TextWidget(title, field.type.name) container.set_inner_widget(inner_container) container.set_remove_callback( @@ -1090,10 +1096,12 @@ class PreviewPanel(QWidget): def remove_message_box(self, prompt: str, callback: Callable) -> None: remove_mb = QMessageBox() remove_mb.setText(prompt) - remove_mb.setWindowTitle("Remove Field") + remove_mb.setWindowTitle("Remove Field") # TODO translate remove_mb.setIcon(QMessageBox.Icon.Warning) - cancel_button = remove_mb.addButton("&Cancel", QMessageBox.ButtonRole.DestructiveRole) - remove_mb.addButton("&Remove", QMessageBox.ButtonRole.RejectRole) + cancel_button = remove_mb.addButton( + Translations["generic.cancel_alt"], QMessageBox.ButtonRole.DestructiveRole + ) + remove_mb.addButton("&Remove", QMessageBox.ButtonRole.RejectRole) # TODO translate # remove_mb.setStandardButtons(QMessageBox.StandardButton.Cancel) remove_mb.setDefaultButton(cancel_button) remove_mb.setEscapeButton(cancel_button) diff --git a/tagstudio/src/qt/widgets/progress.py b/tagstudio/src/qt/widgets/progress.py index f9247ebd..d9bc09a5 100644 --- a/tagstudio/src/qt/widgets/progress.py +++ b/tagstudio/src/qt/widgets/progress.py @@ -15,8 +15,9 @@ class ProgressWidget(QWidget): def __init__( self, - window_title: str, - label_text: str, + *, + window_title: str = "", + label_text: str = "", cancel_button_text: Optional[str], minimum: int, maximum: int, diff --git a/tagstudio/src/qt/widgets/tag.py b/tagstudio/src/qt/widgets/tag.py index 2d4cc7ce..46f5fb9b 100644 --- a/tagstudio/src/qt/widgets/tag.py +++ b/tagstudio/src/qt/widgets/tag.py @@ -20,6 +20,7 @@ from PySide6.QtWidgets import ( from src.core.library import Tag from src.core.library.alchemy.enums import TagColor from src.core.palette import ColorType, get_tag_color +from src.qt.translations import Translations class TagAliasWidget(QWidget): @@ -126,17 +127,20 @@ class TagWidget(QWidget): self.bg_button.setFlat(True) self.bg_button.setText(tag.name) if has_edit: - edit_action = QAction("Edit", self) + edit_action = QAction(self) + Translations.translate_qobject(edit_action, "generic.edit") edit_action.triggered.connect(on_edit_callback) edit_action.triggered.connect(self.on_edit.emit) self.bg_button.addAction(edit_action) # if on_click_callback: self.bg_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) - search_for_tag_action = QAction("Search for Tag", self) + search_for_tag_action = QAction(self) + Translations.translate_qobject(search_for_tag_action, "tag.search_for_tag") search_for_tag_action.triggered.connect(self.on_click.emit) self.bg_button.addAction(search_for_tag_action) - add_to_search_action = QAction("Add to Search", self) + add_to_search_action = QAction(self) + Translations.translate_qobject(add_to_search_action, "tag.add_to_search") self.bg_button.addAction(add_to_search_action) self.inner_layout = QHBoxLayout() diff --git a/tagstudio/src/qt/widgets/tag_box.py b/tagstudio/src/qt/widgets/tag_box.py index a26ec33a..8e095d79 100755 --- a/tagstudio/src/qt/widgets/tag_box.py +++ b/tagstudio/src/qt/widgets/tag_box.py @@ -16,6 +16,7 @@ from src.core.library.alchemy.fields import TagBoxField from src.qt.flowlayout import FlowLayout from src.qt.modals.build_tag import BuildTagPanel from src.qt.modals.tag_search import TagSearchPanel +from src.qt.translations import Translations from src.qt.widgets.fields import FieldWidget from src.qt.widgets.panel import PanelModal from src.qt.widgets.tag import TagWidget @@ -75,7 +76,8 @@ class TagBoxWidget(FieldWidget): ) tsp = TagSearchPanel(self.driver.lib) tsp.tag_chosen.connect(lambda x: self.add_tag_callback(x)) - self.add_modal = PanelModal(tsp, title, "Add Tags") + self.add_modal = PanelModal(tsp, title) + Translations.translate_with_setter(self.add_modal.setWindowTitle, "tag.add.plural") self.add_button.clicked.connect( lambda: ( tsp.update_tags(), @@ -130,11 +132,11 @@ class TagBoxWidget(FieldWidget): self.edit_modal = PanelModal( build_tag_panel, - tag.name, # TODO - display name including subtags - "Edit Tag", + title=tag.name, # TODO - display name including subtags done_callback=self.driver.preview_panel.update_widgets, has_save=True, ) + Translations.translate_with_setter(self.edit_modal.setWindowTitle, "tag.edit") # TODO - this was update_tag() self.edit_modal.saved.connect( lambda: self.driver.lib.update_tag( diff --git a/tagstudio/src/qt/widgets/video_player.py b/tagstudio/src/qt/widgets/video_player.py index 2b4434b1..7e5e6ba1 100644 --- a/tagstudio/src/qt/widgets/video_player.py +++ b/tagstudio/src/qt/widgets/video_player.py @@ -31,6 +31,7 @@ from PySide6.QtWidgets import QGraphicsScene, QGraphicsView from src.core.enums import SettingItems from src.qt.helpers.file_opener import FileOpenerHelper from src.qt.platform_strings import PlatformStrings +from src.qt.translations import Translations if typing.TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -115,7 +116,8 @@ class VideoPlayer(QGraphicsView): self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) self.opener = FileOpenerHelper(filepath=self.filepath) - autoplay_action = QAction("Autoplay", self) + autoplay_action = QAction(self) + Translations.translate_qobject(autoplay_action, "media_player.autoplay") autoplay_action.setCheckable(True) self.addAction(autoplay_action) autoplay_action.setChecked( @@ -124,7 +126,8 @@ class VideoPlayer(QGraphicsView): autoplay_action.triggered.connect(lambda: self.toggle_autoplay()) self.autoplay = autoplay_action - open_file_action = QAction("Open file", self) + open_file_action = QAction(self) + Translations.translate_qobject(open_file_action, "file.open_file") open_file_action.triggered.connect(self.opener.open_file) open_explorer_action = QAction(PlatformStrings.open_file_str, self) diff --git a/tagstudio/tests/qt/test_build_tag_panel.py b/tagstudio/tests/qt/test_build_tag_panel.py index 87971bcc..d0cdea5b 100644 --- a/tagstudio/tests/qt/test_build_tag_panel.py +++ b/tagstudio/tests/qt/test_build_tag_panel.py @@ -1,5 +1,6 @@ from src.core.library.alchemy.models import Tag from src.qt.modals.build_tag import BuildTagPanel +from src.qt.translations import Translations def test_build_tag_panel_add_sub_tag_callback(library, generate_tag): @@ -155,4 +156,4 @@ def test_build_tag_panel_build_tag(library): tag: Tag = panel.build_tag() assert tag - assert tag.name == "New Tag" + assert tag.name == Translations["tag.new"]