Skip to content

Commit 61b9fcf

Browse files
authored
feat(ui): add language setting (#803)
* feat(ui): add language setting * translations: implement remaining todos * ui: show language names in settings instead of codes * translations: add Dutch setting, anticipating #798
1 parent 28de21a commit 61b9fcf

File tree

7 files changed

+144
-29
lines changed

7 files changed

+144
-29
lines changed

tagstudio/resources/translations/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@
199199
"menu.macros.folders_to_tags": "Folders to Tags",
200200
"menu.macros": "&Macros",
201201
"menu.select": "Select",
202+
"menu.settings": "Settings...",
202203
"menu.tools.fix_duplicate_files": "Fix Duplicate &Files",
203204
"menu.tools.fix_unlinked_entries": "Fix &Unlinked Entries",
204205
"menu.tools": "&Tools",
@@ -209,16 +210,20 @@
209210
"namespace.create.title": "Create Namespace",
210211
"namespace.new.button": "New Namespace",
211212
"namespace.new.prompt": "Create a New Namespace to Start Adding Custom Colors!",
213+
"preview.multiple_selection": "<b>{count}</b> Items Selected",
212214
"preview.no_selection": "No Items Selected",
213215
"select.add_tag_to_selected": "Add Tag to Selected",
214216
"select.all": "Select All",
215217
"select.clear": "Clear Selection",
216218
"edit.copy_fields": "Copy Fields",
217219
"edit.paste_fields": "Paste Fields",
218220
"settings.clear_thumb_cache.title": "Clear Thumbnail Cache",
221+
"settings.language": "Language",
219222
"settings.open_library_on_start": "Open Library on Start",
223+
"settings.restart_required": "Please restart TagStudio for changes to take effect.",
220224
"settings.show_filenames_in_grid": "Show Filenames in Grid",
221225
"settings.show_recent_libraries": "Show Recent Libraries",
226+
"settings.title": "Settings",
222227
"sorting.direction.ascending": "Ascending",
223228
"sorting.direction.descending": "Descending",
224229
"splash.opening_library": "Opening Library \"{library_path}\"...",

tagstudio/src/core/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class SettingItems(str, enum.Enum):
1717
SHOW_FILENAMES = "show_filenames"
1818
AUTOPLAY = "autoplay_videos"
1919
THUMB_CACHE_SIZE_LIMIT = "thumb_cache_size_limit"
20+
LANGUAGE = "language"
2021

2122

2223
class Theme(str, enum.Enum):
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
2+
# Licensed under the GPL-3.0 License.
3+
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
4+
5+
6+
from PySide6.QtCore import Qt
7+
from PySide6.QtWidgets import QComboBox, QFormLayout, QLabel, QVBoxLayout, QWidget
8+
from src.core.enums import SettingItems
9+
from src.qt.translations import Translations
10+
from src.qt.widgets.panel import PanelWidget
11+
12+
13+
class SettingsPanel(PanelWidget):
14+
def __init__(self, driver):
15+
super().__init__()
16+
self.driver = driver
17+
self.setMinimumSize(320, 200)
18+
self.root_layout = QVBoxLayout(self)
19+
self.root_layout.setContentsMargins(6, 0, 6, 0)
20+
21+
self.form_container = QWidget()
22+
self.form_layout = QFormLayout(self.form_container)
23+
self.form_layout.setContentsMargins(0, 0, 0, 0)
24+
25+
self.restart_label = QLabel()
26+
self.restart_label.setHidden(True)
27+
Translations.translate_qobject(self.restart_label, "settings.restart_required")
28+
self.restart_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
29+
30+
language_label = QLabel()
31+
Translations.translate_qobject(language_label, "settings.language")
32+
self.languages = {
33+
# "Cantonese (Traditional)": "yue_Hant", # Empty
34+
"Chinese (Traditional)": "zh_Hant",
35+
# "Czech": "cs", # Minimal
36+
# "Danish": "da", # Minimal
37+
"Dutch": "nl",
38+
"English": "en",
39+
"Filipino": "fil",
40+
"French": "fr",
41+
"German": "de",
42+
"Hungarian": "hu",
43+
# "Italian": "it", # Minimal
44+
"Norwegian Bokmål": "nb_NO",
45+
"Polish": "pl",
46+
"Portuguese (Brazil)": "pt_BR",
47+
# "Portuguese (Portugal)": "pt", # Empty
48+
"Russian": "ru",
49+
"Spanish": "es",
50+
"Swedish": "sv",
51+
"Tamil": "ta",
52+
"Toki Pona": "tok",
53+
"Turkish": "tr",
54+
}
55+
self.language_combobox = QComboBox()
56+
self.language_combobox.addItems(list(self.languages.keys()))
57+
current_lang: str = str(
58+
driver.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str)
59+
)
60+
current_lang = "en" if current_lang not in self.languages.values() else current_lang
61+
self.language_combobox.setCurrentIndex(list(self.languages.values()).index(current_lang))
62+
self.language_combobox.currentIndexChanged.connect(
63+
lambda: self.restart_label.setHidden(False)
64+
)
65+
self.form_layout.addRow(language_label, self.language_combobox)
66+
67+
self.root_layout.addWidget(self.form_container)
68+
self.root_layout.addStretch(1)
69+
self.root_layout.addWidget(self.restart_label)
70+
71+
def get_language(self) -> str:
72+
values: list[str] = list(self.languages.values())
73+
return values[self.language_combobox.currentIndex()]

