Skip to content
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
34 changes: 17 additions & 17 deletions app/blitztext_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,18 +720,18 @@ def setup_tray(self) -> None:
self.menu.addSeparator()

# Diktat-Modus (Toggle): sammelt Transkripte als Notizen
self.action_dictation = QAction("🎤 Diktat-Modus", self)
self.action_dictation = QAction(t("tray.dictation_mode"), self)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh new tray actions after language changes

These newly translated tray actions are only evaluated during setup_tray(), while the Settings save path calls _refresh_i18n_texts() without updating action_dictation, action_history, or action_tts. In an app started in English, switching the interface language to Deutsch leaves these tray menu entries in English until restart, so the saved language change is only partially applied.

Useful? React with 👍 / 👎.

self.action_dictation.setCheckable(True)
self.action_dictation.toggled.connect(self._on_dictation_toggled)
self.menu.addAction(self.action_dictation)

# Verlauf anzeigen
self.action_history = QAction("📋 Verlauf…", self)
self.action_history = QAction(t("tray.history"), self)
self.action_history.triggered.connect(self.show_history_panel)
self.menu.addAction(self.action_history)

# Vorlesen (TTS)
self.action_tts = QAction("🔊 Vorlesen…", self)
self.action_tts = QAction(t("tray.tts"), self)
self.action_tts.triggered.connect(self.show_tts_window)
self.menu.addAction(self.action_tts)

Expand Down Expand Up @@ -918,7 +918,7 @@ def _start_recording(self, workflow: WorkflowType) -> None:
self._set_state("RECORDING", f"workflow {workflow.value} started")
except AudioRecorderError as e:
logger.error("Failed to start recording: %s", e)
self.show_tray_error("Aufnahme-Fehler", f"Aufnahme konnte nicht gestartet werden: {e}")
self.show_tray_error(t("error.recording.title"), t("error.recording.start_failed").format(error=e))
self.current_workflow = None
self._set_state("IDLE", "recording start failed")

Expand Down Expand Up @@ -988,7 +988,7 @@ def _stop_recording_and_process(self) -> None:

except AudioRecorderError as e:
logger.error("Failed to stop recording: %s", e)
self.show_tray_error("Aufnahme-Fehler", f"Aufnahme konnte nicht sauber gestoppt werden: {e}")
self.show_tray_error(t("error.recording.title"), t("error.recording.stop_failed").format(error=e))
self.current_workflow = None
self._set_state("IDLE", "recording stop failed")

Expand All @@ -1007,17 +1007,17 @@ def _on_worker_result(self, result_text: str) -> None:
self._add_to_history(result_text, is_dictation=self._dictation_mode)
if self._dictation_mode:
notify_service.notify(
"Blitztext Diktat",
"Eintrag gespeichert ({} Wörter).".format(len(result_text.split())),
t("notify.dictation.title"),
t("notify.dictation.entry_saved").format(count=len(result_text.split())),
)
self.current_workflow = None
self._set_state("IDLE", "worker result")

@pyqtSlot(str)
def _on_worker_error(self, err_msg: str) -> None:
logger.error("Worker error: %s", err_msg)
self.show_tray_error("Blitztext Fehler", err_msg)
notify_service.notify("Blitztext Fehler", err_msg, urgency="critical")
self.show_tray_error(t("notify.error.title"), err_msg)
notify_service.notify(t("notify.error.title"), err_msg, urgency="critical")
self.current_workflow = None
self._set_state("IDLE", "worker error", keep_error=True)

Expand All @@ -1031,7 +1031,7 @@ def _ensure_history_panel(self) -> HistoryPanel:
max_entries=self.config.history_size,
notes_folder=self.config.notes_folder,
)
panel.setWindowTitle("Blitztext – Verlauf")
panel.setWindowTitle(t("history.window_title"))
panel.resize(320, 440)
panel.merged.connect(self._on_dictation_merged)
panel.count_changed.connect(self._on_history_count_changed)
Expand Down Expand Up @@ -1075,12 +1075,12 @@ def set_dictation_mode(self, enabled: bool) -> None:
self._ensure_history_panel()
self.show_history_panel()
notify_service.notify(
"Blitztext Diktat",
"Diktat-Modus aktiv. Aufnahmen werden als Notizen gesammelt.",
t("notify.dictation.title"),
t("notify.dictation.mode_active"),
)

