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:
Z User committed 2026-09-10 15:48:10 +00:00
1 parent a1cbef1d74
commit 676312ae79
6 files changed
+511 -125

No files matched your search

+18 -15
View File
@@ -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
+71 -88
View File
@@ -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 &lt;关键字&gt; — 搜索代理用户 / 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 &lt;关键字&gt; — 按用户名/备注搜索 / 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 &lt;用户名或备注关键字&gt;"
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"))
}
+384
View File
@@ -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 &lt;q&gt; — 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 &lt;keyword&gt; — 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 &lt;username or remark&gt;",
"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 &lt;关键字&gt; — 搜索用户(管理员)
/backup — 备份说明(管理员)
/help — 本帮助`,
"unknown_cmd": "未知指令。发送 /help 查看可用命令。", "perm_denied": "⛔ 此命令仅管理员可用。\n可用: /id /usage /help",
"your_id": "🆔 你的 Telegram ID: <code>%d</code>", "usage_admin_hint": "用法: /usage &lt;关键字&gt; — 搜索代理用户",
"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 &lt;用户名或备注&gt;", "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 &lt;關鍵字&gt;",
"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 &lt;使用者名稱或備註&gt;", "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
}
}
+14 -17
View File
@@ -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"},
},
},
}
+4 -3
View File
@@ -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"
}
+20 -2
View File
@@ -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
/>