Skip to content

Commit 64bb2b5

Browse files
authored
fix(i18n): Hauptfenster, Tray und Notifications vollständig übersetzen (Paket-G-Nachzug) (#15)
Paket G hatte mehrere UI-Call-Sites bei der String-Extraktion ausgelassen, sodass die Oberfläche trotz Sprachwahl "English" teils deutsch blieb. Der i18n-Completeness-Test prüft nur Key-Parität zwischen de/en, nicht ob alle sichtbaren Strings durch t() laufen — darum unbemerkt durch die CI. Übersetzt (über t(), neue Keys in app/i18n.py, de==en je 149): - Hauptfenster (main_window.py): Status "Bereit/Fehler/Aufnahme läuft…/ Transkribiere…/Verarbeite mit KI…", Buttons "Verwerfen/Diktat", History- Button mit Zähler, Tooltips "Vorlesen/Einstellungen". - Tray (blitztext_linux.py): Actions "Diktat-Modus/Verlauf…/Vorlesen…", Tray-Tooltips (Fehler/Aufnahme/Transkribiere/Verarbeite), Verlauf- Fenstertitel, Diktat-/Fehler-Notifications, Aufnahme-Fehlermeldungen. Nicht angefasst: Start/Stopp-Text des Shutter-Buttons (reiner Test-/Logik- Text, gemalt wird der Glyph), Logger-Ausgaben, Docstrings. Regressionsschutz: test_smoke_launch prüft beim Offscreen-Boot je Sprache (de/en) jetzt zusätzlich, dass Hauptfenster- und Tray-Texte der aktiven Sprache entsprechen — fängt künftige vergessene Call-Sites ab. Assertions bewusst im bestehenden Boot (kein zweiter BlitztextApp/QObject-Leak). Volle Suite 262 grün (offscreen, WHISPER_GUI_TESTS=1), stabil in deterministischer und randomisierter Reihenfolge.
1 parent d1ba3cd commit 64bb2b5

4 files changed

Lines changed: 102 additions & 30 deletions

File tree

app/blitztext_linux.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -720,18 +720,18 @@ def setup_tray(self) -> None:
720720
self.menu.addSeparator()
721721

722722
# Diktat-Modus (Toggle): sammelt Transkripte als Notizen
723-
self.action_dictation = QAction("🎤 Diktat-Modus", self)
723+
self.action_dictation = QAction(t("tray.dictation_mode"), self)
724724
self.action_dictation.setCheckable(True)
725725
self.action_dictation.toggled.connect(self._on_dictation_toggled)
726726
self.menu.addAction(self.action_dictation)
727727

728728
# Verlauf anzeigen
729-
self.action_history = QAction("📋 Verlauf…", self)
729+
self.action_history = QAction(t("tray.history"), self)
730730
self.action_history.triggered.connect(self.show_history_panel)
731731
self.menu.addAction(self.action_history)
732732

733733
# Vorlesen (TTS)
734-
self.action_tts = QAction("🔊 Vorlesen…", self)
734+
self.action_tts = QAction(t("tray.tts"), self)
735735
self.action_tts.triggered.connect(self.show_tts_window)
736736
self.menu.addAction(self.action_tts)
737737

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

@@ -988,7 +988,7 @@ def _stop_recording_and_process(self) -> None:
988988

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

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

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

@@ -1031,7 +1031,7 @@ def _ensure_history_panel(self) -> HistoryPanel:
10311031
max_entries=self.config.history_size,
10321032
notes_folder=self.config.notes_folder,
10331033
)
1034-
panel.setWindowTitle("Blitztext – Verlauf")
1034+
panel.setWindowTitle(t("history.window_title"))
10351035
panel.resize(320, 440)
10361036
panel.merged.connect(self._on_dictation_merged)
10371037
panel.count_changed.connect(self._on_history_count_changed)
@@ -1075,12 +1075,12 @@ def set_dictation_mode(self, enabled: bool) -> None:
10751075
self._ensure_history_panel()
10761076
self.show_history_panel()
10771077
notify_service.notify(
1078-
"Blitztext Diktat",
1079-
"Diktat-Modus aktiv. Aufnahmen werden als Notizen gesammelt.",
1078+
t("notify.dictation.title"),
1079+
t("notify.dictation.mode_active"),
10801080
)
10811081

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

