"""Qt widget integration for i18n."""
from __future__ import annotations
from functools import wraps
from typing import TYPE_CHECKING, Any, cast
from PySide6.QtCore import Signal
from PySide6.QtGui import QAction
from PySide6.QtWidgets import (
QComboBox,
QLabel,
QMenu,
QPushButton,
)
from qtframework.i18n.babel_manager import get_i18n_manager
from qtframework.utils.logger import get_logger
if TYPE_CHECKING:
from collections.abc import Callable
from PySide6.QtCore import QObject
from PySide6.QtWidgets import (
QWidget,
)
logger = get_logger(__name__)
[docs]
class TranslatableLabel(QLabel, TranslatableWidget):
"""QLabel with automatic translation support."""
def __init__(
self, translation_key: str | None = None, parent: QWidget | None = None, **kwargs
) -> None:
"""Initialize translatable label.
Args:
translation_key: Optional translation key
parent: Parent widget
**kwargs: Translation variables
"""
QLabel.__init__(self, parent)
TranslatableWidget.__init__(self)
if translation_key:
self.set_translation_key(translation_key, **kwargs)
[docs]
class TranslatableAction(QAction, TranslatableWidget):
"""QAction with automatic translation support."""
def __init__(
self, translation_key: str | None = None, parent: QObject | None = None, **kwargs
) -> None:
"""Initialize translatable action.
Args:
translation_key: Optional translation key
parent: Parent object
**kwargs: Translation variables
"""
QAction.__init__(self, parent)
TranslatableWidget.__init__(self)
if translation_key:
self.set_translation_key(translation_key, **kwargs)
[docs]
class LanguageSelector(QComboBox):
"""Combo box for selecting application language."""
language_changed = Signal(str)
def __init__(self, parent: QWidget | None = None) -> None:
"""Initialize language selector."""
super().__init__(parent)
self.manager = get_i18n_manager()
if not self.manager:
logger.warning("No i18n manager available")
return
# Populate languages
self._populate_languages()
# Set current selection
current_locale = self.manager.get_current_locale()
index = self.findData(current_locale)
if index >= 0:
self.setCurrentIndex(index)
# Connect change signal
self.currentIndexChanged.connect(self._on_selection_changed)
# Update on locale changes
self.manager.locale_changed.connect(self._on_locale_changed)
def _populate_languages(self) -> None:
"""Populate the combo box with available languages."""
self.clear()
if not self.manager:
return
locale_info = self.manager.get_locale_info()
# Sort by display name
sorted_locales = sorted(locale_info.items(), key=lambda x: x[1]["display_name"])
for locale_code, info in sorted_locales:
display_name = info["display_name"]
self.addItem(display_name, locale_code)
def _on_selection_changed(self, index: int) -> None:
"""Handle language selection change."""
if index < 0 or not self.manager:
return
locale = self.itemData(index)
if locale:
self.manager.set_locale(locale)
self.language_changed.emit(locale)
def _on_locale_changed(self, locale: str) -> None:
"""Handle external locale change."""
index = self.findData(locale)
if index >= 0 and index != self.currentIndex():
self.setCurrentIndex(index)
[docs]
def translatable(translation_key: str | None = None, **default_args):
"""Decorator to make a method automatically translatable.
Args:
translation_key: Translation key to use
**default_args: Default translation arguments
Returns:
Decorated method
"""
def decorator(func: Callable) -> Callable:
"""Wrap method to return translated text.
Args:
func: Method to wrap
Returns:
Wrapped method that returns translated text
"""
@wraps(func)
def wrapper(self, *args, **kwargs):
"""Execute method and translate result.
Args:
self: Instance of the class
*args: Positional arguments
**kwargs: Keyword arguments
Returns:
Translated text or original method result
"""
# Get translation
manager = get_i18n_manager()
if manager and translation_key:
# Merge default args with provided args
trans_args = {**default_args, **kwargs}
text = manager.t(translation_key, **trans_args)
# If the method returns a string, replace it
result = func(self, *args, **kwargs)
if isinstance(result, str):
return text
return result
return func(self, *args, **kwargs)
return wrapper
return decorator
[docs]
class TranslationHelper:
"""Helper class for managing translations in complex widgets."""
def __init__(self, widget: QWidget) -> None:
"""Initialize translation helper.
Args:
widget: Widget to manage translations for
"""
self.widget = widget
self.translations: dict[int, dict[str, Any]] = {}
self.manager = get_i18n_manager()
if self.manager:
self.manager.translations_reloaded.connect(self._update_all)
self.manager.locale_changed.connect(lambda _: self._update_all())
[docs]
def register(self, element: Any, key: str, setter: str = "setText", **kwargs) -> None:
"""Register an element for automatic translation.
Args:
element: UI element to translate
key: Translation key
setter: Method name to set text (default: "setText")
**kwargs: Translation arguments
"""
element_id = id(element)
self.translations[element_id] = {
"element": element,
"key": key,
"setter": setter,
"args": kwargs,
}
# Apply initial translation
self._update_element(element_id)
[docs]
def register_plural(
self, element: Any, key: str, count: int, setter: str = "setText", **kwargs
) -> None:
"""Register an element for plural translation.
Args:
element: UI element to translate
key: Translation key
count: Count for pluralization
setter: Method name to set text
**kwargs: Additional arguments
"""
element_id = id(element)
self.translations[element_id] = {
"element": element,
"key": key,
"setter": setter,
"count": count,
"args": kwargs,
}
# Apply initial translation
self._update_element(element_id)
[docs]
def update_count(self, element: Any, count: int) -> None:
"""Update the count for a plural translation.
Args:
element: UI element
count: New count
"""
element_id = id(element)
if element_id in self.translations:
self.translations[element_id]["count"] = count
self._update_element(element_id)
def _update_element(self, element_id: int) -> None:
"""Update a single element's translation."""
if not self.manager or element_id not in self.translations:
return
info = self.translations[element_id]
element = info["element"]
# Get translation
if "count" in info:
text = self.manager.plural(info["key"], info["count"], **info["args"])
else:
text = self.manager.t(info["key"], **info["args"])
# Set text using specified method
setter = getattr(element, info["setter"], None)
if setter:
setter(text)
def _update_all(self) -> None:
"""Update all registered translations."""
for element_id in self.translations:
self._update_element(element_id)
[docs]
def clear(self) -> None:
"""Clear all registered translations."""
self.translations.clear()
__all__ = [
"LanguageSelector",
"TranslatableAction",
"TranslatableButton",
"TranslatableLabel",
"TranslatableMenu",
"TranslatableWidget",
"TranslationHelper",
"setup_widget_translations",
"translatable",
]