Browse Source

Merge pull request #491 from hamid-gh98/main

[tgbot] Multi language + More...
Ho3ein 1 year ago
parent
commit
5f489c3d08

+ 8 - 8
.gitignore

@@ -1,15 +1,15 @@
 .idea
 .vscode
+.cache
+.sync*
+*.tar.gz
+access.log
+error.log
 tmp
+main
 backup/
 bin/
 dist/
-x-ui-*.tar.gz
-/x-ui
-/release.sh
-.sync*
-main
 release/
-access.log
-error.log
-.cache
+/release.sh
+/x-ui

+ 1 - 0
README.md

@@ -194,6 +194,7 @@ Reference syntax:
 | `GET`  | `"/list"`                          | Get all inbounds                            |
 | `GET`  | `"/get/:id"`                       | Get inbound with inbound.id                 |
 | `GET`  | `"/getClientTraffics/:email"`      | Get Client Traffics with email              |
+| `GET`  | `"/createbackup"`                  | Telegram bot sends backup to admins         |
 | `POST` | `"/add"`                           | Add inbound                                 |
 | `POST` | `"/del/:id"`                       | Delete Inbound                              |
 | `POST` | `"/update/:id"`                    | Update Inbound                              |

+ 1 - 0
web/assets/js/model/models.js

@@ -181,6 +181,7 @@ class AllSetting {
         this.tgRunTime = "@daily";
         this.tgBotBackup = false;
         this.tgCpu = "";
+        this.tgLang = "";
         this.xrayTemplateConfig = "";
         this.secretEnable = false;
 

+ 1 - 1
web/controller/api.go

@@ -102,5 +102,5 @@ func (a *APIController) delDepletedClients(c *gin.Context) {
 }
 
 func (a *APIController) createBackup(c *gin.Context) {
-	a.Tgbot.SendBackUP(c)
+	a.Tgbot.SendBackupToAdmins()
 }

+ 12 - 8
web/controller/base.go

@@ -2,6 +2,8 @@ package controller
 
 import (
 	"net/http"
+	"x-ui/logger"
+	"x-ui/web/locale"
 	"x-ui/web/session"
 
 	"github.com/gin-gonic/gin"
@@ -13,7 +15,7 @@ type BaseController struct {
 func (a *BaseController) checkLogin(c *gin.Context) {
 	if !session.IsLogin(c) {
 		if isAjax(c) {
-			pureJsonMsg(c, false, I18n(c, "pages.login.loginAgain"))
+			pureJsonMsg(c, false, I18nWeb(c, "pages.login.loginAgain"))
 		} else {
 			c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
 		}
@@ -23,11 +25,13 @@ func (a *BaseController) checkLogin(c *gin.Context) {
 	}
 }
 
-func I18n(c *gin.Context, name string) string {
-	anyfunc, _ := c.Get("I18n")
-	i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
-
-	message, _ := i18n(name)
-
-	return message
+func I18nWeb(c *gin.Context, name string, params ...string) string {
+	anyfunc, funcExists := c.Get("I18n")
+	if !funcExists {
+		logger.Warning("I18n function not exists in gin context!")
+		return ""
+	}
+	i18nFunc, _ := anyfunc.(func(i18nType locale.I18nType, key string, keyParams ...string) string)
+	msg := i18nFunc(locale.Web, name, params...)
+	return msg
 }

+ 16 - 16
web/controller/inbound.go

@@ -60,7 +60,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
 	user := session.GetLoginUser(c)
 	inbounds, err := a.inboundService.GetInbounds(user.Id)
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
+		jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
 		return
 	}
 	jsonObj(c, inbounds, nil)
@@ -68,12 +68,12 @@ func (a *InboundController) getInbounds(c *gin.Context) {
 func (a *InboundController) getInbound(c *gin.Context) {
 	id, err := strconv.Atoi(c.Param("id"))
 	if err != nil {
-		jsonMsg(c, I18n(c, "get"), err)
+		jsonMsg(c, I18nWeb(c, "get"), err)
 		return
 	}
 	inbound, err := a.inboundService.GetInbound(id)
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
+		jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
 		return
 	}
 	jsonObj(c, inbound, nil)
@@ -93,7 +93,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
 	inbound := &model.Inbound{}
 	err := c.ShouldBind(inbound)
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.inbounds.create"), err)
+		jsonMsg(c, I18nWeb(c, "pages.inbounds.create"), err)
 		return
 	}
 	user := session.GetLoginUser(c)
@@ -101,7 +101,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
 	inbound.Enable = true
 	inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
 	inbound, err = a.inboundService.AddInbound(inbound)
-	jsonMsgObj(c, I18n(c, "pages.inbounds.create"), inbound, err)
+	jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err)
 	if err == nil {
 		a.xrayService.SetToNeedRestart()
 	}
@@ -110,11 +110,11 @@ func (a *InboundController) addInbound(c *gin.Context) {
 func (a *InboundController) delInbound(c *gin.Context) {
 	id, err := strconv.Atoi(c.Param("id"))
 	if err != nil {
-		jsonMsg(c, I18n(c, "delete"), err)
+		jsonMsg(c, I18nWeb(c, "delete"), err)
 		return
 	}
 	err = a.inboundService.DelInbound(id)
-	jsonMsgObj(c, I18n(c, "delete"), id, err)
+	jsonMsgObj(c, I18nWeb(c, "delete"), id, err)
 	if err == nil {
 		a.xrayService.SetToNeedRestart()
 	}
@@ -123,7 +123,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
 func (a *InboundController) updateInbound(c *gin.Context) {
 	id, err := strconv.Atoi(c.Param("id"))
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
+		jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
 		return
 	}
 	inbound := &model.Inbound{
@@ -131,11 +131,11 @@ func (a *InboundController) updateInbound(c *gin.Context) {
 	}
 	err = c.ShouldBind(inbound)
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
+		jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
 		return
 	}
 	inbound, err = a.inboundService.UpdateInbound(inbound)
-	jsonMsgObj(c, I18n(c, "pages.inbounds.update"), inbound, err)
+	jsonMsgObj(c, I18nWeb(c, "pages.inbounds.update"), inbound, err)
 	if err == nil {
 		a.xrayService.SetToNeedRestart()
 	}
@@ -165,7 +165,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
 	data := &model.Inbound{}
 	err := c.ShouldBind(data)
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
+		jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
 		return
 	}
 
@@ -183,7 +183,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
 func (a *InboundController) delInboundClient(c *gin.Context) {
 	id, err := strconv.Atoi(c.Param("id"))
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
+		jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
 		return
 	}
 	clientId := c.Param("clientId")
@@ -205,7 +205,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
 	inbound := &model.Inbound{}
 	err := c.ShouldBind(inbound)
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
+		jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
 		return
 	}
 
@@ -223,7 +223,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
 func (a *InboundController) resetClientTraffic(c *gin.Context) {
 	id, err := strconv.Atoi(c.Param("id"))
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
+		jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
 		return
 	}
 	email := c.Param("email")
@@ -251,7 +251,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
 func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
 	id, err := strconv.Atoi(c.Param("id"))
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
+		jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
 		return
 	}
 
@@ -266,7 +266,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
 func (a *InboundController) delDepletedClients(c *gin.Context) {
 	id, err := strconv.Atoi(c.Param("id"))
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
+		jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
 		return
 	}
 	err = a.inboundService.DelDepletedClients(id)

+ 8 - 7
web/controller/index.go

@@ -49,26 +49,27 @@ func (a *IndexController) login(c *gin.Context) {
 	var form LoginForm
 	err := c.ShouldBind(&form)
 	if err != nil {
-		pureJsonMsg(c, false, I18n(c, "pages.login.toasts.invalidFormData"))
+		pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
 		return
 	}
 	if form.Username == "" {
-		pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyUsername"))
+		pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
 		return
 	}
 	if form.Password == "" {
-		pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword"))
+		pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
 		return
 	}
+
 	user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret)
 	timeStr := time.Now().Format("2006-01-02 15:04:05")
 	if user == nil {
-		a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
 		logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
-		pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
+		a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
+		pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
 		return
 	} else {
-		logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
+		logger.Infof("%s login success, Ip Address: %s\n", form.Username, getRemoteIp(c))
 		a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
 	}
 
@@ -86,7 +87,7 @@ func (a *IndexController) login(c *gin.Context) {
 
 	err = session.SetLoginUser(c, user)
 	logger.Info("user", user.Id, "login success")
-	jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err)
+	jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), err)
 }
 
 func (a *IndexController) logout(c *gin.Context) {

+ 2 - 2
web/controller/server.go

@@ -81,7 +81,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
 
 	versions, err := a.serverService.GetXrayVersions()
 	if err != nil {
-		jsonMsg(c, I18n(c, "getVersion"), err)
+		jsonMsg(c, I18nWeb(c, "getVersion"), err)
 		return
 	}
 
@@ -94,7 +94,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
 func (a *ServerController) installXray(c *gin.Context) {
 	version := c.Param("version")
 	err := a.serverService.UpdateXray(version)
-	jsonMsg(c, I18n(c, "install")+" xray", err)
+	jsonMsg(c, I18nWeb(c, "install")+" xray", err)
 }
 
 func (a *ServerController) stopXrayService(c *gin.Context) {

+ 15 - 15
web/controller/setting.go

@@ -49,7 +49,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
 func (a *SettingController) getAllSetting(c *gin.Context) {
 	allSetting, err := a.settingService.GetAllSetting()
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
 		return
 	}
 	jsonObj(c, allSetting, nil)
@@ -58,7 +58,7 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
 func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
 	defaultJsonConfig, err := a.settingService.GetDefaultJsonConfig()
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
 		return
 	}
 	jsonObj(c, defaultJsonConfig, nil)
@@ -67,22 +67,22 @@ func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
 func (a *SettingController) getDefaultSettings(c *gin.Context) {
 	expireDiff, err := a.settingService.GetExpireDiff()
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
 		return
 	}
 	trafficDiff, err := a.settingService.GetTrafficDiff()
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
 		return
 	}
 	defaultCert, err := a.settingService.GetCertFile()
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
 		return
 	}
 	defaultKey, err := a.settingService.GetKeyFile()
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
 		return
 	}
 	result := map[string]interface{}{
@@ -98,27 +98,27 @@ func (a *SettingController) updateSetting(c *gin.Context) {
 	allSetting := &entity.AllSetting{}
 	err := c.ShouldBind(allSetting)
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
 		return
 	}
 	err = a.settingService.UpdateAllSetting(allSetting)
-	jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
+	jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
 }
 
 func (a *SettingController) updateUser(c *gin.Context) {
 	form := &updateUserForm{}
 	err := c.ShouldBind(form)
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
 		return
 	}
 	user := session.GetLoginUser(c)
 	if user.Username != form.OldUsername || user.Password != form.OldPassword {
-		jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.originalUserPassIncorrect")))
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
 		return
 	}
 	if form.NewUsername == "" || form.NewPassword == "" {
-		jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
 		return
 	}
 	err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