10851085
def show_tts_window(self) -> None:
10861086
if self._tts_window is None:
@@ -1156,20 +1156,20 @@ def _on_state_changed(self) -> None:
11561156
def update_tray_state(self) -> None:
11571157
if self._tray_error_message:
11581158
self.tray_icon.setIcon(self._tray_icons["ERROR"])
1159-
self.tray_icon.setToolTip(f"Blitztext Fehler: {self._tray_error_message}")
1159+
self.tray_icon.setToolTip(t("tray.tooltip.error").format(message=self._tray_error_message))
11601160
elif self.state == "IDLE":
11611161
self.tray_icon.setIcon(self._tray_icons["IDLE"])
11621162
self.tray_icon.setToolTip(t("app.name"))
11631163
elif self.state == "RECORDING":
11641164
self.tray_icon.setIcon(self._tray_icons["RECORDING"])
11651165
wf_name = self.current_workflow.value if self.current_workflow else ""
1166-
self.tray_icon.setToolTip(f"Aufnahme läuft… ({wf_name})")
1166+
self.tray_icon.setToolTip(t("tray.tooltip.recording").format(workflow=wf_name))
11671167
elif self.state == "TRANSCRIBING":
11681168
self.tray_icon.setIcon(self._tray_icons["TRANSCRIBING"])
1169-
self.tray_icon.setToolTip("Transkribiere…")
1169+
self.tray_icon.setToolTip(t("mainwindow.status.transcribing"))
11701170
elif self.state == "LLM_REWRITING":
11711171
self.tray_icon.setIcon(self._tray_icons["LLM_REWRITING"])
1172-
self.tray_icon.setToolTip("Verarbeite mit KI…")
1172+
self.tray_icon.setToolTip(t("mainwindow.status.processing"))
11731173

11741174
@pyqtSlot(str)
11751175
def _on_hotkey_error(self, err_msg: str) -> None:

app/i18n.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,30 @@
126126
"history.note.merged_filename_prefix": "Diktat-zusammengefuehrt_",
127127
"tray.show_window": "Fenster anzeigen",
128128
"tray.writing_preset": "Schreibstil-Vorlage",
129+
"tray.dictation_mode": "🎤 Diktat-Modus",
130+
"tray.history": "📋 Verlauf…",
131+
"tray.tts": "🔊 Vorlesen…",
132+
"mainwindow.button.discard": "↺ Verwerfen",
133+
"mainwindow.button.dictation": "✎ Diktat",
134+
"mainwindow.button.history": "≡ Verlauf ({count})",
135+
"mainwindow.tooltip.tts": "Vorlesen",
136+
"mainwindow.tooltip.settings": "Einstellungen",
137+
"mainwindow.status.ready": "Bereit",
138+
"mainwindow.status.error": "Fehler",
139+
"mainwindow.status.recording": "Aufnahme läuft…",
140+
"mainwindow.status.transcribing": "Transkribiere…",
141+
"mainwindow.status.processing": "Verarbeite mit KI…",
142+
"history.window_title": "Blitztext – Verlauf",
143+
"notify.dictation.title": "Blitztext Diktat",
144+
"notify.dictation.entry_saved": "Eintrag gespeichert ({count} Wörter).",
145+
"notify.dictation.mode_active": "Diktat-Modus aktiv. Aufnahmen werden als Notizen gesammelt.",
146+
"notify.dictation.merged": "Zusammengeführt und gespeichert:\n{path}",
147+
"notify.error.title": "Blitztext Fehler",
148+
"tray.tooltip.error": "Blitztext Fehler: {message}",
149+
"tray.tooltip.recording": "Aufnahme läuft… ({workflow})",
150+
"error.recording.title": "Aufnahme-Fehler",
151+
"error.recording.start_failed": "Aufnahme konnte nicht gestartet werden: {error}",
152+
"error.recording.stop_failed": "Aufnahme konnte nicht sauber gestoppt werden: {error}",
129153
"workflow.transcription.name": "🎙 Blitztext",
130154
"workflow.local.name": "🔒 Blitztext Lokal",
131155
"workflow.text_improver.name": "✨ Blitztext+",
@@ -253,6 +277,30 @@
253277
"history.note.merged_filename_prefix": "Dictation-merged_",
254278
"tray.show_window": "Show window",
255279
"tray.writing_preset": "Writing style preset",
280+
"tray.dictation_mode": "🎤 Dictation mode",
281+
"tray.history": "📋 History…",
282+
"tray.tts": "🔊 Read aloud…",
283+
"mainwindow.button.discard": "↺ Discard",
284+
"mainwindow.button.dictation": "✎ Dictation",
285+
"mainwindow.button.history": "≡ History ({count})",
286+
"mainwindow.tooltip.tts": "Read aloud",
287+
"mainwindow.tooltip.settings": "Settings",
288+
"mainwindow.status.ready": "Ready",
289+
"mainwindow.status.error": "Error",
290+
"mainwindow.status.recording": "Recording…",
291+
"mainwindow.status.transcribing": "Transcribing…",
292+
"mainwindow.status.processing": "Processing with AI…",
293+
"history.window_title": "Blitztext – History",
294+
"notify.dictation.title": "Blitztext Dictation",
295+
"notify.dictation.entry_saved": "Entry saved ({count} words).",
296+
"notify.dictation.mode_active": "Dictation mode active. Recordings are collected as notes.",
297+
"notify.dictation.merged": "Merged and saved:\n{path}",
298+
"notify.error.title": "Blitztext Error",
299+
"tray.tooltip.error": "Blitztext Error: {message}",
300+
"tray.tooltip.recording": "Recording… ({workflow})",
301+
"error.recording.title": "Recording error",
302+
"error.recording.start_failed": "Recording could not be started: {error}",
303+
"error.recording.stop_failed": "Recording could not be stopped cleanly: {error}",
256304
"workflow.transcription.name": "🎙 Blitztext",
257305
"workflow.local.name": "🔒 Blitztext Local",
258306
"workflow.text_improver.name": "✨ Blitztext+",

