Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): add language setting #803

Merged
merged 5 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions tagstudio/resources/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -209,16 +210,20 @@
"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",
"select.clear": "Clear Selection",
"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}\"...",
Expand Down
1 change: 1 addition & 0 deletions tagstudio/src/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
73 changes: 73 additions & 0 deletions tagstudio/src/qt/modals/settings_panel.py
Original file line number Diff line number Diff line change
@@ -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()]
18 changes: 10 additions & 8 deletions tagstudio/src/qt/translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,26 +70,28 @@ 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

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")
53 changes: 40 additions & 13 deletions tagstudio/src/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_color_manager import TagColorManager
from src.qt.modals.tag_database import TagDatabasePanel
from src.qt.modals.tag_search import TagSearchPanel
Expand Down Expand Up @@ -197,6 +198,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(
Expand Down Expand Up @@ -366,19 +371,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(
Expand All @@ -397,6 +389,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)
Expand Down Expand Up @@ -1830,6 +1839,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)}
Expand Down
19 changes: 13 additions & 6 deletions tagstudio/src/qt/widgets/preview/file_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"<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:
Expand All @@ -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("<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("")
Expand Down Expand Up @@ -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"<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("")
Expand Down
4 changes: 2 additions & 2 deletions tagstudio/src/qt/widgets/preview_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down