[client/ui] Introduce localisation (i18n + preferences) feature packages

Adds a tray + React translation pipeline driven by a single JSON locale
tree (frontend/src/i18n/locales) embedded into the Go binary. The tray
re-renders on language switch via a Localizer that subscribes to the
preferences store.

Layout:
- client/ui/i18n: Bundle, LanguageCode, Language, errors, embedded-FS
  loader. Pure domain, no Wails/daemon deps.
- client/ui/preferences: Store + UIPreferences for user-scope UI state,
  persisted under os.UserConfigDir()/netbird/ui-preferences.json with
  atomic writes and a subscribe/broadcast channel.
- client/ui/services: thin Wails-binding facades (services.I18n,
  services.Preferences) so React sees ctx-first signatures.
- client/ui/localizer.go: tray bridge that owns the active language,
  exposes T()/StatusLabel() and re-paints the menu on prefs change.
- tray.go: every user-facing const replaced by translation keys via
  t.loc.T(...); menu rebuild + state replay on language switch.
- main.go: //go:embed all:frontend/src/i18n/locales, wires Bundle ->
  Store -> Localizer -> Wails facades in order.

Frontend API exposed via Wails bindings: I18n.Languages, I18n.Bundle,
Preferences.Get, Preferences.SetLanguage, plus the
netbird:preferences:changed event.

Includes regenerated Wails TS bindings (peers/profileswitcher/etc.
re-emitted as part of the build) and en/hu seed bundles.
This commit is contained in:
Zoltan Papp
2026-05-15 11:08:19 +02:00
parent c0b0eeb6ab
commit 17cae1a75c
18 changed files with 1449 additions and 108 deletions

View File

@@ -0,0 +1,34 @@
//go:build !android && !ios && !freebsd && !js
package services
import (
"context"
"github.com/netbirdio/netbird/client/ui/i18n"
)
// I18n is the Wails-bound facade over i18n.Bundle. It exists only to give
// the binding generator a service type with the context.Context-first
// signatures it expects; the translation logic, locale loading and the
// LanguageCode type all live in client/ui/i18n.
type I18n struct {
bundle *i18n.Bundle
}
func NewI18n(bundle *i18n.Bundle) *I18n {
return &I18n{bundle: bundle}
}
// Languages exposes the list of shipped locales to the frontend so the
// settings page can populate its language picker.
func (s *I18n) Languages(_ context.Context) ([]i18n.Language, error) {
return s.bundle.Languages(), nil
}
// Bundle returns the full key->text map for one language, letting the
// React side drive its own translation library (i18next, etc.) off the
// same source bundles the tray uses.
func (s *I18n) Bundle(_ context.Context, code i18n.LanguageCode) (map[string]string, error) {
return s.bundle.BundleFor(code)
}

View File

@@ -0,0 +1,32 @@
//go:build !android && !ios && !freebsd && !js
package services
import (
"context"
"github.com/netbirdio/netbird/client/ui/i18n"
"github.com/netbirdio/netbird/client/ui/preferences"
)
// Preferences is the Wails-bound facade over preferences.Store. The store
// itself owns persistence and the subscription channel; this type just
// re-exposes Get and SetLanguage with the context.Context-first signature
// the Wails binding generator wants.
type Preferences struct {
store *preferences.Store
}
func NewPreferences(store *preferences.Store) *Preferences {
return &Preferences{store: store}
}
// Get returns the current user-scope preferences.
func (s *Preferences) Get(_ context.Context) (preferences.UIPreferences, error) {
return s.store.Get(), nil
}
// SetLanguage validates and persists a new UI language.
func (s *Preferences) SetLanguage(_ context.Context, lang i18n.LanguageCode) error {
return s.store.SetLanguage(lang)
}