@@ -127,19 +127,19 @@ func (a *SettingController) updateUser(c *gin.Context) {
 		user.Password = form.NewPassword
 		session.SetLoginUser(c, user)
 	}
-	jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
+	jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
 }
 
 func (a *SettingController) restartPanel(c *gin.Context) {
 	err := a.panelService.RestartPanel(time.Second * 3)
-	jsonMsg(c, I18n(c, "pages.settings.restartPanel"), err)
+	jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err)
 }
 
 func (a *SettingController) updateSecret(c *gin.Context) {
 	form := &updateSecretForm{}
 	err := c.ShouldBind(form)
 	if err != nil {
-		jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
 	}
 	user := session.GetLoginUser(c)
 	err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
@@ -147,7 +147,7 @@ func (a *SettingController) updateSecret(c *gin.Context) {
 		user.LoginSecret = form.LoginSecret
 		session.SetLoginUser(c, user)
 	}
-	jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
+	jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
 }
 
 func (a *SettingController) getUserSecret(c *gin.Context) {

+ 3 - 3
web/controller/util.go

@@ -38,12 +38,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
 	if err == nil {
 		m.Success = true
 		if msg != "" {
-			m.Msg = msg + I18n(c, "success")
+			m.Msg = msg + I18nWeb(c, "success")
 		}
 	} else {
 		m.Success = false
-		m.Msg = msg + I18n(c, "fail") + ": " + err.Error()
-		logger.Warning(msg+I18n(c, "fail")+": ", err)
+		m.Msg = msg + I18nWeb(c, "fail") + ": " + err.Error()
+		logger.Warning(msg+I18nWeb(c, "fail")+": ", err)
 	}
 	c.JSON(http.StatusOK, m)
 }

+ 1 - 0
web/entity/entity.go

@@ -41,6 +41,7 @@ type AllSetting struct {
 	TgRunTime          string `json:"tgRunTime" form:"tgRunTime"`
 	TgBotBackup        bool   `json:"tgBotBackup" form:"tgBotBackup"`
 	TgCpu              int    `json:"tgCpu" form:"tgCpu"`
+	TgLang             string `json:"tgLang" form:"tgLang"`
 	XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
 	TimeLocation       string `json:"timeLocation" form:"timeLocation"`
 	SecretEnable       bool   `json:"secretEnable" form:"secretEnable"`

+ 82 - 0
web/global/hashStorage.go

@@ -0,0 +1,82 @@
+package global
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"regexp"
+	"sync"
+	"time"
+)
+
+type HashEntry struct {
+	Hash      string
+	Value     string
+	Timestamp time.Time
+}
+
+type HashStorage struct {
+	sync.RWMutex
+	Data       map[string]HashEntry
+	Expiration time.Duration
+
+}
+
+func NewHashStorage(expiration time.Duration) *HashStorage {
+	return &HashStorage{
+		Data:       make(map[string]HashEntry),
+		Expiration: expiration,
+	}
+}
+
+func (h *HashStorage) SaveHash(query string) string {
+	h.Lock()
+	defer h.Unlock()
+
+	md5Hash := md5.Sum([]byte(query))
+	md5HashString := hex.EncodeToString(md5Hash[:])
+
+	entry := HashEntry{
+		Hash:      md5HashString,
+		Value:     query,
+		Timestamp: time.Now(),
+	}
+
+	h.Data[md5HashString] = entry
+
+	return md5HashString
+}
+
+
+func (h *HashStorage) GetValue(hash string) (string, bool) {
+	h.RLock()
+	defer h.RUnlock()
+
+	entry, exists := h.Data[hash]
+
+	return entry.Value, exists
+}
+
+func (h *HashStorage) IsMD5(hash string) bool {
+	match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
+	return match
+}
+
+func (h *HashStorage) RemoveExpiredHashes() {
+	h.Lock()
+	defer h.Unlock()
+
+	now := time.Now()
+
+	for hash, entry := range h.Data {
+		if now.Sub(entry.Timestamp) > h.Expiration {
+			delete(h.Data, hash)
+		}
+	}
+}
+
+func (h *HashStorage) Reset() {
+	h.Lock()
+	defer h.Unlock()
+
+	h.Data = make(map[string]HashEntry)
+}

+ 3 - 3
web/html/xui/form/tls_settings.html

@@ -10,7 +10,7 @@
             Reality
             <a-tooltip>
                 <template slot="title">
-                  <span>{{ i18n "pages.inbounds.realityDesc" }}</span>
+                    <span>{{ i18n "pages.inbounds.realityDesc" }}</span>
                 </template>
                 <a-icon type="question-circle" theme="filled"></a-icon>
             </a-tooltip>
@@ -22,7 +22,7 @@
             XTLS
             <a-tooltip>
                 <template slot="title">
-                  <span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
+                    <span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
                 </template>
                 <a-icon type="question-circle" theme="filled"></a-icon>
             </a-tooltip>
@@ -100,7 +100,7 @@
 </a-form>
 
 <!-- xtls settings -->
-<a-form v-if="inbound.xtls" layout="inline">
+<a-form v-else-if="inbound.xtls" layout="inline">
     <a-form-item label='{{ i18n "domainName" }}'>
         <a-input v-model.trim="inbound.stream.xtls.server"></a-input>
     </a-form-item>

+ 4 - 4
web/html/xui/inbounds.html

@@ -105,6 +105,10 @@
                                 </a-col>
                             </a-row>
                         </div>
+                        <a-switch v-model="enableFilter"
+                            checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}'
+                            @change="toggleFilter" style="margin-right: 10px;">
+                        </a-switch>
                         <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
                         <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
                             <a-radio-button value="">{{ i18n "none" }}</a-radio-button>
@@ -112,10 +116,6 @@
                             <a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
                             <a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
                         </a-radio-group>
-                        <a-switch v-model="enableFilter"
-                            checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}'
-                            @change="toggleFilter">
-                        </a-switch>
                         <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
                                  :data-source="searchedInbounds"
                                  :loading="spinning" :scroll="{ x: 1300 }"

+ 139 - 97
web/html/xui/settings.html

@@ -23,6 +23,34 @@
     :not(.ant-card-dark)>.ant-tabs-top-bar {
         background: white;
     }
+
+    .alert-msg {
+        color: rgb(194, 117, 18);
+        font-weight: bold;
+        font-size: 20px;
+        margin-top: 5px;
+        padding: 16px 6px;
+        text-align: center;
+        border-bottom: 1px solid;
+    }
+
+    .alert-msg > i {
+        color: inherit;
+        font-size: 24px;
+    }
+
+    .collapse-title {
+        color: inherit;
+        font-weight: bold;
+        font-size: 18px;
+        padding: 10px 20px;
+        border-bottom: 2px solid;
+    }
+
+    .collapse-title > i {
+        color: inherit;
+        font-size: 24px;
+    }
 </style>
 <body>
 <a-layout id="app" v-cloak>
@@ -35,8 +63,14 @@
                         <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
                         <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
                     </a-space>
-                    <a-tabs style="margin:1rem 0.5rem;" default-active-key="1" :class="themeSwitcher.darkCardClass" >
+                    <a-tabs style="margin:1rem 0.5rem;" default-active-key="1" :class="themeSwitcher.darkCardClass">
                         <a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings"}}'>
+                            <a-row :xs="24" :sm="24" :lg="12">
+                                <h2 class="alert-msg">
+                                    <a-icon type="warning"></a-icon>
+                                    {{ i18n "pages.settings.infoDesc" }}
+                                </h2>
+                            </a-row>
                             <a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
                                 <setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
                                 <setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item>
@@ -72,12 +106,6 @@
                                     </a-row>
                                 </a-list-item>
                             </a-list>
