fix(tgbot): resolve client creation race conditions and localization bugs

- Refactored Telegram bot client creation state to use a concurrent-safe map (\clientStates map[int64]*ClientState\), replacing package-level global variables. This prevents data races when multiple administrators interact with the bot simultaneously.
- Fixed hardcoded English strings in \BuildInboundClientDataMessage\ by utilizing the \	.I18nBot()\ localization wrapper.
- Implemented \UpdateBotLocalizer\ to dynamically refresh the bot's language whenever the \	gLang\ setting is updated in the web panel, eliminating the need for a service restart.
- Synchronized missing translation keys for \Sub ID\ and \Flow\ across all non-English/Russian localization files to prevent missing interface elements.
This commit is contained in:
Aleksei Sidorenko 2026-05-13 00:25:35 +03:00
parent 63a7931862
commit 29fa28bf75
13 changed files with 7566 additions and 6920 deletions

View file

@ -6,6 +6,7 @@ import (
"github.com/mhsanaei/3x-ui/v3/util/crypto" "github.com/mhsanaei/3x-ui/v3/util/crypto"
"github.com/mhsanaei/3x-ui/v3/web/entity" "github.com/mhsanaei/3x-ui/v3/web/entity"
"github.com/mhsanaei/3x-ui/v3/web/locale"
"github.com/mhsanaei/3x-ui/v3/web/service" "github.com/mhsanaei/3x-ui/v3/web/service"
"github.com/mhsanaei/3x-ui/v3/web/session" "github.com/mhsanaei/3x-ui/v3/web/session"
@ -77,6 +78,9 @@ func (a *SettingController) updateSetting(c *gin.Context) {
return return
} }
err = a.settingService.UpdateAllSetting(allSetting) err = a.settingService.UpdateAllSetting(allSetting)
if err == nil {
locale.UpdateBotLocalizer(allSetting.TgLang)
}
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
} }

View file

