localize window titles, fix size for windows (and other platforms?)

This commit is contained in:
Eduard Gert
2026-05-29 16:58:08 +02:00
parent fb6138a3ba
commit 558769e671
7 changed files with 118 additions and 11 deletions

View File

@@ -1,4 +1,6 @@
import { ReactNode, forwardRef } from "react";
import {cn} from "@/lib/cn.ts";
import {isMacOS} from "@/lib/platform.ts";
// ConfirmDialog is the shared layout wrapper used by dialog-style window
// surfaces (SessionExpired, SessionAboutToExpire, …). Purely a layout
@@ -23,7 +25,7 @@ export const ConfirmDialog = forwardRef<HTMLDivElement, ConfirmDialogProps>(
>
<div
ref={ref}
className={"flex flex-col items-center gap-5 p-8 text-center"}
className={cn("flex flex-col items-center gap-5 p-8 text-center", !isMacOS() && "pt-4")}
>
{children}
</div>

View File

@@ -1,5 +1,6 @@
import { useLayoutEffect, useRef } from "react";
import { Window } from "@wailsio/runtime";
import {isMacOS} from "@/lib/platform.ts";
// useAutoSizeWindow resizes the current Wails window so its height matches
// the measured height of the content element the returned ref is attached
@@ -21,7 +22,8 @@ export function useAutoSizeWindow<T extends HTMLElement>(width: number) {
if (!el) return;
let shown = false;
const apply = () => {
const h = Math.ceil(el.getBoundingClientRect().height);
let h = Math.ceil(el.getBoundingClientRect().height);
h = isMacOS() ? h : h - el.getBoundingClientRect().height;
if (h <= 0) return;
void Window.SetSize(width, h)
.then(() => {

View File

@@ -294,6 +294,12 @@
"update.page.complete": "Update abgeschlossen",
"update.page.failed": "Update fehlgeschlagen",
"window.title.settings": "Einstellungen",
"window.title.signIn": "Anmeldung",
"window.title.sessionExpired": "Sitzung abgelaufen",
"window.title.sessionExpiring": "Sitzung läuft ab",
"window.title.updating": "Aktualisierung",
"browserLogin.title": "Setzen Sie den Anmeldevorgang im Browser fort",
"browserLogin.notSeeing": "Sehen Sie den Browser-Tab nicht?",
"browserLogin.tryAgain": "Erneut versuchen",

View File

@@ -312,6 +312,12 @@
"update.page.complete": "Update complete",
"update.page.failed": "Update failed",
"window.title.settings": "Settings",
"window.title.signIn": "Sign-in",
"window.title.sessionExpired": "Session Expired",
"window.title.sessionExpiring": "Session Expiring",
"window.title.updating": "Updating",
"browserLogin.title": "Continue in your browser to complete the login",
"browserLogin.notSeeing": "Not seeing the browser tab?",
"browserLogin.tryAgain": "Try again",

View File

@@ -294,6 +294,12 @@
"update.page.complete": "Frissítés kész",
"update.page.failed": "Frissítés sikertelen",
"window.title.settings": "Beállítások",
"window.title.signIn": "Bejelentkezés",
"window.title.sessionExpired": "Munkamenet lejárt",
"window.title.sessionExpiring": "Munkamenet lejár",
"window.title.updating": "Frissítés",
"browserLogin.title": "Folytassa a böngészőben a bejelentkezés befejezéséhez",
"browserLogin.notSeeing": "Nem látja a böngésző fülét?",
"browserLogin.tryAgain": "Próbálja újra",

View File

@@ -140,7 +140,8 @@ func main() {
// (BrowserLogin, Session*, InstallProgress) stay lazy + destroy-on-close
// so they don't linger as hidden windows that Wails's macOS dock-reopen
// handler would pop back up.
windowManager := services.NewWindowManager(app, window)
windowManager := services.NewWindowManager(app, window, bundle, prefStore)
windowManager.WatchLanguage(prefStore)
app.RegisterService(application.NewService(windowManager))
// Register an in-process StatusNotifierWatcher so the tray works on

View File

@@ -9,8 +9,20 @@ import (
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/netbirdio/netbird/client/ui/i18n"
"github.com/netbirdio/netbird/client/ui/preferences"
)
// LanguageSubscriber delivers UI preference changes (currently only the
// language flip; reusing preferences.UIPreferences keeps the channel
// payload identical to preferences.Store.Subscribe). The runtime
// implementation is *preferences.Store. WindowManager uses this to keep
// the long-lived Settings window title in the active language.
type LanguageSubscriber interface {
Subscribe() (<-chan preferences.UIPreferences, func())
}
// EventTriggerLogin asks the frontend's startLogin() orchestrator to begin
// an SSO flow. Emitted by the tray (Login menu item, session expired) since
// the tray can't call JS directly.
@@ -118,6 +130,8 @@ func DialogWindowOptions(name, title, url string) application.WebviewWindowOptio
type WindowManager struct {
app *application.App
mainWindow *application.WebviewWindow
translator ErrorTranslator
prefs LanguagePreference
settings *application.WebviewWindow
browserLogin *application.WebviewWindow
sessionExpired *application.WebviewWindow
@@ -131,19 +145,39 @@ type WindowManager struct {
mu sync.Mutex
}
// title resolves a window-title i18n key in the user's current language.
// Falls back to the raw key when the translator or prefs are missing
// (mirrors services.Connection.translateShort) — a deliberate fail-loud
// signal that a key is missing from the bundle.
func (s *WindowManager) title(key string) string {
if s.translator == nil {
return key
}
lang := i18n.DefaultLanguage
if s.prefs != nil {
if pref := s.prefs.Get().Language; pref != "" {
lang = pref
}
}
return s.translator.Translate(lang, key)
}
// NewWindowManager wires the manager to the main app. `mainWindow` is the
// up-front-created webview the user interacts with from the tray — used to
// pick the BrowserLogin window's display so the sign-in popup follows the
// user onto the screen they're already looking at.
// user onto the screen they're already looking at. `translator` + `prefs`
// resolve the user-facing window titles in the active UI language; both
// may be nil (callers in tests can omit them), in which case title() falls
// back to the raw i18n key.
//
// The Settings window is created here, hidden, so the first OpenSettings
// call paints instantly instead of paying webview construction + asset load
// at click time.
func NewWindowManager(app *application.App, mainWindow *application.WebviewWindow) *WindowManager {
s := &WindowManager{app: app, mainWindow: mainWindow}
func NewWindowManager(app *application.App, mainWindow *application.WebviewWindow, translator ErrorTranslator, prefs LanguagePreference) *WindowManager {
s := &WindowManager{app: app, mainWindow: mainWindow, translator: translator, prefs: prefs}
s.settings = app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Title: s.title("window.title.settings"),
Width: 900,
Height: 640,
Hidden: true,
@@ -169,6 +203,56 @@ func NewWindowManager(app *application.App, mainWindow *application.WebviewWindo
return s
}
// WatchLanguage subscribes to UI preference changes and re-applies the
// localised title to every live auxiliary window whenever the language
// flips. The eagerly-created Settings window outlives its first paint, so
// without this its title would stay frozen in the language it was created
// in; the on-demand dialog windows (BrowserLogin, Session*, InstallProgress)
// can also coexist with a Settings-driven language change. Safe to call
// once at startup; subsequent calls overwrite the previous subscription.
// No-op when sub is nil.
func (s *WindowManager) WatchLanguage(sub LanguageSubscriber) {
if sub == nil {
return
}
ch, _ := sub.Subscribe()
go func() {
var last i18n.LanguageCode
for p := range ch {
if p.Language == "" || p.Language == last {
continue
}
last = p.Language
s.retitleAll()
}
}()
}
// retitleAll re-applies the localised title to every currently-alive
// auxiliary window. Reads the window pointers under s.mu so a concurrent
// Open*/Close* can't observe a torn slice. SetTitle itself dispatches to
// the OS UI thread, so calling it from this goroutine is safe.
func (s *WindowManager) retitleAll() {
s.mu.Lock()
type pair struct {
win *application.WebviewWindow
key string
}
wins := []pair{
{s.settings, "window.title.settings"},
{s.browserLogin, "window.title.signIn"},
{s.sessionExpired, "window.title.sessionExpired"},
{s.sessionAboutToExpire, "window.title.sessionExpiring"},
{s.installProgress, "window.title.updating"},
}
s.mu.Unlock()
for _, p := range wins {
if p.win != nil {
p.win.SetTitle(s.title(p.key))
}
}
}
// OpenSettings asks the (already-mounted, currently-hidden) settings window
// to land on `tab` and bring itself to front. Empty `tab` lands on General.
//
@@ -211,7 +295,7 @@ func (s *WindowManager) OpenBrowserLogin(uri string) {
screen = sc
}
}
opts := DialogWindowOptions("browser-login", "NetBird Sign-in", startURL)
opts := DialogWindowOptions("browser-login", s.title("window.title.signIn"), startURL)
// SSO popup deliberately is NOT always-on-top — the user moves
// between the browser tab and our popup; pinning it would obscure
// the browser at the moment they need to interact with it.
@@ -321,7 +405,7 @@ func (s *WindowManager) OpenSessionExpired() {
defer s.mu.Unlock()
if s.sessionExpired == nil {
s.sessionExpired = s.app.Window.NewWithOptions(
DialogWindowOptions("session-expired", "NetBird", "/#/dialog/session-expired"),
DialogWindowOptions("session-expired", s.title("window.title.sessionExpired"), "/#/dialog/session-expired"),
)
s.sessionExpired.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
s.mu.Lock()
@@ -356,7 +440,7 @@ func (s *WindowManager) OpenSessionAboutToExpire(seconds int) {
startURL := "/#/dialog/session-about-to-expire?seconds=" + strconv.Itoa(seconds)
if s.sessionAboutToExpire == nil {
s.sessionAboutToExpire = s.app.Window.NewWithOptions(
DialogWindowOptions("session-about-to-expire", "NetBird", startURL),
DialogWindowOptions("session-about-to-expire", s.title("window.title.sessionExpiring"), startURL),
)
s.sessionAboutToExpire.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
s.mu.Lock()
@@ -402,7 +486,7 @@ func (s *WindowManager) OpenInstallProgress(version string) {
if s.installProgress == nil {
s.hideOtherWindowsLocked("install-progress")
s.installProgress = s.app.Window.NewWithOptions(
DialogWindowOptions("install-progress", "NetBird", startURL),
DialogWindowOptions("install-progress", s.title("window.title.updating"), startURL),
)
s.installProgress.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
s.mu.Lock()