diff --git a/tagstudio/resources/translations/en.json b/tagstudio/resources/translations/en.json index 32cae5016..e9247b71e 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 3de0118d8..d1125edeb 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -25,6 +25,8 @@ 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 @@ def setupUi(self, MainWindow): # 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 @@ def setupUi(self, MainWindow): 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 @@ def setupUi(self, MainWindow): 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 @@ def setupUi(self, MainWindow): 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 @@ def setupUi(self, MainWindow): 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 @@ def setupUi(self, MainWindow): 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 fd0e0c997..e948e5116 100644 --- a/tagstudio/src/qt/modals/add_field.py +++ b/tagstudio/src/qt/modals/add_field.py @@ -14,6 +14,7 @@ QWidget, ) from src.core.library import Library +from src.qt.translations import Translations class AddFieldModal(QWidget): @@ -26,7 +27,7 @@ def __init__(self, library: Library): 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 @@ def __init__(self, library: Library): # '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 @@ def __init__(self, library: Library): # 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 4a96d177d..5682c7360 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.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 @@ def __init__(self, library: Library, tag: Tag | None = None): 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 @@ def __init__(self, library: Library, tag: Tag | None = None): 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 @@ def __init__(self, library: Library, tag: Tag | None = None): 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 @@ def __init__(self, library: Library, tag: Tag | None = None): 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 @@ def __init__(self, library: Library, tag: Tag | None = None): 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 @@ def __init__(self, library: Library, tag: Tag | None = None): 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 @@ def __init__(self, library: Library, tag: Tag | None = None): 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 b382d0831..da229c994 100644 --- a/tagstudio/src/qt/modals/delete_unlinked.py +++ b/tagstudio/src/qt/modals/delete_unlinked.py @@ -15,6 +15,7 @@ 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 @@ def __init__(self, driver: "QtDriver", tracker: MissingRegistry): 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 @@ def __init__(self, driver: "QtDriver", tracker: MissingRegistry): 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 @@ def __init__(self, driver: "QtDriver", tracker: MissingRegistry): 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 @@ def __init__(self, driver: "QtDriver", tracker: MissingRegistry): 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 @@ def refresh_list(self): 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 ec94a3e50..9b13b3d11 100644 --- a/tagstudio/src/qt/modals/drop_import.py +++ b/tagstudio/src/qt/modals/drop_import.py @@ -17,6 +17,7 @@ QVBoxLayout, QWidget, ) +from src.qt.translations import Translations from src.qt.widgets.progress import ProgressWidget if TYPE_CHECKING: @@ -41,7 +42,7 @@ def __init__(self, driver: "QtDriver"): 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 @@ def __init__(self, driver: "QtDriver"): 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 @@ def __init__(self, driver: "QtDriver"): 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 @@ def collect_files_to_import(self, urls: list[QUrl]): 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 @@ def begin_transfer(self, choice: DuplicateChoice | None = None): 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 224638ca0..49b5b1478 100644 --- a/tagstudio/src/qt/modals/file_extension.py +++ b/tagstudio/src/qt/modals/file_extension.py @@ -18,6 +18,7 @@ ) 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 @@ def __init__(self, library: "Library"): 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 @@ def __init__(self, library: "Library"): # 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 @@ def __init__(self, library: "Library"): 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 fccdb5bcb..762610399 100644 --- a/tagstudio/src/qt/modals/fix_dupes.py +++ b/tagstudio/src/qt/modals/fix_dupes.py @@ -17,6 +17,7 @@ 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def set_filename(self, filename: str): 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 @@ def refresh_dupes(self): 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 7c580d3f7..eafe3297a 100644 --- a/tagstudio/src/qt/modals/fix_unlinked.py +++ b/tagstudio/src/qt/modals/fix_unlinked.py @@ -12,6 +12,7 @@ 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def set_missing_count(self, count: int | None = None): 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 f9962a0e5..f680fdc18 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.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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 66c94dda7..35bdf7c15 100644 --- a/tagstudio/src/qt/modals/merge_dupe_entries.py +++ b/tagstudio/src/qt/modals/merge_dupe_entries.py @@ -7,6 +7,7 @@ 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 6ab199570..4cb37a1cf 100644 --- a/tagstudio/src/qt/modals/mirror_entities.py +++ b/tagstudio/src/qt/modals/mirror_entities.py @@ -17,6 +17,7 @@ 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 @@ def __init__(self, driver: "QtDriver", tracker: DupeRegistry): 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 @@ def __init__(self, driver: "QtDriver", tracker: DupeRegistry): 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 @@ def __init__(self, driver: "QtDriver", tracker: DupeRegistry): 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 @@ def refresh_list(self): 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 ab2a1d408..0363c8486 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 @@ def __init__(self, tracker: MissingRegistry): 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 bc5cfce98..560611d51 100644 --- a/tagstudio/src/qt/modals/tag_database.py +++ b/tagstudio/src/qt/modals/tag_database.py @@ -18,6 +18,7 @@ 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 @@ def __init__(self, library: Library): 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 @@ def __init__(self, library: Library): 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 @@ def __init__(self, library: Library): 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 @@ def remove_tag(self, tag: Tag): 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 @@ def edit_tag(self, tag: Tag): 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 72f3650b9..90932b0de 100644 --- a/tagstudio/src/qt/modals/tag_search.py +++ b/tagstudio/src/qt/modals/tag_search.py @@ -19,6 +19,7 @@ ) 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 @@ def __init__(self, library: Library, exclude: list[int] | None = None): 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 9eda3ef8b..23851dd48 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 000000000..5d2dcea0a --- /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 2c2388075..06c136dc4 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -84,6 +84,7 @@ 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 @@ def init_workers(self): 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 @@ def start(self) -> None: self.main_window.setMenuBar(menu_bar) menu_bar.setNativeMenuBar(True) - file_menu = QMenu("&File", menu_bar) - edit_menu = QMenu("&Edit", menu_bar) - 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 @@ def start(self) -> None: 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 @@ def start(self) -> None: 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 @@ def start(self) -> None: 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 @@ def start(self) -> None: 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 @@ def start(self) -> None: 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 @@ def start(self) -> None: 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 @@ def start(self) -> None: 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 @@ def start(self) -> None: ) 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 @@ def create_fix_unlinked_entries_modal(): self.unlinked_modal = FixUnlinkedEntriesModal(self.lib, self) self.unlinked_modal.show() - fix_unlinked_entries_action = QAction("Fix &Unlinked Entries", menu_bar) + fix_unlinked_entries_action = QAction(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 @@ def create_dupe_files_modal(): self.dupe_modal = FixDupeFilesModal(self.lib, self) self.dupe_modal.show() - fix_dupe_files_action = QAction("Fix Duplicate &Files", menu_bar) + fix_dupe_files_action = QAction(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 @@ def create_folders_tags_modal(): 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 @@ def create_folders_tags_modal(): 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 @@ def create_folders_tags_modal(): # 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 @@ def show_error_message(self, message: str): 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 @@ def close_library(self, is_shutdown: bool = False): return logger.info("Closing Library...") - self.main_window.statusbar.showMessage("Closing Library...") + self.main_window.statusbar.showMessage(Translations["status.library_closing"]) start_time = time.time() self.settings.setValue(SettingItems.LAST_LIBRARY, str(self.lib.library_dir)) @@ -610,26 +640,32 @@ def close_library(self, is_shutdown: bool = False): 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 @@ def clear_select_action_callback(self): 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 @@ def add_new_files_callback(self): 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 @@ def add_new_files_callback(self): lambda x: ( pw.update_progress(x + 1), pw.update_label( - f"Scanning Directories for New Files...\n{x + 1}" - 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 @@ def add_new_files_runnable(self, tracker: RefreshDirTracker): 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 @@ def filter_items(self, filter: FilterState | None = None) -> None: 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 @@ def filter_items(self, filter: FilterState | None = None) -> None: # 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 @@ def update_libs_list(self, path: Path | str): 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 @@ def open_library(self, path: Path) -> None: 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 @@ def init_library(self, path: Path, open_status: LibraryStatus): 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 9935fe4e7..f79708ad4 100644 --- a/tagstudio/src/qt/widgets/item_thumb.py +++ b/tagstudio/src/qt/widgets/item_thumb.py @@ -30,6 +30,7 @@ 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 @@ def __init__( 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 @@ def __init__( 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 c1a5a7d8c..3f6b15942 100644 --- a/tagstudio/src/qt/widgets/landing.py +++ b/tagstudio/src/qt/widgets/landing.py @@ -13,6 +13,7 @@ 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 @@ def __init__(self, driver: "QtDriver", pixel_ratio: float): 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 a2978f562..6e8aeb06b 100644 --- a/tagstudio/src/qt/widgets/migration_modal.py +++ b/tagstudio/src/qt/widgets/migration_modal.py @@ -30,6 +30,7 @@ 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 @@ def __init__(self, path: Path): 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 @@ def __init__(self, path: Path): 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 @@ def init_page_convert(self) -> None: body_container_layout.setContentsMargins(0, 0, 0, 0) tab: str = " " - self.match_text: str = "Matched" - self.differ_text: str = "Discrepancy" - - 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:" + self.match_text: str = Translations["json_migration.heading.match"] + self.differ_text: str = Translations["json_migration.heading.differ"] + + 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 @@ def init_page_convert(self) -> None: 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 @@ def init_page_convert(self) -> None: 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 @@ def init_page_convert(self) -> None: 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 @@ def migration_progress(self, skip_ui: bool = False): 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 @@ def migration_iterator(self): """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 @@ def migration_iterator(self): 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 @@ def migration_iterator(self): 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 @@ def update_sql_value_ui(self, show_msg_box: bool = True): 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 b164acc93..573f6f48d 100755 --- a/tagstudio/src/qt/widgets/panel.py +++ b/tagstudio/src/qt/widgets/panel.py @@ -6,6 +6,7 @@ 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def closeEvent(self, event): # noqa: N802 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 7e8e0c8b3..0bb1c1812 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.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 @@ def __init__(self, library: Library, driver: "QtDriver"): ) 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 @@ def __init__(self, library: Library, driver: "QtDriver"): 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 @@ def update_selected_entry(self, driver: "QtDriver"): 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 @@ def clear_layout(layout_item: QVBoxLayout): # 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 @@ def set_button_style( 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 @@ def update_date_label(self, filepath: Path | None = None) -> None: 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 @@ def update_widgets(self) -> bool: 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 @@ def update_widgets(self) -> bool: 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 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): 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 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): ) ) 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 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): 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 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): 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 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): 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 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): 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 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): ) ) 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 @@ def update_field(self, field: BaseField, content: str) -> None: 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 f9247ebd7..d9bc09a5f 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 2d4cc7ce1..46f5fb9bb 100644 --- a/tagstudio/src/qt/widgets/tag.py +++ b/tagstudio/src/qt/widgets/tag.py @@ -20,6 +20,7 @@ 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 @@ def __init__( 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 a26ec33af..8e095d79a 100755 --- a/tagstudio/src/qt/widgets/tag_box.py +++ b/tagstudio/src/qt/widgets/tag_box.py @@ -16,6 +16,7 @@ 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 @@ def __init__( ) 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 @@ def edit_tag(self, tag: Tag): 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 2b4434b1d..7e5e6ba15 100644 --- a/tagstudio/src/qt/widgets/video_player.py +++ b/tagstudio/src/qt/widgets/video_player.py @@ -31,6 +31,7 @@ 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 @@ def __init__(self, driver: "QtDriver") -> None: 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 @@ def __init__(self, driver: "QtDriver") -> None: 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 87971bcc8..d0cdea5b0 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"]