@ -111,11 +111,20 @@ func initTGBotLocalizer(settingService SettingService) error {
if err != nil { if err != nil {
return err return err
} }
logger.Infof("Initializing TG Bot localizer with language: %s", botLang)
LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang) LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang)
return nil return nil
} }
// UpdateBotLocalizer dynamically updates the bot localizer language.
func UpdateBotLocalizer(botLang string) {
if i18nBundle != nil {
logger.Infof("Updating TG Bot localizer with language: %s", botLang)
LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang)
}
}
// LocalizerMiddleware returns a Gin middleware that sets up localization for web requests. // LocalizerMiddleware returns a Gin middleware that sets up localization for web requests.
// It determines the user's language from cookies or Accept-Language header, // It determines the user's language from cookies or Accept-Language header,
// creates a localizer instance, and stores it in the Gin context for use in handlers. // creates a localizer instance, and stores it in the Gin context for use in handlers.
@ -161,7 +170,9 @@ func loadTranslationsFromDisk(bundle *i18n.Bundle) error {
if err != nil { if err != nil {
return err return err
} }
_, err = bundle.ParseMessageFileBytes(data, path) filename := d.Name()
logger.Infof("Parsing translation file from disk: %s", filename)
_, err = bundle.ParseMessageFileBytes(data, filename)
return err return err
}) })
} }
@ -183,7 +194,9 @@ func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
return err return err
} }
_, err = i18nBundle.ParseMessageFileBytes(data, path) filename := d.Name()
logger.Infof("Parsing translation file from embed: %s (path: %s)", filename, path)
_, err = i18nBundle.ParseMessageFileBytes(data, filename)
return err return err
}) })
if err != nil { if err != nil {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -873,6 +873,11 @@
"exhaustedMsg": "🚨 Exhausted {{ .Type }}:\r\n", "exhaustedMsg": "🚨 Exhausted {{ .Type }}:\r\n",
"exhaustedCount": "🚨 Exhausted {{ .Type }} count:\r\n", "exhaustedCount": "🚨 Exhausted {{ .Type }} count:\r\n",
"onlinesCount": "🌐 Online Clients: {{ .Count }}\r\n", "onlinesCount": "🌐 Online Clients: {{ .Count }}\r\n",
"cpu": "CPU: {{ .Usage }}%\r\n",
"mem": "RAM: {{ .Usage }}/{{ .Total }}\r\n",
"swap": "Swap: {{ .Usage }}/{{ .Total }}\r\n",
"disk": "Disk: {{ .Usage }}/{{ .Total }}\r\n",
"uptime": "Uptime: {{ .Time }}\r\n",
"disabled": "🛑 Disabled: {{ .Disabled }}\r\n", "disabled": "🛑 Disabled: {{ .Disabled }}\r\n",
"depleteSoon": "🔜 Deplete Soon: {{ .Deplete }}\r\n\r\n", "depleteSoon": "🔜 Deplete Soon: {{ .Deplete }}\r\n\r\n",
"backupTime": "🗄 Backup Time: {{ .Time }}\r\n", "backupTime": "🗄 Backup Time: {{ .Time }}\r\n",
@ -947,6 +952,8 @@
"change_subid": "📝 Sub ID", "change_subid": "📝 Sub ID",
"change_flow": "🌊 Flow", "change_flow": "🌊 Flow",
"flow_none": "None", "flow_none": "None",
"qrCode": "QR Code",
"selectTGUser": "Select Telegram User",
"ResetAllTraffics": "Reset All Traffics", "ResetAllTraffics": "Reset All Traffics",
"SortedTrafficUsageReport": "Sorted Traffic Usage Report" "SortedTrafficUsageReport": "Sorted Traffic Usage Report"
}, },

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -873,6 +873,11 @@
"exhaustedMsg": "🚨 Исчерпаны {{ .Type }}:\r\n", "exhaustedMsg": "🚨 Исчерпаны {{ .Type }}:\r\n",
"exhaustedCount": "🚨 Количество исчерпанных {{ .Type }}:\r\n", "exhaustedCount": "🚨 Количество исчерпанных {{ .Type }}:\r\n",
"onlinesCount": "🌐 Клиентов онлайн: {{ .Count }}\r\n", "onlinesCount": "🌐 Клиентов онлайн: {{ .Count }}\r\n",
"cpu": "ЦП: {{ .Usage }}%\r\n",
"mem": "ОЗУ: {{ .Usage }}/{{ .Total }}\r\n",
"swap": "Swap: {{ .Usage }}/{{ .Total }}\r\n",
"disk": "Диск: {{ .Usage }}/{{ .Total }}\r\n",
"uptime": "Время работы: {{ .Time }}\r\n",
"disabled": "🛑 Отключено: {{ .Disabled }}\r\n", "disabled": "🛑 Отключено: {{ .Disabled }}\r\n",
"depleteSoon": "🔜 Клиенты, у которых скоро исчерпание: {{ .Deplete }}\r\n\r\n", "depleteSoon": "🔜 Клиенты, у которых скоро исчерпание: {{ .Deplete }}\r\n\r\n",
"backupTime": "🗄 Время резервного копирования: {{ .Time }}\r\n", "backupTime": "🗄 Время резервного копирования: {{ .Time }}\r\n",
@ -947,6 +952,7 @@
"change_subid": "📝 Sub ID", "change_subid": "📝 Sub ID",
"change_flow": "🌊 Flow", "change_flow": "🌊 Flow",
"flow_none": "Отсутствует", "flow_none": "Отсутствует",
"qrCode": "QR-код",
"ResetAllTraffics": "Сбросить весь трафик", "ResetAllTraffics": "Сбросить весь трафик",
"SortedTrafficUsageReport": "Отсортированный отчет об использовании трафика" "SortedTrafficUsageReport": "Отсортированный отчет об использовании трафика"
}, },

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff