Files
netbird/client/ui/i18n/locales/en/common.json
T
Riccardo Manfrin a8dff998ef [client] Gate settings updates on value, not on field presence (#7398)
* [client] Gate settings updates on value, not on field presence

The update-settings kill switch (--disable-update-settings /
NB_DISABLE_UPDATE_SETTINGS / the MDM DisableUpdateSettings key) forbids
changing settings, but it decided what a "change" was by looking at
whether a field was present in the request. The CLI fills the whole
config surface of SetConfigRequest and LoginRequest from its flags and
environment on every `netbird up` (setupSetConfigReq in cmd/up.go), so a
client configured by environment restates its own configuration on every
start and tripped the gate every time.

SetConfig only warned about that, but Login carries the same fields and
was gated the same way, and Login runs inside the CLI's backoff loop: the
daemon answered every attempt with codes.Unavailable, `netbird up` never
completed, and a container with NB_DISABLE_UPDATE_SETTINGS plus any
config env var (NB_MANAGEMENT_URL, for one) could not come up at all.

Both gates now compare values. Config.WouldChange is the dry-run half of
UpdateConfig: it runs the very same diff logic (Config.apply) against a
copy of the stored config, so the gate cannot drift from what an actual
update would do, nor go stale when a field is added. A request that
restates what the profile already holds changes nothing and is allowed; a
request that diverges is refused exactly as before, and a dry run that
cannot be evaluated fails closed. A profile with no config on disk yet is
judged against the config the daemon would create for it.

For Login, the compared input comes from loginOverridesInput, which
persistLoginOverrides also uses to perform the write, so the gate judges
precisely the two fields a login can persist (management URL, pre-shared
key) and no field it ignores.

Two adjacent defects surfaced while making the comparison exact:

- Config.apply compared URLs as raw strings, so the same endpoint spelled
  without its default port ("https://api.netbird.io" vs
  "https://api.netbird.io:443") counted as a new value and rewrote the
  config. It now compares the parsed forms.
- UpdateConfig did not collapse the redacted pre-shared key, unlike
  UpdateOrCreateConfig and DirectUpdateConfig, so a UI round-trip of the
  mask replaced the stored key with asterisks.

The CLI warning for a refused SetConfig said the method was not available
in the daemon, which sent people looking for a version mismatch that was
not there; it now reports the refusal.

* [client] Do not write the profile config while only reading it to decide

The update-settings gate needs the stored config to decide whether a request
changes anything, so the previous commit moved that read ahead of the refusal.
The read is not side-effect free: profilemanager.GetConfig writes the config
back whenever apply() has to fill in a default the file was missing. A request
that the gate then refuses had therefore already rewritten the profile file.

PeekConfig is GetConfig without that write-back. The returned config is still
normalized in memory, which is what the decision needs; the file is left
exactly as it was found. Every caller of storedConfigAtPath feeds a gate that
can refuse, so they all peek.

Note for reviewers: the daemon still normalizes the file on startup and on
every real update, so nothing depends on a read performing that write.

* [client] Compare service URLs as endpoints, not as strings

Three places in one request path each had their own notion of "same
management URL": the config layer compared the parsed URLs as strings, the
privileged-change gate compared scheme + host + effective port, and the MDM
conflict check compared strings after filling in the default port. Only the
middle one was right.

A string comparison answers the wrong question. "https://api.netbird.io",
"https://api.netbird.io/" and "https://API.netbird.io:443" are one endpoint
written three ways, so a client restating its own management URL with a
trailing slash — a normal way to write it — was still read as a client asking
to be repointed, and the update-settings gate refused it. The MDM check had
the same flaw against the enforced value.

profilemanager.SameServiceURL is now the single comparison: same scheme, same
host case-insensitively as DNS names are, same effective port. The config
layer, the privileged-change gate and the MDM conflict check all defer to it,
so there is one answer to "did this URL change?" instead of three.

* [client] Stop the config dry run from generating throwaway keys

The dry run's baseline for a profile with no config file yet went through
createNewConfig, and apply() generates a WireGuard and an SSH key whenever it
finds those fields empty. The baseline is compared against and discarded, so
every evaluation minted a keypair it threw away — and logged "generated new
Wireguard key". The CLI retries Login in a backoff loop, so a first `netbird up`
on a fresh profile filled the daemon log with what reads like peer-key rotation.

The baseline now starts from the shared skeleton with placeholder keys, so
apply() has nothing to generate. No ConfigInput field maps to either key, so
the comparison is unaffected.

* [client] Cover the login the update-settings gate used to refuse

The gate's decision procedure was tested directly, but no test drove the Login
RPC that the refusal actually broke: the CLI retries Login in a backoff loop,
so a refused no-op login is what kept a client configured by environment from
ever coming up. The handler-level coverage stopped at the refusal case, which
passes on the pre-fix code too.

This test fails on the pre-fix daemon with "update settings are disabled" and
passes now. Past the gate the handler does real work the test does not stand
up, so it asserts only that the refusal did not happen.

* [client] Re-take the update-settings decision under the config lock

Login checks twice on purpose: the first check refuses the ordinary case
early, and authorizeAndPrepareLogin re-takes the authoritative one under
guardedConfigMu because the first is unsynchronized against a concurrent
privileged request. The update-settings decision is now equally
value-dependent — it compares the request against the stored config — but it
was taken only in the first, unlocked check.

So a login that was a no-op when it was checked could be written after a
concurrent writer had repointed the profile, which is exactly the window the
lock exists to close. The decision is now re-taken alongside the privilege one,
which also makes it the last read before persistLoginOverrides writes.

The test drives that interleaving through the existing afterLoginPreCheck seam
and fails without the re-check.

* [client] Drop an unreachable guard and fix two stale comments

- loginOverridesInput's nil-message guard cannot be reached: Login
  dereferences the message well before it, in storedLoginConfig.
- The docstring above afterLoginPreCheck described persistLoginOverrides,
  which lives further down the file and now carries its own.
- UpdateConfig's comment named DirectUpdateConfig; the function is
  DirectUpdateOrCreateConfig.

* [client] Make config reads pure and provision the identity explicitly

Reading a config wrote it back. profilemanager.readConfig persisted whatever
apply() had filled in, and ReadConfig created and wrote the file outright when
it was absent, so every reader was quietly a writer: a gate deciding whether to
refuse a request, a UI listing profiles, a mobile getter reading one preference.
The previous commit worked around that with a PeekConfig variant, which left
two read functions with opposite side effects and the antipattern still there
for everyone else.

Only one thing in a read genuinely had to be persisted: apply() generated the
WireGuard and SSH keys when it found them empty, and a generated key cannot be
recomputed — losing it means the peer comes back with a different identity and
registers again. Everything else apply() fills in is a deterministic default
that the next read recomputes anyway.

So identity provisioning is now its own step, Config.EnsureIdentity, and the
callers that provision write the result out themselves, in the open:

- Server.getConfig, the daemon's provisioning point;
- the CLI's foreground login, which is about to dial management;
- update() / directUpdate(), the config write paths — a stored profile can
  legitimately carry no identity, since a mobile logout clears the keys in
  place, and the next write is what has to mint a new one.

ReadConfig and GetConfig no longer write anything, PeekConfig is gone, and the
dry-run baseline no longer needs placeholder keys to keep apply() from minting
real ones.

One deliberate leftover: readConfig still calls util.EnforcePermission, which
chmods a config file whose permissions are too broad. It changes no content and
is idempotent, and dropping it would leave a legacy file world-readable until
its first write.

* [client] Name the two config readers for what they do

ReadConfig and GetConfig differed in one thing — what happens when the file is
absent — and neither name said which was which:

- ReadConfig      -> ReadOrGenerateConfig  (reads it, or generates one in memory)
- GetConfig       -> GetExistingConfig     (reads it, or fails)

Three comments went with them:

- GetConfig's said "return with Config and if it was created. Errors out if it
  does not exist", which described a bool it does not return and a creation it
  never performs.
- ReadConfig's explained that it does not write, which is what a reader is
  supposed to do anyway.
- Server.getConfig's said it "errors out if it does not exist", which it does
  not — it resolves a default config, and now provisions the identity too.

* [client] Do not panic on a config with no sync message version

apply() wrote the incoming sync message version through the stored pointer,
without checking it was there: a config that carries no version yet made it
dereference nil. Reachable from the update-settings dry run, which runs inside
a request handler — where failing closed is the worst acceptable outcome, and a
panic is not one.

The field is now reassigned like every other optional one, which also means
apply() no longer mutates anything the caller still holds through a pointer, so
the dry run's copy has one less field to detach.

Reported by cubic-dev-ai on PR #7398.

* [client] Compare the client certificate paths before reporting a change

apply() assigned the incoming mTLS certificate and key paths and set updated
unconditionally, without comparing them to what the config already held. It is
the same presence-instead-of-value mistake this branch set out to fix, one
layer down: a caller restating its own certificate paths was reported as
changing them, which trips the value-aware update-settings gate.

Reported by cubic-dev-ai on PR #7398.

* [client] Address the remaining bot findings on PR #7398

- Login logged the active-profile-state error and returned the same cause; the
  repo's guidelines call for one or the other, and the wrapped error is the one
  that carries context. (CodeRabbit)
- `netbird up` reported a codes.Unavailable SetConfig failure as "the daemon
  refused the settings update", but that code also covers a daemon that became
  unreachable. It now reports what the daemon said without asserting why.
  (cubic-dev-ai)
- TestLogin_ChangingTheManagementURLIsRefused asserted the error and nothing
  else, while "refused before it can touch daemon state" is the contract. It now
  checks the stored management URL, the in-progress login and the active profile,
  matching its SetConfig counterpart. (cubic-dev-ai)

* [client] Keep the peer identity out of a read that finds no file

ReadOrGenerateConfig resolves a default config when the profile has no file
yet, and createNewConfig was minting the WireGuard and SSH keys while doing so.
That defeated the provisioning pair it was meant to serve: the CLI's foreground
login calls EnsureIdentity to find out whether it has to persist the keys, got
generated == false because the read had already generated them, and so never
wrote them out. The login then dialed management with an identity that only
existed in memory, and the next login registered a second peer.

createNewConfig no longer provisions. createProvisionedConfig is the variant
that does, and the callers whose contract is "usable as it comes back" use it:
CreateInMemoryConfig, whose callers connect with the result, and the two
create-and-write branches. A read gets a config with no identity, so the
caller's own EnsureIdentity reports the work and triggers the write.

Reported by CodeRabbit and cubic-dev-ai on PR #7398, both on the same defect.

* [client] Stop the gate test from dialing the real management server

TestLogin_RestatingTheStoredConfigPassesTheGate asserts that the gate lets a
no-op login through, and the handler then went on to do the login for real:
isLoginRequired builds an auth client when isLoginRequiredFn is unset, so the
test dialed the profile's management URL — api.netbird.io:443. It took 1.05s
locally and would hang on a runner with no egress, for a fact about the gate
that needs no network at all.

Stubbed like the login_outcome tests do. The test now runs in 0.00s.

Reported by cubic-dev-ai on PR #7398.

* [client] Keep the admin panel path part of its identity

The endpoint comparison introduced for the management URL was applied to the
admin URL too, and that one is opened in a browser rather than dialed over
gRPC: a panel served under /netbird is not the panel served at the root. So a
config whose admin URL differed only by path reported no change, and the new
path was never persisted — a custom panel URL could not be updated at all.

SameServiceURLIncludingPath adds what a URL carries past its endpoint (path,
query, fragment, userinfo) while still treating equivalent spellings as equal:
a missing path and "/" are the same root, and so is a trailing slash. The
management URL keeps the endpoint-only comparison, since only the endpoint is
ever dialed.

Ports are also normalized numerically now, so ":0443" and ":443" are one port.

Reported by cubic-dev-ai on PR #7398 (two findings).

* [client] Treat a profile with no identity as already deregistered

Two findings on the same consequence of pure reads: a profile can legitimately
carry no keys, because logging out clears them in place.

- sendLogoutRequestWithConfig went straight to wgtypes.ParseKey and failed with
  "incorrect key size: 0" on the second logout of the same profile. There is
  nothing to deregister for a peer that was never registered, so it returns
  cleanly. Before pure reads this case was hidden: the read minted a key and
  the daemon dialed management with one it had never seen.
- The mobile logout read the config with the generating reader right after
  checking the file exists. The two are not atomic, so a profile removed in
  between was resolved from the defaults and recreated by the write that
  follows. It uses the existing-file reader now.

Reported by cubic-dev-ai and CodeRabbit on PR #7398.

* [client] Fail `netbird up` when the daemon refuses the settings update

With the update-settings kill switch on, `netbird up --enable-rosenpass`
connected and said almost nothing: SetConfig refused the change, the CLI
downgraded that to a warning, and Login carries no rosenpass field to apply, so
the flag was silently dropped. The setting stayed disabled, which is the point
of the switch, but the caller was never told their request had been ignored.

The refusal now travels as codes.FailedPrecondition instead of
codes.Unavailable, and the CLI fails on it. Unavailable means "the daemon
cannot serve this call", which is why the CLI downgraded it and why
client/ui/services reads it as an unreachable daemon — both wrong for a daemon
that answered and refused. FailedPrecondition also matches what the MDM gate
already returns for a managed field, so both refusals are now one class of
error, and it is added to the login backoff's early-exit codes so a refused
login stops instead of retrying for 30s.

This does not put the container back in the deadlock: with the value-aware
gate, a client restating its own configuration is not refused at all, so
nothing reaches this path unless a real change was asked for.

* [client] Name the reader storedConfigAtPath actually calls

The purity note still said profilemanager.GetConfig, which the rename two
commits later turned into GetExistingConfig.

Reported by cubic-dev-ai on PR #7398.

* [client] Restore the gofmt alignment of the error constants

The comment added above errUpdateSettingsDisabled in the previous commit split
the const block's alignment group, so gofmt wants the two constants above it
re-aligned. CI runs gofmt, so this would have failed the lint job.

* [client] Let an unprivileged caller log out a profile with no identity

The empty-key check sat behind requirePrivilegeForDeregistration, so an
unprivileged logout of an identity-less profile was refused with
PermissionDenied instead of completing as the no-op it is. And it was refused
for most profiles, not a corner case: the gate arms whenever the SSH server is
enabled, and sshServerEnabled reads an absent ServerSSHAllowed as enabled, so
every legacy profile qualifies.

The check now runs first. What the gate protects against is handing this
machine's registered key to another management server; with no key there is
nothing to hand over and nothing to protect.

Reported by CodeRabbit and cubic-dev-ai on PR #7398, both on the same defect.

* [client] Stop `netbird login` from retrying a refusal for 30 seconds

`netbird up` and `netbird login` both run Login through the backoff cycle, and
each carried its own copy of the list of codes that end it. Only up.go learned
about codes.FailedPrecondition, so a refused `netbird login` kept retrying and
then reported "login backoff cycle failed" instead of what the daemon said.

terminalLoginError is now that list, once, next to WithBackOff — the duplicated
copies are what let the two commands disagree in the first place.

Reported by cubic-dev-ai on PR #7398.

* [client] Answer terminalLoginError's nil case on its own terms

A successful Login reaches terminalLoginError with a nil error, and nothing
covered that. It happens to work on grpc v1.80.0 — gstatus.FromError(nil)
answers (nil, true), and Status.Code tolerates a nil receiver by returning
codes.OK, which is not in the terminal set — but that is a chain of internal
details to be relying on for the common path, and none of it was asserted.

Now the nil error is handled where it is obvious, and the table covers it.

Reported by CodeRabbit on PR #7398, which called it a panic; measured on
v1.80.0 it is not one. The gap was the untested reliance, not a crash.

* [client] Treat an unset optional field as its default when diffing a config

Seven Config fields mean "the effective default" when they hold no value:
the five SSH toggles, the SSH JWT cache TTL, and the network monitor. Every
consumer already reads a nil as that default, but apply() diffed them by
presence — `config.X == nil || *input.X != *config.X` — so an input restating
the default counted as a change.

That made the update-settings gate refuse `netbird up` outright. The CLI
sends every flag whose value came from an environment variable
(SetFlagsFromEnvVars goes through pflag's FlagSet.Set, which marks the flag
Changed), and the config a plain login writes leaves all seven unset, so a
container configured with, say, NB_ENABLE_SSH_ROOT=false restated a default
the file held as null on every start and was answered with
FailedPrecondition.

apply() now resolves the seven up front, the way it already did for
ServerSSHAllowed and RemoteJobsAllowed, which also repairs such a profile on
its next write. With the values named, the comparisons below diff values
instead of presence, so their nil branches are gone.

The network monitor keeps its platform default — on for windows and darwin —
and naming it as false elsewhere is what createEngineConfig already read a
nil to be. getJWTCacheTTL reaches the same 0 through its own default, and
Android's GetEnableSSH* getters already answered nil with false.

* [client] Normalize the config before diffing it in WouldChange

apply() reports two different things through one bool: an input that changed
a value, and a field it had to fill in because the config carried none. The
update-settings gate reads that bool as "the caller asked for a change", so
any config still missing a default answered a request that asks for nothing
with a refusal.

Readers already hand out normalized configs — readConfig applies an empty
input for exactly this reason — which is why the gate got away with it. But a
handler that refuses a request must not depend on where its caller obtained
the config, and it must not start reading "this profile predates a field" as
"the caller asked for a change" the day someone adds one with a default.

WouldChange now runs the filling-in as a pass of its own and discards its
verdict, so the pass that answers the caller measures only what the input
did.

* [client] Stop the last config write that skipped normalization

Every path that creates or updates a profile config goes through apply(),
which resolves an optional field to its default — except RenameProfile,
which read the file with a bare json.Unmarshal, set the name, and wrote it
straight back. That copied whatever the file held, so a config written by a
client that stored these fields as null kept them null. It could not
introduce a null, only carry one forward, but renaming a profile is a poor
place to leave a half-resolved config behind. It now reads through
GetExistingConfig, which normalizes what it hands out.

The tests state the invariant the fix completes, over the *bool fields of
Config listed by reflection so a field added later is covered without
touching them: none may come out of apply() unset, and no write may store
one as null. An optional bool that can be nil, true or false forces every
reader to invent the meaning of nil, and makes a diff of the config compare
presence rather than value — which is exactly what refused `netbird up` for
a client restating its own defaults.

SyncMessageVersion stays a genuine three-state field and is not covered: it
is an *int whose absence means the client pins no version, and it travels to
management that way.

* [client] Refuse a serialized config that carries no peer identity

ConfigFromJSON still promised a "fully initialized" config after this PR
moved key generation out of apply() into EnsureIdentity, but identity stopped
being one of the defaults it applies. Its two callers both connect with what
they get back: the iOS SDK's Client.SetConfigFromJSON keeps it as the
preloaded config Run() uses on tvOS, and Auth.SetConfigFromJSON as the config
it authenticates with.

No caller feeds it a document without keys today — every stored document
comes from Auth.GetConfigJSON, whose config is provisioned by
DirectUpdateOrCreateConfig or CreateInMemoryConfig, and the tvOS app only
ever edits fields of a document it already has. This is a safety net for the
next caller, not a live bug.

Provisioning the identity here would be the wrong net. Neither caller can
hand a generated key back to the store the document came from — Client
exports no config at all — so the peer would connect under an identity
nothing persists and register anew on every launch, which is the failure the
EnsureIdentity split exists to prevent. A document with no identity means
nobody has logged in yet, and saying so is the only useful answer.

Both keys are required because both are dead ends when missing: an empty
WireGuard key fails the management login on its size, and an empty SSH key
fails ssh.GeneratePublicKey in ConnectClient before the engine starts.

* [client] Say that the null-on-disk fixture is synthesized, not written

The test comment described the null state in the present tense — "the config
a plain login writes leaves every one of them unset" — which was true before
this branch and is not any more: apply() now resolves those fields, so a
login writes them set. unsetOnDisk puts the null state back deliberately, to
stand in for a profile an older client wrote. Comments only.

* [client] Gather the optional-field defaults into one function

Resolving an unset optional field was spread over five places: the two
values newConfigSkeleton pre-sets, the block this branch added for the SSH
toggles, the network monitor's own if, the `else if` tails of
ServerSSHAllowed and RemoteJobsAllowed, and a trailing if for
DisableNotifications several hundred lines further down. Reading apply() left
no single answer to "what does this field default to, and who decides".

They now live in Config.resolveUnsetDefaults, which apply() calls before it
compares anything — the ordering being the point, since it is what lets
every comparison below diff values instead of presence. The comparisons for
ServerSSHAllowed, RemoteJobsAllowed and DisableNotifications lose their
`config.X == nil ||` clauses accordingly, as the other six already had.

newConfigSkeleton keeps its two, and that is the one asymmetry worth naming:
ServerSSHAllowed defaults to false for a new profile and to true for a
legacy one, and it only works because the skeleton runs first. The doc
comment says so, where before it was implied by the order of two distant
blocks.

Pure refactor. Verified as one: for the four fields whose branches moved,
plus two that did not and the JWT TTL, all 63 combinations of stored value
(nil/false/true) against input value (absent/false/true) produce byte-
identical resolved values and `updated` verdicts before and after.

* [client] Resolve the merge conflicts left in the tree

262ce8c3b landed with the conflict markers still in it, so client/server and
the iOS SDK did not compile. Four regions, resolved as follows.

client/server/mdm.go — main moved the MDM conflict-check machinery into the
mdm package (mdm.ResolveConflicts, mdm.ConflictBool, mdm.ConflictURL, ...).
This branch had edited the local copies, which are now dead: dropped, along
with the profilemanager import that only the local conflictURL needed.

client/server/server.go, Login gate — this branch's value-aware gate stays
(the point of the PR: refuse a real divergence, let a restatement through),
so main's presence-based `loginRequestHasConfigOverrides` block goes; that
helper no longer exists here anyway. Main's other change in the same lines
is real and kept: the MDM policy now comes from the daemon-owned
s.mdmLoader.Load() instead of the package-level loadMDMPolicy, which main
removed. The stale call right below the conflict was the reason the file
would not have compiled even with the markers gone.

client/server/server.go, getConfig — both sides add something and both are
needed. The identity is provisioned and persisted first, then the MDM
overlay is applied, so what reaches disk stays the profile's own config: the
overlay is runtime-only and re-derived on every load.

client/ios/NetBirdSDK/client.go — main reworked SetConfigFromJSON to store
the JSON and re-parse it on each load, which is the shape kept; the parse is
now only a validity check, and this branch's reason for it (a document with
no peer identity is refused, not just an unparseable one) moves into that
comment.