-                            <a-row :xs="24" :sm="24" :lg="12">
-                                <h2 style="color: inherit; font-weight: bold; font-size: 16px; padding: 5px 5px; text-align: center;">
-                                    <a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
-                                    {{ i18n "pages.settings.infoDesc" }}
-                                </h2>
-                            </a-row>
                         </a-tab-pane>
                         <a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding: 20px;">
                             <a-tabs class="ant-card-dark-securitybox-nohover" default-active-key="sec-1" :class="themeSwitcher.darkCardClass">
@@ -144,8 +172,8 @@
                                 </a-space>
                                 <a-divider style="padding: 20px;">{{ i18n "pages.settings.templates.title"}} </a-divider>
                                 <a-row :xs="24" :sm="24" :lg="12">
-                                    <h2 style="color: inherit; font-weight: bold; font-size: 16px; padding: 5px 5px; text-align: center;">
-                                        <a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
+                                    <h2 class="alert-msg">
+                                        <a-icon type="warning"></a-icon>
                                         {{ i18n "pages.settings.infoDesc" }}
                                     </h2>
                                 </a-row>
@@ -154,8 +182,8 @@
                                         <a-collapse>
                                             <a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'>
                                                 <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
-                                                        <a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
+                                                    <h2 class="collapse-title">
+                                                        <a-icon type="warning"></a-icon>
                                                         {{ i18n "pages.settings.templates.generalConfigsDesc" }}
                                                     </h2>
                                                 </a-row>
@@ -199,8 +227,8 @@
                                             </a-collapse-panel>
                                             <a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'>
                                                 <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
-                                                        <a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
+                                                    <h2 class="collapse-title">
+                                                        <a-icon type="warning"></a-icon>
                                                         {{ i18n "pages.settings.templates.blockConfigsDesc" }}
                                                     </h2>
                                                 </a-row>
@@ -212,8 +240,8 @@
                                             </a-collapse-panel>
                                             <a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'>
                                                 <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
-                                                        <a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
+                                                    <h2 class="collapse-title">
+                                                        <a-icon type="warning"></a-icon>
                                                         {{ i18n "pages.settings.templates.blockCountryConfigsDesc" }}
                                                     </h2>
                                                 </a-row>
@@ -226,8 +254,8 @@
                                             </a-collapse-panel>
                                             <a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'>
                                                 <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
-                                                        <a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
+                                                    <h2 class="collapse-title">
+                                                        <a-icon type="warning"></a-icon>
                                                         {{ i18n "pages.settings.templates.directCountryConfigsDesc" }}
                                                     </h2>
                                                 </a-row>
@@ -240,8 +268,8 @@
                                             </a-collapse-panel>
                                             <a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'>
                                                 <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
-                                                        <a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
+                                                    <h2 class="collapse-title">
+                                                        <a-icon type="warning"></a-icon>
                                                         {{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}
                                                     </h2>
                                                 </a-row>
@@ -250,8 +278,8 @@
                                             </a-collapse-panel>
                                             <a-collapse-panel header='{{ i18n "pages.settings.templates.warpConfigs"}}'>
                                                 <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
-                                                        <a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
+                                                    <h2 class="collapse-title">
+                                                        <a-icon type="warning"></a-icon>
                                                         {{ i18n "pages.settings.templates.warpConfigsDesc" }}
                                                     </h2>
                                                 </a-row>
@@ -262,8 +290,8 @@
                                             </a-collapse-panel>
                                             <a-collapse-panel header='{{ i18n "pages.settings.templates.manualLists"}}'>
                                                 <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
-                                                        <a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
+                                                    <h2 class="collapse-title">
+                                                        <a-icon type="warning"></a-icon>
                                                         {{ i18n "pages.settings.templates.manualListsDesc" }}
                                                     </h2>
                                                 </a-row>
@@ -271,6 +299,8 @@
                                                 <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedDomains"}}' v-model="manualBlockedDomains"></setting-list-item>
                                                 <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectIPs"}}' v-model="manualDirectIPs"></setting-list-item>
                                                 <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectDomains"}}' v-model="manualDirectDomains"></setting-list-item>
+                                                <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualIPv4Domains"}}' v-model="manualIPv4Domains"></setting-list-item>
+                                                <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualWARPDomains"}}' v-model="manualWARPDomains"></setting-list-item>
                                             </a-collapse-panel>
                                         </a-collapse>
                                     </a-tab-pane>
@@ -295,6 +325,12 @@
                         </a-tab-pane>
 
                         <a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
+                            <a-row :xs="24" :sm="24" :lg="12">
+                                <h2 class="alert-msg">
+                                    <a-icon type="warning"></a-icon>
+                                    {{ i18n "pages.settings.infoDesc" }}
+                                </h2>
+                            </a-row>
                             <a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
                                 <setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
                                 <setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
@@ -302,13 +338,30 @@
                                 <setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
                                 <setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
                                 <setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
+                                <a-list-item>
+                                    <a-row style="padding: 20px">
+                                        <a-col :lg="24" :xl="12">
+                                            <a-list-item-meta title="Telegram Bot Language" />
+                                        </a-col>
+
+                                        <a-col :lg="24" :xl="12">
+                                            <template>
+                                                <a-select
+                                                    ref="selectBotLang"
+                                                    v-model="allSetting.tgLang"
+                                                    :dropdown-class-name="themeSwitcher.darkCardClass"
+                                                    style="width: 100%"
+                                                >
+                                                    <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
+                                                        <span role="img" aria-label="l.name" v-text="l.icon"></span>
+                                                        &nbsp;&nbsp;<span v-text="l.name"></span>
+                                                    </a-select-option>
+                                                </a-select>
+                                            </template>
+                                        </a-col>
+                                    </a-row>
+                                </a-list-item>
                             </a-list>
-                            <a-row :xs="24" :sm="24" :lg="12">
-                                <h2 style="color: inherit; font-weight: bold; font-size: 16px; padding: 5px 5px; text-align: center;">
-                                    <a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
-                                    {{ i18n "pages.settings.infoDesc" }}
-                                </h2>
-                            </a-row>
                         </a-tab-pane>
                     </a-tabs>
                 </a-space>
@@ -452,7 +505,12 @@
                 if (msg.success) {
                     this.loading(true);
                     await PromiseUtil.sleep(5000);
-                    window.location.replace(this.allSetting.webBasePath + "panel/settings");
+                    let protocol = "http://";
+                    if (this.allSetting.webCertFile !== "") {
+                        protocol = "https://";
+                    }
+                    const { host, pathname } = window.location;
+                    window.location.replace(protocol + host + this.allSetting.webBasePath + pathname.slice(1));
                 }
             },
             async fetchUserSecret() {
@@ -584,30 +642,30 @@
         computed: {
             templateSettings: {
                 get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; },
-                set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
+                set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2); },
             },
             inboundSettings: {
                 get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
                 set: function (newValue) {
                     newTemplateSettings = this.templateSettings;
-                    newTemplateSettings.inbounds = JSON.parse(newValue)
-                    this.templateSettings = newTemplateSettings
+                    newTemplateSettings.inbounds = JSON.parse(newValue);
+                    this.templateSettings = newTemplateSettings;
                 },
             },
             outboundSettings: {
                 get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
                 set: function (newValue) {
                     newTemplateSettings = this.templateSettings;
-                    newTemplateSettings.outbounds = JSON.parse(newValue)
-                    this.templateSettings = newTemplateSettings
+                    newTemplateSettings.outbounds = JSON.parse(newValue);
+                    this.templateSettings = newTemplateSettings;
                 },
             },
             routingRuleSettings: {
                 get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
                 set: function (newValue) {
                     newTemplateSettings = this.templateSettings;
-                    newTemplateSettings.routing.rules = JSON.parse(newValue)
-                    this.templateSettings = newTemplateSettings
+                    newTemplateSettings.routing.rules = JSON.parse(newValue);
+                    this.templateSettings = newTemplateSettings;
                 },
             },
             freedomStrategy: {
@@ -682,6 +740,24 @@
                     this.syncRulesWithOutbound("direct", this.directSettings);
                 }
             },
+            ipv4Domains: {
+                get: function () {
+                    return this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
+                },
+                set: function (newValue) {
+                    this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue });
+                    this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
+                }
+            },
+            warpDomains: {
+                get: function () {
+                    return this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
+                },
+                set: function (newValue) {
+                    this.templateRuleSetter({ outboundTag: "WARP", property: "domain", data: newValue });
+                    this.syncRulesWithOutbound("WARP", this.warpSettings);
+                }
+            },
             manualBlockedIPs: {
                 get: function () { return JSON.stringify(this.blockedIPs, null, 2); },
                 set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000)
@@ -698,6 +774,14 @@
                 get: function () { return JSON.stringify(this.directDomains, null, 2); },
                 set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000)
             },
+            manualIPv4Domains: {
+                get: function () { return JSON.stringify(this.ipv4Domains, null, 2); },
+                set: debounce(function (value) { this.ipv4Domains = JSON.parse(value); }, 1000)
+            },
+            manualWARPDomains: {
+                get: function () { return JSON.stringify(this.warpDomains, null, 2); },
+                set: debounce(function (value) { this.warpDomains = JSON.parse(value); }, 1000)
+            },
             torrentSettings: {
                 get: function () {
                     return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
@@ -763,40 +847,26 @@
             },
             GoogleIPv4Settings: {
                 get: function () {
-                    return doAllItemsExist(this.settingsData.domains.google, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }));
+                    return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains);
                 },
                 set: function (newValue) {
-                    oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
                     if (newValue) {
-                        oldData = [...oldData, ...this.settingsData.domains.google];
+                        this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google];
                     } else {
-                        oldData = oldData.filter(data => !this.settingsData.domains.google.includes(data))
+                        this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data));
                     }
