From 5114fc285400fac8f126ebc1dc85e931eae4b850 Mon Sep 17 00:00:00 2001 From: uubulb Date: Fri, 1 Nov 2024 05:07:04 +0800 Subject: [PATCH] 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() // 加载定时任务