Localize daemon notifications via stable message keys

This commit is contained in:
Viktor Liu
2026-08-05 17:53:02 +02:00
parent f2318a8fef
commit b3ead5ee7e
37 changed files with 1592 additions and 261 deletions
+174 -126
View File
@@ -3909,14 +3909,30 @@ func (*SubscribeRequest) Descriptor() ([]byte, []int) {
}
type SystemEvent struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Severity SystemEvent_Severity `protobuf:"varint,2,opt,name=severity,proto3,enum=daemon.SystemEvent_Severity" json:"severity,omitempty"`
Category SystemEvent_Category `protobuf:"varint,3,opt,name=category,proto3,enum=daemon.SystemEvent_Category" json:"category,omitempty"`
Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"`
UserMessage string `protobuf:"bytes,5,opt,name=userMessage,proto3" json:"userMessage,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Severity SystemEvent_Severity `protobuf:"varint,2,opt,name=severity,proto3,enum=daemon.SystemEvent_Severity" json:"severity,omitempty"`
Category SystemEvent_Category `protobuf:"varint,3,opt,name=category,proto3,enum=daemon.SystemEvent_Category" json:"category,omitempty"`
Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"`
// userMessage is the daemon's English rendering of messageKey, kept for the
// CLI and for UIs that predate messageKey. UIs that localise read messageKey
// and treat this as the fallback.
UserMessage string `protobuf:"bytes,5,opt,name=userMessage,proto3" json:"userMessage,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// messageKey names a user-facing message in a stable, locale-independent
// form. A UI resolves it against its own translation bundle and substitutes
// messageArgs; an unrecognised key falls back to userMessage. Empty on events
// that carry no user-facing text.
MessageKey string `protobuf:"bytes,8,opt,name=messageKey,proto3" json:"messageKey,omitempty"`
// messageArgs holds the placeholder name/value pairs for messageKey, e.g.
// {"version": "0.60.1"} for a "{version}" template. Values are data the
// daemon cannot localise (versions, addresses, error text).
MessageArgs map[string]string `protobuf:"bytes,9,rep,name=messageArgs,proto3" json:"messageArgs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// titleKey names the notification title in the same way as messageKey. Empty
// when the event has no dedicated title, in which case a UI composes one from
// severity and category.
TitleKey string `protobuf:"bytes,10,opt,name=titleKey,proto3" json:"titleKey,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -4000,6 +4016,27 @@ func (x *SystemEvent) GetMetadata() map[string]string {
return nil
}
func (x *SystemEvent) GetMessageKey() string {
if x != nil {
return x.MessageKey
}
return ""
}
func (x *SystemEvent) GetMessageArgs() map[string]string {
if x != nil {
return x.MessageArgs
}
return nil
}
func (x *SystemEvent) GetTitleKey() string {
if x != nil {
return x.TitleKey
}
return ""
}
type GetEventsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
@@ -7332,7 +7369,7 @@ const file_daemon_proto_rawDesc = "" +
"\x13TracePacketResponse\x12*\n" +
"\x06stages\x18\x01 \x03(\v2\x12.daemon.TraceStageR\x06stages\x12+\n" +
"\x11final_disposition\x18\x02 \x01(\bR\x10finalDisposition\"\x12\n" +
"\x10SubscribeRequest\"\x93\x04\n" +
"\x10SubscribeRequest\"\xd7\x05\n" +
"\vSystemEvent\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x128\n" +
"\bseverity\x18\x02 \x01(\x0e2\x1c.daemon.SystemEvent.SeverityR\bseverity\x128\n" +
@@ -7340,9 +7377,18 @@ const file_daemon_proto_rawDesc = "" +
"\amessage\x18\x04 \x01(\tR\amessage\x12 \n" +
"\vuserMessage\x18\x05 \x01(\tR\vuserMessage\x128\n" +
"\ttimestamp\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12=\n" +
"\bmetadata\x18\a \x03(\v2!.daemon.SystemEvent.MetadataEntryR\bmetadata\x1a;\n" +
"\bmetadata\x18\a \x03(\v2!.daemon.SystemEvent.MetadataEntryR\bmetadata\x12\x1e\n" +
"\n" +
"messageKey\x18\b \x01(\tR\n" +
"messageKey\x12F\n" +
"\vmessageArgs\x18\t \x03(\v2$.daemon.SystemEvent.MessageArgsEntryR\vmessageArgs\x12\x1a\n" +
"\btitleKey\x18\n" +
" \x01(\tR\btitleKey\x1a;\n" +
"\rMetadataEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" +
"\x10MessageArgsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\":\n" +
"\bSeverity\x12\b\n" +
"\x04INFO\x10\x00\x12\v\n" +
@@ -7660,7 +7706,7 @@ func file_daemon_proto_rawDescGZIP() []byte {
}
var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 110)
var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 111)
var file_daemon_proto_goTypes = []any{
(LogLevel)(0), // 0: daemon.LogLevel
(ExposeProtocol)(0), // 1: daemon.ExposeProtocol
@@ -7776,16 +7822,17 @@ var file_daemon_proto_goTypes = []any{
nil, // 111: daemon.Network.ResolvedIPsEntry
(*PortInfo_Range)(nil), // 112: daemon.PortInfo.Range
nil, // 113: daemon.SystemEvent.MetadataEntry
(*durationpb.Duration)(nil), // 114: google.protobuf.Duration
(*timestamppb.Timestamp)(nil), // 115: google.protobuf.Timestamp
nil, // 114: daemon.SystemEvent.MessageArgsEntry
(*durationpb.Duration)(nil), // 115: google.protobuf.Duration
(*timestamppb.Timestamp)(nil), // 116: google.protobuf.Timestamp
}
var file_daemon_proto_depIdxs = []int32{
114, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
115, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
25, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
115, // 2: daemon.StatusResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
115, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
115, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
114, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration
116, // 2: daemon.StatusResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
116, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
116, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
115, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration
23, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo
20, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
19, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState
@@ -7808,114 +7855,115 @@ var file_daemon_proto_depIdxs = []int32{
54, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage
2, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity
3, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category
115, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp
116, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp
113, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry
57, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent
114, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
72, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile
115, // 33: daemon.WaitExtendAuthSessionResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
1, // 34: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol
104, // 35: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady
114, // 36: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration
114, // 37: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration
30, // 38: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList
5, // 39: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
7, // 40: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
9, // 41: daemon.DaemonService.Up:input_type -> daemon.UpRequest
11, // 42: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
11, // 43: daemon.DaemonService.SubscribeStatus:input_type -> daemon.StatusRequest
13, // 44: daemon.DaemonService.Down:input_type -> daemon.DownRequest
15, // 45: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
26, // 46: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest
28, // 47: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest
28, // 48: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest
4, // 49: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest
35, // 50: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest
37, // 51: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest
39, // 52: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest
44, // 53: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest
46, // 54: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest
48, // 55: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest
50, // 56: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest
53, // 57: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest
105, // 58: daemon.DaemonService.StartCapture:input_type -> daemon.StartCaptureRequest
107, // 59: daemon.DaemonService.StartBundleCapture:input_type -> daemon.StartBundleCaptureRequest
109, // 60: daemon.DaemonService.StopBundleCapture:input_type -> daemon.StopBundleCaptureRequest
56, // 61: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest
58, // 62: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest
41, // 63: daemon.DaemonService.RegisterUILog:input_type -> daemon.RegisterUILogRequest
60, // 64: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest
62, // 65: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest
64, // 66: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest
66, // 67: daemon.DaemonService.RenameProfile:input_type -> daemon.RenameProfileRequest
68, // 68: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest
70, // 69: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest
73, // 70: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest
75, // 71: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest
79, // 72: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest
82, // 73: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest
84, // 74: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest
86, // 75: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest
88, // 76: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest
90, // 77: daemon.DaemonService.RequestExtendAuthSession:input_type -> daemon.RequestExtendAuthSessionRequest
92, // 78: daemon.DaemonService.WaitExtendAuthSession:input_type -> daemon.WaitExtendAuthSessionRequest
94, // 79: daemon.DaemonService.DismissSessionWarning:input_type -> daemon.DismissSessionWarningRequest
96, // 80: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest
98, // 81: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest
100, // 82: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest
102, // 83: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest
77, // 84: daemon.DaemonService.WailsUIReady:input_type -> daemon.WailsUIReadyRequest
6, // 85: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
8, // 86: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
10, // 87: daemon.DaemonService.Up:output_type -> daemon.UpResponse
12, // 88: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
12, // 89: daemon.DaemonService.SubscribeStatus:output_type -> daemon.StatusResponse
14, // 90: daemon.DaemonService.Down:output_type -> daemon.DownResponse
16, // 91: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
27, // 92: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse
29, // 93: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse
29, // 94: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse
34, // 95: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse
36, // 96: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse
38, // 97: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse
40, // 98: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse
45, // 99: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse
47, // 100: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse
49, // 101: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse
51, // 102: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse
55, // 103: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse
106, // 104: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket
108, // 105: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse
110, // 106: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse
57, // 107: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent
59, // 108: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse
42, // 109: daemon.DaemonService.RegisterUILog:output_type -> daemon.RegisterUILogResponse
61, // 110: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse
63, // 111: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse
65, // 112: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse
67, // 113: daemon.DaemonService.RenameProfile:output_type -> daemon.RenameProfileResponse
69, // 114: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse
71, // 115: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse
74, // 116: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse
76, // 117: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse
80, // 118: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse
83, // 119: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse
85, // 120: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse
87, // 121: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse
89, // 122: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse
91, // 123: daemon.DaemonService.RequestExtendAuthSession:output_type -> daemon.RequestExtendAuthSessionResponse
93, // 124: daemon.DaemonService.WaitExtendAuthSession:output_type -> daemon.WaitExtendAuthSessionResponse
95, // 125: daemon.DaemonService.DismissSessionWarning:output_type -> daemon.DismissSessionWarningResponse
97, // 126: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse
99, // 127: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse
101, // 128: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse
103, // 129: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent
78, // 130: daemon.DaemonService.WailsUIReady:output_type -> daemon.WailsUIReadyResponse
85, // [85:131] is the sub-list for method output_type
39, // [39:85] is the sub-list for method input_type
39, // [39:39] is the sub-list for extension type_name
39, // [39:39] is the sub-list for extension extendee
0, // [0:39] is the sub-list for field type_name
114, // 30: daemon.SystemEvent.messageArgs:type_name -> daemon.SystemEvent.MessageArgsEntry
57, // 31: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent
115, // 32: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
72, // 33: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile
116, // 34: daemon.WaitExtendAuthSessionResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
1, // 35: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol
104, // 36: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady
115, // 37: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration
115, // 38: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration
30, // 39: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList
5, // 40: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
7, // 41: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
9, // 42: daemon.DaemonService.Up:input_type -> daemon.UpRequest
11, // 43: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
11, // 44: daemon.DaemonService.SubscribeStatus:input_type -> daemon.StatusRequest
13, // 45: daemon.DaemonService.Down:input_type -> daemon.DownRequest
15, // 46: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
26, // 47: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest
28, // 48: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest
28, // 49: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest
4, // 50: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest
35, // 51: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest
37, // 52: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest
39, // 53: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest
44, // 54: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest
46, // 55: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest
48, // 56: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest
50, // 57: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest
53, // 58: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest
105, // 59: daemon.DaemonService.StartCapture:input_type -> daemon.StartCaptureRequest
107, // 60: daemon.DaemonService.StartBundleCapture:input_type -> daemon.StartBundleCaptureRequest
109, // 61: daemon.DaemonService.StopBundleCapture:input_type -> daemon.StopBundleCaptureRequest
56, // 62: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest
58, // 63: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest
41, // 64: daemon.DaemonService.RegisterUILog:input_type -> daemon.RegisterUILogRequest
60, // 65: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest
62, // 66: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest
64, // 67: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest
66, // 68: daemon.DaemonService.RenameProfile:input_type -> daemon.RenameProfileRequest
68, // 69: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest
70, // 70: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest
73, // 71: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest
75, // 72: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest
79, // 73: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest
82, // 74: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest
84, // 75: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest
86, // 76: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest
88, // 77: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest
90, // 78: daemon.DaemonService.RequestExtendAuthSession:input_type -> daemon.RequestExtendAuthSessionRequest
92, // 79: daemon.DaemonService.WaitExtendAuthSession:input_type -> daemon.WaitExtendAuthSessionRequest
94, // 80: daemon.DaemonService.DismissSessionWarning:input_type -> daemon.DismissSessionWarningRequest
96, // 81: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest
98, // 82: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest
100, // 83: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest
102, // 84: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest
77, // 85: daemon.DaemonService.WailsUIReady:input_type -> daemon.WailsUIReadyRequest
6, // 86: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
8, // 87: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
10, // 88: daemon.DaemonService.Up:output_type -> daemon.UpResponse
12, // 89: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
12, // 90: daemon.DaemonService.SubscribeStatus:output_type -> daemon.StatusResponse
14, // 91: daemon.DaemonService.Down:output_type -> daemon.DownResponse
16, // 92: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
27, // 93: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse
29, // 94: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse
29, // 95: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse
34, // 96: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse
36, // 97: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse
38, // 98: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse
40, // 99: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse
45, // 100: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse
47, // 101: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse
49, // 102: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse
51, // 103: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse
55, // 104: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse
106, // 105: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket
108, // 106: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse
110, // 107: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse
57, // 108: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent
59, // 109: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse
42, // 110: daemon.DaemonService.RegisterUILog:output_type -> daemon.RegisterUILogResponse
61, // 111: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse
63, // 112: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse
65, // 113: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse
67, // 114: daemon.DaemonService.RenameProfile:output_type -> daemon.RenameProfileResponse
69, // 115: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse
71, // 116: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse
74, // 117: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse
76, // 118: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse
80, // 119: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse
83, // 120: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse
85, // 121: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse
87, // 122: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse
89, // 123: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse
91, // 124: daemon.DaemonService.RequestExtendAuthSession:output_type -> daemon.RequestExtendAuthSessionResponse
93, // 125: daemon.DaemonService.WaitExtendAuthSession:output_type -> daemon.WaitExtendAuthSessionResponse
95, // 126: daemon.DaemonService.DismissSessionWarning:output_type -> daemon.DismissSessionWarningResponse
97, // 127: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse
99, // 128: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse
101, // 129: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse
103, // 130: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent
78, // 131: daemon.DaemonService.WailsUIReady:output_type -> daemon.WailsUIReadyResponse
86, // [86:132] is the sub-list for method output_type
40, // [40:86] is the sub-list for method input_type
40, // [40:40] is the sub-list for extension type_name
40, // [40:40] is the sub-list for extension extendee
0, // [0:40] is the sub-list for field type_name
}
func init() { file_daemon_proto_init() }
@@ -7947,7 +7995,7 @@ func file_daemon_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)),
NumEnums: 4,
NumMessages: 110,
NumMessages: 111,
NumExtensions: 0,
NumServices: 1,
},
+16
View File
@@ -677,9 +677,25 @@ message SystemEvent {
Severity severity = 2;
Category category = 3;
string message = 4;
// userMessage is the daemon's English rendering of messageKey, kept for the
// CLI and for UIs that predate messageKey. UIs that localise read messageKey
// and treat this as the fallback.
string userMessage = 5;
google.protobuf.Timestamp timestamp = 6;
map<string, string> metadata = 7;
// messageKey names a user-facing message in a stable, locale-independent
// form. A UI resolves it against its own translation bundle and substitutes
// messageArgs; an unrecognised key falls back to userMessage. Empty on events
// that carry no user-facing text.
string messageKey = 8;
// messageArgs holds the placeholder name/value pairs for messageKey, e.g.
// {"version": "0.60.1"} for a "{version}" template. Values are data the
// daemon cannot localise (versions, addresses, error text).
map<string, string> messageArgs = 9;
// titleKey names the notification title in the same way as messageKey. Empty
// when the event has no dedicated title, in which case a UI composes one from
// severity and category.
string titleKey = 10;
}
message GetEventsRequest {}
+4 -4
View File
@@ -43,10 +43,10 @@ const (
// UIs to re-fetch their cached config + features. UserMessage is empty so
// the change is silent; the source is carried in MetadataSourceKey.
MetadataTypeConfigChanged = "config_changed"
// MetadataTypePolicyApplied marks an MDM-policy-driven config change. The
// daemon stamps it with a (non-localised) UserMessage; the UI suppresses
// that and builds its own localised toast off the paired config_changed
// event instead.
// MetadataTypePolicyApplied marks an MDM-policy-driven config change. It is
// the user-facing half of the pair: the daemon stamps it with the message
// and title keys a UI localises, while the paired config_changed event stays
// silent and only drives the cache refresh.
MetadataTypePolicyApplied = "policy_applied"
// MetadataSourceKey is the SystemEvent.metadata key carrying what
+183
View File
@@ -0,0 +1,183 @@
package proto
import (
"strings"
log "github.com/sirupsen/logrus"
)
// UserMessageKey names a piece of user-facing SystemEvent text in a
// locale-independent form. The daemon has no notion of the user's language, so it
// publishes the key and lets each UI resolve it.
//
// The value doubles as the lookup key in the desktop UI's translation bundles
// (client/ui/i18n/locales/<code>/common.json), so renaming a constant here means
// renaming the key in every bundle. The tests in client/ui/i18n lock the two
// sides together. Keys that predate this mechanism keep their original bundle
// names so the shipped translations still apply.
type UserMessageKey string
// Message-body keys published by the daemon. Every key needs an entry in
// UserMessageTexts below and a translation in the UI bundles.
const (
// UserMsgPanic is the CRITICAL event published from the recover() guard in
// the connect loop.
UserMsgPanic UserMessageKey = "event.panic"
// UserMsgDNSRecovered and UserMsgDNSUnreachable bracket a nameserver
// group's health transitions.
UserMsgDNSRecovered UserMessageKey = "event.dns.recovered"
UserMsgDNSUnreachable UserMessageKey = "event.dns.unreachable"
// Exit-node (default route) transitions.
UserMsgExitNodeConnected UserMessageKey = "event.exitNode.connected"
UserMsgExitNodeDisconnected UserMessageKey = "event.exitNode.disconnected"
UserMsgExitNodeConnectionLost UserMessageKey = "event.exitNode.connectionLost"
UserMsgExitNodeHAChange UserMessageKey = "event.exitNode.haChange"
UserMsgExitNodeDisconnectedUnknown UserMessageKey = "event.exitNode.disconnectedUnknown"
// Auto-update lifecycle. UserMsgUpdateFailed takes a {reason} argument and
// UserMsgUpdateCompleted a {version}.
UserMsgUpdateInstalling UserMessageKey = "event.update.installing"
UserMsgUpdateCompleted UserMessageKey = "event.update.completed"
UserMsgUpdateFailed UserMessageKey = "event.update.failed"
// UserMsgMDMPolicyApplied reports that an MDM policy replaced the config.
UserMsgMDMPolicyApplied UserMessageKey = "notify.mdm.policyApplied.body"
// Session-expiry events. UserMsgSessionExpiresIn takes a {remaining}
// argument; the "soon" variant is published when the deadline has already
// passed by the time the warning fires.
UserMsgSessionExpiresIn UserMessageKey = "notify.sessionWarning.body"
UserMsgSessionExpiresSoon UserMessageKey = "notify.sessionWarning.bodyGeneric"
UserMsgSessionDeadlineReject UserMessageKey = "notify.sessionDeadlineRejected.body"
)
// Notification-title keys. An event without one leaves titleKey empty and the UI
// composes a title from severity and category, which is what every event did
// before message keys existed. These carry no English fallback: a UI old enough
// to ignore titleKey already builds its own title.
const (
TitleMDMPolicyApplied UserMessageKey = "notify.mdm.policyApplied.title"
TitleSessionWarning UserMessageKey = "notify.sessionWarning.title"
TitleSessionDeadlineReject UserMessageKey = "notify.sessionDeadlineRejected.title"
)
// UserMessageTitleKeys lists every title key a daemon can publish. It sits next
// to the constants above because the two must grow together: a title key missing
// from this slice ships untranslated, and the i18n test that would have caught it
// reads this list.
var UserMessageTitleKeys = []UserMessageKey{
TitleMDMPolicyApplied,
TitleSessionWarning,
TitleSessionDeadlineReject,
}
// ArgReason, ArgVersion and ArgRemaining are the placeholder names used by the
// templates below. Producers pass them to NewUserMessage; the UI bundles use the
// same names inside {}.
const (
ArgReason = "reason"
ArgVersion = "version"
ArgRemaining = "remaining"
)
// UserMessageTexts is the English rendering of every body key, and the source of
// the SystemEvent.userMessage fallback that the CLI and pre-messageKey UIs read.
// It must match the en bundle, which the i18n test enforces.
var UserMessageTexts = map[UserMessageKey]string{
UserMsgPanic: "The NetBird service panicked. Please restart the service and submit a bug report with the client logs.",
UserMsgDNSRecovered: "DNS servers are reachable again.",
UserMsgDNSUnreachable: "Unable to reach one or more DNS servers. This might affect your ability to connect to some services.",
UserMsgExitNodeConnected: "Exit node connected.",
UserMsgExitNodeDisconnected: "Exit node disconnected.",
UserMsgExitNodeConnectionLost: "Exit node connection lost. Your internet access might be affected.",
UserMsgExitNodeHAChange: "Exit node disconnected due to high availability change.",
UserMsgExitNodeDisconnectedUnknown: "Exit node disconnected for unknown reasons.",
UserMsgUpdateInstalling: "Installing update now.",
UserMsgUpdateCompleted: "Your NetBird client was auto-updated to version {version}.",
UserMsgUpdateFailed: "Auto-update failed: {reason}",
UserMsgMDMPolicyApplied: "Your NetBird configuration was updated by your IT policy.",
UserMsgSessionExpiresIn: "Your NetBird session expires in {remaining}. Click Extend now to renew.",
UserMsgSessionExpiresSoon: "Your NetBird session is about to expire. Click Extend now to renew.",
UserMsgSessionDeadlineReject: "The server sent an invalid session deadline. Please sign in again.",
}
// UserMessage is a localizable user-facing event message: a stable body key, an
// optional title key, and the placeholder values to substitute. A nil
// *UserMessage carries no user-facing text, which is how internal control events
// are published.
type UserMessage struct {
key UserMessageKey
title UserMessageKey
args map[string]string
}
// NewUserMessage builds a UserMessage from key and flat placeholder name/value
// pairs, e.g. NewUserMessage(UserMsgUpdateCompleted, ArgVersion, "0.60.1"). An
// unpaired trailing argument is dropped.
func NewUserMessage(key UserMessageKey, args ...string) *UserMessage {
m := &UserMessage{key: key}
if len(args)%2 != 0 {
log.Debugf("user message %q: placeholder args not paired: %d items, last dropped", key, len(args))
args = args[:len(args)-1]
}
if len(args) > 0 {
m.args = make(map[string]string, len(args)/2)
for i := 0; i < len(args); i += 2 {
m.args[args[i]] = args[i+1]
}
}
return m
}
// WithTitle attaches a notification title key and returns m, so it chains onto
// NewUserMessage.
func (m *UserMessage) WithTitle(key UserMessageKey) *UserMessage {
m.title = key
return m
}
// Key returns the body key, or the empty key for a nil message.
func (m *UserMessage) Key() UserMessageKey {
if m == nil {
return ""
}
return m.key
}
// TitleKey returns the title key, which is empty unless WithTitle was called.
func (m *UserMessage) TitleKey() UserMessageKey {
if m == nil {
return ""
}
return m.title
}
// Args returns the placeholder values, or nil for a nil message. The map is the
// message's own and must not be mutated by the caller; it is handed straight to
// SystemEvent.messageArgs.
func (m *UserMessage) Args() map[string]string {
if m == nil {
return nil
}
return m.args
}
// Text renders the English fallback for m, with placeholders substituted. A nil
// message, or a key with no registered template, renders empty so a UI treats
// the event as carrying no user-facing text.
func (m *UserMessage) Text() string {
if m == nil {
return ""
}
tmpl, ok := UserMessageTexts[m.key]
if !ok {
log.Warnf("no English template for user message key %q", m.key)
return ""
}
for name, value := range m.args {
tmpl = strings.ReplaceAll(tmpl, "{"+name+"}", value)
}
return tmpl
}
+91
View File
@@ -0,0 +1,91 @@
package proto
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUserMessageText(t *testing.T) {
tests := []struct {
name string
msg *UserMessage
want string
}{
{
name: "no placeholders",
msg: NewUserMessage(UserMsgExitNodeConnected),
want: "Exit node connected.",
},
{
name: "placeholder substituted",
msg: NewUserMessage(UserMsgUpdateCompleted, ArgVersion, "0.60.1"),
want: "Your NetBird client was auto-updated to version 0.60.1.",
},
{
// A dangling arg is a caller mistake; dropping it must still yield a
// readable sentence rather than panicking on the wire path.
name: "unpaired trailing arg dropped",
msg: NewUserMessage(UserMsgUpdateFailed, ArgReason, "disk full", "extra"),
want: "Auto-update failed: disk full",
},
{
name: "unknown key renders empty so the UI treats the event as silent",
msg: NewUserMessage("event.notRegistered"),
want: "",
},
{
name: "nil message carries no text",
msg: nil,
want: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, tc.msg.Text())
})
}
}
// A nil *UserMessage is how every internal control event is published, so the
// accessors must stay usable without a nil check at each of the ~25 call sites.
func TestNilUserMessageAccessors(t *testing.T) {
var msg *UserMessage
assert.Empty(t, msg.Key(), "nil message must have no body key")
assert.Empty(t, msg.TitleKey(), "nil message must have no title key")
assert.Nil(t, msg.Args(), "nil message must have no args")
assert.Empty(t, msg.Text(), "nil message must render empty")
}
func TestUserMessageArgsAndTitle(t *testing.T) {
msg := NewUserMessage(UserMsgSessionExpiresIn, ArgRemaining, "10m").
WithTitle(TitleSessionWarning)
assert.Equal(t, UserMsgSessionExpiresIn, msg.Key())
assert.Equal(t, TitleSessionWarning, msg.TitleKey())
assert.Equal(t, map[string]string{ArgRemaining: "10m"}, msg.Args())
assert.Equal(t, "Your NetBird session expires in 10m. Click Extend now to renew.", msg.Text())
}
// Every registered template must be reachable: a key whose text is empty would
// publish an event with a key but no fallback for the CLI and older UIs.
func TestUserMessageTextsAreNonEmpty(t *testing.T) {
texts := UserMessageTexts
assert.NotEmpty(t, texts, "the catalog must not be empty")
for key, text := range texts {
assert.NotEmpty(t, text, "message key %q has an empty template", key)
assert.NotEmpty(t, key, "the catalog must not contain an empty key")
}
}
func TestUserMessageTitleKeysAreUnique(t *testing.T) {
seen := make(map[UserMessageKey]struct{})
for _, key := range UserMessageTitleKeys {
assert.NotEmpty(t, key, "title keys must not be empty")
_, dup := seen[key]
assert.False(t, dup, "title key %q listed twice", key)
seen[key] = struct{}{}
}
}