mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-16 21:59:56 +00:00
[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:
@@ -0,0 +1,64 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* I18n holds the embedded translation bundles and serves them to both the
|
||||
* tray (Translate, used on every menu rebuild) and the frontend (Bundle,
|
||||
* optional Wails-bound endpoint for runtime fetches). The bundles are
|
||||
* loaded once at construction and never mutated.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* Bundle returns the full key->text map for one language. Bound to React via
|
||||
* Wails as an optional endpoint — the frontend can either import the JSON
|
||||
* files directly through Vite or fetch them through this RPC. The returned
|
||||
* map is a copy.
|
||||
*/
|
||||
export function Bundle(code: string): $CancellablePromise<{ [_ in string]?: string }> {
|
||||
return $Call.ByID(1780869897, code).then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* HasLanguage reports whether a bundle is loaded for the given code.
|
||||
* Preferences.SetLanguage uses this to validate input.
|
||||
*/
|
||||
export function HasLanguage(code: string): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(3348861033, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Languages returns the list of available locales. Bound to React via Wails
|
||||
* so the settings page can populate its language selector.
|
||||
*/
|
||||
export function Languages(): $CancellablePromise<$models.Language[]> {
|
||||
return $Call.ByID(768152924).then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate resolves key for the given language with a placeholder pass.
|
||||
* Args must come in {placeholderName, value} pairs (e.g. "version", "1.2.3"
|
||||
* substitutes "{version}"). Unknown keys fall back to the default language;
|
||||
* if even that fails, the key itself is returned (debug-friendly — a missed
|
||||
* key is visible in the UI rather than blank).
|
||||
*/
|
||||
export function Translate(lang: string, key: string, ...args: string[]): $CancellablePromise<string> {
|
||||
return $Call.ByID(2739709937, lang, key, args);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType1 = $models.Language.createFrom;
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
@@ -4,8 +4,10 @@
|
||||
import * as Connection from "./connection.js";
|
||||
import * as Debug from "./debug.js";
|
||||
import * as Forwarding from "./forwarding.js";
|
||||
import * as I18n from "./i18n.js";
|
||||
import * as Networks from "./networks.js";
|
||||
import * as Peers from "./peers.js";
|
||||
import * as Preferences from "./preferences.js";
|
||||
import * as ProfileSwitcher from "./profileswitcher.js";
|
||||
import * as Profiles from "./profiles.js";
|
||||
import * as Settings from "./settings.js";
|
||||
@@ -15,8 +17,10 @@ export {
|
||||
Connection,
|
||||
Debug,
|
||||
Forwarding,
|
||||
I18n,
|
||||
Networks,
|
||||
Peers,
|
||||
Preferences,
|
||||
ProfileSwitcher,
|
||||
Profiles,
|
||||
Settings,
|
||||
@@ -32,6 +36,7 @@ export {
|
||||
DebugBundleResult,
|
||||
Features,
|
||||
ForwardingRule,
|
||||
Language,
|
||||
LocalPeer,
|
||||
LogLevel,
|
||||
LoginParams,
|
||||
@@ -48,6 +53,7 @@ export {
|
||||
SetConfigParams,
|
||||
Status,
|
||||
SystemEvent,
|
||||
UIPreferences,
|
||||
UpParams,
|
||||
UpdateAvailable,
|
||||
UpdateProgress,
|
||||
|
||||
@@ -344,6 +344,41 @@ export class ForwardingRule {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Language describes one shipped UI locale. Code is the BCP-47-ish key the
|
||||
* frontend and the preferences file use; DisplayName is shown in the language
|
||||
* picker in its own script (so a Hungarian user sees "Magyar" even when the
|
||||
* current UI language is English).
|
||||
*/
|
||||
export class Language {
|
||||
"code": string;
|
||||
"displayName": string;
|
||||
"englishName": string;
|
||||
|
||||
/** Creates a new Language instance. */
|
||||
constructor($$source: Partial<Language> = {}) {
|
||||
if (!("code" in $$source)) {
|
||||
this["code"] = "";
|
||||
}
|
||||
if (!("displayName" in $$source)) {
|
||||
this["displayName"] = "";
|
||||
}
|
||||
if (!("englishName" in $$source)) {
|
||||
this["englishName"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Language instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Language {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new Language($$parsedSource as Partial<Language>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalPeer mirrors LocalPeerState — what this client looks like on the mesh.
|
||||
*/
|
||||
@@ -1038,6 +1073,32 @@ export class SystemEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UIPreferences is the user-scope UI state mirrored to disk and to the
|
||||
* frontend. Pointer-free because the whole document is rewritten on every
|
||||
* change — there are no per-field partial updates.
|
||||
*/
|
||||
export class UIPreferences {
|
||||
"language": string;
|
||||
|
||||
/** Creates a new UIPreferences instance. */
|
||||
constructor($$source: Partial<UIPreferences> = {}) {
|
||||
if (!("language" in $$source)) {
|
||||
this["language"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new UIPreferences instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): UIPreferences {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new UIPreferences($$parsedSource as Partial<UIPreferences>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UpParams selects the profile the daemon should bring up.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Preferences is the user-scope UI preferences service. Read at app start,
|
||||
* updated by the React settings page (Wails-bound SetLanguage), and observed
|
||||
* by the tray which re-renders its menu in the new language.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* Get returns a copy of the current preferences. Bound to React via Wails.
|
||||
*/
|
||||
export function Get(): $CancellablePromise<$models.UIPreferences> {
|
||||
return $Call.ByID(3500743391).then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SetLanguage validates and persists a new language preference, then
|
||||
* broadcasts the change to the frontend and to internal subscribers (tray).
|
||||
* Bound to React via Wails.
|
||||
*/
|
||||
export function SetLanguage(lang: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3710099805, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe returns a channel that receives every persisted change. The
|
||||
* unsubscribe function closes the channel and removes it from the list;
|
||||
* callers must not close the channel themselves.
|
||||
*/
|
||||
export function Subscribe(): $CancellablePromise<[any, any]> {
|
||||
return $Call.ByID(2696446779);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.UIPreferences.createFrom;
|
||||
@@ -13,16 +13,18 @@ import * as services$0 from "../../../../netbirdio/netbird/client/ui/services/mo
|
||||
function configure() {
|
||||
Object.freeze(Object.assign($Create.Events, {
|
||||
"netbird:event": $$createType0,
|
||||
"netbird:status": $$createType1,
|
||||
"netbird:update:available": $$createType2,
|
||||
"netbird:update:progress": $$createType3,
|
||||
"netbird:preferences:changed": $$createType1,
|
||||
"netbird:status": $$createType2,
|
||||
"netbird:update:available": $$createType3,
|
||||
"netbird:update:progress": $$createType4,
|
||||
}));
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = services$0.SystemEvent.createFrom;
|
||||
const $$createType1 = services$0.Status.createFrom;
|
||||
const $$createType2 = services$0.UpdateAvailable.createFrom;
|
||||
const $$createType3 = services$0.UpdateProgress.createFrom;
|
||||
const $$createType1 = services$0.UIPreferences.createFrom;
|
||||
const $$createType2 = services$0.Status.createFrom;
|
||||
const $$createType3 = services$0.UpdateAvailable.createFrom;
|
||||
const $$createType4 = services$0.UpdateProgress.createFrom;
|
||||
|
||||
configure();
|
||||
|
||||
@@ -13,6 +13,7 @@ declare module "@wailsio/runtime" {
|
||||
namespace Events {
|
||||
interface CustomEvents {
|
||||
"netbird:event": services$0.SystemEvent;
|
||||
"netbird:preferences:changed": services$0.UIPreferences;
|
||||
"netbird:status": services$0.Status;
|
||||
"netbird:update:available": services$0.UpdateAvailable;
|
||||
"netbird:update:progress": services$0.UpdateProgress;
|
||||
|
||||
6
client/ui/frontend/src/i18n/locales/_index.json
Normal file
6
client/ui/frontend/src/i18n/locales/_index.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"languages": [
|
||||
{"code": "en", "displayName": "English", "englishName": "English"},
|
||||
{"code": "hu", "displayName": "Magyar", "englishName": "Hungarian"}
|
||||
]
|
||||
}
|
||||
34
client/ui/frontend/src/i18n/locales/en/common.json
Normal file
34
client/ui/frontend/src/i18n/locales/en/common.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"tray.tooltip": "NetBird",
|
||||
"tray.status.disconnected": "Disconnected",
|
||||
"tray.status.daemonUnavailable": "Not running",
|
||||
"tray.status.error": "Error",
|
||||
|
||||
"tray.menu.open": "Open NetBird",
|
||||
"tray.menu.connect": "Connect",
|
||||
"tray.menu.disconnect": "Disconnect",
|
||||
"tray.menu.exitNode": "Exit Node",
|
||||
"tray.menu.networks": "Resources",
|
||||
"tray.menu.profiles": "Profiles",
|
||||
"tray.menu.settings": "Settings",
|
||||
"tray.menu.debugBundle": "Create Debug Bundle",
|
||||
"tray.menu.about": "About",
|
||||
"tray.menu.github": "GitHub",
|
||||
"tray.menu.documentation": "Documentation",
|
||||
"tray.menu.downloadLatest": "Download latest version",
|
||||
"tray.menu.installVersion": "Install version {version}",
|
||||
"tray.menu.guiVersion": "GUI: {version}",
|
||||
"tray.menu.daemonVersion": "Daemon: {version}",
|
||||
"tray.menu.versionUnknown": "—",
|
||||
"tray.menu.quit": "Quit",
|
||||
|
||||
"notify.update.title": "NetBird update available",
|
||||
"notify.update.body": "NetBird {version} is available.",
|
||||
"notify.update.enforcedSuffix": " Your administrator requires this update.",
|
||||
"notify.error.title": "Error",
|
||||
"notify.error.connect": "Failed to connect",
|
||||
"notify.error.disconnect": "Failed to disconnect",
|
||||
"notify.error.switchProfile": "Failed to switch to {profile}",
|
||||
"notify.sessionExpired.title": "NetBird session expired",
|
||||
"notify.sessionExpired.body": "Your NetBird session has expired. Please log in again."
|
||||
}
|
||||
34
client/ui/frontend/src/i18n/locales/hu/common.json
Normal file
34
client/ui/frontend/src/i18n/locales/hu/common.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"tray.tooltip": "NetBird",
|
||||
"tray.status.disconnected": "Lekapcsolva",
|
||||
"tray.status.daemonUnavailable": "Nem fut",
|
||||
"tray.status.error": "Hiba",
|
||||
|
||||
"tray.menu.open": "NetBird megnyitása",
|
||||
"tray.menu.connect": "Csatlakozás",
|
||||
"tray.menu.disconnect": "Bontás",
|
||||
"tray.menu.exitNode": "Kilépő csomópont",
|
||||
"tray.menu.networks": "Erőforrások",
|
||||
"tray.menu.profiles": "Profilok",
|
||||
"tray.menu.settings": "Beállítások",
|
||||
"tray.menu.debugBundle": "Hibakeresési csomag készítése",
|
||||
"tray.menu.about": "Névjegy",
|
||||
"tray.menu.github": "GitHub",
|
||||
"tray.menu.documentation": "Dokumentáció",
|
||||
"tray.menu.downloadLatest": "Legfrissebb verzió letöltése",
|
||||
"tray.menu.installVersion": "{version} verzió telepítése",
|
||||
"tray.menu.guiVersion": "Felület: {version}",
|
||||
"tray.menu.daemonVersion": "Daemon: {version}",
|
||||
"tray.menu.versionUnknown": "—",
|
||||
"tray.menu.quit": "Kilépés",
|
||||
|
||||
"notify.update.title": "NetBird frissítés elérhető",
|
||||
"notify.update.body": "Elérhető a NetBird {version}.",
|
||||
"notify.update.enforcedSuffix": " A rendszergazda kötelezővé tette ezt a frissítést.",
|
||||
"notify.error.title": "Hiba",
|
||||
"notify.error.connect": "Csatlakozás sikertelen",
|
||||
"notify.error.disconnect": "Bontás sikertelen",
|
||||
"notify.error.switchProfile": "Átváltás sikertelen erre: {profile}",
|
||||
"notify.sessionExpired.title": "NetBird munkamenet lejárt",
|
||||
"notify.sessionExpired.body": "A NetBird munkamenet lejárt. Kérjük, jelentkezzen be újra."
|
||||
}
|
||||
Reference in New Issue
Block a user