client/server/update_settings_gate_test.go — follows the sentinel constant
to its new home, mdm.PreSharedKeyRedactedSentinel.

* [client] Reuse util's service-URL comparison instead of a second copy

The endpoint-comparison rules this branch introduced now live in util (PR
#7472 moved them there so the MDM conflict check could stop comparing URLs
as strings). Keeping a copy here is what produced that bug in the first
place: two implementations of "is this the same endpoint?" drift, and the
one that drifts starts refusing a URL that addresses the very server it
already points at.

So SameServiceURL delegates the port normalization to util.ServiceURLPort
and drops the local one, and SameServiceURLIncludingPath — endpoint plus
path, for the admin panel URL, which is opened rather than dialed — is
util.SameServiceURL plus the query, fragment and userinfo it adds on top,
so the local path normalization goes too.

What stays here is the distinction util does not make: SameServiceURL is
endpoint-only, because a management URL is dialed and only its host and port
are, while util.SameServiceURL includes the path.

Pure refactor. Verified as one: all 198 pairs of a 14-spelling matrix
(default and zero-padded ports, host case, trailing slash, path, query,
fragment, userinfo, both schemes, nil operands) answer identically for both
functions before and after.

* [client] Give a newly added profile its identity (review item 1)

AddProfile writes the config it builds straight to disk, but built it with
createNewConfig, which stopped generating the peer's keys when identity
generation moved out of apply() into EnsureIdentity. The profile file landed
with an empty PrivateKey and SSHKey.

Nothing lost the keys permanently — the daemon's own getConfig provisions and
persists them on first use — but every reader that does not write got a
config that cannot connect in the meantime, which is exactly the set this
branch grew: the update-settings gate deciding whether to refuse a request,
and the mobile SDKs loading a stored profile.

createProvisionedConfig exists for callers that persist or connect, and this
is one; before the split, createNewConfig produced the keys here too.

* [client] Let a logged-out profile deserialize again (review item 2)

ConfigFromJSON refused a document with no WireGuard or SSH key. A config
legitimately has none between a logout and the next login: mobile
LogoutProfile clears both in place and writes the profile back, so the peer
re-registers on the next login instead of returning as itself.

So the refusal broke the mobile flows it was meant to protect. On iOS and
tvOS the stored JSON of a logged-out profile stopped loading through
Client.SetConfigFromJSON and Auth.SetConfigFromJSON, and copyConfig — which
round-trips a Config through JSON to take an in-memory copy before applying
the MDM overlay — failed on the same document. Where the old code silently
minted a key, this returned an error, which is worse for logout and profile
switching alike: neither is asking to connect.

The deserializer now stays out of the identity question in both directions:
it does not generate one (a read cannot hand back keys nothing will write
down) and does not refuse one that is absent. Whoever goes on to connect is
where an absent identity has to be answered — and it already is, by the
login path that provisions and persists.

ErrConfigWithoutIdentity goes with it; nothing else used it.

