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:
- Your existing library save file will NOT be deleted
- Your personal files will NOT be deleted, moved, or modified
- The new v9.5+ save format can not be opened in earlier versions of TagStudio
",
+ "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:
"
- ""
- "- Your existing library save file will NOT be deleted
"
- "- Your personal files will NOT be deleted, moved, or modified
"
- "- The new v9.5+ save format can not be opened in earlier versions of TagStudio
"
- "
"
- )
+ 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"]