mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-28 16:55:00 +03:00
15fee2a035
The bot answered every action with a new message, so a chat filled with stale menus whose buttons still acted, and long client or inbound lists were paged through dozens of buttons. Each chat now has one live screen that is edited in place: the menu is grouped into Server, Clients, Traffic and Maintenance, lists open through inline search, the add-client flow starts from a draft card, QR codes arrive as one labelled sheet, and repeated notices collapse into one with a counter. A client's /start and /usage show only that client's own links and usage, groups are refused, and the settings page warns when the bot's inline mode is off. Admin topics, the indexed Telegram lookup and every locale, Slovak included, carry over.
132 lines
3.8 KiB
Go
132 lines
3.8 KiB
Go
package tgbot
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// I18nBot returns "" for a missing key, so a screen renders blank buttons without failing; that is how
|
|
// /start once answered with no menu. Every key a Go source asks for must exist.
|
|
|
|
var (
|
|
i18nBotKeyPattern = regexp.MustCompile(`I18nBot\(\s*"([^"]+)"`)
|
|
// Button labels go through btn(), which also calls I18nBot internally, so a
|
|
// broken button key is invisible to a scan of I18nBot calls alone.
|
|
btnKeyPattern = regexp.MustCompile(`\.btn(?:2)?\(\s*"([^"]+)"`)
|
|
// The category screen asks for "tgbot.messages.category_" + name.
|
|
categoryKeyPattern = regexp.MustCompile(`tgbot\.messages\.category_"`)
|
|
)
|
|
|
|
func TestEveryBotKeyExistsInTheTranslations(t *testing.T) {
|
|
root := repoRootForTest(t)
|
|
keys := map[string]string{} // key -> first file that asks for it
|
|
err := filepath.Walk(filepath.Join(root, "internal"), func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
|
|
return err
|
|
}
|
|
if strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
patterns := []*regexp.Regexp{i18nBotKeyPattern, btnKeyPattern}
|
|
for _, pattern := range patterns {
|
|
for _, m := range pattern.FindAllStringSubmatch(string(data), -1) {
|
|
key := m[1]
|
|
// Keys are also built at run time ("tgbot.screens." + kind); a literal ending in a separator is such
|
|
// a prefix, and the set it expands to is checked explicitly.
|
|
if strings.ContainsAny(key, "%$") || strings.HasSuffix(key, ".") || strings.HasSuffix(key, "_") {
|
|
continue
|
|
}
|
|
if _, seen := keys[key]; !seen {
|
|
keys[key] = strings.TrimPrefix(path, root+"/")
|
|
}
|
|
}
|
|
}
|
|
if categoryKeyPattern.MatchString(string(data)) {
|
|
for _, category := range []string{"server", "clients", "traffic", "maintenance"} {
|
|
keys["tgbot.messages.category_"+category] = strings.TrimPrefix(path, root+"/")
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("scan sources: %v", err)
|
|
}
|
|
|
|
// Keys assembled from a prefix at run time; their full set is checked so the
|
|
// dynamic path cannot hide a missing entry.
|
|
for _, kind := range screenKinds {
|
|
keys["tgbot.screens."+kind] = "screens_art.go (screenKinds)"
|
|
}
|
|
|
|
files, err := filepath.Glob(filepath.Join(root, "internal/web/translation/*.json"))
|
|
if err != nil || len(files) == 0 {
|
|
t.Fatalf("no translation files found: %v", err)
|
|
}
|
|
for _, file := range files {
|
|
data, err := os.ReadFile(file)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", file, err)
|
|
}
|
|
var bundle map[string]any
|
|
if err := json.Unmarshal(data, &bundle); err != nil {
|
|
t.Fatalf("parse %s: %v", file, err)
|
|
}
|
|
var missing []string
|
|
for key, source := range keys {
|
|
if !nestedKeyPresent(bundle, key) {
|
|
missing = append(missing, fmt.Sprintf("%s (%s)", key, source))
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
sort.Strings(missing)
|
|
t.Errorf("%s is missing %d key(s) the bot asks for:\n %s",
|
|
filepath.Base(file), len(missing), strings.Join(missing, "\n "))
|
|
}
|
|
}
|
|
}
|
|
|
|
// nestedKeyPresent walks a dotted key through the nested translation objects.
|
|
func nestedKeyPresent(bundle map[string]any, key string) bool {
|
|
var current any = bundle
|
|
for _, part := range strings.Split(key, ".") {
|
|
obj, ok := current.(map[string]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
current, ok = obj[part]
|
|
if !ok {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// repoRootForTest walks up from the test's working directory to the module root.
|
|
func repoRootForTest(t *testing.T) string {
|
|
t.Helper()
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("getwd: %v", err)
|
|
}
|
|
for {
|
|
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
|
return dir
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
t.Fatal("module root not found")
|
|
}
|
|
dir = parent
|
|
}
|
|
}
|