mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-02-01 15:49:09 +00:00
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
This commit is contained in:
committed by
GitHub
parent
28de21ade7
commit
61b9fcf764
@@ -199,6 +199,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",
|
||||
@@ -209,6 +210,7 @@
|
||||
"namespace.create.title": "Create Namespace",
|
||||
"namespace.new.button": "New Namespace",
|
||||
"namespace.new.prompt": "Create a New Namespace to Start Adding Custom Colors!",
|
||||
"preview.multiple_selection": "<b>{count}</b> Items Selected",
|
||||
"preview.no_selection": "No Items Selected",
|
||||
"select.add_tag_to_selected": "Add Tag to Selected",
|
||||
"select.all": "Select All",
|
||||
@@ -216,9 +218,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}\"...",
|
||||
|
||||
@@ -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):
|
||||
|
||||
73
tagstudio/src/qt/modals/settings_panel.py
Normal file
73
tagstudio/src/qt/modals/settings_panel.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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")
|
||||
self.languages = {
|
||||
# "Cantonese (Traditional)": "yue_Hant", # Empty
|
||||
"Chinese (Traditional)": "zh_Hant",
|
||||
# "Czech": "cs", # Minimal
|
||||
# "Danish": "da", # Minimal
|
||||
"Dutch": "nl",
|
||||
"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(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 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)
|
||||
)
|
||||
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:
|
||||
values: list[str] = list(self.languages.values())
|
||||
return values[self.language_combobox.currentIndex()]
|
||||
@@ -70,16 +70,20 @@ class Translator:
|
||||
|
||||
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 @@ class Translator:
|
||||
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")
|
||||
|
||||
@@ -87,6 +87,7 @@ from src.qt.modals.file_extension import FileExtensionModal
|
||||
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_color_manager import TagColorManager
|
||||
from src.qt.modals.tag_database import TagDatabasePanel
|
||||
from src.qt.modals.tag_search import TagSearchPanel
|
||||
@@ -197,6 +198,10 @@ class QtDriver(DriverMixin, QObject):
|
||||
)
|
||||
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(
|
||||
@@ -366,19 +371,6 @@ class QtDriver(DriverMixin, QObject):
|
||||
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(
|
||||
@@ -397,6 +389,23 @@ class QtDriver(DriverMixin, QObject):
|
||||
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)
|
||||
@@ -1830,6 +1839,24 @@ class QtDriver(DriverMixin, QObject):
|
||||
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)}
|
||||
|
||||
@@ -23,6 +23,7 @@ from src.core.enums import Theme
|
||||
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 @@ class FileAttributes(QWidget):
|
||||
created = dt.fromtimestamp(filepath.stat().st_ctime)
|
||||
modified: dt = dt.fromtimestamp(filepath.stat().st_mtime)
|
||||
self.date_created_label.setText(
|
||||
f"<b>Date Created:</b> {dt.strftime(created, "%a, %x, %X")}" # TODO: Translate
|
||||
f"<b>{Translations["file.date_created"]}:</b> "
|
||||
f"{dt.strftime(created, "%a, %x, %X")}"
|
||||
)
|
||||
self.date_modified_label.setText(
|
||||
f"<b>Date Modified:</b> {dt.strftime(modified, "%a, %x, %X")}" # TODO: Translate
|
||||
f"<b>{Translations["file.date_modified"]}:</b> "
|
||||
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("<b>Date Created:</b> <i>N/A</i>") # TODO: Translate
|
||||
self.date_modified_label.setText("<b>Date Modified:</b> <i>N/A</i>") # TODO: Translate
|
||||
self.date_created_label.setText(
|
||||
f"<b>{Translations["file.date_created"]}:</b> <i>N/A</i>"
|
||||
)
|
||||
self.date_modified_label.setText(
|
||||
f"<b>{Translations["file.date_modified"]}:</b> <i>N/A</i>"
|
||||
)
|
||||
self.date_created_label.setHidden(False)
|
||||
self.date_modified_label.setHidden(False)
|
||||
else:
|
||||
@@ -132,7 +139,7 @@ class FileAttributes(QWidget):
|
||||
if not filepath:
|
||||
self.layout().setSpacing(0)
|
||||
self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.file_label.setText("<i>No Items Selected</i>") # TODO: Translate
|
||||
self.file_label.setText(f"<i>{Translations["preview.no_selection"]}</i>")
|
||||
self.file_label.set_file_path("")
|
||||
self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.dimensions_label.setText("")
|
||||
@@ -221,7 +228,7 @@ class FileAttributes(QWidget):
|
||||
"""Format attributes for multiple selected items."""
|
||||
self.layout().setSpacing(0)
|
||||
self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.file_label.setText(f"<b>{count}</b> 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("")
|
||||
|
||||
@@ -105,14 +105,14 @@ class PreviewPanel(QWidget):
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user