-                    this.templateRuleSetter({
-                        outboundTag: "IPv4",
-                        property: "domain",
-                        data: oldData
-                    });
-                    this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
                 },
             },
             NetflixIPv4Settings: {
                 get: function () {
-                    return doAllItemsExist(this.settingsData.domains.netflix, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }));
+                    return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains);
                 },
                 set: function (newValue) {
-                    oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
                     if (newValue) {
-                        oldData = [...oldData, ...this.settingsData.domains.netflix];
+                        this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix];
                     } else {
-                        oldData = oldData.filter(data => !this.settingsData.domains.netflix.includes(data))
+                        this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data));
                     }
-                    this.templateRuleSetter({
-                        outboundTag: "IPv4",
-                        property: "domain",
-                        data: oldData
-                    });
-                    this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
                 },
             },
             IRIpSettings: {
@@ -945,78 +1015,50 @@
             },
             GoogleWARPSettings: {
                 get: function () {
-                    return doAllItemsExist(this.settingsData.domains.google, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }));
+                    return doAllItemsExist(this.settingsData.domains.google, this.warpDomains);
                 },
                 set: function (newValue) {
-                    oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
                     if (newValue) {
-                        oldData = [...oldData, ...this.settingsData.domains.google];
+                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.google];
                     } else {
-                        oldData = oldData.filter(data => !this.settingsData.domains.google.includes(data))
+                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.google.includes(data));
                     }
-                    this.templateRuleSetter({
-                    outboundTag: "WARP",
-                    property: "domain",
-                    data: oldData
-                    });
-                    this.syncRulesWithOutbound("WARP", this.warpSettings);
                 },
             },
             OpenAIWARPSettings: {
                 get: function () {
-                    return doAllItemsExist(this.settingsData.domains.openai, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }));
+                    return doAllItemsExist(this.settingsData.domains.openai, this.warpDomains);
                 },
                 set: function (newValue) {
-                    oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
                     if (newValue) {
-                        oldData = [...oldData, ...this.settingsData.domains.openai];
+                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.openai];
                     } else {
-                        oldData = oldData.filter(data => !this.settingsData.domains.openai.includes(data))
+                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.openai.includes(data));
                     }
-                    this.templateRuleSetter({
-                    outboundTag: "WARP",
-                    property: "domain",
-                    data: oldData
-                    });
-                    this.syncRulesWithOutbound("WARP", this.warpSettings);
                 },
             },
             NetflixWARPSettings: {
                 get: function () {
-                    return doAllItemsExist(this.settingsData.domains.netflix, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }));
+                    return doAllItemsExist(this.settingsData.domains.netflix, this.warpDomains);
                 },
                 set: function (newValue) {
-                    oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
                     if (newValue) {
-                        oldData = [...oldData, ...this.settingsData.domains.netflix];
+                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.netflix];
                     } else {
-                        oldData = oldData.filter(data => !this.settingsData.domains.netflix.includes(data))
+                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.netflix.includes(data));
                     }
-                    this.templateRuleSetter({
-                    outboundTag: "WARP",
-                    property: "domain",
-                    data: oldData
-                    });
-                    this.syncRulesWithOutbound("WARP", this.warpSettings);
                 },
             },
             SpotifyWARPSettings: {
                 get: function () {
-                    return doAllItemsExist(this.settingsData.domains.spotify, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }));
+                    return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains);
                 },
                 set: function (newValue) {
-                    oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
                     if (newValue) {
-                        oldData = [...oldData, ...this.settingsData.domains.spotify];
+                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.spotify];
                     } else {
-                        oldData = oldData.filter(data => !this.settingsData.domains.spotify.includes(data))
+                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.spotify.includes(data));
                     }
-                    this.templateRuleSetter({
-                    outboundTag: "WARP",
-                    property: "domain",
-                    data: oldData
-                    });
-                    this.syncRulesWithOutbound("WARP", this.warpSettings);
                 },
             },
         },

+ 5 - 2
web/job/check_cpu_usage.go

@@ -1,7 +1,7 @@
 package job
 
 import (
-	"fmt"
+	"strconv"
 	"time"
 	"x-ui/web/service"
 
@@ -24,7 +24,10 @@ func (j *CheckCpuJob) Run() {
 	// get latest status of server
 	percent, err := cpu.Percent(1*time.Second, false)
 	if err == nil && percent[0] > float64(threshold) {
-		msg := fmt.Sprintf("🔴 CPU usage %.2f%% is more than threshold %d%%", percent[0], threshold)
+		msg := j.tgbotService.I18nBot("tgbot.messages.cpuThreshold",
+			"Percent=="+strconv.FormatFloat(percent[0], 'f', 2, 64),
+			"Threshold=="+strconv.Itoa(threshold))
+
 		j.tgbotService.SendMsgToTgbotAdmins(msg)
 	}
 }

+ 19 - 0
web/job/check_hash_storage.go

@@ -0,0 +1,19 @@
+package job
+
+import (
+	"x-ui/web/service"
+)
+
+type CheckHashStorageJob struct {
+	tgbotService service.Tgbot
+}
+
+func NewCheckHashStorageJob() *CheckHashStorageJob {
+	return new(CheckHashStorageJob)
+}
+
+// Here Run is an interface method of the Job interface
+func (j *CheckHashStorageJob) Run() {
+	// Remove expired hashes from storage
+	j.tgbotService.GetHashStorage().RemoveExpiredHashes()
+}

+ 144 - 0
web/locale/locale.go

@@ -0,0 +1,144 @@
+package locale
+
+import (
+	"embed"
+	"io/fs"
+	"strings"
+	"x-ui/logger"
+
+	"github.com/gin-gonic/gin"
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+	"github.com/pelletier/go-toml/v2"
+	"golang.org/x/text/language"
+)
+
+var i18nBundle *i18n.Bundle
+var LocalizerWeb *i18n.Localizer
+var LocalizerBot *i18n.Localizer
+
+type I18nType string
+
+const (
+	Bot I18nType = "bot"
+	Web I18nType = "web"
+)
+
+type SettingService interface {
+	GetTgLang() (string, error)
+}
+
+func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
+	// set default bundle to english
+	i18nBundle = i18n.NewBundle(language.English)
+	i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
+
+	// parse files
+	if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil {
+		return err
+	}
+
+	// setup bot locale
+	if err := initTGBotLocalizer(settingService); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func createTemplateData(params []string, seperator ...string) map[string]interface{} {
+	var sep string = "=="
+	if len(seperator) > 0 {
+		sep = seperator[0]
+	}
+
+	templateData := make(map[string]interface{})
+	for _, param := range params {
+		parts := strings.SplitN(param, sep, 2)
+		templateData[parts[0]] = parts[1]
+	}
+
+	return templateData
+}
+
+func I18n(i18nType I18nType, key string, params ...string) string {
+	var localizer *i18n.Localizer
+
+	switch i18nType {
+	case "bot":
+		localizer = LocalizerBot
+	case "web":
+		localizer = LocalizerWeb
+	default:
+		logger.Errorf("Invalid type for I18n: %s", i18nType)
+		return ""
+	}
+
+	templateData := createTemplateData(params)
+
+	msg, err := localizer.Localize(&i18n.LocalizeConfig{
+		MessageID:    key,
+		TemplateData: templateData,
+	})
+
+	if err != nil {
+		logger.Errorf("Failed to localize message: %v", err)
+		return ""
+	}
+
+	return msg
+}
+
+func initTGBotLocalizer(settingService SettingService) error {
+	botLang, err := settingService.GetTgLang()
+	if err != nil {
+		return err
+	}
+
+	LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang)
+	return nil
+}
+
+func LocalizerMiddleware() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		var lang string
+
+		if cookie, err := c.Request.Cookie("lang"); err == nil {
+			lang = cookie.Value
+		} else {
+			lang = c.GetHeader("Accept-Language")
+		}
+
+		LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang)
+
+		c.Set("localizer", LocalizerWeb)
+		c.Set("I18n", I18n)
+		c.Next()
+	}
+}
+
+func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
+	err := fs.WalkDir(i18nFS, "translation",
+		func(path string, d fs.DirEntry, err error) error {
+			if err != nil {
+				return err
+			}
+
+			if d.IsDir() {
+				return nil
+			}
+
+			data, err := i18nFS.ReadFile(path)
+			if err != nil {
+				return err
+			}
+
+			_, err = i18nBundle.ParseMessageFileBytes(data, path)
+			return err
+		})
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 0 - 0
web/network/autp_https_conn.go → web/network/auto_https_conn.go


+ 5 - 0
web/service/setting.go

@@ -39,6 +39,7 @@ var defaultValueMap = map[string]string{
 	"tgRunTime":          "@daily",
 	"tgBotBackup":        "false",
 	"tgCpu":              "0",
+	"tgLang":             "en-US",
 	"secretEnable":       "false",
 }
 
@@ -256,6 +257,10 @@ func (s *SettingService) GetTgCpu() (int, error) {
 	return s.getInt("tgCpu")
 }
 
