From aae65a827e507fb2f0a43f55eb34776e2471b3a8 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sun, 16 Feb 2025 20:21:46 -0800 Subject: [PATCH 1/4] feat(ui): add language setting --- tagstudio/resources/translations/en.json | 4 ++ tagstudio/src/core/enums.py | 1 + tagstudio/src/qt/modals/settings_panel.py | 71 +++++++++++++++++++++++ tagstudio/src/qt/translations.py | 18 +++--- tagstudio/src/qt/ts_qt.py | 53 ++++++++++++----- tagstudio/src/qt/widgets/preview_panel.py | 4 +- 6 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 tagstudio/src/qt/modals/settings_panel.py diff --git a/tagstudio/resources/translations/en.json b/tagstudio/resources/translations/en.json index 7608d3cae..caaf8499a 100644 --- a/tagstudio/resources/translations/en.json +++ b/tagstudio/resources/translations/en.json @@ -180,6 +180,7 @@ "menu.macros.folders_to_tags": "Folders to Tags", "menu.macros": "&Macros", "menu.select": "Select", + "menu.settings": "Settings...", "menu.tools.fix_duplicate_files": "Fix Duplicate &Files", "menu.tools.fix_unlinked_entries": "Fix &Unlinked Entries", "menu.tools": "&Tools", @@ -192,9 +193,12 @@ "edit.copy_fields": "Copy Fields", "edit.paste_fields": "Paste Fields", "settings.clear_thumb_cache.title": "Clear Thumbnail Cache", + "settings.language": "Language", "settings.open_library_on_start": "Open Library on Start", + "settings.restart_required": "Please restart TagStudio for changes to take effect.", "settings.show_filenames_in_grid": "Show Filenames in Grid", "settings.show_recent_libraries": "Show Recent Libraries", + "settings.title": "Settings", "sorting.direction.ascending": "Ascending", "sorting.direction.descending": "Descending", "splash.opening_library": "Opening Library \"{library_path}\"...", diff --git a/tagstudio/src/core/enums.py b/tagstudio/src/core/enums.py index 6cdedbcfb..c3e720b28 100644 --- a/tagstudio/src/core/enums.py +++ b/tagstudio/src/core/enums.py @@ -17,6 +17,7 @@ class SettingItems(str, enum.Enum): SHOW_FILENAMES = "show_filenames" AUTOPLAY = "autoplay_videos" THUMB_CACHE_SIZE_LIMIT = "thumb_cache_size_limit" + LANGUAGE = "language" class Theme(str, enum.Enum): diff --git a/tagstudio/src/qt/modals/settings_panel.py b/tagstudio/src/qt/modals/settings_panel.py new file mode 100644 index 000000000..fff54318a --- /dev/null +++ b/tagstudio/src/qt/modals/settings_panel.py @@ -0,0 +1,71 @@ +# Copyright (C) 2025 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QComboBox, QFormLayout, QLabel, QVBoxLayout, QWidget +from src.core.enums import SettingItems +from src.qt.translations import Translations +from src.qt.widgets.panel import PanelWidget + + +class SettingsPanel(PanelWidget): + def __init__(self, driver): + super().__init__() + self.driver = driver + self.setMinimumSize(320, 200) + self.root_layout = QVBoxLayout(self) + self.root_layout.setContentsMargins(6, 0, 6, 0) + + self.form_container = QWidget() + self.form_layout = QFormLayout(self.form_container) + self.form_layout.setContentsMargins(0, 0, 0, 0) + + self.restart_label = QLabel() + self.restart_label.setHidden(True) + Translations.translate_qobject(self.restart_label, "settings.restart_required") + self.restart_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + language_label = QLabel() + Translations.translate_qobject(language_label, "settings.language") + languages = [ + # "cs", # Minimal + # "da", # Minimal + "de", + "en", + "es", + "fil", + "fr", + "hu", + # "it", # Minimal + "nb_NO", + "pl", + "pt_BR", + # "pt", # Empty + "ru", + "sv", + "ta", + "tok", + "tr", + # "yue_Hant", # Empty + "zh_Hant", + ] + self.language_combobox = QComboBox() + self.language_combobox.addItems(languages) + current_lang: str = str( + driver.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str) + ) + current_lang = "en" if current_lang not in languages else current_lang + self.language_combobox.setCurrentIndex(languages.index(current_lang)) + self.language_combobox.currentIndexChanged.connect( + lambda: self.restart_label.setHidden(False) + ) + self.form_layout.addRow(language_label, self.language_combobox) + + self.root_layout.addWidget(self.form_container) + self.root_layout.addStretch(1) + self.root_layout.addWidget(self.restart_label) + + def get_language(self) -> str: + return self.language_combobox.currentText() diff --git a/tagstudio/src/qt/translations.py b/tagstudio/src/qt/translations.py index 5d2dcea0a..fae788aca 100644 --- a/tagstudio/src/qt/translations.py +++ b/tagstudio/src/qt/translations.py @@ -70,16 +70,20 @@ def translate_with_setter(self, setter: Callable[[str], None], key: str, **kwarg 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))) + # TODO: Fix so deleted Qt objects aren't referenced any longer + # 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 + except (KeyError, ValueError): + logger.error( + "[Translations] Error while formatting translation.", + text=text, + kwargs=kwargs, + language=self._lang, ) return text @@ -87,9 +91,7 @@ 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" + return self._strings[key].value if key in self._strings else f"[{key}]" Translations = Translator() -# Translations.change_language("de") diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 166b02292..a212d8086 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -87,6 +87,7 @@ from src.qt.modals.fix_dupes import FixDupeFilesModal from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal from src.qt.modals.folders_to_tags import FoldersToTagsModal +from src.qt.modals.settings_panel import SettingsPanel from src.qt.modals.tag_database import TagDatabasePanel from src.qt.modals.tag_search import TagSearchPanel from src.qt.platform_strings import trash_term @@ -195,6 +196,10 @@ def __init__(self, backend, args): ) self.config_path = self.settings.fileName() + Translations.change_language( + str(self.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str)) + ) + # NOTE: This should be a per-library setting rather than an application setting. thumb_cache_size_limit: int = int( str( @@ -361,19 +366,6 @@ def start(self) -> None: file_menu.addMenu(self.open_recent_library_menu) self.update_recent_lib_menu() - 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)) - ) - open_on_start_action.triggered.connect( - lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked) - ) - file_menu.addAction(open_on_start_action) - - file_menu.addSeparator() - self.save_library_backup_action = QAction(menu_bar) Translations.translate_qobject(self.save_library_backup_action, "menu.file.save_backup") self.save_library_backup_action.triggered.connect( @@ -392,6 +384,23 @@ def start(self) -> None: self.save_library_backup_action.setEnabled(False) file_menu.addAction(self.save_library_backup_action) + file_menu.addSeparator() + settings_action = QAction(self) + Translations.translate_qobject(settings_action, "menu.settings") + settings_action.triggered.connect(self.open_settings_modal) + file_menu.addAction(settings_action) + + 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)) + ) + open_on_start_action.triggered.connect( + lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked) + ) + file_menu.addAction(open_on_start_action) + file_menu.addSeparator() self.refresh_dir_action = QAction(menu_bar) @@ -1816,6 +1825,24 @@ def clear_recent_libs(self): self.settings.sync() self.update_recent_lib_menu() + def open_settings_modal(self): + # TODO: Implement a proper settings panel, and don't re-create it each time it's opened. + settings_panel = SettingsPanel(self) + modal = PanelModal( + widget=settings_panel, + done_callback=lambda: self.update_language_settings(settings_panel.get_language()), + has_save=False, + ) + Translations.translate_with_setter(modal.setTitle, "settings.title") + Translations.translate_with_setter(modal.setWindowTitle, "settings.title") + modal.show() + + def update_language_settings(self, language: str): + Translations.change_language(language) + + self.settings.setValue(SettingItems.LANGUAGE, language) + self.settings.sync() + def open_library(self, path: Path) -> None: """Open a TagStudio library.""" translation_params = {"key": "splash.opening_library", "library_path": str(path)} diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index ab1791a95..9df1222d9 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -105,14 +105,14 @@ def __init__(self, library: Library, driver: "QtDriver"): self.add_tag_button.setCursor(Qt.CursorShape.PointingHandCursor) self.add_tag_button.setMinimumHeight(28) self.add_tag_button.setStyleSheet(PreviewPanel.button_style) - self.add_tag_button.setText("Add Tag") # TODO: Translate + Translations.translate_qobject(self.add_tag_button, "tag.add") self.add_field_button = QPushButton() self.add_field_button.setEnabled(False) self.add_field_button.setCursor(Qt.CursorShape.PointingHandCursor) self.add_field_button.setMinimumHeight(28) self.add_field_button.setStyleSheet(PreviewPanel.button_style) - self.add_field_button.setText("Add Field") # TODO: Translate + Translations.translate_qobject(self.add_field_button, "library.field.add") add_buttons_layout.addWidget(self.add_tag_button) add_buttons_layout.addWidget(self.add_field_button) From 7efd5719b5c69d323fcb642c9e6d55709b33e887 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sun, 16 Feb 2025 20:36:26 -0800 Subject: [PATCH 2/4] translations: implement remaining todos --- tagstudio/resources/translations/en.json | 1 + .../src/qt/widgets/preview/file_attributes.py | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/tagstudio/resources/translations/en.json b/tagstudio/resources/translations/en.json index caaf8499a..b97afa10f 100644 --- a/tagstudio/resources/translations/en.json +++ b/tagstudio/resources/translations/en.json @@ -186,6 +186,7 @@ "menu.tools": "&Tools", "menu.view": "&View", "menu.window": "Window", + "preview.multiple_selection": "{count} Items Selected", "preview.no_selection": "No Items Selected", "select.add_tag_to_selected": "Add Tag to Selected", "select.all": "Select All", diff --git a/tagstudio/src/qt/widgets/preview/file_attributes.py b/tagstudio/src/qt/widgets/preview/file_attributes.py index fa1a6c7e5..50f3205fd 100644 --- a/tagstudio/src/qt/widgets/preview/file_attributes.py +++ b/tagstudio/src/qt/widgets/preview/file_attributes.py @@ -23,6 +23,7 @@ from src.core.library.alchemy.library import Library from src.core.media_types import MediaCategories from src.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel +from src.qt.translations import Translations if typing.TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -108,16 +109,22 @@ 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")}" # TODO: Translate + f"{Translations["file.date_created"]}: " + f"{dt.strftime(created, "%a, %x, %X")}" ) self.date_modified_label.setText( - f"Date Modified: {dt.strftime(modified, "%a, %x, %X")}" # TODO: Translate + f"{Translations["file.date_modified"]}: " + f"{dt.strftime(modified, "%a, %x, %X")}" ) self.date_created_label.setHidden(False) self.date_modified_label.setHidden(False) elif filepath: - 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.setText( + f"{Translations["file.date_created"]}: N/A" + ) + self.date_modified_label.setText( + f"{Translations["file.date_modified"]}: N/A" + ) self.date_created_label.setHidden(False) self.date_modified_label.setHidden(False) else: @@ -132,7 +139,7 @@ def update_stats(self, filepath: Path | None = None, ext: str = ".", stats: dict if not filepath: self.layout().setSpacing(0) self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.file_label.setText("No Items Selected") # TODO: Translate + self.file_label.setText(f"{Translations["preview.no_selection"]}") self.file_label.set_file_path("") self.file_label.setCursor(Qt.CursorShape.ArrowCursor) self.dimensions_label.setText("") @@ -221,7 +228,7 @@ def update_multi_selection(self, count: int): """Format attributes for multiple selected items.""" self.layout().setSpacing(0) self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.file_label.setText(f"{count} Items Selected") # TODO: Translate + Translations.translate_qobject(self.file_label, "preview.multiple_selection", count=count) self.file_label.setCursor(Qt.CursorShape.ArrowCursor) self.file_label.set_file_path("") self.dimensions_label.setText("") From d46196ff095d45d859838d340be84c6faf39073d Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 17 Feb 2025 00:48:54 -0800 Subject: [PATCH 3/4] ui: show language names in settings instead of codes --- tagstudio/src/qt/modals/settings_panel.py | 53 ++++++++++++----------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/tagstudio/src/qt/modals/settings_panel.py b/tagstudio/src/qt/modals/settings_panel.py index fff54318a..05c5dc6a7 100644 --- a/tagstudio/src/qt/modals/settings_panel.py +++ b/tagstudio/src/qt/modals/settings_panel.py @@ -29,35 +29,35 @@ def __init__(self, driver): language_label = QLabel() Translations.translate_qobject(language_label, "settings.language") - languages = [ - # "cs", # Minimal - # "da", # Minimal - "de", - "en", - "es", - "fil", - "fr", - "hu", - # "it", # Minimal - "nb_NO", - "pl", - "pt_BR", - # "pt", # Empty - "ru", - "sv", - "ta", - "tok", - "tr", - # "yue_Hant", # Empty - "zh_Hant", - ] + self.languages = { + # "Cantonese (Traditional)": "yue_Hant", # Empty + "Chinese (Traditional)": "zh_Hant", + # "Czech": "cs", # Minimal + # "Danish": "da", # Minimal + "English": "en", + "Filipino": "fil", + "French": "fr", + "German": "de", + "Hungarian": "hu", + # "Italian": "it", # Minimal + "Norwegian Bokmål": "nb_NO", + "Polish": "pl", + "Portuguese (Brazil)": "pt_BR", + # "Portuguese (Portugal)": "pt", # Empty + "Russian": "ru", + "Spanish": "es", + "Swedish": "sv", + "Tamil": "ta", + "Toki Pona": "tok", + "Turkish": "tr", + } self.language_combobox = QComboBox() - self.language_combobox.addItems(languages) + self.language_combobox.addItems(list(self.languages.keys())) current_lang: str = str( driver.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str) ) - current_lang = "en" if current_lang not in languages else current_lang - self.language_combobox.setCurrentIndex(languages.index(current_lang)) + current_lang = "en" if current_lang not in self.languages.values() else current_lang + self.language_combobox.setCurrentIndex(list(self.languages.values()).index(current_lang)) self.language_combobox.currentIndexChanged.connect( lambda: self.restart_label.setHidden(False) ) @@ -68,4 +68,5 @@ def __init__(self, driver): self.root_layout.addWidget(self.restart_label) def get_language(self) -> str: - return self.language_combobox.currentText() + values: list[str] = list(self.languages.values()) + return values[self.language_combobox.currentIndex()] From 575e041be65ed372fa13f6b93e72d7ca94a12d87 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 17 Feb 2025 02:16:47 -0800 Subject: [PATCH 4/4] translations: add Dutch setting, anticipating #798 --- tagstudio/src/qt/modals/settings_panel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tagstudio/src/qt/modals/settings_panel.py b/tagstudio/src/qt/modals/settings_panel.py index 05c5dc6a7..a499f6ddb 100644 --- a/tagstudio/src/qt/modals/settings_panel.py +++ b/tagstudio/src/qt/modals/settings_panel.py @@ -34,6 +34,7 @@ def __init__(self, driver): "Chinese (Traditional)": "zh_Hant", # "Czech": "cs", # Minimal # "Danish": "da", # Minimal + "Dutch": "nl", "English": "en", "Filipino": "fil", "French": "fr",