Skip to content

Commit

Permalink
feat(ui): add language setting (#803)
Browse files Browse the repository at this point in the history
* feat(ui): add language setting

* translations: implement remaining todos

* ui: show language names in settings instead of codes

* translations: add Dutch setting, anticipating #798
  • Loading branch information
CyanVoxel authored Feb 17, 2025
1 parent 28de21a commit 61b9fcf
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 29 deletions.
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

0 comments on commit 61b9fcf

Please sign in to comment.