* [client] Fold the scheme case here too, like util does (review item 6)

profilemanager.SameServiceURL compared the scheme with ==, util.SameServiceURL
with EqualFold. No observable difference — net/url lowercases the scheme when
it parses, and both functions take parsed URLs — but two functions of the same
name with two different rules is a trap for whoever reads one and assumes the
other.

* [client] Classify the daemon's refusals in the GUI (review item 3)

FailedPrecondition reached the classifier unmatched, so a refusal showed as
"Operation failed". It is the code both of the daemon's deliberate refusals
carry: the update-settings kill switch, and a field an MDM policy manages.

Both are now named — settings_locked and settings_managed_by_mdm, matched on
the message the daemon composes — and FailedPrecondition itself falls back to
change_refused, so a refusal the daemon grows later still reads as a refusal
rather than a failure.

Only the English strings are added. Bundle.Translate falls back to the
default language for a missing key, so other locales show English until the
usual translation pass, rather than the bare "error.<code>" the classifier
would otherwise surface.

Note: the package needs GTK4/WebKit to build, which this machine has not, so
the test is type-checked (go vet, GOOS=windows) but was not executed locally;
CI's Linux job runs it.

* [client] Cover the mobile profile round trip: create, logout, reload

Both mobile regressions this branch's review turned up lived on the same
path, and neither was visible from the desktop client: a profile created
without an identity, and a logged-out profile that would no longer
deserialize. The desktop never meets the second one — it is mobile logout
that clears the peer's keys in place, so the next login registers a new peer
instead of bringing the old one back.

The test walks a profile through the round its user puts it through —
created, logged out, loaded again, switched away from and back — and loads it
at each step the way the SDKs do: read the stored config, serialize it, load
it back. That is Client.SetConfigFromJSON storing the document for tvOS,
Auth.SetConfigFromJSON authenticating with it, and copyConfig taking an
in-memory copy before the MDM overlay.

Verified to fail on each regression separately: restoring the bare
constructor in AddProfile fails it with "a new profile was written with no
identity", and restoring the identity check in ConfigFromJSON fails it at
"load the profile back".

client/mobile already had the coverage for the first one in
TestLogoutProfile_DisableProfiles — which arrived from main with the MDM
work, and which I had not been running.

* [client] Name only the refusals, not every FailedPrecondition

The classifier gained a blanket FailedPrecondition -> change_refused fallback
so a refusal would stop reading as "Operation failed". It reaches too far:
the daemon returns that code for two dozen states that are not settings
refusals — "not logged in", "client is not running", "another capture is
already running", "session can no longer be extended, log in again to
reconnect" — and errorClassifier is shared with the session and connection
services, not just the settings save.

So the user was told the service had refused their change while what they
actually had to do was log in again. The two refusals the daemon composes
stay named by their message; everything else goes back to the generic
message, which says nothing rather than something wrong.

Reported by cubic on the PR.

* [client] Say what each assertion was checking in the mobile test

AGENTS.md asks for a context message on comparison and boolean assertions,
and four of the ones added with this test had none, so a failure would have
read as a bare Empty/Equal with no hint of which step of the round trip broke.

Reported by cubic on the PR.

* [client] Translate the two new error strings into every locale

The GUI classifier gained error.settings_locked and
error.settings_managed_by_mdm, and only the English strings were added: the
bundle falls back to the default language for a missing key, so nothing would
have shown a bare "error.<code>" to a user.

CI disagrees, and it is right to: check-translations.mjs requires every
locale to carry the full English key set, so English-only fails the gate
rather than degrading quietly.

The ten locales now carry both strings. These are my translations, not a
localization pass — worth a second pass by whoever owns the language, in
particular for the phrasing of "an administrator has locked them".

The uk file also loses two lines of stray 8-space indentation, normalized by
rewriting the file; no key or value changed with it.

* [client] Persist the profile before overlaying MDM on it (review item)

`netbird login` read the config, applied the MDM policy on top, and only then
provisioned the identity and wrote the result out. On a profile with no
identity yet — a first login — that write persisted the enforced values into
the user's own config file: an MDM-managed management URL or pre-shared key
became indistinguishable from one the user set, and stayed behind once the
policy was withdrawn.

Provisioning and its write now come first, and the overlay is applied to the
in-memory config afterwards, where it belongs: it is re-derived on every load
and never meant to reach disk from here. Server.getConfig already orders the
two this way; the two paths now agree.

Reported by cubic on the PR.

* [client] Assert against the stored config, not a resolved default (review item)

The login-gate test read the profile back with ReadOrGenerateConfig, which
resolves a default config in memory when the file is missing — and that
default's management URL is the very value the assertion checks. An erased or
mislocated profile would have passed the test instead of failing it.

The file is written by the test itself, so GetExistingConfig is the right
reader: it errors when the file is gone.

Reported by cubic on the PR.

* [client] Keep the mTLS pair off the gate's dry run (review item)

WouldChange runs the real apply() against a throwaway copy, and apply() loads
the client mTLS certificate and key from disk whenever the config names them.
So every gated SetConfig and Login read the pair — twice per request, once for
the normalization pass and once for the verdict — including requests that were
about to be refused or that changed nothing, and logged an error per request
when the files were missing. The gate used to be presence-based and never
called apply(), so this was new work on a request path.

The loaded pair feeds the connection and never the comparison: nothing in
apply() reads it back, and it does not move the `updated` verdict. A config
built only to be compared against now says so, and apply() skips the load for
it.

Reported by cubic on the PR.

* Makes it explicit that RenameProfile does write on disk

* [client] Provision the peer identity under the config lock (review item)

Login took the authoritative update-settings and privilege decisions under
guardedConfigMu, then released it and called getConfig, which mints the peer's
identity and writes the config out. Between that read and that write, a
SetConfig holding the same lock could land a change and answer its caller —
and then be overwritten by the config the login had already read.

The window is narrow: getConfig only writes when the profile has no identity
or no file, so in practice a first login racing a settings change on the same
profile. It is also narrower than before this branch, where the write happened
inside the reader on every read that filled in a default.

Provisioning now runs where the decision it belongs to runs: at the end of
authorizeAndPrepareLogin, with the lock already held, next to
persistLoginOverrides, which writes there too. No lock is taken that was not
held before, so the documented guardedConfigMu-then-mutex order is untouched.

getConfig keeps its behaviour by calling the same extracted helper; on the
login path it now finds the identity already there and writes nothing. The
other callers are unchanged, and still provision outside any lock — a
concurrent SetConfig is not part of their flow.

Reported by cubic on the PR.

* [client] Declare the probe marker to the debug-bundle field check

TestAddConfig_AllFieldsCovered walks Config by reflection and fails until every
field is either rendered in the debug bundle or listed as excluded with a
reason. The probe marker added for the gate's dry run was neither, so the
client unit suite went red on every platform.

It is excluded: it marks a throwaway copy built to be compared against and
discarded, so it is never set on a config anyone runs with, and rendering it
would only ever print false.

* [client] Provision the peer identity on the iOS login path

Key generation used to happen inside apply(), so a config loaded from JSON with
no keys got them in memory on the way in, the login worked, and the app stored
the result. This branch moved generation into EnsureIdentity, and nothing in
the iOS SDK called it.

The consequence lands on the flow the mobile logout sets up: logout clears both
keys in place so the next login registers a new peer. The app then hands that
keyless JSON to Auth.SetConfigFromJSON, and the login calls auth.NewAuth with
an empty WireGuard key, which fails on key size before the SSO flow starts —
the user cannot sign back in.

Auth.setBaseConfig now provisions, which covers both entry points (NewAuth and
SetConfigFromJSON). It mints on the base config, the one GetConfigJSON returns
for the caller to persist, and writes it to disk itself when the profile has a
file — non-atomically, like NewAuth's own write, since the tvOS App Group
sandbox blocks temp-file-and-rename.

Not covered by a test: the package builds only under GOOS=ios, which the test
jobs do not run. Verified by building and vetting for GOOS=ios/arm64.

Reported by pappz in review.

* [client] Name the resolving reader for what it does, not what it makes

ReadOrGenerateConfig reads the profile config and falls back to the defaults in
memory when there is no file. "Generate" reads as "produces and stores", which
is the opposite of the property the rename it came from was meant to advertise:
the read is pure, writes nothing and mints no identity.

ReadConfigOrDefault says the same without the side effect, and pairs with
GetExistingConfig, which fails where this one falls back. Its doc comment now
states the absence of a write rather than only the fallback.

Pure rename; the two remaining mentions of the pre-branch name ReadConfig in
the tests go with it.

Reported by pappz in review.

* [client] Read an emptied NAT list as the absent one it matches

apply() compared NATExternalIPs with reflect.DeepEqual, which calls a nil
slice and an empty slice different. Both mean the same thing — no NAT
mappings — and the two meet on a perfectly ordinary start: a profile stores
the absent list as JSON null and reads it back nil, while `netbird up` sends
CleanNATExternalIPs, an empty list, whenever NB_EXTERNAL_IP_MAP is set to
nothing, which a deployment template does by default.

So the gate saw a change where nothing changed and refused the request with
FailedPrecondition. That is the same deadlock this branch exists to remove,
reached through another field: a container with the kill switch on could not
come up, and `netbird up` reported "the daemon refused the settings update".

The DNS label list next to it already used slices.Equal, which treats nil and
empty as the same list. The NAT list now does too, and the last use of
reflect in the package goes with it.

Reported by pappz in review.
2026-10-06 11:39:22 +02:00

