Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions app/main_gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
88 changes: 82 additions & 6 deletions app/setup_wizard_gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,15 @@

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)

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
Expand All @@ -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))
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Loading