From c7b14ff84ef1e3ecddae49400b7cb17a80310c07 Mon Sep 17 00:00:00 2001 From: senolsun <5411207+senolsun@users.noreply.github.com> Date: Tue, 18 Aug 2026 00:27:09 +0300 Subject: [PATCH 1/2] Rebuild the settings window when the language changes Saving already switched the language everywhere strings are made at the moment they are shown: the tray is rebuilt, the indicator and the message box translate as they speak. The settings window is the one place written once, at construction, so the window that took the new language was the one place still showing the old one, and a tooltip asked for a restart. Now the window remembers the language it was built in, and a save that changed it has the app replace the window: a fresh one comes up where the old one stood, on the same tab. The restart tooltip goes, having nothing left to excuse. Co-Authored-By: Claude Fable 5 --- dikte/app.py | 26 ++++++++++++++++++++++++++ dikte/i18n.py | 2 -- dikte/settings_ui.py | 17 ++++++++++++++--- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/dikte/app.py b/dikte/app.py index a098199..33933f1 100644 --- a/dikte/app.py +++ b/dikte/app.py @@ -903,6 +903,7 @@ class Dikte: if self.settings_window is None: self.settings_window = SettingsWindow(self.conf, self.meetings) self.settings_window.applied.connect(self._apply_settings) + self.settings_window.language_changed.connect(self._reopen_settings) self.settings_window.finished.connect(self._settings_closed) self.settings_window.show() self.settings_window.raise_() @@ -912,6 +913,31 @@ class Dikte: # Don't drop the object while its own signal is still being delivered. QTimer.singleShot(0, lambda: setattr(self, "settings_window", None)) + def _reopen_settings(self): + """Replace the settings window, so a language change reaches it too. + + A save switches the language everywhere strings are made at the moment + they are shown: the tray is rebuilt, the indicator and the message box + translate as they speak. The settings window is the one place written + once, at construction, so the window that took the new language is the + one place still showing the old one. A fresh window comes up where the + old one stood, on the same tab. + """ + old = self.settings_window + if old is None: + return + tab = old.tabs.currentIndex() + geometry = old.geometry() + # Replaced rather than merely closed: left connected, _settings_closed + # would drop the reference to the new window a moment after it is made. + old.finished.disconnect(self._settings_closed) + old.close() + old.deleteLater() + self.settings_window = None + self.open_settings() + self.settings_window.tabs.setCurrentIndex(tab) + self.settings_window.setGeometry(geometry) + def _apply_local(self): """Pass the local settings on, and hold the models ready if asked to. diff --git a/dikte/i18n.py b/dikte/i18n.py index 6a29fec..c5dc63a 100644 --- a/dikte/i18n.py +++ b/dikte/i18n.py @@ -158,8 +158,6 @@ TR = { "Automatic (system)": "Otomatik (sistem)", "Turkish": "Türkçe", "English": "İngilizce", - "Restart Dikte for the language change to reach every window.": - "Dil değişikliğinin her pencereye işlemesi için Dikte'yi yeniden başlat.", "Microphone": "Mikrofon", "Default microphone": "Varsayılan mikrofon", "Speech language": "Konuşma dili", diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index 1a73968..eccda97 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -21,6 +21,7 @@ from . import config as cfg from . import filetranscribe from . import ggml from . import hotkey +from . import i18n from . import ipc from . import meeting from . import paste @@ -512,6 +513,10 @@ class LocalModelBox(QGroupBox): class SettingsWindow(QDialog): applied = pyqtSignal() + # A save changed the interface language. Every label below is translated + # as the window is built, so the change cannot reach this window: the + # owner replaces it with a fresh one instead. + language_changed = pyqtSignal() _models_loaded = pyqtSignal(list, str) _transcribe_models_loaded = pyqtSignal(list, str) @@ -533,6 +538,8 @@ class SettingsWindow(QDialog): self._key_fields = {} self._testers = {} self._shown_provider = "" + # What _save compares against to know a rebuild is due. + self._built_language = i18n.language() self.transcriber = FileTranscriber(conf, self) self.setWindowTitle(t("Dikte Settings")) @@ -627,9 +634,6 @@ class SettingsWindow(QDialog): self.ui_language = QComboBox() for label, code in UI_LANGUAGES: self.ui_language.addItem(t(label), code) - self.ui_language.setToolTip( - t("Restart Dikte for the language change to reach every window.") - ) form.addRow(t("Interface language"), self.ui_language) self.mic = QComboBox() @@ -1744,7 +1748,14 @@ class SettingsWindow(QDialog): print(f"dikte: could not trim the history ({exc})") self._load_history() # the trim may just have dropped rows from the list self.applied.emit() + # conf.save() has switched the language t() speaks, so the message box + # already answers in the new one; the labels around it were translated + # when the window was built and stay behind. Asking for the rebuild + # waits until the box is dismissed, so the window is not pulled out + # from under a dialog it is holding up. QMessageBox.information(self, t("Dikte Settings"), t("Saved successfully.")) + if i18n.language() != self._built_language: + self.language_changed.emit() @staticmethod def _select_data(combo, value): From a3bb5ed29baa6d9030b429d1054bc54e2716ac47 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Thu, 27 Aug 2026 15:13:54 +0300 Subject: [PATCH 2/2] Keep the rebuild away from a window a thread is still writing to deleteLater destroyed a window whose model boxes a daemon thread was still reporting into: a running download died with a RuntimeError, the .part stayed on disk and the UI said nothing. Dropping the reference is how the ordinary close path lets a window go, and the closures a thread holds keep the object alive until it is done, so the rebuild now does the same. While the transcriber or either model box is working the rebuild is skipped altogether; _built_language keeps the old language, so the next quiet save asks for it by itself. The new window is also placed and turned to the old tab before it is shown, so it no longer comes up at the default size and jumps. And tests/test_ui.py now says that a save with a changed language emits language_changed, and one without does not. --- dikte/app.py | 29 +++++++++++++++++++++-------- dikte/settings_ui.py | 15 ++++++++++++++- tests/test_ui.py | 15 +++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/dikte/app.py b/dikte/app.py index d82cc7c..5bf335b 100644 --- a/dikte/app.py +++ b/dikte/app.py @@ -1103,15 +1103,20 @@ class Dikte: def open_settings(self): if self.settings_window is None: - self.settings_window = SettingsWindow(self.conf, self.meetings) - self.settings_window.applied.connect(self._apply_settings) - self.settings_window.language_changed.connect(self._reopen_settings) - self.settings_window.update_found.connect(self._found_update) - self.settings_window.finished.connect(self._settings_closed) + self._make_settings() self.settings_window.show() self.settings_window.raise_() self.settings_window.activateWindow() + def _make_settings(self): + """Build the window without showing it, so a caller that knows where + it belongs can place it first.""" + self.settings_window = SettingsWindow(self.conf, self.meetings) + self.settings_window.applied.connect(self._apply_settings) + self.settings_window.language_changed.connect(self._reopen_settings) + self.settings_window.update_found.connect(self._found_update) + self.settings_window.finished.connect(self._settings_closed) + def _settings_closed(self, *_): # Don't drop the object while its own signal is still being delivered. QTimer.singleShot(0, lambda: setattr(self, "settings_window", None)) @@ -1135,11 +1140,19 @@ class Dikte: # would drop the reference to the new window a moment after it is made. old.finished.disconnect(self._settings_closed) old.close() - old.deleteLater() + # No deleteLater: a daemon thread of the old window's may still be + # running, and a closure holding self is what keeps the object alive + # until the thread is done. Dropping the reference is how the ordinary + # close path lets a window go, and it is enough here too. self.settings_window = None - self.open_settings() - self.settings_window.tabs.setCurrentIndex(tab) + self._make_settings() + # Placed and turned to the old tab before it is shown, so the new + # window does not come up at the default size and jump. self.settings_window.setGeometry(geometry) + self.settings_window.tabs.setCurrentIndex(tab) + self.settings_window.show() + self.settings_window.raise_() + self.settings_window.activateWindow() def _apply_local(self): """Pass the local settings on, and hold the models ready if asked to. diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index 276efb0..f166140 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -1804,9 +1804,22 @@ class SettingsWindow(QDialog): # waits until the box is dismissed, so the window is not pulled out # from under a dialog it is holding up. QMessageBox.information(self, t("Dikte Settings"), t("Saved successfully.")) - if i18n.language() != self._built_language: + if i18n.language() != self._built_language and not self._work_in_flight(): self.language_changed.emit() + def _work_in_flight(self): + """A daemon thread of this window's is still running. + + Replacing the window now would let it be collected, taking the C++ + side of the model boxes down with it, and the thread's next progress + report would land on a deleted object. The stale labels stand until a + later save finds the window quiet; _built_language keeps the old + language, so that save asks for the rebuild by itself. + """ + return (self.transcriber.busy + or self.local_whisper._downloading + or self.local_llm._downloading) + @staticmethod def _select_data(combo, value): index = combo.findData(value) diff --git a/tests/test_ui.py b/tests/test_ui.py index 867e040..060686a 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -240,6 +240,21 @@ class Settings(DikteTest): with self.subTest(key=key): self.assertEqual(stored[key], value) + def test_only_a_save_that_changed_the_language_says_so(self): + # The owner answers language_changed by replacing the window, so a + # save that left the language alone must keep quiet. + i18n = settings_ui.i18n + self.addCleanup(i18n.set_language, i18n.language()) + window = self.window(cfg.Config()) + heard = [] + window.language_changed.connect(lambda: heard.append(True)) + window._save() + self.assertEqual(heard, []) + other = "en" if i18n.language() == "tr" else "tr" + window._select_data(window.ui_language, other) + window._save() + self.assertEqual(heard, [True]) + def test_the_model_box_on_screen_belongs_to_whoever_cleans_up(self): """An OpenRouter id and a Claude alias are not the same field.""" window = self.window(cfg.Config())