+func (s *SettingService) GetTgLang() (string, error) {
+	return s.getString("tgLang")
+}
+
 func (s *SettingService) GetPort() (int, error) {
 	return s.getInt("webPort")
 }

+ 2 - 1
web/service/sub.go

@@ -603,7 +603,8 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
 		}
 	}
 	encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
-	return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, clients[clientIndex].Email)
+	remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email)
+	return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, remark)
 }
 
 func searchKey(data interface{}, key string) (interface{}, bool) {

File diff suppressed because it is too large
+ 321 - 199
web/service/tgbot.go


+ 115 - 1
web/translation/translate.en_US.toml

@@ -209,7 +209,7 @@
 [pages.settings]
 "title" = "Settings"
 "save" = "Save"
-"infoDesc" = "Every change made here needs to be saved. Please restart the panel for the changes to take effect."
+"infoDesc" = "Every change made here needs to be saved. Please restart the panel to apply changes."
 "restartPanel" = "Restart Panel "
 "restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server."
 "actions" = "Actions"
@@ -336,6 +336,8 @@
 "manualBlockedDomains" = "List of Blocked Domains"
 "manualDirectIPs" = "List of Direct IPs"
 "manualDirectDomains" = "List of Direct Domains"
+"manualIPv4Domains" = "List of IPv4 Domains"
+"manualWARPDomains" = "List of WARP Domains"
 
 [pages.settings.security]
 "admin" = "Admin"
@@ -351,3 +353,115 @@
 "modifyUser" = "Modify User "
 "originalUserPassIncorrect" = "Incorrect original username or password"
 "userPassMustBeNotEmpty" = "New username and new password cannot be empty"
+
+[tgbot]
+"keyboardClosed" = "❌ Custom keyboard closed!"
+"noResult" = "❗ No result!"
+"noQuery" = "❌ Query not found! Please use the command again!"
+"wentWrong" = "❌ Something went wrong!"
+"noIpRecord" = "❗ No IP Record!"
+"noInbounds" = "❗ No inbound found!"
+"unlimited" = "♾ Unlimited"
+"month" = "Month"
+"months" = "Months"
+"day" = "Day"
+"days" = "Days"
+"unknown" = "Unknown"
+"inbounds" = "Inbounds"
+"clients" = "Clients"
+
+[tgbot.commands]
+"unknown" = "❗ Unknown command"
+"pleaseChoose" = "👇 Please choose:\r\n"
+"help" = "🤖 Welcome to this bot! It's designed to offer you specific data from the server, and it allows you to make modifications as needed.\r\n\r\n"
+"start" = "👋 Hello <i>{{ .Firstname }}</i>.\r\n"
+"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
+"status" = "✅ Bot is ok!"
+"usage" = "❗ Please provide a text to search!"
+"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
+"helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
+
+[tgbot.messages]
+"cpuThreshold" = "🔴 The CPU usage {{ .Percent }}% is more than threshold {{ .Threshold }}%"
+"selectUserFailed" = "❌ Error in user selection!"
+"userSaved" = "✅ Telegram User saved."
+"loginSuccess" = "✅ Successfully logged-in to the panel.\r\n"
+"loginFailed" = "❗️ Login to the panel failed.\r\n"
+"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
+"datetime" = "⏰ Date-Time: {{ .DateTime }}\r\n"
+"hostname" = "💻 Hostname: {{ .Hostname }}\r\n"
+"version" = "🚀 X-UI Version: {{ .Version }}\r\n"
+"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
+"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
+"ip" = "🌐 IP: {{ .IP }}\r\n"
+"ips" = "🔢 IPs: \r\n{{ .IPs }}\r\n"
+"serverUpTime" = "⏳ Server Uptime: {{ .UpTime }} {{ .Unit }}\r\n"
+"serverLoad" = "📈 Server Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
+"serverMemory" = "📋 Server Memory: {{ .Current }}/{{ .Total }}\r\n"
+"tcpCount" = "🔹 TcpCount: {{ .Count }}\r\n"
+"udpCount" = "🔸 UdpCount: {{ .Count }}\r\n"
+"traffic" = "🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
+"xrayStatus" = "ℹ️ Xray Status: {{ .State }}\r\n"
+"username" = "👤 Username: {{ .Username }}\r\n"
+"time" = "⏰ Time: {{ .Time }}\r\n"
+"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
+"port" = "🔌 Port: {{ .Port }}\r\n"
+"expire" = "📅 Expire Date: {{ .DateTime }}\r\n \r\n"
+"expireIn" = "📅 Expire In: {{ .Time }}\r\n \r\n"
+"active" = "💡 Active: {{ .Enable }}\r\n"
+"email" = "📧 Email: {{ .Email }}\r\n"
+"upload" = "🔼 Upload↑: {{ .Upload }}\r\n"
+"download" = "🔽 Download↓: {{ .Download }}\r\n"
+"total" = "🔄 Total: {{ .UpDown }} / {{ .Total }}\r\n"
+"TGUser" = "👤 Telegram User: {{ .TelegramID }}\r\n"
+"exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n"
+"exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n"
+"disabled" = "🛑 Disabled: {{ .Disabled }}\r\n"
+"depleteSoon" = "🔜 Deplete soon: {{ .Deplete }}\r\n \r\n"
+"backupTime" = "🗄 Backup Time: {{ .Time }}\r\n"
+"refreshedOn" = "🔄🕒 Refreshed On: {{ .Time }}\r\n"
+
+[tgbot.buttons]
+"closeKeyboard" = "❌ Close Keyboard"
+"cancel" = "❌ Cancel"
+"cancelReset" = "❌ Cancel Reset"
+"cancelIpLimit" = "❌ Cancel IP Limit"
+"confirmResetTraffic" = "✅ Confirm Reset Traffic?"
+"confirmClearIps" = "✅ Confirm Clear IPs?"
+"confirmRemoveTGUser" = "✅ Confirm Remove Telegram User?"
+"dbBackup" = "Get DB Backup"
+"serverUsage" = "Server Usage"
+"getInbounds" = "Get Inbounds"
+"depleteSoon" = "Deplete soon"
+"clientUsage" = "Get Usage"
+"commands" = "Commands"
+"refresh" = "🔄 Refresh"
+"clearIPs" = "❌ Clear IPs"
+"removeTGUser" = "❌ Remove Telegram User"
+"selectTGUser" = "👤 Select Telegram User"
+"selectOneTGUser" = "👤 Select a telegram user:"
+"resetTraffic" = "📈 Reset Traffic"
+"resetExpire" = "📅 Reset Expire Days"
+"ipLog" = "🔢 IP Log"
+"ipLimit" = "🔢 IP Limit"
+"setTGUser" = "👤 Set Telegram User"
+"toggle" = "🔘 Enable / Disable"
+
+[tgbot.answers]
+"errorOperation" = "❗ Error in Operation."
+"getInboundsFailed" = "❌ Failed to get inbounds"
+"canceled" = "❌ {{ .Email }} : Operation canceled."
+"clientRefreshSuccess" = "✅ {{ .Email }} : Client refreshed successfully."
+"IpRefreshSuccess" = "✅ {{ .Email }} : IPs refreshed successfully."
+"TGIdRefreshSuccess" = "✅ {{ .Email }} : Client's Telegram User refreshed successfully."
+"resetTrafficSuccess" = "✅ {{ .Email }} : Traffic reset successfully."
+"expireResetSuccess" = "✅ {{ .Email }} : Expire days reset successfully."
+"resetIpSuccess" = "✅ {{ .Email }} : IP limit {{ .Count }} saved successfully."
+"clearIpSuccess" = "✅ {{ .Email }} : IPs cleared successfully."
+"getIpLog" = "✅ {{ .Email }} : Get IP Log."
+"getUserInfo" = "✅ {{ .Email }} : Get Telegram User Info."
+"removedTGUserSuccess" = "✅ {{ .Email }} : Telegram User removed successfully."
+"enableSuccess" = "✅ {{ .Email }} : Enabled successfully."
+"disableSuccess" = "✅ {{ .Email }} : Disabled successfully."
+"askToAddUserId" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram user id in your configuration(s).\r\n\r\nYour user id: <b>{{ .TgUserID }}</b>"
+"askToAddUserName" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram username or user id in your configuration(s).\r\n\r\nYour username: <b>@{{ .TgUserName }}</b>\r\n\r\nYour user id: <b>{{ .TgUserID }}</b>"

+ 114 - 0
web/translation/translate.fa_IR.toml

@@ -336,6 +336,8 @@
 "manualBlockedDomains" = "لیست دامنه های مسدود شده"
 "manualDirectIPs" = "لیست آی‌پی های مستقیم"
 "manualDirectDomains" = "لیست دامنه های مستقیم"
+"manualIPv4Domains" = "لیست دامنه‌های IPv4"
+"manualWARPDomains" = "لیست دامنه های WARP"
 
 [pages.settings.security]
 "admin" = "مدیر"
@@ -351,3 +353,115 @@
 "modifyUser" = "ویرایش کاربر"
 "originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد "
 "userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد "
+
+[tgbot]
+"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!"
+"noResult" = "❗ نتیجه‌ای یافت نشد!"
+"noQuery" = "❌ کوئری یافت نشد! لطفاً دستور را مجدداً استفاده کنید!"
+"wentWrong" = "❌ مشکلی رخ داده است!"
+"noIpRecord" = "❗ رکورد IP یافت نشد!"
+"noInbounds" = "❗ هیچ ورودی یافت نشد!"
+"unlimited" = "♾ نامحدود"
+"month" = "ماه"
+"months" = "ماه‌ها"
+"day" = "روز"
+"days" = "روزها"
+"unknown" = "نامشخص"
+"inbounds" = "ورودی‌ها"
+"clients" = "کلاینت‌ها"
+
+[tgbot.commands]
+"unknown" = "❗ دستور ناشناخته"
+"pleaseChoose" = "👇 لطفاً انتخاب کنید:\r\n"
+"help" = "🤖 به این ربات خوش آمدید! این ربات برای ارائه داده‌های خاص از سرور طراحی شده است و به شما امکان تغییرات لازم را می‌دهد.\r\n\r\n"
+"start" = "👋 سلام <i>{{ .Firstname }}</i>.\r\n"
+"welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n"
+"status" = "✅ ربات در حالت عادی است!"
+"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
+"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
+"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید."
+
+[tgbot.messages]
+"cpuThreshold" = "🔴 میزان استفاده از CPU {{ .Percent }}% بیشتر از آستانه {{ .Threshold }}% است."
+"selectUserFailed" = "❌ خطا در انتخاب کاربر!"
+"userSaved" = "✅ کاربر تلگرام ذخیره شد."
+"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
+"loginFailed" = "❗️ ورود به پنل ناموفق بود.\r\n"
+"report" = "🕰 گزارشات زمان‌بندی شده: {{ .RunTime }}\r\n"
+"datetime" = "⏰ تاریخ-زمان: {{ .DateTime }}\r\n"
+"hostname" = "💻 نام میزبان: {{ .Hostname }}\r\n"
+"version" = "🚀 نسخه X-UI: {{ .Version }}\r\n"
+"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
+"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
+"ip" = "🌐 آدرس IP: {{ .IP }}\r\n"
+"ips" = "🔢 آدرس‌های IP: \r\n{{ .IPs }}\r\n"
+"serverUpTime" = "⏳ زمان کارکرد سرور: {{ .UpTime }} {{ .Unit }}\r\n"
+"serverLoad" = "📈 بار سرور: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
+"serverMemory" = "📋 حافظه سرور: {{ .Current }}/{{ .Total }}\r\n"
+"tcpCount" = "🔹 تعداد ترافیک TCP: {{ .Count }}\r\n"
+"udpCount" = "🔸 تعداد ترافیک UDP: {{ .Count }}\r\n"
+"traffic" = "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
+"xrayStatus" = "ℹ️ وضعیت Xray: {{ .State }}\r\n"
+"username" = "👤 نام کاربری: {{ .Username }}\r\n"
+"time" = "⏰ زمان: {{ .Time }}\r\n"
+"inbound" = "📍 ورودی: {{ .Remark }}\r\n"
+"port" = "🔌 پورت: {{ .Port }}\r\n"
+"expire" = "📅 تاریخ انقضا: {{ .DateTime }}\r\n \r\n"
+"expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n \r\n"
+"active" = "💡 فعال: {{ .Enable }}\r\n"
+"email" = "📧 ایمیل: {{ .Email }}\r\n"
+"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
+"download" = "🔽 دانلود↓: {{ .Download }}\r\n"
+"total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n"
+"TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n"
+"exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n"
+"exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n"
+"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
+"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n"
+"backupTime" = "🗄 زمان پشتیبان‌گیری: {{ .Time }}\r\n"
+"refreshedOn" = "🔄🕒 تازه‌سازی شده در: {{ .Time }}\r\n"
+
+[tgbot.buttons]
+"closeKeyboard" = "❌ بستن کیبورد"
+"cancel" = "❌ لغو"
+"cancelReset" = "❌ لغو تنظیم مجدد"
+"cancelIpLimit" = "❌ لغو محدودیت IP"
+"confirmResetTraffic" = "✅ تأیید تنظیم مجدد ترافیک؟"
+"confirmClearIps" = "✅ تأیید پاک‌سازی آدرس‌های IP؟"
+"confirmRemoveTGUser" = "✅ تأیید حذف کاربر تلگرام؟"
+"dbBackup" = "دریافت پشتیبان پایگاه داده"
+"serverUsage" = "استفاده از سرور"
+"getInbounds" = "دریافت ورودی‌ها"
+"depleteSoon" = "به زودی به پایان خواهد رسید"
+"clientUsage" = "دریافت آمار کاربر"
+"commands" = "دستورات"
+"refresh" = "🔄 تازه‌سازی"
+"clearIPs" = "❌ پاک‌سازی آدرس‌ها"
+"removeTGUser" = "❌ حذف کاربر تلگرام"
+"selectTGUser" = "👤 انتخاب کاربر تلگرام"
+"selectOneTGUser" = "👤 یک کاربر تلگرام را انتخاب کنید:"
+"resetTraffic" = "📈 تنظیم مجدد ترافیک"
+"resetExpire" = "📅 تنظیم مجدد تاریخ انقضا"
+"ipLog" = "🔢 لاگ آدرس‌های IP"
+"ipLimit" = "🔢 محدودیت IP"
+"setTGUser" = "👤 تنظیم کاربر تلگرام"
+"toggle" = "🔘 فعال / غیرفعال"
+
+[tgbot.answers]
+"errorOperation" = "❗ خطا در عملیات."
+"getInboundsFailed" = "❌ دریافت ورودی‌ها با خطا مواجه شد."
+"canceled" = "❌ {{ .Email }} : عملیات لغو شد."
+"clientRefreshSuccess" = "✅ {{ .Email }} : کلاینت با موفقیت تازه‌سازی شد."
+"IpRefreshSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت تازه‌سازی شدند."
+"TGIdRefreshSuccess" = "✅ {{ .Email }} : کاربر تلگرام کلاینت با موفقیت تازه‌سازی شد."
+"resetTrafficSuccess" = "✅ {{ .Email }} : ترافیک با موفقیت تنظیم مجدد شد."
+"expireResetSuccess" = "✅ {{ .Email }} : تاریخ انقضا با موفقیت تنظیم مجدد شد."
+"resetIpSuccess" = "✅ {{ .Email }} : محدودیت آدرس IP {{ .Count }} با موفقیت ذخیره شد."
+"clearIpSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت پاک‌سازی شدند."
+"getIpLog" = "✅ {{ .Email }} : دریافت لاگ آدرس‌های IP."
+"getUserInfo" = "✅ {{ .Email }} : دریافت اطلاعات کاربر تلگرام."
+"removedTGUserSuccess" = "✅ {{ .Email }} : کاربر تلگرام با موفقیت حذف شد."
+"enableSuccess" = "✅ {{ .Email }} : با موفقیت فعال شد."
+"disableSuccess" = "✅ {{ .Email }} : با موفقیت غیرفعال شد."
+"askToAddUserId" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: <b>{{ .TgUserID }}</b>"
+"askToAddUserName" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که نام کاربری یا شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nنام کاربری شما: <b>@{{ .TgUserName }}</b>\r\n\r\nشناسه کاربری شما: <b>{{ .TgUserID }}</b>"

+ 114 - 0
web/translation/translate.ru_RU.toml

@@ -336,6 +336,8 @@
 "manualBlockedDomains" = "Список заблокированных доменов"
 "manualDirectIPs" = "Список прямых IP адресов"
 "manualDirectDomains" = "Список прямых доменов"
+"manualIPv4Domains" = "Список доменов IPv4"
+"manualWARPDomains" = "Список доменов WARP"
 
 [pages.settings.security]
 "admin" = "Админ"
@@ -351,3 +353,115 @@
 "modifyUser" = "Изменение пользователя"
 "originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
 "userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
+
+[tgbot]
+"keyboardClosed" = "❌ Закрыта настраиваемая клавиатура!"
+"noResult" = "❗ Нет результатов!"
+"noQuery" = "❌ Запрос не найден! Пожалуйста, повторите команду!"
+"wentWrong" = "❌ Что-то пошло не так!"
+"noIpRecord" = "❗ Нет записей об IP-адресе!"
+"noInbounds" = "❗ Входящих соединений не найдено!"
+"unlimited" = "♾ Неограниченно"
+"month" = "Месяц"
+"months" = "Месяцев"
+"day" = "День"
+"days" = "Дней"
+"unknown" = "Неизвестно"
+"inbounds" = "Входящие"
+"clients" = "Клиенты"
+
+[tgbot.commands]
+"unknown" = "❗ Неизвестная команда"
+"pleaseChoose" = "👇 Пожалуйста, выберите:\r\n"
+"help" = "🤖 Добро пожаловать в этого бота! Он предназначен для предоставления вам конкретных данных с сервера и позволяет вносить необходимые изменения.\r\n\r\n"
+"start" = "👋 Привет, <i>{{ .Firstname }}</i>.\r\n"
+"welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n"
+"status" = "✅ Бот работает нормально!"
+"usage" = "❗ Пожалуйста, укажите текст для поиска!"
+"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</code>"
+"helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan."
+
+[tgbot.messages]
+"cpuThreshold" = "🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%"
+"selectUserFailed" = "❌ Ошибка при выборе пользователя!"
+"userSaved" = "✅ Пользователь Telegram сохранен."
+"loginSuccess" = "✅ Успешный вход в панель.\r\n"
+"loginFailed" = "❗️ Ошибка входа в панель.\r\n"
+"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
+"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
+"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"
+"version" = "🚀 Версия X-UI: {{ .Version }}\r\n"
+"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
+"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
+"ip" = "🌐 IP: {{ .IP }}\r\n"
+"ips" = "🔢 IP-адреса: \r\n{{ .IPs }}\r\n"
+"serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n"
+"serverLoad" = "📈 Загрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
+"serverMemory" = "📋 Память сервера: {{ .Current }}/{{ .Total }}\r\n"
+"tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n"
+"udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n"
+"traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
+"xrayStatus" = "ℹ️ Состояние Xray: {{ .State }}\r\n"
+"username" = "👤 Имя пользователя: {{ .Username }}\r\n"
+"time" = "⏰ Время: {{ .Time }}\r\n"
+"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n"
+"port" = "🔌 Порт: {{ .Port }}\r\n"
+"expire" = "📅 Дата окончания: {{ .DateTime }}\r\n \r\n"
+"expireIn" = "📅 Окончание через: {{ .Time }}\r\n \r\n"
+"active" = "💡 Активен: {{ .Enable }}\r\n"
+"email" = "📧 Email: {{ .Email }}\r\n"
+"upload" = "🔼 Загрузка↑: {{ .Upload }}\r\n"
+"download" = "🔽 Скачивание↓: {{ .Download }}\r\n"
+"total" = "🔄 Всего: {{ .UpDown }} / {{ .Total }}\r\n"
+"TGUser" = "👤 Пользователь Telegram: {{ .TelegramID }}\r\n"
+"exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n"
+"exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n"
+"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n"
+"depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n \r\n"
+"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n"
+"refreshedOn" = "🔄🕒 Обновлено: {{ .Time }}\r\n"
+
+[tgbot.buttons]
+"closeKeyboard" = "❌ Закрыть клавиатуру"
+"cancel" = "❌ Отмена"
+"cancelReset" = "❌ Отменить сброс"
+"cancelIpLimit" = "❌ Отменить лимит IP"
+"confirmResetTraffic" = "✅ Подтвердить сброс трафика?"
+"confirmClearIps" = "✅ Подтвердить очистку IP?"
+"confirmRemoveTGUser" = "✅ Подтвердить удаление пользователя Telegram?"
+"dbBackup" = "Получить резервную копию DB"
+"serverUsage" = "Использование сервера"
+"getInbounds" = "Получить входящие потоки"
+"depleteSoon" = "Скоро исчерпание"
+"clientUsage" = "Получить использование"
+"commands" = "Команды"
+"refresh" = "🔄 Обновить"
+"clearIPs" = "❌ Очистить IP"
+"removeTGUser" = "❌ Удалить пользователя Telegram"
+"selectTGUser" = "👤 Выбрать пользователя Telegram"
+"selectOneTGUser" = "👤 Выберите пользователя Telegram:"
+"resetTraffic" = "📈 Сбросить трафик"
+"resetExpire" = "📅 Сбросить дату окончания"
+"ipLog" = "🔢 Лог IP"
+"ipLimit" = "🔢 Лимит IP"
+"setTGUser" = "👤 Установить пользователя Telegram"
+"toggle" = "🔘 Вкл./Выкл."
+
+[tgbot.answers]
+"errorOperation" = "❗ Ошибка в операции."
+"getInboundsFailed" = "❌ Не удалось получить входящие потоки."
+"canceled" = "❌ {{ .Email }}: Операция отменена."
+"clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен."
+"IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреса успешно обновлены."
+"TGIdRefreshSuccess" = "✅ {{ .Email }}: Пользователь Telegram клиента успешно обновлен."
+"resetTrafficSuccess" = "✅ {{ .Email }}: Трафик успешно сброшен."
+"expireResetSuccess" = "✅ {{ .Email }}: Дни истечения успешно сброшены."
+"resetIpSuccess" = "✅ {{ .Email }}: Лимит IP ({{ .Count }}) успешно сохранен."
+"clearIpSuccess" = "✅ {{ .Email }}: IP-адреса успешно очищены."
+"getIpLog" = "✅ {{ .Email }}: Получен лог IP."
+"getUserInfo" = "✅ {{ .Email }}: Получена информация о пользователе Telegram."
+"removedTGUserSuccess" = "✅ {{ .Email }}: Пользователь Telegram успешно удален."
+"enableSuccess" = "✅ {{ .Email }}: Включено успешно."
+"disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
+"askToAddUserId" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваш идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаш идентификатор пользователя: <b>{{ .TgUserID }}</b>"
+"askToAddUserName" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваше имя пользователя или идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаше имя пользователя: <b>@{{ .TgUserName }}</b>\r\n\r\nВаш идентификатор пользователя: <b>{{ .TgUserID }}</b>"

+ 114 - 0
web/translation/translate.zh_Hans.toml

@@ -336,6 +336,8 @@
 "manualBlockedDomains" = "被阻止的域列表"
 "manualDirectIPs" = "直接 IP 列表"
 "manualDirectDomains" = "直接域列表"
+"manualIPv4Domains" = "IPv4 域名列表"
+"manualWARPDomains" = "WARP域名列表"
 
 [pages.settings.security]
 "admin" = "行政"
@@ -351,3 +353,115 @@
 "modifyUser" = "修改用户"
 "originalUserPassIncorrect" = "原用户名或原密码错误"
 "userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
+
+[tgbot]
+"keyboardClosed" = "❌ 自定义键盘已关闭!"
+"noResult" = "❗ 没有结果!"
+"noQuery" = "❌ 未找到查询!请重新使用命令!"
+"wentWrong" = "❌ 出了点问题!"
+"noIpRecord" = "❗ 没有IP记录!"
+"noInbounds" = "❗ 没有找到入站连接!"
+"unlimited" = "♾ 无限制"
+"month" = "月"
+"months" = "月"
+"day" = "天"
+"days" = "天"
+"unknown" = "未知"
+"inbounds" = "入站连接"
+"clients" = "客户端"
+
+[tgbot.commands]
+"unknown" = "❗ 未知命令"
+"pleaseChoose" = "👇 请选择:\r\n"
+"help" = "🤖 欢迎使用本机器人!它旨在为您提供来自服务器的特定数据,并允许您进行必要的修改。\r\n\r\n"
+"start" = "👋 你好,<i>{{ .Firstname }}</i>。\r\n"
+"welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n"
+"status" = "✅ 机器人正常运行!"
+"usage" = "❗ 请输入要搜索的文本!"
+"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n \r\n搜索入站连接(包含客户端统计信息):\r\n<code>/inbound [Remark]</code>"
+"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\n对于vmess/vless,请使用UUID;对于Trojan,请使用密码。"
+
+[tgbot.messages]
+"cpuThreshold" = "🔴 CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%"
+"selectUserFailed" = "❌ 用户选择错误!"
+"userSaved" = "✅ 电报用户已保存。"
+"loginSuccess" = "✅ 成功登录到面板。\r\n"
+"loginFailed" = "❗️ 面板登录失败。\r\n"
+"report" = "🕰 定时报告:{{ .RunTime }}\r\n"
+"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n"
+"hostname" = "💻 主机名:{{ .Hostname }}\r\n"
+"version" = "🚀 X-UI 版本:{{ .Version }}\r\n"
+"ipv6" = "🌐 IPv6:{{ .IPv6 }}\r\n"
+"ipv4" = "🌐 IPv4:{{ .IPv4 }}\r\n"
+"ip" = "🌐 IP:{{ .IP }}\r\n"
+"ips" = "🔢 IP 地址:\r\n{{ .IPs }}\r\n"
+"serverUpTime" = "⏳ 服务器运行时间:{{ .UpTime }} {{ .Unit }}\r\n"
+"serverLoad" = "📈 服务器负载:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
+"serverMemory" = "📋 服务器内存:{{ .Current }}/{{ .Total }}\r\n"
+"tcpCount" = "🔹 TCP 连接数:{{ .Count }}\r\n"
+"udpCount" = "🔸 UDP 连接数:{{ .Count }}\r\n"
+"traffic" = "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
+"xrayStatus" = "ℹ️ Xray 状态:{{ .State }}\r\n"
+"username" = "👤 用户名:{{ .Username }}\r\n"
+"time" = "⏰ 时间:{{ .Time }}\r\n"
+"inbound" = "📍 入站:{{ .Remark }}\r\n"
+"port" = "🔌 端口:{{ .Port }}\r\n"
+"expire" = "📅 过期日期:{{ .DateTime }}\r\n \r\n"
+"expireIn" = "📅 剩余时间:{{ .Time }}\r\n \r\n"
+"active" = "💡 激活:{{ .Enable }}\r\n"
+"email" = "📧 邮箱:{{ .Email }}\r\n"
+"upload" = "🔼 上传↑:{{ .Upload }}\r\n"
+"download" = "🔽 下载↓:{{ .Download }}\r\n"
+"total" = "🔄 总计:{{ .UpDown }} / {{ .Total }}\r\n"
+"TGUser" = "👤 电报用户:{{ .TelegramID }}\r\n"
+"exhaustedMsg" = "🚨 耗尽的{{ .Type }}:\r\n"
+"exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n"
+"disabled" = "🛑 禁用:{{ .Disabled }}\r\n"
+"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n"
+"backupTime" = "🗄 备份时间:{{ .Time }}\r\n"
+"refreshedOn" = "🔄🕒 刷新时间:{{ .Time }}\r\n"
+
+[tgbot.buttons]
+"closeKeyboard" = "❌ 关闭键盘"
+"cancel" = "❌ 取消"
+"cancelReset" = "❌ 取消重置"
+"cancelIpLimit" = "❌ 取消 IP 限制"
+"confirmResetTraffic" = "✅ 确认重置流量?"
+"confirmClearIps" = "✅ 确认清除 IP?"
+"confirmRemoveTGUser" = "✅ 确认移除 Telegram 用户?"
+"dbBackup" = "获取数据库备份"
+"serverUsage" = "服务器使用情况"
+"getInbounds" = "获取入站信息"
+"depleteSoon" = "即将耗尽"
+"clientUsage" = "获取使用情况"
+"commands" = "命令"
+"refresh" = "🔄 刷新"
+"clearIPs" = "❌ 清除 IP"
+"removeTGUser" = "❌ 移除 Telegram 用户"
+"selectTGUser" = "👤 选择 Telegram 用户"
+"selectOneTGUser" = "👤 选择一个 Telegram 用户:"
+"resetTraffic" = "📈 重置流量"
+"resetExpire" = "📅 重置过期天数"
+"ipLog" = "🔢 IP 日志"
+"ipLimit" = "🔢 IP 限制"
+"setTGUser" = "👤 设置 Telegram 用户"
+"toggle" = "🔘 启用/禁用"
+
+[tgbot.answers]
+"errorOperation" = "❗ 操作错误。"
+"getInboundsFailed" = "❌ 获取入站信息失败。"
+"canceled" = "❌ {{ .Email }}:操作已取消。"
+"clientRefreshSuccess" = "✅ {{ .Email }}:客户端刷新成功。"
+"IpRefreshSuccess" = "✅ {{ .Email }}:IP 刷新成功。"
+"TGIdRefreshSuccess" = "✅ {{ .Email }}:客户端的 Telegram 用户刷新成功。"
+"resetTrafficSuccess" = "✅ {{ .Email }}:流量已重置成功。"
+"expireResetSuccess" = "✅ {{ .Email }}:过期天数已重置成功。"
+"resetIpSuccess" = "✅ {{ .Email }}:成功保存 IP 限制数量为 {{ .Count }}。"
+"clearIpSuccess" = "✅ {{ .Email }}:IP 已成功清除。"
+"getIpLog" = "✅ {{ .Email }}:获取 IP 日志。"
+"getUserInfo" = "✅ {{ .Email }}:获取 Telegram 用户信息。"
+"removedTGUserSuccess" = "✅ {{ .Email }}:Telegram 用户已成功移除。"
+"enableSuccess" = "✅ {{ .Email }}:已成功启用。"
+"disableSuccess" = "✅ {{ .Email }}:已成功禁用。"
+"askToAddUserId" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户ID。\r\n\r\n您的用户ID:<b>{{ .TgUserID }}</b>"
+"askToAddUserName" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户名或用户ID。\r\n\r\n您的用户名:<b>@{{ .TgUserName }}</b>\r\n\r\n您的用户ID:<b>{{ .TgUserID }}</b>"

+ 22 - 93
web/web.go

@@ -18,16 +18,14 @@ import (
 	"x-ui/util/common"
 	"x-ui/web/controller"
 	"x-ui/web/job"
+	"x-ui/web/locale"
 	"x-ui/web/network"
 	"x-ui/web/service"
 
 	"github.com/gin-contrib/sessions"
 	"github.com/gin-contrib/sessions/cookie"
 	"github.com/gin-gonic/gin"
-	"github.com/nicksnyder/go-i18n/v2/i18n"
-	"github.com/pelletier/go-toml/v2"
 	"github.com/robfig/cron/v3"
-	"golang.org/x/text/language"
 )
 
 //go:embed assets/*
@@ -202,13 +200,23 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 			c.Header("Cache-Control", "max-age=31536000")
 		}
 	})
