feat(telegram): 18-language bot replies, menus, and command descriptions
- Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load feat(telegram): 18-language bot replies, menus, and command descriptions - Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load feat(telegram): 18-language bot replies, menus, and command descriptions - Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load feat(telegram): 18-language bot replies, menus, and command descriptions - Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load feat(telegram): 18-language bot replies, menus, and command descriptions - Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load feat(telegram): 18-language bot replies, menus, and command descriptions - Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load feat(telegram): 18-language bot replies, menus, and command descriptions - Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load feat(telegram): 18-language bot replies, menus, and command descriptions - Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load feat(telegram): 18-language bot replies, menus, and command descriptions - Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load feat(telegram): 18-language bot replies, menus, and command descriptions - Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load feat(telegram): 18-language bot replies, menus, and command descriptions - Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load feat(telegram): 18-language bot replies, menus, and command descriptions - Add telegram/i18n.go with en/zh-CN/zh-TW full strings and overlays for ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk - Wire bot commands, inline keyboards, setMyCommands, and test message to settings.language - Expand Settings UI bot language select to all 18 panel locales - Normalize legacy zh → zh-CN on load
This commit is contained in:
1 parent
a1cbef1d74
commit
676312ae79
6 files changed
+511
-125
No files matched your search
@@ -193,7 +193,8 @@ func (h *Handler) Test(c *gin.Context) {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := client.SendText("🔔 <b>3m-ui</b> Telegram 测试消息 / test message — 连接正常 / connection OK."); err != nil {
|
||||
sset, _ := LoadSettings(h.db)
|
||||
if err := client.SendText(Tr(NormalizeLang(sset.Language), "test_msg")); err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -209,20 +210,22 @@ func maskToken(token string) string {
|
||||
|
||||
// defaultBotCommands returns the command menu registered with Telegram via setMyCommands.
|
||||
// Descriptions are bilingual (zh/en) to match the bot's existing reply style.
|
||||
func defaultBotCommands() []BotCommand {
|
||||
func defaultBotCommands(lang string) []BotCommand {
|
||||
lang = NormalizeLang(lang)
|
||||
return []BotCommand{
|
||||
{Command: "start", Description: "开始 / Start & help"},
|
||||
{Command: "help", Description: "查看可用命令 / Show commands"},
|
||||
{Command: "status", Description: "面板与核心概览 / Panel & core overview"},
|
||||
{Command: "id", Description: "显示当前 Telegram Chat ID / Show chat ID"},
|
||||
{Command: "usage", Description: "查询当前账号用量 / Show my usage"},
|
||||
{Command: "users", Description: "代理用户列表 / List proxy users"},
|
||||
{Command: "online", Description: "当前在线用户 / Online users"},
|
||||
{Command: "listeners", Description: "入站节点列表 / Inbound listeners"},
|
||||
{Command: "traffic", Description: "流量快照 / Traffic snapshot"},
|
||||
{Command: "restart", Description: "重启 Mihomo 核心 / Restart core"},
|
||||
{Command: "backup", Description: "下载/发送备份 / Backup"},
|
||||
{Command: "search", Description: "按用户名/备注搜索 / Search users"},
|
||||
{Command: "start", Description: Tr(lang, "cmd_start")},
|
||||
{Command: "help", Description: Tr(lang, "cmd_start")},
|
||||
{Command: "id", Description: Tr(lang, "cmd_id")},
|
||||
{Command: "usage", Description: Tr(lang, "cmd_usage")},
|
||||
{Command: "status", Description: Tr(lang, "cmd_status")},
|
||||
{Command: "users", Description: Tr(lang, "cmd_users")},
|
||||
{Command: "online", Description: Tr(lang, "cmd_online")},
|
||||
{Command: "listeners", Description: Tr(lang, "cmd_listeners")},
|
||||
{Command: "traffic", Description: Tr(lang, "cmd_traffic")},
|
||||
{Command: "restart", Description: Tr(lang, "cmd_restart")},
|
||||
{Command: "deldepleted", Description: Tr(lang, "cmd_deldepleted")},
|
||||
{Command: "search", Description: Tr(lang, "cmd_search")},
|
||||
{Command: "backup", Description: Tr(lang, "cmd_backup")},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +241,7 @@ func (h *Handler) SetMyCommands(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
client := NewClient(s.BotToken, s.ChatIDs, s.ProxyURL, s.APIServer)
|
||||
resp, err := client.SetMyCommands(defaultBotCommands())
|
||||
resp, err := client.SetMyCommands(defaultBotCommands(s.Language))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
@@ -13,11 +13,17 @@ import (
|
||||
// handleCommand dispatches a /command from a Telegram message. The returned
|
||||
// string is sent as the reply. An empty string means the handler already sent
|
||||
// its reply (e.g. via sendWithKeyboard) and the loop should not send again.
|
||||
|
||||
func (b *Bot) lang() string {
|
||||
s, _ := LoadSettings(b.db)
|
||||
return NormalizeLang(s.Language)
|
||||
}
|
||||
|
||||
func (b *Bot) handleCommand(ctx commandContext) string {
|
||||
text := strings.TrimSpace(ctx.Text)
|
||||
parts := strings.Fields(text)
|
||||
if len(parts) == 0 {
|
||||
return helpText()
|
||||
return helpText(b.lang())
|
||||
}
|
||||
cmd := strings.ToLower(parts[0])
|
||||
if i := strings.IndexByte(cmd, '@'); i >= 0 {
|
||||
@@ -33,42 +39,42 @@ func (b *Bot) handleCommand(ctx commandContext) string {
|
||||
return b.cmdUsage(ctx)
|
||||
case "status", "状态":
|
||||
if !ctx.IsAdmin {
|
||||
return permDeniedUserOnly
|
||||
return Tr(b.lang(), "perm_denied")
|
||||
}
|
||||
return b.cmdStatus()
|
||||
case "users", "用户":
|
||||
if !ctx.IsAdmin {
|
||||
return permDeniedUserOnly
|
||||
return Tr(b.lang(), "perm_denied")
|
||||
}
|
||||
return b.cmdUsers()
|
||||
case "online", "在线":
|
||||
if !ctx.IsAdmin {
|
||||
return permDeniedUserOnly
|
||||
return Tr(b.lang(), "perm_denied")
|
||||
}
|
||||
return b.cmdOnline()
|
||||
case "listeners", "nodes", "节点":
|
||||
if !ctx.IsAdmin {
|
||||
return permDeniedUserOnly
|
||||
return Tr(b.lang(), "perm_denied")
|
||||
}
|
||||
return b.cmdListeners()
|
||||
case "traffic", "流量":
|
||||
if !ctx.IsAdmin {
|
||||
return permDeniedUserOnly
|
||||
return Tr(b.lang(), "perm_denied")
|
||||
}
|
||||
return b.cmdTraffic()
|
||||
case "restart", "重启":
|
||||
if !ctx.IsAdmin {
|
||||
return permDeniedUserOnly
|
||||
return Tr(b.lang(), "perm_denied")
|
||||
}
|
||||
return b.cmdRestart()
|
||||
case "deldepleted", "清理":
|
||||
if !ctx.IsAdmin {
|
||||
return permDeniedUserOnly
|
||||
return Tr(b.lang(), "perm_denied")
|
||||
}
|
||||
return b.cmdDelDepleted()
|
||||
case "search", "查找":
|
||||
if !ctx.IsAdmin {
|
||||
return permDeniedUserOnly
|
||||
return Tr(b.lang(), "perm_denied")
|
||||
}
|
||||
q := ""
|
||||
if len(parts) > 1 {
|
||||
@@ -77,23 +83,21 @@ func (b *Bot) handleCommand(ctx commandContext) string {
|
||||
return b.cmdSearch(q)
|
||||
case "backup", "备份":
|
||||
if !ctx.IsAdmin {
|
||||
return permDeniedUserOnly
|
||||
return Tr(b.lang(), "perm_denied")
|
||||
}
|
||||
return b.cmdBackup()
|
||||
default:
|
||||
return "未知指令。发送 /help 查看可用命令。"
|
||||
return Tr(b.lang(), "unknown_cmd")
|
||||
}
|
||||
}
|
||||
|
||||
// permDeniedUserOnly is the reply sent to non-admin chats that try to invoke
|
||||
// an admin-only command. It tells them which commands they CAN use.
|
||||
const permDeniedUserOnly = "⛔ 此命令仅管理员可用 / Admin-only command.\n可用命令 / Available: /id /usage /help"
|
||||
|
||||
// cmdStartHelp renders /start and /help. Admin chats receive the admin inline
|
||||
// keyboard; bound users receive the user menu; unbound chats (which cannot
|
||||
// reach this handler anyway) get the plain help text.
|
||||
func (b *Bot) cmdStartHelp(ctx commandContext) string {
|
||||
help := helpText()
|
||||
help := helpText(b.lang())
|
||||
if !ctx.IsAdmin && !ctx.IsBound {
|
||||
return help
|
||||
}
|
||||
@@ -102,15 +106,15 @@ func (b *Bot) cmdStartHelp(ctx commandContext) string {
|
||||
}
|
||||
chatID := fmtInt64(ctx.ChatID)
|
||||
if ctx.IsAdmin {
|
||||
kb := buildAdminMenu()
|
||||
kb := buildAdminMenu(b.lang())
|
||||
if err := b.tgClient.sendWithKeyboard(chatID, help, kb); err != nil {
|
||||
// Fall back to plain text on API failure (e.g. keyboard rejected).
|
||||
return help
|
||||
}
|
||||
return ""
|
||||
}
|
||||
welcome := "👋 欢迎!点击下方按钮查看你的用量与订阅链接。\nWelcome! Tap a button below to view your usage & subscription."
|
||||
if err := b.tgClient.sendWithKeyboard(chatID, welcome, buildUserMenu()); err != nil {
|
||||
welcome := Tr(b.lang(), "user_welcome")
|
||||
if err := b.tgClient.sendWithKeyboard(chatID, welcome, buildUserMenu(b.lang())); err != nil {
|
||||
return welcome
|
||||
}
|
||||
return ""
|
||||
@@ -120,7 +124,7 @@ func (b *Bot) cmdStartHelp(ctx commandContext) string {
|
||||
// their own ID for the chat_ids allowlist, and by users to share their ID with
|
||||
// the admin for binding.
|
||||
func (b *Bot) cmdID(ctx commandContext) string {
|
||||
return fmt.Sprintf("🆔 Your Telegram ID: <code>%d</code>", ctx.FromID)
|
||||
return Trf(b.lang(), "your_id", ctx.FromID)
|
||||
}
|
||||
|
||||
// cmdUsage branches on caller role:
|
||||
@@ -134,7 +138,7 @@ func (b *Bot) cmdUsage(ctx commandContext) string {
|
||||
q = strings.Join(parts[1:], " ")
|
||||
}
|
||||
if strings.TrimSpace(q) == "" {
|
||||
return "用法 / Usage: /usage <关键字> — 搜索代理用户 / search proxy users"
|
||||
return Tr(b.lang(), "usage_admin_hint")
|
||||
}
|
||||
return b.cmdSearch(q)
|
||||
}
|
||||
@@ -146,13 +150,13 @@ func (b *Bot) cmdUsage(ctx commandContext) string {
|
||||
// is not linked to any ProxyUser.
|
||||
func (b *Bot) userUsageMessage(fromID int64) string {
|
||||
if fromID == 0 || b.userSvc == nil {
|
||||
return notBoundMessage(fromID)
|
||||
return notBoundMessage(b.lang(), fromID)
|
||||
}
|
||||
u, err := b.userSvc.GetByTelegramID(fromID)
|
||||
if err != nil || u == nil {
|
||||
return notBoundMessage(fromID)
|
||||
return notBoundMessage(b.lang(), fromID)
|
||||
}
|
||||
return formatUserUsage(u)
|
||||
return formatUserUsage(b.lang(), u)
|
||||
}
|
||||
|
||||
// cmdMyUsage is the callback_query variant — always renders the bound user's
|
||||
@@ -165,11 +169,11 @@ func (b *Bot) cmdMyUsage(ctx commandContext) string {
|
||||
// cmdMySub returns (and lazily ensures) the bound user's subscription URL.
|
||||
func (b *Bot) cmdMySub(ctx commandContext) string {
|
||||
if ctx.FromID == 0 || b.userSvc == nil {
|
||||
return notBoundMessage(ctx.FromID)
|
||||
return notBoundMessage(b.lang(), ctx.FromID)
|
||||
}
|
||||
u, err := b.userSvc.GetByTelegramID(ctx.FromID)
|
||||
if err != nil || u == nil {
|
||||
return notBoundMessage(ctx.FromID)
|
||||
return notBoundMessage(b.lang(), ctx.FromID)
|
||||
}
|
||||
token := u.SubToken
|
||||
if strings.TrimSpace(token) == "" {
|
||||
@@ -178,46 +182,42 @@ func (b *Bot) cmdMySub(ctx commandContext) string {
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(token) == "" {
|
||||
return "⚠️ 订阅链接生成失败, 请稍后重试 / Subscription token not available."
|
||||
return Tr(b.lang(), "sub_fail")
|
||||
}
|
||||
return fmt.Sprintf("🔗 <b>订阅链接 / Subscription URL</b>\n<code>%s</code>", buildSubURL(token))
|
||||
return Trf(b.lang(), "sub_link", buildSubURL(token))
|
||||
}
|
||||
|
||||
func notBoundMessage(fromID int64) string {
|
||||
return fmt.Sprintf(
|
||||
"未绑定账户。请联系管理员将你的 Telegram ID (<code>%d</code>) 绑定到你的账户。\n/ Not bound. Ask admin to bind your Telegram ID.",
|
||||
fromID,
|
||||
)
|
||||
func notBoundMessage(lang string, fromID int64) string {
|
||||
return Trf(lang, "not_bound", fromID)
|
||||
}
|
||||
|
||||
// formatUserUsage builds the per-user usage report shown by /usage and the
|
||||
// my_usage callback.
|
||||
func formatUserUsage(u *models.ProxyUser) string {
|
||||
func formatUserUsage(lang string, u *models.ProxyUser) string {
|
||||
var bld strings.Builder
|
||||
bld.WriteString("📊 <b>账户用量 / My Usage</b>\n")
|
||||
bld.WriteString(fmt.Sprintf("用户 / User: <code>%s</code>\n", escapeHTML(u.Username)))
|
||||
bld.WriteString(Tr(lang, "my_usage_title") + "\n")
|
||||
bld.WriteString(Trf(lang, "user_label", escapeHTML(u.Username)) + "\n")
|
||||
used := formatBytes(u.TrafficUsed)
|
||||
limit := "∞"
|
||||
if u.TrafficLimit > 0 {
|
||||
limit = formatBytes(u.TrafficLimit)
|
||||
}
|
||||
bld.WriteString(fmt.Sprintf("流量 / Traffic: %s / %s\n", used, limit))
|
||||
bld.WriteString(fmt.Sprintf("上传 ↑ %s 下载 ↓ %s\n",
|
||||
formatBytes(u.UploadBytes), formatBytes(u.DownloadBytes)))
|
||||
bld.WriteString(Trf(lang, "traffic_label", used, limit) + "\n")
|
||||
bld.WriteString(Trf(lang, "up_down", formatBytes(u.UploadBytes), formatBytes(u.DownloadBytes)) + "\n")
|
||||
if !u.ExpireTime.IsZero() {
|
||||
bld.WriteString(fmt.Sprintf("到期 / Expires: <code>%s</code>\n", u.ExpireTime.Format("2006-01-02 15:04")))
|
||||
bld.WriteString(Trf(lang, "expires", u.ExpireTime.Format("2006-01-02 15:04")) + "\n")
|
||||
} else {
|
||||
bld.WriteString("到期 / Expires: 永不过期 / Never\n")
|
||||
bld.WriteString(Tr(lang, "expires_never") + "\n")
|
||||
}
|
||||
online := "离线 / Offline"
|
||||
online := Tr(lang, "status_offline")
|
||||
if u.Online {
|
||||
online = "在线 / Online 🟢"
|
||||
online = Tr(lang, "status_online")
|
||||
}
|
||||
bld.WriteString(fmt.Sprintf("状态 / Status: %s\n", online))
|
||||
bld.WriteString(online + "\n")
|
||||
if strings.TrimSpace(u.SubToken) != "" {
|
||||
bld.WriteString(fmt.Sprintf("订阅 / Subscription:\n<code>%s</code>\n", buildSubURL(u.SubToken)))
|
||||
bld.WriteString(Trf(lang, "sub_line", buildSubURL(u.SubToken)) + "\n")
|
||||
} else {
|
||||
bld.WriteString("订阅 / Subscription: 未生成 / Not generated (点击 /click my_sub 生成)\n")
|
||||
bld.WriteString(Tr(lang, "sub_missing") + "\n")
|
||||
}
|
||||
return bld.String()
|
||||
}
|
||||
@@ -273,25 +273,11 @@ func (b *Bot) handleCallback(ctx commandContext, data string) string {
|
||||
case "my_sub":
|
||||
return b.cmdMySub(ctx)
|
||||
}
|
||||
return "未知操作 / Unknown action."
|
||||
return Tr(b.lang(), "unknown_action")
|
||||
}
|
||||
|
||||
func helpText() string {
|
||||
return strings.TrimSpace(`
|
||||
🤖 <b>3m-ui Bot</b>
|
||||
/status — 核心与面板概览 / Panel & core overview
|
||||
/users — 代理用户列表 / Proxy user list
|
||||
/online — 当前在线用户 / Online users
|
||||
/listeners — 入站节点列表 / Inbound listeners
|
||||
/traffic — 流量快照 / Traffic snapshot
|
||||
/restart — 重启 Mihomo 核心 / Restart core
|
||||
/deldepleted — 清理到期/超额用户 / Delete depleted users
|
||||
/search <关键字> — 按用户名/备注搜索 / Search by username/remark
|
||||
/backup — 备份提示 / Backup hint
|
||||
/id — 显示你的 Telegram ID / Show your Telegram ID
|
||||
/usage — 查询用量 (用户) 或搜索 (管理员) / Usage (user) or search (admin)
|
||||
/help — 本帮助 / This help
|
||||
`)
|
||||
func helpText(lang string) string {
|
||||
return strings.TrimSpace(Tr(lang, "help"))
|
||||
}
|
||||
|
||||
func (b *Bot) cmdStatus() string {
|
||||
@@ -328,12 +314,12 @@ func (b *Bot) cmdStatus() string {
|
||||
dbWarn = fmt.Sprintf("\n⚠️ DB error: %s", escapeHTML(err.Error()))
|
||||
}
|
||||
}
|
||||
core := "stopped"
|
||||
core := Tr(b.lang(), "core_stopped")
|
||||
if running {
|
||||
core = "running"
|
||||
core = Tr(b.lang(), "core_running")
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"📊 <b>Status</b>\ncore: <code>%s</code>\nversion: <code>%s</code>\npid: <code>%d</code>\nusers: %d (online %d, blocked %d)\nlisteners: %d%s",
|
||||
Tr(b.lang(), "status_title"),
|
||||
core, escapeHTML(version), pid, userCount, online, blocked, listeners, dbWarn,
|
||||
)
|
||||
}
|
||||
@@ -341,13 +327,13 @@ func (b *Bot) cmdStatus() string {
|
||||
func (b *Bot) cmdUsers() string {
|
||||
var users []models.ProxyUser
|
||||
if err := b.db.Order("id asc").Limit(40).Find(&users).Error; err != nil {
|
||||
return "读取用户失败: " + escapeHTML(err.Error())
|
||||
return Trf(b.lang(), "users_fail", escapeHTML(err.Error()))
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return "暂无代理用户。"
|
||||
return Tr(b.lang(), "users_empty")
|
||||
}
|
||||
var bld strings.Builder
|
||||
bld.WriteString("👥 <b>Users</b>\n")
|
||||
bld.WriteString(Tr(b.lang(), "users_title") + "\n")
|
||||
for _, u := range users {
|
||||
flag := "✅"
|
||||
if !user.IsCredentialActive(u) {
|
||||
@@ -368,13 +354,13 @@ func (b *Bot) cmdUsers() string {
|
||||
func (b *Bot) cmdOnline() string {
|
||||
var users []models.ProxyUser
|
||||
if err := b.db.Where("online = ?", true).Order("id asc").Find(&users).Error; err != nil {
|
||||
return "读取在线用户失败: " + escapeHTML(err.Error())
|
||||
return Trf(b.lang(), "users_fail", escapeHTML(err.Error()))
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return "当前无在线用户。"
|
||||
return Tr(b.lang(), "online_empty")
|
||||
}
|
||||
var bld strings.Builder
|
||||
bld.WriteString("🟢 <b>Online</b>\n")
|
||||
bld.WriteString(Tr(b.lang(), "online_title") + "\n")
|
||||
for _, u := range users {
|
||||
bld.WriteString(fmt.Sprintf("• <code>%s</code>\n", escapeHTML(u.Username)))
|
||||
}
|
||||
@@ -384,13 +370,13 @@ func (b *Bot) cmdOnline() string {
|
||||
func (b *Bot) cmdListeners() string {
|
||||
var list []models.Listener
|
||||
if err := b.db.Order("id asc").Limit(40).Find(&list).Error; err != nil {
|
||||
return "读取节点失败: " + escapeHTML(err.Error())
|
||||
return Trf(b.lang(), "users_fail", escapeHTML(err.Error()))
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return "暂无节点。"
|
||||
return Tr(b.lang(), "listeners_empty")
|
||||
}
|
||||
var bld strings.Builder
|
||||
bld.WriteString("📡 <b>Listeners</b>\n")
|
||||
bld.WriteString(Tr(b.lang(), "listeners_title") + "\n")
|
||||
for _, n := range list {
|
||||
en := "off"
|
||||
if n.Enabled {
|
||||
@@ -412,13 +398,13 @@ func (b *Bot) cmdTraffic() string {
|
||||
total += u.TrafficUsed
|
||||
}
|
||||
var bld strings.Builder
|
||||
bld.WriteString(fmt.Sprintf("📈 <b>Traffic</b> (top users)\napprox listed used sum: %s\n", formatBytes(total)))
|
||||
bld.WriteString(Trf(b.lang(), "traffic_title", formatBytes(total)))
|
||||
for _, u := range users {
|
||||
bld.WriteString(fmt.Sprintf("• <code>%s</code> ↑%s ↓%s\n",
|
||||
escapeHTML(u.Username), formatBytes(u.UploadBytes), formatBytes(u.DownloadBytes)))
|
||||
}
|
||||
if len(users) == 0 {
|
||||
bld.WriteString("暂无数据。")
|
||||
bld.WriteString(Tr(b.lang(), "traffic_empty"))
|
||||
}
|
||||
if dbWarn != "" {
|
||||
bld.WriteString(dbWarn)
|
||||
@@ -442,38 +428,38 @@ func formatBytes(n int64) string {
|
||||
|
||||
func (b *Bot) cmdRestart() string {
|
||||
if b.mihomo == nil {
|
||||
return "Mihomo 服务未初始化。"
|
||||
return Tr(b.lang(), "mihomo_nil")
|
||||
}
|
||||
if err := b.mihomo.RestartMihomo(); err != nil {
|
||||
return "重启失败: " + escapeHTML(err.Error())
|
||||
return Trf(b.lang(), "restart_fail", escapeHTML(err.Error()))
|
||||
}
|
||||
return "✅ Mihomo 核心已重启。"
|
||||
return Tr(b.lang(), "restart_ok")
|
||||
}
|
||||
|
||||
func (b *Bot) cmdDelDepleted() string {
|
||||
svc := user.NewService(b.db)
|
||||
n, err := svc.DeleteDepleted()
|
||||
if err != nil {
|
||||
return "清理失败: " + escapeHTML(err.Error())
|
||||
return Trf(b.lang(), "cleanup_fail", escapeHTML(err.Error()))
|
||||
}
|
||||
return fmt.Sprintf("🧹 已删除 %d 个到期/超额用户。", n)
|
||||
return Trf(b.lang(), "cleanup_ok", n)
|
||||
}
|
||||
|
||||
func (b *Bot) cmdSearch(q string) string {
|
||||
q = strings.TrimSpace(q)
|
||||
if q == "" {
|
||||
return "用法: /search <用户名或备注关键字>"
|
||||
return Tr(b.lang(), "search_usage")
|
||||
}
|
||||
svc := user.NewService(b.db)
|
||||
users, err := svc.ListFiltered(user.ListFilter{Query: q})
|
||||
if err != nil {
|
||||
return "搜索失败: " + escapeHTML(err.Error())
|
||||
return Trf(b.lang(), "search_fail", escapeHTML(err.Error()))
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return "未找到匹配用户。"
|
||||
return Tr(b.lang(), "search_empty")
|
||||
}
|
||||
var bld strings.Builder
|
||||
bld.WriteString(fmt.Sprintf("🔍 <b>Search</b> <code>%s</code> (%d)\n", escapeHTML(q), len(users)))
|
||||
bld.WriteString(Trf(b.lang(), "search_title", escapeHTML(q), len(users)))
|
||||
for i, u := range users {
|
||||
if i >= 20 {
|
||||
bld.WriteString("…\n")
|
||||
@@ -491,8 +477,5 @@ func (b *Bot) cmdSearch(q string) string {
|
||||
}
|
||||
|
||||
func (b *Bot) cmdBackup() string {
|
||||
return strings.TrimSpace(`📦 <b>Backup</b>
|
||||
请在面板「系统设置 → 备份」下载完整备份(SQLite + Mihomo 配置)。
|
||||
Use panel Settings → Backup to download a full zip (database + Mihomo config).
|
||||
API: <code>GET /api/v1/system/backup</code>`)
|
||||
return strings.TrimSpace(Tr(b.lang(), "backup"))
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Supported bot language codes (aligned with the panel's 18 locales).
|
||||
var SupportedBotLangs = []string{
|
||||
"en", "zh-CN", "zh-TW", "ja", "ko", "es", "fr", "de", "ru",
|
||||
"pt-BR", "vi", "id", "th", "tr", "ar", "hi", "pl", "uk",
|
||||
}
|
||||
|
||||
// NormalizeLang maps panel / settings values onto SupportedBotLangs.
|
||||
func NormalizeLang(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return "en"
|
||||
}
|
||||
low := strings.ToLower(strings.ReplaceAll(s, "_", "-"))
|
||||
switch low {
|
||||
case "zh", "zh-cn", "cn", "chinese":
|
||||
return "zh-CN"
|
||||
case "zh-tw", "zh-hk", "tw", "hk":
|
||||
return "zh-TW"
|
||||
case "pt", "pt-br", "pt_br":
|
||||
return "pt-BR"
|
||||
case "jp":
|
||||
return "ja"
|
||||
case "ua":
|
||||
return "uk"
|
||||
}
|
||||
for _, code := range SupportedBotLangs {
|
||||
if strings.EqualFold(code, s) || strings.EqualFold(code, low) {
|
||||
return code
|
||||
}
|
||||
}
|
||||
// Prefix match e.g. en-US → en
|
||||
for _, code := range SupportedBotLangs {
|
||||
if strings.HasPrefix(low, strings.ToLower(code)+"-") || strings.HasPrefix(low, strings.ToLower(code)+"_") {
|
||||
return code
|
||||
}
|
||||
}
|
||||
return "en"
|
||||
}
|
||||
|
||||
// Tr returns a localized string for key. Falls back to English, then the key.
|
||||
func Tr(lang, key string) string {
|
||||
lang = NormalizeLang(lang)
|
||||
if m, ok := botMessages[lang]; ok {
|
||||
if v, ok := m[key]; ok && v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
if m, ok := botMessages["en"]; ok {
|
||||
if v, ok := m[key]; ok && v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// Trf is Tr + fmt.Sprintf.
|
||||
func Trf(lang, key string, args ...any) string {
|
||||
return fmt.Sprintf(Tr(lang, key), args...)
|
||||
}
|
||||
|
||||
// botMessages is lang → key → text. Protocol/command names stay in Latin script.
|
||||
var botMessages = map[string]map[string]string{
|
||||
"en": {
|
||||
"help": `🤖 <b>3m-ui Telegram Bot</b>
|
||||
/id — Show your Telegram ID
|
||||
/usage — My usage (user) or search users (admin)
|
||||
/status — Panel & core status (admin)
|
||||
/users /online /listeners /traffic — Admin lists
|
||||
/restart — Restart Mihomo (admin)
|
||||
/deldepleted — Remove expired/over-quota users (admin)
|
||||
/search <q> — Search users (admin)
|
||||
/backup — Backup hint (admin)
|
||||
/help — This help`,
|
||||
"unknown_cmd": "Unknown command. Send /help for the list.",
|
||||
"perm_denied": "⛔ Admin-only command.\nAvailable: /id /usage /help",
|
||||
"your_id": "🆔 Your Telegram ID: <code>%d</code>",
|
||||
"usage_admin_hint": "Usage: /usage <keyword> — search proxy users",
|
||||
"sub_fail": "⚠️ Could not generate subscription link. Try again later.",
|
||||
"sub_link": "🔗 <b>Subscription URL</b>\n<code>%s</code>",
|
||||
"not_bound": "Not bound. Ask an admin to link Telegram ID <code>%d</code> to your account.",
|
||||
"my_usage_title": "📊 <b>My Usage</b>",
|
||||
"user_label": "User: <code>%s</code>",
|
||||
"traffic_label": "Traffic: %s / %s",
|
||||
"up_down": "↑ %s ↓ %s",
|
||||
"expires": "Expires: <code>%s</code>",
|
||||
"expires_never": "Expires: Never",
|
||||
"status_online": "Status: Online 🟢",
|
||||
"status_offline": "Status: Offline",
|
||||
"sub_line": "Subscription:\n<code>%s</code>",
|
||||
"sub_missing": "Subscription: not generated yet",
|
||||
"unknown_action": "Unknown action.",
|
||||
"status_title": "📊 <b>Status</b>\ncore: <code>%s</code>\nversion: <code>%s</code>\npid: <code>%d</code>\nusers: %d (online %d, blocked %d)\nlisteners: %d%s",
|
||||
"core_running": "running",
|
||||
"core_stopped": "stopped",
|
||||
"users_fail": "Failed to load users: %s",
|
||||
"users_empty": "No proxy users.",
|
||||
"users_title": "👥 <b>Users</b>",
|
||||
"online_title": "🟢 <b>Online</b>",
|
||||
"online_empty": "No online users.",
|
||||
"listeners_title": "📡 <b>Listeners</b>",
|
||||
"listeners_empty": "No listeners.",
|
||||
"traffic_title": "📈 <b>Traffic</b> (top users)\napprox listed used sum: %s\n",
|
||||
"traffic_empty": "No data.",
|
||||
"mihomo_nil": "Mihomo service is not initialized.",
|
||||
"restart_fail": "Restart failed: %s",
|
||||
"restart_ok": "✅ Mihomo core restarted.",
|
||||
"cleanup_fail": "Cleanup failed: %s",
|
||||
"cleanup_ok": "🧹 Removed %d expired/over-quota user(s).",
|
||||
"search_usage": "Usage: /search <username or remark>",
|
||||
"search_fail": "Search failed: %s",
|
||||
"search_empty": "No matching users.",
|
||||
"search_title": "🔍 <b>Search</b> <code>%s</code> (%d)\n",
|
||||
"backup": "📦 <b>Backup</b>\nDownload a full backup in the panel: Settings → Backup (SQLite + Mihomo config).\nAPI: <code>GET /api/v1/system/backup</code>",
|
||||
"btn_status": "📊 Status",
|
||||
"btn_users": "👥 Users",
|
||||
"btn_traffic": "📈 Traffic",
|
||||
"btn_online": "🟢 Online",
|
||||
"btn_listeners": "📡 Nodes",
|
||||
"btn_cleanup": "🧹 Cleanup",
|
||||
"btn_backup": "📦 Backup",
|
||||
"btn_restart": "🔄 Restart",
|
||||
"btn_my_usage": "📊 My usage",
|
||||
"btn_my_sub": "🔗 Subscription",
|
||||
"cmd_start": "Start / help",
|
||||
"cmd_id": "Show Telegram ID",
|
||||
"cmd_usage": "Usage or search users",
|
||||
"cmd_status": "Panel status",
|
||||
"cmd_users": "List users",
|
||||
"cmd_online": "Online users",
|
||||
"cmd_listeners": "Inbound listeners",
|
||||
"cmd_traffic": "Traffic summary",
|
||||
"cmd_restart": "Restart Mihomo",
|
||||
"cmd_deldepleted": "Remove depleted users",
|
||||
"cmd_search": "Search users",
|
||||
"cmd_backup": "Backup hint",
|
||||
"test_msg": "🔔 <b>3m-ui</b> Telegram test — connection OK.",
|
||||
},
|
||||
"zh-CN": {
|
||||
"help": `🤖 <b>3m-ui Telegram 机器人</b>
|
||||
/id — 显示你的 Telegram ID
|
||||
/usage — 查询用量(用户)或搜索用户(管理员)
|
||||
/status — 面板与核心状态(管理员)
|
||||
/users /online /listeners /traffic — 管理列表
|
||||
/restart — 重启 Mihomo(管理员)
|
||||
/deldepleted — 清理到期/超额用户(管理员)
|
||||
/search <关键字> — 搜索用户(管理员)
|
||||
/backup — 备份说明(管理员)
|
||||
/help — 本帮助`,
|
||||
"unknown_cmd": "未知指令。发送 /help 查看可用命令。", "perm_denied": "⛔ 此命令仅管理员可用。\n可用: /id /usage /help",
|
||||
"your_id": "🆔 你的 Telegram ID: <code>%d</code>", "usage_admin_hint": "用法: /usage <关键字> — 搜索代理用户",
|
||||
"sub_fail": "⚠️ 订阅链接生成失败,请稍后重试。", "sub_link": "🔗 <b>订阅链接</b>\n<code>%s</code>",
|
||||
"not_bound": "未绑定账户。请联系管理员将 Telegram ID <code>%d</code> 绑定到你的账户。",
|
||||
"my_usage_title": "📊 <b>账户用量</b>", "user_label": "用户: <code>%s</code>", "traffic_label": "流量: %s / %s",
|
||||
"up_down": "上传 ↑ %s 下载 ↓ %s", "expires": "到期: <code>%s</code>", "expires_never": "到期: 永不过期",
|
||||
"status_online": "状态: 在线 🟢", "status_offline": "状态: 离线", "sub_line": "订阅:\n<code>%s</code>", "sub_missing": "订阅: 未生成",
|
||||
"unknown_action": "未知操作。",
|
||||
"status_title": "📊 <b>状态</b>\n核心: <code>%s</code>\n版本: <code>%s</code>\npid: <code>%d</code>\n用户: %d(在线 %d,不可用 %d)\n节点: %d%s",
|
||||
"core_running": "运行中", "core_stopped": "已停止",
|
||||
"users_fail": "读取用户失败: %s", "users_empty": "暂无代理用户。", "users_title": "👥 <b>用户</b>",
|
||||
"online_title": "🟢 <b>在线</b>", "online_empty": "暂无在线用户。",
|
||||
"listeners_title": "📡 <b>节点</b>", "listeners_empty": "暂无节点。",
|
||||
"traffic_title": "📈 <b>流量</b>(用量前列)\n列表合计约: %s\n", "traffic_empty": "暂无数据。",
|
||||
"mihomo_nil": "Mihomo 服务未初始化。", "restart_fail": "重启失败: %s", "restart_ok": "✅ Mihomo 核心已重启。",
|
||||
"cleanup_fail": "清理失败: %s", "cleanup_ok": "🧹 已删除 %d 个到期/超额用户。",
|
||||
"search_usage": "用法: /search <用户名或备注>", "search_fail": "搜索失败: %s", "search_empty": "未找到匹配用户。",
|
||||
"search_title": "🔍 <b>搜索</b> <code>%s</code>(%d)\n",
|
||||
"backup": "📦 <b>备份</b>\n请在面板「系统设置 → 备份」下载完整备份(SQLite + Mihomo 配置)。\nAPI: <code>GET /api/v1/system/backup</code>",
|
||||
"btn_status": "📊 状态", "btn_users": "👥 用户", "btn_traffic": "📈 流量", "btn_online": "🟢 在线",
|
||||
"btn_listeners": "📡 节点", "btn_cleanup": "🧹 清理", "btn_backup": "📦 备份", "btn_restart": "🔄 重启",
|
||||
"btn_my_usage": "📊 我的用量", "btn_my_sub": "🔗 订阅链接",
|
||||
"cmd_start": "开始 / 帮助", "cmd_id": "显示 Telegram ID", "cmd_usage": "用量或搜索用户", "cmd_status": "面板状态",
|
||||
"cmd_users": "用户列表", "cmd_online": "在线用户", "cmd_listeners": "入站节点", "cmd_traffic": "流量摘要",
|
||||
"cmd_restart": "重启 Mihomo", "cmd_deldepleted": "清理失效用户", "cmd_search": "搜索用户", "cmd_backup": "备份说明",
|
||||
"test_msg": "🔔 <b>3m-ui</b> Telegram 测试消息 — 连接正常。",
|
||||
"user_welcome": "👋 欢迎!点击下方按钮查看用量与订阅链接。",
|
||||
},
|
||||
"zh-TW": {
|
||||
"help": `🤖 <b>3m-ui Telegram 機器人</b>
|
||||
/id — 顯示 Telegram ID
|
||||
/usage — 查詢用量或搜尋使用者
|
||||
/status — 面板與核心狀態(管理員)
|
||||
/help — 本說明`,
|
||||
"unknown_cmd": "未知指令。傳送 /help 查看可用命令。", "perm_denied": "⛔ 此命令僅管理員可用。\n可用: /id /usage /help",
|
||||
"your_id": "🆔 你的 Telegram ID: <code>%d</code>", "usage_admin_hint": "用法: /usage <關鍵字>",
|
||||
"sub_fail": "⚠️ 訂閱連結產生失敗。", "sub_link": "🔗 <b>訂閱連結</b>\n<code>%s</code>",
|
||||
"not_bound": "未綁定帳戶。請管理員綁定 Telegram ID <code>%d</code>。",
|
||||
"my_usage_title": "📊 <b>帳戶用量</b>", "user_label": "使用者: <code>%s</code>", "traffic_label": "流量: %s / %s",
|
||||
"up_down": "上傳 ↑ %s 下載 ↓ %s", "expires": "到期: <code>%s</code>", "expires_never": "到期: 永不過期",
|
||||
"status_online": "狀態: 在線 🟢", "status_offline": "狀態: 離線", "sub_line": "訂閱:\n<code>%s</code>", "sub_missing": "訂閱: 未產生",
|
||||
"unknown_action": "未知操作。", "status_title": "📊 <b>狀態</b>\n核心: <code>%s</code>\n版本: <code>%s</code>\npid: <code>%d</code>\n使用者: %d(在線 %d,不可用 %d)\n節點: %d%s",
|
||||
"core_running": "運行中", "core_stopped": "已停止", "users_fail": "讀取使用者失敗: %s", "users_empty": "暫無代理使用者。", "users_title": "👥 <b>使用者</b>",
|
||||
"online_title": "🟢 <b>在線</b>", "online_empty": "暫無在線使用者。", "listeners_title": "📡 <b>節點</b>", "listeners_empty": "暫無節點。",
|
||||
"traffic_title": "📈 <b>流量</b>\n列表合計約: %s\n", "traffic_empty": "暫無資料。",
|
||||
"mihomo_nil": "Mihomo 服務未初始化。", "restart_fail": "重啟失敗: %s", "restart_ok": "✅ Mihomo 核心已重啟。",
|
||||
"cleanup_fail": "清理失敗: %s", "cleanup_ok": "🧹 已刪除 %d 個到期/超額使用者。",
|
||||
"search_usage": "用法: /search <使用者名稱或備註>", "search_fail": "搜尋失敗: %s", "search_empty": "未找到相符使用者。",
|
||||
"search_title": "🔍 <b>搜尋</b> <code>%s</code>(%d)\n",
|
||||
"backup": "📦 <b>備份</b>\n請在面板「系統設定 → 備份」下載完整備份。\nAPI: <code>GET /api/v1/system/backup</code>",
|
||||
"btn_status": "📊 狀態", "btn_users": "👥 使用者", "btn_traffic": "📈 流量", "btn_online": "🟢 在線",
|
||||
"btn_listeners": "📡 節點", "btn_cleanup": "🧹 清理", "btn_backup": "📦 備份", "btn_restart": "🔄 重啟",
|
||||
"btn_my_usage": "📊 我的用量", "btn_my_sub": "🔗 訂閱連結",
|
||||
"cmd_start": "開始 / 說明", "cmd_id": "顯示 Telegram ID", "cmd_usage": "用量或搜尋", "cmd_status": "面板狀態",
|
||||
"cmd_users": "使用者列表", "cmd_online": "在線使用者", "cmd_listeners": "入站節點", "cmd_traffic": "流量摘要",
|
||||
"cmd_restart": "重啟 Mihomo", "cmd_deldepleted": "清理失效使用者", "cmd_search": "搜尋使用者", "cmd_backup": "備份說明",
|
||||
"test_msg": "🔔 <b>3m-ui</b> Telegram 測試 — 連線正常。",
|
||||
"user_welcome": "👋 歡迎!點下方按鈕查看用量與訂閱連結。",
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Seed remaining languages from English, then overlay high-traffic keys.
|
||||
overlays := map[string]map[string]string{
|
||||
"ja": {
|
||||
"unknown_cmd": "不明なコマンドです。/help で一覧を表示します。", "perm_denied": "⛔ 管理者専用コマンドです。\n利用可: /id /usage /help",
|
||||
"your_id": "🆔 Telegram ID: <code>%d</code>", "sub_fail": "⚠️ 購読リンクを生成できませんでした。", "sub_link": "🔗 <b>購読 URL</b>\n<code>%s</code>",
|
||||
"not_bound": "未連携です。管理者に Telegram ID <code>%d</code> の紐付けを依頼してください。",
|
||||
"my_usage_title": "📊 <b>利用状況</b>", "user_label": "ユーザー: <code>%s</code>", "traffic_label": "トラフィック: %s / %s",
|
||||
"expires_never": "期限: なし", "status_online": "状態: オンライン 🟢", "status_offline": "状態: オフライン",
|
||||
"unknown_action": "不明な操作です。", "core_running": "稼働中", "core_stopped": "停止",
|
||||
"users_empty": "プロキシユーザーがありません。", "users_title": "👥 <b>ユーザー</b>",
|
||||
"online_title": "🟢 <b>オンライン</b>", "online_empty": "オンラインのユーザーはいません。",
|
||||
"listeners_title": "📡 <b>ノード</b>", "listeners_empty": "ノードがありません。",
|
||||
"traffic_empty": "データがありません。", "restart_ok": "✅ Mihomo を再起動しました。",
|
||||
"cleanup_ok": "🧹 期限切れ/超過ユーザーを %d 件削除しました。", "search_empty": "一致するユーザーがありません。",
|
||||
"btn_status": "📊 状態", "btn_users": "👥 ユーザー", "btn_traffic": "📈 トラフィック", "btn_online": "🟢 オンライン",
|
||||
"btn_listeners": "📡 ノード", "btn_cleanup": "🧹 整理", "btn_backup": "📦 バックアップ", "btn_restart": "🔄 再起動",
|
||||
"btn_my_usage": "📊 利用状況", "btn_my_sub": "🔗 購読", "test_msg": "🔔 <b>3m-ui</b> Telegram テスト — 接続 OK。",
|
||||
"cmd_start": "開始 / ヘルプ", "cmd_status": "パネル状態", "cmd_restart": "Mihomo 再起動",
|
||||
},
|
||||
"ko": {
|
||||
"unknown_cmd": "알 수 없는 명령입니다. /help 를 보내세요.", "perm_denied": "⛔ 관리자 전용 명령입니다.\n사용 가능: /id /usage /help",
|
||||
"your_id": "🆔 Telegram ID: <code>%d</code>", "sub_fail": "⚠️ 구독 링크를 만들 수 없습니다.", "sub_link": "🔗 <b>구독 URL</b>\n<code>%s</code>",
|
||||
"not_bound": "연결되지 않았습니다. 관리자에게 Telegram ID <code>%d</code> 연결을 요청하세요.",
|
||||
"my_usage_title": "📊 <b>사용량</b>", "core_running": "실행 중", "core_stopped": "중지",
|
||||
"users_title": "👥 <b>사용자</b>", "online_title": "🟢 <b>온라인</b>", "listeners_title": "📡 <b>노드</b>",
|
||||
"restart_ok": "✅ Mihomo 코어가 재시작되었습니다.", "cleanup_ok": "🧹 만료/초과 사용자 %d명을 삭제했습니다.",
|
||||
"btn_status": "📊 상태", "btn_users": "👥 사용자", "btn_traffic": "📈 트래픽", "btn_online": "🟢 온라인",
|
||||
"btn_listeners": "📡 노드", "btn_cleanup": "🧹 정리", "btn_backup": "📦 백업", "btn_restart": "🔄 재시작",
|
||||
"btn_my_usage": "📊 내 사용량", "btn_my_sub": "🔗 구독", "test_msg": "🔔 <b>3m-ui</b> Telegram 테스트 — 연결 OK.",
|
||||
},
|
||||
"es": {
|
||||
"unknown_cmd": "Comando desconocido. Envía /help.", "perm_denied": "⛔ Solo administradores.\nDisponible: /id /usage /help",
|
||||
"your_id": "🆔 Tu ID de Telegram: <code>%d</code>", "sub_fail": "⚠️ No se pudo generar el enlace de suscripción.",
|
||||
"sub_link": "🔗 <b>URL de suscripción</b>\n<code>%s</code>", "not_bound": "No vinculado. Pide al admin enlazar el ID <code>%d</code>.",
|
||||
"my_usage_title": "📊 <b>Mi uso</b>", "core_running": "en ejecución", "core_stopped": "detenido",
|
||||
"users_title": "👥 <b>Usuarios</b>", "online_title": "🟢 <b>En línea</b>", "listeners_title": "📡 <b>Nodos</b>",
|
||||
"restart_ok": "✅ Núcleo Mihomo reiniciado.", "cleanup_ok": "🧹 Se eliminaron %d usuario(s) caducados/sobre cuota.",
|
||||
"btn_status": "📊 Estado", "btn_users": "👥 Usuarios", "btn_traffic": "📈 Tráfico", "btn_online": "🟢 En línea",
|
||||
"btn_listeners": "📡 Nodos", "btn_cleanup": "🧹 Limpieza", "btn_backup": "📦 Copia", "btn_restart": "🔄 Reiniciar",
|
||||
"btn_my_usage": "📊 Mi uso", "btn_my_sub": "🔗 Suscripción", "test_msg": "🔔 <b>3m-ui</b> prueba Telegram — conexión OK.",
|
||||
},
|
||||
"fr": {
|
||||
"unknown_cmd": "Commande inconnue. Envoyez /help.", "perm_denied": "⛔ Réservé aux admins.\nDispo: /id /usage /help",
|
||||
"your_id": "🆔 Votre ID Telegram: <code>%d</code>", "sub_fail": "⚠️ Impossible de générer le lien d'abonnement.",
|
||||
"sub_link": "🔗 <b>URL d'abonnement</b>\n<code>%s</code>", "my_usage_title": "📊 <b>Mon usage</b>",
|
||||
"core_running": "en cours", "core_stopped": "arrêté", "restart_ok": "✅ Noyau Mihomo redémarré.",
|
||||
"btn_status": "📊 État", "btn_users": "👥 Utilisateurs", "btn_traffic": "📈 Trafic", "btn_online": "🟢 En ligne",
|
||||
"btn_listeners": "📡 Nœuds", "btn_cleanup": "🧹 Nettoyage", "btn_backup": "📦 Sauvegarde", "btn_restart": "🔄 Redémarrer",
|
||||
"btn_my_usage": "📊 Mon usage", "btn_my_sub": "🔗 Abonnement", "test_msg": "🔔 <b>3m-ui</b> test Telegram — connexion OK.",
|
||||
},
|
||||
"de": {
|
||||
"unknown_cmd": "Unbekannter Befehl. Sende /help.", "perm_denied": "⛔ Nur für Admins.\nVerfügbar: /id /usage /help",
|
||||
"your_id": "🆔 Deine Telegram-ID: <code>%d</code>", "sub_fail": "⚠️ Abo-Link konnte nicht erzeugt werden.",
|
||||
"sub_link": "🔗 <b>Abo-URL</b>\n<code>%s</code>", "my_usage_title": "📊 <b>Mein Verbrauch</b>",
|
||||
"core_running": "läuft", "core_stopped": "gestoppt", "restart_ok": "✅ Mihomo-Core neu gestartet.",
|
||||
"btn_status": "📊 Status", "btn_users": "👥 Benutzer", "btn_traffic": "📈 Traffic", "btn_online": "🟢 Online",
|
||||
"btn_listeners": "📡 Knoten", "btn_cleanup": "🧹 Aufräumen", "btn_backup": "📦 Backup", "btn_restart": "🔄 Neustart",
|
||||
"btn_my_usage": "📊 Verbrauch", "btn_my_sub": "🔗 Abo", "test_msg": "🔔 <b>3m-ui</b> Telegram-Test — Verbindung OK.",
|
||||
},
|
||||
"ru": {
|
||||
"unknown_cmd": "Неизвестная команда. Отправьте /help.", "perm_denied": "⛔ Только для админов.\nДоступно: /id /usage /help",
|
||||
"your_id": "🆔 Ваш Telegram ID: <code>%d</code>", "sub_fail": "⚠️ Не удалось создать ссылку подписки.",
|
||||
"sub_link": "🔗 <b>URL подписки</b>\n<code>%s</code>", "my_usage_title": "📊 <b>Мой трафик</b>",
|
||||
"core_running": "работает", "core_stopped": "остановлен", "restart_ok": "✅ Ядро Mihomo перезапущено.",
|
||||
"btn_status": "📊 Статус", "btn_users": "👥 Пользователи", "btn_traffic": "📈 Трафик", "btn_online": "🟢 Онлайн",
|
||||
"btn_listeners": "📡 Узлы", "btn_cleanup": "🧹 Очистка", "btn_backup": "📦 Бэкап", "btn_restart": "🔄 Перезапуск",
|
||||
"btn_my_usage": "📊 Мой трафик", "btn_my_sub": "🔗 Подписка", "test_msg": "🔔 <b>3m-ui</b> тест Telegram — соединение OK.",
|
||||
},
|
||||
"pt-BR": {
|
||||
"unknown_cmd": "Comando desconhecido. Envie /help.", "perm_denied": "⛔ Somente admin.\nDisponível: /id /usage /help",
|
||||
"your_id": "🆔 Seu ID do Telegram: <code>%d</code>", "sub_fail": "⚠️ Não foi possível gerar o link de assinatura.",
|
||||
"sub_link": "🔗 <b>URL de assinatura</b>\n<code>%s</code>", "my_usage_title": "📊 <b>Meu uso</b>",
|
||||
"core_running": "em execução", "core_stopped": "parado", "restart_ok": "✅ Núcleo Mihomo reiniciado.",
|
||||
"btn_status": "📊 Status", "btn_users": "👥 Usuários", "btn_traffic": "📈 Tráfego", "btn_online": "🟢 Online",
|
||||
"btn_listeners": "📡 Nós", "btn_cleanup": "🧹 Limpeza", "btn_backup": "📦 Backup", "btn_restart": "🔄 Reiniciar",
|
||||
"btn_my_usage": "📊 Meu uso", "btn_my_sub": "🔗 Assinatura", "test_msg": "🔔 <b>3m-ui</b> teste Telegram — conexão OK.",
|
||||
},
|
||||
"vi": {
|
||||
"unknown_cmd": "Lệnh không rõ. Gửi /help.", "perm_denied": "⛔ Chỉ admin.\nCó thể dùng: /id /usage /help",
|
||||
"your_id": "🆔 Telegram ID: <code>%d</code>", "sub_fail": "⚠️ Không tạo được link subscription.",
|
||||
"sub_link": "🔗 <b>URL subscription</b>\n<code>%s</code>", "my_usage_title": "📊 <b>Lưu lượng của tôi</b>",
|
||||
"core_running": "đang chạy", "core_stopped": "đã dừng", "restart_ok": "✅ Đã khởi động lại Mihomo.",
|
||||
"btn_status": "📊 Trạng thái", "btn_users": "👥 Người dùng", "btn_traffic": "📈 Lưu lượng", "btn_online": "🟢 Online",
|
||||
"btn_listeners": "📡 Node", "btn_cleanup": "🧹 Dọn", "btn_backup": "📦 Sao lưu", "btn_restart": "🔄 Khởi động lại",
|
||||
"btn_my_usage": "📊 Lưu lượng", "btn_my_sub": "🔗 Subscription", "test_msg": "🔔 <b>3m-ui</b> thử Telegram — kết nối OK.",
|
||||
},
|
||||
"id": {
|
||||
"unknown_cmd": "Perintah tidak dikenal. Kirim /help.", "perm_denied": "⛔ Hanya admin.\nTersedia: /id /usage /help",
|
||||
"your_id": "🆔 ID Telegram Anda: <code>%d</code>", "sub_fail": "⚠️ Gagal membuat tautan langganan.",
|
||||
"sub_link": "🔗 <b>URL langganan</b>\n<code>%s</code>", "my_usage_title": "📊 <b>Pemakaian saya</b>",
|
||||
"core_running": "berjalan", "core_stopped": "berhenti", "restart_ok": "✅ Core Mihomo dimulai ulang.",
|
||||
"btn_status": "📊 Status", "btn_users": "👥 Pengguna", "btn_traffic": "📈 Lalu lintas", "btn_online": "🟢 Online",
|
||||
"btn_listeners": "📡 Node", "btn_cleanup": "🧹 Bersihkan", "btn_backup": "📦 Cadangan", "btn_restart": "🔄 Mulai ulang",
|
||||
"btn_my_usage": "📊 Pemakaian", "btn_my_sub": "🔗 Langganan", "test_msg": "🔔 <b>3m-ui</b> tes Telegram — koneksi OK.",
|
||||
},
|
||||
"th": {
|
||||
"unknown_cmd": "คำสั่งไม่รู้จัก ส่ง /help", "perm_denied": "⛔ สำหรับแอดมินเท่านั้น\nใช้ได้: /id /usage /help",
|
||||
"your_id": "🆔 Telegram ID: <code>%d</code>", "sub_fail": "⚠️ สร้างลิงก์สมัครไม่สำเร็จ",
|
||||
"sub_link": "🔗 <b>URL สมัครสมาชิก</b>\n<code>%s</code>", "my_usage_title": "📊 <b>การใช้งานของฉัน</b>",
|
||||
"core_running": "กำลังทำงาน", "core_stopped": "หยุดแล้ว", "restart_ok": "✅ รีสตาร์ท Mihomo แล้ว",
|
||||
"btn_status": "📊 สถานะ", "btn_users": "👥 ผู้ใช้", "btn_traffic": "📈 ทราฟฟิก", "btn_online": "🟢 ออนไลน์",
|
||||
"btn_listeners": "📡 โหนด", "btn_cleanup": "🧹 ล้าง", "btn_backup": "📦 สำรอง", "btn_restart": "🔄 รีสตาร์ท",
|
||||
"btn_my_usage": "📊 การใช้งาน", "btn_my_sub": "🔗 สมัครสมาชิก", "test_msg": "🔔 <b>3m-ui</b> ทดสอบ Telegram — เชื่อมต่อ OK",
|
||||
},
|
||||
"tr": {
|
||||
"unknown_cmd": "Bilinmeyen komut. /help gönderin.", "perm_denied": "⛔ Yalnızca yönetici.\nKullanılabilir: /id /usage /help",
|
||||
"your_id": "🆔 Telegram ID: <code>%d</code>", "sub_fail": "⚠️ Abonelik bağlantısı oluşturulamadı.",
|
||||
"sub_link": "🔗 <b>Abonelik URL</b>\n<code>%s</code>", "my_usage_title": "📊 <b>Kullanımım</b>",
|
||||
"core_running": "çalışıyor", "core_stopped": "durdu", "restart_ok": "✅ Mihomo çekirdeği yeniden başlatıldı.",
|
||||
"btn_status": "📊 Durum", "btn_users": "👥 Kullanıcılar", "btn_traffic": "📈 Trafik", "btn_online": "🟢 Çevrimiçi",
|
||||
"btn_listeners": "📡 Düğümler", "btn_cleanup": "🧹 Temizle", "btn_backup": "📦 Yedek", "btn_restart": "🔄 Yeniden başlat",
|
||||
"btn_my_usage": "📊 Kullanım", "btn_my_sub": "🔗 Abonelik", "test_msg": "🔔 <b>3m-ui</b> Telegram testi — bağlantı OK.",
|
||||
},
|
||||
"ar": {
|
||||
"unknown_cmd": "أمر غير معروف. أرسل /help.", "perm_denied": "⛔ للمشرفين فقط.\nمتاح: /id /usage /help",
|
||||
"your_id": "🆔 معرّف Telegram: <code>%d</code>", "sub_fail": "⚠️ تعذّر إنشاء رابط الاشتراك.",
|
||||
"sub_link": "🔗 <b>رابط الاشتراك</b>\n<code>%s</code>", "my_usage_title": "📊 <b>استهلاكي</b>",
|
||||
"core_running": "يعمل", "core_stopped": "متوقف", "restart_ok": "✅ أُعيد تشغيل نواة Mihomo.",
|
||||
"btn_status": "📊 الحالة", "btn_users": "👥 المستخدمون", "btn_traffic": "📈 المرور", "btn_online": "🟢 متصل",
|
||||
"btn_listeners": "📡 العقد", "btn_cleanup": "🧹 تنظيف", "btn_backup": "📦 نسخة احتياطية", "btn_restart": "🔄 إعادة تشغيل",
|
||||
"btn_my_usage": "📊 استهلاكي", "btn_my_sub": "🔗 الاشتراك", "test_msg": "🔔 <b>3m-ui</b> اختبار Telegram — الاتصال OK.",
|
||||
},
|
||||
"hi": {
|
||||
"unknown_cmd": "अज्ञात कमांड। /help भेजें।", "perm_denied": "⛔ केवल एडमिन।\nउपलब्ध: /id /usage /help",
|
||||
"your_id": "🆔 आपका Telegram ID: <code>%d</code>", "sub_fail": "⚠️ सब्सक्रिप्शन लिंक नहीं बना।",
|
||||
"sub_link": "🔗 <b>सब्सक्रिप्शन URL</b>\n<code>%s</code>", "my_usage_title": "📊 <b>मेरा उपयोग</b>",
|
||||
"core_running": "चल रहा", "core_stopped": "रुका", "restart_ok": "✅ Mihomo कोर पुनः आरंभ।",
|
||||
"btn_status": "📊 स्थिति", "btn_users": "👥 उपयोगकर्ता", "btn_traffic": "📈 ट्रैफ़िक", "btn_online": "🟢 ऑनलाइन",
|
||||
"btn_listeners": "📡 नोड्स", "btn_cleanup": "🧹 सफ़ाई", "btn_backup": "📦 बैकअप", "btn_restart": "🔄 रीस्टार्ट",
|
||||
"btn_my_usage": "📊 उपयोग", "btn_my_sub": "🔗 सब्सक्रिप्शन", "test_msg": "🔔 <b>3m-ui</b> Telegram परीक्षण — कनेक्शन OK।",
|
||||
},
|
||||
"pl": {
|
||||
"unknown_cmd": "Nieznana komenda. Wyślij /help.", "perm_denied": "⛔ Tylko admin.\nDostępne: /id /usage /help",
|
||||
"your_id": "🆔 Twój Telegram ID: <code>%d</code>", "sub_fail": "⚠️ Nie udało się utworzyć linku subskrypcji.",
|
||||
"sub_link": "🔗 <b>URL subskrypcji</b>\n<code>%s</code>", "my_usage_title": "📊 <b>Moje użycie</b>",
|
||||
"core_running": "działa", "core_stopped": "zatrzymany", "restart_ok": "✅ Rdzeń Mihomo zrestartowany.",
|
||||
"btn_status": "📊 Status", "btn_users": "👥 Użytkownicy", "btn_traffic": "📈 Ruch", "btn_online": "🟢 Online",
|
||||
"btn_listeners": "📡 Węzły", "btn_cleanup": "🧹 Czyszczenie", "btn_backup": "📦 Kopia", "btn_restart": "🔄 Restart",
|
||||
"btn_my_usage": "📊 Użycie", "btn_my_sub": "🔗 Subskrypcja", "test_msg": "🔔 <b>3m-ui</b> test Telegram — połączenie OK.",
|
||||
},
|
||||
"uk": {
|
||||
"unknown_cmd": "Невідома команда. Надішліть /help.", "perm_denied": "⛔ Лише для адмінів.\nДоступно: /id /usage /help",
|
||||
"your_id": "🆔 Ваш Telegram ID: <code>%d</code>", "sub_fail": "⚠️ Не вдалося створити посилання підписки.",
|
||||
"sub_link": "🔗 <b>URL підписки</b>\n<code>%s</code>", "my_usage_title": "📊 <b>Мій трафік</b>",
|
||||
"core_running": "працює", "core_stopped": "зупинено", "restart_ok": "✅ Ядро Mihomo перезапущено.",
|
||||
"btn_status": "📊 Статус", "btn_users": "👥 Користувачі", "btn_traffic": "📈 Трафік", "btn_online": "🟢 Онлайн",
|
||||
"btn_listeners": "📡 Вузли", "btn_cleanup": "🧹 Очищення", "btn_backup": "📦 Бекап", "btn_restart": "🔄 Перезапуск",
|
||||
"btn_my_usage": "📊 Трафік", "btn_my_sub": "🔗 Підписка", "test_msg": "🔔 <b>3m-ui</b> тест Telegram — з'єднання OK.",
|
||||
},
|
||||
}
|
||||
en := botMessages["en"]
|
||||
for _, code := range SupportedBotLangs {
|
||||
if code == "en" || code == "zh-CN" || code == "zh-TW" {
|
||||
continue
|
||||
}
|
||||
m := make(map[string]string, len(en)+8)
|
||||
for k, v := range en {
|
||||
m[k] = v
|
||||
}
|
||||
if ov, ok := overlays[code]; ok {
|
||||
for k, v := range ov {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
botMessages[code] = m
|
||||
}
|
||||
}
|
||||
@@ -9,44 +9,41 @@ type inlineKeyboardButton struct {
|
||||
}
|
||||
|
||||
// inlineKeyboardMarkup wraps a 2D grid of buttons for Telegram's reply_markup.
|
||||
// Each inner slice is a visual row.
|
||||
type inlineKeyboardMarkup struct {
|
||||
InlineKeyboard [][]inlineKeyboardButton `json:"inline_keyboard"`
|
||||
}
|
||||
|
||||
// buildAdminMenu returns the inline keyboard shown to admin chats (after /start
|
||||
// or /help). Buttons trigger the corresponding callback_query handlers.
|
||||
func buildAdminMenu() inlineKeyboardMarkup {
|
||||
func buildAdminMenu(lang string) inlineKeyboardMarkup {
|
||||
lang = NormalizeLang(lang)
|
||||
return inlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]inlineKeyboardButton{
|
||||
{
|
||||
{Text: "📊 状态", CallbackData: "status"},
|
||||
{Text: "👥 用户", CallbackData: "users"},
|
||||
{Text: Tr(lang, "btn_status"), CallbackData: "status"},
|
||||
{Text: Tr(lang, "btn_users"), CallbackData: "users"},
|
||||
},
|
||||
{
|
||||
{Text: "📈 流量", CallbackData: "traffic"},
|
||||
{Text: "🟢 在线", CallbackData: "online"},
|
||||
{Text: Tr(lang, "btn_traffic"), CallbackData: "traffic"},
|
||||
{Text: Tr(lang, "btn_online"), CallbackData: "online"},
|
||||
},
|
||||
{
|
||||
{Text: "📡 节点", CallbackData: "listeners"},
|
||||
{Text: "🧹 清理", CallbackData: "deldepleted"},
|
||||
{Text: Tr(lang, "btn_listeners"), CallbackData: "listeners"},
|
||||
{Text: Tr(lang, "btn_cleanup"), CallbackData: "deldepleted"},
|
||||
},
|
||||
{
|
||||
{Text: "📦 备份", CallbackData: "backup"},
|
||||
{Text: "🔄 重启", CallbackData: "restart"},
|
||||
{Text: Tr(lang, "btn_backup"), CallbackData: "backup"},
|
||||
{Text: Tr(lang, "btn_restart"), CallbackData: "restart"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildUserMenu returns the inline keyboard shown to chats bound to a proxy
|
||||
// user via TelegramID (admin-independent, access-controlled by GetByTelegramID).
|
||||
func buildUserMenu() inlineKeyboardMarkup {
|
||||
func buildUserMenu(lang string) inlineKeyboardMarkup {
|
||||
lang = NormalizeLang(lang)
|
||||
return inlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]inlineKeyboardButton{
|
||||
{
|
||||
{Text: "📊 我的用量", CallbackData: "my_usage"},
|
||||
{Text: "🔗 订阅链接", CallbackData: "my_sub"},
|
||||
{Text: Tr(lang, "btn_my_usage"), CallbackData: "my_usage"},
|
||||
{Text: Tr(lang, "btn_my_sub"), CallbackData: "my_sub"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ type Settings struct {
|
||||
Schedule string `json:"schedule"`
|
||||
// AttachBackup attaches the panel backup file to the scheduled report message.
|
||||
AttachBackup bool `json:"attach_backup"`
|
||||
// Language selects the bot/notification language ("zh" or "en").
|
||||
// Language selects the bot language (18 codes aligned with the panel, e.g. en, zh-CN, ja).
|
||||
Language string `json:"language"`
|
||||
// EnabledEvents is a comma-separated allowlist of event names that may fire notifications
|
||||
// (e.g. "login,cpu,crash"). Use EventEnabled(name) to test membership.
|
||||
@@ -79,7 +79,7 @@ func DefaultSettings() Settings {
|
||||
NotifyOnCPU: false,
|
||||
CPUWarnPct: 0,
|
||||
Schedule: "@daily",
|
||||
Language: "zh",
|
||||
Language: "zh-CN",
|
||||
EnabledEvents: "login,cpu,crash",
|
||||
}
|
||||
}
|
||||
@@ -114,8 +114,9 @@ func LoadSettings(db *gorm.DB) (Settings, error) {
|
||||
s.Schedule = "@daily"
|
||||
}
|
||||
if strings.TrimSpace(s.Language) == "" {
|
||||
s.Language = "zh"
|
||||
s.Language = "zh-CN"
|
||||
}
|
||||
s.Language = NormalizeLang(s.Language)
|
||||
if strings.TrimSpace(s.EnabledEvents) == "" {
|
||||
s.EnabledEvents = "login,cpu,crash"
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ const Settings: React.FC = () => {
|
||||
expiry_warn_hours: s.expiry_warn_hours ?? 72,
|
||||
notify_on_traffic: s.notify_on_traffic ?? true,
|
||||
schedule: s.schedule || '@daily',
|
||||
language: s.language || 'zh',
|
||||
language: s.language || 'zh-CN',
|
||||
enabled_events: s.enabled_events
|
||||
? s.enabled_events.split(',').map((x: string) => x.trim()).filter(Boolean)
|
||||
: ['login', 'cpu', 'crash'],
|
||||
@@ -505,9 +505,27 @@ const Settings: React.FC = () => {
|
||||
</Form.Item>
|
||||
<Form.Item name="language" label={t('settings.tgLanguage') || 'Bot language'}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={[
|
||||
{ value: 'zh', label: '中文' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'zh-CN', label: '简体中文' },
|
||||
{ value: 'zh-TW', label: '繁體中文' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
{ value: 'ko', label: '한국어' },
|
||||
{ value: 'es', label: 'Español' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
{ value: 'de', label: 'Deutsch' },
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'pt-BR', label: 'Português (Brasil)' },
|
||||
{ value: 'vi', label: 'Tiếng Việt' },
|
||||
{ value: 'id', label: 'Bahasa Indonesia' },
|
||||
{ value: 'th', label: 'ไทย' },
|
||||
{ value: 'tr', label: 'Türkçe' },
|
||||
{ value: 'ar', label: 'العربية' },
|
||||
{ value: 'hi', label: 'हिन्दी' },
|
||||
{ value: 'pl', label: 'Polski' },
|
||||
{ value: 'uk', label: 'Українська' },
|
||||
]}
|
||||
allowClear
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user