From 5114fc285400fac8f126ebc1dc85e931eae4b850 Mon Sep 17 00:00:00 2001 From: uubulb Date: Fri, 1 Nov 2024 05:07:04 +0800 Subject: [PATCH 1/4] feat: add i18n support --- cmd/dashboard/controller/alertrule.go | 14 +- cmd/dashboard/controller/controller.go | 2 +- cmd/dashboard/controller/cron.go | 7 +- cmd/dashboard/controller/ddns.go | 12 +- cmd/dashboard/controller/fm.go | 5 +- cmd/dashboard/controller/nat.go | 3 +- cmd/dashboard/controller/notification.go | 7 +- .../controller/notification_group.go | 7 +- cmd/dashboard/controller/server.go | 3 +- cmd/dashboard/controller/server_group.go | 7 +- cmd/dashboard/controller/service.go | 14 +- cmd/dashboard/controller/setting.go | 3 +- cmd/dashboard/controller/terminal.go | 3 +- cmd/dashboard/controller/user.go | 6 +- go.mod | 1 + go.sum | 2 + model/config.go | 2 +- pkg/i18n/i18n.go | 105 ++++++++ pkg/i18n/template.pot | 220 +++++++++++++++++ .../translations/en_US/LC_MESSAGES/nezha.mo | Bin 0 -> 4074 bytes .../translations/en_US/LC_MESSAGES/nezha.po | 225 ++++++++++++++++++ .../translations/zh_CN/LC_MESSAGES/nezha.mo | Bin 0 -> 3944 bytes .../translations/zh_CN/LC_MESSAGES/nezha.po | 222 +++++++++++++++++ service/rpc/io_stream.go | 8 +- service/rpc/nezha.go | 6 +- service/singleton/alertsentinel.go | 4 +- service/singleton/crontask.go | 26 +- service/singleton/i18n.go | 80 +++++++ service/singleton/servicesentinel.go | 26 +- service/singleton/singleton.go | 1 + 30 files changed, 930 insertions(+), 91 deletions(-) create mode 100644 pkg/i18n/i18n.go create mode 100644 pkg/i18n/template.pot create mode 100644 pkg/i18n/translations/en_US/LC_MESSAGES/nezha.mo create mode 100644 pkg/i18n/translations/en_US/LC_MESSAGES/nezha.po create mode 100644 pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.mo create mode 100644 pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.po create mode 100644 service/singleton/i18n.go diff --git a/cmd/dashboard/controller/alertrule.go b/cmd/dashboard/controller/alertrule.go index 953cdf4..ba8a218 100644 --- a/cmd/dashboard/controller/alertrule.go +++ b/cmd/dashboard/controller/alertrule.go @@ -1,8 +1,6 @@ package controller import ( - "errors" - "fmt" "strconv" "time" @@ -99,7 +97,7 @@ func updateAlertRule(c *gin.Context) (any, error) { var r model.AlertRule if err := singleton.DB.First(&r, id).Error; err != nil { - return nil, fmt.Errorf("alert id %d does not exist", id) + return nil, singleton.Localizer.ErrorT("alert id %d does not exist", id) } if err := validateRule(&r); err != nil { @@ -154,22 +152,22 @@ func validateRule(r *model.AlertRule) error { for _, rule := range r.Rules { if !rule.IsTransferDurationRule() { if rule.Duration < 3 { - return errors.New("错误: Duration 至少为 3") + return singleton.Localizer.ErrorT("duration need to be at least 3") } } else { if rule.CycleInterval < 1 { - return errors.New("错误: cycle_interval 至少为 1") + return singleton.Localizer.ErrorT("cycle_interval need to be at least 1") } if rule.CycleStart == nil { - return errors.New("错误: cycle_start 未设置") + return singleton.Localizer.ErrorT("cycle_start is not set") } if rule.CycleStart.After(time.Now()) { - return errors.New("错误: cycle_start 是个未来值") + return singleton.Localizer.ErrorT("cycle_start is a future value") } } } } else { - return errors.New("至少定义一条规则") + return singleton.Localizer.ErrorT("need to configure at least a single rule") } return nil } diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index ed745a2..3ff62b3 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -185,7 +185,7 @@ func commonHandler[T any](handler handlerFunc[T]) func(*gin.Context) { switch err.(type) { case *gormError: log.Printf("NEZHA>> gorm error: %v", err) - c.JSON(http.StatusOK, newErrorResponse(errors.New("database error"))) + c.JSON(http.StatusOK, newErrorResponse(singleton.Localizer.ErrorT("database error"))) return case *wsError: // Connection is upgraded to WebSocket, so c.Writer is no longer usable diff --git a/cmd/dashboard/controller/cron.go b/cmd/dashboard/controller/cron.go index d67986f..71be0a1 100644 --- a/cmd/dashboard/controller/cron.go +++ b/cmd/dashboard/controller/cron.go @@ -1,7 +1,6 @@ package controller import ( - "errors" "fmt" "strconv" @@ -61,7 +60,7 @@ func createCron(c *gin.Context) (uint64, error) { cr.Cover = cf.Cover if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger { - return 0, errors.New("计划任务类型不得使用触发服务器执行方式") + return 0, singleton.Localizer.ErrorT("scheduled tasks cannot be triggered by alarms") } // 对于计划任务类型,需要更新CronJob @@ -120,7 +119,7 @@ func updateCron(c *gin.Context) (any, error) { cr.Cover = cf.Cover if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger { - return nil, errors.New("计划任务类型不得使用触发服务器执行方式") + return nil, singleton.Localizer.ErrorT("scheduled tasks cannot be triggered by alarms") } // 对于计划任务类型,需要更新CronJob @@ -159,7 +158,7 @@ func manualTriggerCron(c *gin.Context) (any, error) { var cr model.Cron if err := singleton.DB.First(&cr, id).Error; err != nil { - return nil, fmt.Errorf("task id %d does not exist", id) + return nil, singleton.Localizer.ErrorT("task id %d does not exist", id) } singleton.ManualTrigger(&cr) diff --git a/cmd/dashboard/controller/ddns.go b/cmd/dashboard/controller/ddns.go index 35c7d8d..7c5fb79 100644 --- a/cmd/dashboard/controller/ddns.go +++ b/cmd/dashboard/controller/ddns.go @@ -1,8 +1,6 @@ package controller import ( - "errors" - "fmt" "strconv" "github.com/gin-gonic/gin" @@ -55,7 +53,7 @@ func createDDNS(c *gin.Context) (uint64, error) { } if df.MaxRetries < 1 || df.MaxRetries > 10 { - return 0, errors.New("重试次数必须为大于 1 且不超过 10 的整数") + return 0, singleton.Localizer.ErrorT("the retry count must be an integer between 1 and 10") } p.Name = df.Name @@ -78,7 +76,7 @@ func createDDNS(c *gin.Context) (uint64, error) { // IDN to ASCII domainValid, domainErr := idna.Lookup.ToASCII(domain) if domainErr != nil { - return 0, fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr) + return 0, singleton.Localizer.ErrorT("error parsing %s: %v", domain, domainErr) } p.Domains[n] = domainValid } @@ -119,12 +117,12 @@ func updateDDNS(c *gin.Context) (any, error) { } if df.MaxRetries < 1 || df.MaxRetries > 10 { - return nil, errors.New("重试次数必须为大于 1 且不超过 10 的整数") + return nil, singleton.Localizer.ErrorT("the retry count must be an integer between 1 and 10") } var p model.DDNSProfile if err = singleton.DB.First(&p, id).Error; err != nil { - return nil, fmt.Errorf("profile id %d does not exist", id) + return nil, singleton.Localizer.ErrorT("profile id %d does not exist", id) } p.Name = df.Name @@ -147,7 +145,7 @@ func updateDDNS(c *gin.Context) (any, error) { // IDN to ASCII domainValid, domainErr := idna.Lookup.ToASCII(domain) if domainErr != nil { - return nil, fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr) + return nil, singleton.Localizer.ErrorT("error parsing %s: %v", domain, domainErr) } p.Domains[n] = domainValid } diff --git a/cmd/dashboard/controller/fm.go b/cmd/dashboard/controller/fm.go index d593042..d03a41e 100644 --- a/cmd/dashboard/controller/fm.go +++ b/cmd/dashboard/controller/fm.go @@ -1,7 +1,6 @@ package controller import ( - "errors" "strconv" "time" @@ -26,7 +25,7 @@ import ( // @Success 200 {object} model.CreateFMResponse // @Router /file [get] func createFM(c *gin.Context) (*model.CreateFMResponse, error) { - idStr := c.Param("id") + idStr := c.Query("id") id, err := strconv.ParseUint(idStr, 10, 64) if err != nil { return nil, err @@ -43,7 +42,7 @@ func createFM(c *gin.Context) (*model.CreateFMResponse, error) { server := singleton.ServerList[id] singleton.ServerLock.RUnlock() if server == nil || server.TaskStream == nil { - return nil, errors.New("server not found or not connected") + return nil, singleton.Localizer.ErrorT("server not found or not connected") } fmData, _ := utils.Json.Marshal(&model.TaskFM{ diff --git a/cmd/dashboard/controller/nat.go b/cmd/dashboard/controller/nat.go index ed6f23f..95fec72 100644 --- a/cmd/dashboard/controller/nat.go +++ b/cmd/dashboard/controller/nat.go @@ -1,7 +1,6 @@ package controller import ( - "fmt" "strconv" "github.com/gin-gonic/gin" @@ -93,7 +92,7 @@ func updateNAT(c *gin.Context) (any, error) { var n model.NAT if err = singleton.DB.First(&n, id).Error; err != nil { - return nil, fmt.Errorf("profile id %d does not exist", id) + return nil, singleton.Localizer.ErrorT("profile id %d does not exist", id) } n.Name = nf.Name diff --git a/cmd/dashboard/controller/notification.go b/cmd/dashboard/controller/notification.go index a46c019..3360021 100644 --- a/cmd/dashboard/controller/notification.go +++ b/cmd/dashboard/controller/notification.go @@ -1,7 +1,6 @@ package controller import ( - "fmt" "strconv" "github.com/gin-gonic/gin" @@ -65,7 +64,7 @@ func createNotification(c *gin.Context) (uint64, error) { } // 未勾选跳过检查 if !nf.SkipCheck { - if err := ns.Send("这是测试消息"); err != nil { + if err := ns.Send(singleton.Localizer.T("a test message")); err != nil { return 0, err } } @@ -104,7 +103,7 @@ func updateNotification(c *gin.Context) (any, error) { var n model.Notification if err := singleton.DB.First(&n, id).Error; err != nil { - return nil, fmt.Errorf("notification id %d does not exist", id) + return nil, singleton.Localizer.ErrorT("notification id %d does not exist", id) } n.Name = nf.Name @@ -123,7 +122,7 @@ func updateNotification(c *gin.Context) (any, error) { } // 未勾选跳过检查 if !nf.SkipCheck { - if err := ns.Send("这是测试消息"); err != nil { + if err := ns.Send(singleton.Localizer.T("a test message")); err != nil { return nil, err } } diff --git a/cmd/dashboard/controller/notification_group.go b/cmd/dashboard/controller/notification_group.go index d8bdf88..f2e20a2 100644 --- a/cmd/dashboard/controller/notification_group.go +++ b/cmd/dashboard/controller/notification_group.go @@ -1,7 +1,6 @@ package controller import ( - "fmt" "slices" "strconv" @@ -78,7 +77,7 @@ func createNotificationGroup(c *gin.Context) (uint64, error) { } if count != int64(len(ngf.Notifications)) { - return 0, fmt.Errorf("have invalid notification id") + return 0, singleton.Localizer.ErrorT("have invalid notification id") } err := singleton.DB.Transaction(func(tx *gorm.DB) error { @@ -129,7 +128,7 @@ func updateNotificationGroup(c *gin.Context) (any, error) { } var ngDB model.NotificationGroup if err := singleton.DB.First(&ngDB, id).Error; err != nil { - return nil, fmt.Errorf("group id %d does not exist", id) + return nil, singleton.Localizer.ErrorT("group id %d does not exist", id) } ngDB.Name = ngf.Name @@ -140,7 +139,7 @@ func updateNotificationGroup(c *gin.Context) (any, error) { return nil, newGormError("%v", err) } if count != int64(len(ngf.Notifications)) { - return nil, fmt.Errorf("have invalid notification id") + return nil, singleton.Localizer.ErrorT("have invalid notification id") } err = singleton.DB.Transaction(func(tx *gorm.DB) error { diff --git a/cmd/dashboard/controller/server.go b/cmd/dashboard/controller/server.go index 3b195aa..03f1712 100644 --- a/cmd/dashboard/controller/server.go +++ b/cmd/dashboard/controller/server.go @@ -1,7 +1,6 @@ package controller import ( - "fmt" "strconv" "github.com/gin-gonic/gin" @@ -57,7 +56,7 @@ func updateServer(c *gin.Context) (any, error) { var s model.Server if err := singleton.DB.First(&s, id).Error; err != nil { - return nil, fmt.Errorf("server id %d does not exist", id) + return nil, singleton.Localizer.ErrorT("server id %d does not exist", id) } s.Name = sf.Name diff --git a/cmd/dashboard/controller/server_group.go b/cmd/dashboard/controller/server_group.go index 301c0b7..2503761 100644 --- a/cmd/dashboard/controller/server_group.go +++ b/cmd/dashboard/controller/server_group.go @@ -1,7 +1,6 @@ package controller import ( - "fmt" "slices" "strconv" @@ -76,7 +75,7 @@ func createServerGroup(c *gin.Context) (uint64, error) { return 0, newGormError("%v", err) } if count != int64(len(sgf.Servers)) { - return 0, fmt.Errorf("have invalid server id") + return 0, singleton.Localizer.ErrorT("have invalid server id") } err := singleton.DB.Transaction(func(tx *gorm.DB) error { @@ -128,7 +127,7 @@ func updateServerGroup(c *gin.Context) (any, error) { var sgDB model.ServerGroup if err := singleton.DB.First(&sgDB, id).Error; err != nil { - return nil, fmt.Errorf("group id %d does not exist", id) + return nil, singleton.Localizer.ErrorT("group id %d does not exist", id) } sgDB.Name = sg.Name @@ -137,7 +136,7 @@ func updateServerGroup(c *gin.Context) (any, error) { return nil, err } if count != int64(len(sg.Servers)) { - return nil, fmt.Errorf("have invalid server id") + return nil, singleton.Localizer.ErrorT("have invalid server id") } err = singleton.DB.Transaction(func(tx *gorm.DB) error { diff --git a/cmd/dashboard/controller/service.go b/cmd/dashboard/controller/service.go index 2d61abf..7c0d018 100644 --- a/cmd/dashboard/controller/service.go +++ b/cmd/dashboard/controller/service.go @@ -1,8 +1,6 @@ package controller import ( - "errors" - "fmt" "strconv" "strings" "time" @@ -78,14 +76,14 @@ func listServiceHistory(c *gin.Context) ([]*model.ServiceInfos, error) { singleton.ServerLock.RLock() server, ok := singleton.ServerList[id] if !ok { - return nil, errors.New("server not found") + return nil, singleton.Localizer.ErrorT("server not found") } _, isMember := c.Get(model.CtxKeyAuthorizedUser) authorized := isMember // TODO || isViewPasswordVerfied if server.HideForGuest && !authorized { - return nil, errors.New("unauthorized") + return nil, singleton.Localizer.ErrorT("unauthorized") } singleton.ServerLock.RUnlock() @@ -154,7 +152,7 @@ func listServerWithServices(c *gin.Context) ([]uint64, error) { server, ok := singleton.ServerList[id] if !ok { singleton.ServerLock.RUnlock() - return nil, errors.New("server not found") + return nil, singleton.Localizer.ErrorT("server not found") } if !server.HideForGuest || authorized { @@ -201,7 +199,7 @@ func createService(c *gin.Context) (uint64, error) { m.FailTriggerTasks = mf.FailTriggerTasks if err := singleton.DB.Create(&m).Error; err != nil { - return 0, err + return 0, newGormError("%v", err) } var skipServers []uint64 @@ -246,7 +244,7 @@ func updateService(c *gin.Context) (any, error) { } var m model.Service if err := singleton.DB.First(&m, id).Error; err != nil { - return nil, fmt.Errorf("service id %d does not exist", id) + return nil, singleton.Localizer.ErrorT("service id %d does not exist", id) } m.Name = mf.Name m.Target = strings.TrimSpace(mf.Target) @@ -265,7 +263,7 @@ func updateService(c *gin.Context) (any, error) { m.FailTriggerTasks = mf.FailTriggerTasks if err := singleton.DB.Save(&m).Error; err != nil { - return nil, err + return nil, newGormError("%v", err) } var skipServers []uint64 diff --git a/cmd/dashboard/controller/setting.go b/cmd/dashboard/controller/setting.go index dc2a56f..ee0e1be 100644 --- a/cmd/dashboard/controller/setting.go +++ b/cmd/dashboard/controller/setting.go @@ -63,9 +63,10 @@ func updateConfig(c *gin.Context) (any, error) { singleton.Conf.CustomCodeDashboard = sf.CustomCodeDashboard if err := singleton.Conf.Save(); err != nil { - return nil, err + return nil, newGormError("%v", err) } singleton.OnNameserverUpdate() + singleton.OnUpdateLang(singleton.Conf.Language) return nil, nil } diff --git a/cmd/dashboard/controller/terminal.go b/cmd/dashboard/controller/terminal.go index 1eb54c3..d1c577e 100644 --- a/cmd/dashboard/controller/terminal.go +++ b/cmd/dashboard/controller/terminal.go @@ -1,7 +1,6 @@ package controller import ( - "errors" "time" "github.com/gin-gonic/gin" @@ -41,7 +40,7 @@ func createTerminal(c *gin.Context) (*model.CreateTerminalResponse, error) { server := singleton.ServerList[createTerminalReq.ServerID] singleton.ServerLock.RUnlock() if server == nil || server.TaskStream == nil { - return nil, errors.New("server not found or not connected") + return nil, singleton.Localizer.ErrorT("server not found or not connected") } terminalData, _ := utils.Json.Marshal(&model.TerminalTask{ diff --git a/cmd/dashboard/controller/user.go b/cmd/dashboard/controller/user.go index 0dbf29d..f3c6718 100644 --- a/cmd/dashboard/controller/user.go +++ b/cmd/dashboard/controller/user.go @@ -1,8 +1,6 @@ package controller import ( - "errors" - "github.com/gin-gonic/gin" "github.com/naiba/nezha/model" "github.com/naiba/nezha/service/singleton" @@ -44,10 +42,10 @@ func createUser(c *gin.Context) (uint64, error) { } if len(uf.Password) < 6 { - return 0, errors.New("password length must be greater than 6") + return 0, singleton.Localizer.ErrorT("password length must be greater than 6") } if uf.Username == "" { - return 0, errors.New("username can't be empty") + return 0, singleton.Localizer.ErrorT("username can't be empty") } var u model.User diff --git a/go.mod b/go.mod index ff01d36..b3bb783 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.23.1 require ( github.com/appleboy/gin-jwt/v2 v2.10.0 + github.com/chai2010/gettext-go v1.0.3 github.com/gin-contrib/pprof v1.4.0 github.com/gin-gonic/gin v1.10.0 github.com/gorilla/websocket v1.5.1 diff --git a/go.sum b/go.sum index 5466d56..db2fb82 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKz github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= +github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= diff --git a/model/config.go b/model/config.go index c59d4c2..a5fadd1 100644 --- a/model/config.go +++ b/model/config.go @@ -18,7 +18,7 @@ const ( type Config struct { Debug bool `mapstructure:"debug" json:"debug,omitempty"` // debug模式开关 - Language string `mapstructure:"language" json:"language,omitempty"` // 系统语言,默认 zh-CN + Language string `mapstructure:"language" json:"language,omitempty"` // 系统语言,默认 zh_CN SiteName string `mapstructure:"site_name" json:"site_name,omitempty"` JWTSecretKey string `mapstructure:"jwt_secret_key" json:"jwt_secret_key,omitempty"` AgentSecretKey string `mapstructure:"agent_secret_key" json:"agent_secret_key,omitempty"` diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go new file mode 100644 index 0000000..91cbca1 --- /dev/null +++ b/pkg/i18n/i18n.go @@ -0,0 +1,105 @@ +package i18n + +import ( + "embed" + "fmt" + "sync" + + "github.com/chai2010/gettext-go" +) + +//go:embed translations +var Translations embed.FS + +var Languages = map[string]string{ + "zh_CN": "简体中文", + "zh_TW": "繁體中文", + "en_US": "English", + "es_ES": "Español", +} + +type Localizer struct { + intlMap map[string]gettext.Gettexter + lang string + + mu sync.RWMutex +} + +func NewLocalizer(lang, domain, path string, data any) *Localizer { + intl := gettext.New(domain, path, data) + intl.SetLanguage(lang) + + intlMap := make(map[string]gettext.Gettexter) + intlMap[lang] = intl + + return &Localizer{intlMap: intlMap, lang: lang} +} + +func (l *Localizer) SetLanguage(lang string) { + l.mu.Lock() + defer l.mu.Unlock() + + l.lang = lang +} + +func (l *Localizer) Exists(lang string) bool { + l.mu.RLock() + defer l.mu.RUnlock() + + if _, ok := l.intlMap[lang]; ok { + return ok + } + return false +} + +func (l *Localizer) AppendIntl(lang, domain, path string, data any) { + intl := gettext.New(domain, path, data) + intl.SetLanguage(lang) + + l.mu.Lock() + defer l.mu.Unlock() + + l.intlMap[lang] = intl +} + +// Modified from k8s.io/kubectl/pkg/util/i18n + +func (l *Localizer) T(orig string) string { + l.mu.RLock() + intl, ok := l.intlMap[l.lang] + l.mu.RUnlock() + if !ok { + return orig + } + + return intl.PGettext("", orig) +} + +// N translates a string, possibly substituting arguments into it along +// the way. If len(args) is > 0, args1 is assumed to be the plural value +// and plural translation is used. +func (l *Localizer) N(orig string, args ...int) string { + l.mu.RLock() + intl, ok := l.intlMap[l.lang] + l.mu.RUnlock() + if !ok { + return orig + } + + if len(args) == 0 { + return intl.PGettext("", orig) + } + return fmt.Sprintf(intl.PNGettext("", orig, orig+".plural", args[0]), + args[0]) +} + +// ErrorT produces an error with a translated error string. +// Substitution is performed via the `T` function above, following +// the same rules. +func (l *Localizer) ErrorT(defaultValue string, args ...any) error { + return fmt.Errorf(l.T(defaultValue), args...) +} + +func (l *Localizer) Tf(defaultValue string, args ...any) string { + return fmt.Sprintf(l.T(defaultValue), args...) +} diff --git a/pkg/i18n/template.pot b/pkg/i18n/template.pot new file mode 100644 index 0000000..f3d6764 --- /dev/null +++ b/pkg/i18n/template.pot @@ -0,0 +1,220 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-11-01 05:06+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: cmd/dashboard/controller/alertrule.go:100 +#, c-format +msgid "alert id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:155 +msgid "duration need to be at least 3" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:159 +msgid "cycle_interval need to be at least 1" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:162 +msgid "cycle_start is not set" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:165 +msgid "cycle_start is a future value" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:170 +msgid "need to configure at least a single rule" +msgstr "" + +#: cmd/dashboard/controller/controller.go:188 +msgid "database error" +msgstr "" + +#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122 +msgid "scheduled tasks cannot be triggered by alarms" +msgstr "" + +#: cmd/dashboard/controller/cron.go:161 +#, c-format +msgid "task id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120 +msgid "the retry count must be an integer between 1 and 10" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148 +msgid "error parsing %s: %v" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95 +#, c-format +msgid "profile id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43 +msgid "server not found or not connected" +msgstr "" + +#: cmd/dashboard/controller/notification.go:67 +#: cmd/dashboard/controller/notification.go:125 +msgid "a test message" +msgstr "" + +#: cmd/dashboard/controller/notification.go:106 +#, c-format +msgid "notification id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/notification_group.go:80 +#: cmd/dashboard/controller/notification_group.go:142 +msgid "have invalid notification id" +msgstr "" + +#: cmd/dashboard/controller/notification_group.go:131 +#: cmd/dashboard/controller/server_group.go:130 +#, c-format +msgid "group id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/server.go:59 +#, c-format +msgid "server id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/server_group.go:78 +#: cmd/dashboard/controller/server_group.go:139 +msgid "have invalid server id" +msgstr "" + +#: cmd/dashboard/controller/service.go:79 +#: cmd/dashboard/controller/service.go:155 +msgid "server not found" +msgstr "" + +#: cmd/dashboard/controller/service.go:86 +msgid "unauthorized" +msgstr "" + +#: cmd/dashboard/controller/service.go:247 +#, c-format +msgid "service id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/user.go:45 +msgid "password length must be greater than 6" +msgstr "" + +#: cmd/dashboard/controller/user.go:48 +msgid "username can't be empty" +msgstr "" + +#: service/rpc/io_stream.go:122 +msgid "timeout: no connection established" +msgstr "" + +#: service/rpc/io_stream.go:125 +msgid "timeout: user connection not established" +msgstr "" + +#: service/rpc/io_stream.go:128 +msgid "timeout: agent connection not established" +msgstr "" + +#: service/rpc/nezha.go:57 +msgid "Scheduled Task Executed Successfully" +msgstr "" + +#: service/rpc/nezha.go:61 +msgid "Scheduled Task Executed Failed" +msgstr "" + +#: service/rpc/nezha.go:156 +msgid "IP Changed" +msgstr "" + +#: service/singleton/alertsentinel.go:159 +msgid "Incident" +msgstr "" + +#: service/singleton/alertsentinel.go:169 +msgid "Resolved" +msgstr "" + +#: service/singleton/crontask.go:52 +msgid "Tasks failed to register: [" +msgstr "" + +#: service/singleton/crontask.go:59 +msgid "" +"] These tasks will not execute properly. Fix them in the admin dashboard." +msgstr "" + +#: service/singleton/crontask.go:150 service/singleton/crontask.go:175 +#, c-format +msgid "[Task failed] %s: server %s is offline and cannot execute the task" +msgstr "" + +#: service/singleton/servicesentinel.go:439 +#, c-format +msgid "[Latency] %s %2f > %2f, Reporter: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:446 +#, c-format +msgid "[Latency] %s %2f < %2f, Reporter: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:472 +#, c-format +msgid "[%s] %s Reporter: %s, Error: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:515 +#, c-format +msgid "[SSL] Fetch cert info failed, %s %s" +msgstr "" + +#: service/singleton/servicesentinel.go:555 +#, c-format +msgid "The SSL certificate will expire within seven days. Expiration time: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:568 +#, c-format +msgid "" +"SSL certificate changed, old: issuer %s, expires at %s; new: issuer %s, " +"expires at %s" +msgstr "" + +#: service/singleton/servicesentinel.go:604 +msgid "No Data" +msgstr "" + +#: service/singleton/servicesentinel.go:606 +msgid "Good" +msgstr "" + +#: service/singleton/servicesentinel.go:608 +msgid "Low Availability" +msgstr "" + +#: service/singleton/servicesentinel.go:610 +msgid "Down" +msgstr "" diff --git a/pkg/i18n/translations/en_US/LC_MESSAGES/nezha.mo b/pkg/i18n/translations/en_US/LC_MESSAGES/nezha.mo new file mode 100644 index 0000000000000000000000000000000000000000..109907810e07778455979d642a46540680e298d5 GIT binary patch literal 4074 zcmeH|O^g&p6vt~3QO6HH{1%j_xB{|0%z$e)gO~{H;FtwA?5@EOf;HVWGX*_WV^#Ic zju($&;*|swFB+o>H!q&NNMbw~4|p&hj3*N>#OTGCV50xmGc3b89KCHPHNWYuSM}ce z*Q=_Zx2(S?@D$Oup}jvL#6j@yb@;*a`yE282mcuIPq2XZ>)_qs26S!$H-dM81oHP? z;1XB{p90?np9Mb!w}Rh-Rq$7EJ6ODP(0_bL5AMMCOW+~!ORxt113m!OHVE+qcnais zJ@_Pe348?n0%W`IK=%I=_z-v<pY48I09C#UIyQ^RX{swaV z+mIyZJp;0R4P?9X;N##0a6kAlxEuTdd@5AdH_$c@W_!syV2uZ{_h|IJg z>mBfeDa1P<`@H}n6!F3E`$r(_eHr9@#k~Vvwu2n+5wHrr39{aw4g0?t@*2o`Z$?n8 z_cT}omqFI?3dp)$1zGQFAnU#P{(*jbLAI-c?0*Sly#vVO-UC^mPe9iDD{vS1Bgpzq zU@+^w17y3`zzA%D9REd-^Zp!U`zs*Z{Q|Py6B`G5Zv}Vb{RGH*_d)jaA&8L0*C6Zt zJvaeE?%a-ZK}qL10AeQ^Oma>5oqbKAaf}E#e~S2JE%WAavHRn9}*?`6|vkv^Qj-1Qrvw7r&>w#-z9U5!=JQ{2FAlf9_!)W4& z?HO^@+C2OP$rovwn>qMC#=4_8%=}Mi-x=wZIi{qA#$RNc@B7Ps#mWWU z(6J1P;=vSCWYeTV+WSm7n)FkomOI)hPcq>302$T0eYaSMTPn#?mC&N}=V^9X#aX~x z6``=x+jK8vajd*=WNF$LJknDmA3?#AQ%&uIauqr)7F&u|N8ZzEI*gKfI<&MQue!=m zBKy9G7!bb<+8PSFtt!YuoSyV&Ksu>9*5y-krnAmjc;WszNMhoCZZx^CK`#-0r*5(T zuhGlnj1cV^s;UsTa=A&Hh8-q1#UZe_T&qFC?6HFQT{o8>3qiKgNVQQUO+v9W#s*}Y z>q-G~1jr@M5R~zQ7Y`Mn$f#Y%*^Y8)zerWR%%j?fBOfi3HvT2jx9V29q$nf>o?BDevundM>?-fGna3&E1J$`o$+zEWLF`5 zM1Zv5aqzf=)+Lb0G((Hp z8H9ppO{Y+F4yz&!y(q9`4eXimDfpFr#H;Ublm%u9PB)v%VJYf;lBslUFILhR6V0%X zm2cS0BsYv#K9F_!nj>?gV(jJILdUm>jN=&}GuNt9!SyMIBUIQhZ)D3ggrK29=qYU6 za$f7@lHd(%vrr+FB8CiO;HBdDP^Yqlos}db(e{s67cJZuSnQIr%+QsA6E*_l3YZp%iH`3^x^8Z~t F{{p+;ZR`L5 literal 0 HcmV?d00001 diff --git a/pkg/i18n/translations/en_US/LC_MESSAGES/nezha.po b/pkg/i18n/translations/en_US/LC_MESSAGES/nezha.po new file mode 100644 index 0000000..c4bd8c9 --- /dev/null +++ b/pkg/i18n/translations/en_US/LC_MESSAGES/nezha.po @@ -0,0 +1,225 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-11-01 04:58+0800\n" +"PO-Revision-Date: 2024-11-01 05:05+0800\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.5\n" + +#: cmd/dashboard/controller/alertrule.go:100 +#, c-format +msgid "alert id %d does not exist" +msgstr "alert id %d does not exist" + +#: cmd/dashboard/controller/alertrule.go:155 +msgid "duration need to be at least 3" +msgstr "duration need to be at least 3" + +#: cmd/dashboard/controller/alertrule.go:159 +msgid "cycle_interval need to be at least 1" +msgstr "cycle_interval need to be at least 1" + +#: cmd/dashboard/controller/alertrule.go:162 +msgid "cycle_start is not set" +msgstr "cycle_start is not set" + +#: cmd/dashboard/controller/alertrule.go:165 +msgid "cycle_start is a future value" +msgstr "cycle_start is a future value" + +#: cmd/dashboard/controller/alertrule.go:170 +msgid "need to configure at least a single rule" +msgstr "need to configure at least a single rule" + +#: cmd/dashboard/controller/controller.go:188 +msgid "database error" +msgstr "database error" + +#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122 +msgid "scheduled tasks cannot be triggered by alarms" +msgstr "scheduled tasks cannot be triggered by alarms" + +#: cmd/dashboard/controller/cron.go:161 +#, c-format +msgid "task id %d does not exist" +msgstr "task id %d does not exist" + +#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120 +msgid "the retry count must be an integer between 1 and 10" +msgstr "the retry count must be an integer between 1 and 10" + +#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148 +msgid "error parsing %s: %v" +msgstr "error parsing %s: %v" + +#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95 +#, c-format +msgid "profile id %d does not exist" +msgstr "profile id %d does not exist" + +#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43 +msgid "server not found or not connected" +msgstr "server not found or not connected" + +#: cmd/dashboard/controller/notification.go:67 +#: cmd/dashboard/controller/notification.go:125 +msgid "a test message" +msgstr "a test message" + +#: cmd/dashboard/controller/notification.go:106 +#, c-format +msgid "notification id %d does not exist" +msgstr "notification id %d does not exist" + +#: cmd/dashboard/controller/notification_group.go:80 +#: cmd/dashboard/controller/notification_group.go:142 +msgid "have invalid notification id" +msgstr "have invalid notification id" + +#: cmd/dashboard/controller/notification_group.go:131 +#: cmd/dashboard/controller/server_group.go:130 +#, c-format +msgid "group id %d does not exist" +msgstr "group id %d does not exist" + +#: cmd/dashboard/controller/server.go:59 +#, c-format +msgid "server id %d does not exist" +msgstr "server id %d does not exist" + +#: cmd/dashboard/controller/server_group.go:78 +#: cmd/dashboard/controller/server_group.go:139 +msgid "have invalid server id" +msgstr "have invalid server id" + +#: cmd/dashboard/controller/service.go:79 +#: cmd/dashboard/controller/service.go:155 +msgid "server not found" +msgstr "server not found" + +#: cmd/dashboard/controller/service.go:86 +msgid "unauthorized" +msgstr "unauthorized" + +#: cmd/dashboard/controller/service.go:247 +#, c-format +msgid "service id %d does not exist" +msgstr "service id %d does not exist" + +#: cmd/dashboard/controller/user.go:45 +msgid "password length must be greater than 6" +msgstr "password length must be greater than 6" + +#: cmd/dashboard/controller/user.go:48 +msgid "username can't be empty" +msgstr "username can't be empty" + +#: service/rpc/io_stream.go:122 +msgid "timeout: no connection established" +msgstr "timeout: no connection established" + +#: service/rpc/io_stream.go:125 +msgid "timeout: user connection not established" +msgstr "timeout: user connection not established" + +#: service/rpc/io_stream.go:128 +msgid "timeout: agent connection not established" +msgstr "timeout: agent connection not established" + +#: service/rpc/nezha.go:57 +msgid "Scheduled Task Executed Successfully" +msgstr "Scheduled Task Executed Successfully" + +#: service/rpc/nezha.go:61 +msgid "Scheduled Task Executed Failed" +msgstr "Scheduled Task Executed Failed" + +#: service/rpc/nezha.go:156 +msgid "IP Changed" +msgstr "IP Changed" + +#: service/singleton/alertsentinel.go:159 +msgid "Incident" +msgstr "Incident" + +#: service/singleton/alertsentinel.go:169 +msgid "Resolved" +msgstr "Resolved" + +#: service/singleton/crontask.go:52 +msgid "Tasks failed to register: [" +msgstr "Tasks failed to register: [" + +#: service/singleton/crontask.go:59 +msgid "" +"] These tasks will not execute properly. Fix them in the admin dashboard." +msgstr "" +"] These tasks will not execute properly. Fix them in the admin dashboard." + +#: service/singleton/crontask.go:150 service/singleton/crontask.go:175 +#, c-format +msgid "[Task failed] %s: server %s is offline and cannot execute the task" +msgstr "[Task failed] %s: server %s is offline and cannot execute the task" + +#: service/singleton/servicesentinel.go:439 +#, c-format +msgid "[Latency] %s %2f > %2f, Reporter: %s" +msgstr "[Latency] %s %2f > %2f, Reporter: %s" + +#: service/singleton/servicesentinel.go:446 +#, c-format +msgid "[Latency] %s %2f < %2f, Reporter: %s" +msgstr "[Latency] %s %2f < %2f, Reporter: %s" + +#: service/singleton/servicesentinel.go:472 +#, c-format +msgid "[%s] %s Reporter: %s, Error: %s" +msgstr "[%s] %s Reporter: %s, Error: %s" + +#: service/singleton/servicesentinel.go:515 +#, c-format +msgid "[SSL] Fetch cert info failed, %s %s" +msgstr "[SSL] Fetch cert info failed, %s %s" + +#: service/singleton/servicesentinel.go:555 +#, c-format +msgid "The SSL certificate will expire within seven days. Expiration time: %s" +msgstr "The SSL certificate will expire within seven days. Expiration time: %s" + +#: service/singleton/servicesentinel.go:568 +#, c-format +#| msgid "SSL certificate changed, old: %s, %s expired; new: %s, %s expired." +msgid "" +"SSL certificate changed, old: issuer %s, expires at %s; new: issuer %s, " +"expires at %s" +msgstr "" +"SSL certificate changed, old: issuer %s, expires at %s; new: issuer %s, " +"expires at %s" + +#: service/singleton/servicesentinel.go:604 +msgid "No Data" +msgstr "No Data" + +#: service/singleton/servicesentinel.go:606 +msgid "Good" +msgstr "Good" + +#: service/singleton/servicesentinel.go:608 +msgid "Low Availability" +msgstr "Low Availability" + +#: service/singleton/servicesentinel.go:610 +msgid "Down" +msgstr "Down" diff --git a/pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.mo b/pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.mo new file mode 100644 index 0000000000000000000000000000000000000000..87be06f7b4ee243ac8da5c725bcff693aef8f077 GIT binary patch literal 3944 zcmai#TWlOx8OIN_lwxinlv^qElwz7RUb9X}YHeF2n#8RoCyf%fDvF}$c<1cywBwoW z%&aecveVevTx=)BjxTZQ*op15CALw5>sz|d2zcOyCxnpLnVo&1^aUXW3I5;A#A~lN zVx-yMo^!r)&i}idIe*>$@DRgOi}wk<7e2?>i{O7gj~_h$e2B4!!Ouee3#>x>9{4Eu z2nM%-UkASka*+CWfUkjd;19tw;E%x>@cZEJ!3MAZJ_*)-As9a#(gL4G{|NXJxCS9{Sl=7{{lV+-UDggHi#vEcY$R83b+G22|fj8 z!JmL1f@Jq6FarJqB>hjoN%FfMB>P5??A`*u2WG)N;1swM{4@9?kbNoG=V|aOXf}c0 z0e=bp8+;ptMD`|vOne)}RMru;yFr++Y{>V*{&7%2|8?-Y;Gss*_{tL1wM!NGD!P=29myQ z5Jl~sAjRz!kn|n}QR;pl<%MboCGFP?z@EVi6Tb$jO&V+PQcXaj|J34_YL6a-&)<*s zBE3ih+l80oB|k_zO4qM-iqB5GXj)PxUG)DPHc>O$4o8rEM{%m-@vEe^T)2|ge zx)dAAGu0}^pK?onvjaxE&R#YQg*6`G`%^+sO8nL1sv>opH5u*v7i~h-M2o7ab|*V* z@B_jY?5MO1tqrEfjy3VPG;KAZ#)U0;JXoTJ8=4a1s%2%Q$*ZjzE<4hyDJ?E+G@s|X zY=7`DI~Gq#C8J4&Hw)`6ey~HvGd5ZckV2b2VE9-j9+#Gt$Y@$8qnQ>@_%pa|a8o8# z%a&%0A7{-e$;*|utC|+7X#Vbwk2(* zpi)}|+>w+KrewnWRoY??^Nn~~n3k$1{qR+{v7~8a(v@|mM4N>BPyn~EWpDzhK2(Km z8Vu8b?)y8(4LzYI$$@nr1*d&A$xYNfD_gHZ*wVtX+6_}di1ehL;;k733PDSn5=Cbs zD?;b{7*dwNnW^l8wSJB$`_}!kz$&(>CX>=cDq1?Z(1h7)v2`~ps0qg?@(ClOEBB4p zJGf!`Ifv)EjN>f(7Ha$f%1BRT%~Y$Vw9QT)$402I&~JE4H3ZR!g5550aO?b9uZuD| zutvs?ag-vagc>MSqz`o}S~S(dC#Pho8yn3knq=VgMx*kzGP=mvDZ^CX!NO#$i&lIf zh5n46DA}6E2la?)yoq>38kNZF5~XM8F`rI@$N~tE&&?s_`P^0K z)(FDK@=IOr)k*ia=e_x%KphKW!5a_I$11?IOj5{*#j(_N0 zokpc>x?Rxg&3#h1i)!?X(pmXckd4yqQd)Bl^JcHR?_VVU-QmTcTmy4TaT_bFo(?~9 zC56SVvBJc(dwJPe8=yhI{< zKXI}P>t2+MOJmu~`PDuOaPED#e>$|-q9Z7lE!`*#B;Wn@B=QpYgRBNF;U35@4U*^RM&$?IN SWx=>}u`dW3R24p0VgCa`28lEP literal 0 HcmV?d00001 diff --git a/pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.po b/pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.po new file mode 100644 index 0000000..7b8dd36 --- /dev/null +++ b/pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.po @@ -0,0 +1,222 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-11-01 04:58+0800\n" +"PO-Revision-Date: 2024-11-01 05:05+0800\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.5\n" + +#: cmd/dashboard/controller/alertrule.go:100 +#, c-format +msgid "alert id %d does not exist" +msgstr "告警 ID %d 不存在" + +#: cmd/dashboard/controller/alertrule.go:155 +msgid "duration need to be at least 3" +msgstr "duration 至少为 3" + +#: cmd/dashboard/controller/alertrule.go:159 +msgid "cycle_interval need to be at least 1" +msgstr "cycle_interval 至少为 1" + +#: cmd/dashboard/controller/alertrule.go:162 +msgid "cycle_start is not set" +msgstr "cycle_start 未设置" + +#: cmd/dashboard/controller/alertrule.go:165 +msgid "cycle_start is a future value" +msgstr "cycle_start 是未来值" + +#: cmd/dashboard/controller/alertrule.go:170 +msgid "need to configure at least a single rule" +msgstr "需要至少定义一条规则" + +#: cmd/dashboard/controller/controller.go:188 +msgid "database error" +msgstr "数据库错误" + +#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122 +msgid "scheduled tasks cannot be triggered by alarms" +msgstr "计划任务不能被告警触发" + +#: cmd/dashboard/controller/cron.go:161 +#, c-format +msgid "task id %d does not exist" +msgstr "任务 id %d 不存在" + +#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120 +msgid "the retry count must be an integer between 1 and 10" +msgstr "重试次数必须为大于 1 且不超过 10 的整数" + +#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148 +msgid "error parsing %s: %v" +msgstr "解析 %s 时发生错误:%v" + +#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95 +#, c-format +msgid "profile id %d does not exist" +msgstr "配置 id %d 不存在" + +#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43 +msgid "server not found or not connected" +msgstr "服务器未找到或仍未连接" + +#: cmd/dashboard/controller/notification.go:67 +#: cmd/dashboard/controller/notification.go:125 +msgid "a test message" +msgstr "一条测试信息" + +#: cmd/dashboard/controller/notification.go:106 +#, c-format +msgid "notification id %d does not exist" +msgstr "通知方式 id %d 不存在" + +#: cmd/dashboard/controller/notification_group.go:80 +#: cmd/dashboard/controller/notification_group.go:142 +msgid "have invalid notification id" +msgstr "通知方式 id 无效" + +#: cmd/dashboard/controller/notification_group.go:131 +#: cmd/dashboard/controller/server_group.go:130 +#, c-format +msgid "group id %d does not exist" +msgstr "组 id %d 不存在" + +#: cmd/dashboard/controller/server.go:59 +#, c-format +msgid "server id %d does not exist" +msgstr "服务器 id %d 不存在" + +#: cmd/dashboard/controller/server_group.go:78 +#: cmd/dashboard/controller/server_group.go:139 +msgid "have invalid server id" +msgstr "服务器 id 无效" + +#: cmd/dashboard/controller/service.go:79 +#: cmd/dashboard/controller/service.go:155 +msgid "server not found" +msgstr "未找到服务器" + +#: cmd/dashboard/controller/service.go:86 +msgid "unauthorized" +msgstr "未授权" + +#: cmd/dashboard/controller/service.go:247 +#, c-format +msgid "service id %d does not exist" +msgstr "服务 id %d 不存在" + +#: cmd/dashboard/controller/user.go:45 +msgid "password length must be greater than 6" +msgstr "密码长度必须大于6" + +#: cmd/dashboard/controller/user.go:48 +msgid "username can't be empty" +msgstr "用户名不能为空" + +#: service/rpc/io_stream.go:122 +msgid "timeout: no connection established" +msgstr "超时:无连接建立" + +#: service/rpc/io_stream.go:125 +msgid "timeout: user connection not established" +msgstr "超时:用户连接未建立" + +#: service/rpc/io_stream.go:128 +msgid "timeout: agent connection not established" +msgstr "超时:agent 连接未建立" + +#: service/rpc/nezha.go:57 +msgid "Scheduled Task Executed Successfully" +msgstr "计划任务执行成功" + +#: service/rpc/nezha.go:61 +msgid "Scheduled Task Executed Failed" +msgstr "计划任务执行失败" + +#: service/rpc/nezha.go:156 +msgid "IP Changed" +msgstr "IP变更" + +#: service/singleton/alertsentinel.go:159 +msgid "Incident" +msgstr "事件" + +#: service/singleton/alertsentinel.go:169 +msgid "Resolved" +msgstr "恢复" + +#: service/singleton/crontask.go:52 +msgid "Tasks failed to register: [" +msgstr "注册失败的任务:[" + +#: service/singleton/crontask.go:59 +msgid "" +"] These tasks will not execute properly. Fix them in the admin dashboard." +msgstr "这些任务将无法正常执行,请进入后点重新修改保存。" + +#: service/singleton/crontask.go:150 service/singleton/crontask.go:175 +#, c-format +msgid "[Task failed] %s: server %s is offline and cannot execute the task" +msgstr "[任务失败] %s,服务器 %s 离线,无法执行" + +#: service/singleton/servicesentinel.go:439 +#, c-format +msgid "[Latency] %s %2f > %2f, Reporter: %s" +msgstr "[延迟告警] %s %2f > %2f, 报告服务: %s" + +#: service/singleton/servicesentinel.go:446 +#, c-format +msgid "[Latency] %s %2f < %2f, Reporter: %s" +msgstr "[延迟告警] %s %2f < %2f, 报告服务: %s" + +#: service/singleton/servicesentinel.go:472 +#, c-format +msgid "[%s] %s Reporter: %s, Error: %s" +msgstr "[%s] %s 报告服务: %s, 错误信息: %s" + +#: service/singleton/servicesentinel.go:515 +#, c-format +msgid "[SSL] Fetch cert info failed, %s %s" +msgstr "[SSL] 获取证书信息失败, %s %s" + +#: service/singleton/servicesentinel.go:555 +#, c-format +msgid "The SSL certificate will expire within seven days. Expiration time: %s" +msgstr "SSL 证书将在 7 天内过期。过期时间为:%s" + +#: service/singleton/servicesentinel.go:568 +#, c-format +msgid "" +"SSL certificate changed, old: issuer %s, expires at %s; new: issuer %s, " +"expires at %s" +msgstr "" +"SSL 证书发生更改,旧值:颁发者 %s,过期日 %s;新值:颁发者 %s,过期日 %s" + +#: service/singleton/servicesentinel.go:604 +msgid "No Data" +msgstr "无数据" + +#: service/singleton/servicesentinel.go:606 +msgid "Good" +msgstr "正常" + +#: service/singleton/servicesentinel.go:608 +msgid "Low Availability" +msgstr "低可用" + +#: service/singleton/servicesentinel.go:610 +msgid "Down" +msgstr "故障" diff --git a/service/rpc/io_stream.go b/service/rpc/io_stream.go index b1014cb..2270672 100644 --- a/service/rpc/io_stream.go +++ b/service/rpc/io_stream.go @@ -6,6 +6,8 @@ import ( "sync" "sync/atomic" "time" + + "github.com/naiba/nezha/service/singleton" ) type ioStreamContext struct { @@ -117,13 +119,13 @@ LOOP: } if stream.userIo == nil && stream.agentIo == nil { - return errors.New("timeout: no connection established") + return singleton.Localizer.ErrorT("timeout: no connection established") } if stream.userIo == nil { - return errors.New("timeout: user connection not established") + return singleton.Localizer.ErrorT("timeout: user connection not established") } if stream.agentIo == nil { - return errors.New("timeout: agent connection not established") + return singleton.Localizer.ErrorT("timeout: agent connection not established") } isDone := new(atomic.Bool) diff --git a/service/rpc/nezha.go b/service/rpc/nezha.go index ad4707d..6f3e2a6 100644 --- a/service/rpc/nezha.go +++ b/service/rpc/nezha.go @@ -54,11 +54,11 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece curServer := model.Server{} copier.Copy(&curServer, singleton.ServerList[clientID]) if cr.PushSuccessful && r.GetSuccessful() { - singleton.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", "Scheduled Task Executed Successfully", + singleton.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", singleton.Localizer.T("Scheduled Task Executed Successfully"), cr.Name, singleton.ServerList[clientID].Name, r.GetData()), nil, &curServer) } if !r.GetSuccessful() { - singleton.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", "Scheduled Task Executed Failed", + singleton.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", singleton.Localizer.T("Scheduled Task Executed Failed"), cr.Name, singleton.ServerList[clientID].Name, r.GetData()), nil, &curServer) } singleton.DB.Model(cr).Updates(model.Cron{ @@ -153,7 +153,7 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece singleton.SendNotification(singleton.Conf.IPChangeNotificationGroupID, fmt.Sprintf( "[%s] %s, %s => %s", - "IPChanged", + singleton.Localizer.T("IP Changed"), singleton.ServerList[clientID].Name, singleton.IPDesensitize(singleton.ServerList[clientID].Host.IP), singleton.IPDesensitize(host.IP), ), diff --git a/service/singleton/alertsentinel.go b/service/singleton/alertsentinel.go index d0fd576..b2c700c 100644 --- a/service/singleton/alertsentinel.go +++ b/service/singleton/alertsentinel.go @@ -156,7 +156,7 @@ func checkStatus() { // 始终触发模式或上次检查不为失败时触发报警(跳过单次触发+上次失败的情况) if alert.TriggerMode == model.ModeAlwaysTrigger || alertsPrevState[alert.ID][server.ID] != _RuleCheckFail { alertsPrevState[alert.ID][server.ID] = _RuleCheckFail - message := fmt.Sprintf("[%s] %s(%s) %s", "Incident", + message := fmt.Sprintf("[%s] %s(%s) %s", Localizer.T("Incident"), server.Name, IPDesensitize(server.Host.IP), alert.Name) go SendTriggerTasks(alert.FailTriggerTasks, curServer.ID) go SendNotification(alert.NotificationGroupID, message, NotificationMuteLabel.ServerIncident(server.ID, alert.ID), &curServer) @@ -166,7 +166,7 @@ func checkStatus() { } else { // 本次通过检查但上一次的状态为失败,则发送恢复通知 if alertsPrevState[alert.ID][server.ID] == _RuleCheckFail { - message := fmt.Sprintf("[%s] %s(%s) %s", "Resolved", + message := fmt.Sprintf("[%s] %s(%s) %s", Localizer.T("Resolved"), server.Name, IPDesensitize(server.Host.IP), alert.Name) go SendTriggerTasks(alert.RecoverTriggerTasks, curServer.ID) go SendNotification(alert.NotificationGroupID, message, NotificationMuteLabel.ServerIncidentResolved(server.ID, alert.ID), &curServer) diff --git a/service/singleton/crontask.go b/service/singleton/crontask.go index 652c199..5cda99d 100644 --- a/service/singleton/crontask.go +++ b/service/singleton/crontask.go @@ -34,29 +34,29 @@ func loadCronTasks() { var err error var notificationGroupList []uint64 notificationMsgMap := make(map[uint64]*bytes.Buffer) - for i := 0; i < len(CronList); i++ { + for _, cron := range CronList { // 触发任务类型无需注册 - if CronList[i].TaskType == model.CronTypeTriggerTask { - Crons[CronList[i].ID] = CronList[i] + if cron.TaskType == model.CronTypeTriggerTask { + Crons[cron.ID] = cron continue } // 注册计划任务 - CronList[i].CronJobID, err = Cron.AddFunc(CronList[i].Scheduler, CronTrigger(CronList[i])) + cron.CronJobID, err = Cron.AddFunc(cron.Scheduler, CronTrigger(cron)) if err == nil { - Crons[CronList[i].ID] = CronList[i] + Crons[cron.ID] = cron } else { // 当前通知组首次出现 将其加入通知组列表并初始化通知组消息缓存 - if _, ok := notificationMsgMap[CronList[i].NotificationGroupID]; !ok { - notificationGroupList = append(notificationGroupList, CronList[i].NotificationGroupID) - notificationMsgMap[CronList[i].NotificationGroupID] = bytes.NewBufferString("") - notificationMsgMap[CronList[i].NotificationGroupID].WriteString("调度失败的计划任务:[") + if _, ok := notificationMsgMap[cron.NotificationGroupID]; !ok { + notificationGroupList = append(notificationGroupList, cron.NotificationGroupID) + notificationMsgMap[cron.NotificationGroupID] = bytes.NewBufferString("") + notificationMsgMap[cron.NotificationGroupID].WriteString(Localizer.T("Tasks failed to register: [")) } - notificationMsgMap[CronList[i].NotificationGroupID].WriteString(fmt.Sprintf("%d,", CronList[i].ID)) + notificationMsgMap[cron.NotificationGroupID].WriteString(fmt.Sprintf("%d,", cron.ID)) } } // 向注册错误的计划任务所在通知组发送通知 for _, gid := range notificationGroupList { - notificationMsgMap[gid].WriteString("] 这些任务将无法正常执行,请进入后点重新修改保存。") + notificationMsgMap[gid].WriteString(Localizer.T("] These tasks will not execute properly. Fix them in the admin dashboard.")) SendNotification(gid, notificationMsgMap[gid].String(), nil) } Cron.Start() @@ -147,7 +147,7 @@ func CronTrigger(cr *model.Cron, triggerServer ...uint64) func() { // 保存当前服务器状态信息 curServer := model.Server{} copier.Copy(&curServer, s) - SendNotification(cr.NotificationGroupID, fmt.Sprintf("[任务失败] %s,服务器 %s 离线,无法执行。", cr.Name, s.Name), nil, &curServer) + SendNotification(cr.NotificationGroupID, Localizer.Tf("[Task failed] %s: server %s is offline and cannot execute the task", cr.Name, s.Name), nil, &curServer) } } return @@ -172,7 +172,7 @@ func CronTrigger(cr *model.Cron, triggerServer ...uint64) func() { // 保存当前服务器状态信息 curServer := model.Server{} copier.Copy(&curServer, s) - SendNotification(cr.NotificationGroupID, fmt.Sprintf("[任务失败] %s,服务器 %s 离线,无法执行。", cr.Name, s.Name), nil, &curServer) + SendNotification(cr.NotificationGroupID, Localizer.Tf("[Task failed] %s: server %s is offline and cannot execute the task", cr.Name, s.Name), nil, &curServer) } } } diff --git a/service/singleton/i18n.go b/service/singleton/i18n.go new file mode 100644 index 0000000..5e839b9 --- /dev/null +++ b/service/singleton/i18n.go @@ -0,0 +1,80 @@ +package singleton + +import ( + "archive/zip" + "bytes" + "fmt" + "log" + + "github.com/naiba/nezha/pkg/i18n" +) + +const domain = "nezha" + +var Localizer *i18n.Localizer + +func initI18n() { + if err := loadTranslation(); err != nil { + log.Printf("NEZHA>> init i18n failed: %v", err) + } +} + +func loadTranslation() error { + lang := Conf.Language + if lang == "" { + lang = "zh_CN" + } + + data, err := getTranslationArchive(lang) + if err != nil { + return err + } + + Localizer = i18n.NewLocalizer(lang, domain, domain+".zip", data) + return nil +} + +func OnUpdateLang(lang string) error { + if Localizer.Exists(lang) { + Localizer.SetLanguage(lang) + return nil + } + + data, err := getTranslationArchive(lang) + if err != nil { + return err + } + + Localizer.AppendIntl(lang, domain, domain+".zip", data) + Localizer.SetLanguage(lang) + return nil +} + +func getTranslationArchive(lang string) ([]byte, error) { + files := [...]string{ + fmt.Sprintf("translations/%s/LC_MESSAGES/%s.po", lang, domain), + fmt.Sprintf("translations/%s/LC_MESSAGES/%s.mo", lang, domain), + } + + buf := new(bytes.Buffer) + w := zip.NewWriter(buf) + + for _, file := range files { + f, err := w.Create(file) + if err != nil { + return nil, err + } + data, err := i18n.Translations.ReadFile(file) + if err != nil { + return nil, err + } + if _, err := f.Write(data); err != nil { + return nil, err + } + } + if err := w.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/service/singleton/servicesentinel.go b/service/singleton/servicesentinel.go index 16ac5f9..bb467f2 100644 --- a/service/singleton/servicesentinel.go +++ b/service/singleton/servicesentinel.go @@ -436,14 +436,14 @@ func (ss *ServiceSentinel) worker() { // 延迟超过最大值 ServerLock.RLock() reporterServer := ServerList[r.Reporter] - msg := fmt.Sprintf("[Latency] %s %2f > %2f, Reporter: %s", ss.Services[mh.GetId()].Name, mh.Delay, ss.Services[mh.GetId()].MaxLatency, reporterServer.Name) + msg := Localizer.Tf("[Latency] %s %2f > %2f, Reporter: %s", ss.Services[mh.GetId()].Name, mh.Delay, ss.Services[mh.GetId()].MaxLatency, reporterServer.Name) go SendNotification(notificationGroupID, msg, minMuteLabel) ServerLock.RUnlock() } else if mh.Delay < ss.Services[mh.GetId()].MinLatency { // 延迟低于最小值 ServerLock.RLock() reporterServer := ServerList[r.Reporter] - msg := fmt.Sprintf("[Latency] %s %2f < %2f, Reporter: %s", ss.Services[mh.GetId()].Name, mh.Delay, ss.Services[mh.GetId()].MinLatency, reporterServer.Name) + msg := Localizer.Tf("[Latency] %s %2f < %2f, Reporter: %s", ss.Services[mh.GetId()].Name, mh.Delay, ss.Services[mh.GetId()].MinLatency, reporterServer.Name) go SendNotification(notificationGroupID, msg, maxMuteLabel) ServerLock.RUnlock() } else { @@ -469,7 +469,7 @@ func (ss *ServiceSentinel) worker() { reporterServer := ServerList[r.Reporter] notificationGroupID := ss.Services[mh.GetId()].NotificationGroupID - notificationMsg := fmt.Sprintf("[%s] %s Reporter: %s, Error: %s", StatusCodeToString(stateCode), ss.Services[mh.GetId()].Name, reporterServer.Name, mh.Data) + notificationMsg := Localizer.Tf("[%s] %s Reporter: %s, Error: %s", StatusCodeToString(stateCode), ss.Services[mh.GetId()].Name, reporterServer.Name, mh.Data) muteLabel := NotificationMuteLabel.ServiceStateChanged(mh.GetId()) // 状态变更时,清除静音缓存 @@ -512,7 +512,7 @@ func (ss *ServiceSentinel) worker() { ss.ServicesLock.RLock() if ss.Services[mh.GetId()].Notify { muteLabel := NotificationMuteLabel.ServiceSSL(mh.GetId(), "network") - go SendNotification(ss.Services[mh.GetId()].NotificationGroupID, fmt.Sprintf("[SSL] Fetch cert info failed, %s %s", ss.Services[mh.GetId()].Name, errMsg), muteLabel) + go SendNotification(ss.Services[mh.GetId()].NotificationGroupID, Localizer.Tf("[SSL] Fetch cert info failed, %s %s", ss.Services[mh.GetId()].Name, errMsg), muteLabel) } ss.ServicesLock.RUnlock() @@ -551,7 +551,7 @@ func (ss *ServiceSentinel) worker() { // 证书过期提醒 if expiresNew.Before(time.Now().AddDate(0, 0, 7)) { expiresTimeStr := expiresNew.Format("2006-01-02 15:04:05") - errMsg = fmt.Sprintf( + errMsg = Localizer.Tf( "The SSL certificate will expire within seven days. Expiration time: %s", expiresTimeStr, ) @@ -564,8 +564,8 @@ func (ss *ServiceSentinel) worker() { // 证书变更提醒 if isCertChanged { - errMsg = fmt.Sprintf( - "SSL certificate changed, old: %s, %s expired; new: %s, %s expired.", + errMsg = Localizer.Tf( + "SSL certificate changed, old: issuer %s, expires at %s; new: issuer %s, expires at %s", oldCert[0], expiresOld.Format("2006-01-02 15:04:05"), newCert[0], expiresNew.Format("2006-01-02 15:04:05")) // 证书变更后会自动更新缓存,所以不需要静音 @@ -601,17 +601,13 @@ func GetStatusCode[T float32 | uint64](percent T) int { func StatusCodeToString(statusCode int) string { switch statusCode { case StatusNoData: - // return Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "StatusNoData"}) - return "No Data" + return Localizer.T("No Data") case StatusGood: - // return Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "StatusGood"}) - return "Good" + return Localizer.T("Good") case StatusLowAvailability: - // return Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "StatusLowAvailability"}) - return "Low Availability" + return Localizer.T("Low Availability") case StatusDown: - // return Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "StatusDown"}) - return "Down" + return Localizer.T("Down") default: return "" } diff --git a/service/singleton/singleton.go b/service/singleton/singleton.go index 541ae48..b4548d9 100644 --- a/service/singleton/singleton.go +++ b/service/singleton/singleton.go @@ -33,6 +33,7 @@ func InitTimezoneAndCache() { // LoadSingleton 加载子服务并执行 func LoadSingleton() { + initI18n() // 加载本地化服务 loadNotifications() // 加载通知服务 loadServers() // 加载服务器列表 loadCronTasks() // 加载定时任务 From ca997cc994e9177193abd43efd887c33b870f176 Mon Sep 17 00:00:00 2001 From: uubulb Date: Fri, 1 Nov 2024 14:07:05 +0800 Subject: [PATCH 2/4] add zh_TW locale, rename ssl to tls --- .gitignore | 1 + cmd/dashboard/controller/member_api.go | 4 +- cmd/dashboard/controller/notification.go | 8 +- model/notification.go | 4 +- model/notification_api.go | 2 +- pkg/i18n/template.pot | 8 +- .../translations/en_US/LC_MESSAGES/nezha.mo | Bin 4074 -> 4110 bytes .../translations/en_US/LC_MESSAGES/nezha.po | 17 +- .../translations/zh_CN/LC_MESSAGES/nezha.mo | Bin 3944 -> 4002 bytes .../translations/zh_CN/LC_MESSAGES/nezha.po | 28 +-- .../translations/zh_TW/LC_MESSAGES/nezha.mo | Bin 0 -> 4011 bytes .../translations/zh_TW/LC_MESSAGES/nezha.po | 222 ++++++++++++++++++ pkg/utils/http.go | 8 +- pkg/utils/utils.go | 4 - script/i18n.sh | 175 ++++++++++++++ service/singleton/notification.go | 4 +- service/singleton/servicesentinel.go | 32 +-- 17 files changed, 455 insertions(+), 62 deletions(-) create mode 100644 pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.mo create mode 100644 pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.po create mode 100755 script/i18n.sh diff --git a/.gitignore b/.gitignore index 89b21d4..782cf4b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.test # Output of the go coverage tool, specifically when used with LiteIDE +*~ *.out *.pprof .idea diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go index 158fa7e..13a7aba 100644 --- a/cmd/dashboard/controller/member_api.go +++ b/cmd/dashboard/controller/member_api.go @@ -299,8 +299,8 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) { n.RequestHeader = nf.RequestHeader n.RequestBody = nf.RequestBody n.URL = nf.URL - verifySSL := nf.VerifySSL == "on" - n.VerifySSL = &verifySSL + //verifySSL := nf.VerifySSL == "on" + //n.VerifySSL = &verifySSL n.ID = nf.ID ns := model.NotificationServerBundle{ Notification: &n, diff --git a/cmd/dashboard/controller/notification.go b/cmd/dashboard/controller/notification.go index 3360021..5ba950c 100644 --- a/cmd/dashboard/controller/notification.go +++ b/cmd/dashboard/controller/notification.go @@ -54,8 +54,8 @@ func createNotification(c *gin.Context) (uint64, error) { n.RequestHeader = nf.RequestHeader n.RequestBody = nf.RequestBody n.URL = nf.URL - verifySSL := nf.VerifySSL - n.VerifySSL = &verifySSL + verifyTLS := nf.VerifyTLS + n.VerifyTLS = &verifyTLS ns := model.NotificationServerBundle{ Notification: &n, @@ -112,8 +112,8 @@ func updateNotification(c *gin.Context) (any, error) { n.RequestHeader = nf.RequestHeader n.RequestBody = nf.RequestBody n.URL = nf.URL - verifySSL := nf.VerifySSL - n.VerifySSL = &verifySSL + verifyTLS := nf.VerifyTLS + n.VerifyTLS = &verifyTLS ns := model.NotificationServerBundle{ Notification: &n, diff --git a/model/notification.go b/model/notification.go index 992c85f..02c683b 100644 --- a/model/notification.go +++ b/model/notification.go @@ -38,7 +38,7 @@ type Notification struct { RequestType int `json:"request_type,omitempty"` RequestHeader string `json:"request_header,omitempty" gorm:"type:longtext"` RequestBody string `json:"request_body,omitempty" gorm:"type:longtext"` - VerifySSL *bool `json:"verify_ssl,omitempty"` + VerifyTLS *bool `json:"verify_tls,omitempty"` } func (ns *NotificationServerBundle) reqURL(message string) string { @@ -111,7 +111,7 @@ func (n *Notification) setRequestHeader(req *http.Request) error { func (ns *NotificationServerBundle) Send(message string) error { var client *http.Client n := ns.Notification - if n.VerifySSL != nil && *n.VerifySSL { + if n.VerifyTLS != nil && *n.VerifyTLS { client = utils.HttpClient } else { client = utils.HttpClientSkipTlsVerify diff --git a/model/notification_api.go b/model/notification_api.go index d5fde9e..8f36189 100644 --- a/model/notification_api.go +++ b/model/notification_api.go @@ -7,6 +7,6 @@ type NotificationForm struct { RequestType int `json:"request_type,omitempty"` RequestHeader string `json:"request_header,omitempty"` RequestBody string `json:"request_body,omitempty"` - VerifySSL bool `json:"verify_ssl,omitempty"` + VerifyTLS bool `json:"verify_tls,omitempty"` SkipCheck bool `json:"skip_check,omitempty"` } diff --git a/pkg/i18n/template.pot b/pkg/i18n/template.pot index f3d6764..46b419a 100644 --- a/pkg/i18n/template.pot +++ b/pkg/i18n/template.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-01 05:06+0800\n" +"POT-Creation-Date: 2024-11-01 13:32+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -188,18 +188,18 @@ msgstr "" #: service/singleton/servicesentinel.go:515 #, c-format -msgid "[SSL] Fetch cert info failed, %s %s" +msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s" msgstr "" #: service/singleton/servicesentinel.go:555 #, c-format -msgid "The SSL certificate will expire within seven days. Expiration time: %s" +msgid "The TLS certificate will expire within seven days. Expiration time: %s" msgstr "" #: service/singleton/servicesentinel.go:568 #, c-format msgid "" -"SSL certificate changed, old: issuer %s, expires at %s; new: issuer %s, " +"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, " "expires at %s" msgstr "" diff --git a/pkg/i18n/translations/en_US/LC_MESSAGES/nezha.mo b/pkg/i18n/translations/en_US/LC_MESSAGES/nezha.mo index 109907810e07778455979d642a46540680e298d5..6a3e9703800918d0cbfc2b80afbf9167edfcda07 100644 GIT binary patch delta 845 zcma*k&r8#B9LMqZ{BG(tXBt{*nagQS{DCG55+aCD79E7CV2a>Tg>f2`4 z88mCeJ{-U|%h{G(iee>X_5{CT8Y_#<%5fNLaRRk)4(sp(Uc+5f-#%)CQ>?;RiNCG^ zZ}I#H)&ByMn8iDoFR`56rPwYs{=hb#t<>x$v||N6LoFCa{oyp$U>>`06Pxh}8}Mq_ z-zbG;JdfgaoWwJnMMAKtNX{>2B4&JSo-h8xmQfq$k(q3*@cRbpq}!-O_E1q;)E{^d zdw6_>^|*!V`(E%5D)9tUDe<8kMVew273Co+@?WTh6)`{Z6e_|Ds&4`{$Xit8E2wqr zsDyS={Rf!Db5w#&ELP&ZsJ`)+6m1lXsK~caC;x_8aENLwz2ZmSgo^wzHsfnlcetJK I&;MO@0fh@<-v9sr delta 839 zcma*lJ4jn$7{>88IVZKT_7bRfZETNMqE(TAbu3j(5jq4xq0l;sb&#NgPG@XfigG9@ zxG18alL&&~q9BFR4sj3%M<)r+1qb_oJQ+InBj^0yFZr$y`POuuT>4l!YG%Ez*$e!N z&A7FkOZgtfO3dsn9$^OW@CjxfnAPDJYQiiw;tE#b0czZD)WVne9B)zco|Tz3U{#ha1>|=lBXO9&Y4e1>F&> z#&P_EIV1%8QtpSvOt~2!o8!eA=A$;wBWJPI()a7AD0fgdwnt$yNz{Sg;ShetByN{} z|537tN<6`-lz7^w$WY9qqC7!GS3pHxL`9x>97foU8aISGz$hy6In=yGR6?7m$d9od zFHi|an5@J-)VQhl6kQaPs2hAo-Fy!<;RH4C3KeyWhYJ7|<@D0kk&nOoxU^b>F zC-xQLD)M@i^@p(({kRsx$ttKy2xjetUvVS3N>Qo+DsdS$ploPCIq)*(VHj(08q3ha zBFsunTvUf?sE>}6+)R9w(~o-_|@+n>0bd;?X=C2vRRWW zYmVX^+Ndgc|9mR-D~DhAId>-QYtpsOCjHiFI+}?WoKND8xbm|Pbe!!9oa+eqOmBCE zxhoLp3izg8xRRW%sv2MQCOwxrW0?A)yG*Bes^hyR+{d)flOK-{dG_Qk&W63+i?jXT zhHqPu$o$-BtS7>l{^H5Y-e!8MPcRn?jYo$>29L2r5>Z;UjMBpICO!_ zr$#g+t?6xq|9|%CPe#_A8PStwMh}`7^@`bTWQe!Tw+&04usfEke)hOc&%jV}U`Vfe z-|JalyfORBcTd-RC-r52xDk*0Q^BMa%h}e}i-pZfW&Pt~eX5eQ^s+zVwk`eBZ+F^O sz4&Tlu2?V4ZWI?KTiuou`vYN*O=Rhbz(eE5^iuU}VANGT*Y?2m4-C_J %2f, Reporter: %s" -msgstr "[延迟告警] %s %2f > %2f, 报告服务: %s" +msgstr "[延迟告警] %s %2f > %2f,报告服务: %s" #: service/singleton/servicesentinel.go:446 #, c-format msgid "[Latency] %s %2f < %2f, Reporter: %s" -msgstr "[延迟告警] %s %2f < %2f, 报告服务: %s" +msgstr "[延迟告警] %s %2f < %2f,报告服务: %s" #: service/singleton/servicesentinel.go:472 #, c-format msgid "[%s] %s Reporter: %s, Error: %s" -msgstr "[%s] %s 报告服务: %s, 错误信息: %s" +msgstr "[%s] %s 报告服务:%s,错误信息:%s" #: service/singleton/servicesentinel.go:515 #, c-format -msgid "[SSL] Fetch cert info failed, %s %s" -msgstr "[SSL] 获取证书信息失败, %s %s" +msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s" +msgstr "[TLS] 获取证书信息失败,报告服务:%s,错误信息:%s" #: service/singleton/servicesentinel.go:555 #, c-format -msgid "The SSL certificate will expire within seven days. Expiration time: %s" -msgstr "SSL 证书将在 7 天内过期。过期时间为:%s" +msgid "The TLS certificate will expire within seven days. Expiration time: %s" +msgstr "TLS 证书将在 7 天内过期。过期时间为:%s" #: service/singleton/servicesentinel.go:568 #, c-format msgid "" -"SSL certificate changed, old: issuer %s, expires at %s; new: issuer %s, " +"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, " "expires at %s" msgstr "" -"SSL 证书发生更改,旧值:颁发者 %s,过期日 %s;新值:颁发者 %s,过期日 %s" +"TLS 证书发生更改,旧值:颁发者 %s,过期日 %s;新值:颁发者 %s,过期日 %s" #: service/singleton/servicesentinel.go:604 msgid "No Data" diff --git a/pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.mo b/pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.mo new file mode 100644 index 0000000000000000000000000000000000000000..3789727725d434ef73be55296c87c14e7451d650 GIT binary patch literal 4011 zcmai#du$v>9ml7o4=|L{@GQ@v)NR`Mj4y85;1Z;^i7Queu@V;$QK;6tb9dXGcdz?6 z#}!D1^OR>p9b&4 z-$1hC@8Flf2Vf+vdjuqZcYq}Sb8s{G2KZfY z1bhr!21)KiFb0xyWPdeCe!m2g{63K6eg!@Zj)1k`HSh^A03QK2eMaip27V6xUw~f) zUkCpK9tL45`z3-*JPcxp^+f%Fs6QI_ za{h;iAA$AgKMA8q?hTOo)8HoXHb`;0`vDohhe6W!6!>MZ5quC-L5i~tQrx@2FM?Ab ztzU}ze~#vTkm7bPk`?9!B)J_R#kCnE`?Da~aWWb|1CrctBmM<^2K|45q<0I9A^Tne zX&)7&ICg@Rzkcv8a5Ea^g=z;S9o7uM9!G-+`yO@hk!|!nhDPa8;U7#m%%<$93Cwg~coFDlQIWbW7M=anOC18=~#wry1E}^F(L|cT8@Hq;5OH zs^j}ub4u`yTHEw=I?}+~bSI@7+!k43a7}5qtKk`RD~@g&+|gS_9V}q`D((Fs-y<@n z6>d#d?y@WsT4;VZ+%eSlP^fZig8z`?x2i<`PN5%_6N0t-dA)GdR2V9*8woRVu5`Z= z{b5dI2*`#ycw}Y6U?QyCOeE5}A-G~_TvZIibl`uOKkgtv4g$#bbHvJ)9UE~$jMmF# zEHfjlbbB?g*ALLDR#+CUROF!aw=+BD8w_0W$Q*V3}0oIB`wp_eR@Eot4x)mD@=%=KbrMO-+&UTEW9uA#V2%Cz)1u`vl7N-M5A z#dthSlxWT1ifyvY*AS0bgBJUVKRRl1H)+S3O$;~vIJQS*brOo< zUdFAowPx#6v3NXI6X)^Ty7&*a)NHS*so0G(6Kl2T}~J=k}jN&H4CM6UB8aM znR>PPrxlG2jk}8a^}KDfQy8+7-09m}_uM+&Fk##GSJiGz>v z!od^%*tq}Bxv)Fx56%ByF&0cOc}IJsDfy8j(mrgB_XjVYRPW7qd1KT38Sb5(@j9oZ znh!e;Mf!#h6^4GDKeUKdmBshQJAK)EtH&SfgTLVn8Q41)oIi`W1k*i{w6}DBtWo~(qd>fud zmlsAS@;62>imdxr2W5I$p<_6=I2x^;?~-$}a->96aN&5NeaKnv z)+MCbJ95RFJ%u2%+(L&x@vgscF_@W@<}(>C-msJIwA?};B$1$vZ&J{8)ti3{iP`WH z`KRXmvGci=qo|IOM+l?A^sIMjgvrwN7B9$*6(6DEp0zBPI^(~0lKl6F56Kdh;uOg& z%m3y`bQcyaEOd}JJa7RL%>9l_*5sShUQS7u}&g2{gGSQmm@ zx>HFYyt$+KiRHrJ4R3MMyVY4Z-R+&7@`sl3vJ^V|@;5KtVc7c933G$PH-m`_GR(mg zoxf5WOSBO;5CaR#-*J1 h+%OxLl4B>j{fj-g^CA)NWM6*zvWy, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-11-01 13:14+0800\n" +"PO-Revision-Date: 2024-11-01 13:19+0800\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.5\n" + +#: cmd/dashboard/controller/alertrule.go:100 +#, c-format +msgid "alert id %d does not exist" +msgstr "告警 ID %d 不存在" + +#: cmd/dashboard/controller/alertrule.go:155 +msgid "duration need to be at least 3" +msgstr "duration 至少為 3" + +#: cmd/dashboard/controller/alertrule.go:159 +msgid "cycle_interval need to be at least 1" +msgstr "cycle_interval 至少為 1" + +#: cmd/dashboard/controller/alertrule.go:162 +msgid "cycle_start is not set" +msgstr "cycle_start 未設定" + +#: cmd/dashboard/controller/alertrule.go:165 +msgid "cycle_start is a future value" +msgstr "cycle_start 是未來值" + +#: cmd/dashboard/controller/alertrule.go:170 +msgid "need to configure at least a single rule" +msgstr "需要至少定義一條規則" + +#: cmd/dashboard/controller/controller.go:188 +msgid "database error" +msgstr "資料庫錯誤" + +#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122 +msgid "scheduled tasks cannot be triggered by alarms" +msgstr "排程任務不能被告警觸發" + +#: cmd/dashboard/controller/cron.go:161 +#, c-format +msgid "task id %d does not exist" +msgstr "任務 id %d 不存在" + +#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120 +msgid "the retry count must be an integer between 1 and 10" +msgstr "重試次數必須為大於 1 且不超過 10 的整數" + +#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148 +msgid "error parsing %s: %v" +msgstr "解析 %s 時發生錯誤:%v" + +#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95 +#, c-format +msgid "profile id %d does not exist" +msgstr "配置 id %d 不存在" + +#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43 +msgid "server not found or not connected" +msgstr "伺服器未找到或仍未連線" + +#: cmd/dashboard/controller/notification.go:67 +#: cmd/dashboard/controller/notification.go:125 +msgid "a test message" +msgstr "一條測試資訊" + +#: cmd/dashboard/controller/notification.go:106 +#, c-format +msgid "notification id %d does not exist" +msgstr "通知方式 id %d 不存在" + +#: cmd/dashboard/controller/notification_group.go:80 +#: cmd/dashboard/controller/notification_group.go:142 +msgid "have invalid notification id" +msgstr "通知方式 id 無效" + +#: cmd/dashboard/controller/notification_group.go:131 +#: cmd/dashboard/controller/server_group.go:130 +#, c-format +msgid "group id %d does not exist" +msgstr "組 id %d 不存在" + +#: cmd/dashboard/controller/server.go:59 +#, c-format +msgid "server id %d does not exist" +msgstr "伺服器 id %d 不存在" + +#: cmd/dashboard/controller/server_group.go:78 +#: cmd/dashboard/controller/server_group.go:139 +msgid "have invalid server id" +msgstr "伺服器 id 無效" + +#: cmd/dashboard/controller/service.go:79 +#: cmd/dashboard/controller/service.go:155 +msgid "server not found" +msgstr "未找到伺服器" + +#: cmd/dashboard/controller/service.go:86 +msgid "unauthorized" +msgstr "未授權" + +#: cmd/dashboard/controller/service.go:247 +#, c-format +msgid "service id %d does not exist" +msgstr "服務 id %d 不存在" + +#: cmd/dashboard/controller/user.go:45 +msgid "password length must be greater than 6" +msgstr "密碼長度必須大於 6" + +#: cmd/dashboard/controller/user.go:48 +msgid "username can't be empty" +msgstr "使用者名稱不能為空" + +#: service/rpc/io_stream.go:122 +msgid "timeout: no connection established" +msgstr "超時:無連線建立" + +#: service/rpc/io_stream.go:125 +msgid "timeout: user connection not established" +msgstr "超時:使用者連線未建立" + +#: service/rpc/io_stream.go:128 +msgid "timeout: agent connection not established" +msgstr "超時:agent 連線未建立" + +#: service/rpc/nezha.go:57 +msgid "Scheduled Task Executed Successfully" +msgstr "排程任務執行成功" + +#: service/rpc/nezha.go:61 +msgid "Scheduled Task Executed Failed" +msgstr "排程任務執行失敗" + +#: service/rpc/nezha.go:156 +msgid "IP Changed" +msgstr "IP 變更" + +#: service/singleton/alertsentinel.go:159 +msgid "Incident" +msgstr "事件" + +#: service/singleton/alertsentinel.go:169 +msgid "Resolved" +msgstr "恢復" + +#: service/singleton/crontask.go:52 +msgid "Tasks failed to register: [" +msgstr "註冊失敗的任務:[" + +#: service/singleton/crontask.go:59 +msgid "" +"] These tasks will not execute properly. Fix them in the admin dashboard." +msgstr "這些任務將無法正常執行,請進入後台重新修改儲存。" + +#: service/singleton/crontask.go:150 service/singleton/crontask.go:175 +#, c-format +msgid "[Task failed] %s: server %s is offline and cannot execute the task" +msgstr "[任務失敗] %s,伺服器 %s 離線,無法執行" + +#: service/singleton/servicesentinel.go:439 +#, c-format +msgid "[Latency] %s %2f > %2f, Reporter: %s" +msgstr "[延遲告警] %s %2f > %2f,報告服務: %s" + +#: service/singleton/servicesentinel.go:446 +#, c-format +msgid "[Latency] %s %2f < %2f, Reporter: %s" +msgstr "[延遲告警] %s %2f < %2f,報告服務: %s" + +#: service/singleton/servicesentinel.go:472 +#, c-format +msgid "[%s] %s Reporter: %s, Error: %s" +msgstr "[%s] %s 報告服務:%s,錯誤資訊:%s" + +#: service/singleton/servicesentinel.go:515 +#, c-format +msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s" +msgstr "[TLS] 獲取證書資訊失敗,報告服務:%s,錯誤資訊:%s" + +#: service/singleton/servicesentinel.go:555 +#, c-format +msgid "The TLS certificate will expire within seven days. Expiration time: %s" +msgstr "TLS 證書將在 7 天內過期。過期時間為:%s" + +#: service/singleton/servicesentinel.go:568 +#, c-format +msgid "" +"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, " +"expires at %s" +msgstr "" +"TLS 證書發生更改,舊值:頒發者 %s,過期日 %s;新值:頒發者 %s,過期日 %s" + +#: service/singleton/servicesentinel.go:604 +msgid "No Data" +msgstr "無資料" + +#: service/singleton/servicesentinel.go:606 +msgid "Good" +msgstr "正常" + +#: service/singleton/servicesentinel.go:608 +msgid "Low Availability" +msgstr "低可用" + +#: service/singleton/servicesentinel.go:610 +msgid "Down" +msgstr "故障" diff --git a/pkg/utils/http.go b/pkg/utils/http.go index a792bf9..19227c4 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -14,12 +14,12 @@ var ( func init() { HttpClientSkipTlsVerify = httpClient(_httpClient{ Transport: httpTransport(_httpTransport{ - SkipVerifySSL: true, + SkipVerifyTLS: true, }), }) HttpClient = httpClient(_httpClient{ Transport: httpTransport(_httpTransport{ - SkipVerifySSL: false, + SkipVerifyTLS: false, }), }) @@ -27,12 +27,12 @@ func init() { } type _httpTransport struct { - SkipVerifySSL bool + SkipVerifyTLS bool } func httpTransport(conf _httpTransport) *http.Transport { return &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: conf.SkipVerifySSL}, + TLSClientConfig: &tls.Config{InsecureSkipVerify: conf.SkipVerifyTLS}, Proxy: http.ProxyFromEnvironment, } } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index b2d2ffa..08f2766 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -19,10 +19,6 @@ var ( DNSServers = []string{"1.1.1.1:53", "223.5.5.5:53"} ) -func IsWindows() bool { - return os.PathSeparator == '\\' && os.PathListSeparator == ';' -} - var ipv4Re = regexp.MustCompile(`(\d*\.).*(\.\d*)`) func ipv4Desensitize(ipv4Addr string) string { diff --git a/script/i18n.sh b/script/i18n.sh new file mode 100755 index 0000000..e25ea81 --- /dev/null +++ b/script/i18n.sh @@ -0,0 +1,175 @@ +#!/bin/bash + +LANG=() +while IFS='' read -r line; do LANG+=("$line"); done < <(ls pkg/i18n/translations) +TEMPLATE="pkg/i18n/template.pot" +PODIR="pkg/i18n/translations/%s/LC_MESSAGES" +GIT_ROOT=$(git rev-parse --show-toplevel) + +red='\033[0;31m' +green='\033[0;32m' +yellow='\033[0;33m' +plain='\033[0m' + +err() { + printf "${red}%s${plain}\n" "$*" >&2 +} + +success() { + printf "${green}%s${plain}\n" "$*" +} + +info() { + printf "${yellow}%s${plain}\n" "$*" +} + +generate() { + case $1 in + "template") + generate_template + ;; + "en") + generate_en + ;; + *) + err "invalid argument" + ;; + esac +} + +generate_template() { + read -ra src < <(find . -name "*.go" | sort) + xgettext -C --add-comments=TRANSLATORS: -kErrorT -kT -kTf -kN:1,2 --from-code=UTF-8 -o $TEMPLATE "${src[@]}" +} + +generate_en() { + local po_file + po_file=$(printf "$PODIR/nezha.po" "en_US") + local mo_file + mo_file=$(printf "$PODIR/nezha.mo" "en_US") + msginit --input=$TEMPLATE --locale=en_US.UTF-8 --output-file="$po_file" --no-translator + msgfmt "$po_file" -o "$mo_file" +} + +compile() { + if [[ $# != 0 ]]; then + compile_single "$1" + else + compile_all + fi +} + +compile_single() { + local param="$1" + local found=0 + + for lang in "${LANG[@]}"; do + if [[ "$lang" == "$param" ]]; then + found=1 + break + fi + done + + if [[ $found == 0 ]]; then + err "the language does not exist." + return + fi + + local po_file + po_file=$(printf "$PODIR/nezha.po" "$param") + local mo_file + mo_file=$(printf "$PODIR/nezha.mo" "$param") + + msgfmt "$po_file" -o "$mo_file" +} + +compile_all() { + local po_file + local mo_file + for lang in "${LANG[@]}"; do + po_file=$(printf "$PODIR/nezha.po" "$lang") + mo_file=$(printf "$PODIR/nezha.mo" "$lang") + + msgfmt "$po_file" -o "$mo_file" + done +} + +update() { + if [[ $# != 0 ]]; then + update_single "$1" + else + update_all + fi +} + +update_single() { + local param="$1" + local found=0 + + for lang in "${LANG[@]}"; do + if [[ "$lang" == "$param" ]]; then + found=1 + break + fi + done + + if [[ $found == 0 ]]; then + err "the language does not exist." + return + fi + + local po_file + po_file=$(printf "$PODIR/nezha.po" "$param") + msgmerge -U "$po_file" $TEMPLATE +} + +update_all() { + for lang in "${LANG[@]}"; do + local po_file + po_file=$(printf "$PODIR/nezha.po" "$lang") + msgmerge -U "$po_file" $TEMPLATE + done +} + +show_help() { + echo "Usage: $0 [command] args" + echo "" + echo "Available commands:" + echo " update Update .po from .pot" + echo " compile Compile .mo from .po" + echo " generate Generate template or en_US locale" + echo "" + echo "Examples:" + echo " $0 update # Update all locales" + echo " $0 update zh_CN # Update zh_CN locale" + echo " $0 compile # Compile all locales" + echo " $0 compile zh_CN # Compile zh_CN locale" + echo " $0 generate template # Generate template" + echo " $0 generate en # Generate en_US locale" +} + +main() { + if [[ $PWD != "$GIT_ROOT" ]]; then + err "Must execute in the project root" + exit 1 + fi + + case "$1" in + "update") + update "$2" + ;; + "compile") + compile "$2" + ;; + "generate") + generate "$2" + ;; + *) + echo "Error: Unknown command '$1'" + show_help + exit 1 + ;; + esac +} + +main "$@" diff --git a/service/singleton/notification.go b/service/singleton/notification.go index 5d28ceb..d2b26d8 100644 --- a/service/singleton/notification.go +++ b/service/singleton/notification.go @@ -322,7 +322,7 @@ func (_NotificationMuteLabel) ServiceStateChanged(serviceId uint64) *string { return &label } -func (_NotificationMuteLabel) ServiceSSL(serviceId uint64, extraInfo string) *string { - label := fmt.Sprintf("bf::sssl-%d-%s", serviceId, extraInfo) +func (_NotificationMuteLabel) ServiceTLS(serviceId uint64, extraInfo string) *string { + label := fmt.Sprintf("bf::stls-%d-%s", serviceId, extraInfo) return &label } diff --git a/service/singleton/servicesentinel.go b/service/singleton/servicesentinel.go index bb467f2..a7fc0b7 100644 --- a/service/singleton/servicesentinel.go +++ b/service/singleton/servicesentinel.go @@ -43,7 +43,7 @@ func NewServiceSentinel(serviceSentinelDispatchBus chan<- model.Service) { serviceResponseDataStoreCurrentAvgDelay: make(map[uint64]float32), serviceResponsePing: make(map[uint64]map[uint64]*pingStore), Services: make(map[uint64]*model.Service), - sslCertCache: make(map[uint64]string), + tlsCertCache: make(map[uint64]string), // 30天数据缓存 monthlyStatus: make(map[uint64]*model.ServiceResponseItem), dispatchBus: serviceSentinelDispatchBus, @@ -102,7 +102,7 @@ type ServiceSentinel struct { serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [service_id] -> 当前服务离线计数 serviceResponsePing map[uint64]map[uint64]*pingStore // [service_id] -> ClientID -> delay lastStatus map[uint64]int - sslCertCache map[uint64]string + tlsCertCache map[uint64]string ServicesLock sync.RWMutex Services map[uint64]*model.Service @@ -279,7 +279,7 @@ func (ss *ServiceSentinel) OnServiceDelete(ids []uint64) { delete(ss.serviceResponseDataStoreCurrentUp, id) delete(ss.serviceResponseDataStoreCurrentDown, id) delete(ss.serviceResponseDataStoreCurrentAvgDelay, id) - delete(ss.sslCertCache, id) + delete(ss.tlsCertCache, id) delete(ss.serviceStatusToday, id) // 停掉定时任务 @@ -501,7 +501,7 @@ func (ss *ServiceSentinel) worker() { } ss.serviceResponseDataStoreLock.Unlock() - // SSL 证书报警 + // TLS 证书报警 var errMsg string if strings.HasPrefix(mh.Data, "SSL证书错误:") { // i/o timeout、connection timeout、EOF 错误 @@ -511,15 +511,15 @@ func (ss *ServiceSentinel) worker() { errMsg = mh.Data ss.ServicesLock.RLock() if ss.Services[mh.GetId()].Notify { - muteLabel := NotificationMuteLabel.ServiceSSL(mh.GetId(), "network") - go SendNotification(ss.Services[mh.GetId()].NotificationGroupID, Localizer.Tf("[SSL] Fetch cert info failed, %s %s", ss.Services[mh.GetId()].Name, errMsg), muteLabel) + muteLabel := NotificationMuteLabel.ServiceTLS(mh.GetId(), "network") + go SendNotification(ss.Services[mh.GetId()].NotificationGroupID, Localizer.Tf("[TLS] Fetch cert info failed, Reporter: %s, Error: %s", ss.Services[mh.GetId()].Name, errMsg), muteLabel) } ss.ServicesLock.RUnlock() } } else { // 清除网络错误静音缓存 - UnMuteNotification(ss.Services[mh.GetId()].NotificationGroupID, NotificationMuteLabel.ServiceSSL(mh.GetId(), "network")) + UnMuteNotification(ss.Services[mh.GetId()].NotificationGroupID, NotificationMuteLabel.ServiceTLS(mh.GetId(), "network")) var newCert = strings.Split(mh.Data, "|") if len(newCert) > 1 { @@ -527,11 +527,11 @@ func (ss *ServiceSentinel) worker() { enableNotify := ss.Services[mh.GetId()].Notify // 首次获取证书信息时,缓存证书信息 - if ss.sslCertCache[mh.GetId()] == "" { - ss.sslCertCache[mh.GetId()] = mh.Data + if ss.tlsCertCache[mh.GetId()] == "" { + ss.tlsCertCache[mh.GetId()] = mh.Data } - oldCert := strings.Split(ss.sslCertCache[mh.GetId()], "|") + oldCert := strings.Split(ss.tlsCertCache[mh.GetId()], "|") isCertChanged := false expiresOld, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", oldCert[1]) expiresNew, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", newCert[1]) @@ -539,7 +539,7 @@ func (ss *ServiceSentinel) worker() { // 证书变更时,更新缓存 if oldCert[0] != newCert[0] && !expiresNew.Equal(expiresOld) { isCertChanged = true - ss.sslCertCache[mh.GetId()] = mh.Data + ss.tlsCertCache[mh.GetId()] = mh.Data } notificationGroupID := ss.Services[mh.GetId()].NotificationGroupID @@ -552,24 +552,24 @@ func (ss *ServiceSentinel) worker() { if expiresNew.Before(time.Now().AddDate(0, 0, 7)) { expiresTimeStr := expiresNew.Format("2006-01-02 15:04:05") errMsg = Localizer.Tf( - "The SSL certificate will expire within seven days. Expiration time: %s", + "The TLS certificate will expire within seven days. Expiration time: %s", expiresTimeStr, ) // 静音规则: 服务id+证书过期时间 // 用于避免多个监测点对相同证书同时报警 - muteLabel := NotificationMuteLabel.ServiceSSL(mh.GetId(), fmt.Sprintf("expire_%s", expiresTimeStr)) - go SendNotification(notificationGroupID, fmt.Sprintf("[SSL] %s %s", serviceName, errMsg), muteLabel) + muteLabel := NotificationMuteLabel.ServiceTLS(mh.GetId(), fmt.Sprintf("expire_%s", expiresTimeStr)) + go SendNotification(notificationGroupID, fmt.Sprintf("[TLS] %s %s", serviceName, errMsg), muteLabel) } // 证书变更提醒 if isCertChanged { errMsg = Localizer.Tf( - "SSL certificate changed, old: issuer %s, expires at %s; new: issuer %s, expires at %s", + "TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, expires at %s", oldCert[0], expiresOld.Format("2006-01-02 15:04:05"), newCert[0], expiresNew.Format("2006-01-02 15:04:05")) // 证书变更后会自动更新缓存,所以不需要静音 - go SendNotification(notificationGroupID, fmt.Sprintf("[SSL] %s %s", serviceName, errMsg), nil) + go SendNotification(notificationGroupID, fmt.Sprintf("[TLS] %s %s", serviceName, errMsg), nil) } } } From d7012e36bf7516e0e7cdf5b5b447be4bf8a0d4f3 Mon Sep 17 00:00:00 2001 From: uubulb Date: Fri, 1 Nov 2024 14:36:14 +0800 Subject: [PATCH 3/4] set c.Language to zh_CN if not specified --- model/config.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/model/config.go b/model/config.go index a5fadd1..70eb0c2 100644 --- a/model/config.go +++ b/model/config.go @@ -62,6 +62,9 @@ func (c *Config) Read(path string) error { if c.ListenPort == 0 { c.ListenPort = 8008 } + if c.Language == "" { + c.Language = "zh_CN" + } if c.Location == "" { c.Location = "Asia/Shanghai" } From 40c811c143a895f179d6cdc283ba7e2c61fce330 Mon Sep 17 00:00:00 2001 From: uubulb Date: Fri, 1 Nov 2024 16:31:26 +0800 Subject: [PATCH 4/4] remove old sources --- cmd/dashboard/controller/common_page.go | 257 ---------- cmd/dashboard/controller/member_api.go | 636 ------------------------ cmd/dashboard/controller/member_page.go | 78 --- 3 files changed, 971 deletions(-) delete mode 100644 cmd/dashboard/controller/common_page.go delete mode 100644 cmd/dashboard/controller/member_api.go delete mode 100644 cmd/dashboard/controller/member_page.go diff --git a/cmd/dashboard/controller/common_page.go b/cmd/dashboard/controller/common_page.go deleted file mode 100644 index 3de8ecc..0000000 --- a/cmd/dashboard/controller/common_page.go +++ /dev/null @@ -1,257 +0,0 @@ -package controller - -import ( - "net/http" - "strconv" - "time" - - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" - "github.com/hashicorp/go-uuid" - - "github.com/naiba/nezha/model" - "github.com/naiba/nezha/pkg/utils" - "github.com/naiba/nezha/pkg/websocketx" - "github.com/naiba/nezha/proto" - "github.com/naiba/nezha/service/rpc" - "github.com/naiba/nezha/service/singleton" -) - -type commonPage struct { - r *gin.Engine -} - -func (cp *commonPage) serve() { - cr := cp.r.Group("") - // TODO: 界面直接跳转使用该接口 - cr.GET("/network/:id", cp.network) - cr.GET("/network", cp.network) - cr.GET("/file", cp.createFM) - cr.GET("/file/:id", cp.fm) -} - -func (cp *commonPage) network(c *gin.Context) { - var ( - monitorHistory *model.ServiceHistory - servers []model.Server - serverIdsWithMonitor []uint64 - monitorInfos = []byte("{}") - id uint64 - ) - if len(singleton.SortedServerList) > 0 { - id = singleton.SortedServerList[0].ID - } - if err := singleton.DB.Model(&model.ServiceHistory{}).Select("monitor_id, server_id"). - Where("monitor_id != 0 and server_id != 0").Limit(1).First(&monitorHistory).Error; err != nil { - // mygin.ShowErrorPage(c, mygin.ErrInfo{ - // Code: http.StatusForbidden, - // Title: "请求失败", - // Msg: "请求参数有误:" + "server monitor history not found", - // Link: "/", - // Btn: "返回重试", - // }, true) - return - } else { - if monitorHistory == nil || monitorHistory.ServerID == 0 { - if len(singleton.SortedServerList) > 0 { - id = singleton.SortedServerList[0].ID - } - } else { - id = monitorHistory.ServerID - } - } - - idStr := c.Param("id") - if idStr != "" { - var err error - id, err = strconv.ParseUint(idStr, 10, 64) - if err != nil { - // mygin.ShowErrorPage(c, mygin.ErrInfo{ - // Code: http.StatusForbidden, - // Title: "请求失败", - // Msg: "请求参数有误:" + err.Error(), - // Link: "/", - // Btn: "返回重试", - // }, true) - return - } - _, ok := singleton.ServerList[id] - if !ok { - // mygin.ShowErrorPage(c, mygin.ErrInfo{ - // Code: http.StatusForbidden, - // Title: "请求失败", - // Msg: "请求参数有误:" + "server id not found", - // Link: "/", - // Btn: "返回重试", - // }, true) - return - } - } - _, isMember := c.Get(model.CtxKeyAuthorizedUser) - var isViewPasswordVerfied bool - - if err := singleton.DB.Model(&model.ServiceHistory{}). - Select("distinct(server_id)"). - Where("server_id != 0"). - Find(&serverIdsWithMonitor). - Error; err != nil { - // mygin.ShowErrorPage(c, mygin.ErrInfo{ - // Code: http.StatusForbidden, - // Title: "请求失败", - // Msg: "请求参数有误:" + "no server with monitor histories", - // Link: "/", - // Btn: "返回重试", - // }, true) - return - } - if isMember || isViewPasswordVerfied { - for _, server := range singleton.SortedServerList { - for _, id := range serverIdsWithMonitor { - if server.ID == id { - servers = append(servers, *server) - } - } - } - } else { - for _, server := range singleton.SortedServerListForGuest { - for _, id := range serverIdsWithMonitor { - if server.ID == id { - servers = append(servers, *server) - } - } - } - } - serversBytes, _ := utils.Json.Marshal(model.StreamServerData{ - Now: time.Now().Unix() * 1000, - // Servers: servers, - }) - - c.HTML(http.StatusOK, "", gin.H{ - "Servers": string(serversBytes), - "MonitorInfos": string(monitorInfos), - }) -} - -func (cp *commonPage) fm(c *gin.Context) { - streamId := c.Param("id") - if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil { - // mygin.ShowErrorPage(c, mygin.ErrInfo{ - // Code: http.StatusForbidden, - // Title: "无权访问", - // Msg: "FM会话不存在", - // Link: "/", - // Btn: "返回首页", - // }, true) - return - } - defer rpc.NezhaHandlerSingleton.CloseStream(streamId) - - wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - // mygin.ShowErrorPage(c, mygin.ErrInfo{ - // Code: http.StatusInternalServerError, - // // Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{ - // // MessageID: "NetworkError", - // // }), - // Msg: "Websocket协议切换失败", - // Link: "/", - // Btn: "返回首页", - // }, true) - return - } - defer wsConn.Close() - conn := websocketx.NewConn(wsConn) - - go func() { - // PING 保活 - for { - if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil { - return - } - time.Sleep(time.Second * 10) - } - }() - - if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil { - return - } - - rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10) -} - -func (cp *commonPage) createFM(c *gin.Context) { - IdString := c.Query("id") - if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized { - // mygin.ShowErrorPage(c, mygin.ErrInfo{ - // Code: http.StatusForbidden, - // Title: "无权访问", - // Msg: "用户未登录", - // Link: "/login", - // Btn: "去登录", - // }, true) - return - } - - streamId, err := uuid.GenerateUUID() - if err != nil { - // mygin.ShowErrorPage(c, mygin.ErrInfo{ - // Code: http.StatusInternalServerError, - // // Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{ - // // MessageID: "SystemError", - // // }), - // Msg: "生成会话ID失败", - // Link: "/server", - // Btn: "返回重试", - // }, true) - return - } - - rpc.NezhaHandlerSingleton.CreateStream(streamId) - - serverId, err := strconv.Atoi(IdString) - if err != nil { - // mygin.ShowErrorPage(c, mygin.ErrInfo{ - // Code: http.StatusForbidden, - // Title: "请求失败", - // Msg: "请求参数有误:" + err.Error(), - // Link: "/server", - // Btn: "返回重试", - // }, true) - return - } - - singleton.ServerLock.RLock() - server := singleton.ServerList[uint64(serverId)] - singleton.ServerLock.RUnlock() - if server == nil { - // mygin.ShowErrorPage(c, mygin.ErrInfo{ - // Code: http.StatusForbidden, - // Title: "请求失败", - // Msg: "服务器不存在或处于离线状态", - // Link: "/server", - // Btn: "返回重试", - // }, true) - return - } - - fmData, _ := utils.Json.Marshal(&model.TaskFM{ - StreamID: streamId, - }) - if err := server.TaskStream.Send(&proto.Task{ - Type: model.TaskTypeFM, - Data: string(fmData), - }); err != nil { - // mygin.ShowErrorPage(c, mygin.ErrInfo{ - // Code: http.StatusForbidden, - // Title: "请求失败", - // Msg: "Agent信令下发失败", - // Link: "/server", - // Btn: "返回重试", - // }, true) - return - } - - c.HTML(http.StatusOK, "dashboard-", gin.H{ - "SessionID": streamId, - }) -} diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go deleted file mode 100644 index 13a7aba..0000000 --- a/cmd/dashboard/controller/member_api.go +++ /dev/null @@ -1,636 +0,0 @@ -package controller - -import ( - "bytes" - "errors" - "fmt" - "net/http" - "strconv" - "strings" - "time" - - "github.com/gin-gonic/gin" - "github.com/jinzhu/copier" - "golang.org/x/net/idna" - - "github.com/naiba/nezha/model" - "github.com/naiba/nezha/pkg/utils" - "github.com/naiba/nezha/proto" - "github.com/naiba/nezha/service/singleton" -) - -type memberAPI struct { - r gin.IRouter -} - -func (ma *memberAPI) serve() { - mr := ma.r.Group("") - // mr.Use(mygin.Authorize(mygin.AuthorizeOption{ - // MemberOnly: true, - // IsPage: false, - // Msg: "访问此接口需要登录", - // Btn: "点此登录", - // Redirect: "/login", - // })) - mr.POST("/cron", ma.addOrEditCron) - mr.GET("/cron/:id/manual", ma.manualTrigger) - mr.POST("/force-update", ma.forceUpdate) - mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup) - mr.POST("/notification", ma.addOrEditNotification) - mr.POST("/ddns", ma.addOrEditDDNS) - mr.POST("/nat", ma.addOrEditNAT) - mr.POST("/alert-rule", ma.addOrEditAlertRule) - mr.POST("/setting", ma.updateSetting) - mr.DELETE("/:model/:id", ma.delete) - mr.POST("/logout", ma.logout) -} - -func (ma *memberAPI) delete(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if id < 1 { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: "错误的 Server ID", - }) - return - } - - var err error - switch c.Param("model") { - - case "cron": - err = singleton.DB.Unscoped().Delete(&model.Cron{}, "id = ?", id).Error - if err == nil { - singleton.CronLock.RLock() - defer singleton.CronLock.RUnlock() - cr := singleton.Crons[id] - if cr != nil && cr.CronJobID != 0 { - singleton.Cron.Remove(cr.CronJobID) - } - delete(singleton.Crons, id) - } - } - if err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("数据库错误:%s", err), - }) - return - } - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - }) -} - -type cronForm struct { - ID uint64 - TaskType uint8 // 0:计划任务 1:触发任务 - Name string - Scheduler string - Command string - ServersRaw string - Cover uint8 - PushSuccessful string - NotificationTag string -} - -func (ma *memberAPI) addOrEditCron(c *gin.Context) { - var cf cronForm - var cr model.Cron - err := c.ShouldBindJSON(&cf) - if err == nil { - cr.TaskType = cf.TaskType - cr.Name = cf.Name - cr.Scheduler = cf.Scheduler - cr.Command = cf.Command - cr.ServersRaw = cf.ServersRaw - cr.PushSuccessful = cf.PushSuccessful == "on" - //cr.NotificationTag = cf.NotificationTag - cr.ID = cf.ID - cr.Cover = cf.Cover - err = utils.Json.Unmarshal([]byte(cf.ServersRaw), &cr.Servers) - } - - // 计划任务类型不得使用触发服务器执行方式 - if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger { - err = errors.New("计划任务类型不得使用触发服务器执行方式") - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("请求错误:%s", err), - }) - return - } - - tx := singleton.DB.Begin() - if err == nil { - // 保证NotificationTag不为空 - //if cr.NotificationTag == "" { - // cr.NotificationTag = "default" - //} - if cf.ID == 0 { - err = tx.Create(&cr).Error - } else { - err = tx.Save(&cr).Error - } - } - if err == nil { - err = tx.Commit().Error - } else { - tx.Rollback() - } - if err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("请求错误:%s", err), - }) - return - } - - singleton.CronLock.Lock() - defer singleton.CronLock.Unlock() - crOld := singleton.Crons[cr.ID] - if crOld != nil && crOld.CronJobID != 0 { - singleton.Cron.Remove(crOld.CronJobID) - } - - delete(singleton.Crons, cr.ID) - singleton.Crons[cr.ID] = &cr - - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - }) -} - -func (ma *memberAPI) manualTrigger(c *gin.Context) { - var cr model.Cron - if err := singleton.DB.First(&cr, "id = ?", c.Param("id")).Error; err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: err.Error(), - }) - return - } - - //singleton.ManualTrigger(cr) - - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - }) -} - -type BatchUpdateServerGroupRequest struct { - Servers []uint64 - Group string -} - -func (ma *memberAPI) batchUpdateServerGroup(c *gin.Context) { - var req BatchUpdateServerGroupRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: err.Error(), - }) - return - } - - if err := singleton.DB.Model(&model.Server{}).Where("id in (?)", req.Servers).Update("tag", req.Group).Error; err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: err.Error(), - }) - return - } - - singleton.ServerLock.Lock() - - for i := 0; i < len(req.Servers); i++ { - serverId := req.Servers[i] - var s model.Server - copier.Copy(&s, singleton.ServerList[serverId]) - // s.Tag = req.Group - // // 如果修改了Ta - // oldTag := singleton.ServerList[serverId].Tag - // newTag := s.Tag - // if newTag != oldTag { - // index := -1 - // for i := 0; i < len(singleton.ServerTagToIDList[oldTag]); i++ { - // if singleton.ServerTagToIDList[oldTag][i] == s.ID { - // index = i - // break - // } - // } - // if index > -1 { - // // 删除旧 Tag-ID 绑定关系 - // singleton.ServerTagToIDList[oldTag] = append(singleton.ServerTagToIDList[oldTag][:index], singleton.ServerTagToIDList[oldTag][index+1:]...) - // if len(singleton.ServerTagToIDList[oldTag]) == 0 { - // delete(singleton.ServerTagToIDList, oldTag) - // } - // } - // // 设置新的 Tag-ID 绑定关系 - // singleton.ServerTagToIDList[newTag] = append(singleton.ServerTagToIDList[newTag], s.ID) - // } - singleton.ServerList[s.ID] = &s - } - - singleton.ServerLock.Unlock() - - singleton.ReSortServer() - - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - }) -} - -func (ma *memberAPI) forceUpdate(c *gin.Context) { - var forceUpdateServers []uint64 - if err := c.ShouldBindJSON(&forceUpdateServers); err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: err.Error(), - }) - return - } - - var executeResult bytes.Buffer - - for i := 0; i < len(forceUpdateServers); i++ { - singleton.ServerLock.RLock() - server := singleton.ServerList[forceUpdateServers[i]] - singleton.ServerLock.RUnlock() - if server != nil && server.TaskStream != nil { - if err := server.TaskStream.Send(&proto.Task{ - Type: model.TaskTypeUpgrade, - }); err != nil { - executeResult.WriteString(fmt.Sprintf("%d 下发指令失败 %+v
", forceUpdateServers[i], err)) - } else { - executeResult.WriteString(fmt.Sprintf("%d 下发指令成功
", forceUpdateServers[i])) - } - } else { - executeResult.WriteString(fmt.Sprintf("%d 离线
", forceUpdateServers[i])) - } - } - - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - Message: executeResult.String(), - }) -} - -type notificationForm struct { - ID uint64 - Name string - URL string - RequestMethod int - RequestType int - RequestHeader string - RequestBody string - VerifySSL string - SkipCheck string -} - -func (ma *memberAPI) addOrEditNotification(c *gin.Context) { - var nf notificationForm - var n model.Notification - err := c.ShouldBindJSON(&nf) - if err == nil { - n.Name = nf.Name - n.RequestMethod = nf.RequestMethod - n.RequestType = nf.RequestType - n.RequestHeader = nf.RequestHeader - n.RequestBody = nf.RequestBody - n.URL = nf.URL - //verifySSL := nf.VerifySSL == "on" - //n.VerifySSL = &verifySSL - n.ID = nf.ID - ns := model.NotificationServerBundle{ - Notification: &n, - Server: nil, - Loc: singleton.Loc, - } - // 勾选了跳过检查 - if nf.SkipCheck != "on" { - err = ns.Send("这是测试消息") - } - } - if err == nil { - if n.ID == 0 { - err = singleton.DB.Create(&n).Error - } else { - err = singleton.DB.Save(&n).Error - } - } - if err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("请求错误:%s", err), - }) - return - } - singleton.OnRefreshOrAddNotification(&n) - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - }) -} - -type ddnsForm struct { - ID uint64 - MaxRetries uint64 - EnableIPv4 string - EnableIPv6 string - Name string - Provider string - DomainsRaw string - AccessID string - AccessSecret string - WebhookURL string - WebhookMethod uint8 - WebhookRequestType uint8 - WebhookRequestBody string - WebhookHeaders string -} - -func (ma *memberAPI) addOrEditDDNS(c *gin.Context) { - var df ddnsForm - var p model.DDNSProfile - err := c.ShouldBindJSON(&df) - if err == nil { - if df.MaxRetries < 1 || df.MaxRetries > 10 { - err = errors.New("重试次数必须为大于 1 且不超过 10 的整数") - } - } - if err == nil { - p.Name = df.Name - p.ID = df.ID - enableIPv4 := df.EnableIPv4 == "on" - enableIPv6 := df.EnableIPv6 == "on" - p.EnableIPv4 = &enableIPv4 - p.EnableIPv6 = &enableIPv6 - p.MaxRetries = df.MaxRetries - p.Provider = df.Provider - p.DomainsRaw = df.DomainsRaw - p.Domains = strings.Split(p.DomainsRaw, ",") - p.AccessID = df.AccessID - p.AccessSecret = df.AccessSecret - p.WebhookURL = df.WebhookURL - p.WebhookMethod = df.WebhookMethod - p.WebhookRequestType = df.WebhookRequestType - p.WebhookRequestBody = df.WebhookRequestBody - p.WebhookHeaders = df.WebhookHeaders - - for n, domain := range p.Domains { - // IDN to ASCII - domainValid, domainErr := idna.Lookup.ToASCII(domain) - if domainErr != nil { - err = fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr) - break - } - p.Domains[n] = domainValid - } - } - if err == nil { - if p.ID == 0 { - err = singleton.DB.Create(&p).Error - } else { - err = singleton.DB.Save(&p).Error - } - } - if err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("请求错误:%s", err), - }) - return - } - //singleton.OnDDNSUpdate() - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - }) -} - -type natForm struct { - ID uint64 - Name string - ServerID uint64 - Host string - Domain string -} - -func (ma *memberAPI) addOrEditNAT(c *gin.Context) { - var nf natForm - var n model.NAT - err := c.ShouldBindJSON(&nf) - if err == nil { - n.Name = nf.Name - n.ID = nf.ID - n.Domain = nf.Domain - n.Host = nf.Host - n.ServerID = nf.ServerID - } - if err == nil { - if n.ID == 0 { - err = singleton.DB.Create(&n).Error - } else { - err = singleton.DB.Save(&n).Error - } - } - if err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("请求错误:%s", err), - }) - return - } - //singleton.OnNATUpdate() - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - }) -} - -type alertRuleForm struct { - ID uint64 - Name string - RulesRaw string - FailTriggerTasksRaw string // 失败时触发的任务id - RecoverTriggerTasksRaw string // 恢复时触发的任务id - NotificationTag string - TriggerMode int - Enable string -} - -func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) { - var arf alertRuleForm - var r model.AlertRule - err := c.ShouldBindJSON(&arf) - if err == nil { - err = utils.Json.Unmarshal([]byte(arf.RulesRaw), &r.Rules) - } - if err == nil { - if len(r.Rules) == 0 { - err = errors.New("至少定义一条规则") - } else { - for i := 0; i < len(r.Rules); i++ { - if !r.Rules[i].IsTransferDurationRule() { - if r.Rules[i].Duration < 3 { - err = errors.New("错误:Duration 至少为 3") - break - } - } else { - if r.Rules[i].CycleInterval < 1 { - err = errors.New("错误: cycle_interval 至少为 1") - break - } - if r.Rules[i].CycleStart == nil { - err = errors.New("错误: cycle_start 未设置") - break - } - if r.Rules[i].CycleStart.After(time.Now()) { - err = errors.New("错误: cycle_start 是个未来值") - break - } - } - } - } - } - if err == nil { - r.Name = arf.Name - r.RulesRaw = arf.RulesRaw - r.FailTriggerTasksRaw = arf.FailTriggerTasksRaw - r.RecoverTriggerTasksRaw = arf.RecoverTriggerTasksRaw - //r.NotificationTag = arf.NotificationTag - enable := arf.Enable == "on" - r.TriggerMode = arf.TriggerMode - r.Enable = &enable - r.ID = arf.ID - } - if err == nil { - err = utils.Json.Unmarshal([]byte(arf.FailTriggerTasksRaw), &r.FailTriggerTasks) - } - if err == nil { - err = utils.Json.Unmarshal([]byte(arf.RecoverTriggerTasksRaw), &r.RecoverTriggerTasks) - } - //保证NotificationTag不为空 - if err == nil { - //if r.NotificationTag == "" { - // r.NotificationTag = "default" - //} - if r.ID == 0 { - err = singleton.DB.Create(&r).Error - } else { - err = singleton.DB.Save(&r).Error - } - } - if err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("请求错误:%s", err), - }) - return - } - //singleton.OnRefreshOrAddAlert(r) - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - }) -} - -type logoutForm struct { - ID uint64 -} - -func (ma *memberAPI) logout(c *gin.Context) { - admin := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User) - var lf logoutForm - if err := c.ShouldBindJSON(&lf); err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("请求错误:%s", err), - }) - return - } - if lf.ID != admin.ID { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("请求错误:%s", "用户ID不匹配"), - }) - return - } - singleton.DB.Model(admin).UpdateColumns(model.User{ - // Token: "", - // TokenExpired: time.Now(), - }) - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - }) - - // if oidcLogoutUrl := singleton.Conf.Oauth2.OidcLogoutURL; oidcLogoutUrl != "" { - // // 重定向到 OIDC 退出登录地址。不知道为什么,这里的重定向不生效 - // c.Redirect(http.StatusOK, oidcLogoutUrl) - // } -} - -type settingForm struct { - SiteName string - Language string - CustomNameservers string - IgnoredIPNotification string - IPChangeNotificationTag string // IP变更提醒的通知组 - InstallHost string - Cover uint8 - - EnableIPChangeNotification string - EnablePlainIPInNotification string -} - -func (ma *memberAPI) updateSetting(c *gin.Context) { - var sf settingForm - if err := c.ShouldBind(&sf); err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("请求错误:%s", err), - }) - return - } - - // if _, yes := model.Themes[sf.Theme]; !yes { - // c.JSON(http.StatusOK, model.Response{ - // Code: http.StatusBadRequest, - // Message: fmt.Sprintf("前台主题不存在:%s", sf.Theme), - // }) - // return - // } - - // if _, yes := model.DashboardThemes[sf.DashboardTheme]; !yes { - // c.JSON(http.StatusOK, model.Response{ - // Code: http.StatusBadRequest, - // Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme), - // }) - // return - // } - - /* - singleton.Conf.Language = sf.Language - singleton.Conf.EnableIPChangeNotification = sf.EnableIPChangeNotification == "on" - singleton.Conf.EnablePlainIPInNotification = sf.EnablePlainIPInNotification == "on" - singleton.Conf.Cover = sf.Cover - singleton.Conf.InstallHost = sf.InstallHost - singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification - singleton.Conf.IPChangeNotificationTag = sf.IPChangeNotificationTag - singleton.Conf.SiteName = sf.SiteName - singleton.Conf.DNSServers = sf.CustomNameservers - // 保证NotificationTag不为空 - if singleton.Conf.IPChangeNotificationTag == "" { - singleton.Conf.IPChangeNotificationTag = "default" - } - if err := singleton.Conf.Save(); err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("请求错误:%s", err), - }) - return - } - // 更新DNS服务器 - singleton.OnNameserverUpdate() - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - }) - */ -} diff --git a/cmd/dashboard/controller/member_page.go b/cmd/dashboard/controller/member_page.go deleted file mode 100644 index 110a069..0000000 --- a/cmd/dashboard/controller/member_page.go +++ /dev/null @@ -1,78 +0,0 @@ -package controller - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/naiba/nezha/model" - "github.com/naiba/nezha/service/singleton" -) - -type memberPage struct { - r *gin.Engine -} - -func (mp *memberPage) serve() { - mr := mp.r.Group("") - // mr.Use(mygin.Authorize(mygin.AuthorizeOption{ - // MemberOnly: true, - // IsPage: true, - // // Msg: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}), - // // Btn: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}), - // Redirect: "/login", - // })) - mr.GET("/cron", mp.cron) - mr.GET("/notification", mp.notification) - mr.GET("/ddns", mp.ddns) - mr.GET("/nat", mp.nat) - mr.GET("/setting", mp.setting) -} - -func (mp *memberPage) cron(c *gin.Context) { - var crons []model.Cron - singleton.DB.Find(&crons) - c.HTML(http.StatusOK, "dashboard-", gin.H{ - // "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ScheduledTasks"}), - "Crons": crons, - }) -} - -func (mp *memberPage) notification(c *gin.Context) { - var nf []model.Notification - singleton.DB.Find(&nf) - var ar []model.AlertRule - singleton.DB.Find(&ar) - c.HTML(http.StatusOK, "dashboard-", gin.H{ - // "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Notification"}), - "Notifications": nf, - "AlertRules": ar, - }) -} - -func (mp *memberPage) ddns(c *gin.Context) { - var data []model.DDNSProfile - singleton.DB.Find(&data) - c.HTML(http.StatusOK, "dashboard-", gin.H{ - // "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "DDNS"}), - "DDNS": data, - //"ProviderMap": model.ProviderMap, - //"ProviderList": model.ProviderList, - }) -} - -func (mp *memberPage) nat(c *gin.Context) { - var data []model.NAT - singleton.DB.Find(&data) - c.HTML(http.StatusOK, "dashboard-", gin.H{ - // "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "NAT"}), - "NAT": data, - }) -} - -func (mp *memberPage) setting(c *gin.Context) { - c.HTML(http.StatusOK, "dashboard-", gin.H{ - // "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}), - //"Languages": model.Languages, - //"DashboardThemes": model.DashboardThemes, - }) -}