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",