Merge pull request #46 from senolsun/fix-live-language-switch

Rebuild the settings window when the language changes
This commit is contained in:
Yusuf İpek
2026-08-27 15:25:34 +03:00
committed by GitHub
4 changed files with 85 additions and 9 deletions
+43 -4
View File
@@ -1103,18 +1103,57 @@ class Dikte:
def open_settings(self): def open_settings(self):
if self.settings_window is None: if self.settings_window is None:
self.settings_window = SettingsWindow(self.conf, self.meetings) self._make_settings()
self.settings_window.applied.connect(self._apply_settings)
self.settings_window.update_found.connect(self._found_update)
self.settings_window.finished.connect(self._settings_closed)
self.settings_window.show() self.settings_window.show()
self.settings_window.raise_() self.settings_window.raise_()
self.settings_window.activateWindow() 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, *_): def _settings_closed(self, *_):
# Don't drop the object while its own signal is still being delivered. # Don't drop the object while its own signal is still being delivered.
QTimer.singleShot(0, lambda: setattr(self, "settings_window", None)) 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()
# 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._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): def _apply_local(self):
"""Pass the local settings on, and hold the models ready if asked to. """Pass the local settings on, and hold the models ready if asked to.
-2
View File
@@ -162,8 +162,6 @@ TR = {
"Automatic (system)": "Otomatik (sistem)", "Automatic (system)": "Otomatik (sistem)",
"Turkish": "Türkçe", "Turkish": "Türkçe",
"English": "İngilizce", "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", "Microphone": "Mikrofon",
"Default microphone": "Varsayılan mikrofon", "Default microphone": "Varsayılan mikrofon",
"Speech language": "Konuşma dili", "Speech language": "Konuşma dili",
+27 -3
View File
@@ -23,6 +23,7 @@ from . import filetranscribe
from . import ggml from . import ggml
from . import hotkey from . import hotkey
from . import hub from . import hub
from . import i18n
from . import ipc from . import ipc
from . import meeting from . import meeting
from . import paste from . import paste
@@ -515,6 +516,10 @@ class LocalModelBox(QGroupBox):
class SettingsWindow(QDialog): class SettingsWindow(QDialog):
applied = pyqtSignal() 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()
# A newer release this window's own check found, so that the tray icon # A newer release this window's own check found, so that the tray icon
# hears about it from here rather than waiting for its own next check. # hears about it from here rather than waiting for its own next check.
update_found = pyqtSignal(object) update_found = pyqtSignal(object)
@@ -541,6 +546,8 @@ class SettingsWindow(QDialog):
self._key_fields = {} self._key_fields = {}
self._testers = {} self._testers = {}
self._shown_provider = "" self._shown_provider = ""
# What _save compares against to know a rebuild is due.
self._built_language = i18n.language()
# Where "Open the release page" goes: the release itself once a check # Where "Open the release page" goes: the release itself once a check
# has named one, and the page that redirects to the newest until then. # has named one, and the page that redirects to the newest until then.
self._release_url = update.RELEASES_PAGE self._release_url = update.RELEASES_PAGE
@@ -639,9 +646,6 @@ class SettingsWindow(QDialog):
self.ui_language = QComboBox() self.ui_language = QComboBox()
for label, code in UI_LANGUAGES: for label, code in UI_LANGUAGES:
self.ui_language.addItem(t(label), code) 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) form.addRow(t("Interface language"), self.ui_language)
self.mic = QComboBox() self.mic = QComboBox()
@@ -1794,7 +1798,27 @@ class SettingsWindow(QDialog):
print(f"dikte: could not trim the history ({exc})") print(f"dikte: could not trim the history ({exc})")
self._load_history() # the trim may just have dropped rows from the list self._load_history() # the trim may just have dropped rows from the list
self.applied.emit() 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.")) QMessageBox.information(self, t("Dikte Settings"), t("Saved successfully."))
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 @staticmethod
def _select_data(combo, value): def _select_data(combo, value):
+15
View File
@@ -240,6 +240,21 @@ class Settings(DikteTest):
with self.subTest(key=key): with self.subTest(key=key):
self.assertEqual(stored[key], value) 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): 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.""" """An OpenRouter id and a Claude alias are not the same field."""
window = self.window(cfg.Config()) window = self.window(cfg.Config())