def _on_dictation_merged(self, path: str) -> None:
notify_service.notify("Blitztext Diktat", f"Zusammengeführt und gespeichert:\n{path}")
notify_service.notify(t("notify.dictation.title"), t("notify.dictation.merged").format(path=path))

def show_tts_window(self) -> None:
if self._tts_window is None:
Expand Down Expand Up @@ -1156,20 +1156,20 @@ def _on_state_changed(self) -> None:
def update_tray_state(self) -> None:
if self._tray_error_message:
self.tray_icon.setIcon(self._tray_icons["ERROR"])
self.tray_icon.setToolTip(f"Blitztext Fehler: {self._tray_error_message}")
self.tray_icon.setToolTip(t("tray.tooltip.error").format(message=self._tray_error_message))
elif self.state == "IDLE":
self.tray_icon.setIcon(self._tray_icons["IDLE"])
self.tray_icon.setToolTip(t("app.name"))
elif self.state == "RECORDING":
self.tray_icon.setIcon(self._tray_icons["RECORDING"])
wf_name = self.current_workflow.value if self.current_workflow else ""
self.tray_icon.setToolTip(f"Aufnahme läuft… ({wf_name})")
self.tray_icon.setToolTip(t("tray.tooltip.recording").format(workflow=wf_name))
elif self.state == "TRANSCRIBING":
self.tray_icon.setIcon(self._tray_icons["TRANSCRIBING"])
self.tray_icon.setToolTip("Transkribiere…")
self.tray_icon.setToolTip(t("mainwindow.status.transcribing"))
elif self.state == "LLM_REWRITING":
self.tray_icon.setIcon(self._tray_icons["LLM_REWRITING"])
self.tray_icon.setToolTip("Verarbeite mit KI…")
self.tray_icon.setToolTip(t("mainwindow.status.processing"))

