mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-21 08:09:55 +00:00
add i18n to frontend
This commit is contained in:
@@ -13,30 +13,40 @@ import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import { welcome } from "@/lib/welcome";
|
||||
import BrowserLogin from "@/pages/BrowserLogin.tsx";
|
||||
import { initI18n } from "@/lib/i18n";
|
||||
|
||||
welcome();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<SkeletonTheme baseColor={"#25282d"} highlightColor={"#33373e"}>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/quick" element={<QuickActions />} />
|
||||
<Route path="/browser-login" element={<BrowserLogin />} />
|
||||
<Route path="/update" element={<Update />} />
|
||||
<Route path="/session-expired" element={<SessionExpired />} />
|
||||
<Route element={<SettingsLayout />}>
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
<Route element={<AppLayout />}>
|
||||
<Route index element={<Main />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate to={"/"} replace />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</SkeletonTheme>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
initI18n()
|
||||
.catch((e) => {
|
||||
// Surface init failures in the console so a misconfigured glob
|
||||
// doesn't quietly blank the UI; render anyway with i18next in
|
||||
// whatever state it ended up in (t() will fall back to keys).
|
||||
console.error("i18n init failed:", e);
|
||||
})
|
||||
.finally(() => {
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<SkeletonTheme baseColor={"#25282d"} highlightColor={"#33373e"}>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/quick" element={<QuickActions />} />
|
||||
<Route path="/browser-login" element={<BrowserLogin />} />
|
||||
<Route path="/update" element={<Update />} />
|
||||
<Route path="/session-expired" element={<SessionExpired />} />
|
||||
<Route element={<SettingsLayout />}>
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
<Route element={<AppLayout />}>
|
||||
<Route index element={<Main />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate to={"/"} replace />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</SkeletonTheme>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
7
client/ui/frontend/src/assets/flags/1x1/en.svg
Normal file
7
client/ui/frontend/src/assets/flags/1x1/en.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-gb" viewBox="0 0 512 512">
|
||||
<path fill="#012169" d="M0 0h512v512H0z"/>
|
||||
<path fill="#FFF" d="M512 0v64L322 256l190 187v69h-67L254 324 68 512H0v-68l186-187L0 74V0h62l192 188L440 0z"/>
|
||||
<path fill="#C8102E" d="m184 324 11 34L42 512H0v-3zm124-12 54 8 150 147v45zM512 0 320 196l-4-44L466 0zM0 1l193 189-59-8L0 49z"/>
|
||||
<path fill="#FFF" d="M176 0v512h160V0zM0 176v160h512V176z"/>
|
||||
<path fill="#C8102E" d="M0 208v96h512v-96zM208 0v512h96V0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 505 B |
7
client/ui/frontend/src/assets/flags/4x3/en.svg
Normal file
7
client/ui/frontend/src/assets/flags/4x3/en.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-gb" viewBox="0 0 640 480">
|
||||
<path fill="#012169" d="M0 0h640v480H0z"/>
|
||||
<path fill="#FFF" d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0z"/>
|
||||
<path fill="#C8102E" d="m424 281 216 159v40L369 281zm-184 20 6 35L54 480H0zM640 0v3L391 191l2-44L590 0zM0 0l239 176h-60L0 42z"/>
|
||||
<path fill="#FFF" d="M241 0v480h160V0zM0 160v160h640V160z"/>
|
||||
<path fill="#C8102E" d="M0 193v96h640v-96zM273 0v480h96V0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 504 B |
@@ -1,4 +1,5 @@
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Dialog from "@/components/Dialog";
|
||||
import { Input } from "@/components/Input";
|
||||
import { Button } from "@/components/Button";
|
||||
@@ -10,6 +11,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const NewProfileDialog = ({ open, onOpenChange, onCreate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,17 +36,16 @@ export const NewProfileDialog = ({ open, onOpenChange, onCreate }: Props) => {
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="px-8 pt-2">
|
||||
<Dialog.Title>New Profile</Dialog.Title>
|
||||
<Dialog.Title>{t("profile.dialog.title")}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Profiles let you keep separate NetBird connections
|
||||
side by side. Give your profile a memorable name.
|
||||
{t("profile.dialog.description")}
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
|
||||
<div className="px-8 pt-3">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="e.g. Work"
|
||||
placeholder={t("profile.dialog.placeholder")}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
@@ -56,14 +57,14 @@ export const NewProfileDialog = ({ open, onOpenChange, onCreate }: Props) => {
|
||||
variant="secondary"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
Create
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
@@ -14,6 +15,7 @@ import { useProfile } from "@/modules/profile/ProfileContext.tsx";
|
||||
const DEFAULT_PROFILE = "default";
|
||||
|
||||
export const ProfileSelector = () => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
profiles,
|
||||
activeProfile,
|
||||
@@ -53,34 +55,38 @@ export const ProfileSelector = () => {
|
||||
const handleSelect = (name: string) => {
|
||||
setOpen(false);
|
||||
if (name === activeProfile) return;
|
||||
void guarded("Switch Profile Failed", () => switchProfile(name));
|
||||
void guarded(t("profile.error.switchTitle"), () => switchProfile(name));
|
||||
};
|
||||
|
||||
const handleDeregister = async (name: string) => {
|
||||
const cancelLabel = t("common.cancel");
|
||||
const confirmLabel = t("profile.deregister.confirm");
|
||||
const result = await Dialogs.Warning({
|
||||
Title: "Deregister Profile",
|
||||
Message: `Are you sure you want to deregister "${name}"? You will need to log in again to use it.`,
|
||||
Title: t("profile.deregister.title"),
|
||||
Message: t("profile.deregister.message", { name }),
|
||||
Buttons: [
|
||||
{ Label: "Cancel", IsCancel: true },
|
||||
{ Label: "Deregister", IsDefault: true },
|
||||
{ Label: cancelLabel, IsCancel: true },
|
||||
{ Label: confirmLabel, IsDefault: true },
|
||||
],
|
||||
});
|
||||
if (result !== "Deregister") return;
|
||||
void guarded("Deregister Profile Failed", () => logoutProfile(name));
|
||||
if (result !== confirmLabel) return;
|
||||
void guarded(t("profile.error.deregisterTitle"), () => logoutProfile(name));
|
||||
};
|
||||
|
||||
const handleDelete = async (name: string) => {
|
||||
if (name === DEFAULT_PROFILE) return;
|
||||
const cancelLabel = t("common.cancel");
|
||||
const confirmLabel = t("common.delete");
|
||||
const result = await Dialogs.Warning({
|
||||
Title: "Delete Profile",
|
||||
Message: `Are you sure you want to delete "${name}"? This action cannot be undone.`,
|
||||
Title: t("profile.delete.title"),
|
||||
Message: t("profile.delete.message", { name }),
|
||||
Buttons: [
|
||||
{ Label: "Cancel", IsCancel: true },
|
||||
{ Label: "Delete", IsDefault: true },
|
||||
{ Label: cancelLabel, IsCancel: true },
|
||||
{ Label: confirmLabel, IsDefault: true },
|
||||
],
|
||||
});
|
||||
if (result !== "Delete") return;
|
||||
void guarded("Delete Profile Failed", () => removeProfile(name));
|
||||
if (result !== confirmLabel) return;
|
||||
void guarded(t("profile.error.deleteTitle"), () => removeProfile(name));
|
||||
};
|
||||
|
||||
const handleNewProfile = () => {
|
||||
@@ -89,10 +95,12 @@ export const ProfileSelector = () => {
|
||||
};
|
||||
|
||||
const handleCreateProfile = (name: string) => {
|
||||
void guarded("Create Profile Failed", () => addProfile(name));
|
||||
void guarded(t("profile.error.createTitle"), () => addProfile(name));
|
||||
};
|
||||
|
||||
const displayName = selected?.name ?? (loaded ? "No profile" : "Loading...");
|
||||
const displayName =
|
||||
selected?.name ??
|
||||
(loaded ? t("profile.selector.noProfile") : t("profile.selector.loading"));
|
||||
const initial = (selected?.name ?? "?").charAt(0).toUpperCase();
|
||||
const initialColor = generateColorFromString(selected?.name);
|
||||
|
||||
@@ -155,7 +163,7 @@ export const ProfileSelector = () => {
|
||||
<Search size={12} className="text-nb-gray-300 shrink-0" />
|
||||
<Command.Input
|
||||
autoFocus
|
||||
placeholder="Search profile by name..."
|
||||
placeholder={t("profile.selector.searchPlaceholder")}
|
||||
className={cn(
|
||||
"w-full bg-transparent text-xs text-nb-gray-200 placeholder:text-nb-gray-400",
|
||||
"outline-none border-none",
|
||||
@@ -170,11 +178,10 @@ export const ProfileSelector = () => {
|
||||
<Command.Empty>
|
||||
<div className="flex flex-col items-center text-center px-4 pt-2 pb-3">
|
||||
<h3 className="text-xs font-semibold text-nb-gray-200">
|
||||
No Profiles Found
|
||||
{t("profile.selector.emptyTitle")}
|
||||
</h3>
|
||||
<p className="text-[0.7rem] leading-snug text-nb-gray-400 mt-1 text-balance">
|
||||
Try a different search term or create a new
|
||||
profile.
|
||||
{t("profile.selector.emptyDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</Command.Empty>
|
||||
@@ -220,7 +227,7 @@ export const ProfileSelector = () => {
|
||||
>
|
||||
<PlusCircle size={12} className="text-netbird" />
|
||||
</div>
|
||||
<span className="text-xs font-semibold">New Profile</span>
|
||||
<span className="text-xs font-semibold">{t("profile.selector.newProfile")}</span>
|
||||
</button>
|
||||
</Command>
|
||||
</Popover.Content>
|
||||
@@ -252,6 +259,7 @@ const ProfileRow = ({
|
||||
onDelete,
|
||||
deletable,
|
||||
}: ProfileRowProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const initial = profile.name.charAt(0).toUpperCase();
|
||||
const initialColor = generateColorFromString(profile.name);
|
||||
@@ -300,7 +308,7 @@ const ProfileRow = ({
|
||||
"hover:bg-nb-gray-800 hover:text-nb-gray-200 outline-none",
|
||||
"data-[state=open]:bg-nb-gray-800 data-[state=open]:text-nb-gray-200",
|
||||
)}
|
||||
aria-label="More options"
|
||||
aria-label={t("profile.selector.moreOptions")}
|
||||
>
|
||||
<MoreVertical size={14} />
|
||||
</button>
|
||||
@@ -328,7 +336,7 @@ const ProfileRow = ({
|
||||
)}
|
||||
>
|
||||
<UserMinus size={14} className="text-nb-gray-300" />
|
||||
<span>Deregister</span>
|
||||
<span>{t("profile.selector.deregister")}</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
disabled={!deletable}
|
||||
@@ -347,7 +355,7 @@ const ProfileRow = ({
|
||||
)}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>Delete Profile</span>
|
||||
<span>{t("profile.selector.delete")}</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
|
||||
@@ -33,7 +33,7 @@ export const SwitchItemGroup = ({ value, onChange, children, className }: Props)
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className={cn(
|
||||
"flex shrink-0 rounded-lg border border-nb-gray-850 bg-nb-gray-910 p-1",
|
||||
"flex shrink-0 rounded-lg border border-nb-gray-850 bg-nb-gray-910 p-1 overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -30,5 +30,248 @@
|
||||
"notify.error.disconnect": "Trennen fehlgeschlagen",
|
||||
"notify.error.switchProfile": "Wechsel zu {profile} fehlgeschlagen",
|
||||
"notify.sessionExpired.title": "NetBird-Sitzung abgelaufen",
|
||||
"notify.sessionExpired.body": "Ihre NetBird-Sitzung ist abgelaufen. Bitte melden Sie sich erneut an."
|
||||
"notify.sessionExpired.body": "Ihre NetBird-Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
|
||||
|
||||
"common.cancel": "Abbrechen",
|
||||
"common.save": "Speichern",
|
||||
"common.saveChanges": "Änderungen speichern",
|
||||
"common.saving": "Speichert…",
|
||||
"common.close": "Schließen",
|
||||
"common.delete": "Löschen",
|
||||
"common.create": "Erstellen",
|
||||
"common.add": "Hinzufügen",
|
||||
"common.remove": "Entfernen",
|
||||
"common.refresh": "Aktualisieren",
|
||||
"common.loading": "Lädt…",
|
||||
"common.netbird": "NetBird",
|
||||
|
||||
"connect.status.disconnected": "Getrennt",
|
||||
"connect.status.connecting": "Verbindet…",
|
||||
"connect.status.connected": "Verbunden",
|
||||
"connect.status.disconnecting": "Trennt…",
|
||||
"connect.status.daemonUnavailable": "Daemon nicht verfügbar",
|
||||
"connect.status.loginRequired": "Anmeldung erforderlich",
|
||||
|
||||
"connect.error.loginTitle": "Anmeldung fehlgeschlagen",
|
||||
"connect.error.connectTitle": "Verbindung fehlgeschlagen",
|
||||
"connect.error.disconnectTitle": "Trennen fehlgeschlagen",
|
||||
|
||||
"nav.peers.title": "Peers",
|
||||
"nav.peers.description": "{online} von {total} online",
|
||||
"nav.resources.title": "Ressourcen",
|
||||
"nav.resources.description": "{active} von {total} aktiv",
|
||||
"nav.exitNode.title": "Exit-Node {location}",
|
||||
"nav.exitNode.flagAlt": "{country}",
|
||||
|
||||
"header.openSettings": "Einstellungen öffnen",
|
||||
"header.togglePanel": "Seitenleiste umschalten",
|
||||
|
||||
"profile.selector.loading": "Lädt…",
|
||||
"profile.selector.noProfile": "Kein Profil",
|
||||
"profile.selector.searchPlaceholder": "Profil nach Namen suchen…",
|
||||
"profile.selector.emptyTitle": "Keine Profile gefunden",
|
||||
"profile.selector.emptyDescription": "Versuchen Sie einen anderen Suchbegriff oder erstellen Sie ein neues Profil.",
|
||||
"profile.selector.newProfile": "Neues Profil",
|
||||
"profile.selector.moreOptions": "Weitere Optionen",
|
||||
"profile.selector.deregister": "Abmelden",
|
||||
"profile.selector.delete": "Profil löschen",
|
||||
|
||||
"profile.dialog.title": "Neues Profil",
|
||||
"profile.dialog.description": "Mit Profilen können Sie mehrere NetBird-Verbindungen nebeneinander verwalten. Geben Sie Ihrem Profil einen aussagekräftigen Namen.",
|
||||
"profile.dialog.placeholder": "z. B. Arbeit",
|
||||
|
||||
"profile.deregister.title": "Profil abmelden",
|
||||
"profile.deregister.message": "Sind Sie sicher, dass Sie \"{name}\" abmelden möchten? Sie müssen sich erneut anmelden, um es zu nutzen.",
|
||||
"profile.deregister.confirm": "Abmelden",
|
||||
"profile.delete.title": "Profil löschen",
|
||||
"profile.delete.message": "Sind Sie sicher, dass Sie \"{name}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"profile.error.switchTitle": "Profilwechsel fehlgeschlagen",
|
||||
"profile.error.deregisterTitle": "Abmeldung fehlgeschlagen",
|
||||
"profile.error.deleteTitle": "Löschen des Profils fehlgeschlagen",
|
||||
"profile.error.createTitle": "Erstellen des Profils fehlgeschlagen",
|
||||
"profile.error.loadTitle": "Laden der Profile fehlgeschlagen",
|
||||
|
||||
"settings.error.loadTitle": "Laden der Einstellungen fehlgeschlagen",
|
||||
"settings.error.saveTitle": "Speichern der Einstellungen fehlgeschlagen",
|
||||
"settings.error.debugBundleTitle": "Debug-Paket fehlgeschlagen",
|
||||
|
||||
"settings.tabs.general": "Allgemein",
|
||||
"settings.tabs.network": "Netzwerk",
|
||||
"settings.tabs.security": "Sicherheit",
|
||||
"settings.tabs.ssh": "SSH",
|
||||
"settings.tabs.advanced": "Erweitert",
|
||||
"settings.tabs.troubleshooting": "Fehlerbehebung",
|
||||
"settings.tabs.about": "Über",
|
||||
"settings.tabs.updateAvailable": "Update verfügbar",
|
||||
|
||||
"settings.general.section.general": "Allgemein",
|
||||
"settings.general.section.connection": "Verbindung",
|
||||
"settings.general.connectOnStartup.label": "Beim Start verbinden",
|
||||
"settings.general.connectOnStartup.help": "Beim Start des Dienstes automatisch eine Verbindung herstellen.",
|
||||
"settings.general.notifications.label": "Desktop-Benachrichtigungen",
|
||||
"settings.general.notifications.help": "Desktop-Benachrichtigungen für neue Updates und Verbindungsereignisse anzeigen.",
|
||||
"settings.general.language.label": "Anzeigesprache",
|
||||
"settings.general.language.help": "Wählen Sie die Sprache der NetBird-Oberfläche.",
|
||||
"settings.general.language.search": "Sprache suchen…",
|
||||
"settings.general.language.empty": "Keine Sprachen gefunden.",
|
||||
"settings.general.management.label": "Management-Server",
|
||||
"settings.general.management.help": "Mit NetBird Cloud oder Ihrem eigenen self-hosted Management-Server verbinden. Änderungen lösen eine Neuverbindung des Clients aus.",
|
||||
"settings.general.management.cloud": "Cloud",
|
||||
"settings.general.management.selfHosted": "Self-hosted",
|
||||
"settings.general.management.urlPlaceholder": "https://netbird.selfhosted.com:443",
|
||||
"settings.general.management.urlError": "Bitte geben Sie eine gültige URL ein, z. B. https://netbird.selfhosted.com:443",
|
||||
"settings.general.management.switchCloudTitle": "Zu NetBird Cloud wechseln?",
|
||||
"settings.general.management.switchCloudMessage": "Dadurch wird die Verbindung zu Ihrem self-hosted Management-Server getrennt und eine neue Verbindung zu NetBird Cloud hergestellt. Möglicherweise müssen Sie sich erneut anmelden.",
|
||||
"settings.general.management.switchCloudConfirm": "Zu Cloud wechseln",
|
||||
|
||||
"settings.network.section.connectivity": "Konnektivität",
|
||||
"settings.network.section.routingDns": "Routing & DNS",
|
||||
"settings.network.lazy.label": "Verzögerte Verbindungen",
|
||||
"settings.network.lazy.help": "Statt durchgehend aktive Verbindungen zu halten, aktiviert NetBird sie bei Bedarf anhand von Aktivität oder Signalisierung.",
|
||||
"settings.network.monitor.label": "Bei Netzwerkwechsel neu verbinden",
|
||||
"settings.network.monitor.help": "Das Netzwerk überwachen und bei Änderungen (z. B. WLAN-Wechsel, Ethernet-Änderungen oder Rückkehr aus dem Ruhezustand) automatisch neu verbinden.",
|
||||
"settings.network.dns.label": "DNS aktivieren",
|
||||
"settings.network.dns.help": "NetBird-verwaltete DNS-Einstellungen auf den Host-Resolver anwenden.",
|
||||
"settings.network.clientRoutes.label": "Client-Routen aktivieren",
|
||||
"settings.network.clientRoutes.help": "Routen von anderen Peers übernehmen, um deren Netzwerke zu erreichen.",
|
||||
"settings.network.serverRoutes.label": "Server-Routen aktivieren",
|
||||
"settings.network.serverRoutes.help": "Lokale Routen dieses Hosts an andere Peers ankündigen.",
|
||||
|
||||
"settings.security.section.firewall": "Firewall",
|
||||
"settings.security.section.encryption": "Verschlüsselung",
|
||||
"settings.security.blockInbound.label": "Eingehenden Verkehr blockieren",
|
||||
"settings.security.blockInbound.help": "Unaufgeforderte Verbindungen von Peers zu diesem Gerät und den von ihm gerouteten Netzwerken ablehnen. Ausgehender Verkehr ist nicht betroffen.",
|
||||
"settings.security.blockLan.label": "LAN-Zugriff blockieren",
|
||||
"settings.security.blockLan.help": "Verhindert, dass Peers Ihr lokales Netzwerk oder dessen Geräte erreichen, wenn dieses Gerät deren Verkehr routet.",
|
||||
"settings.security.rosenpass.label": "Quantenresistenz aktivieren",
|
||||
"settings.security.rosenpass.help": "Einen post-quanten Schlüsselaustausch via Rosenpass zusätzlich zu WireGuard® hinzufügen.",
|
||||
"settings.security.rosenpassPermissive.label": "Permissiven Modus aktivieren",
|
||||
"settings.security.rosenpassPermissive.help": "Verbindungen zu Peers ohne Quantenresistenz-Unterstützung erlauben.",
|
||||
|
||||
"settings.ssh.section.server": "Server",
|
||||
"settings.ssh.section.capabilities": "Funktionen",
|
||||
"settings.ssh.section.authentication": "Authentifizierung",
|
||||
"settings.ssh.server.label": "SSH-Server aktivieren",
|
||||
"settings.ssh.server.help": "Den NetBird SSH-Server auf diesem Host ausführen, damit andere Peers sich verbinden können.",
|
||||
"settings.ssh.root.label": "Root-Login erlauben",
|
||||
"settings.ssh.root.help": "Peers dürfen sich als root anmelden. Deaktivieren, um ein nicht-privilegiertes Konto zu erfordern.",
|
||||
"settings.ssh.sftp.label": "SFTP erlauben",
|
||||
"settings.ssh.sftp.help": "Dateien sicher über native SFTP- oder SCP-Clients übertragen.",
|
||||
"settings.ssh.localForward.label": "Lokale Portweiterleitung",
|
||||
"settings.ssh.localForward.help": "Verbundene Peers können lokale Ports zu von diesem Host erreichbaren Diensten tunneln.",
|
||||
"settings.ssh.remoteForward.label": "Remote-Portweiterleitung",
|
||||
"settings.ssh.remoteForward.help": "Verbundene Peers können Ports dieses Hosts an ihren eigenen Rechner weitergeben.",
|
||||
"settings.ssh.jwt.label": "JWT-Authentifizierung aktivieren",
|
||||
"settings.ssh.jwt.help": "Jede SSH-Sitzung gegen Ihren IdP für Identität und Audit prüfen. Deaktivieren, um sich nur auf Netzwerk-ACL-Richtlinien zu verlassen — sinnvoll, wenn kein IdP verfügbar ist.",
|
||||
"settings.ssh.jwtTtl.label": "JWT-Cache-TTL",
|
||||
"settings.ssh.jwtTtl.help": "Wie lange dieser Client ein JWT zwischenspeichert, bevor bei ausgehenden SSH-Verbindungen erneut nachgefragt wird. Auf 0 setzen, um den Cache zu deaktivieren und bei jeder Verbindung zu authentifizieren.",
|
||||
"settings.ssh.jwtTtl.suffix": "Sekunde(n)",
|
||||
|
||||
"settings.advanced.section.interface": "Schnittstelle",
|
||||
"settings.advanced.section.security": "Sicherheit",
|
||||
"settings.advanced.interfaceName.label": "Name",
|
||||
"settings.advanced.port.label": "Port",
|
||||
"settings.advanced.mtu.label": "MTU",
|
||||
"settings.advanced.psk.label": "Pre-shared Key",
|
||||
"settings.advanced.psk.help": "Optionaler WireGuard-PSK für zusätzliche symmetrische Verschlüsselung. Nicht identisch mit einem NetBird Setup Key. Sie kommunizieren nur mit Peers, die denselben Pre-shared Key verwenden.",
|
||||
|
||||
"settings.troubleshooting.section.title": "Debug-Paket",
|
||||
"settings.troubleshooting.intro": "Ein Debug-Paket hilft dem NetBird-Support bei der Untersuchung von Verbindungsproblemen. <br /> Es ist eine .zip-Datei mit Logs, Systemdetails und Debug-Informationen Ihres Geräts.",
|
||||
"settings.troubleshooting.anonymize.label": "Sensible Informationen anonymisieren",
|
||||
"settings.troubleshooting.anonymize.help": "Versteckt öffentliche IP-Adressen und nicht-NetBird-Domains in Logs.",
|
||||
"settings.troubleshooting.systemInfo.label": "Systeminformationen einschließen",
|
||||
"settings.troubleshooting.systemInfo.help": "OS, Kernel, Netzwerkschnittstellen und Routing-Tabellen einschließen.",
|
||||
"settings.troubleshooting.upload.label": "Paket an NetBird-Server hochladen",
|
||||
"settings.troubleshooting.upload.help": "Lädt das Paket sicher hoch und gibt einen Upload-Schlüssel zurück. Teilen Sie den Schlüssel über GitHub oder Slack mit dem NetBird-Support, anstatt die Datei direkt anzuhängen.",
|
||||
"settings.troubleshooting.trace.label": "Trace-Logs erfassen",
|
||||
"settings.troubleshooting.trace.help": "Erhöht das Logging auf TRACE und schaltet NetBird kurz aus und wieder ein, um Verbindungs-Logs zu erfassen. Das vorherige Level wird nach Erstellung des Pakets wiederhergestellt.",
|
||||
"settings.troubleshooting.duration.label": "Aufzeichnungsdauer",
|
||||
"settings.troubleshooting.duration.help": "Wie lange Trace-Logs vor der Paketerstellung erfasst werden sollen.",
|
||||
"settings.troubleshooting.duration.suffix": "Minute(n)",
|
||||
"settings.troubleshooting.create": "Paket erstellen",
|
||||
"settings.troubleshooting.progress.description": "Logs, Systemdetails und Verbindungszustand werden gesammelt. Dies dauert in der Regel einen Moment — lassen Sie dieses Fenster geöffnet, bis es abgeschlossen ist.",
|
||||
"settings.troubleshooting.cancelling": "Wird abgebrochen…",
|
||||
"settings.troubleshooting.done.uploadedTitle": "Debug-Paket erfolgreich hochgeladen!",
|
||||
"settings.troubleshooting.done.savedTitle": "Paket gespeichert",
|
||||
"settings.troubleshooting.done.uploadedDescription": "Teilen Sie den unten angezeigten Upload-Schlüssel mit dem NetBird-Support. Eine lokale Kopie wurde ebenfalls auf Ihrem Gerät gespeichert.",
|
||||
"settings.troubleshooting.done.savedDescription": "Ihr Debug-Paket wurde lokal gespeichert.",
|
||||
"settings.troubleshooting.done.copyKey": "Schlüssel kopieren",
|
||||
"settings.troubleshooting.done.openFolder": "Ordner öffnen",
|
||||
"settings.troubleshooting.done.openFileLocation": "Speicherort öffnen",
|
||||
"settings.troubleshooting.uploadFailedWithReason": "Upload fehlgeschlagen: {reason} Das Paket wurde trotzdem lokal gespeichert.",
|
||||
"settings.troubleshooting.uploadFailed": "Upload fehlgeschlagen. Das Paket wurde trotzdem lokal gespeichert.",
|
||||
"settings.troubleshooting.stage.preparingTrace": "Wechsel zu Trace-Logging…",
|
||||
"settings.troubleshooting.stage.reconnecting": "NetBird wird neu verbunden…",
|
||||
"settings.troubleshooting.stage.capturing": "Logs werden erfasst — {elapsed} / {total}",
|
||||
"settings.troubleshooting.stage.restoring": "Vorheriges Log-Level wird wiederhergestellt…",
|
||||
"settings.troubleshooting.stage.bundling": "Debug-Paket wird erstellt…",
|
||||
"settings.troubleshooting.stage.uploading": "Wird zu NetBird hochgeladen…",
|
||||
"settings.troubleshooting.stage.cancelling": "Wird abgebrochen…",
|
||||
|
||||
"settings.about.client": "NetBird Client v{version}",
|
||||
"settings.about.gui": "Oberfläche v{version}",
|
||||
"settings.about.copyright": "© {year} NetBird. Alle Rechte vorbehalten.",
|
||||
"settings.about.links.imprint": "Impressum",
|
||||
"settings.about.links.privacy": "Datenschutz",
|
||||
"settings.about.links.cla": "CLA",
|
||||
"settings.about.links.terms": "Nutzungsbedingungen",
|
||||
|
||||
"update.banner.message": "NetBird {version} ist installationsbereit.",
|
||||
"update.banner.later": "Später",
|
||||
"update.banner.installNow": "Jetzt installieren",
|
||||
"update.card.versionAvailable": "Version {version} ist verfügbar.",
|
||||
"update.card.whatsNew": "Was ist neu?",
|
||||
"update.card.installNow": "Jetzt installieren",
|
||||
"update.card.getInstaller": "Installer holen",
|
||||
"update.card.lastChecked": "Zuletzt geprüft am {date}",
|
||||
"update.card.changelog": "Änderungsprotokoll",
|
||||
"update.card.checkForUpdates": "Nach Updates suchen",
|
||||
"update.header.tooltip": "Update verfügbar",
|
||||
"update.overlay.updatingVersion": "NetBird wird auf v{version} aktualisiert",
|
||||
"update.overlay.updating": "NetBird wird aktualisiert",
|
||||
"update.overlay.description": "Eine neuere Version ist verfügbar und wird installiert. NetBird startet nach Abschluss des Updates automatisch neu.",
|
||||
"update.overlay.error.timeoutTitle": "Update dauert zu lange",
|
||||
"update.overlay.error.timeoutDescription": "Die Installation von {target} hat zu lange gedauert und wurde nicht abgeschlossen.",
|
||||
"update.overlay.error.canceledTitle": "Update wurde abgebrochen",
|
||||
"update.overlay.error.canceledDescription": "Das Update auf {target} wurde vor dem Abschluss abgebrochen.",
|
||||
"update.overlay.error.failTitle": "Update konnte nicht installiert werden",
|
||||
"update.overlay.error.failDescription": "{target} konnte nicht installiert werden.",
|
||||
"update.overlay.error.unknownMessage": "unbekannter Fehler",
|
||||
"update.overlay.error.targetVersion": "v{version}",
|
||||
"update.overlay.error.targetFallback": "die neue Version",
|
||||
|
||||
"update.page.versionLine": "Client wird aktualisiert auf: {version}.",
|
||||
"update.page.versionLineGeneric": "Client wird aktualisiert.",
|
||||
"update.page.outdated": "Ihre Client-Version ist älter als die im Management eingestellte Auto-Update-Version.",
|
||||
"update.page.status.running": "Aktualisiert",
|
||||
"update.page.status.timeout": "Zeitüberschreitung beim Update. Bitte erneut versuchen.",
|
||||
"update.page.status.canceled": "Update abgebrochen.",
|
||||
"update.page.status.failed": "Update fehlgeschlagen: {message}",
|
||||
"update.page.status.unknownError": "unbekannter Update-Fehler",
|
||||
"update.page.failedTitle": "Update fehlgeschlagen",
|
||||
"update.page.timeoutMessage": "Zeitüberschreitung beim Update.",
|
||||
"update.page.dontClose": "Bitte schließen Sie dieses Fenster nicht.",
|
||||
"update.page.updating": "Aktualisiert…",
|
||||
"update.page.complete": "Update abgeschlossen",
|
||||
"update.page.failed": "Update fehlgeschlagen",
|
||||
|
||||
"browserLogin.title": "Setzen Sie den Anmeldevorgang im Browser fort",
|
||||
"browserLogin.description": "Bitte schließen Sie die Kontoauthentifizierung im Browser-Tab ab und fahren Sie dort fort.",
|
||||
"browserLogin.waiting": "Warten auf Anmeldung…",
|
||||
"browserLogin.notSeeing": "Sehen Sie den Browser-Tab nicht?",
|
||||
"browserLogin.tryAgain": "Erneut versuchen",
|
||||
|
||||
"sessionExpired.title": "Sitzung abgelaufen",
|
||||
"sessionExpired.description": "Ihre NetBird-Sitzung ist abgelaufen. Melden Sie sich erneut an, damit Ihre Geräte verbunden bleiben.",
|
||||
"sessionExpired.later": "Später",
|
||||
"sessionExpired.signIn": "Anmelden",
|
||||
|
||||
"peers.search.placeholder": "Nach Peer-Name, DNS oder IP-Adresse suchen",
|
||||
"peers.filter.all": "Alle",
|
||||
"peers.filter.online": "Online",
|
||||
"peers.filter.offline": "Offline",
|
||||
"peers.empty": "Keine Peers entsprechen den aktuellen Filtern.",
|
||||
|
||||
"quickActions.connect": "Verbinden",
|
||||
"quickActions.disconnect": "Trennen"
|
||||
}
|
||||
|
||||
@@ -30,5 +30,248 @@
|
||||
"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."
|
||||
"notify.sessionExpired.body": "Your NetBird session has expired. Please log in again.",
|
||||
|
||||
"common.cancel": "Cancel",
|
||||
"common.save": "Save",
|
||||
"common.saveChanges": "Save Changes",
|
||||
"common.saving": "Saving…",
|
||||
"common.close": "Close",
|
||||
"common.delete": "Delete",
|
||||
"common.create": "Create",
|
||||
"common.add": "Add",
|
||||
"common.remove": "Remove",
|
||||
"common.refresh": "Refresh",
|
||||
"common.loading": "Loading…",
|
||||
"common.netbird": "NetBird",
|
||||
|
||||
"connect.status.disconnected": "Disconnected",
|
||||
"connect.status.connecting": "Connecting...",
|
||||
"connect.status.connected": "Connected",
|
||||
"connect.status.disconnecting": "Disconnecting...",
|
||||
"connect.status.daemonUnavailable": "Daemon unavailable",
|
||||
"connect.status.loginRequired": "Login required",
|
||||
|
||||
"connect.error.loginTitle": "Login Failed",
|
||||
"connect.error.connectTitle": "Connect Failed",
|
||||
"connect.error.disconnectTitle": "Disconnect Failed",
|
||||
|
||||
"nav.peers.title": "Peers",
|
||||
"nav.peers.description": "{online} of {total} Online",
|
||||
"nav.resources.title": "Resources",
|
||||
"nav.resources.description": "{active} of {total} Active",
|
||||
"nav.exitNode.title": "Exit Node {location}",
|
||||
"nav.exitNode.flagAlt": "{country}",
|
||||
|
||||
"header.openSettings": "Open settings",
|
||||
"header.togglePanel": "Toggle side panel",
|
||||
|
||||
"profile.selector.loading": "Loading...",
|
||||
"profile.selector.noProfile": "No profile",
|
||||
"profile.selector.searchPlaceholder": "Search profile by name...",
|
||||
"profile.selector.emptyTitle": "No Profiles Found",
|
||||
"profile.selector.emptyDescription": "Try a different search term or create a new profile.",
|
||||
"profile.selector.newProfile": "New Profile",
|
||||
"profile.selector.moreOptions": "More options",
|
||||
"profile.selector.deregister": "Deregister",
|
||||
"profile.selector.delete": "Delete Profile",
|
||||
|
||||
"profile.dialog.title": "New Profile",
|
||||
"profile.dialog.description": "Profiles let you keep separate NetBird connections side by side. Give your profile a memorable name.",
|
||||
"profile.dialog.placeholder": "e.g. Work",
|
||||
|
||||
"profile.deregister.title": "Deregister Profile",
|
||||
"profile.deregister.message": "Are you sure you want to deregister \"{name}\"? You will need to log in again to use it.",
|
||||
"profile.deregister.confirm": "Deregister",
|
||||
"profile.delete.title": "Delete Profile",
|
||||
"profile.delete.message": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
|
||||
"profile.error.switchTitle": "Switch Profile Failed",
|
||||
"profile.error.deregisterTitle": "Deregister Profile Failed",
|
||||
"profile.error.deleteTitle": "Delete Profile Failed",
|
||||
"profile.error.createTitle": "Create Profile Failed",
|
||||
"profile.error.loadTitle": "Load Profiles Failed",
|
||||
|
||||
"settings.error.loadTitle": "Load Settings Failed",
|
||||
"settings.error.saveTitle": "Save Settings Failed",
|
||||
"settings.error.debugBundleTitle": "Debug Bundle Failed",
|
||||
|
||||
"settings.tabs.general": "General",
|
||||
"settings.tabs.network": "Network",
|
||||
"settings.tabs.security": "Security",
|
||||
"settings.tabs.ssh": "SSH",
|
||||
"settings.tabs.advanced": "Advanced",
|
||||
"settings.tabs.troubleshooting": "Troubleshooting",
|
||||
"settings.tabs.about": "About",
|
||||
"settings.tabs.updateAvailable": "Update Available",
|
||||
|
||||
"settings.general.section.general": "General",
|
||||
"settings.general.section.connection": "Connection",
|
||||
"settings.general.connectOnStartup.label": "Connect on Startup",
|
||||
"settings.general.connectOnStartup.help": "Automatically establish a connection when the service starts.",
|
||||
"settings.general.notifications.label": "Desktop Notifications",
|
||||
"settings.general.notifications.help": "Show desktop notifications for new updates and connection events.",
|
||||
"settings.general.language.label": "Display Language",
|
||||
"settings.general.language.help": "Choose the language for the NetBird interface.",
|
||||
"settings.general.language.search": "Search language…",
|
||||
"settings.general.language.empty": "No languages match.",
|
||||
"settings.general.management.label": "Management Server",
|
||||
"settings.general.management.help": "Connect to NetBird Cloud or your own self-hosted management server. Changes will reconnect the client.",
|
||||
"settings.general.management.cloud": "Cloud",
|
||||
"settings.general.management.selfHosted": "Self-hosted",
|
||||
"settings.general.management.urlPlaceholder": "https://netbird.selfhosted.com:443",
|
||||
"settings.general.management.urlError": "Please enter a valid URL, e.g., https://netbird.selfhosted.com:443",
|
||||
"settings.general.management.switchCloudTitle": "Switch to NetBird Cloud?",
|
||||
"settings.general.management.switchCloudMessage": "This will disconnect from your self-hosted management server and reconnect to NetBird Cloud. You may need to log in again.",
|
||||
"settings.general.management.switchCloudConfirm": "Switch to Cloud",
|
||||
|
||||
"settings.network.section.connectivity": "Connectivity",
|
||||
"settings.network.section.routingDns": "Routing & DNS",
|
||||
"settings.network.lazy.label": "Lazy Connections",
|
||||
"settings.network.lazy.help": "Instead of maintaining always-on connections, NetBird activates them on-demand based on activity or signaling.",
|
||||
"settings.network.monitor.label": "Reconnect on Network Change",
|
||||
"settings.network.monitor.help": "Monitor the network and automatically reconnect on changes such as Wi-Fi switching, Ethernet changes, or resume from sleep.",
|
||||
"settings.network.dns.label": "Enable DNS",
|
||||
"settings.network.dns.help": "Apply NetBird-managed DNS settings to the host resolver.",
|
||||
"settings.network.clientRoutes.label": "Enable Client Routes",
|
||||
"settings.network.clientRoutes.help": "Accept routes from other peers to reach their networks.",
|
||||
"settings.network.serverRoutes.label": "Enable Server Routes",
|
||||
"settings.network.serverRoutes.help": "Advertise this host's local routes to other peers.",
|
||||
|
||||
"settings.security.section.firewall": "Firewall",
|
||||
"settings.security.section.encryption": "Encryption",
|
||||
"settings.security.blockInbound.label": "Block Inbound Traffic",
|
||||
"settings.security.blockInbound.help": "Reject unsolicited connections from peers to this device and any networks it routes. Outbound traffic is unaffected.",
|
||||
"settings.security.blockLan.label": "Block LAN Access",
|
||||
"settings.security.blockLan.help": "Prevent peers from reaching your local network or its devices when this device routes their traffic.",
|
||||
"settings.security.rosenpass.label": "Enable Quantum-Resistance",
|
||||
"settings.security.rosenpass.help": "Add a post-quantum key exchange via Rosenpass on top of WireGuard®.",
|
||||
"settings.security.rosenpassPermissive.label": "Enable Permissive Mode",
|
||||
"settings.security.rosenpassPermissive.help": "Allow connections to peers without quantum-resistance support.",
|
||||
|
||||
"settings.ssh.section.server": "Server",
|
||||
"settings.ssh.section.capabilities": "Capabilities",
|
||||
"settings.ssh.section.authentication": "Authentication",
|
||||
"settings.ssh.server.label": "Enable SSH Server",
|
||||
"settings.ssh.server.help": "Run the NetBird SSH server on this host so other peers can connect to it.",
|
||||
"settings.ssh.root.label": "Allow Root Login",
|
||||
"settings.ssh.root.help": "Let peers sign in as the root user. Disable to require a non-privileged account.",
|
||||
"settings.ssh.sftp.label": "Allow SFTP",
|
||||
"settings.ssh.sftp.help": "Transfer files securely using native SFTP or SCP clients.",
|
||||
"settings.ssh.localForward.label": "Local Port Forwarding",
|
||||
"settings.ssh.localForward.help": "Let connecting peers tunnel local ports to services reachable from this host.",
|
||||
"settings.ssh.remoteForward.label": "Remote Port Forwarding",
|
||||
"settings.ssh.remoteForward.help": "Let connecting peers expose ports on this host back to their own machine.",
|
||||
"settings.ssh.jwt.label": "Enable JWT Authentication",
|
||||
"settings.ssh.jwt.help": "Verify each SSH session against your IdP for user identity and audit. Disable to rely on network ACL policies only, useful when no IdP is available.",
|
||||
"settings.ssh.jwtTtl.label": "JWT Cache TTL",
|
||||
"settings.ssh.jwtTtl.help": "How long this client caches a JWT before prompting again on outgoing SSH connections. Set to 0 to disable caching and authenticate on every connection.",
|
||||
"settings.ssh.jwtTtl.suffix": "Second(s)",
|
||||
|
||||
"settings.advanced.section.interface": "Interface",
|
||||
"settings.advanced.section.security": "Security",
|
||||
"settings.advanced.interfaceName.label": "Name",
|
||||
"settings.advanced.port.label": "Port",
|
||||
"settings.advanced.mtu.label": "MTU",
|
||||
"settings.advanced.psk.label": "Pre-shared Key",
|
||||
"settings.advanced.psk.help": "Optional WireGuard PSK for extra symmetric encryption. Not the same as a NetBird Setup Key. You will only communicate with peers that use the same pre-shared key.",
|
||||
|
||||
"settings.troubleshooting.section.title": "Debug bundle",
|
||||
"settings.troubleshooting.intro": "A debug bundle helps NetBird support investigate connection problems. <br /> It's a .zip file with logs, system details and debug information from your device.",
|
||||
"settings.troubleshooting.anonymize.label": "Anonymize Sensitive Information",
|
||||
"settings.troubleshooting.anonymize.help": "Hides public IP addresses and non-NetBird domains from logs.",
|
||||
"settings.troubleshooting.systemInfo.label": "Include System Information",
|
||||
"settings.troubleshooting.systemInfo.help": "Include OS, kernel, network interfaces, and routing tables.",
|
||||
"settings.troubleshooting.upload.label": "Upload Bundle to NetBird Servers",
|
||||
"settings.troubleshooting.upload.help": "Securely uploads the bundle and returns an upload key. Share the key with NetBird support over GitHub or Slack instead of attaching the file directly.",
|
||||
"settings.troubleshooting.trace.label": "Capture Trace Logs",
|
||||
"settings.troubleshooting.trace.help": "Raises logging to TRACE and cycles NetBird up and down to capture connection logs. The previous level is restored after the bundle is built.",
|
||||
"settings.troubleshooting.duration.label": "Capture Duration",
|
||||
"settings.troubleshooting.duration.help": "How long to capture trace logs before generating the bundle.",
|
||||
"settings.troubleshooting.duration.suffix": "Minute(s)",
|
||||
"settings.troubleshooting.create": "Create Bundle",
|
||||
"settings.troubleshooting.progress.description": "Collecting logs, system details, and connection state. This usually takes a moment — keep this window open until it completes.",
|
||||
"settings.troubleshooting.cancelling": "Cancelling…",
|
||||
"settings.troubleshooting.done.uploadedTitle": "Debug bundle successfully uploaded!",
|
||||
"settings.troubleshooting.done.savedTitle": "Bundle saved",
|
||||
"settings.troubleshooting.done.uploadedDescription": "Share the upload key below with NetBird support. A local copy was also saved on your device.",
|
||||
"settings.troubleshooting.done.savedDescription": "Your debug bundle has been saved locally.",
|
||||
"settings.troubleshooting.done.copyKey": "Copy Key",
|
||||
"settings.troubleshooting.done.openFolder": "Open Folder",
|
||||
"settings.troubleshooting.done.openFileLocation": "Open file location",
|
||||
"settings.troubleshooting.uploadFailedWithReason": "Upload failed: {reason} The bundle is still saved locally.",
|
||||
"settings.troubleshooting.uploadFailed": "Upload failed. The bundle is still saved locally.",
|
||||
"settings.troubleshooting.stage.preparingTrace": "Switching to trace logging…",
|
||||
"settings.troubleshooting.stage.reconnecting": "Reconnecting NetBird…",
|
||||
"settings.troubleshooting.stage.capturing": "Capturing logs — {elapsed} / {total}",
|
||||
"settings.troubleshooting.stage.restoring": "Restoring previous log level…",
|
||||
"settings.troubleshooting.stage.bundling": "Generating debug bundle…",
|
||||
"settings.troubleshooting.stage.uploading": "Uploading to NetBird…",
|
||||
"settings.troubleshooting.stage.cancelling": "Cancelling…",
|
||||
|
||||
"settings.about.client": "NetBird Client v{version}",
|
||||
"settings.about.gui": "GUI v{version}",
|
||||
"settings.about.copyright": "© {year} NetBird. All Rights Reserved.",
|
||||
"settings.about.links.imprint": "Imprint",
|
||||
"settings.about.links.privacy": "Privacy",
|
||||
"settings.about.links.cla": "CLA",
|
||||
"settings.about.links.terms": "Terms of Service",
|
||||
|
||||
"update.banner.message": "NetBird {version} is ready to install.",
|
||||
"update.banner.later": "Later",
|
||||
"update.banner.installNow": "Install now",
|
||||
"update.card.versionAvailable": "Version {version} is available.",
|
||||
"update.card.whatsNew": "What's new?",
|
||||
"update.card.installNow": "Install now",
|
||||
"update.card.getInstaller": "Get installer",
|
||||
"update.card.lastChecked": "Last checked on {date}",
|
||||
"update.card.changelog": "Changelog",
|
||||
"update.card.checkForUpdates": "Check for updates",
|
||||
"update.header.tooltip": "Update Available",
|
||||
"update.overlay.updatingVersion": "Updating NetBird to v{version}",
|
||||
"update.overlay.updating": "Updating NetBird",
|
||||
"update.overlay.description": "A newer version is available and is being installed. NetBird will restart automatically once the update is finished.",
|
||||
"update.overlay.error.timeoutTitle": "Update Is Taking Too Long",
|
||||
"update.overlay.error.timeoutDescription": "Installing {target} took too long and didn't finish.",
|
||||
"update.overlay.error.canceledTitle": "Update Was Stopped",
|
||||
"update.overlay.error.canceledDescription": "The update to {target} was canceled before it finished.",
|
||||
"update.overlay.error.failTitle": "Couldn't Install the Update",
|
||||
"update.overlay.error.failDescription": "{target} couldn't be installed.",
|
||||
"update.overlay.error.unknownMessage": "unknown error",
|
||||
"update.overlay.error.targetVersion": "v{version}",
|
||||
"update.overlay.error.targetFallback": "the new version",
|
||||
|
||||
"update.page.versionLine": "Updating client to: {version}.",
|
||||
"update.page.versionLineGeneric": "Updating client.",
|
||||
"update.page.outdated": "Your client version is older than the auto-update version set in Management.",
|
||||
"update.page.status.running": "Updating",
|
||||
"update.page.status.timeout": "Update timed out. Please try again.",
|
||||
"update.page.status.canceled": "Update canceled.",
|
||||
"update.page.status.failed": "Update failed: {message}",
|
||||
"update.page.status.unknownError": "unknown update error",
|
||||
"update.page.failedTitle": "Update Failed",
|
||||
"update.page.timeoutMessage": "Update timed out.",
|
||||
"update.page.dontClose": "Please don't close this window.",
|
||||
"update.page.updating": "Updating…",
|
||||
"update.page.complete": "Update complete",
|
||||
"update.page.failed": "Update failed",
|
||||
|
||||
"browserLogin.title": "Continue in your browser to complete the login",
|
||||
"browserLogin.description": "Please complete the account authentication process in the browser tab and continue from there.",
|
||||
"browserLogin.waiting": "Waiting for sign-in…",
|
||||
"browserLogin.notSeeing": "Not seeing the browser tab?",
|
||||
"browserLogin.tryAgain": "Try again",
|
||||
|
||||
"sessionExpired.title": "Session expired",
|
||||
"sessionExpired.description": "Your NetBird session has expired. Sign in again to keep your devices connected.",
|
||||
"sessionExpired.later": "Later",
|
||||
"sessionExpired.signIn": "Sign in",
|
||||
|
||||
"peers.search.placeholder": "Search by peer name, DNS or IP address",
|
||||
"peers.filter.all": "All",
|
||||
"peers.filter.online": "Online",
|
||||
"peers.filter.offline": "Offline",
|
||||
"peers.empty": "No peers match the current filters.",
|
||||
|
||||
"quickActions.connect": "Connect",
|
||||
"quickActions.disconnect": "Disconnect"
|
||||
}
|
||||
|
||||
@@ -30,5 +30,248 @@
|
||||
"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."
|
||||
"notify.sessionExpired.body": "A NetBird munkamenet lejárt. Kérjük, jelentkezzen be újra.",
|
||||
|
||||
"common.cancel": "Mégse",
|
||||
"common.save": "Mentés",
|
||||
"common.saveChanges": "Módosítások mentése",
|
||||
"common.saving": "Mentés…",
|
||||
"common.close": "Bezárás",
|
||||
"common.delete": "Törlés",
|
||||
"common.create": "Létrehozás",
|
||||
"common.add": "Hozzáadás",
|
||||
"common.remove": "Eltávolítás",
|
||||
"common.refresh": "Frissítés",
|
||||
"common.loading": "Betöltés…",
|
||||
"common.netbird": "NetBird",
|
||||
|
||||
"connect.status.disconnected": "Lekapcsolva",
|
||||
"connect.status.connecting": "Csatlakozás…",
|
||||
"connect.status.connected": "Csatlakoztatva",
|
||||
"connect.status.disconnecting": "Bontás…",
|
||||
"connect.status.daemonUnavailable": "Daemon nem elérhető",
|
||||
"connect.status.loginRequired": "Bejelentkezés szükséges",
|
||||
|
||||
"connect.error.loginTitle": "Bejelentkezés sikertelen",
|
||||
"connect.error.connectTitle": "Csatlakozás sikertelen",
|
||||
"connect.error.disconnectTitle": "Bontás sikertelen",
|
||||
|
||||
"nav.peers.title": "Társak",
|
||||
"nav.peers.description": "{online} / {total} online",
|
||||
"nav.resources.title": "Erőforrások",
|
||||
"nav.resources.description": "{active} / {total} aktív",
|
||||
"nav.exitNode.title": "Kilépő csomópont: {location}",
|
||||
"nav.exitNode.flagAlt": "{country}",
|
||||
|
||||
"header.openSettings": "Beállítások megnyitása",
|
||||
"header.togglePanel": "Oldalsó panel váltása",
|
||||
|
||||
"profile.selector.loading": "Betöltés…",
|
||||
"profile.selector.noProfile": "Nincs profil",
|
||||
"profile.selector.searchPlaceholder": "Profil keresése név alapján…",
|
||||
"profile.selector.emptyTitle": "Nem található profil",
|
||||
"profile.selector.emptyDescription": "Próbáljon más keresőkifejezést, vagy hozzon létre új profilt.",
|
||||
"profile.selector.newProfile": "Új profil",
|
||||
"profile.selector.moreOptions": "További műveletek",
|
||||
"profile.selector.deregister": "Leválasztás",
|
||||
"profile.selector.delete": "Profil törlése",
|
||||
|
||||
"profile.dialog.title": "Új profil",
|
||||
"profile.dialog.description": "A profilok lehetővé teszik, hogy különálló NetBird-kapcsolatokat tartson egymás mellett. Adjon profiljának egy könnyen megjegyezhető nevet.",
|
||||
"profile.dialog.placeholder": "pl. Munka",
|
||||
|
||||
"profile.deregister.title": "Profil leválasztása",
|
||||
"profile.deregister.message": "Biztosan le szeretné választani a következőt: \"{name}\"? Újra be kell jelentkeznie a használatához.",
|
||||
"profile.deregister.confirm": "Leválasztás",
|
||||
"profile.delete.title": "Profil törlése",
|
||||
"profile.delete.message": "Biztosan törölni szeretné a következőt: \"{name}\"? Ez a művelet nem vonható vissza.",
|
||||
"profile.error.switchTitle": "Profilváltás sikertelen",
|
||||
"profile.error.deregisterTitle": "Leválasztás sikertelen",
|
||||
"profile.error.deleteTitle": "Profil törlése sikertelen",
|
||||
"profile.error.createTitle": "Profil létrehozása sikertelen",
|
||||
"profile.error.loadTitle": "Profilok betöltése sikertelen",
|
||||
|
||||
"settings.error.loadTitle": "Beállítások betöltése sikertelen",
|
||||
"settings.error.saveTitle": "Beállítások mentése sikertelen",
|
||||
"settings.error.debugBundleTitle": "Hibakeresési csomag sikertelen",
|
||||
|
||||
"settings.tabs.general": "Általános",
|
||||
"settings.tabs.network": "Hálózat",
|
||||
"settings.tabs.security": "Biztonság",
|
||||
"settings.tabs.ssh": "SSH",
|
||||
"settings.tabs.advanced": "Speciális",
|
||||
"settings.tabs.troubleshooting": "Hibaelhárítás",
|
||||
"settings.tabs.about": "Névjegy",
|
||||
"settings.tabs.updateAvailable": "Frissítés elérhető",
|
||||
|
||||
"settings.general.section.general": "Általános",
|
||||
"settings.general.section.connection": "Kapcsolat",
|
||||
"settings.general.connectOnStartup.label": "Csatlakozás indításkor",
|
||||
"settings.general.connectOnStartup.help": "A szolgáltatás indulásakor automatikusan kapcsolatot létesít.",
|
||||
"settings.general.notifications.label": "Asztali értesítések",
|
||||
"settings.general.notifications.help": "Asztali értesítések megjelenítése új frissítésekről és kapcsolati eseményekről.",
|
||||
"settings.general.language.label": "Megjelenítési nyelv",
|
||||
"settings.general.language.help": "Válassza ki a NetBird felület nyelvét.",
|
||||
"settings.general.language.search": "Nyelv keresése…",
|
||||
"settings.general.language.empty": "Nincs találat.",
|
||||
"settings.general.management.label": "Kezelőszerver",
|
||||
"settings.general.management.help": "Csatlakozás a NetBird Cloudhoz vagy saját self-hosted kezelőszerverhez. A módosítások újracsatlakozást váltanak ki.",
|
||||
"settings.general.management.cloud": "Felhő",
|
||||
"settings.general.management.selfHosted": "Saját üzemeltetésű",
|
||||
"settings.general.management.urlPlaceholder": "https://netbird.selfhosted.com:443",
|
||||
"settings.general.management.urlError": "Adjon meg egy érvényes URL-t, pl. https://netbird.selfhosted.com:443",
|
||||
"settings.general.management.switchCloudTitle": "Átváltás a NetBird Cloudra?",
|
||||
"settings.general.management.switchCloudMessage": "Ez megszünteti a kapcsolatot a saját üzemeltetésű kezelőszerverrel, és újra csatlakozik a NetBird Cloudhoz. Lehet, hogy újra be kell jelentkeznie.",
|
||||
"settings.general.management.switchCloudConfirm": "Váltás a felhőre",
|
||||
|
||||
"settings.network.section.connectivity": "Kapcsolódás",
|
||||
"settings.network.section.routingDns": "Útválasztás és DNS",
|
||||
"settings.network.lazy.label": "Késleltetett kapcsolatok",
|
||||
"settings.network.lazy.help": "Állandó kapcsolatok fenntartása helyett a NetBird igény szerint, aktivitás vagy jelzés alapján aktiválja azokat.",
|
||||
"settings.network.monitor.label": "Újracsatlakozás hálózatváltáskor",
|
||||
"settings.network.monitor.help": "A hálózat figyelése és automatikus újracsatlakozás változások (pl. Wi-Fi-váltás, Ethernet-változás vagy alvó állapotból való visszatérés) esetén.",
|
||||
"settings.network.dns.label": "DNS engedélyezése",
|
||||
"settings.network.dns.help": "A NetBird által kezelt DNS-beállítások alkalmazása a gazda DNS-feloldójára.",
|
||||
"settings.network.clientRoutes.label": "Kliens útvonalak engedélyezése",
|
||||
"settings.network.clientRoutes.help": "Más társak útvonalainak elfogadása az ő hálózataik eléréséhez.",
|
||||
"settings.network.serverRoutes.label": "Szerver útvonalak engedélyezése",
|
||||
"settings.network.serverRoutes.help": "Ennek a gazdának a helyi útvonalainak meghirdetése más társak számára.",
|
||||
|
||||
"settings.security.section.firewall": "Tűzfal",
|
||||
"settings.security.section.encryption": "Titkosítás",
|
||||
"settings.security.blockInbound.label": "Bejövő forgalom blokkolása",
|
||||
"settings.security.blockInbound.help": "Visszautasítja a társaktól érkező nem kért kapcsolatokat ezen eszközhöz és az általa irányított hálózatokhoz. A kimenő forgalmat nem érinti.",
|
||||
"settings.security.blockLan.label": "LAN-hozzáférés blokkolása",
|
||||
"settings.security.blockLan.help": "Megakadályozza, hogy a társak elérjék a helyi hálózatot vagy annak eszközeit, amikor ez az eszköz irányítja a forgalmukat.",
|
||||
"settings.security.rosenpass.label": "Kvantumellenálló titkosítás engedélyezése",
|
||||
"settings.security.rosenpass.help": "Post-kvantum kulcscsere hozzáadása Rosenpass segítségével a WireGuard® tetejére.",
|
||||
"settings.security.rosenpassPermissive.label": "Engedékeny mód engedélyezése",
|
||||
"settings.security.rosenpassPermissive.help": "Kapcsolatok engedélyezése kvantumellenálló titkosítás nélküli társakkal.",
|
||||
|
||||
"settings.ssh.section.server": "Szerver",
|
||||
"settings.ssh.section.capabilities": "Képességek",
|
||||
"settings.ssh.section.authentication": "Hitelesítés",
|
||||
"settings.ssh.server.label": "SSH szerver engedélyezése",
|
||||
"settings.ssh.server.help": "Futtassa a NetBird SSH szervert ezen a gazdán, hogy más társak csatlakozhassanak.",
|
||||
"settings.ssh.root.label": "Root bejelentkezés engedélyezése",
|
||||
"settings.ssh.root.help": "Társak bejelentkezhetnek root felhasználóként. Tiltsa le, ha nem privilegizált fiók szükséges.",
|
||||
"settings.ssh.sftp.label": "SFTP engedélyezése",
|
||||
"settings.ssh.sftp.help": "Fájlok biztonságos átvitele natív SFTP- vagy SCP-kliensekkel.",
|
||||
"settings.ssh.localForward.label": "Helyi porttovábbítás",
|
||||
"settings.ssh.localForward.help": "A csatlakozó társak helyi portokat alagútba helyezhetnek erről a gazdáról elérhető szolgáltatásokhoz.",
|
||||
"settings.ssh.remoteForward.label": "Távoli porttovábbítás",
|
||||
"settings.ssh.remoteForward.help": "A csatlakozó társak ezen a gazdán lévő portokat tehetnek elérhetővé a saját gépük számára.",
|
||||
"settings.ssh.jwt.label": "JWT-hitelesítés engedélyezése",
|
||||
"settings.ssh.jwt.help": "Minden SSH-munkamenet ellenőrzése az IdP-vel a felhasználói identitás és audit céljából. Tiltsa le, ha csak a hálózati ACL-szabályokra kíván támaszkodni — hasznos, ha nincs elérhető IdP.",
|
||||
"settings.ssh.jwtTtl.label": "JWT gyorsítótár TTL",
|
||||
"settings.ssh.jwtTtl.help": "Mennyi ideig őrzi meg a kliens a JWT-t, mielőtt újra kérné a kimenő SSH-kapcsolatoknál. 0 érték esetén a gyorsítótárazás kikapcsol, és minden kapcsolatnál hitelesít.",
|
||||
"settings.ssh.jwtTtl.suffix": "másodperc",
|
||||
|
||||
"settings.advanced.section.interface": "Interfész",
|
||||
"settings.advanced.section.security": "Biztonság",
|
||||
"settings.advanced.interfaceName.label": "Név",
|
||||
"settings.advanced.port.label": "Port",
|
||||
"settings.advanced.mtu.label": "MTU",
|
||||
"settings.advanced.psk.label": "Pre-shared kulcs",
|
||||
"settings.advanced.psk.help": "Opcionális WireGuard PSK további szimmetrikus titkosításhoz. Nem azonos a NetBird telepítőkulccsal. Csak olyan társakkal kommunikál, akik ugyanazt a pre-shared kulcsot használják.",
|
||||
|
||||
"settings.troubleshooting.section.title": "Hibakeresési csomag",
|
||||
"settings.troubleshooting.intro": "A hibakeresési csomag segít a NetBird támogatásnak a kapcsolati problémák kivizsgálásában. <br /> Egy .zip fájl, amely naplókat, rendszerinformációkat és hibakeresési adatokat tartalmaz az eszközéről.",
|
||||
"settings.troubleshooting.anonymize.label": "Érzékeny információk anonimizálása",
|
||||
"settings.troubleshooting.anonymize.help": "Elrejti a nyilvános IP-címeket és a nem-NetBird tartományokat a naplókban.",
|
||||
"settings.troubleshooting.systemInfo.label": "Rendszerinformációk beillesztése",
|
||||
"settings.troubleshooting.systemInfo.help": "Tartalmazza az OS-t, a kernelt, a hálózati interfészeket és az útválasztási táblákat.",
|
||||
"settings.troubleshooting.upload.label": "Csomag feltöltése a NetBird szerverekre",
|
||||
"settings.troubleshooting.upload.help": "Biztonságosan feltölti a csomagot, és visszaad egy feltöltési kulcsot. Ossza meg a kulcsot a NetBird támogatással a GitHubon vagy Slacken keresztül a fájl közvetlen csatolása helyett.",
|
||||
"settings.troubleshooting.trace.label": "Trace naplók rögzítése",
|
||||
"settings.troubleshooting.trace.help": "TRACE szintre emeli a naplózást, és újraindítja a NetBird kapcsolatot a kapcsolati naplók rögzítéséhez. Az előző szint a csomag elkészülte után visszaáll.",
|
||||
"settings.troubleshooting.duration.label": "Rögzítés időtartama",
|
||||
"settings.troubleshooting.duration.help": "Mennyi ideig rögzítse a trace naplókat a csomag elkészítése előtt.",
|
||||
"settings.troubleshooting.duration.suffix": "perc",
|
||||
"settings.troubleshooting.create": "Csomag létrehozása",
|
||||
"settings.troubleshooting.progress.description": "Naplók, rendszerinformációk és kapcsolati állapot gyűjtése folyamatban. Általában néhány pillanatot vesz igénybe — tartsa nyitva ezt az ablakot a befejezésig.",
|
||||
"settings.troubleshooting.cancelling": "Megszakítás…",
|
||||
"settings.troubleshooting.done.uploadedTitle": "A hibakeresési csomag feltöltése sikeres!",
|
||||
"settings.troubleshooting.done.savedTitle": "Csomag elmentve",
|
||||
"settings.troubleshooting.done.uploadedDescription": "Ossza meg az alábbi feltöltési kulcsot a NetBird támogatással. A helyi másolat is elmentve van az eszközén.",
|
||||
"settings.troubleshooting.done.savedDescription": "A hibakeresési csomag helyileg elmentve.",
|
||||
"settings.troubleshooting.done.copyKey": "Kulcs másolása",
|
||||
"settings.troubleshooting.done.openFolder": "Mappa megnyitása",
|
||||
"settings.troubleshooting.done.openFileLocation": "Fájl helyének megnyitása",
|
||||
"settings.troubleshooting.uploadFailedWithReason": "Feltöltés sikertelen: {reason} A csomag továbbra is el van mentve helyileg.",
|
||||
"settings.troubleshooting.uploadFailed": "Feltöltés sikertelen. A csomag továbbra is el van mentve helyileg.",
|
||||
"settings.troubleshooting.stage.preparingTrace": "Váltás trace naplózásra…",
|
||||
"settings.troubleshooting.stage.reconnecting": "NetBird újracsatlakoztatása…",
|
||||
"settings.troubleshooting.stage.capturing": "Naplók rögzítése — {elapsed} / {total}",
|
||||
"settings.troubleshooting.stage.restoring": "Korábbi napló szint visszaállítása…",
|
||||
"settings.troubleshooting.stage.bundling": "Hibakeresési csomag generálása…",
|
||||
"settings.troubleshooting.stage.uploading": "Feltöltés a NetBirdhöz…",
|
||||
"settings.troubleshooting.stage.cancelling": "Megszakítás…",
|
||||
|
||||
"settings.about.client": "NetBird Kliens v{version}",
|
||||
"settings.about.gui": "Felület v{version}",
|
||||
"settings.about.copyright": "© {year} NetBird. Minden jog fenntartva.",
|
||||
"settings.about.links.imprint": "Impresszum",
|
||||
"settings.about.links.privacy": "Adatvédelem",
|
||||
"settings.about.links.cla": "CLA",
|
||||
"settings.about.links.terms": "Felhasználási feltételek",
|
||||
|
||||
"update.banner.message": "A NetBird {version} telepítésre kész.",
|
||||
"update.banner.later": "Később",
|
||||
"update.banner.installNow": "Telepítés most",
|
||||
"update.card.versionAvailable": "Elérhető a {version} verzió.",
|
||||
"update.card.whatsNew": "Mi az újdonság?",
|
||||
"update.card.installNow": "Telepítés most",
|
||||
"update.card.getInstaller": "Telepítő letöltése",
|
||||
"update.card.lastChecked": "Utolsó ellenőrzés: {date}",
|
||||
"update.card.changelog": "Változásnapló",
|
||||
"update.card.checkForUpdates": "Frissítések keresése",
|
||||
"update.header.tooltip": "Frissítés elérhető",
|
||||
"update.overlay.updatingVersion": "NetBird frissítése a következőre: v{version}",
|
||||
"update.overlay.updating": "NetBird frissítése",
|
||||
"update.overlay.description": "Egy újabb verzió elérhető és települ. A NetBird automatikusan újraindul a frissítés befejeztével.",
|
||||
"update.overlay.error.timeoutTitle": "A frissítés túl sokáig tart",
|
||||
"update.overlay.error.timeoutDescription": "A(z) {target} telepítése túl sokáig tartott, és nem fejeződött be.",
|
||||
"update.overlay.error.canceledTitle": "A frissítés megszakítva",
|
||||
"update.overlay.error.canceledDescription": "A(z) {target} frissítését megszakították a befejezés előtt.",
|
||||
"update.overlay.error.failTitle": "A frissítés nem telepíthető",
|
||||
"update.overlay.error.failDescription": "A(z) {target} nem volt telepíthető.",
|
||||
"update.overlay.error.unknownMessage": "ismeretlen hiba",
|
||||
"update.overlay.error.targetVersion": "v{version}",
|
||||
"update.overlay.error.targetFallback": "az új verzió",
|
||||
|
||||
"update.page.versionLine": "Kliens frissítése erre: {version}.",
|
||||
"update.page.versionLineGeneric": "Kliens frissítése.",
|
||||
"update.page.outdated": "Az Ön kliensverziója régebbi, mint a Managementben beállított automatikus frissítési verzió.",
|
||||
"update.page.status.running": "Frissítés",
|
||||
"update.page.status.timeout": "A frissítés időtúllépés miatt megszakadt. Kérjük, próbálja újra.",
|
||||
"update.page.status.canceled": "Frissítés megszakítva.",
|
||||
"update.page.status.failed": "Frissítés sikertelen: {message}",
|
||||
"update.page.status.unknownError": "ismeretlen frissítési hiba",
|
||||
"update.page.failedTitle": "Frissítés sikertelen",
|
||||
"update.page.timeoutMessage": "Frissítés időtúllépés.",
|
||||
"update.page.dontClose": "Kérjük, ne zárja be ezt az ablakot.",
|
||||
"update.page.updating": "Frissítés…",
|
||||
"update.page.complete": "Frissítés kész",
|
||||
"update.page.failed": "Frissítés sikertelen",
|
||||
|
||||
"browserLogin.title": "Folytassa a böngészőben a bejelentkezés befejezéséhez",
|
||||
"browserLogin.description": "Kérjük, fejezze be a fiókhitelesítést a böngésző fülén, és folytassa onnan.",
|
||||
"browserLogin.waiting": "Várakozás a bejelentkezésre…",
|
||||
"browserLogin.notSeeing": "Nem látja a böngésző fülét?",
|
||||
"browserLogin.tryAgain": "Próbálja újra",
|
||||
|
||||
"sessionExpired.title": "Munkamenet lejárt",
|
||||
"sessionExpired.description": "A NetBird munkamenete lejárt. Jelentkezzen be újra, hogy az eszközei kapcsolatban maradjanak.",
|
||||
"sessionExpired.later": "Később",
|
||||
"sessionExpired.signIn": "Bejelentkezés",
|
||||
|
||||
"peers.search.placeholder": "Keresés társ neve, DNS vagy IP-cím alapján",
|
||||
"peers.filter.all": "Összes",
|
||||
"peers.filter.online": "Online",
|
||||
"peers.filter.offline": "Offline",
|
||||
"peers.empty": "Egyetlen társ sem felel meg a jelenlegi szűrőknek.",
|
||||
|
||||
"quickActions.connect": "Csatlakozás",
|
||||
"quickActions.disconnect": "Bontás"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dialogs, Events } from "@wailsio/runtime";
|
||||
import { Connection, WindowManager } from "@bindings/services";
|
||||
import i18next from "@/lib/i18n";
|
||||
import { ToggleSwitch } from "@/components/ToggleSwitch.tsx";
|
||||
import { useStatus } from "@/hooks/useStatus";
|
||||
import { useProfile } from "@/modules/profile/ProfileContext.tsx";
|
||||
@@ -14,11 +16,11 @@ enum ConnectionState {
|
||||
Disconnecting = "disconnecting",
|
||||
}
|
||||
|
||||
const STATUS_LABEL: Record<ConnectionState, string> = {
|
||||
[ConnectionState.Disconnected]: "Disconnected",
|
||||
[ConnectionState.Connecting]: "Connecting...",
|
||||
[ConnectionState.Connected]: "Connected",
|
||||
[ConnectionState.Disconnecting]: "Disconnecting...",
|
||||
const STATUS_KEY: Record<ConnectionState, string> = {
|
||||
[ConnectionState.Disconnected]: "connect.status.disconnected",
|
||||
[ConnectionState.Connecting]: "connect.status.connecting",
|
||||
[ConnectionState.Connected]: "connect.status.connected",
|
||||
[ConnectionState.Disconnecting]: "connect.status.disconnecting",
|
||||
};
|
||||
|
||||
const EVENT_BROWSER_LOGIN_CANCEL = "browser-login:cancel";
|
||||
@@ -90,7 +92,7 @@ async function startLogin(): Promise<void> {
|
||||
WindowManager.CloseBrowserLogin().catch(console.error);
|
||||
if (cancelled) return;
|
||||
await Dialogs.Error({
|
||||
Title: "Login Failed",
|
||||
Title: i18next.t("connect.error.loginTitle"),
|
||||
Message: errorMessage(e),
|
||||
});
|
||||
} finally {
|
||||
@@ -100,6 +102,7 @@ async function startLogin(): Promise<void> {
|
||||
}
|
||||
|
||||
export const ConnectionStatusSwitch = () => {
|
||||
const { t } = useTranslation();
|
||||
const { status, refresh } = useStatus();
|
||||
const { activeProfile, username } = useProfile();
|
||||
|
||||
@@ -141,7 +144,7 @@ export const ConnectionStatusSwitch = () => {
|
||||
});
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: "Connect Failed",
|
||||
Title: t("connect.error.connectTitle"),
|
||||
Message: errorMessage(e),
|
||||
});
|
||||
} finally {
|
||||
@@ -156,7 +159,7 @@ export const ConnectionStatusSwitch = () => {
|
||||
await Connection.Down();
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: "Disconnect Failed",
|
||||
Title: t("connect.error.disconnectTitle"),
|
||||
Message: errorMessage(e),
|
||||
});
|
||||
} finally {
|
||||
@@ -215,10 +218,10 @@ export const ConnectionStatusSwitch = () => {
|
||||
}
|
||||
>
|
||||
{unreachable
|
||||
? "Daemon unavailable"
|
||||
? t("connect.status.daemonUnavailable")
|
||||
: needsLogin
|
||||
? "Login required"
|
||||
: STATUS_LABEL[connState]}
|
||||
? t("connect.status.loginRequired")
|
||||
: t(STATUS_KEY[connState])}
|
||||
</h1>
|
||||
<p
|
||||
className={cn(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CardNavItem } from "@/components/CardNavItem.tsx";
|
||||
import { Layers3Icon, MonitorSmartphoneIcon } from "lucide-react";
|
||||
import deFlag from "@/assets/flags/1x1/de.svg";
|
||||
@@ -8,32 +9,33 @@ type Props = {
|
||||
};
|
||||
|
||||
export const Navigation = ({ peersActive = false, onPeersClick }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<nav className={"w-full flex flex-col gap-1 mt-auto"}>
|
||||
<CardNavItem
|
||||
icon={MonitorSmartphoneIcon}
|
||||
title={"Peers"}
|
||||
description={"17 of 25 Online"}
|
||||
title={t("nav.peers.title")}
|
||||
description={t("nav.peers.description", { online: 17, total: 25 })}
|
||||
active={peersActive}
|
||||
onClick={onPeersClick}
|
||||
/>
|
||||
<CardNavItem
|
||||
icon={Layers3Icon}
|
||||
title={"Resources"}
|
||||
description={"13 of 16 Active"}
|
||||
title={t("nav.resources.title")}
|
||||
description={t("nav.resources.description", { active: 13, total: 16 })}
|
||||
iconSize={14}
|
||||
/>
|
||||
<CardNavItem
|
||||
iconNode={
|
||||
<img
|
||||
src={deFlag}
|
||||
alt={"Germany"}
|
||||
alt={t("nav.exitNode.flagAlt", { country: "Germany" })}
|
||||
className={
|
||||
"h-6 w-6 rounded-full border-[3px] border-nb-gray-850 shrink-0"
|
||||
}
|
||||
/>
|
||||
}
|
||||
title={"Exit Node Berlin"}
|
||||
title={t("nav.exitNode.title", { location: "Berlin" })}
|
||||
description={"100.92.14.37"}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
71
client/ui/frontend/src/lib/i18n.ts
Normal file
71
client/ui/frontend/src/lib/i18n.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import i18next from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
|
||||
import { Preferences, I18n } from "@bindings/services";
|
||||
|
||||
// Vite glob-imports every shipped bundle at build time. Adding a language
|
||||
// only requires dropping the new folder under src/i18n/locales/ and the
|
||||
// row in _index.json — no edit to this file. The `eager: true` import
|
||||
// keeps the bundles inlined in the main JS chunk, same shape as a static
|
||||
// import. Path is relative on purpose — alias-based globs (`@/…`) silently
|
||||
// resolve to an empty match in some Vite dev-mode setups.
|
||||
const bundleModules = import.meta.glob<Record<string, string>>(
|
||||
"../i18n/locales/*/common.json",
|
||||
{ eager: true, import: "default" },
|
||||
);
|
||||
|
||||
const resources: Record<string, { common: Record<string, string> }> = {};
|
||||
for (const path in bundleModules) {
|
||||
const match = path.match(/locales\/([^/]+)\/common\.json$/);
|
||||
if (match) {
|
||||
resources[match[1]] = { common: bundleModules[path] };
|
||||
}
|
||||
}
|
||||
|
||||
// initI18n is awaited from app.tsx before the first render. The Go-side
|
||||
// preferences.Store returns the in-memory default "en" when no on-disk
|
||||
// preferences file exists; if Get() rejects (daemon unreachable) we also
|
||||
// fall through with "en" so the UI still renders.
|
||||
export async function initI18n(): Promise<void> {
|
||||
let language = "en";
|
||||
try {
|
||||
const prefs = await Preferences.Get();
|
||||
if (prefs?.language) {
|
||||
language = prefs.language;
|
||||
}
|
||||
} catch {
|
||||
// Daemon / preferences store unreachable — fall through with "en".
|
||||
}
|
||||
|
||||
await i18next.use(initReactI18next).init({
|
||||
lng: language,
|
||||
fallbackLng: "en",
|
||||
defaultNS: "common",
|
||||
ns: ["common"],
|
||||
resources,
|
||||
interpolation: {
|
||||
prefix: "{",
|
||||
suffix: "}",
|
||||
escapeValue: false,
|
||||
},
|
||||
returnNull: false,
|
||||
});
|
||||
|
||||
// The event name + payload type come from Wails' generated module
|
||||
// augmentation (bindings/.../wails/v3/internal/eventdata.d.ts) which
|
||||
// extends @wailsio/runtime's CustomEvents interface, so e.data is
|
||||
// typed as UIPreferences without any hand-written cast.
|
||||
Events.On("netbird:preferences:changed", (e) => {
|
||||
const next = e.data?.language;
|
||||
if (next && next !== i18next.language) {
|
||||
void i18next.changeLanguage(next);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadLanguages() {
|
||||
return I18n.Languages();
|
||||
}
|
||||
|
||||
export default i18next;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/Button";
|
||||
import { useClientVersion } from "@/modules/auto-update/ClientVersionContext";
|
||||
import { cn } from "@/lib/cn";
|
||||
@@ -9,6 +10,7 @@ import { cn } from "@/lib/cn";
|
||||
// tray menu instead; the force-install branch (installing=true) takes over
|
||||
// with the full-screen UpdatingOverlay.
|
||||
export const UpdateAvailableBanner = () => {
|
||||
const { t } = useTranslation();
|
||||
const { updateVersion, enforced, installing, triggerUpdate } = useClientVersion();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
@@ -26,14 +28,14 @@ export const UpdateAvailableBanner = () => {
|
||||
)}
|
||||
>
|
||||
<p className={"text-sm text-nb-gray-900 pr-4 pl-2 font-medium"}>
|
||||
NetBird {updateVersion} is ready to install.
|
||||
{t("update.banner.message", { version: updateVersion })}
|
||||
</p>
|
||||
<div className={"flex gap-2"}>
|
||||
<Button variant={"subtle"} size={"xs"} onClick={() => setDismissed(true)}>
|
||||
Later
|
||||
{t("update.banner.later")}
|
||||
</Button>
|
||||
<Button variant={"primary"} size={"xs"} onClick={triggerUpdate}>
|
||||
Install now
|
||||
{t("update.banner.installNow")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowUpCircleIcon } from "lucide-react";
|
||||
import { IconButton } from "@/components/IconButton.tsx";
|
||||
import { Tooltip } from "@/components/Tooltip.tsx";
|
||||
import { useClientVersion } from "@/modules/auto-update/ClientVersionContext";
|
||||
|
||||
export const UpdateHeaderTrigger = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { updateAvailable } = useClientVersion();
|
||||
|
||||
if (!updateAvailable) return null;
|
||||
|
||||
return (
|
||||
<Tooltip content={"Update Available"}>
|
||||
<Tooltip content={t("update.header.tooltip")}>
|
||||
<div className={"relative h-11 w-11 flex items-center justify-center"}>
|
||||
<span
|
||||
className={
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Browser } from "@wailsio/runtime";
|
||||
import { Button } from "@/components/Button";
|
||||
import { useClientVersion } from "@/modules/auto-update/ClientVersionContext";
|
||||
@@ -10,8 +11,8 @@ function openUrl(url: string) {
|
||||
void Browser.OpenURL(url).catch(() => window.open(url, "_blank"));
|
||||
}
|
||||
|
||||
function formatLastChecked(date: Date) {
|
||||
return date.toLocaleString(undefined, {
|
||||
function formatLastChecked(date: Date, locale?: string) {
|
||||
return date.toLocaleString(locale, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
@@ -20,22 +21,23 @@ function formatLastChecked(date: Date) {
|
||||
}
|
||||
|
||||
export function UpdateVersionCard() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { updateVersion, enforced, triggerUpdate } = useClientVersion();
|
||||
|
||||
if (updateVersion) {
|
||||
return (
|
||||
<Card>
|
||||
<div>
|
||||
<Title>Version {updateVersion} is available.</Title>
|
||||
<Title>{t("update.card.versionAvailable", { version: updateVersion })}</Title>
|
||||
<Link
|
||||
url={`https://github.com/netbirdio/netbird/releases/tag/v${updateVersion}`}
|
||||
>
|
||||
What's new?
|
||||
{t("update.card.whatsNew")}
|
||||
</Link>
|
||||
</div>
|
||||
{enforced ? (
|
||||
<Button variant={"primary"} size={"xs"} onClick={triggerUpdate}>
|
||||
Install now
|
||||
{t("update.card.installNow")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -43,7 +45,7 @@ export function UpdateVersionCard() {
|
||||
size={"xs"}
|
||||
onClick={() => openUrl(GITHUB_RELEASES)}
|
||||
>
|
||||
Get installer
|
||||
{t("update.card.getInstaller")}
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
@@ -53,11 +55,15 @@ export function UpdateVersionCard() {
|
||||
return (
|
||||
<Card className={"max-w-md"}>
|
||||
<div>
|
||||
<Title>Last checked on {formatLastChecked(new Date())}</Title>
|
||||
<Link url={"https://github.com/netbirdio/netbird/releases/latest"}>Changelog</Link>
|
||||
<Title>
|
||||
{t("update.card.lastChecked", {
|
||||
date: formatLastChecked(new Date(), i18n.language),
|
||||
})}
|
||||
</Title>
|
||||
<Link url={GITHUB_RELEASES}>{t("update.card.changelog")}</Link>
|
||||
</div>
|
||||
<Button variant={"primary"} size={"xs"} onClick={triggerUpdate}>
|
||||
Check for updates
|
||||
{t("update.card.checkForUpdates")}
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Loader2, XCircle } from "lucide-react";
|
||||
import { Button } from "@/components/Button";
|
||||
|
||||
@@ -13,31 +14,38 @@ type Variant = {
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function classifyError(msg: string, version: string | null): Variant {
|
||||
function classifyError(
|
||||
msg: string,
|
||||
version: string | null,
|
||||
t: (key: string, options?: Record<string, unknown>) => string,
|
||||
): Variant {
|
||||
const lower = msg.toLowerCase();
|
||||
const target = version ? `v${version}` : "the new version";
|
||||
const target = version
|
||||
? t("update.overlay.error.targetVersion", { version })
|
||||
: t("update.overlay.error.targetFallback");
|
||||
if (lower.includes("timeout") || lower.includes("timed out")) {
|
||||
return {
|
||||
title: "Update Is Taking Too Long",
|
||||
description: `Installing ${target} took too long and didn't finish.`,
|
||||
title: t("update.overlay.error.timeoutTitle"),
|
||||
description: t("update.overlay.error.timeoutDescription", { target }),
|
||||
};
|
||||
}
|
||||
if (lower.includes("cancel")) {
|
||||
return {
|
||||
title: "Update Was Stopped",
|
||||
description: `The update to ${target} was canceled before it finished.`,
|
||||
title: t("update.overlay.error.canceledTitle"),
|
||||
description: t("update.overlay.error.canceledDescription", { target }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: "Couldn't Install the Update",
|
||||
description: `${target} couldn't be installed.`,
|
||||
message: msg || "unknown error",
|
||||
title: t("update.overlay.error.failTitle"),
|
||||
description: t("update.overlay.error.failDescription", { target }),
|
||||
message: msg || t("update.overlay.error.unknownMessage"),
|
||||
};
|
||||
}
|
||||
|
||||
export const UpdatingOverlay = ({ version, error, onDismiss }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const isError = Boolean(error);
|
||||
const errorInfo = error ? classifyError(error, version) : null;
|
||||
const errorInfo = error ? classifyError(error, version, t) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -75,8 +83,8 @@ export const UpdatingOverlay = ({ version, error, onDismiss }: Props) => {
|
||||
{isError
|
||||
? errorInfo!.title
|
||||
: version
|
||||
? `Updating NetBird to v${version}`
|
||||
: "Updating NetBird"}
|
||||
? t("update.overlay.updatingVersion", { version })
|
||||
: t("update.overlay.updating")}
|
||||
</p>
|
||||
<p className={"text-sm text-nb-gray-300"}>
|
||||
{isError ? (
|
||||
@@ -92,7 +100,7 @@ export const UpdatingOverlay = ({ version, error, onDismiss }: Props) => {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
"A newer version is available and is being installed. NetBird will restart automatically once the update is finished."
|
||||
t("update.overlay.description")
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -100,7 +108,7 @@ export const UpdatingOverlay = ({ version, error, onDismiss }: Props) => {
|
||||
{isError && (
|
||||
<div className={"wails-no-draggable"}>
|
||||
<Button variant={"secondary"} size={"xs"} onClick={onDismiss}>
|
||||
Close
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Debug as DebugSvc,
|
||||
} from "@bindings/services";
|
||||
import type { DebugBundleResult } from "@bindings/services/models.js";
|
||||
import i18next from "@/lib/i18n";
|
||||
import { useProfile } from "@/modules/profile/ProfileContext.tsx";
|
||||
|
||||
const NETBIRD_UPLOAD_URL = "https://upload.debug.netbird.io/upload-url";
|
||||
@@ -156,7 +157,7 @@ export const useDebugBundle = () => {
|
||||
}
|
||||
setStage({ kind: "idle" });
|
||||
await Dialogs.Error({
|
||||
Title: "Debug Bundle Failed",
|
||||
Title: i18next.t("settings.error.debugBundleTitle"),
|
||||
Message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SwitchItem } from "@/components/SwitchItem";
|
||||
import { SwitchItemGroup } from "@/components/SwitchItemGroup";
|
||||
|
||||
export type StatusFilter = "all" | "online" | "offline";
|
||||
|
||||
const FILTERS: { value: StatusFilter; label: string }[] = [
|
||||
{ value: "all", label: "All" },
|
||||
{ value: "online", label: "Online" },
|
||||
{ value: "offline", label: "Offline" },
|
||||
];
|
||||
|
||||
type Props = {
|
||||
value: StatusFilter;
|
||||
onChange: (value: StatusFilter) => void;
|
||||
@@ -16,13 +11,21 @@ type Props = {
|
||||
};
|
||||
|
||||
export const PeerFilters = ({ value, onChange, counts }: Props) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const filters: { value: StatusFilter; label: string }[] = [
|
||||
{ value: "all", label: t("peers.filter.all") },
|
||||
{ value: "online", label: t("peers.filter.online") },
|
||||
{ value: "offline", label: t("peers.filter.offline") },
|
||||
];
|
||||
|
||||
return (
|
||||
<SwitchItemGroup
|
||||
key={i18n.language}
|
||||
value={value}
|
||||
onChange={(v) => onChange(v as StatusFilter)}
|
||||
className={"w-full"}
|
||||
>
|
||||
{FILTERS.map((f) => (
|
||||
{filters.map((f) => (
|
||||
<SwitchItem key={f.value} value={f.value} className={"flex-1"}>
|
||||
{f.label}
|
||||
<span className={"font-normal text-nb-gray-200"}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { SearchInput } from "@/components/SearchInput";
|
||||
@@ -9,6 +10,7 @@ import { PeersList } from "./PeersList";
|
||||
const isOnline = (status: string) => status === "connected";
|
||||
|
||||
export const Peers = () => {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
|
||||
|
||||
@@ -37,7 +39,7 @@ export const Peers = () => {
|
||||
<div className={"flex flex-col w-full h-full min-h-0 pt-4"}>
|
||||
<div className={"flex flex-col gap-3 px-6"}>
|
||||
<SearchInput
|
||||
placeholder={"Search by peer name, DNS or IP address"}
|
||||
placeholder={t("peers.search.placeholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { Peer, PeerStatus } from "./types";
|
||||
|
||||
@@ -8,10 +9,11 @@ const DOT: Record<PeerStatus, string> = {
|
||||
};
|
||||
|
||||
export const PeersList = ({ data }: { data: Peer[] }) => {
|
||||
const { t } = useTranslation();
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className={"py-12 text-center text-sm text-nb-gray-400"}>
|
||||
No peers match the current filters.
|
||||
{t("peers.empty")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,13 @@ import {
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import { Connection, Peers, Profiles as ProfilesSvc } from "@bindings/services";
|
||||
import {
|
||||
Connection,
|
||||
ProfileSwitcher,
|
||||
Profiles as ProfilesSvc,
|
||||
} from "@bindings/services";
|
||||
import type { Profile } from "@bindings/services/models.js";
|
||||
import i18next from "@/lib/i18n";
|
||||
|
||||
type ProfileContextValue = {
|
||||
username: string;
|
||||
@@ -50,7 +55,7 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
|
||||
setProfiles(list);
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: "Load Profiles Failed",
|
||||
Title: i18next.t("profile.error.loadTitle"),
|
||||
Message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
} finally {
|
||||
@@ -64,26 +69,7 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const switchProfile = useCallback(
|
||||
async (name: string) => {
|
||||
// Mirror tray.go switchProfile: only reconnect when the daemon was
|
||||
// actively online. Calling Up on an Idle/NeedsLogin daemon makes
|
||||
// the daemon wait 50s on its internal waitForUp and return
|
||||
// DeadlineExceeded.
|
||||
let wasActive = false;
|
||||
try {
|
||||
const prev = await Peers.Get();
|
||||
const s = (prev?.status ?? "").toLowerCase();
|
||||
wasActive = s === "connected" || s === "connecting";
|
||||
} catch {
|
||||
wasActive = false;
|
||||
}
|
||||
|
||||
await ProfilesSvc.Switch({ profileName: name, username });
|
||||
|
||||
if (wasActive) {
|
||||
await Connection.Down();
|
||||
await Connection.Up({ profileName: name, username });
|
||||
}
|
||||
|
||||
await ProfileSwitcher.SwitchActive({ profileName: name, username });
|
||||
await refresh();
|
||||
},
|
||||
[username, refresh],
|
||||
|
||||
243
client/ui/frontend/src/modules/settings/LanguagePicker.tsx
Normal file
243
client/ui/frontend/src/modules/settings/LanguagePicker.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { Command } from "cmdk";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import { CheckIcon, ChevronDown, Search } from "lucide-react";
|
||||
import { Preferences } from "@bindings/services";
|
||||
import { LanguageCode, type Language } from "@bindings/i18n/models.js";
|
||||
import { HelpText } from "@/components/HelpText";
|
||||
import { Label } from "@/components/Label";
|
||||
import { loadLanguages } from "@/lib/i18n";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
// Flags live alongside the rest of the SVG flag library under
|
||||
// assets/flags/1x1 and are filename-matched to the language code
|
||||
// (de → de.svg, en → en.svg, hu → hu.svg). Vite eager-globs them at
|
||||
// build time; the JS bundle only holds URL refs, not the SVG bytes.
|
||||
const FLAG_URLS = import.meta.glob<string>("@/assets/flags/1x1/*.svg", {
|
||||
eager: true,
|
||||
import: "default",
|
||||
query: "?url",
|
||||
});
|
||||
|
||||
const flagByCode: Record<string, string> = {};
|
||||
for (const path in FLAG_URLS) {
|
||||
const match = path.match(/1x1\/([^/]+)\.svg$/);
|
||||
if (match) flagByCode[match[1]] = FLAG_URLS[path];
|
||||
}
|
||||
|
||||
const flagFor = (code: string): string | undefined =>
|
||||
flagByCode[code.toLowerCase().split("-")[0]];
|
||||
|
||||
function Flag({ code, label }: { code: string; label: string }) {
|
||||
const src = flagFor(code);
|
||||
if (!src) {
|
||||
return (
|
||||
<span
|
||||
className={"h-3.5 w-3.5 rounded-full bg-nb-gray-800 shrink-0 inline-block"}
|
||||
aria-hidden
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={label}
|
||||
className={"h-3.5 w-3.5 rounded-full object-cover shrink-0 select-none"}
|
||||
draggable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function LanguagePicker() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [languages, setLanguages] = useState<Language[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
loadLanguages()
|
||||
.then((list) => {
|
||||
if (!cancelled) setLanguages(list);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sorted = useMemo(
|
||||
() => [...languages].sort((a, b) => a.displayName.localeCompare(b.displayName)),
|
||||
[languages],
|
||||
);
|
||||
|
||||
const current = useMemo(
|
||||
() =>
|
||||
languages.find((l) => l.code === i18n.language) ??
|
||||
languages.find((l) => l.code === "en"),
|
||||
[languages, i18n.language],
|
||||
);
|
||||
|
||||
const select = async (code: string) => {
|
||||
if (busy || code === i18n.language) {
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await Preferences.SetLanguage(code as LanguageCode);
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: t("settings.error.saveTitle"),
|
||||
Message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
} finally {
|
||||
setBusy(false);
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={"flex items-center gap-6 justify-between"}>
|
||||
<div className={"flex-1 max-w-md"}>
|
||||
<Label as={"div"}>{t("settings.general.language.label")}</Label>
|
||||
<HelpText margin={false}>{t("settings.general.language.help")}</HelpText>
|
||||
</div>
|
||||
<div className={"shrink-0"}>
|
||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
type={"button"}
|
||||
disabled={busy || languages.length === 0}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 h-[40px] px-3 min-w-[240px]",
|
||||
"rounded-md border bg-white dark:bg-nb-gray-900",
|
||||
"border-neutral-200 dark:border-nb-gray-700",
|
||||
"text-xs font-semibold text-nb-gray-100 cursor-default outline-none",
|
||||
"hover:border-nb-gray-600 data-[state=open]:border-nb-gray-600",
|
||||
"disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
{current && <Flag code={current.code} label={current.displayName} />}
|
||||
<span className={"truncate flex-1 text-left"}>
|
||||
{current?.displayName ?? "—"}
|
||||
</span>
|
||||
<ChevronDown size={12} className={"text-nb-gray-400 shrink-0"} />
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
align={"start"}
|
||||
sideOffset={4}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
className={cn(
|
||||
"w-[var(--radix-popover-trigger-width)]",
|
||||
"rounded-md border border-nb-gray-700 bg-nb-gray-900 shadow-lg p-1 z-50",
|
||||
"origin-[var(--radix-popover-content-transform-origin)]",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
|
||||
"data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-1",
|
||||
"data-[side=top]:slide-in-from-bottom-1",
|
||||
"duration-150 ease-out",
|
||||
)}
|
||||
>
|
||||
<Command
|
||||
loop
|
||||
className={cn(
|
||||
"flex flex-col",
|
||||
"[&_[cmdk-input-wrapper]]:flex [&_[cmdk-input-wrapper]]:items-center",
|
||||
)}
|
||||
>
|
||||
<div className={"px-1 pb-1"}>
|
||||
<div className={"group flex items-center gap-2 px-1 h-8"}>
|
||||
<Search size={14} className={"text-nb-gray-200 shrink-0"} />
|
||||
<Command.Input
|
||||
autoFocus
|
||||
placeholder={t("settings.general.language.search")}
|
||||
className={cn(
|
||||
"w-full bg-transparent text-xs text-nb-gray-100 placeholder:text-nb-gray-300",
|
||||
"outline-none border-none",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea.Root type={"auto"} className={"overflow-hidden -mx-1"}>
|
||||
<ScrollArea.Viewport className={"max-h-64 px-1"}>
|
||||
<Command.List>
|
||||
<Command.Empty>
|
||||
<div
|
||||
className={
|
||||
"px-3 py-4 text-center text-[0.7rem] text-nb-gray-400"
|
||||
}
|
||||
>
|
||||
{t("settings.general.language.empty")}
|
||||
</div>
|
||||
</Command.Empty>
|
||||
|
||||
{sorted.map((lang) => {
|
||||
const checked = lang.code === i18n.language;
|
||||
return (
|
||||
<Command.Item
|
||||
key={lang.code}
|
||||
value={`${lang.displayName} ${lang.englishName} ${lang.code}`}
|
||||
onSelect={() => void select(lang.code)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-2 py-2 rounded-md cursor-default outline-none",
|
||||
"text-xs font-semibold text-nb-gray-100",
|
||||
"data-[selected=true]:bg-nb-gray-850 my-0.5",
|
||||
checked &&
|
||||
"bg-nb-gray-800 data-[selected=true]:bg-nb-gray-800",
|
||||
)}
|
||||
>
|
||||
<Flag
|
||||
code={lang.code}
|
||||
label={lang.displayName}
|
||||
/>
|
||||
<span className={"flex-1 truncate"}>
|
||||
{lang.displayName}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
"w-4 shrink-0 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
{checked && (
|
||||
<CheckIcon
|
||||
size={12}
|
||||
className={"text-white"}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.List>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
orientation={"vertical"}
|
||||
className={cn(
|
||||
"flex select-none touch-none transition-colors",
|
||||
"w-1.5 bg-transparent py-1",
|
||||
)}
|
||||
>
|
||||
<ScrollArea.Thumb
|
||||
className={
|
||||
"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"
|
||||
}
|
||||
/>
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
</Command>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import netbirdLogo from "@/assets/logos/netbird.svg";
|
||||
import { SwitchItem } from "@/components/SwitchItem";
|
||||
import { SwitchItemGroup } from "@/components/SwitchItemGroup";
|
||||
@@ -9,13 +10,20 @@ type Props = {
|
||||
};
|
||||
|
||||
export const ManagementServerSwitch = ({ value, onChange }: Props) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
return (
|
||||
<SwitchItemGroup value={value} onChange={(v) => onChange(v as ManagementMode)}>
|
||||
<SwitchItemGroup
|
||||
key={i18n.language}
|
||||
value={value}
|
||||
onChange={(v) => onChange(v as ManagementMode)}
|
||||
>
|
||||
<SwitchItem value={ManagementMode.Cloud}>
|
||||
<img src={netbirdLogo} alt={""} className={"h-[0.8rem] aspect-[31/23] shrink-0"} />
|
||||
Cloud
|
||||
{t("settings.general.management.cloud")}
|
||||
</SwitchItem>
|
||||
<SwitchItem value={ManagementMode.SelfHosted}>
|
||||
{t("settings.general.management.selfHosted")}
|
||||
</SwitchItem>
|
||||
<SwitchItem value={ManagementMode.SelfHosted}>Self-hosted</SwitchItem>
|
||||
</SwitchItemGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,35 +14,19 @@ import { SettingsAdvanced } from "@/modules/settings/SettingsAdvanced.tsx";
|
||||
import { SettingsTroubleshooting } from "@/modules/settings/SettingsTroubleshooting.tsx";
|
||||
import { SettingsAbout } from "@/modules/settings/SettingsAbout.tsx";
|
||||
|
||||
const LAST_TAB_KEY = "netbird:settings:lastTab";
|
||||
|
||||
const readLastTab = () => {
|
||||
try {
|
||||
return localStorage.getItem(LAST_TAB_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// The settings window always opens at General. The only way to land on a
|
||||
// different tab is via navigation state (e.g. the update-available header
|
||||
// trigger jumps to About). No persistence across opens — a user who wants
|
||||
// to revisit a deep tab gets there in two clicks.
|
||||
export const Settings = () => {
|
||||
const location = useLocation();
|
||||
const navState = location.state as { tab?: string } | null;
|
||||
const [active, setActive] = useState(
|
||||
() => navState?.tab ?? readLastTab() ?? "general",
|
||||
);
|
||||
const [active, setActive] = useState(() => navState?.tab ?? "general");
|
||||
|
||||
useEffect(() => {
|
||||
if (navState?.tab) setActive(navState.tab);
|
||||
}, [navState?.tab, location.key]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(LAST_TAB_KEY, active);
|
||||
} catch {
|
||||
// ignore quota / unavailable storage
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
<VerticalTabs value={active} onValueChange={setActive} className={"p-4"}>
|
||||
<SettingsNavigationTriggers />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Browser } from "@wailsio/runtime";
|
||||
import netbirdFull from "@/assets/logos/netbird-full.svg";
|
||||
import pkg from "../../../package.json";
|
||||
@@ -5,24 +6,25 @@ import { useStatus } from "@/hooks/useStatus";
|
||||
import { UpdateVersionCard } from "@/modules/auto-update/UpdateVersionCard";
|
||||
import { useAccentTrigger } from "@/modules/settings/SettingsAccent";
|
||||
|
||||
const LEGAL_LINKS: { label: string; url: string }[] = [
|
||||
{ label: "Imprint", url: "https://netbird.io/imprint" },
|
||||
{ label: "Privacy", url: "https://netbird.io/privacy" },
|
||||
{ label: "CLA", url: "https://netbird.io/cla" },
|
||||
{ label: "Terms of Service", url: "https://netbird.io/terms" },
|
||||
];
|
||||
|
||||
function openUrl(url: string) {
|
||||
void Browser.OpenURL(url).catch(() => window.open(url, "_blank"));
|
||||
}
|
||||
|
||||
export function SettingsAbout() {
|
||||
const { t } = useTranslation();
|
||||
const { status } = useStatus();
|
||||
const guiVersion = pkg.version;
|
||||
const daemonVersion = status?.daemonVersion ?? "—";
|
||||
|
||||
const handleVersionClick = useAccentTrigger();
|
||||
|
||||
const LEGAL_LINKS: { label: string; url: string }[] = [
|
||||
{ label: t("settings.about.links.imprint"), url: "https://netbird.io/imprint" },
|
||||
{ label: t("settings.about.links.privacy"), url: "https://netbird.io/privacy" },
|
||||
{ label: t("settings.about.links.cla"), url: "https://netbird.io/cla" },
|
||||
{ label: t("settings.about.links.terms"), url: "https://netbird.io/terms" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
@@ -35,15 +37,17 @@ export function SettingsAbout() {
|
||||
className={"text-sm font-semibold text-nb-gray-100 cursor-default select-none"}
|
||||
onClick={handleVersionClick}
|
||||
>
|
||||
NetBird Client v{daemonVersion}
|
||||
{t("settings.about.client", { version: daemonVersion })}
|
||||
</p>
|
||||
<p className={"text-sm text-nb-gray-300"}>
|
||||
{t("settings.about.gui", { version: guiVersion })}
|
||||
</p>
|
||||
<p className={"text-sm text-nb-gray-300"}>GUI v{guiVersion}</p>
|
||||
</div>
|
||||
|
||||
<UpdateVersionCard />
|
||||
|
||||
<p className={"text-sm text-nb-gray-300 text-center"}>
|
||||
© {new Date().getFullYear()} NetBird. All Rights Reserved.
|
||||
{t("settings.about.copyright", { year: new Date().getFullYear() })}
|
||||
</p>
|
||||
<div
|
||||
className={"flex flex-wrap justify-center gap-x-4 gap-y-1 text-xs text-nb-gray-200"}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Button from "@/components/Button";
|
||||
import { HelpText } from "@/components/HelpText";
|
||||
import { Input } from "@/components/Input";
|
||||
@@ -7,6 +8,7 @@ import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
|
||||
import { useSettings } from "@/modules/settings/SettingsContext.tsx";
|
||||
|
||||
export function SettingsAdvanced() {
|
||||
const { t } = useTranslation();
|
||||
const { config, saveFields } = useSettings();
|
||||
|
||||
const [values, setValues] = useState({
|
||||
@@ -35,9 +37,9 @@ export function SettingsAdvanced() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionGroup title={"Interface"}>
|
||||
<SectionGroup title={t("settings.advanced.section.interface")}>
|
||||
<Input
|
||||
label={"Name"}
|
||||
label={t("settings.advanced.interfaceName.label")}
|
||||
value={values.interfaceName}
|
||||
onChange={(e) =>
|
||||
setValues((v) => ({ ...v, interfaceName: e.target.value }))
|
||||
@@ -45,7 +47,7 @@ export function SettingsAdvanced() {
|
||||
/>
|
||||
<div className={"grid grid-cols-2 gap-4"}>
|
||||
<Input
|
||||
label={"Port"}
|
||||
label={t("settings.advanced.port.label")}
|
||||
type={"number"}
|
||||
value={values.wireguardPort}
|
||||
onChange={(e) =>
|
||||
@@ -56,7 +58,7 @@ export function SettingsAdvanced() {
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
label={"MTU"}
|
||||
label={t("settings.advanced.mtu.label")}
|
||||
type={"number"}
|
||||
value={values.mtu}
|
||||
onChange={(e) =>
|
||||
@@ -66,13 +68,11 @@ export function SettingsAdvanced() {
|
||||
</div>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Security"}>
|
||||
<SectionGroup title={t("settings.advanced.section.security")}>
|
||||
<div>
|
||||
<Label as={"div"}>Pre-shared Key</Label>
|
||||
<Label as={"div"}>{t("settings.advanced.psk.label")}</Label>
|
||||
<HelpText>
|
||||
Optional WireGuard PSK for extra symmetric encryption. Not the same as a
|
||||
NetBird Setup Key. You will only communicate with peers that use the same
|
||||
pre-shared key.
|
||||
{t("settings.advanced.psk.help")}
|
||||
</HelpText>
|
||||
<Input
|
||||
type={"password"}
|
||||
@@ -94,7 +94,7 @@ export function SettingsAdvanced() {
|
||||
disabled={!hasChanges || saving}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save Changes
|
||||
{t("common.saveChanges")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import { Settings as SettingsSvc } from "@bindings/services";
|
||||
import type { Config } from "@bindings/services/models.js";
|
||||
import i18next from "@/lib/i18n";
|
||||
import { useProfile } from "@/modules/profile/ProfileContext.tsx";
|
||||
import { SkeletonSettings } from "@/modules/skeletons/SkeletonSettings.tsx";
|
||||
|
||||
@@ -52,7 +53,7 @@ const useSettingsState = () => {
|
||||
setConfig(c);
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: "Load Settings Failed",
|
||||
Title: i18next.t("settings.error.loadTitle"),
|
||||
Message: errorMessage(e),
|
||||
});
|
||||
}
|
||||
@@ -82,7 +83,7 @@ const useSettingsState = () => {
|
||||
});
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: "Save Settings Failed",
|
||||
Title: i18next.t("settings.error.saveTitle"),
|
||||
Message: errorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/Button";
|
||||
import FancyToggleSwitch from "@/components/FancyToggleSwitch";
|
||||
import { HelpText } from "@/components/HelpText";
|
||||
@@ -8,8 +9,10 @@ import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
|
||||
import { useSettings } from "@/modules/settings/SettingsContext.tsx";
|
||||
import { ManagementServerSwitch } from "@/modules/settings/ManagementServerSwitch.tsx";
|
||||
import { ManagementMode, useManagementUrl } from "@/modules/settings/useManagementUrl.ts";
|
||||
import { LanguagePicker } from "@/modules/settings/LanguagePicker.tsx";
|
||||
|
||||
export function SettingsGeneral() {
|
||||
const { t } = useTranslation();
|
||||
const { config, setField } = useSettings();
|
||||
const { mode, setMode, setUrl, displayUrl, showError, canSave, save } = useManagementUrl();
|
||||
|
||||
@@ -27,29 +30,29 @@ export function SettingsGeneral() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionGroup title={"General"}>
|
||||
<SectionGroup title={t("settings.general.section.general")}>
|
||||
<LanguagePicker />
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableAutoConnect}
|
||||
onChange={(v) => setField("disableAutoConnect", !v)}
|
||||
label={"Connect on Startup"}
|
||||
helpText={"Automatically establish a connection when the service starts."}
|
||||
label={t("settings.general.connectOnStartup.label")}
|
||||
helpText={t("settings.general.connectOnStartup.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableNotifications}
|
||||
onChange={(v) => setField("disableNotifications", !v)}
|
||||
label={"Desktop Notifications"}
|
||||
helpText={"Show desktop notifications for new updates and connection events."}
|
||||
label={t("settings.general.notifications.label")}
|
||||
helpText={t("settings.general.notifications.help")}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Connection"}>
|
||||
<SectionGroup title={t("settings.general.section.connection")}>
|
||||
<div>
|
||||
<div className={"flex items-start gap-3"}>
|
||||
<div className={"flex-1 min-w-0"}>
|
||||
<Label as={"div"}>Management Server</Label>
|
||||
<Label as={"div"}>{t("settings.general.management.label")}</Label>
|
||||
<HelpText>
|
||||
Connect to NetBird Cloud or your own self-hosted management server.
|
||||
Changes will reconnect the client.
|
||||
{t("settings.general.management.help")}
|
||||
</HelpText>
|
||||
</div>
|
||||
<ManagementServerSwitch value={mode} onChange={setMode} />
|
||||
@@ -60,10 +63,10 @@ export function SettingsGeneral() {
|
||||
ref={inputRef}
|
||||
value={displayUrl}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder={"https://netbird.selfhosted.com:443"}
|
||||
placeholder={t("settings.general.management.urlPlaceholder")}
|
||||
error={
|
||||
showError
|
||||
? "Please enter a valid URL, e.g., https://netbird.selfhosted.com:443"
|
||||
? t("settings.general.management.urlError")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
@@ -73,7 +76,7 @@ export function SettingsGeneral() {
|
||||
disabled={!canSave}
|
||||
onClick={() => save()}
|
||||
>
|
||||
Save
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tooltip } from "@/components/Tooltip.tsx";
|
||||
import { VerticalTabs } from "@/components/VerticalTabs.tsx";
|
||||
import { UpdateBadge } from "@/modules/auto-update/UpdateBadge.tsx";
|
||||
@@ -13,10 +14,11 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
export const SettingsNavigationTriggers = () => {
|
||||
const { t } = useTranslation();
|
||||
const { updateAvailable } = useClientVersion();
|
||||
|
||||
const aboutAdornment = updateAvailable ? (
|
||||
<Tooltip content={"Update Available"} side={"right"}>
|
||||
<Tooltip content={t("settings.tabs.updateAvailable")} side={"right"}>
|
||||
<UpdateBadge />
|
||||
</Tooltip>
|
||||
) : undefined;
|
||||
@@ -27,37 +29,37 @@ export const SettingsNavigationTriggers = () => {
|
||||
<VerticalTabs.Trigger
|
||||
value={"general"}
|
||||
icon={SlidersHorizontalIcon}
|
||||
title={"General"}
|
||||
title={t("settings.tabs.general")}
|
||||
/>
|
||||
<VerticalTabs.Trigger
|
||||
value={"network"}
|
||||
icon={NetworkIcon}
|
||||
title={"Network"}
|
||||
title={t("settings.tabs.network")}
|
||||
/>
|
||||
<VerticalTabs.Trigger
|
||||
value={"security"}
|
||||
icon={ShieldIcon}
|
||||
title={"Security"}
|
||||
title={t("settings.tabs.security")}
|
||||
/>
|
||||
<VerticalTabs.Trigger
|
||||
value={"ssh"}
|
||||
icon={SquareTerminalIcon}
|
||||
title={"SSH"}
|
||||
title={t("settings.tabs.ssh")}
|
||||
/>
|
||||
<VerticalTabs.Trigger
|
||||
value={"advanced"}
|
||||
icon={BoltIcon}
|
||||
title={"Advanced"}
|
||||
title={t("settings.tabs.advanced")}
|
||||
/>
|
||||
<VerticalTabs.Trigger
|
||||
value={"troubleshooting"}
|
||||
icon={LifeBuoyIcon}
|
||||
title={"Troubleshooting"}
|
||||
title={t("settings.tabs.troubleshooting")}
|
||||
/>
|
||||
<VerticalTabs.Trigger
|
||||
value={"about"}
|
||||
icon={InfoIcon}
|
||||
title={"About"}
|
||||
title={t("settings.tabs.about")}
|
||||
adornment={aboutAdornment}
|
||||
/>
|
||||
</VerticalTabs.List>
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FancyToggleSwitch from "@/components/FancyToggleSwitch";
|
||||
import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
|
||||
import { useSettings } from "@/modules/settings/SettingsContext.tsx";
|
||||
|
||||
export function SettingsNetwork() {
|
||||
const { t } = useTranslation();
|
||||
const { config, setField } = useSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionGroup title={"Connectivity"}>
|
||||
<SectionGroup title={t("settings.network.section.connectivity")}>
|
||||
<FancyToggleSwitch
|
||||
value={config.lazyConnectionEnabled}
|
||||
onChange={(v) => setField("lazyConnectionEnabled", v)}
|
||||
label={"Lazy Connections"}
|
||||
helpText={
|
||||
"Instead of maintaining always-on connections, NetBird activates them on-demand based on activity or signaling."
|
||||
}
|
||||
label={t("settings.network.lazy.label")}
|
||||
helpText={t("settings.network.lazy.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={config.networkMonitor}
|
||||
onChange={(v) => setField("networkMonitor", v)}
|
||||
label={"Reconnect on Network Change"}
|
||||
helpText={
|
||||
"Monitor the network and automatically reconnect on changes such as Wi-Fi switching, Ethernet changes, or resume from sleep."
|
||||
}
|
||||
label={t("settings.network.monitor.label")}
|
||||
helpText={t("settings.network.monitor.help")}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Routing & DNS"}>
|
||||
<SectionGroup title={t("settings.network.section.routingDns")}>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableDns}
|
||||
onChange={(v) => setField("disableDns", !v)}
|
||||
label={"Enable DNS"}
|
||||
helpText={"Apply NetBird-managed DNS settings to the host resolver."}
|
||||
label={t("settings.network.dns.label")}
|
||||
helpText={t("settings.network.dns.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableClientRoutes}
|
||||
onChange={(v) => setField("disableClientRoutes", !v)}
|
||||
label={"Enable Client Routes"}
|
||||
helpText={"Accept routes from other peers to reach their networks."}
|
||||
label={t("settings.network.clientRoutes.label")}
|
||||
helpText={t("settings.network.clientRoutes.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableServerRoutes}
|
||||
onChange={(v) => setField("disableServerRoutes", !v)}
|
||||
label={"Enable Server Routes"}
|
||||
helpText={"Advertise this host's local routes to other peers."}
|
||||
label={t("settings.network.serverRoutes.label")}
|
||||
helpText={t("settings.network.serverRoutes.help")}
|
||||
/>
|
||||
</SectionGroup>
|
||||
</>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FancyToggleSwitch from "@/components/FancyToggleSwitch";
|
||||
import { HelpText } from "@/components/HelpText";
|
||||
import { Input } from "@/components/Input";
|
||||
@@ -8,11 +9,11 @@ import { useSettings } from "@/modules/settings/SettingsContext.tsx";
|
||||
import { type ChangeEvent, useEffect, useState } from "react";
|
||||
|
||||
export function SettingsSSH() {
|
||||
const { t } = useTranslation();
|
||||
const { config, setField } = useSettings();
|
||||
const isSSHServerEnabled = config.serverSshAllowed;
|
||||
const [jwtTtlInput, setJwtTtlInput] = useState(String(config.sshJwtCacheTtl));
|
||||
|
||||
// Keep the local input in sync when the config changes from elsewhere
|
||||
useEffect(() => {
|
||||
setJwtTtlInput(String(config.sshJwtCacheTtl));
|
||||
}, [config.sshJwtCacheTtl]);
|
||||
@@ -40,58 +41,48 @@ export function SettingsSSH() {
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<SectionGroup title={"Server"}>
|
||||
<SectionGroup title={t("settings.ssh.section.server")}>
|
||||
<FancyToggleSwitch
|
||||
value={config.serverSshAllowed}
|
||||
onChange={(v) => setField("serverSshAllowed", v)}
|
||||
label={"Enable SSH Server"}
|
||||
helpText={
|
||||
"Run the NetBird SSH server on this host so other peers can connect to it."
|
||||
}
|
||||
label={t("settings.ssh.server.label")}
|
||||
helpText={t("settings.ssh.server.help")}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Capabilities"} disabled={!isSSHServerEnabled}>
|
||||
<SectionGroup title={t("settings.ssh.section.capabilities")} disabled={!isSSHServerEnabled}>
|
||||
<FancyToggleSwitch
|
||||
value={config.enableSshRoot}
|
||||
onChange={(v) => setField("enableSshRoot", v)}
|
||||
label={"Allow Root Login"}
|
||||
helpText={
|
||||
"Let peers sign in as the root user. Disable to require a non-privileged account."
|
||||
}
|
||||
label={t("settings.ssh.root.label")}
|
||||
helpText={t("settings.ssh.root.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={config.enableSshSftp}
|
||||
onChange={(v) => setField("enableSshSftp", v)}
|
||||
label={"Allow SFTP"}
|
||||
helpText={"Transfer files securely using native SFTP or SCP clients."}
|
||||
label={t("settings.ssh.sftp.label")}
|
||||
helpText={t("settings.ssh.sftp.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={config.enableSshLocalPortForwarding}
|
||||
onChange={(v) => setField("enableSshLocalPortForwarding", v)}
|
||||
label={"Local Port Forwarding"}
|
||||
helpText={
|
||||
"Let connecting peers tunnel local ports to services reachable from this host."
|
||||
}
|
||||
label={t("settings.ssh.localForward.label")}
|
||||
helpText={t("settings.ssh.localForward.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={config.enableSshRemotePortForwarding}
|
||||
onChange={(v) => setField("enableSshRemotePortForwarding", v)}
|
||||
label={"Remote Port Forwarding"}
|
||||
helpText={
|
||||
"Let connecting peers expose ports on this host back to their own machine."
|
||||
}
|
||||
label={t("settings.ssh.remoteForward.label")}
|
||||
helpText={t("settings.ssh.remoteForward.help")}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Authentication"} disabled={!isSSHServerEnabled}>
|
||||
<SectionGroup title={t("settings.ssh.section.authentication")} disabled={!isSSHServerEnabled}>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableSshAuth}
|
||||
onChange={(v) => setField("disableSshAuth", !v)}
|
||||
label={"Enable JWT Authentication"}
|
||||
helpText={
|
||||
"Verify each SSH session against your IdP for user identity and audit. Disable to rely on network ACL policies only, useful when no IdP is available."
|
||||
}
|
||||
label={t("settings.ssh.jwt.label")}
|
||||
helpText={t("settings.ssh.jwt.help")}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -100,11 +91,9 @@ export function SettingsSSH() {
|
||||
)}
|
||||
>
|
||||
<div className={"flex-1 max-w-md"}>
|
||||
<Label as={"div"}>JWT Cache TTL</Label>
|
||||
<Label as={"div"}>{t("settings.ssh.jwtTtl.label")}</Label>
|
||||
<HelpText margin={false}>
|
||||
How long this client caches a JWT before prompting again on outgoing SSH
|
||||
connections. Set to 0 to disable caching and authenticate on every
|
||||
connection.
|
||||
{t("settings.ssh.jwtTtl.help")}
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-40 shrink-0"}>
|
||||
@@ -114,7 +103,7 @@ export function SettingsSSH() {
|
||||
value={jwtTtlInput}
|
||||
onChange={handleJwtTtlChange}
|
||||
onBlur={handleJwtTtlBlur}
|
||||
customSuffix={"Second(s)"}
|
||||
customSuffix={t("settings.ssh.jwtTtl.suffix")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,49 +1,43 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FancyToggleSwitch from "@/components/FancyToggleSwitch";
|
||||
import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
|
||||
import { useSettings } from "@/modules/settings/SettingsContext.tsx";
|
||||
|
||||
export function SettingsSecurity() {
|
||||
const { t } = useTranslation();
|
||||
const { config, setField } = useSettings();
|
||||
return (
|
||||
<>
|
||||
<SectionGroup title={"Firewall"}>
|
||||
<SectionGroup title={t("settings.security.section.firewall")}>
|
||||
<FancyToggleSwitch
|
||||
value={config.blockInbound}
|
||||
onChange={(v) => setField("blockInbound", v)}
|
||||
label={"Block Inbound Traffic"}
|
||||
helpText={
|
||||
"Reject unsolicited connections from peers to this device and any networks it routes. Outbound traffic is unaffected."
|
||||
}
|
||||
label={t("settings.security.blockInbound.label")}
|
||||
helpText={t("settings.security.blockInbound.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={config.blockLanAccess}
|
||||
onChange={(v) => setField("blockLanAccess", v)}
|
||||
label={"Block LAN Access"}
|
||||
helpText={
|
||||
"Prevent peers from reaching your local network or its devices when this device routes their traffic."
|
||||
}
|
||||
label={t("settings.security.blockLan.label")}
|
||||
helpText={t("settings.security.blockLan.help")}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Encryption"}>
|
||||
<SectionGroup title={t("settings.security.section.encryption")}>
|
||||
<FancyToggleSwitch
|
||||
value={config.rosenpassEnabled}
|
||||
onChange={(v) => {
|
||||
setField("rosenpassEnabled", v);
|
||||
if (!v) setField("rosenpassPermissive", false);
|
||||
}}
|
||||
label={"Enable Quantum-Resistance"}
|
||||
helpText={
|
||||
"Add a post-quantum key exchange via Rosenpass on top of WireGuard®."
|
||||
}
|
||||
label={t("settings.security.rosenpass.label")}
|
||||
helpText={t("settings.security.rosenpass.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={config.rosenpassPermissive}
|
||||
onChange={(v) => setField("rosenpassPermissive", v)}
|
||||
label={"Enable Permissive Mode"}
|
||||
helpText={
|
||||
"Allow connections to peers without quantum-resistance support."
|
||||
}
|
||||
label={t("settings.security.rosenpassPermissive.label")}
|
||||
helpText={t("settings.security.rosenpassPermissive.help")}
|
||||
disabled={!config.rosenpassEnabled}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { FolderOpen } from "lucide-react";
|
||||
import { Debug as DebugSvc } from "@bindings/services";
|
||||
import type { DebugBundleResult } from "@bindings/services/models.js";
|
||||
@@ -14,6 +15,7 @@ import { useDebugBundleContext } from "@/modules/debug-bundle/useDebugBundleCont
|
||||
import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
|
||||
|
||||
export function SettingsTroubleshooting() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
anonymize,
|
||||
setAnonymize,
|
||||
@@ -45,39 +47,34 @@ export function SettingsTroubleshooting() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionGroup title={"Debug bundle"}>
|
||||
<SectionGroup title={t("settings.troubleshooting.section.title")}>
|
||||
<HelpText className={"-mt-2 mb-2"}>
|
||||
A debug bundle helps NetBird support investigate connection problems. <br /> It's a
|
||||
.zip file with logs, system details and debug information from your device.
|
||||
<Trans i18nKey={"settings.troubleshooting.intro"} components={{ br: <br /> }} />
|
||||
</HelpText>
|
||||
|
||||
<FancyToggleSwitch
|
||||
value={anonymize}
|
||||
onChange={setAnonymize}
|
||||
label={"Anonymize Sensitive Information"}
|
||||
helpText={"Hides public IP addresses and non-NetBird domains from logs."}
|
||||
label={t("settings.troubleshooting.anonymize.label")}
|
||||
helpText={t("settings.troubleshooting.anonymize.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={systemInfo}
|
||||
onChange={setSystemInfo}
|
||||
label={"Include System Information"}
|
||||
helpText={"Include OS, kernel, network interfaces, and routing tables."}
|
||||
label={t("settings.troubleshooting.systemInfo.label")}
|
||||
helpText={t("settings.troubleshooting.systemInfo.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={upload}
|
||||
onChange={setUpload}
|
||||
label={"Upload Bundle to NetBird Servers"}
|
||||
helpText={
|
||||
"Securely uploads the bundle and returns an upload key. Share the key with NetBird support over GitHub or Slack instead of attaching the file directly."
|
||||
}
|
||||
label={t("settings.troubleshooting.upload.label")}
|
||||
helpText={t("settings.troubleshooting.upload.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={trace}
|
||||
onChange={setTrace}
|
||||
label={"Capture Trace Logs"}
|
||||
helpText={
|
||||
"Raises logging to TRACE and cycles NetBird up and down to capture connection logs. The previous level is restored after the bundle is built."
|
||||
}
|
||||
label={t("settings.troubleshooting.trace.label")}
|
||||
helpText={t("settings.troubleshooting.trace.help")}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -86,9 +83,9 @@ export function SettingsTroubleshooting() {
|
||||
)}
|
||||
>
|
||||
<div className={"flex-1 max-w-md"}>
|
||||
<Label as={"div"}>Capture Duration</Label>
|
||||
<Label as={"div"}>{t("settings.troubleshooting.duration.label")}</Label>
|
||||
<HelpText margin={false}>
|
||||
How long to capture trace logs before generating the bundle.
|
||||
{t("settings.troubleshooting.duration.help")}
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-40 shrink-0"}>
|
||||
@@ -100,7 +97,7 @@ export function SettingsTroubleshooting() {
|
||||
onChange={(e) =>
|
||||
setTraceMinutes(Math.max(1, Math.min(30, Number(e.target.value) || 1)))
|
||||
}
|
||||
customSuffix={"Minute(s)"}
|
||||
customSuffix={t("settings.troubleshooting.duration.suffix")}
|
||||
disabled={!trace}
|
||||
/>
|
||||
</div>
|
||||
@@ -108,7 +105,7 @@ export function SettingsTroubleshooting() {
|
||||
|
||||
<BottomBar>
|
||||
<Button variant={"primary"} size={"md"} onClick={run}>
|
||||
Create Bundle
|
||||
{t("settings.troubleshooting.create")}
|
||||
</Button>
|
||||
</BottomBar>
|
||||
</SectionGroup>
|
||||
@@ -116,17 +113,18 @@ export function SettingsTroubleshooting() {
|
||||
}
|
||||
|
||||
function ProgressSection({ stage, onCancel }: { stage: DebugStage; onCancel: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const cancelling = stage.kind === "cancelling";
|
||||
return (
|
||||
<StatusPanel
|
||||
variant={"loading"}
|
||||
title={stageLabel(stage)}
|
||||
description={
|
||||
"Collecting logs, system details, and connection state. This usually takes a moment — keep this window open until it completes."
|
||||
}
|
||||
title={stageLabel(stage, t)}
|
||||
description={t("settings.troubleshooting.progress.description")}
|
||||
actions={
|
||||
<Button variant={"secondary"} size={"xs"} onClick={onCancel} disabled={cancelling}>
|
||||
{cancelling ? "Cancelling…" : "Cancel"}
|
||||
{cancelling
|
||||
? t("settings.troubleshooting.cancelling")
|
||||
: t("common.cancel")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
@@ -142,6 +140,7 @@ function DoneResult({
|
||||
uploaded: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const showKey = uploaded && Boolean(result.uploadedKey);
|
||||
const uploadFailed = uploaded && !result.uploadedKey;
|
||||
const onRevealPath = () => {
|
||||
@@ -151,26 +150,30 @@ function DoneResult({
|
||||
return (
|
||||
<StatusPanel
|
||||
variant={"success"}
|
||||
title={showKey ? "Debug bundle successfully uploaded!" : "Bundle saved"}
|
||||
title={
|
||||
showKey
|
||||
? t("settings.troubleshooting.done.uploadedTitle")
|
||||
: t("settings.troubleshooting.done.savedTitle")
|
||||
}
|
||||
description={
|
||||
showKey
|
||||
? "Share the upload key below with NetBird support. A local copy was also saved on your device."
|
||||
: "Your debug bundle has been saved locally."
|
||||
? t("settings.troubleshooting.done.uploadedDescription")
|
||||
: t("settings.troubleshooting.done.savedDescription")
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Button variant={"secondary"} size={"xs"} onClick={onClose}>
|
||||
Close
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
{showKey ? (
|
||||
<Button variant={"primary"} size={"xs"} copy={result.uploadedKey}>
|
||||
Copy Key
|
||||
{t("settings.troubleshooting.done.copyKey")}
|
||||
</Button>
|
||||
) : (
|
||||
result.path && (
|
||||
<Button variant={"primary"} size={"xs"} onClick={onRevealPath}>
|
||||
<FolderOpen size={12} />
|
||||
Open Folder
|
||||
{t("settings.troubleshooting.done.openFolder")}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
@@ -189,7 +192,7 @@ function DoneResult({
|
||||
type={"button"}
|
||||
onClick={onRevealPath}
|
||||
className={"pointer-events-auto hover:text-white transition-all"}
|
||||
aria-label={"Open file location"}
|
||||
aria-label={t("settings.troubleshooting.done.openFileLocation")}
|
||||
>
|
||||
<FolderOpen size={16} />
|
||||
</button>
|
||||
@@ -203,9 +206,11 @@ function DoneResult({
|
||||
"rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300"
|
||||
}
|
||||
>
|
||||
Upload failed
|
||||
{result.uploadFailureReason ? `: ${result.uploadFailureReason}` : "."} The
|
||||
bundle is still saved locally.
|
||||
{result.uploadFailureReason
|
||||
? t("settings.troubleshooting.uploadFailedWithReason", {
|
||||
reason: result.uploadFailureReason,
|
||||
})
|
||||
: t("settings.troubleshooting.uploadFailed")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -227,26 +232,27 @@ function BottomBar({ children }: { children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
const stageLabel = (stage: DebugStage): string => {
|
||||
const stageLabel = (stage: DebugStage, t: (key: string, options?: Record<string, unknown>) => string): string => {
|
||||
switch (stage.kind) {
|
||||
case "preparing-trace":
|
||||
return "Switching to trace logging…";
|
||||
return t("settings.troubleshooting.stage.preparingTrace");
|
||||
case "reconnecting":
|
||||
return "Reconnecting NetBird…";
|
||||
return t("settings.troubleshooting.stage.reconnecting");
|
||||
case "capturing": {
|
||||
const fmt = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
||||
return `Capturing logs — ${fmt(
|
||||
stage.totalSec - stage.remainingSec,
|
||||
)} / ${fmt(stage.totalSec)}`;
|
||||
return t("settings.troubleshooting.stage.capturing", {
|
||||
elapsed: fmt(stage.totalSec - stage.remainingSec),
|
||||
total: fmt(stage.totalSec),
|
||||
});
|
||||
}
|
||||
case "restoring-level":
|
||||
return "Restoring previous log level…";
|
||||
return t("settings.troubleshooting.stage.restoring");
|
||||
case "bundling":
|
||||
return "Generating debug bundle…";
|
||||
return t("settings.troubleshooting.stage.bundling");
|
||||
case "uploading":
|
||||
return "Uploading to NetBird…";
|
||||
return t("settings.troubleshooting.stage.uploading");
|
||||
case "cancelling":
|
||||
return "Cancelling…";
|
||||
return t("settings.troubleshooting.stage.cancelling");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import i18next from "@/lib/i18n";
|
||||
import { useSettings } from "@/modules/settings/SettingsContext.tsx";
|
||||
|
||||
export enum ManagementMode {
|
||||
@@ -60,16 +61,17 @@ export function useManagementUrl() {
|
||||
// Switching from a self-hosted management server to NetBird Cloud
|
||||
// re-points the client at a different deployment and forces a
|
||||
// reconnect/re-login. Confirm before applying.
|
||||
const cancelLabel = i18next.t("common.cancel");
|
||||
const confirmLabel = i18next.t("settings.general.management.switchCloudConfirm");
|
||||
void Dialogs.Warning({
|
||||
Title: "Switch to NetBird Cloud?",
|
||||
Message:
|
||||
"This will disconnect from your self-hosted management server and reconnect to NetBird Cloud. You may need to log in again.",
|
||||
Title: i18next.t("settings.general.management.switchCloudTitle"),
|
||||
Message: i18next.t("settings.general.management.switchCloudMessage"),
|
||||
Buttons: [
|
||||
{ Label: "Cancel", IsCancel: true, IsDefault: true },
|
||||
{ Label: "Switch to Cloud" },
|
||||
{ Label: cancelLabel, IsCancel: true, IsDefault: true },
|
||||
{ Label: confirmLabel },
|
||||
],
|
||||
}).then((result) => {
|
||||
if (result !== "Switch to Cloud") return;
|
||||
if (result !== confirmLabel) return;
|
||||
setModeState(ManagementMode.Cloud);
|
||||
void saveField("managementUrl", CLOUD_MANAGEMENT_URL);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import { Loader2 } from "lucide-react";
|
||||
@@ -9,6 +10,7 @@ import netbirdFull from "@/assets/logos/netbird-full.svg";
|
||||
const EVENT_CANCEL = "browser-login:cancel";
|
||||
|
||||
export default function BrowserLogin() {
|
||||
const { t } = useTranslation();
|
||||
const [params] = useSearchParams();
|
||||
const uri = params.get("uri") ?? "";
|
||||
|
||||
@@ -24,30 +26,25 @@ export default function BrowserLogin() {
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center gap-3 p-8 text-center">
|
||||
<img src={netbirdFull} alt="NetBird" className="mb-2 h-9" />
|
||||
<h1 className="text-lg font-semibold text-white">
|
||||
Continue in your browser to complete the login
|
||||
</h1>
|
||||
<p className="max-w-sm text-sm text-nb-gray-400">
|
||||
Please complete the account authentication process in the browser tab
|
||||
and continue from there.
|
||||
</p>
|
||||
<h1 className="text-lg font-semibold text-white">{t("browserLogin.title")}</h1>
|
||||
<p className="max-w-sm text-sm text-nb-gray-400">{t("browserLogin.description")}</p>
|
||||
<div className="flex items-center gap-2 text-sm text-nb-gray-400">
|
||||
<Loader2 className="h-4 w-4 animate-spin" strokeWidth={1.5} />
|
||||
Waiting for sign-in…
|
||||
{t("browserLogin.waiting")}
|
||||
</div>
|
||||
<p className="text-sm text-nb-gray-400">
|
||||
Not seeing the browser tab?{" "}
|
||||
{t("browserLogin.notSeeing")}{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={tryAgain}
|
||||
disabled={!uri}
|
||||
className="text-netbird hover:underline disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
Try again
|
||||
{t("browserLogin.tryAgain")}
|
||||
</button>
|
||||
</p>
|
||||
<Button variant="secondary" onClick={cancel} className="mt-2">
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ShieldAlertIcon } from "lucide-react";
|
||||
import { Button } from "@/components/Button";
|
||||
|
||||
export default function SessionExpired() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
@@ -15,16 +17,18 @@ export default function SessionExpired() {
|
||||
>
|
||||
<ShieldAlertIcon size={22} />
|
||||
</div>
|
||||
<h1 className={"text-base font-semibold text-nb-gray-100"}>Session expired</h1>
|
||||
<h1 className={"text-base font-semibold text-nb-gray-100"}>
|
||||
{t("sessionExpired.title")}
|
||||
</h1>
|
||||
<p className={"text-xs text-nb-gray-400 mt-1.5 max-w-[20rem] leading-snug"}>
|
||||
Your NetBird session has expired. Sign in again to keep your devices connected.
|
||||
{t("sessionExpired.description")}
|
||||
</p>
|
||||
<div className={"flex gap-2 mt-5 w-full max-w-[18rem]"}>
|
||||
<Button variant={"secondary"} size={"xs"} className={"flex-1"}>
|
||||
Later
|
||||
{t("sessionExpired.later")}
|
||||
</Button>
|
||||
<Button variant={"primary"} size={"xs"} className={"flex-1"}>
|
||||
Sign in
|
||||
{t("sessionExpired.signIn")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import { Update as UpdateSvc } from "@bindings/services";
|
||||
import i18next from "@/lib/i18n";
|
||||
|
||||
const TIMEOUT_MS = 15 * 60 * 1000;
|
||||
|
||||
const showError = (message: string) =>
|
||||
Dialogs.Error({ Title: "Update Failed", Message: message });
|
||||
Dialogs.Error({ Title: i18next.t("update.page.failedTitle"), Message: message });
|
||||
|
||||
export default function Update() {
|
||||
const { t } = useTranslation();
|
||||
const [done, setDone] = useState(false);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
@@ -25,7 +28,7 @@ export default function Update() {
|
||||
if (Date.now() - start > TIMEOUT_MS) {
|
||||
clearInterval(timer);
|
||||
setFailed(true);
|
||||
void showError("Update timed out.");
|
||||
void showError(i18next.t("update.page.timeoutMessage"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -53,15 +56,19 @@ export default function Update() {
|
||||
<div className="flex h-full items-center justify-center p-6">
|
||||
<div className="text-center">
|
||||
{done ? (
|
||||
<h1 className="text-xl font-semibold text-green-500">Update complete</h1>
|
||||
<h1 className="text-xl font-semibold text-green-500">
|
||||
{t("update.page.complete")}
|
||||
</h1>
|
||||
) : failed ? (
|
||||
<h1 className="text-xl font-semibold text-red-500">Update failed</h1>
|
||||
<h1 className="text-xl font-semibold text-red-500">
|
||||
{t("update.page.failed")}
|
||||
</h1>
|
||||
) : (
|
||||
<>
|
||||
<Loader2 className="mx-auto mb-3 h-8 w-8 animate-spin text-netbird" strokeWidth={1.5} />
|
||||
<h1 className="text-xl font-semibold">Updating…</h1>
|
||||
<h1 className="text-xl font-semibold">{t("update.page.updating")}</h1>
|
||||
<p className="mt-1 text-sm text-nb-gray-500">
|
||||
Please don't close this window.
|
||||
{t("update.page.dontClose")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user