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