@pyqtSlot(str)
def _on_hotkey_error(self, err_msg: str) -> None:
Expand Down
48 changes: 48 additions & 0 deletions app/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,30 @@
"history.note.merged_filename_prefix": "Diktat-zusammengefuehrt_",
"tray.show_window": "Fenster anzeigen",
"tray.writing_preset": "Schreibstil-Vorlage",
"tray.dictation_mode": "🎤 Diktat-Modus",
"tray.history": "📋 Verlauf…",
"tray.tts": "🔊 Vorlesen…",
"mainwindow.button.discard": "↺ Verwerfen",
"mainwindow.button.dictation": "✎ Diktat",
"mainwindow.button.history": "≡ Verlauf ({count})",
"mainwindow.tooltip.tts": "Vorlesen",
"mainwindow.tooltip.settings": "Einstellungen",
"mainwindow.status.ready": "Bereit",
"mainwindow.status.error": "Fehler",
"mainwindow.status.recording": "Aufnahme läuft…",
"mainwindow.status.transcribing": "Transkribiere…",
"mainwindow.status.processing": "Verarbeite mit KI…",
"history.window_title": "Blitztext – Verlauf",
"notify.dictation.title": "Blitztext Diktat",
"notify.dictation.entry_saved": "Eintrag gespeichert ({count} Wörter).",
"notify.dictation.mode_active": "Diktat-Modus aktiv. Aufnahmen werden als Notizen gesammelt.",
"notify.dictation.merged": "Zusammengeführt und gespeichert:\n{path}",
"notify.error.title": "Blitztext Fehler",
"tray.tooltip.error": "Blitztext Fehler: {message}",
"tray.tooltip.recording": "Aufnahme läuft… ({workflow})",
"error.recording.title": "Aufnahme-Fehler",
"error.recording.start_failed": "Aufnahme konnte nicht gestartet werden: {error}",
"error.recording.stop_failed": "Aufnahme konnte nicht sauber gestoppt werden: {error}",
"workflow.transcription.name": "🎙 Blitztext",
"workflow.local.name": "🔒 Blitztext Lokal",
"workflow.text_improver.name": "✨ Blitztext+",
Expand Down Expand Up @@ -253,6 +277,30 @@
"history.note.merged_filename_prefix": "Dictation-merged_",
"tray.show_window": "Show window",
"tray.writing_preset": "Writing style preset",
"tray.dictation_mode": "🎤 Dictation mode",
"tray.history": "📋 History…",
"tray.tts": "🔊 Read aloud…",
"mainwindow.button.discard": "↺ Discard",
"mainwindow.button.dictation": "✎ Dictation",
"mainwindow.button.history": "≡ History ({count})",
"mainwindow.tooltip.tts": "Read aloud",
"mainwindow.tooltip.settings": "Settings",
"mainwindow.status.ready": "Ready",
"mainwindow.status.error": "Error",
"mainwindow.status.recording": "Recording…",
"mainwindow.status.transcribing": "Transcribing…",
"mainwindow.status.processing": "Processing with AI…",
"history.window_title": "Blitztext – History",
"notify.dictation.title": "Blitztext Dictation",
"notify.dictation.entry_saved": "Entry saved ({count} words).",
"notify.dictation.mode_active": "Dictation mode active. Recordings are collected as notes.",
"notify.dictation.merged": "Merged and saved:\n{path}",
"notify.error.title": "Blitztext Error",
"tray.tooltip.error": "Blitztext Error: {message}",
"tray.tooltip.recording": "Recording… ({workflow})",
"error.recording.title": "Recording error",
"error.recording.start_failed": "Recording could not be started: {error}",
"error.recording.stop_failed": "Recording could not be stopped cleanly: {error}",
"workflow.transcription.name": "🎙 Blitztext",
"workflow.local.name": "🔒 Blitztext Local",
"workflow.text_improver.name": "✨ Blitztext+",
Expand Down
24 changes: 12 additions & 12 deletions app/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def _setup_ui(self) -> None:
self._rec_indicator = QLabel("●")
self._rec_indicator.setStyleSheet(f"color: {theme.STATE_IDLE}; font-size: 10px;")
status_row.addWidget(self._rec_indicator)
self._status_label = QLabel("Bereit")
self._status_label = QLabel(t("mainwindow.status.ready"))
self._status_label.setStyleSheet("font-size: 12px; font-weight: 600;")
status_row.addWidget(self._status_label)
self._timer_label = QLabel("00:00")
Expand All @@ -213,14 +213,14 @@ def _setup_ui(self) -> None:
# Sekundaerzeile: Verwerfen + Diktat (Pills)
sec_row = QHBoxLayout()
sec_row.setSpacing(6)
self._btn_discard = QPushButton("↺ Verwerfen")
self._btn_discard = QPushButton(t("mainwindow.button.discard"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh main-window labels after language changes

When the main window already exists, changing the interface language in Settings calls BlitztextApp._refresh_i18n_texts(), but that method only updates the window title for _main_window; the buttons, tooltips, history label, and current status text initialized here keep the old language until restart or window recreation. This is visible when starting in English and switching to Deutsch, even though the settings help says changes apply after saving.

Useful? React with 👍 / 👎.

self._btn_discard.setMinimumHeight(28)
self._btn_discard.setStyleSheet("border-radius: 14px; font-weight: 600;")
self._btn_discard.setEnabled(False)
self._btn_discard.clicked.connect(self._on_discard_clicked)
sec_row.addWidget(self._btn_discard)

self._btn_dictation = QPushButton("✎ Diktat")
self._btn_dictation = QPushButton(t("mainwindow.button.dictation"))
self._btn_dictation.setMinimumHeight(28)
self._btn_dictation.setStyleSheet("border-radius: 14px; font-weight: 600;")
self._btn_dictation.setCheckable(True)
Expand All @@ -231,7 +231,7 @@ def _setup_ui(self) -> None:
# Unterzeile: Verlauf (mit Zaehler), Vorlesen, Einstellungen
bottom_row = QHBoxLayout()
bottom_row.setSpacing(6)
self._btn_history = QPushButton("≡ Verlauf (0)")
self._btn_history = QPushButton(t("mainwindow.button.history").format(count=0))
self._btn_history.setMinimumHeight(28)
self._btn_history.setStyleSheet(f"border-radius: 14px; color: {theme.APP_TEXT_DIM};")
self._btn_history.clicked.connect(self._controller.show_history_panel)
Expand All @@ -242,7 +242,7 @@ def _setup_ui(self) -> None:
self._btn_tts.setStyleSheet(
f"border-radius: 14px; font-size: 13px; color: {theme.APP_TEXT_DIM}; padding: 0;"
)
self._btn_tts.setToolTip("Vorlesen")
self._btn_tts.setToolTip(t("mainwindow.tooltip.tts"))
self._btn_tts.clicked.connect(self._controller.show_tts_window)
bottom_row.addWidget(self._btn_tts)

Expand All @@ -251,7 +251,7 @@ def _setup_ui(self) -> None:
self._btn_settings.setStyleSheet(
f"border-radius: 14px; font-size: 13px; color: {theme.APP_TEXT_DIM}; padding: 0;"
)
self._btn_settings.setToolTip("Einstellungen")
self._btn_settings.setToolTip(t("mainwindow.tooltip.settings"))
self._btn_settings.clicked.connect(self._controller.show_settings_dialog)
bottom_row.addWidget(self._btn_settings)
layout.addLayout(bottom_row)
Expand Down Expand Up @@ -295,16 +295,16 @@ def update_state(self, state: str, workflow: Optional[WorkflowType], error: Opti
self._workflow_combo.setEnabled(state == "IDLE")

if error:
self._set_status("Fehler", theme.STATE_ERROR)
self._set_status(t("mainwindow.status.error"), theme.STATE_ERROR)
elif recording:
# Der Workflow steht bereits im Dropdown darueber — kein Suffix noetig.
self._set_status("Aufnahme läuft…", theme.STATE_RECORDING)
self._set_status(t("mainwindow.status.recording"), theme.STATE_RECORDING)
elif state == "TRANSCRIBING":
self._set_status("Transkribiere…", theme.STATE_PROCESSING)
self._set_status(t("mainwindow.status.transcribing"), theme.STATE_PROCESSING)
elif state == "LLM_REWRITING":
self._set_status("Verarbeite mit KI…", theme.STATE_PROCESSING)
self._set_status(t("mainwindow.status.processing"), theme.STATE_PROCESSING)
else:
self._set_status("Bereit", theme.STATE_IDLE)
self._set_status(t("mainwindow.status.ready"), theme.STATE_IDLE)

if recording:
if self._rec_start is None:
Expand Down Expand Up @@ -336,7 +336,7 @@ def _set_status(self, text: str, color: str) -> None:
self._rec_indicator.setStyleSheet(f"color: {color}; font-size: 10px;")

def set_history_count(self, count: int) -> None:
self._btn_history.setText(f"≡ Verlauf ({count})")
self._btn_history.setText(t("mainwindow.button.history").format(count=count))

def set_dictation_checked(self, checked: bool) -> None:
self._btn_dictation.blockSignals(True)
Expand Down
26 changes: 25 additions & 1 deletion tests/test_smoke_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import pytest

from app.i18n import DEFAULT_LANGUAGE, get_language, set_language
from app.i18n import DEFAULT_LANGUAGE, get_language, set_language, t

_GUI = os.environ.get("WHISPER_GUI_TESTS") == "1"
gui_only = pytest.mark.skipif(not _GUI, reason="benötigt WHISPER_GUI_TESTS=1 (Display)")
Expand Down Expand Up @@ -56,6 +56,30 @@ def test_app_boots_idles_and_exits_clean(ui_language, tmp_path):

assert exit_code == 0
assert get_language() == ui_language

# Regressionsschutz: Hauptfenster- und Tray-Texte laufen über t() und
# spiegeln die aktive Sprache. Fängt vergessene Call-Sites ab, die der
# reine Key-Vollständigkeitstest (test_i18n) nicht erkennt. Bewusst im
# selben Boot wie der Smoke-Test (kein zweiter BlitztextApp im Prozess,
# um QObject-Leaks in Folgetests zu vermeiden).
win = app._main_window
assert win._btn_discard.text() == t("mainwindow.button.discard")
assert win._btn_dictation.text() == t("mainwindow.button.dictation")
assert win._btn_history.text() == t("mainwindow.button.history").format(count=0)
assert win._btn_tts.toolTip() == t("mainwindow.tooltip.tts")
assert win._btn_settings.toolTip() == t("mainwindow.tooltip.settings")
assert win._status_label.text() == t("mainwindow.status.ready")
assert app.action_dictation.text() == t("tray.dictation_mode")
assert app.action_history.text() == t("tray.history")
assert app.action_tts.text() == t("tray.tts")

# Sprachabhängigkeit echt verankern (nicht nur Tautologie über t()).
if ui_language == "en":
assert "Discard" in win._btn_discard.text()
assert "History" in app.action_history.text()
else:
assert "Verwerfen" in win._btn_discard.text()
assert "Verlauf" in app.action_history.text()
finally:
# Idempotentes Cleanup, damit kein Thread in Folgetests nachhaengt.
if app is not None:
Expand Down