1863 lines
85 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"tray.tooltip": {
"message": "NetBird",
"description": "Hover tooltip on the system-tray / menu-bar icon. Brand name — do not translate."
},
"tray.status.disconnected": {
"message": "Disconnected",
"description": "Connection status surfaced through the tray icon: the client is not connected to the network."
},
"tray.status.daemonUnavailable": {
"message": "Not running",
"description": "Tray status: the background NetBird service (daemon) is not running."
},
"tray.status.error": {
"message": "Error",
"description": "Tray status: the client is in an error state."
},
"tray.status.connected": {
"message": "Connected",
"description": "Tray status: connected to the NetBird network."
},
"tray.status.connecting": {
"message": "Connecting",
"description": "Tray status: a connection is being established."
},
"tray.status.needsLogin": {
"message": "Login required",
"description": "Tray status: the user must sign in before connecting."
},
"tray.status.loginFailed": {
"message": "Login failed",
"description": "Tray status: the last sign-in attempt failed."
},
"tray.status.sessionExpired": {
"message": "Session expired",
"description": "Tray status: the authenticated session expired; the user must sign in again."
},
"tray.session.expiresIn": {
"message": "Session expires in {remaining}",
"description": "Tray row showing time left before the session expires. {remaining} is a human-readable duration such as '5 minutes', built from the tray.session.unit.* strings. Keep {remaining} unchanged."
},
"tray.session.unit.lessThanMinute": {
"message": "less than a minute",
"description": "Duration fragment substituted into {remaining} (see tray.session.expiresIn). Used when under one minute remains."
},
"tray.session.unit.minute": {
"message": "1 minute",
"description": "Duration fragment for exactly one minute, substituted into {remaining}."
},
"tray.session.unit.minutes": {
"message": "{count} minutes",
"description": "Duration fragment for several minutes, substituted into {remaining}. {count} is the number of minutes; keep {count}."
},
"tray.session.unit.hour": {
"message": "1 hour",
"description": "Duration fragment for exactly one hour, substituted into {remaining}."
},
"tray.session.unit.hours": {
"message": "{count} hours",
"description": "Duration fragment for several hours. {count} is the number of hours; keep {count}."
},
"tray.session.unit.day": {
"message": "1 day",
"description": "Duration fragment for exactly one day, substituted into {remaining}."
},
"tray.session.unit.days": {
"message": "{count} days",
"description": "Duration fragment for several days. {count} is the number of days; keep {count}."
},
"tray.menu.open": {
"message": "Open NetBird",
"description": "Tray menu item that opens the main NetBird window. Keep short."
},
"tray.menu.connect": {
"message": "Connect",
"description": "Tray menu item that connects to the network. Keep short."
},
"tray.menu.disconnect": {
"message": "Disconnect",
"description": "Tray menu item that disconnects from the network. Keep short."
},
"tray.menu.exitNode": {
"message": "Exit Node",
"description": "Tray submenu title for choosing an exit node (route all traffic through another peer)."
},
"tray.menu.networks": {
"message": "Resources",
"description": "Tray submenu title listing network resources the user can reach. Labelled 'Resources' in the UI even though the key says networks."
},
"tray.menu.profiles": {
"message": "Profiles",
"description": "Tray submenu title listing connection profiles."
},
"tray.menu.manageProfiles": {
"message": "Manage Profiles",
"description": "Tray menu item that opens Settings → Profiles."
},
"tray.menu.settings": {
"message": "Settings...",
"description": "Tray menu item that opens the Settings window. The trailing '...' signals that a window opens; keep it."
},
"tray.menu.debugBundle": {
"message": "Create Debug Bundle",
"description": "Tray menu item that generates a diagnostic log bundle for support."
},
"tray.menu.about": {
"message": "Help & Support",
"description": "Tray submenu title for help and support links."
},
"tray.menu.github": {
"message": "GitHub",
"description": "Tray menu link to the GitHub repository. Brand name — do not translate."
},
"tray.menu.documentation": {
"message": "Documentation",
"description": "Tray menu link to the online documentation."
},
"tray.menu.troubleshoot": {
"message": "Troubleshoot",
"description": "Tray menu link to troubleshooting help."
},
"tray.menu.downloadLatest": {
"message": "Download latest version",
"description": "Tray menu item to download the latest available version."
},
"tray.menu.installVersion": {
"message": "Install version {version}",
"description": "Tray menu item to install a specific update. {version} is a version number like 0.30.1; keep {version}."
},
"tray.menu.guiVersion": {
"message": "GUI: {version}",
"description": "Tray menu row showing the installed UI version. 'GUI' = the graphical app. {version} is a version number; keep it."
},
"tray.menu.daemonVersion": {
"message": "Daemon: {version}",
"description": "Tray menu row showing the background-service version. 'Daemon' = the background NetBird service. {version} is a version number; keep it."
},
"tray.menu.versionUnknown": {
"message": "—",
"description": "Placeholder shown in version rows when the version can't be determined. It is an em dash — leave as-is."
},
"tray.menu.quit": {
"message": "Quit NetBird",
"description": "Tray menu item that fully exits the app and removes the tray icon. Keep short."
},
"notify.daemonOutdated.title": {
"message": "NetBird service is outdated",
"description": "Title of the OS desktop notification shown when the running daemon is too old for this UI."
},
"notify.daemonOutdated.body": {
"message": "Update the NetBird service to use this app.",
"description": "Body of the desktop notification telling the user to upgrade the daemon."
},
"notify.update.title": {
"message": "NetBird update available",
"description": "Title of the OS desktop notification shown when an app update is available."
},
"notify.update.body": {
"message": "NetBird {version} is available.",
"description": "Body of the update-available desktop notification. {version} is the new version number; keep it."
},
"notify.update.enforcedSuffix": {
"message": " Your administrator requires this update.",
"description": "Sentence appended to the update notification body when the admin has made the update mandatory. Note the leading space — keep it."
},
"notify.error.title": {
"message": "Error",
"description": "Title of a generic error desktop notification."
},
"notify.error.connect": {
"message": "Failed to connect",
"description": "Error notification body shown when connecting failed."
},
"notify.error.disconnect": {
"message": "Failed to disconnect",
"description": "Error notification body shown when disconnecting failed."
},
"notify.error.switchProfile": {
"message": "Failed to switch to {profile}",
"description": "Error notification shown when switching profiles failed. {profile} is the target profile name; keep it."
},
"notify.error.exitNode": {
"message": "Failed to update exit node {name}",
"description": "Error notification shown when updating the exit node failed. {name} is the exit node name; keep it."
},
"notify.sessionExpired.title": {
"message": "NetBird session expired",
"description": "Title of the desktop notification shown when the session has expired."
},
"notify.sessionExpired.body": {
"message": "Your NetBird session has expired. Please log in again.",
"description": "Body of the session-expired desktop notification."
},
"notify.sessionWarning.title": {
"message": "Session expires soon",
"description": "Title of the desktop notification warning that the session will expire soon."
},
"notify.sessionWarning.body": {
"message": "Your NetBird session expires in {remaining}. Click Extend now to renew.",
"description": "Body of the session-expiry warning notification. {remaining} is a human-readable duration (see tray.session.unit.*); keep it. 'Extend now' refers to the action button notify.sessionWarning.extend."
},
"notify.sessionWarning.bodyGeneric": {
"message": "Your NetBird session is about to expire. Click Extend now to renew.",
"description": "Generic session-expiry warning body used when the exact remaining time isn't known."
},
"notify.sessionWarning.extend": {
"message": "Extend now",
"description": "Action button on the session-expiry notification that renews the session. Keep short."
},
"notify.sessionWarning.dismiss": {
"message": "Dismiss",
"description": "Action button on the session-expiry notification that dismisses it. Keep short."
},
"notify.sessionWarning.failed": {
"message": "Failed to extend NetBird session",
"description": "Notification shown when renewing the session failed."
},
"notify.sessionWarning.successTitle": {
"message": "NetBird session extended",
"description": "Title of the notification confirming the session was renewed."
},
"notify.sessionWarning.successBody": {
"message": "Your session has been refreshed.",
"description": "Body of the notification confirming the session was renewed."
},
"notify.sessionDeadlineRejected.title": {
"message": "Session deadline rejected",
"description": "Title of the notification shown when the server sent an invalid session deadline."
},
"notify.sessionDeadlineRejected.body": {
"message": "The server sent an invalid session deadline. Please sign in again.",
"description": "Body explaining the server sent an invalid session deadline and the user must sign in again."
},
"notify.mdm.policyApplied.title": {
"message": "NetBird settings updated",
"description": "Title of the desktop notification shown when an MDM (IT-managed) policy changed the daemon configuration at runtime."
},
"notify.mdm.policyApplied.body": {
"message": "Your NetBird configuration was updated by your IT policy.",
"description": "Body of the MDM policy-applied notification, telling the user their settings were changed by their organization's device-management policy."
},
"common.cancel": {
"message": "Cancel",
"description": "Generic Cancel button label, reused across dialogs. Keep short."
},
"common.save": {
"message": "Save",
"description": "Generic Save button label. Keep short."
},
"common.saveChanges": {
"message": "Save Changes",
"description": "Button label to save edited settings. Keep short."
},
"common.saving": {
"message": "Saving…",
"description": "Button label / status shown while a save is in progress. Ends with an ellipsis."
},
"common.close": {
"message": "Close",
"description": "Generic Close button label. Keep short."
},
"common.copy": {
"message": "Copy",
"description": "Generic Copy button label (copy to clipboard). Keep short."
},
"common.togglePasswordVisibility": {
"message": "Toggle password visibility",
"description": "Accessibility label for the show/hide button inside a password field."
},
"common.increase": {
"message": "Increase",
"description": "Accessibility label for the increment (+) button on a numeric stepper input."
},
"common.decrease": {
"message": "Decrease",
"description": "Accessibility label for the decrement (−) button on a numeric stepper input."
},
"common.delete": {
"message": "Delete",
"description": "Generic Delete button label. Keep short."
},
"common.create": {
"message": "Create",
"description": "Generic Create button label. Keep short."
},
"common.add": {
"message": "Add",
"description": "Generic Add button label. Keep short."
},
"common.remove": {
"message": "Remove",
"description": "Generic Remove button label. Keep short."
},
"common.refresh": {
"message": "Refresh",
"description": "Generic Refresh button label. Keep short."
},
"common.loading": {
"message": "Loading…",
"description": "Generic loading indicator text. Ends with an ellipsis."
},
"common.netbird": {
"message": "NetBird",
"description": "The product name. Brand — do not translate."
},
"common.noResults.title": {
"message": "Could not find any results",
"description": "Title of the empty state shown when a search or filter returns nothing."
},
"common.noResults.description": {
"message": "We couldn't find any results. Please try a different search term or change your filters.",
"description": "Body of the no-results empty state, suggesting a different search term or filters."
},
"notConnected.title": {
"message": "Disconnected",
"description": "Title of the placeholder shown on data screens while disconnected."
},
"notConnected.description": {
"message": "Connect to NetBird first to view detailed information about your peers, network resources, and exit nodes.",
"description": "Body explaining the user must connect to NetBird first to see peer, resource, and exit-node details."
},
"connect.status.disconnected": {
"message": "Disconnected",
"description": "Label on the main-window connection toggle: not connected."
},
"connect.status.connecting": {
"message": "Connecting...",
"description": "Connection toggle label while connecting. Ends with an ellipsis."
},
"connect.status.connected": {
"message": "Connected",
"description": "Connection toggle label when connected."
},
"connect.status.disconnecting": {
"message": "Disconnecting...",
"description": "Connection toggle label while disconnecting. Ends with an ellipsis."
},
"connect.status.daemonUnavailable": {
"message": "Daemon unavailable",
"description": "Connection toggle label when the background service is unavailable. 'Daemon' = background service."
},
"connect.status.loginRequired": {
"message": "Login required",
"description": "Connection toggle label when sign-in is required before connecting."
},
"connect.error.loginTitle": {
"message": "Login Failed",
"description": "Error-dialog title shown when sign-in fails. The action-named '… Failed' style is intentional — keep it."
},
"connect.error.connectTitle": {
"message": "Connect Failed",
"description": "Error-dialog title shown when connecting fails."
},
"connect.error.disconnectTitle": {
"message": "Disconnect Failed",
"description": "Error-dialog title shown when disconnecting fails."
},
"nav.peers.title": {
"message": "Peers",
"description": "Navigation label for the Peers section (other devices in the network)."
},
"nav.peers.description": {
"message": "{connected} of {total} connected",
"description": "Sub-label under Peers showing how many are connected. {connected} and {total} are numbers; keep both."
},
"nav.resources.title": {
"message": "Resources",
"description": "Navigation label for the Resources section (routed networks)."
},
"nav.resources.description": {
"message": "{active} of {total} active",
"description": "Sub-label under Resources showing how many are active. {active} and {total} are numbers; keep both."
},
"nav.exitNode.title": {
"message": "Exit Nodes",
"description": "Navigation label for the Exit Nodes section."
},
"nav.exitNode.none": {
"message": "Not active",
"description": "Sub-label under Exit Nodes when no exit node is in use."
},
"nav.exitNode.using": {
"message": "Via {name}",
"description": "Sub-label under Exit Nodes when one is active. {name} is the exit node's name; keep it."
},
"header.openSettings": {
"message": "Open settings",
"description": "Accessibility label / tooltip for the gear icon that opens Settings."
},
"header.togglePanel": {
"message": "Toggle side panel",
"description": "Accessibility label / tooltip for the button that shows or hides the side panel."
},
"profile.selector.loading": {
"message": "Loading...",
"description": "Shown in the profile picker while profiles load. Ends with an ellipsis."
},
"profile.selector.noProfile": {
"message": "No profile",
"description": "Shown in the profile picker when no profile is selected."
},
"profile.selector.searchPlaceholder": {
"message": "Search profile by name...",
"description": "Placeholder text in the profile picker's search field."
},
"profile.selector.emptyTitle": {
"message": "No Profiles Found",
"description": "Title shown in the profile picker when a search matches no profiles."
},
"profile.selector.emptyDescription": {
"message": "Try a different search term or create a new profile.",
"description": "Body shown when no profiles match the search, suggesting a different term or creating one."
},
"profile.selector.newProfile": {
"message": "New Profile",
"description": "Button in the profile picker to create a new profile. Keep short."
},
"profile.selector.moreOptions": {
"message": "More options",
"description": "Accessibility label for the per-profile kebab (⋯) menu."
},
"profile.selector.deregister": {
"message": "Deregister",
"description": "Per-profile menu action: deregister (sign out of the profile but keep it)."
},
"profile.selector.delete": {
"message": "Delete",
"description": "Per-profile menu action: delete the profile."
},
"profile.selector.switchTo": {
"message": "Switch to this profile",
"description": "Tooltip / label for the action that switches to a profile."
},
"profile.selector.edit": {
"message": "Edit",
"description": "Per-profile menu action: open the edit dialog to rename or change the management server."
},
"profile.edit.title": {
"message": "Edit Profile",
"description": "Title of the dialog for editing an existing profile."
},
"profile.edit.submit": {
"message": "Save Changes",
"description": "Submit button on the edit-profile dialog. Keep short."
},
"profile.dialog.title": {
"message": "Enter Profile Name",
"description": "Title of the dialog for naming a new profile."
},
"profile.dialog.nameLabel": {
"message": "Profile Name",
"description": "Field label for the profile name."
},
"profile.dialog.description": {
"message": "Set an easily identifiable name for your profile.",
"description": "Helper text under the profile-name field."
},
"profile.dialog.placeholder": {
"message": "e.g. Work",
"description": "Example placeholder shown in the profile-name field. 'Work' is a sample value; translate it to a natural example."
},
"profile.dialog.submit": {
"message": "Add Profile",
"description": "Submit button on the add-profile dialog. Keep short."
},
"profile.dialog.required": {
"message": "Please enter a profile name, e.g. work, home",
"description": "Validation message shown when the profile name is empty. The examples (work, home) may be localized."
},
"profile.dialog.managementHelp": {
"message": "Use NetBird Cloud or your own server.",
"description": "Helper text noting the profile can use NetBird Cloud or a self-hosted server."
},
"profile.dialog.urlUnreachable": {
"message": "Couldn't reach this server. Check the URL, or add the profile anyway if you're sure it's correct.",
"description": "Soft warning when the entered server URL couldn't be reached; the user may add the profile anyway."
},
"header.menu.settings": {
"message": "Settings...",
"description": "'More' menu item in the header that opens Settings. The trailing '...' signals a window opens."
},
"header.menu.defaultView": {
"message": "Default View",
"description": "'More' menu item that switches the main window to the compact default view."
},
"header.menu.advancedView": {
"message": "Advanced View",
"description": "'More' menu item that switches the main window to the wider advanced view."
},
"header.menu.updateAvailable": {
"message": "Update Available",
"description": "'More' menu item / badge shown when an update is available."
},
"header.menu.open": {
"message": "Open menu",
"description": "Accessibility label for the header's more (⋮) button that opens the menu."
},
"header.profile.switch": {
"message": "Switch profile",
"description": "Accessibility label for the header's profile selector button."
},
"connect.toggle.label": {
"message": "Toggle NetBird connection",
"description": "Accessibility label for the large connect/disconnect toggle on the main page."
},
"connect.localIp.label": {
"message": "Local IP addresses",
"description": "Accessibility label for the local IP selector that toggles between IPv4 and IPv6 on the main page."
},
"common.search": {
"message": "Search",
"description": "Accessibility label for a generic search input."
},
"common.filter": {
"message": "Filter",
"description": "Accessibility label for a generic filter control."
},
"exitNodes.dropdown.trigger": {
"message": "Select exit node",
"description": "Accessibility label for the exit-node picker button at the bottom of the main page."
},
"peers.row.label": {
"message": "Open details for {name}, {status}",
"description": "Accessibility label for a peer row in the list. {name} is the peer name and {status} is the connection status; keep both placeholders."
},
"peers.dialog.title": {
"message": "Peer details",
"description": "Accessibility title (announced to screen readers) for the peer details dialog/panel."
},
"networks.row.toggle": {
"message": "Toggle {name}",
"description": "Accessibility label for the row-wide toggle on a network/resource. {name} is the resource id; keep the placeholder."
},
"networks.bulk.label": {
"message": "Toggle all visible resources",
"description": "Accessibility label for the bulk enable/disable button at the bottom of the resources list."
},
"profile.switch.title": {
"message": "Switch Profile to \"{name}\"?",
"description": "Confirmation-dialog title for switching profiles. {name} is the target profile name, shown in quotes; keep {name} and the surrounding quotes."
},
"profile.switch.message": {
"message": "Are you sure you want to switch profiles?\nYour current profile will be disconnected.",
"description": "Confirmation body for switching profiles. Contains a line break (\\n) — keep it."
},
"profile.switch.confirm": {
"message": "Confirm",
"description": "Confirm button on the switch-profile dialog. Keep short."
},
"profile.deregister.title": {
"message": "Deregister Profile \"{name}\"?",
"description": "Confirmation-dialog title for deregistering a profile. {name} is the profile name; keep it and the quotes."
},
"profile.deregister.message": {
"message": "Are you sure you want to deregister this profile?\nYou will need to log in again to use it.",
"description": "Confirmation body for deregistering; warns the user must sign in again. Contains a line break (\\n) — keep it."
},
"profile.deregister.confirm": {
"message": "Deregister",
"description": "Confirm button on the deregister dialog. Keep short."
},
"profile.delete.title": {
"message": "Delete Profile \"{name}\"?",
"description": "Confirmation-dialog title for deleting a profile. {name} is the profile name; keep it and the quotes."
},
"profile.delete.message": {
"message": "Are you sure you want to delete this profile?\nThis action cannot be undone.",
"description": "Confirmation body for deleting; warns the action can't be undone. Contains a line break (\\n) — keep it."
},
"profile.delete.disabledActive": {
"message": "Active profiles cannot be deleted. Switch to a different one before deleting this profile.",
"description": "Tooltip explaining the active profile can't be deleted until the user switches away."
},
"profile.delete.disabledDefault": {
"message": "The default profile cannot be deleted.",
"description": "Tooltip explaining the default profile can't be deleted."
},
"profile.error.switchTitle": {
"message": "Switch Profile Failed",
"description": "Error-dialog title when switching profiles fails."
},
"profile.error.deregisterTitle": {
"message": "Deregister Profile Failed",
"description": "Error-dialog title when deregistering a profile fails."
},
"profile.error.deleteTitle": {
"message": "Delete Profile Failed",
"description": "Error-dialog title when deleting a profile fails."
},
"profile.error.createTitle": {
"message": "Create Profile Failed",
"description": "Error-dialog title when creating a profile fails."
},
"profile.error.editTitle": {
"message": "Edit Profile Failed",
"description": "Error-dialog title when editing a profile (rename or management URL change) fails."
},
"profile.error.loadTitle": {
"message": "Load Profiles Failed",
"description": "Error-dialog title when loading profiles fails."
},
"profile.dropdown.activeProfile": {
"message": "Active profile",
"description": "Section heading in the header profile dropdown for the current profile."
},
"profile.dropdown.switchProfile": {
"message": "Switch Profile",
"description": "Section heading / action in the profile dropdown to switch profiles."
},
"profile.dropdown.noEmail": {
"message": "Other",
"description": "Label shown for a profile that has no associated email address."
},
"profile.dropdown.addProfile": {
"message": "Add Profile",
"description": "Profile dropdown action to add a profile."
},
"profile.dropdown.manageProfiles": {
"message": "Manage Profiles",
"description": "Profile dropdown action that opens Settings → Profiles."
},
"profile.dropdown.settings": {
"message": "Settings",
"description": "Profile dropdown action that opens Settings."
},
"settings.profiles.section.profiles": {
"message": "Profiles",
"description": "Section heading on the Profiles settings tab."
},
"settings.profiles.intro": {
"message": "Keep separate NetBird identities side by side, for example work and personal accounts, or different management servers. Add, deregister, or delete profiles below.",
"description": "Intro paragraph on the Profiles settings tab explaining what profiles are for."
},
"settings.profiles.addProfile": {
"message": "Add Profile",
"description": "Button on the Profiles settings tab to add a profile."
},
"settings.profiles.active": {
"message": "Active",
"description": "Badge marking the currently active profile in the profiles table."
},
"settings.profiles.emptyTitle": {
"message": "No Profiles",
"description": "Title of the empty state when there are no profiles."
},
"settings.profiles.emptyDescription": {
"message": "Create a profile to connect to a NetBird management server.",
"description": "Body of the no-profiles empty state."
},
"settings.error.loadTitle": {
"message": "Load Settings Failed",
"description": "Error-dialog title when loading settings fails."
},
"settings.error.saveTitle": {
"message": "Save Settings Failed",
"description": "Error-dialog title when saving settings fails."
},
"settings.error.debugBundleTitle": {
"message": "Debug Bundle Failed",
"description": "Error-dialog title when creating the debug bundle fails."
},
"settings.nav.label": {
"message": "Settings sections",
"description": "Accessibility label for the Settings page's side navigation (list of section tabs)."
},
"settings.tabs.general": {
"message": "General",
"description": "Settings tab label: General. Keep short."
},
"settings.tabs.network": {
"message": "Network",
"description": "Settings tab label: Network. Keep short."
},
"settings.tabs.security": {
"message": "Security",
"description": "Settings tab label: Security. Keep short."
},
"settings.tabs.profiles": {
"message": "Profiles",
"description": "Settings tab label: Profiles. Keep short."
},
"settings.tabs.ssh": {
"message": "SSH",
"description": "Settings tab label: SSH. Acronym — keep as-is."
},
"settings.tabs.advanced": {
"message": "Advanced",
"description": "Settings tab label: Advanced. Keep short."
},
"settings.tabs.troubleshooting": {
"message": "Troubleshoot",
"description": "Settings tab label: Troubleshoot. Keep short."
},
"settings.tabs.about": {
"message": "About",
"description": "Settings tab label: About. Keep short."
},
"settings.tabs.updateAvailable": {
"message": "Update Available",
"description": "Settings tab label / badge shown when an update is available."
},
"settings.general.section.general": {
"message": "General",
"description": "Section heading on the General settings tab."
},
"settings.general.section.connection": {
"message": "Connection",
"description": "Section heading for connection-related options on the General tab."
},
"settings.general.connectOnStartup.label": {
"message": "Connect on Startup",
"description": "Toggle label: connect automatically when the service starts."
},
"settings.general.connectOnStartup.help": {
"message": "Automatically establish a connection when the service starts.",
"description": "Helper text for the connect-on-startup toggle."
},
"settings.general.notifications.label": {
"message": "Desktop Notifications",
"description": "Toggle label: enable desktop notifications."
},
"settings.general.notifications.help": {
"message": "Show desktop notifications for new updates and connection events.",
"description": "Helper text for the desktop-notifications toggle."
},
"settings.general.autostart.label": {
"message": "Launch NetBird UI at Login",
"description": "Toggle label: launch the NetBird UI at login."
},
"settings.general.autostart.help": {
"message": "Start the NetBird interface automatically when you log in. This affects the graphical interface only, not the background service.",
"description": "Helper text clarifying autostart affects only the UI, not the background service."
},
"settings.general.autostart.errorTitle": {
"message": "Autostart Change Failed",
"description": "Error-dialog title when changing the autostart setting fails."
},
"settings.general.keepConnectedOnQuit.label": {
"message": "Stay Connected After Quitting",
"description": "Toggle label: keep the VPN connection up after quitting the UI."
},
"settings.general.keepConnectedOnQuit.help": {
"message": "The connection stays up in the background after you close NetBird. It only stops when you disconnect it yourself.",
"description": "Helper text for the stay-connected-after-quitting toggle."
},
"settings.general.language.label": {
"message": "Display Language",
"description": "Label for the display-language picker."
},
"settings.general.language.help": {
"message": "Choose the language for the NetBird interface.",
"description": "Helper text for the language picker."
},
"settings.general.language.search": {
"message": "Search language…",
"description": "Placeholder in the language picker's search field."
},
"settings.general.language.empty": {
"message": "No languages match.",
"description": "Shown when no languages match the search."
},
"settings.general.theme.label": {
"message": "Theme",
"description": "Label for the appearance/theme picker."
},
"settings.general.theme.help": {
"message": "Choose light or dark, or follow your system appearance.",
"description": "Helper text for the theme picker."
},
"settings.general.theme.system": {
"message": "System",
"description": "Theme option: follow the OS appearance."
},
"settings.general.theme.light": {
"message": "Light",
"description": "Theme option: light appearance."
},
"settings.general.theme.dark": {
"message": "Dark",
"description": "Theme option: dark appearance."
},
"settings.general.management.label": {
"message": "Management Server",
"description": "Label for the management-server selector."
},
"settings.general.management.help": {
"message": "Connect to NetBird Cloud or your own self-hosted management server. Changes will reconnect the client.",
"description": "Helper text explaining Cloud vs self-hosted management server; warns that changes reconnect the client."
},
"settings.general.management.cloud": {
"message": "Cloud",
"description": "Option label for using NetBird Cloud as the management server."
},
"settings.general.management.selfHosted": {
"message": "Self-hosted",
"description": "Option label for using a self-hosted management server. 'Self-hosted' is a common technical term."
},
"settings.general.management.urlPlaceholder": {
"message": "https://netbird.selfhosted.com:443",
"description": "Example URL placeholder for the self-hosted server field. It is a sample URL — do not translate the URL itself."
},
"settings.general.management.urlError": {
"message": "Please enter a valid URL, e.g., https://netbird.selfhosted.com:443",
"description": "Validation message for an invalid management-server URL. The example URL stays as-is."
},
"settings.general.management.urlUnreachable": {
"message": "Couldn't reach this server. Check the URL, or save anyway if you're sure it's correct.",
"description": "Soft warning when the management URL couldn't be reached; the user may save anyway."
},
"settings.general.management.switchCloudTitle": {
"message": "Switch to NetBird Cloud?",
"description": "Confirmation-dialog title for switching to NetBird Cloud."
},
"settings.general.management.switchCloudMessage": {
"message": "This disconnects your self-hosted server.\nYou may need to log in again.",
"description": "Confirmation body warning the self-hosted server will be disconnected. Contains a line break (\\n) — keep it."
},
"settings.general.management.switchCloudConfirm": {
"message": "Switch to Cloud",
"description": "Confirm button for switching to Cloud. Keep short."
},
"settings.network.section.connectivity": {
"message": "Connectivity",
"description": "Section heading for connectivity options on the Network tab."
},
"settings.network.section.routingDns": {
"message": "Routing & DNS",
"description": "Section heading for routing and DNS options. 'DNS' is an acronym — keep it."
},
"settings.network.monitor.label": {
"message": "Reconnect on Network Change",
"description": "Toggle label: reconnect automatically on network change."
},
"settings.network.monitor.help": {
"message": "Monitor the network and automatically reconnect on changes such as Wi-Fi switching, Ethernet changes, or resume from sleep.",
"description": "Helper text for the network-change reconnect toggle."
},
"settings.network.dns.label": {
"message": "Enable DNS",
"description": "Toggle label: enable DNS. 'DNS' — keep acronym."
},
"settings.network.dns.help": {
"message": "Apply NetBird-managed DNS settings to the host resolver.",
"description": "Helper text for the enable-DNS toggle."
},
"settings.network.clientRoutes.label": {
"message": "Enable Client Routes",
"description": "Toggle label: enable client routes."
},
"settings.network.clientRoutes.help": {
"message": "Accept routes from other peers to reach their networks.",
"description": "Helper text for client routes (accept routes from other peers)."
},
"settings.network.serverRoutes.label": {
"message": "Enable Server Routes",
"description": "Toggle label: enable server routes."
},
"settings.network.serverRoutes.help": {
"message": "Advertise this host's local routes to other peers.",
"description": "Helper text for server routes (advertise this host's local routes to other peers)."
},
"settings.network.ipv6.label": {
"message": "Enable IPv6",
"description": "Toggle label: enable IPv6. 'IPv6' — keep as-is."
},
"settings.network.ipv6.help": {
"message": "Use IPv6 addressing for the NetBird overlay network.",
"description": "Helper text for the IPv6 toggle."
},
"settings.security.section.firewall": {
"message": "Firewall",
"description": "Section heading: Firewall."
},
"settings.security.section.encryption": {
"message": "Encryption",
"description": "Section heading: Encryption."
},
"settings.security.blockInbound.label": {
"message": "Block Inbound Traffic",
"description": "Toggle label: block inbound traffic."
},
"settings.security.blockInbound.help": {
"message": "Reject unsolicited connections from peers to this device and any networks it routes. Outbound traffic is unaffected.",
"description": "Helper text for blocking inbound traffic."
},
"settings.security.blockLan.label": {
"message": "Block LAN Access",
"description": "Toggle label: block LAN access. 'LAN' — keep acronym."
},
"settings.security.blockLan.help": {
"message": "Prevent peers from reaching your local network or its devices when this device routes their traffic.",
"description": "Helper text for blocking LAN access."
},
"settings.security.rosenpass.label": {
"message": "Enable Quantum-Resistance",
"description": "Toggle label: enable quantum-resistance."
},
"settings.security.rosenpass.help": {
"message": "Add a post-quantum key exchange via Rosenpass on top of WireGuard®.",
"description": "Helper text: adds a post-quantum key exchange via Rosenpass on top of WireGuard®. 'Rosenpass' and 'WireGuard®' are product names — do not translate; keep the ® symbol."
},
"settings.security.rosenpassPermissive.label": {
"message": "Enable Permissive Mode",
"description": "Toggle label: enable permissive mode (for quantum-resistance)."
},
"settings.security.rosenpassPermissive.help": {
"message": "Allow connections to peers without quantum-resistance support.",
"description": "Helper text for permissive mode (allow peers without quantum-resistance support)."
},
"settings.ssh.section.server": {
"message": "Server",
"description": "Section heading: Server (SSH settings)."
},
"settings.ssh.section.capabilities": {
"message": "Capabilities",
"description": "Section heading: Capabilities (SSH features)."
},
"settings.ssh.section.authentication": {
"message": "Authentication",
"description": "Section heading: Authentication (SSH)."
},
"settings.ssh.server.label": {
"message": "Enable SSH Server",
"description": "Toggle label: enable the SSH server."
},
"settings.ssh.server.help": {
"message": "Run the NetBird SSH server on this host so other peers can connect to it.",
"description": "Helper text for the SSH server toggle."
},
"settings.ssh.root.label": {
"message": "Allow Root Login",
"description": "Toggle label: allow root login over SSH. 'root' is the Unix superuser account — keep as-is."
},
"settings.ssh.root.help": {
"message": "Let peers sign in as the root user. Disable to require a non-privileged account.",
"description": "Helper text for allowing root login."
},
"settings.ssh.sftp.label": {
"message": "Allow SFTP",
"description": "Toggle label: allow SFTP. 'SFTP' — keep acronym."
},
"settings.ssh.sftp.help": {
"message": "Transfer files securely using native SFTP or SCP clients.",
"description": "Helper text about SFTP/SCP file transfer. Keep the acronyms."
},
"settings.ssh.localForward.label": {
"message": "Local Port Forwarding",
"description": "Toggle label: local port forwarding."
},
"settings.ssh.localForward.help": {
"message": "Let connecting peers tunnel local ports to services reachable from this host.",
"description": "Helper text for local port forwarding."
},
"settings.ssh.remoteForward.label": {
"message": "Remote Port Forwarding",
"description": "Toggle label: remote port forwarding."
},
"settings.ssh.remoteForward.help": {
"message": "Let connecting peers expose ports on this host back to their own machine.",
"description": "Helper text for remote port forwarding."
},
"settings.ssh.jwt.label": {
"message": "Enable JWT Authentication",
"description": "Toggle label: enable JWT authentication. 'JWT' — keep acronym."
},
"settings.ssh.jwt.help": {
"message": "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.",
"description": "Helper text for JWT auth. 'IdP' (identity provider) and 'ACL' are acronyms — keep them."
},
"settings.ssh.jwtTtl.label": {
"message": "JWT Cache TTL",
"description": "Label for the JWT cache time-to-live field. 'JWT' and 'TTL' — keep acronyms."
},
"settings.ssh.jwtTtl.help": {
"message": "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.",
"description": "Helper text for the JWT cache TTL; mentions setting 0 to disable caching."
},
"settings.ssh.jwtTtl.suffix": {
"message": "Second(s)",
"description": "Unit suffix shown after the JWT TTL number field. The '(s)' marks an optional plural."
},
"settings.advanced.section.interface": {
"message": "Interface",
"description": "Section heading: Interface (network-interface settings)."
},
"settings.advanced.section.security": {
"message": "Security",
"description": "Section heading: Security (advanced)."
},
"settings.advanced.interfaceName.label": {
"message": "Name",
"description": "Field label for the WireGuard interface name."
},
"settings.advanced.interfaceName.error": {
"message": "Use 1-15 letters, digits, dots, hyphens, or underscores.",
"description": "Validation message for the interface name (allowed characters)."
},
"settings.advanced.interfaceName.errorMac": {
"message": "Must start with \"utun\" followed by a number (e.g. utun100).",
"description": "Validation message specific to macOS, where the name must start with 'utun' followed by a number. Keep 'utun' and the example."
},
"settings.advanced.port.label": {
"message": "Port",
"description": "Field label: Port."
},
"settings.advanced.port.error": {
"message": "Enter a port between {min} and {max}.",
"description": "Validation message for the port range. {min} and {max} are numbers; keep them."
},
"settings.advanced.port.help": {
"message": "If set to 0, a random free port will be used.",
"description": "Helper text: 0 means a random free port is used."
},
"settings.advanced.mtu.label": {
"message": "MTU",
"description": "Field label: MTU. 'MTU' — keep acronym."
},
"settings.advanced.mtu.error": {
"message": "Enter an MTU value between {min} and {max}.",
"description": "Validation message for the MTU range. {min} and {max} are numbers; keep them."
},
"settings.advanced.psk.label": {
"message": "Pre-shared Key",
"description": "Field label: Pre-shared Key."
},
"settings.advanced.psk.help": {
"message": "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.",
"description": "Helper text for the WireGuard PSK. 'WireGuard', 'PSK', and 'NetBird Setup Key' are product/technical terms — keep them."
},
"settings.troubleshooting.section.title": {
"message": "Debug bundle",
"description": "Section heading: Debug bundle."
},
"settings.troubleshooting.anonymize.label": {
"message": "Anonymize Sensitive Information",
"description": "Label for the anonymization level dropdown (None, Default, Strict)."
},
"settings.troubleshooting.anonymize.help": {
"message": "Hides IP addresses, domains, and other sensitive values.",
"description": "Helper text under the anonymization dropdown. The level details live in the info tooltip."
},
"settings.troubleshooting.anonymize.info": {
"message": "Default keeps internal IPv4 addresses and peer names readable for support. Strict additionally anonymizes private (RFC 1918), CGNAT, and link-local IP addresses, peer names, and WireGuard public keys. Recurring values map to the same placeholder, so peers stay distinguishable. Use Strict when sharing the bundle outside your organization.",
"description": "Info tooltip explaining the anonymization levels. 'RFC 1918', 'CGNAT', 'link-local', and 'WireGuard' are technical terms — keep them."
},
"settings.troubleshooting.anonymize.none": {
"message": "None",
"description": "Dropdown option: no anonymization."
},
"settings.troubleshooting.anonymize.default": {
"message": "Default",
"description": "Dropdown option: default anonymization level."
},
"settings.troubleshooting.anonymize.strict": {
"message": "Strict",
"description": "Dropdown option: strict anonymization level."
},
"settings.troubleshooting.systemInfo.label": {
"message": "Include System Information",
"description": "Toggle label: include system information in the bundle."
},
"settings.troubleshooting.systemInfo.help": {
"message": "Include OS, kernel, network interfaces, and routing tables.",
"description": "Helper text listing the system info included (OS, kernel, interfaces, routing tables)."
},
"settings.troubleshooting.upload.label": {
"message": "Upload Bundle to NetBird Servers",
"description": "Toggle label: upload the bundle to NetBird servers."
},
"settings.troubleshooting.upload.help": {
"message": "Returns an upload key to share with NetBird support.",
"description": "Helper text for uploading the bundle."
},
"settings.troubleshooting.trace.label": {
"message": "Enable Trace Logs",
"description": "Toggle label: raise daemon log level to TRACE while the bundle is built. 'TRACE' is a log level."
},
"settings.troubleshooting.trace.help": {
"message": "Raises the log level to TRACE and restores it after.",
"description": "Helper text for the trace toggle. 'TRACE' is a log level — keep as-is."
},
"settings.troubleshooting.capture.label": {
"message": "Capture Session",
"description": "Toggle label: open a capture session — reconnect NetBird, wait a duration, optionally record packets."
},
"settings.troubleshooting.capture.help": {
"message": "Reconnects and waits so you can reproduce the issue.",
"description": "Helper text for the master Capture Session toggle."
},
"settings.troubleshooting.packets.label": {
"message": "Capture Network Packets",
"description": "Toggle label: capture packets to a .pcap during the capture session."
},
"settings.troubleshooting.packets.help": {
"message": "Saves a .pcap of network traffic during the capture window.",
"description": "Helper text for the packet recording toggle. '.pcap' is a file extension — keep it."
},
"settings.troubleshooting.duration.label": {
"message": "Capture Duration",
"description": "Label for the trace-capture duration field."
},
"settings.troubleshooting.duration.help": {
"message": "How long the capture session runs.",
"description": "Helper text for the capture duration."
},
"settings.troubleshooting.duration.suffix": {
"message": "Minute(s)",
"description": "Unit suffix after the duration field. The '(s)' marks an optional plural."
},
"settings.troubleshooting.create": {
"message": "Create Bundle",
"description": "Button to create the debug bundle. Keep short."
},
"settings.troubleshooting.progress.description": {
"message": "Collecting logs, system details, and connection state. This usually takes a moment. You can keep using NetBird or close Settings while it finishes.",
"description": "Status text shown while the debug bundle is being collected; reassures the user they can keep using NetBird or close Settings meanwhile."
},
"settings.troubleshooting.cancelling": {
"message": "Canceling…",
"description": "Status shown while cancelling bundle creation. Ends with an ellipsis."
},
"settings.troubleshooting.done.uploadedTitle": {
"message": "Debug bundle successfully uploaded!",
"description": "Success title after the bundle was uploaded."
},
"settings.troubleshooting.done.savedTitle": {
"message": "Bundle saved",
"description": "Title shown when the bundle was saved locally (not uploaded)."
},
"settings.troubleshooting.done.uploadedDescription": {
"message": "Share the upload key below with <docs>NetBird support</docs>. A local copy was also saved on your device.",
"description": "Body after upload, telling the user to share the upload key with support. '<docs>…</docs>' wraps the inline link to the NetBird support docs — keep the tags exactly and wrap the phrase that means 'NetBird support'."
},
"settings.troubleshooting.done.savedDescription": {
"message": "Your debug bundle has been saved locally.",
"description": "Body shown when the bundle was only saved locally."
},
"settings.troubleshooting.done.copyKey": {
"message": "Copy Key",
"description": "Button to copy the upload key. Keep short."
},
"settings.troubleshooting.done.openFolder": {
"message": "Open Folder",
"description": "Button to open the folder containing the bundle. Keep short."
},
"settings.troubleshooting.done.openFileLocation": {
"message": "Open file location",
"description": "Button to reveal the bundle file in the OS file manager."
},
"settings.troubleshooting.uploadFailedWithReason": {
"message": "Upload failed: {reason} The bundle is still saved locally.",
"description": "Shown when upload failed but the bundle was saved locally. {reason} is the server error text; keep it."
},
"settings.troubleshooting.uploadFailed": {
"message": "Upload failed. The bundle is still saved locally.",
"description": "Shown when upload failed (no specific reason) but the bundle was saved locally."
},
"settings.troubleshooting.stage.reconnecting": {
"message": "Reconnecting NetBird…",
"description": "Progress stage: reconnecting NetBird. Ends with an ellipsis."
},
"settings.troubleshooting.stage.capturing": {
"message": "Capturing debug logs",
"description": "Progress stage: capturing logs. {elapsed} and {total} are time values (e.g. 0:30 / 2:00); keep both."
},
"settings.troubleshooting.stage.bundling": {
"message": "Generating debug bundle…",
"description": "Progress stage: generating the debug bundle. Ends with an ellipsis."
},
"settings.troubleshooting.stage.uploading": {
"message": "Uploading to NetBird…",
"description": "Progress stage: uploading to NetBird. Ends with an ellipsis."
},
"settings.troubleshooting.stage.cancelling": {
"message": "Canceling…",
"description": "Progress stage: cancelling. Ends with an ellipsis."
},
"settings.about.client": {
"message": "NetBird Client v{version}",
"description": "About tab: client product name and version. {version} is a version number; keep it. 'NetBird Client' — keep brand."
},
"settings.about.clientName": {
"message": "NetBird Client",
"description": "About tab: the client product name shown when no version is available. Brand — do not translate."
},
"settings.about.development": {
"message": "[Development]",
"description": "Badge shown next to the version on development builds. Keep the square brackets."
},
"settings.about.gui": {
"message": "GUI v{version}",
"description": "About tab: UI component name and version. {version} is a version number; keep it. 'GUI' = the graphical app."
},
"settings.about.guiName": {
"message": "GUI",
"description": "About tab: the UI component name shown without a version. 'GUI' = the graphical app."
},
"settings.about.copyright": {
"message": "© {year} NetBird. All Rights Reserved.",
"description": "Copyright line. {year} is the current year; keep it. 'NetBird' — brand."
},
"settings.about.links.imprint": {
"message": "Imprint",
"description": "Footer link: Imprint (legal notice / Impressum)."
},
"settings.about.links.privacy": {
"message": "Privacy",
"description": "Footer link: Privacy policy."
},
"settings.about.links.cla": {
"message": "CLA",
"description": "Footer link: CLA (Contributor License Agreement). Acronym — keep as-is."
},
"settings.about.links.terms": {
"message": "Terms of Service",
"description": "Footer link: Terms of Service."
},
"settings.about.community.github": {
"message": "GitHub",
"description": "Community link: GitHub. Brand — do not translate."
},
"settings.about.community.slack": {
"message": "Slack",
"description": "Community link: Slack. Brand — do not translate."
},
"settings.about.community.forum": {
"message": "Forum",
"description": "Community link: Forum."
},
"settings.about.community.documentation": {
"message": "Documentation",
"description": "Community link: Documentation."
},
"settings.about.community.feedback": {
"message": "Feedback",
"description": "Community link: Feedback."
},
"update.banner.message": {
"message": "NetBird {version} is ready to install.",
"description": "Update banner text when a new version is ready to install. {version} is the version number; keep it."
},
"update.banner.later": {
"message": "Later",
"description": "Banner button to postpone the update. Keep short."
},
"update.banner.installNow": {
"message": "Install now",
"description": "Banner button to install the update now. Keep short."
},
"update.card.versionAvailableDownload": {
"message": "Version {version} is available for download.",
"description": "Update card text when a version is available to download. {version} is the version number; keep it."
},
"update.card.versionAvailableInstall": {
"message": "Version {version} is available for install.",
"description": "Update card text when a version is available to install. {version} is the version number; keep it."
},
"update.card.whatsNew": {
"message": "What's new?",
"description": "Link / label that opens the release notes."
},
"update.card.installNow": {
"message": "Install Now",
"description": "Update card button: install now. Keep short."
},
"update.card.getInstaller": {
"message": "Download",
"description": "Update card button: download the installer."
},
"update.card.autoCheckInterval": {
"message": "NetBird checks for updates in the background.",
"description": "Note that NetBird checks for updates in the background."
},
"update.card.changelog": {
"message": "Changelog",
"description": "Link / label: Changelog."
},
"update.card.onLatestVersion": {
"message": "You're on the latest version",
"description": "Shown when the client is already up to date."
},
"update.header.tooltip": {
"message": "Update Available",
"description": "Tooltip on the header update badge."
},
"update.overlay.updatingVersion": {
"message": "Updating NetBird to v{version}",
"description": "Heading in the install-progress window while updating to a specific version. {version} is the version number; keep it. The 'v' prefix is part of the version display."
},
"update.overlay.updating": {
"message": "Updating NetBird",
"description": "Heading in the install-progress window while updating when the target version isn't known."
},
"update.overlay.description": {
"message": "A newer version is available and is being installed. NetBird will restart automatically once the update is finished.",
"description": "Install-progress window body explaining NetBird will restart automatically after the update."
},
"update.overlay.error.timeoutTitle": {
"message": "Update Is Taking Too Long",
"description": "Install-progress window error title: the update took too long."
},
"update.overlay.error.timeoutDescription": {
"message": "Installing {target} took too long and didn't finish.",
"description": "Install-progress window error body for a timeout. {target} is the target-version label (see update.overlay.error.targetVersion / targetFallback); keep it."
},
"update.overlay.error.canceledTitle": {
"message": "Update Was Stopped",
"description": "Install-progress window error title: the update was stopped / canceled."
},
"update.overlay.error.canceledDescription": {
"message": "The update to {target} was canceled before it finished.",
"description": "Install-progress window error body for a canceled update. {target} is the target-version label; keep it."
},
"update.overlay.error.failTitle": {
"message": "Couldn't Install the Update",
"description": "Install-progress window error title: the update couldn't be installed."
},
"update.overlay.error.failDescription": {
"message": "{target} couldn't be installed.",
"description": "Install-progress window error body for a failed install. {target} is the target-version label; keep it."
},
"update.overlay.error.unknownMessage": {
"message": "unknown error",
"description": "Fallback text for an unspecified error, used inside other messages."
},
"update.overlay.error.targetVersion": {
"message": "v{version}",
"description": "The target-version label substituted into {target}. {version} is a number; keep the 'v' prefix."
},
"update.overlay.error.targetFallback": {
"message": "the new version",
"description": "Fallback target label used when the version isn't known, substituted into {target}."
},
"update.error.loadStateTitle": {
"message": "Load Update State Failed",
"description": "Error-dialog title when loading update state fails."
},
"update.error.triggerTitle": {
"message": "Start Update Failed",
"description": "Error-dialog title when starting the update fails."
},
"update.page.versionLine": {
"message": "Updating client to: {version}.",
"description": "Install-progress window text naming the target version. {version} is the version number; keep it."
},
"update.page.versionLineGeneric": {
"message": "Updating client.",
"description": "Install-progress text used when the target version isn't known."
},
"update.page.outdated": {
"message": "Your client version is older than the auto-update version set in Management.",
"description": "Note that the client is older than the auto-update version set in Management. 'Management' = the NetBird management server / console."
},
"update.page.status.running": {
"message": "Updating",
"description": "Install status: updating."
},
"update.page.status.timeout": {
"message": "Update timed out. Please try again.",
"description": "Install status: timed out; asks the user to retry."
},
"update.page.status.canceled": {
"message": "Update canceled.",
"description": "Install status: canceled."
},
"update.page.status.failed": {
"message": "Update failed: {message}",
"description": "Install status: failed. {message} is the error detail; keep it."
},
"update.page.status.unknownError": {
"message": "unknown update error",
"description": "Fallback text for an unknown update error, used inside update.page.status.failed."
},
"update.page.failedTitle": {
"message": "Update Failed",
"description": "Heading shown when the install failed."
},
"update.page.timeoutMessage": {
"message": "Update timed out.",
"description": "Message shown when the install timed out."
},
"update.page.dontClose": {
"message": "Please don't close this window.",
"description": "Warning asking the user not to close the install window."
},
"update.page.updating": {
"message": "Updating…",
"description": "Short status: updating. Ends with an ellipsis."
},
"update.page.complete": {
"message": "Update complete",
"description": "Short status: update complete."
},
"update.page.failed": {
"message": "Update failed",
"description": "Short status: update failed."
},
"window.title.settings": {
"message": "Settings",
"description": "OS window-chrome title for the Settings window. The app prefixes it with 'NetBird - '."
},
"window.title.signIn": {
"message": "Sign-in",
"description": "OS window-chrome title for the sign-in window."
},
"window.title.sessionExpiration": {
"message": "Session Expiring",
"description": "OS window-chrome title for the session-expiration window."
},
"window.title.updating": {
"message": "Updating",
"description": "OS window-chrome title for the update / install window."
},
"window.title.welcome": {
"message": "Welcome to NetBird",
"description": "OS window-chrome title for the first-launch welcome window. 'NetBird' — brand."
},
"window.title.error": {
"message": "Error",
"description": "OS window-chrome title for the error window."
},
"welcome.title": {
"message": "Look for NetBird in your tray",
"description": "Heading on the first onboarding step, pointing the user to the tray icon. Shown on Windows and Linux; macOS uses welcome.titleMac."
},
"welcome.titleMac": {
"message": "Look for NetBird in your menu bar",
"description": "Heading on the first onboarding step on macOS, pointing the user to the menu bar icon. Use your language's Apple term for the macOS menu bar."
},
"welcome.description": {
"message": "NetBird lives in your tray. Click the icon to connect, switch profiles, or open settings.",
"description": "Body of the first onboarding step explaining the tray icon. Shown on Windows and Linux; macOS uses welcome.descriptionMac."
},
"welcome.descriptionMac": {
"message": "NetBird lives in your menu bar. Click the icon to connect, switch profiles, or open settings.",
"description": "Body of the first onboarding step on macOS explaining the menu bar icon. Use your language's Apple term for the macOS menu bar."
},
"welcome.continue": {
"message": "Continue",
"description": "Primary button to advance the onboarding. Keep short."
},
"welcome.back": {
"message": "Back",
"description": "Button to go back a step in onboarding. Keep short."
},
"welcome.management.title": {
"message": "Set up NetBird",
"description": "Heading on the onboarding step for choosing a management server."
},
"welcome.management.description": {
"message": "Click Continue to get started, or pick Self-hosted if you have your own NetBird server.",
"description": "Body of the management step; mentions choosing Self-hosted if the user runs their own server."
},
"welcome.management.cloud.title": {
"message": "NetBird Cloud",
"description": "Option title: NetBird Cloud. Brand — do not translate 'NetBird Cloud'."
},
"welcome.management.cloud.description": {
"message": "Use our hosted service. No setup required.",
"description": "Option body for NetBird Cloud (hosted service, no setup required)."
},
"welcome.management.selfHosted.title": {
"message": "Self-hosted",
"description": "Option title: Self-hosted."
},
"welcome.management.selfHosted.description": {
"message": "Connect to your own management server.",
"description": "Option body for connecting to your own management server."
},
"welcome.management.urlLabel": {
"message": "Management server URL",
"description": "Field label for the management-server URL."
},
"welcome.management.urlPlaceholder": {
"message": "https://netbird.selfhosted.com:443",
"description": "Example URL placeholder. Sample URL — do not translate it."
},
"welcome.management.urlInvalid": {
"message": "Please enter a valid URL, e.g., https://netbird.selfhosted.com:443",
"description": "Validation message for an invalid URL. The example URL stays as-is."
},
"welcome.management.urlUnreachable": {
"message": "Couldn't reach this server. Check the URL or your network, then continue if you're sure it's correct.",
"description": "Soft warning when the server couldn't be reached during onboarding; the user may continue anyway."
},
"welcome.management.checking": {
"message": "Checking…",
"description": "Status shown while checking the server URL. Ends with an ellipsis."
},
"browserLogin.title": {
"message": "Complete login in your browser",
"description": "Heading telling the user to finish signing in via their browser."
},
"browserLogin.notSeeing": {
"message": "We opened a browser tab so you can finish signing in. Not seeing it?",
"description": "Prompt asking whether the user doesn't see the opened browser tab."
},
"browserLogin.tryAgain": {
"message": "Try again",
"description": "Button to re-open the browser sign-in page. Keep short."
},
"browserLogin.openFailedTitle": {
"message": "Open Browser Failed",
"description": "Error-dialog title when the browser couldn't be opened."
},
"sessionExpiration.title": {
"message": "Session expiring soon",
"description": "Heading warning the session will expire soon."
},
"sessionExpiration.titleLater": {
"message": "Your session will expire",
"description": "Alternate, less-urgent heading: the session will expire."
},
"sessionExpiration.description": {
"message": "This device will disconnect soon. Renew with a browser sign-in.",
"description": "Body warning the device will disconnect soon; renew via a browser sign-in."
},
"sessionExpiration.descriptionLater": {
"message": "A browser sign-in keeps this device connected to your network.",
"description": "Body for the less-urgent variant; a browser sign-in keeps the device connected."
},
"sessionExpiration.stay": {
"message": "Renew session",
"description": "Button to renew the session (keep connected). Keep short."
},
"sessionExpiration.authenticate": {
"message": "Authenticate",
"description": "Button to authenticate / sign in. Keep short."
},
"sessionExpiration.logout": {
"message": "Logout",
"description": "Button to log out. Keep short."
},
"sessionExpiration.expired": {
"message": "Session expired",
"description": "Heading shown once the session has actually expired."
},
"sessionExpiration.expiredDescription": {
"message": "Device disconnected. Authenticate with a browser sign-in to reconnect.",
"description": "Body shown after expiry; authenticate via a browser sign-in to reconnect."
},
"sessionExpiration.close": {
"message": "Close",
"description": "Close button on the session window. Keep short."
},
"sessionExpiration.extendFailedTitle": {
"message": "Extend Session Failed",
"description": "Error-dialog title when extending the session fails."
},
"sessionExpiration.logoutFailedTitle": {
"message": "Logout Failed",
"description": "Error-dialog title when logging out fails."
},
"peers.search.placeholder": {
"message": "Search by name or IP",
"description": "Placeholder in the peers search field."
},
"peers.filter.all": {
"message": "All",
"description": "Peers filter option: All. Keep short."
},
"peers.filter.online": {
"message": "Online",
"description": "Peers filter option: Online. Keep short."
},
"peers.filter.offline": {
"message": "Offline",
"description": "Peers filter option: Offline. Keep short."
},
"peers.empty.title": {
"message": "No peers available",
"description": "Title of the empty state when no peers are available."
},
"peers.empty.description": {
"message": "You either don't have any peers available or have no access to any of them.",
"description": "Body of the no-peers empty state."
},
"peers.details.domain": {
"message": "Domain",
"description": "Peer detail label: Domain."
},
"peers.details.netbirdIp": {
"message": "NetBird IP",
"description": "Peer detail label: NetBird IP address. 'NetBird' — brand; 'IP' — keep acronym."
},
"peers.details.netbirdIpv6": {
"message": "NetBird IPv6",
"description": "Peer detail label: NetBird IPv6 address. Keep 'NetBird' and 'IPv6'."
},
"peers.details.publicKey": {
"message": "Public key",
"description": "Peer detail label: WireGuard public key."
},
"peers.details.connection": {
"message": "Connection",
"description": "Peer detail label: Connection."
},
"peers.details.latency": {
"message": "Latency",
"description": "Peer detail label: Latency."
},
"peers.details.lastHandshake": {
"message": "Last handshake",
"description": "Peer detail label: time of the last WireGuard handshake."
},
"peers.details.statusSince": {
"message": "Last connection update",
"description": "Peer detail label: time since the last connection-status update."
},
"peers.details.bytes": {
"message": "Bytes",
"description": "Peer detail label: Bytes (data transferred)."
},
"peers.details.bytesSent": {
"message": "Sent",
"description": "Peer detail label: bytes sent."
},
"peers.details.bytesReceived": {
"message": "Received",
"description": "Peer detail label: bytes received."
},
"peers.details.localIce": {
"message": "Local ICE",
"description": "Peer detail label: local ICE candidate. 'ICE' — keep acronym."
},
"peers.details.remoteIce": {
"message": "Remote ICE",
"description": "Peer detail label: remote ICE candidate. 'ICE' — keep acronym."
},
"peers.details.never": {
"message": "Never",
"description": "Value shown when an event has never happened (e.g. last handshake = Never)."
},
"peers.details.justNow": {
"message": "Just now",
"description": "Relative-time value meaning a moment ago."
},
"peers.details.refresh": {
"message": "Refresh",
"description": "Button to refresh peer details. Keep short."
},
"peers.status.connected": {
"message": "Connected",
"description": "Peer status: connected."
},
"peers.status.connecting": {
"message": "Connecting",
"description": "Peer status: connecting."
},
"peers.status.disconnected": {
"message": "Disconnected",
"description": "Peer status: disconnected."
},
"peers.details.relayAddress": {
"message": "Relay",
"description": "Peer detail label: Relay (relay-server address)."
},
"peers.details.networks": {
"message": "Resources",
"description": "Peer detail label for the peer's network resources. Labelled 'Resources' in the UI."
},
"peers.details.relayed": {
"message": "Relayed",
"description": "Connection-type value: the connection is relayed (not direct). Technical term."
},
"peers.details.p2p": {
"message": "P2P",
"description": "Connection-type value: peer-to-peer (direct). 'P2P' — keep as-is."
},
"peers.details.rosenpass": {
"message": "Rosenpass enabled",
"description": "Peer detail indicating Rosenpass (quantum-resistance) is enabled. 'Rosenpass' — product name, do not translate."
},
"networks.search.placeholder": {
"message": "Search by network or domain",
"description": "Placeholder in the resources search field."
},
"networks.filter.all": {
"message": "All",
"description": "Resources filter: All. Keep short."
},
"networks.filter.active": {
"message": "Active",
"description": "Resources filter: Active. Keep short."
},
"networks.filter.overlapping": {
"message": "Overlapping",
"description": "Resources filter: Overlapping (overlapping IP ranges). Keep short."
},
"networks.empty.title": {
"message": "No resources available",
"description": "Title of the empty state when no resources are available."
},
"networks.empty.description": {
"message": "You either don't have any network resources available or have no access to any of them.",
"description": "Body of the no-resources empty state."
},
"networks.selected": {
"message": "Selected",
"description": "Label / badge: the resource is selected (enabled)."
},
"networks.unselected": {
"message": "Not selected",
"description": "Label / badge: the resource is not selected."
},
"networks.ips.heading": {
"message": "Resolved IPs",
"description": "Heading for the list of a resource's resolved IP addresses."
},
"networks.bulk.selectionCount": {
"message": "{selected} of {total} Active",
"description": "Header showing how many resources are active. {selected} and {total} are numbers; keep both."
},
"networks.bulk.enableAll": {
"message": "Enable all",
"description": "Button to enable all resources. Keep short."
},
"networks.bulk.disableAll": {
"message": "Disable all",
"description": "Button to disable all resources. Keep short."
},
"exitNodes.search.placeholder": {
"message": "Search exit nodes",
"description": "Placeholder in the exit-nodes search field."
},
"exitNodes.none": {
"message": "None",
"description": "Option meaning no exit node. Keep short."
},
"exitNodes.empty.title": {
"message": "No exit nodes available",
"description": "Title of the empty state when no exit nodes are available."
},
"exitNodes.empty.description": {
"message": "No exit nodes have been shared with this peer.",
"description": "Body of the no-exit-nodes empty state."
},
"exitNodes.card.title": {
"message": "Exit Node",
"description": "Card heading: Exit Node."
},
"exitNodes.card.statusActive": {
"message": "Active",
"description": "Exit node card status: Active."
},
"exitNodes.card.statusInactive": {
"message": "Inactive",
"description": "Exit node card status: Inactive."
},
"exitNodes.dropdown.noneTitle": {
"message": "None",
"description": "Dropdown option title: None (no exit node)."
},
"exitNodes.dropdown.noneDescription": {
"message": "Direct connection without an exit node",
"description": "Dropdown option body for None: direct connection without an exit node."
},
"quickActions.connect": {
"message": "Connect",
"description": "Quick-action button: Connect. Keep short."
},
"quickActions.disconnect": {
"message": "Disconnect",
"description": "Quick-action button: Disconnect. Keep short."
},
"daemon.unavailable.title": {
"message": "NetBird Service Is Not Running",
"description": "Title of the overlay shown when the NetBird background service isn't running."
},
"daemon.unavailable.description": {
"message": "The app will reconnect automatically once the service is running.",
"description": "Body reassuring the user the app will reconnect once the service is running."
},
"daemon.unavailable.docsLink": {
"message": "Documentation",
"description": "Documentation link on the daemon-unavailable overlay."
},
"daemon.outdated.title": {
"message": "NetBird Client Is Outdated",
"description": "Title of the overlay shown when the NetBird client (daemon) is too old to drive this UI."
},
"daemon.outdated.description": {
"message": "The new GUI isn't compatible with the older NetBird client. Update your client to use the new application.",
"description": "Body of the daemon-outdated overlay explaining that the GUI is newer than the client and the client must be updated."
},
"daemon.outdated.download": {
"message": "Download Latest",
"description": "Button on the daemon-outdated overlay that opens the download page for the latest release."
},
"error.jwt_clock_skew": {
"message": "Sign-in failed: this device's clock is out of sync with the server. Please sync your system clock and try again.",
"description": "Sign-in error shown to the user: the device clock is out of sync with the server."
},
"error.jwt_expired": {
"message": "Your sign-in token has expired. Please sign in again.",
"description": "Sign-in error: the sign-in token expired; sign in again."
},
"error.jwt_signature_invalid": {
"message": "Sign-in failed: the token signature is invalid. Please contact your administrator.",
"description": "Sign-in error: the token signature is invalid; contact the administrator."
},
"error.session_expired": {
"message": "Your session has expired. Please sign in again.",
"description": "Error: the session expired; sign in again."
},
"error.invalid_setup_key": {
"message": "The setup key is missing or invalid.",
"description": "Error: the setup key is missing or invalid. 'setup key' is a NetBird term."
},
"error.permission_denied": {
"message": "Sign-in was rejected by the server.",
"description": "Error: the server rejected the sign-in."
},
"error.daemon_unreachable": {
"message": "The NetBird daemon is not responding. Please check that the service is running.",
"description": "Error: the NetBird background service isn't responding. 'daemon' = the background service."
},
"error.settings_locked": {
"message": "Settings cannot be changed on this device: an administrator has locked them.",
"description": "Error: the local daemon was started with update-settings disabled, so it refuses configuration changes."
},
"error.settings_managed_by_mdm": {
"message": "This setting is managed by your organization and cannot be changed.",
"description": "Error: the setting is enforced by an MDM policy. 'MDM' = mobile device management, the organization's device-management system."
},
"error.unknown": {
"message": "Operation failed.",
"description": "Generic fallback error message used when no specific error applies."
},
"error.elevation_unavailable": {
"message": "NetBird could not ask this system for the privileges the change needs. Run this instead:",
"description": "Error: this computer has no way to prompt for elevated privileges. Followed by a copyable command that applies the setting from a terminal."
},
"error.elevation_failed": {
"message": "The change could not be applied with elevated privileges. Run this instead:",
"description": "Error: the authorization succeeded but applying the setting afterwards failed. Followed by a copyable command that applies the setting from a terminal."
},
"settings.ssh.privilege.actorRoot": {
"message": "root",
"description": "Fills {actor} in the settings.ssh.privilege.* messages on Linux, macOS and BSD, where the daemon requires the root account. 'root' is an account name and stays as it is; add the word for privileges or rights around it if the sentence needs one to read naturally."
},
"settings.ssh.privilege.actorAdministrator": {
"message": "administrator privileges",
"description": "Fills {actor} in the settings.ssh.privilege.* messages on Windows, where the daemon requires an elevated administrator. The Windows term for the rights an account is asked to elevate to."
},
"settings.ssh.privilege.hint": {
"message": "Requires {actor}. Run this instead:",
"description": "Help text under an SSH setting the user cannot change: it needs elevated privileges. {actor} is 'root' on Linux/macOS or 'administrator privileges' on Windows. Followed by a copyable command."
},
"settings.ssh.privilege.oneWay": {
"message": "You can switch this off, but switching it back on needs {actor}.",
"description": "Help text under an SSH setting that is already on: an unprivileged user may switch it off freely, and switching it on again is what needs the privileges. No command follows, since the direction they can take is theirs to take. {actor} is 'root' on Linux/macOS or 'administrator privileges' on Windows."
},
"settings.ssh.privilege.oneWayInverted": {
"message": "You can switch this on, but switching it back off needs {actor}.",
"description": "Same as settings.ssh.privilege.oneWay, for the SSH authentication setting once it has been switched off: switching it off again is what needs the privileges."
},
"settings.ssh.privilege.authorizePending": {
"message": "Waiting for authorization…",
"description": "Replaces the help text under a guarded SSH setting while the authorization prompt is open, which can take a few seconds to appear. Keep the trailing ellipsis."
}
}