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

@@ -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() // 加载定时任务