From 20e01d231c082d253ee0179afdf9e5057bf1d096 Mon Sep 17 00:00:00 2001 From: Ashija <81858000+AhegaoZKun@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:05:45 +0200 Subject: [PATCH 1/2] Fix README formatting by removing incomplete bullet Removed incomplete bullet point from requirements section. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 37cf0df..2fd7352 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,6 @@ Adding a new term is simple. Edit the relevant `addon/BabelChat/Data/*.lua` file - **Linux Compositor** – Requires compositor to support layer-shell (GNOME doesn't) - **DeepL Free limit** — 500K chars/month (~10K messages). Paid plans available - **Outgoing messages** — copy → paste in WoW chat (by design, ToS compliance) -- ** ## Tech Stack From 85687c09106868475e63cc2508c03eb9a5ff7d9a Mon Sep 17 00:00:00 2001 From: Ashija <81858000+AhegaoZKun@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:39:50 +0200 Subject: [PATCH 2/2] Fixed an issue where the setup wizard on linux would not update the shown language when the dropdown box was selected to something other than russian. (It used to stay russian despite the selection.) Added a function to find the users locale and use that as the shown language first, fallsback to russian if the locale can't be read or doesn't map the three supported languages. --- app/main_gtk.py | 48 +++++++++++++++++++--- app/setup_wizard_gtk.py | 88 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 124 insertions(+), 12 deletions(-) diff --git a/app/main_gtk.py b/app/main_gtk.py index d8ff38c..585eb53 100644 --- a/app/main_gtk.py +++ b/app/main_gtk.py @@ -61,6 +61,30 @@ def _build_pipeline_config(config: AppConfig) -> PipelineConfig: ) +def _guess_locale_lang() -> str: + """Tries to find the user's locale from the OS. + + Falls back to RU if the locale can't be read + or doesn't map to a supported language. + """ + import locale + + for env_var in ("LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"): + val = os.environ.get(env_var, "") + if val: + code = val.split(".")[0].split("_")[0].upper() + if code in ("RU", "EN", "ES"): + return code + try: + loc = locale.getlocale()[0] or "" + code = loc.split("_")[0].upper() + if code in ("RU", "EN", "ES"): + return code + except Exception: # noqa: BLE001 + pass + return "RU" + + def main() -> int: load_dotenv() logging.basicConfig( @@ -72,20 +96,29 @@ def main() -> int: # Off unless asked for: it records every chat line in full. debug_log.configure(config.debug_capture_trace) - # The Qt entry point has always done this and the GTK one never did, so - # every Linux user got the default interface language regardless of what - # they picked in Settings — and the default is Russian. - tr.set_language(config.ui_language) - # First run: no config file yet, or no translation API configured → # run the setup wizard (its own blocking GTK loop) before normal startup. if not os.path.exists(CONFIG_FILE) or not any_configured(config.providers): + # Give the wizard itself a reasonable display language BEFORE it opens. + # It has no config yet to base this on (config.ui_language is just the + # RU default, not a real signal), so guess from the OS locale instead — + # the wizard's own on-screen text otherwise defaults to Russian for + # every first-run user regardless of their system language. + tr.set_language(_guess_locale_lang()) + from app.setup_wizard_gtk import run_setup_wizard config = run_setup_wizard(config) if config is None: # user closed the wizard without finishing return 0 + # The Qt entry point has always done this and the GTK one never did, so + # every Linux user got the default interface language regardless of what + # they picked — and the default is Russian. Must run AFTER the wizard: + # applying it first used the pre-wizard config and ignored the language + # the user had just chosen on first run. + tr.set_language(config.ui_language) + overlay = ChatOverlayGtk(config) # Reply translator (outgoing): default EN unless own language is EN. @@ -124,6 +157,9 @@ def _open_settings() -> None: def _on_saved(updated: AppConfig) -> None: # Apply live: rebuild pipeline config (channels/langs). pipeline.update_config(_build_pipeline_config(updated)) + # If this save is what FIRST configured a provider, this is the + # signal the startup failsafe was waiting on, start scanning now. + _start_pipeline_if_ready(updated) # Rebuild the reply translator so API key/priority changes take # effect without a restart. new_translator = TranslatorService.from_config(updated) @@ -192,7 +228,7 @@ def _toggle_translation(enabled: bool) -> None: except Exception: # noqa: BLE001 logging.exception("history load failed (continuing without it)") - pipeline.start() + _start_pipeline_if_ready(config) try: return overlay.run() except KeyboardInterrupt: diff --git a/app/setup_wizard_gtk.py b/app/setup_wizard_gtk.py index 68f6dd3..2765c71 100644 --- a/app/setup_wizard_gtk.py +++ b/app/setup_wizard_gtk.py @@ -43,7 +43,7 @@ class _WizardWindow(Gtk.ApplicationWindow): def __init__(self, app: Gtk.Application, config: AppConfig, result: dict) -> None: - super().__init__(application=app, title="BabelChat Setup") + super().__init__(application=app, title=tr("wizard.title")) self._config = config self._result = result # {"config": AppConfig|None} self.set_default_size(520, 480) @@ -51,10 +51,7 @@ def __init__(self, app: Gtk.Application, config: AppConfig, result: dict) -> Non self._stack = Gtk.Stack() self._stack.set_vexpand(True) self._pages: list[Gtk.Widget] = [] - for builder in (self._page_welcome, self._page_api, self._page_wow, self._page_langs, self._page_ready): - page = builder() - self._pages.append(page) - self._stack.add_child(page) + self._build_pages() self._index = 0 # Nav bar @@ -79,6 +76,77 @@ def __init__(self, app: Gtk.Application, config: AppConfig, result: dict) -> Non self.set_child(root) self._sync_nav() + # ── page (re)building ──────────────────────────────────────────────── + def _build_pages(self) -> None: + """(Re)build all pages from scratch so tr() text reflects the current + language. Called on init and again whenever the interface-language + dropdown changes, so the wizard updates live rather than only on the + NEXT run.""" + snapshot = self._snapshot_fields() if self._pages else None + + for page in self._pages: + self._stack.remove(page) + self._pages = [] + + for builder in (self._page_welcome, self._page_api, self._page_wow, + self._page_langs, self._page_ready): + page = builder() + self._pages.append(page) + self._stack.add_child(page) + + if snapshot is not None: + self._restore_fields(snapshot) + self._stack.set_visible_child(self._pages[self._index]) + + def _snapshot_fields(self) -> dict: + """Capture whatever the user has already entered, so switching the + interface language mid-wizard (a rebuild) doesn't lose it.""" + snap: dict = { + "ui_lang": self._dd_code(self._ui_lang), + "priority": self._dd_code(self._priority), + "wow_path": self._wow_entry.get_text(), + "own_lang": self._dd_code(self._own_lang), + "target_lang": self._dd_code(self._target_lang), + "providers": { + pid: {key: entry.get_text() for key, entry in fields.items()} + for pid, fields in self._provider_entries.items() + }, + } + return snap + + def _restore_fields(self, snap: dict) -> None: + self._set_dd_code(self._ui_lang, snap["ui_lang"]) + self._set_dd_code(self._priority, snap["priority"]) + self._wow_entry.set_text(snap["wow_path"]) + self._set_dd_code(self._own_lang, snap["own_lang"]) + self._set_dd_code(self._target_lang, snap["target_lang"]) + for pid, values in snap["providers"].items(): + fields = self._provider_entries.get(pid, {}) + for key, text in values.items(): + entry = fields.get(key) + if entry is not None: + entry.set_text(text) + + @staticmethod + def _set_dd_code(dd: Gtk.DropDown, code: str) -> None: + codes = getattr(dd, "_codes", []) + try: + dd.set_selected(codes.index(code)) + except ValueError: + pass + + def _on_ui_lang_changed(self, dd: Gtk.DropDown, _param: object) -> None: + code = self._dd_code(dd) + if code == tr.get_language(): + return + tr.set_language(code) + self._config.ui_language = code + # Rebuild so every page (including this one) re-renders in the new + # language immediately, instead of only taking effect next launch. + self._build_pages() + self._sync_nav() + self.set_title(tr("wizard.title")) + # ── navigation ──────────────────────────────────────────────────────── def _go(self, delta: int) -> None: self._index = max(0, min(len(self._pages) - 1, self._index + delta)) @@ -88,6 +156,7 @@ def _go(self, delta: int) -> None: def _sync_nav(self) -> None: last = self._index == len(self._pages) - 1 self._back.set_sensitive(self._index > 0) + self._back.set_label(tr("wizard.back")) self._next.set_label(tr("wizard.start") if last else tr("wizard.next")) # Same step names the Qt wizard uses, from the one string that holds # them; the key wants a name as well as the numbers. @@ -146,7 +215,14 @@ def _page_welcome(self) -> Gtk.Widget: ) row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) row.append(self._body(tr("wizard.welcome.ui_lang"))) - self._ui_lang = self._dropdown(_UI_LANGS, self._config.ui_language or "EN") + # Source the selection from tr (what's actually on screen right now), + # not self._config.ui_language directly — before anything is saved, + # config.ui_language is just its raw default and can disagree with + # the language the page text is actually rendered in (e.g. the + # locale-guessed language on first open), showing a dropdown value + # that doesn't match what the user is looking at. + self._ui_lang = self._dropdown(_UI_LANGS, tr.get_language()) + self._ui_lang.connect("notify::selected", self._on_ui_lang_changed) row.append(self._ui_lang) box.append(row) return box