tagstudio/src/qt/translations.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,26 +70,28 @@ def translate_with_setter(self, setter: Callable[[str], None], key: str, **kwarg
7070
7171
Also formats the translation with the given keyword arguments.
7272
"""
73-
if key in self._strings:
74-
self._strings[key].changed.connect(lambda text: setter(self.__format(text, **kwargs)))
73+
# TODO: Fix so deleted Qt objects aren't referenced any longer
74+
# if key in self._strings:
75+
# self._strings[key].changed.connect(lambda text: setter(self.__format(text, **kwargs)))
7576
setter(self.translate_formatted(key, **kwargs))
7677

7778
def __format(self, text: str, **kwargs) -> str:
7879
try:
7980
return text.format(**kwargs)
80-
except KeyError:
81-
logger.warning(
82-
"Error while formatting translation.", text=text, kwargs=kwargs, language=self._lang
81+
except (KeyError, ValueError):
82+
logger.error(
83+
"[Translations] Error while formatting translation.",
84+
text=text,
85+
kwargs=kwargs,
86+
language=self._lang,
8387
)
8488
return text
8589

8690
def translate_formatted(self, key: str, **kwargs) -> str:
8791
return self.__format(self[key], **kwargs)
8892

8993
def __getitem__(self, key: str) -> str:
90-
# return "???"
91-
return self._strings[key].value if key in self._strings else "Not Translated"
94+
return self._strings[key].value if key in self._strings else f"[{key}]"
9295

9396

9497
Translations = Translator()
95-
# Translations.change_language("de")

tagstudio/src/qt/ts_qt.py

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
from src.qt.modals.fix_dupes import FixDupeFilesModal
8888
from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal
8989
from src.qt.modals.folders_to_tags import FoldersToTagsModal
90+
from src.qt.modals.settings_panel import SettingsPanel
9091
from src.qt.modals.tag_color_manager import TagColorManager
9192
from src.qt.modals.tag_database import TagDatabasePanel
9293
from src.qt.modals.tag_search import TagSearchPanel
@@ -197,6 +198,10 @@ def __init__(self, backend, args):
197198
)
198199
self.config_path = self.settings.fileName()
199200

201+
Translations.change_language(
202+
str(self.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str))
203+
)
204+
200205
# NOTE: This should be a per-library setting rather than an application setting.
201206
thumb_cache_size_limit: int = int(
202207
str(
@@ -366,19 +371,6 @@ def start(self) -> None:
366371
file_menu.addMenu(self.open_recent_library_menu)
367372
self.update_recent_lib_menu()
368373

369-
open_on_start_action = QAction(self)
370-
Translations.translate_qobject(open_on_start_action, "settings.open_library_on_start")
371-
open_on_start_action.setCheckable(True)
372-
open_on_start_action.setChecked(
373-
bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool))
374-
)
375-
open_on_start_action.triggered.connect(
376-
lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked)
377-
)
378-
file_menu.addAction(open_on_start_action)
379-
380-
file_menu.addSeparator()
381-
382374
self.save_library_backup_action = QAction(menu_bar)
383375
Translations.translate_qobject(self.save_library_backup_action, "menu.file.save_backup")
384376
self.save_library_backup_action.triggered.connect(
@@ -397,6 +389,23 @@ def start(self) -> None:
397389
self.save_library_backup_action.setEnabled(False)
398390
file_menu.addAction(self.save_library_backup_action)
399391

392+
file_menu.addSeparator()
393+
settings_action = QAction(self)
394+
Translations.translate_qobject(settings_action, "menu.settings")
395+
settings_action.triggered.connect(self.open_settings_modal)
396+
file_menu.addAction(settings_action)
397+
398+
open_on_start_action = QAction(self)
399+
Translations.translate_qobject(open_on_start_action, "settings.open_library_on_start")
400+
open_on_start_action.setCheckable(True)
401+
open_on_start_action.setChecked(
402+
bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool))
403+
)
404+
open_on_start_action.triggered.connect(
405+
lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked)
406+
)
407+
file_menu.addAction(open_on_start_action)
408+
400409
file_menu.addSeparator()
401410

402411
self.refresh_dir_action = QAction(menu_bar)
@@ -1830,6 +1839,24 @@ def clear_recent_libs(self):
18301839
self.settings.sync()
18311840
self.update_recent_lib_menu()
18321841

1842+
def open_settings_modal(self):
1843+
# TODO: Implement a proper settings panel, and don't re-create it each time it's opened.
1844+
settings_panel = SettingsPanel(self)
1845+
modal = PanelModal(
1846+
widget=settings_panel,
1847+
done_callback=lambda: self.update_language_settings(settings_panel.get_language()),
1848+
has_save=False,
1849+
)
1850+
Translations.translate_with_setter(modal.setTitle, "settings.title")
1851+
Translations.translate_with_setter(modal.setWindowTitle, "settings.title")
1852+
modal.show()
1853+
1854+
def update_language_settings(self, language: str):
1855+
Translations.change_language(language)
1856+
1857+
self.settings.setValue(SettingItems.LANGUAGE, language)
1858+
self.settings.sync()
1859+
18331860
def open_library(self, path: Path) -> None:
18341861
"""Open a TagStudio library."""
18351862
translation_params = {"key": "splash.opening_library", "library_path": str(path)}

tagstudio/src/qt/widgets/preview/file_attributes.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from src.core.library.alchemy.library import Library
2424
from src.core.media_types import MediaCategories
2525
from src.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel
26+
from src.qt.translations import Translations
2627

2728
if typing.TYPE_CHECKING:
2829
from src.qt.ts_qt import QtDriver
@@ -108,16 +109,22 @@ def update_date_label(self, filepath: Path | None = None) -> None:
108109
created = dt.fromtimestamp(filepath.stat().st_ctime)
109110
modified: dt = dt.fromtimestamp(filepath.stat().st_mtime)
110111
self.date_created_label.setText(
111-
f"<b>Date Created:</b> {dt.strftime(created, "%a, %x, %X")}" # TODO: Translate
112+
f"<b>{Translations["file.date_created"]}:</b> "
113+
f"{dt.strftime(created, "%a, %x, %X")}"
112114
)
113115
self.date_modified_label.setText(
114-
f"<b>Date Modified:</b> {dt.strftime(modified, "%a, %x, %X")}" # TODO: Translate
116+
f"<b>{Translations["file.date_modified"]}:</b> "
117+
f"{dt.strftime(modified, "%a, %x, %X")}"
115118
)
116119
self.date_created_label.setHidden(False)
117120
self.date_modified_label.setHidden(False)
118121
elif filepath:
119-
self.date_created_label.setText("<b>Date Created:</b> <i>N/A</i>") # TODO: Translate
120-
self.date_modified_label.setText("<b>Date Modified:</b> <i>N/A</i>") # TODO: Translate
122+
self.date_created_label.setText(
123+
f"<b>{Translations["file.date_created"]}:</b> <i>N/A</i>"
124+
)
125+
self.date_modified_label.setText(
126+
f"<b>{Translations["file.date_modified"]}:</b> <i>N/A</i>"
127+
)
121128
self.date_created_label.setHidden(False)
122129
self.date_modified_label.setHidden(False)
123130
else:
@@ -132,7 +139,7 @@ def update_stats(self, filepath: Path | None = None, ext: str = ".", stats: dict
132139
if not filepath:
133140
self.layout().setSpacing(0)
134141
self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
135-
self.file_label.setText("<i>No Items Selected</i>") # TODO: Translate
142+
self.file_label.setText(f"<i>{Translations["preview.no_selection"]}</i>")
136143
self.file_label.set_file_path("")
137144
self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
138145
self.dimensions_label.setText("")
@@ -221,7 +228,7 @@ def update_multi_selection(self, count: int):
221228
"""Format attributes for multiple selected items."""
222229
self.layout().setSpacing(0)
223230
self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
224-
self.file_label.setText(f"<b>{count}</b> Items Selected") # TODO: Translate
231+
Translations.translate_qobject(self.file_label, "preview.multiple_selection", count=count)
225232
self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
226233
self.file_label.set_file_path("")
227234
self.dimensions_label.setText("")

tagstudio/src/qt/widgets/preview_panel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,14 @@ def __init__(self, library: Library, driver: "QtDriver"):
105105
self.add_tag_button.setCursor(Qt.CursorShape.PointingHandCursor)
106106
self.add_tag_button.setMinimumHeight(28)
107107
self.add_tag_button.setStyleSheet(PreviewPanel.button_style)
108-
self.add_tag_button.setText("Add Tag") # TODO: Translate
108+
Translations.translate_qobject(self.add_tag_button, "tag.add")
109109

110110
self.add_field_button = QPushButton()
111111
self.add_field_button.setEnabled(False)
112112
self.add_field_button.setCursor(Qt.CursorShape.PointingHandCursor)
113113
self.add_field_button.setMinimumHeight(28)
114114
self.add_field_button.setStyleSheet(PreviewPanel.button_style)
115-
self.add_field_button.setText("Add Field") # TODO: Translate
115+
Translations.translate_qobject(self.add_field_button, "library.field.add")
116116

117117
add_buttons_layout.addWidget(self.add_tag_button)
118118
add_buttons_layout.addWidget(self.add_field_button)

0 commit comments

Comments
 (0)