feat: add i18n support

This commit is contained in:
uubulb 2024-11-01 05:07:04 +08:00
parent 482d787a56
commit 5114fc2854
30 changed files with 930 additions and 91 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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{

View File

@ -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

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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{

View File

@ -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

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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"`

105
pkg/i18n/i18n.go Normal file
View File

@ -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...)
}

220
pkg/i18n/template.pot Normal file
View File

@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""

Binary file not shown.

View File

@ -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 <EMAIL@ADDRESS>, 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"

Binary file not shown.

View File

@ -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 <EMAIL@ADDRESS>, 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 "故障"

View File

@ -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)

View File

@ -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),
),

View File

@ -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)

View File

@ -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)
}
}
}

80
service/singleton/i18n.go Normal file
View File

@ -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
}

View File

@ -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 ""
}

View File

@ -33,6 +33,7 @@ func InitTimezoneAndCache() {
// LoadSingleton 加载子服务并执行
func LoadSingleton() {
initI18n() // 加载本地化服务
loadNotifications() // 加载通知服务
loadServers() // 加载服务器列表
loadCronTasks() // 加载定时任务