app/main_window.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def _setup_ui(self) -> None:
198198
self._rec_indicator = QLabel("●")
199199
self._rec_indicator.setStyleSheet(f"color: {theme.STATE_IDLE}; font-size: 10px;")
200200
status_row.addWidget(self._rec_indicator)
201-
self._status_label = QLabel("Bereit")
201+
self._status_label = QLabel(t("mainwindow.status.ready"))
202202
self._status_label.setStyleSheet("font-size: 12px; font-weight: 600;")
203203
status_row.addWidget(self._status_label)
204204
self._timer_label = QLabel("00:00")
@@ -213,14 +213,14 @@ def _setup_ui(self) -> None:
213213
# Sekundaerzeile: Verwerfen + Diktat (Pills)
214214
sec_row = QHBoxLayout()
215215
sec_row.setSpacing(6)
216-
self._btn_discard = QPushButton("↺ Verwerfen")
216+
self._btn_discard = QPushButton(t("mainwindow.button.discard"))
217217
self._btn_discard.setMinimumHeight(28)
218218
self._btn_discard.setStyleSheet("border-radius: 14px; font-weight: 600;")
219219
self._btn_discard.setEnabled(False)
220220
self._btn_discard.clicked.connect(self._on_discard_clicked)
221221
sec_row.addWidget(self._btn_discard)
222222

