diff --git a/backend/internal/telegram/api.go b/backend/internal/telegram/api.go index 922ee9a..a53bcb8 100644 --- a/backend/internal/telegram/api.go +++ b/backend/internal/telegram/api.go @@ -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("🔔 3m-ui 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 diff --git a/backend/internal/telegram/bot_commands.go b/backend/internal/telegram/bot_commands.go index 9fb0486..29fdeab 100644 --- a/backend/internal/telegram/bot_commands.go +++ b/backend/internal/telegram/bot_commands.go @@ -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: %d", 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("🔗 订阅链接 / Subscription URL\n%s", buildSubURL(token)) + return Trf(b.lang(), "sub_link", buildSubURL(token)) } -func notBoundMessage(fromID int64) string { - return fmt.Sprintf( - "未绑定账户。请联系管理员将你的 Telegram ID (%d) 绑定到你的账户。\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("📊 账户用量 / My Usage\n") - bld.WriteString(fmt.Sprintf("用户 / User: %s\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: %s\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%s\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(` -🤖 3m-ui Bot -/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( - "📊 Status\ncore: %s\nversion: %s\npid: %d\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("👥 Users\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("🟢 Online\n") + bld.WriteString(Tr(b.lang(), "online_title") + "\n") for _, u := range users { bld.WriteString(fmt.Sprintf("• %s\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("📡 Listeners\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("📈 Traffic (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("• %s ↑%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("🔍 Search %s (%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(`📦 Backup -请在面板「系统设置 → 备份」下载完整备份(SQLite + Mihomo 配置)。 -Use panel Settings → Backup to download a full zip (database + Mihomo config). -API: GET /api/v1/system/backup`) + return strings.TrimSpace(Tr(b.lang(), "backup")) } diff --git a/backend/internal/telegram/i18n.go b/backend/internal/telegram/i18n.go new file mode 100644 index 0000000..1cbcf55 --- /dev/null +++ b/backend/internal/telegram/i18n.go @@ -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": `🤖 3m-ui Telegram Bot +/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: %d", + "usage_admin_hint": "Usage: /usage <keyword> — search proxy users", + "sub_fail": "⚠️ Could not generate subscription link. Try again later.", + "sub_link": "🔗 Subscription URL\n%s", + "not_bound": "Not bound. Ask an admin to link Telegram ID %d to your account.", + "my_usage_title": "📊 My Usage", + "user_label": "User: %s", + "traffic_label": "Traffic: %s / %s", + "up_down": "↑ %s ↓ %s", + "expires": "Expires: %s", + "expires_never": "Expires: Never", + "status_online": "Status: Online 🟢", + "status_offline": "Status: Offline", + "sub_line": "Subscription:\n%s", + "sub_missing": "Subscription: not generated yet", + "unknown_action": "Unknown action.", + "status_title": "📊 Status\ncore: %s\nversion: %s\npid: %d\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": "👥 Users", + "online_title": "🟢 Online", + "online_empty": "No online users.", + "listeners_title": "📡 Listeners", + "listeners_empty": "No listeners.", + "traffic_title": "📈 Traffic (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": "🔍 Search %s (%d)\n", + "backup": "📦 Backup\nDownload a full backup in the panel: Settings → Backup (SQLite + Mihomo config).\nAPI: GET /api/v1/system/backup", + "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": "🔔 3m-ui Telegram test — connection OK.", + }, + "zh-CN": { + "help": `🤖 3m-ui Telegram 机器人 +/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: %d", "usage_admin_hint": "用法: /usage <关键字> — 搜索代理用户", + "sub_fail": "⚠️ 订阅链接生成失败,请稍后重试。", "sub_link": "🔗 订阅链接\n%s", + "not_bound": "未绑定账户。请联系管理员将 Telegram ID %d 绑定到你的账户。", + "my_usage_title": "📊 账户用量", "user_label": "用户: %s", "traffic_label": "流量: %s / %s", + "up_down": "上传 ↑ %s 下载 ↓ %s", "expires": "到期: %s", "expires_never": "到期: 永不过期", + "status_online": "状态: 在线 🟢", "status_offline": "状态: 离线", "sub_line": "订阅:\n%s", "sub_missing": "订阅: 未生成", + "unknown_action": "未知操作。", + "status_title": "📊 状态\n核心: %s\n版本: %s\npid: %d\n用户: %d(在线 %d,不可用 %d)\n节点: %d%s", + "core_running": "运行中", "core_stopped": "已停止", + "users_fail": "读取用户失败: %s", "users_empty": "暂无代理用户。", "users_title": "👥 用户", + "online_title": "🟢 在线", "online_empty": "暂无在线用户。", + "listeners_title": "📡 节点", "listeners_empty": "暂无节点。", + "traffic_title": "📈 流量(用量前列)\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": "🔍 搜索 %s(%d)\n", + "backup": "📦 备份\n请在面板「系统设置 → 备份」下载完整备份(SQLite + Mihomo 配置)。\nAPI: GET /api/v1/system/backup", + "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": "🔔 3m-ui Telegram 测试消息 — 连接正常。", + "user_welcome": "👋 欢迎!点击下方按钮查看用量与订阅链接。", + }, + "zh-TW": { + "help": `🤖 3m-ui Telegram 機器人 +/id — 顯示 Telegram ID +/usage — 查詢用量或搜尋使用者 +/status — 面板與核心狀態(管理員) +/help — 本說明`, + "unknown_cmd": "未知指令。傳送 /help 查看可用命令。", "perm_denied": "⛔ 此命令僅管理員可用。\n可用: /id /usage /help", + "your_id": "🆔 你的 Telegram ID: %d", "usage_admin_hint": "用法: /usage <關鍵字>", + "sub_fail": "⚠️ 訂閱連結產生失敗。", "sub_link": "🔗 訂閱連結\n%s", + "not_bound": "未綁定帳戶。請管理員綁定 Telegram ID %d。", + "my_usage_title": "📊 帳戶用量", "user_label": "使用者: %s", "traffic_label": "流量: %s / %s", + "up_down": "上傳 ↑ %s 下載 ↓ %s", "expires": "到期: %s", "expires_never": "到期: 永不過期", + "status_online": "狀態: 在線 🟢", "status_offline": "狀態: 離線", "sub_line": "訂閱:\n%s", "sub_missing": "訂閱: 未產生", + "unknown_action": "未知操作。", "status_title": "📊 狀態\n核心: %s\n版本: %s\npid: %d\n使用者: %d(在線 %d,不可用 %d)\n節點: %d%s", + "core_running": "運行中", "core_stopped": "已停止", "users_fail": "讀取使用者失敗: %s", "users_empty": "暫無代理使用者。", "users_title": "👥 使用者", + "online_title": "🟢 在線", "online_empty": "暫無在線使用者。", "listeners_title": "📡 節點", "listeners_empty": "暫無節點。", + "traffic_title": "📈 流量\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": "🔍 搜尋 %s(%d)\n", + "backup": "📦 備份\n請在面板「系統設定 → 備份」下載完整備份。\nAPI: GET /api/v1/system/backup", + "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": "🔔 3m-ui 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: %d", "sub_fail": "⚠️ 購読リンクを生成できませんでした。", "sub_link": "🔗 購読 URL\n%s", + "not_bound": "未連携です。管理者に Telegram ID %d の紐付けを依頼してください。", + "my_usage_title": "📊 利用状況", "user_label": "ユーザー: %s", "traffic_label": "トラフィック: %s / %s", + "expires_never": "期限: なし", "status_online": "状態: オンライン 🟢", "status_offline": "状態: オフライン", + "unknown_action": "不明な操作です。", "core_running": "稼働中", "core_stopped": "停止", + "users_empty": "プロキシユーザーがありません。", "users_title": "👥 ユーザー", + "online_title": "🟢 オンライン", "online_empty": "オンラインのユーザーはいません。", + "listeners_title": "📡 ノード", "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": "🔔 3m-ui Telegram テスト — 接続 OK。", + "cmd_start": "開始 / ヘルプ", "cmd_status": "パネル状態", "cmd_restart": "Mihomo 再起動", + }, + "ko": { + "unknown_cmd": "알 수 없는 명령입니다. /help 를 보내세요.", "perm_denied": "⛔ 관리자 전용 명령입니다.\n사용 가능: /id /usage /help", + "your_id": "🆔 Telegram ID: %d", "sub_fail": "⚠️ 구독 링크를 만들 수 없습니다.", "sub_link": "🔗 구독 URL\n%s", + "not_bound": "연결되지 않았습니다. 관리자에게 Telegram ID %d 연결을 요청하세요.", + "my_usage_title": "📊 사용량", "core_running": "실행 중", "core_stopped": "중지", + "users_title": "👥 사용자", "online_title": "🟢 온라인", "listeners_title": "📡 노드", + "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": "🔔 3m-ui Telegram 테스트 — 연결 OK.", + }, + "es": { + "unknown_cmd": "Comando desconocido. Envía /help.", "perm_denied": "⛔ Solo administradores.\nDisponible: /id /usage /help", + "your_id": "🆔 Tu ID de Telegram: %d", "sub_fail": "⚠️ No se pudo generar el enlace de suscripción.", + "sub_link": "🔗 URL de suscripción\n%s", "not_bound": "No vinculado. Pide al admin enlazar el ID %d.", + "my_usage_title": "📊 Mi uso", "core_running": "en ejecución", "core_stopped": "detenido", + "users_title": "👥 Usuarios", "online_title": "🟢 En línea", "listeners_title": "📡 Nodos", + "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": "🔔 3m-ui 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: %d", "sub_fail": "⚠️ Impossible de générer le lien d'abonnement.", + "sub_link": "🔗 URL d'abonnement\n%s", "my_usage_title": "📊 Mon usage", + "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": "🔔 3m-ui 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: %d", "sub_fail": "⚠️ Abo-Link konnte nicht erzeugt werden.", + "sub_link": "🔗 Abo-URL\n%s", "my_usage_title": "📊 Mein Verbrauch", + "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": "🔔 3m-ui Telegram-Test — Verbindung OK.", + }, + "ru": { + "unknown_cmd": "Неизвестная команда. Отправьте /help.", "perm_denied": "⛔ Только для админов.\nДоступно: /id /usage /help", + "your_id": "🆔 Ваш Telegram ID: %d", "sub_fail": "⚠️ Не удалось создать ссылку подписки.", + "sub_link": "🔗 URL подписки\n%s", "my_usage_title": "📊 Мой трафик", + "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": "🔔 3m-ui тест 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: %d", "sub_fail": "⚠️ Não foi possível gerar o link de assinatura.", + "sub_link": "🔗 URL de assinatura\n%s", "my_usage_title": "📊 Meu uso", + "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": "🔔 3m-ui 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: %d", "sub_fail": "⚠️ Không tạo được link subscription.", + "sub_link": "🔗 URL subscription\n%s", "my_usage_title": "📊 Lưu lượng của tôi", + "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": "🔔 3m-ui 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: %d", "sub_fail": "⚠️ Gagal membuat tautan langganan.", + "sub_link": "🔗 URL langganan\n%s", "my_usage_title": "📊 Pemakaian saya", + "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": "🔔 3m-ui tes Telegram — koneksi OK.", + }, + "th": { + "unknown_cmd": "คำสั่งไม่รู้จัก ส่ง /help", "perm_denied": "⛔ สำหรับแอดมินเท่านั้น\nใช้ได้: /id /usage /help", + "your_id": "🆔 Telegram ID: %d", "sub_fail": "⚠️ สร้างลิงก์สมัครไม่สำเร็จ", + "sub_link": "🔗 URL สมัครสมาชิก\n%s", "my_usage_title": "📊 การใช้งานของฉัน", + "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": "🔔 3m-ui ทดสอบ 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: %d", "sub_fail": "⚠️ Abonelik bağlantısı oluşturulamadı.", + "sub_link": "🔗 Abonelik URL\n%s", "my_usage_title": "📊 Kullanımım", + "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": "🔔 3m-ui Telegram testi — bağlantı OK.", + }, + "ar": { + "unknown_cmd": "أمر غير معروف. أرسل /help.", "perm_denied": "⛔ للمشرفين فقط.\nمتاح: /id /usage /help", + "your_id": "🆔 معرّف Telegram: %d", "sub_fail": "⚠️ تعذّر إنشاء رابط الاشتراك.", + "sub_link": "🔗 رابط الاشتراك\n%s", "my_usage_title": "📊 استهلاكي", + "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": "🔔 3m-ui اختبار Telegram — الاتصال OK.", + }, + "hi": { + "unknown_cmd": "अज्ञात कमांड। /help भेजें।", "perm_denied": "⛔ केवल एडमिन।\nउपलब्ध: /id /usage /help", + "your_id": "🆔 आपका Telegram ID: %d", "sub_fail": "⚠️ सब्सक्रिप्शन लिंक नहीं बना।", + "sub_link": "🔗 सब्सक्रिप्शन URL\n%s", "my_usage_title": "📊 मेरा उपयोग", + "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": "🔔 3m-ui 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: %d", "sub_fail": "⚠️ Nie udało się utworzyć linku subskrypcji.", + "sub_link": "🔗 URL subskrypcji\n%s", "my_usage_title": "📊 Moje użycie", + "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": "🔔 3m-ui test Telegram — połączenie OK.", + }, + "uk": { + "unknown_cmd": "Невідома команда. Надішліть /help.", "perm_denied": "⛔ Лише для адмінів.\nДоступно: /id /usage /help", + "your_id": "🆔 Ваш Telegram ID: %d", "sub_fail": "⚠️ Не вдалося створити посилання підписки.", + "sub_link": "🔗 URL підписки\n%s", "my_usage_title": "📊 Мій трафік", + "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": "🔔 3m-ui тест 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 + } +} diff --git a/backend/internal/telegram/inline_menus.go b/backend/internal/telegram/inline_menus.go index bb6d634..bdc9e13 100644 --- a/backend/internal/telegram/inline_menus.go +++ b/backend/internal/telegram/inline_menus.go @@ -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"}, }, }, } diff --git a/backend/internal/telegram/settings.go b/backend/internal/telegram/settings.go index 404adc7..6e69626 100644 --- a/backend/internal/telegram/settings.go +++ b/backend/internal/telegram/settings.go @@ -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" } diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index d0aa323..32a20fe 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -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 = () => {