-	err = s.initI18n(engine)
+
+	// init i18n
+	err = locale.InitLocalizer(i18nFS, &s.settingService)
 	if err != nil {
 		return nil, err
 	}
 
+	// Apply locale middleware for i18n
+	i18nWebFunc := func(key string, params ...string) string {
+		return locale.I18n(locale.Web, key, params...)
+	}
+	engine.FuncMap["i18n"] = i18nWebFunc
+	engine.Use(locale.LocalizerMiddleware())
+
+	// set static files and template
 	if config.IsDebug() {
-		// for develop
+		// for development
 		files, err := s.getHtmlFiles()
 		if err != nil {
 			return nil, err
@@ -216,12 +224,12 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 		engine.LoadHTMLFiles(files...)
 		engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets")))
 	} else {
-		// for prod
-		t, err := s.getHtmlTemplate(engine.FuncMap)
+		// for production
+		template, err := s.getHtmlTemplate(engine.FuncMap)
 		if err != nil {
 			return nil, err
 		}
-		engine.SetHTMLTemplate(t)
+		engine.SetHTMLTemplate(template)
 		engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS}))
 	}
 
@@ -239,87 +247,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 	return engine, nil
 }
 
-func (s *Server) initI18n(engine *gin.Engine) error {
-	bundle := i18n.NewBundle(language.SimplifiedChinese)
-	bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
-	err := fs.WalkDir(i18nFS, "translation", func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-		if d.IsDir() {
-			return nil
-		}
-		data, err := i18nFS.ReadFile(path)
-		if err != nil {
-			return err
-		}
-		_, err = bundle.ParseMessageFileBytes(data, path)
-		return err
-	})
-	if err != nil {
-		return err
-	}
-
-	findI18nParamNames := func(key string) []string {
-		names := make([]string, 0)
-		keyLen := len(key)
-		for i := 0; i < keyLen-1; i++ {
-			if key[i:i+2] == "{{" { // 判断开头 "{{"
-				j := i + 2
-				isFind := false
-				for ; j < keyLen-1; j++ {
-					if key[j:j+2] == "}}" { // 结尾 "}}"
-						isFind = true
-						break
-					}
-				}
-				if isFind {
-					names = append(names, key[i+3:j])
-				}
-			}
-		}
-		return names
-	}
-
-	var localizer *i18n.Localizer
-
-	I18n := func(key string, params ...string) (string, error) {
-		names := findI18nParamNames(key)
-		if len(names) != len(params) {
-			return "", common.NewError("find names:", names, "---------- params:", params, "---------- num not equal")
-		}
-		templateData := map[string]interface{}{}
-		for i := range names {
-			templateData[names[i]] = params[i]
-		}
-		return localizer.Localize(&i18n.LocalizeConfig{
-			MessageID:    key,
-			TemplateData: templateData,
-		})
-	}
-
-	engine.FuncMap["i18n"] = I18n
-
-	engine.Use(func(c *gin.Context) {
-		//accept := c.GetHeader("Accept-Language")
-
-		var lang string
-
-		if cookie, err := c.Request.Cookie("lang"); err == nil {
-			lang = cookie.Value
-		} else {
-			lang = c.GetHeader("Accept-Language")
-		}
-
-		localizer = i18n.NewLocalizer(bundle, lang)
-		c.Set("localizer", localizer)
-		c.Set("I18n", I18n)
-		c.Next()
-	})
-
-	return nil
-}
-
 func (s *Server) startTask() {
 	err := s.xrayService.RestartXray(true)
 	if err != nil {
@@ -346,7 +273,7 @@ func (s *Server) startTask() {
 	if (err == nil) && (isTgbotenabled) {
 		runtime, err := s.settingService.GetTgbotRuntime()
 		if err != nil || runtime == "" {
-			logger.Errorf("Add NewStatsNotifyJob error[%s],Runtime[%s] invalid,wil run default", err, runtime)
+			logger.Errorf("Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default", err, runtime)
 			runtime = "@daily"
 		}
 		logger.Infof("Tg notify enabled,run at %s", runtime)
@@ -356,12 +283,14 @@ func (s *Server) startTask() {
 			return
 		}
 
+		// check for Telegram bot callback query hash storage reset
+		s.cron.AddJob("@every 2m", job.NewCheckHashStorageJob())
+
 		// Check CPU load and alarm to TgBot if threshold passes
 		cpuThreshold, err := s.settingService.GetTgCpu()
 		if (err == nil) && (cpuThreshold > 0) {
 			s.cron.AddJob("@every 10s", job.NewCheckCpuJob())
 		}
-
 	} else {
 		s.cron.Remove(entry)
 	}
@@ -441,7 +370,7 @@ func (s *Server) Start() (err error) {
 	isTgbotenabled, err := s.settingService.GetTgbotenabled()
 	if (err == nil) && (isTgbotenabled) {
 		tgBot := s.tgbotService.NewTgbot()
-		tgBot.Start()
+		tgBot.Start(i18nFS)
 	}
 
 	return nil
@@ -453,7 +382,7 @@ func (s *Server) Stop() error {
 	if s.cron != nil {
 		s.cron.Stop()
 	}
-	if s.tgbotService.IsRunnging() {
+	if s.tgbotService.IsRunning() {
 		s.tgbotService.Stop()
 	}
 	var err1 error

Some files were not shown because too many files changed in this diff