223-
self._btn_dictation = QPushButton("✎ Diktat")
223+
self._btn_dictation = QPushButton(t("mainwindow.button.dictation"))
224224
self._btn_dictation.setMinimumHeight(28)
225225
self._btn_dictation.setStyleSheet("border-radius: 14px; font-weight: 600;")
226226
self._btn_dictation.setCheckable(True)
@@ -231,7 +231,7 @@ def _setup_ui(self) -> None:
231231
# Unterzeile: Verlauf (mit Zaehler), Vorlesen, Einstellungen
232232
bottom_row = QHBoxLayout()
233233
bottom_row.setSpacing(6)
234-
self._btn_history = QPushButton("≡ Verlauf (0)")
234+
self._btn_history = QPushButton(t("mainwindow.button.history").format(count=0))
235235
self._btn_history.setMinimumHeight(28)
236236
self._btn_history.setStyleSheet(f"border-radius: 14px; color: {theme.APP_TEXT_DIM};")
237237
self._btn_history.clicked.connect(self._controller.show_history_panel)
@@ -242,7 +242,7 @@ def _setup_ui(self) -> None:
242242
self._btn_tts.setStyleSheet(
243243
f"border-radius: 14px; font-size: 13px; color: {theme.APP_TEXT_DIM}; padding: 0;"
244244
)
245-
self._btn_tts.setToolTip("Vorlesen")
245+
self._btn_tts.setToolTip(t("mainwindow.tooltip.tts"))
246246
self._btn_tts.clicked.connect(self._controller.show_tts_window)
247247
bottom_row.addWidget(self._btn_tts)
248248

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

297297
if error:
298-
self._set_status("Fehler", theme.STATE_ERROR)
298+
self._set_status(t("mainwindow.status.error"), theme.STATE_ERROR)
299299
elif recording:
300300
# Der Workflow steht bereits im Dropdown darueber — kein Suffix noetig.
301-
self._set_status("Aufnahme läuft…", theme.STATE_RECORDING)
301+
self._set_status(t("mainwindow.status.recording"), theme.STATE_RECORDING)
302302
elif state == "TRANSCRIBING":
303-
self._set_status("Transkribiere…", theme.STATE_PROCESSING)
303+
self._set_status(t("mainwindow.status.transcribing"), theme.STATE_PROCESSING)
304304
elif state == "LLM_REWRITING":
305-
self._set_status("Verarbeite mit KI…", theme.STATE_PROCESSING)
305+
self._set_status(t("mainwindow.status.processing"), theme.STATE_PROCESSING)
306306
else:
307-
self._set_status("Bereit", theme.STATE_IDLE)
307+
self._set_status(t("mainwindow.status.ready"), theme.STATE_IDLE)
308308

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

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

341341
def set_dictation_checked(self, checked: bool) -> None:
342342
self._btn_dictation.blockSignals(True)

tests/test_smoke_launch.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import pytest
2323

24-
from app.i18n import DEFAULT_LANGUAGE, get_language, set_language
24+
from app.i18n import DEFAULT_LANGUAGE, get_language, set_language, t
2525

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

5757
assert exit_code == 0
5858
assert get_language() == ui_language
59+
60+
# Regressionsschutz: Hauptfenster- und Tray-Texte laufen über t() und
61+
# spiegeln die aktive Sprache. Fängt vergessene Call-Sites ab, die der
62+
# reine Key-Vollständigkeitstest (test_i18n) nicht erkennt. Bewusst im
63+
# selben Boot wie der Smoke-Test (kein zweiter BlitztextApp im Prozess,
64+
# um QObject-Leaks in Folgetests zu vermeiden).
65+
win = app._main_window
66+
assert win._btn_discard.text() == t("mainwindow.button.discard")
67+
assert win._btn_dictation.text() == t("mainwindow.button.dictation")
68+
assert win._btn_history.text() == t("mainwindow.button.history").format(count=0)
69+
assert win._btn_tts.toolTip() == t("mainwindow.tooltip.tts")
70+
assert win._btn_settings.toolTip() == t("mainwindow.tooltip.settings")
71+
assert win._status_label.text() == t("mainwindow.status.ready")
72+
assert app.action_dictation.text() == t("tray.dictation_mode")
73+
assert app.action_history.text() == t("tray.history")
74+
assert app.action_tts.text() == t("tray.tts")
75+
76+
# Sprachabhängigkeit echt verankern (nicht nur Tautologie über t()).
77+
if ui_language == "en":
78+
assert "Discard" in win._btn_discard.text()
79+
assert "History" in app.action_history.text()
80+
else:
81+
assert "Verwerfen" in win._btn_discard.text()
82+
assert "Verlauf" in app.action_history.text()
5983
finally:
6084
# Idempotentes Cleanup, damit kein Thread in Folgetests nachhaengt.
6185
if app is not None:

0 commit comments

Comments
 (0)