19 次代碼提交 a07b68894c ... 1fd2c1333c

作者 SHA1 備註 提交日期
  MHSanaei 1fd2c1333c v3.2.0 5 小時之前
  MHSanaei ffe661d212 fix(groups): fetch full client list for Add/Remove/SubLinks modals 5 小時之前
  MHSanaei 3f0b7fbe97 feat(tls): surface pinnedPeerCertSha256 in panel, share links, and subs 6 小時之前
  MHSanaei c5b5606bf5 i18n(panel): translate Copy/Cancel buttons, Stream/Sniffing tabs, and All-Inbounds filenames 6 小時之前
  MHSanaei bee8288d41 fix(clients): bump auto-generated email length to 10 chars 7 小時之前
  MHSanaei 99df5d70a8 fix(clients): backfill missing subId on startup and guard create/update 7 小時之前
  MHSanaei 72b97efa8a i18n(panel): migrate hardcoded panel strings to en-US and translate all locales 7 小時之前
  Aleksey Surkov 0829f1ecd4 change tg message when send qrCode (#4623) 8 小時之前
  Sanaei 058c030e81 Random PostgreSQL role + post-install credentials display (#4608) 8 小時之前
  Puya c03ecfe638 Fix REALITY share links missing SNI (#4621) 8 小時之前
  MHSanaei c5dc84d314 refactor(inbound-tag): drop protocol segment from canonical shape 9 小時之前
  MHSanaei aefee2c15f fix(clients): log bulk attach/detach failures to console 10 小時之前
  MHSanaei b42a4d93fc fix(inbounds): heal legacy client data and TLS cert form hydration 10 小時之前
  MHSanaei 8046d1519d fix(links): include TCP HTTP host header in share links 11 小時之前
  MHSanaei 2fea71387b fix(ui): polish across routing, groups, inbounds, mobile sidebar 12 小時之前
  MHSanaei 530e338c66 refactor(clients): coherent group management — rename, split, extract 12 小時之前
  MHSanaei bf1b488a63 feat(clients): tidier bulk action toolbar 14 小時之前
  MHSanaei 8d6d845262 feat(settings): include email in default remarkModel pattern 14 小時之前
  MHSanaei 72b68cce22 feat(clients): selective bulk attach + new bulk detach 14 小時之前
共有 83 個文件被更改,包括 8956 次插入1510 次删除
  1. 1 1
      config/version
  2. 164 1
      database/db.go
  3. 84 0
      database/db_seed_test.go
  4. 32 32
      frontend/package-lock.json
  5. 2 2
      frontend/package.json
  6. 117 4
      frontend/public/openapi.json
  7. 9 0
      frontend/src/api/invalidationTracker.ts
  8. 1 0
      frontend/src/api/queryKeys.ts
  9. 2 0
      frontend/src/api/websocketBridge.ts
  10. 18 0
      frontend/src/components/AppSidebar.css
  11. 5 3
      frontend/src/components/PromptModal.tsx
  12. 4 2
      frontend/src/components/TextModal.tsx
  13. 57 10
      frontend/src/hooks/useClients.ts
  14. 1 1
      frontend/src/lib/xray/inbound-defaults.ts
  15. 27 9
      frontend/src/lib/xray/inbound-form-adapter.ts
  16. 34 10
      frontend/src/lib/xray/inbound-link.ts
  17. 21 3
      frontend/src/pages/api-docs/endpoints.ts
  18. 10 12
      frontend/src/pages/clients/BulkAddToGroupModal.tsx
  19. 98 0
      frontend/src/pages/clients/BulkAttachInboundsModal.tsx
  20. 98 0
      frontend/src/pages/clients/BulkDetachInboundsModal.tsx
  21. 1 1
      frontend/src/pages/clients/ClientBulkAddModal.tsx
  22. 1 1
      frontend/src/pages/clients/ClientFormModal.tsx
  23. 13 0
      frontend/src/pages/clients/ClientsPage.css
  24. 169 38
      frontend/src/pages/clients/ClientsPage.tsx
  25. 161 0
      frontend/src/pages/groups/GroupAddClientsModal.tsx
  26. 145 0
      frontend/src/pages/groups/GroupRemoveClientsModal.tsx
  27. 92 5
      frontend/src/pages/groups/GroupsPage.tsx
  28. 9 9
      frontend/src/pages/inbounds/AddClientsToGroupModal.tsx
  29. 112 12
      frontend/src/pages/inbounds/AttachClientsModal.tsx
  30. 183 0
      frontend/src/pages/inbounds/DetachClientsModal.tsx
  31. 178 121
      frontend/src/pages/inbounds/InboundFormModal.tsx
  32. 22 22
      frontend/src/pages/inbounds/InboundInfoModal.tsx
  33. 5 1
      frontend/src/pages/inbounds/InboundList.tsx
  34. 31 15
      frontend/src/pages/inbounds/InboundsPage.tsx
  35. 1 1
      frontend/src/pages/nodes/NodesPage.tsx
  36. 24 24
      frontend/src/pages/settings/GeneralTab.tsx
  37. 1 1
      frontend/src/pages/settings/SettingsPage.tsx
  38. 15 15
      frontend/src/pages/settings/SubscriptionFormatsTab.tsx
  39. 2 2
      frontend/src/pages/settings/SubscriptionGeneralTab.tsx
  40. 12 12
      frontend/src/pages/xray/BalancerFormModal.tsx
  41. 27 25
      frontend/src/pages/xray/NordModal.tsx
  42. 135 135
      frontend/src/pages/xray/OutboundFormModal.tsx
  43. 4 4
      frontend/src/pages/xray/OutboundsTab.tsx
  44. 15 2
      frontend/src/pages/xray/RoutingTab.css
  45. 6 4
      frontend/src/pages/xray/RoutingTab.tsx
  46. 22 22
      frontend/src/pages/xray/RuleFormModal.tsx
  47. 30 28
      frontend/src/pages/xray/WarpModal.tsx
  48. 6 6
      frontend/src/pages/xray/XrayPage.tsx
  49. 14 0
      frontend/src/schemas/client.ts
  50. 2 1
      frontend/src/schemas/protocols/security/tls.ts
  51. 18 0
      frontend/src/styles/page-shell.css
  52. 95 0
      frontend/src/test/__snapshots__/inbound-full.test.ts.snap
  53. 6 2
      frontend/src/test/__snapshots__/inbound-link.test.ts.snap
  54. 1 0
      frontend/src/test/__snapshots__/security.test.ts.snap
  55. 80 0
      frontend/src/test/golden/fixtures/inbound-full/vless-ws-tls-pinned.json
  56. 88 13
      install.sh
  57. 3 0
      sub/subClashService.go
  58. 3 0
      sub/subJsonService.go
  59. 55 4
      sub/subService.go
  60. 13 0
      util/random/random.go
  61. 1 0
      web/controller/api.go
  62. 2 0
      web/controller/api_docs_test.go
  63. 16 98
      web/controller/client.go
  64. 154 0
      web/controller/group.go
  65. 126 24
      web/service/client.go
  66. 4 1
      web/service/inbound.go
  67. 3 111
      web/service/port_conflict.go
  68. 23 23
      web/service/port_conflict_test.go
  69. 1 1
      web/service/setting.go
  70. 1 1
      web/service/tgbot.go
  71. 502 79
      web/translation/ar-EG.json
  72. 354 11
      web/translation/en-US.json
  73. 474 51
      web/translation/es-ES.json
  74. 473 64
      web/translation/fa-IR.json
  75. 455 32
      web/translation/id-ID.json
  76. 473 50
      web/translation/ja-JP.json
  77. 452 29
      web/translation/pt-BR.json
  78. 491 68
      web/translation/ru-RU.json
  79. 460 37
      web/translation/tr-TR.json
  80. 480 57
      web/translation/uk-UA.json
  81. 465 42
      web/translation/vi-VN.json
  82. 475 52
      web/translation/zh-CN.json
  83. 486 63
      web/translation/zh-TW.json

+ 1 - 1
config/version

@@ -1 +1 @@
-3.1.0
+3.2.0

+ 164 - 1
database/db.go

@@ -8,6 +8,7 @@ import (
 	"errors"
 	"io"
 	"log"
+	"math"
 	"os"
 	"path"
 	"slices"
@@ -18,6 +19,7 @@ import (
 	"github.com/mhsanaei/3x-ui/v3/config"
 	"github.com/mhsanaei/3x-ui/v3/database/model"
 	"github.com/mhsanaei/3x-ui/v3/util/crypto"
+	"github.com/mhsanaei/3x-ui/v3/util/random"
 	"github.com/mhsanaei/3x-ui/v3/xray"
 
 	"gorm.io/driver/postgres"
@@ -143,7 +145,7 @@ func runSeeders(isUsersEmpty bool) error {
 	}
 
 	if empty && isUsersEmpty {
-		seeders := []string{"UserPasswordHash", "ClientsTable"}
+		seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix"}
 		for _, name := range seeders {
 			if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
 				return err
@@ -196,9 +198,170 @@ func runSeeders(isUsersEmpty bool) error {
 			return err
 		}
 	}
+
+	if !slices.Contains(seedersHistory, "InboundClientsArrayFix") {
+		if err := normalizeInboundClientsArray(); err != nil {
+			return err
+		}
+	}
+
+	if !slices.Contains(seedersHistory, "InboundClientTgIdFix") {
+		if err := normalizeInboundClientTgId(); err != nil {
+			return err
+		}
+	}
+
+	if !slices.Contains(seedersHistory, "InboundClientSubIdFix") {
+		if err := normalizeInboundClientSubId(); err != nil {
+			return err
+		}
+	}
 	return nil
 }
 
+func normalizeInboundClientTgId() error {
+	var inbounds []model.Inbound
+	if err := db.Find(&inbounds).Error; err != nil {
+		return err
+	}
+
+	return db.Transaction(func(tx *gorm.DB) error {
+		for _, inbound := range inbounds {
+			if strings.TrimSpace(inbound.Settings) == "" {
+				continue
+			}
+			var settings map[string]any
+			if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
+				log.Printf("InboundClientTgIdFix: skip inbound %d (invalid settings json): %v", inbound.Id, err)
+				continue
+			}
+			clients, ok := settings["clients"].([]any)
+			if !ok {
+				continue
+			}
+			mutated := false
+			for i, raw := range clients {
+				obj, ok := raw.(map[string]any)
+				if !ok {
+					continue
+				}
+				tgRaw, present := obj["tgId"]
+				if !present {
+					continue
+				}
+				v, isFloat := tgRaw.(float64)
+				if isFloat && !math.IsNaN(v) && !math.IsInf(v, 0) && v == math.Trunc(v) {
+					continue
+				}
+				obj["tgId"] = int64(0)
+				clients[i] = obj
+				mutated = true
+			}
+			if !mutated {
+				continue
+			}
+			settings["clients"] = clients
+			newSettings, err := json.MarshalIndent(settings, "", "  ")
+			if err != nil {
+				log.Printf("InboundClientTgIdFix: skip inbound %d (marshal failed): %v", inbound.Id, err)
+				continue
+			}
+			if err := tx.Model(&model.Inbound{}).Where("id = ?", inbound.Id).
+				Update("settings", string(newSettings)).Error; err != nil {
+				return err
+			}
+		}
+		return tx.Create(&model.HistoryOfSeeders{SeederName: "InboundClientTgIdFix"}).Error
+	})
+}
+
+func normalizeInboundClientSubId() error {
+	var inbounds []model.Inbound
+	if err := db.Find(&inbounds).Error; err != nil {
+		return err
+	}
+
+	return db.Transaction(func(tx *gorm.DB) error {
+		for _, inbound := range inbounds {
+			if strings.TrimSpace(inbound.Settings) == "" {
+				continue
+			}
+			var settings map[string]any
+			if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
+				log.Printf("InboundClientSubIdFix: skip inbound %d (invalid settings json): %v", inbound.Id, err)
+				continue
+			}
+			clients, ok := settings["clients"].([]any)
+			if !ok {
+				continue
+			}
+			mutated := false
+			for i, raw := range clients {
+				obj, ok := raw.(map[string]any)
+				if !ok {
+					continue
+				}
+				existing, _ := obj["subId"].(string)
+				if strings.TrimSpace(existing) != "" {
+					continue
+				}
+				obj["subId"] = random.NumLower(16)
+				clients[i] = obj
+				mutated = true
+			}
+			if !mutated {
+				continue
+			}
+			settings["clients"] = clients
+			newSettings, err := json.MarshalIndent(settings, "", "  ")
+			if err != nil {
+				log.Printf("InboundClientSubIdFix: skip inbound %d (marshal failed): %v", inbound.Id, err)
+				continue
+			}
+			if err := tx.Model(&model.Inbound{}).Where("id = ?", inbound.Id).
+				Update("settings", string(newSettings)).Error; err != nil {
+				return err
+			}
+		}
+		return tx.Create(&model.HistoryOfSeeders{SeederName: "InboundClientSubIdFix"}).Error
+	})
+}
+
+func normalizeInboundClientsArray() error {
+	var inbounds []model.Inbound
+	if err := db.Find(&inbounds).Error; err != nil {
+		return err
+	}
+
+	return db.Transaction(func(tx *gorm.DB) error {
+		for _, inbound := range inbounds {
+			if strings.TrimSpace(inbound.Settings) == "" {
+				continue
+			}
+			var settings map[string]any
+			if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
+				log.Printf("InboundClientsArrayFix: skip inbound %d (invalid settings json): %v", inbound.Id, err)
+				continue
+			}
+			raw, exists := settings["clients"]
+			if !exists || raw != nil {
+				continue
+			}
+			settings["clients"] = []any{}
+			newSettings, err := json.MarshalIndent(settings, "", "  ")
+			if err != nil {
+				log.Printf("InboundClientsArrayFix: skip inbound %d (marshal failed): %v", inbound.Id, err)
+				continue
+			}
+			if err := tx.Model(&model.Inbound{}).Where("id = ?", inbound.Id).
+				Update("settings", string(newSettings)).Error; err != nil {
+				return err
+			}
+		}
+		return tx.Create(&model.HistoryOfSeeders{SeederName: "InboundClientsArrayFix"}).Error
+	})
+}
+
 // normalizeClientJSONFields coerces loosely-typed numeric fields in a raw
 // settings.clients entry so json.Unmarshal into model.Client doesn't fail
 // when older rows wrote tgId/limitIp/totalGB/etc. as strings. Empty strings

+ 84 - 0
database/db_seed_test.go

@@ -3,6 +3,7 @@ package database
 import (
 	"encoding/json"
 	"path/filepath"
+	"regexp"
 	"testing"
 
 	"github.com/mhsanaei/3x-ui/v3/database/model"
@@ -69,3 +70,86 @@ func TestSeedClientsFromInboundJSON_IsIdempotentAgainstExistingClients(t *testin
 		t.Fatalf("[email protected] should resolve to exactly one row, got %d", count)
 	}
 }
+
+func TestNormalizeInboundClientSubId_FillsMissingAndPreservesExisting(t *testing.T) {
+	dbDir := t.TempDir()
+	t.Setenv("XUI_DB_FOLDER", dbDir)
+	if err := InitDB(filepath.Join(dbDir, "3x-ui.db")); err != nil {
+		t.Fatalf("InitDB failed: %v", err)
+	}
+	t.Cleanup(func() { _ = CloseDB() })
+
+	settings, err := json.Marshal(map[string]any{
+		"clients": []any{
+			map[string]any{
+				"id":    "00000000-0000-0000-0000-000000000001",
+				"email": "[email protected]",
+				"subId": "",
+			},
+			map[string]any{
+				"id":    "00000000-0000-0000-0000-000000000002",
+				"email": "[email protected]",
+			},
+			map[string]any{
+				"id":    "00000000-0000-0000-0000-000000000003",
+				"email": "[email protected]",
+				"subId": "keep-me-1234",
+			},
+		},
+	})
+	if err != nil {
+		t.Fatalf("marshal settings: %v", err)
+	}
+	inbound := model.Inbound{
+		UserId:   1,
+		Port:     23456,
+		Protocol: model.VLESS,
+		Settings: string(settings),
+		Tag:      "subid-fix-inbound",
+	}
+	if err := db.Create(&inbound).Error; err != nil {
+		t.Fatalf("seed inbound: %v", err)
+	}
+
+	if err := db.Where("seeder_name = ?", "InboundClientSubIdFix").Delete(&model.HistoryOfSeeders{}).Error; err != nil {
+		t.Fatalf("clear seeder history: %v", err)
+	}
+
+	if err := normalizeInboundClientSubId(); err != nil {
+		t.Fatalf("normalizeInboundClientSubId: %v", err)
+	}
+
+	var reloaded model.Inbound
+	if err := db.First(&reloaded, inbound.Id).Error; err != nil {
+		t.Fatalf("reload inbound: %v", err)
+	}
+	var parsed map[string]any
+	if err := json.Unmarshal([]byte(reloaded.Settings), &parsed); err != nil {
+		t.Fatalf("unmarshal settings: %v", err)
+	}
+	clients, ok := parsed["clients"].([]any)
+	if !ok || len(clients) != 3 {
+		t.Fatalf("expected 3 clients, got %v", parsed["clients"])
+	}
+
+	subIdPattern := regexp.MustCompile(`^[0-9a-z]{16}$`)
+	for i := 0; i < 2; i++ {
+		obj := clients[i].(map[string]any)
+		sub, _ := obj["subId"].(string)
+		if !subIdPattern.MatchString(sub) {
+			t.Fatalf("client %d: expected 16-char [0-9a-z] subId, got %q", i, sub)
+		}
+	}
+	preserved := clients[2].(map[string]any)["subId"].(string)
+	if preserved != "keep-me-1234" {
+		t.Fatalf("expected existing subId preserved, got %q", preserved)
+	}
+
+	var historyCount int64
+	if err := db.Model(&model.HistoryOfSeeders{}).Where("seeder_name = ?", "InboundClientSubIdFix").Count(&historyCount).Error; err != nil {
+		t.Fatalf("count seeder history: %v", err)
+	}
+	if historyCount != 1 {
+		t.Fatalf("expected one InboundClientSubIdFix history row, got %d", historyCount)
+	}
+}

+ 32 - 32
frontend/package-lock.json

@@ -1,14 +1,14 @@
 {
   "name": "3x-ui-frontend",
-  "version": "0.1.0",
+  "version": "0.2.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "3x-ui-frontend",
-      "version": "0.1.0",
+      "version": "0.2.0",
       "dependencies": {
-        "@ant-design/icons": "^6.2.3",
+        "@ant-design/icons": "^6.2.5",
         "@codemirror/lang-json": "^6.0.2",
         "@codemirror/theme-one-dark": "^6.1.3",
         "@tanstack/react-query": "^5.100.14",
@@ -101,14 +101,14 @@
       }
     },
     "node_modules/@ant-design/icons": {
-      "version": "6.2.3",
-      "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.3.tgz",
-      "integrity": "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==",
+      "version": "6.2.5",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.5.tgz",
+      "integrity": "sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw==",
       "license": "MIT",
       "dependencies": {
         "@ant-design/colors": "^8.0.1",
         "@ant-design/icons-svg": "^4.4.2",
-        "@rc-component/util": "^1.10.1",
+        "@rc-component/util": "^1.11.0",
         "clsx": "^2.1.1"
       },
       "engines": {
@@ -1011,13 +1011,13 @@
       }
     },
     "node_modules/@rc-component/form": {
-      "version": "1.8.1",
-      "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.1.tgz",
-      "integrity": "sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ==",
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.2.tgz",
+      "integrity": "sha512-ZidCvOLmM9Xr+3vzk4UAoR7Aj1W/5IHyrzlBB7sNkygpTeRVrohQSo4TN7W/nARTH+nt8zSAPsn4BEl4zLEO2g==",
       "license": "MIT",
       "dependencies": {
         "@rc-component/async-validator": "^5.1.0",
-        "@rc-component/util": "^1.6.2",
+        "@rc-component/util": "^1.11.1",
         "clsx": "^2.1.1"
       },
       "engines": {
@@ -1092,15 +1092,15 @@
       }
     },
     "node_modules/@rc-component/menu": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.3.0.tgz",
-      "integrity": "sha512-u3NfiwpiEgT177qa5Yxm5QsI8i/93EBGpWj8HYZQDnh2pCZ2xtQCe/+w3pSR2NlwKOZDTCKzEhEyD09mGphssA==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.3.1.tgz",
+      "integrity": "sha512-pSZl9nBPgKgxN0aaW7NilIBEwWsc+43S+ulGdWAg9afak96dNOGWsGx0DLLBB1VQsAJvo6bQMTDzXoPlEHsBEw==",
       "license": "MIT",
       "dependencies": {
         "@rc-component/motion": "^1.1.4",
         "@rc-component/overflow": "^1.0.0",
         "@rc-component/trigger": "^3.0.0",
-        "@rc-component/util": "^1.3.0",
+        "@rc-component/util": "^1.11.1",
         "clsx": "^2.1.1"
       },
       "peerDependencies": {
@@ -1398,14 +1398,14 @@
       }
     },
     "node_modules/@rc-component/table": {
-      "version": "1.10.1",
-      "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.10.1.tgz",
-      "integrity": "sha512-XEjyZePbePSdfJjBV3p+I5x/HZ2+UevdiaUJ/ghRm3UtQ9AC+V9hIFM2H349nM/C5ndOa433e/RRQF+RbJQB5g==",
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.10.2.tgz",
+      "integrity": "sha512-b3PjqB9Gp25p5t/zq+9QrbXbodkptT8/zvLmwgd2FNPUUtaYyDnQqfxeD5a7ao8E8lpinLHsi2u2vdfPhyNvAw==",
       "license": "MIT",
       "dependencies": {
         "@rc-component/context": "^2.0.1",
         "@rc-component/resize-observer": "^1.0.0",
-        "@rc-component/util": "^1.1.0",
+        "@rc-component/util": "^1.11.1",
         "@rc-component/virtual-list": "^1.0.1",
         "clsx": "^2.1.1"
       },
@@ -1418,16 +1418,16 @@
       }
     },
     "node_modules/@rc-component/tabs": {
-      "version": "1.9.0",
-      "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.9.0.tgz",
-      "integrity": "sha512-tn1slmbbaTyt8mgwyWJcT8jo/qNiYUs6u1H7OgGQt9faYO06BJIkU5cTmMqORzIrNmSEeeUY6pD5i+JlqSHYhg==",
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.9.1.tgz",
+      "integrity": "sha512-6mY08Fce6aNOHuGsxbzT+f2ekgL9mg1cGGHkittMlVGymjGg+kGupu5v90sRxcUd/paRU9jclLLXtF/PkK1FUA==",
       "license": "MIT",
       "dependencies": {
         "@rc-component/dropdown": "~1.0.0",
         "@rc-component/menu": "~1.3.0",
         "@rc-component/motion": "^1.1.3",
         "@rc-component/resize-observer": "^1.0.0",
-        "@rc-component/util": "^1.3.0",
+        "@rc-component/util": "^1.11.1",
         "clsx": "^2.1.1"
       },
       "engines": {
@@ -3952,9 +3952,9 @@
       }
     },
     "node_modules/dompurify": {
-      "version": "3.4.6",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.6.tgz",
-      "integrity": "sha512-+7gzEI8trIIQkVCvQ3ucGtNfH3nOmDgVTzc62rAAOlMxLth78pwpPoZCPc7CyRzAQF89MqcfPdEWkDwnjgqktg==",
+      "version": "3.4.7",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz",
+      "integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==",
       "license": "(MPL-2.0 OR Apache-2.0)",
       "optionalDependencies": {
         "@types/trusted-types": "^2.0.7"
@@ -3984,9 +3984,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.5.362",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.362.tgz",
-      "integrity": "sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w==",
+      "version": "1.5.363",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.363.tgz",
+      "integrity": "sha512-VjUKPyWzGnT1fujlkEGC/BvN70Hh70KXtAqcmniXviYlJC/ivcT+BWGPyxWVbJZLfvtKR6dqg1L7T7pgAMBtWA==",
       "dev": true,
       "license": "ISC"
     },
@@ -4590,9 +4590,9 @@
       }
     },
     "node_modules/hasown": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
-      "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
       "license": "MIT",
       "dependencies": {
         "function-bind": "^1.1.2"

+ 2 - 2
frontend/package.json

@@ -1,7 +1,7 @@
 {
   "name": "3x-ui-frontend",
   "private": true,
-  "version": "0.1.0",
+  "version": "0.2.0",
   "type": "module",
   "description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).",
   "engines": {
@@ -20,7 +20,7 @@
     "gen:zod": "cd .. && go run ./tools/openapigen"
   },
   "dependencies": {
-    "@ant-design/icons": "^6.2.3",
+    "@ant-design/icons": "^6.2.5",
     "@codemirror/lang-json": "^6.0.2",
     "@codemirror/theme-one-dark": "^6.1.3",
     "@tanstack/react-query": "^5.100.14",

+ 117 - 4
frontend/public/openapi.json

@@ -2852,13 +2852,13 @@
         }
       }
     },
-    "/panel/api/clients/bulkAssignGroup": {
+    "/panel/api/clients/groups/bulkAdd": {
       "post": {
         "tags": [
           "Clients"
         ],
-        "summary": "Assign the given group label to many clients in one call. Updates clients.group_name and patches the matching client entry inside every owning inbound's settings JSON in a single transaction. Pass an empty group to clear the label. If the group name does not yet exist (in client_groups or as a derived label), it is auto-created as a persistent group.",
-        "operationId": "post_panel_api_clients_bulkAssignGroup",
+        "summary": "Add many clients to a group in one call. Updates clients.group_name and patches the matching client entry inside every owning inbound's settings JSON in a single transaction. If the group name does not yet exist (in client_groups or as a derived label), it is auto-created as a persistent group. To clear the group label, use /groups/bulkRemove instead.",
+        "operationId": "post_panel_api_clients_groups_bulkAdd",
         "requestBody": {
           "required": true,
           "content": {
@@ -2905,6 +2905,58 @@
         }
       }
     },
+    "/panel/api/clients/groups/bulkRemove": {
+      "post": {
+        "tags": [
+          "Clients"
+        ],
+        "summary": "Clear the group label on many clients in one call. Inverse of /groups/bulkAdd. Clients themselves are kept — only the group label is cleared from clients.group_name and from each owning inbound's settings JSON. Groups become empty if all their members are removed.",
+        "operationId": "post_panel_api_clients_groups_bulkRemove",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object"
+              },
+              "example": {
+                "emails": [
+                  "alice",
+                  "bob"
+                ]
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "Successful response",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "success": {
+                      "type": "boolean"
+                    },
+                    "msg": {
+                      "type": "string"
+                    },
+                    "obj": {}
+                  }
+                },
+                "example": {
+                  "success": true,
+                  "obj": {
+                    "affected": 2
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/panel/api/clients/bulkAttach": {
       "post": {
         "tags": [
@@ -2968,6 +3020,67 @@
         }
       }
     },
+    "/panel/api/clients/bulkDetach": {
+      "post": {
+        "tags": [
+          "Clients"
+        ],
+        "summary": "Mirror of bulkAttach: detach many existing clients from many inbounds in one call. For each email, intersects the client's current inbounds with the requested set and detaches from those only; (email, inbound) pairs where the client is not currently attached are silently no-ops. Emails not attached to any of the requested inbounds are reported under skipped. Client records are kept even if they become orphaned — use bulkDel for full removal. Returns per-email detached/skipped/errors lists and triggers a single Xray restart if any target inbound was running.",
+        "operationId": "post_panel_api_clients_bulkDetach",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object"
+              },
+              "example": {
+                "emails": [
+                  "alice",
+                  "bob"
+                ],
+                "inboundIds": [
+                  7,
+                  9
+                ]
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "Successful response",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "success": {
+                      "type": "boolean"
+                    },
+                    "msg": {
+                      "type": "string"
+                    },
+                    "obj": {}
+                  }
+                },
+                "example": {
+                  "success": true,
+                  "obj": {
+                    "detached": [
+                      "alice",
+                      "bob"
+                    ],
+                    "skipped": [],
+                    "errors": []
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/panel/api/clients/bulkResetTraffic": {
       "post": {
         "tags": [
@@ -3117,7 +3230,7 @@
         "tags": [
           "Clients"
         ],
-        "summary": "Create a new empty (placeholder) group. The group becomes selectable in client forms and the filter drawer even before any client is assigned to it. Errors if a group with the same name already exists.",
+        "summary": "Create a new empty (placeholder) group. The group becomes selectable in client forms and the filter drawer even before any client is added to it. Errors if a group with the same name already exists.",
         "operationId": "post_panel_api_clients_groups_create",
         "requestBody": {
           "required": true,

+ 9 - 0
frontend/src/api/invalidationTracker.ts

@@ -0,0 +1,9 @@
+let lastLocalInvalidateAt = 0;
+
+export function markLocalInvalidate(): void {
+  lastLocalInvalidateAt = Date.now();
+}
+
+export function isRecentLocalInvalidate(windowMs = 1500): boolean {
+  return Date.now() - lastLocalInvalidateAt < windowMs;
+}

+ 1 - 0
frontend/src/api/queryKeys.ts

@@ -19,6 +19,7 @@ export const keys = {
   clients: {
     root: () => ['clients'] as const,
     list: (params: unknown) => ['clients', 'list', params] as const,
+    all: () => ['clients', 'all'] as const,
     onlines: () => ['clients', 'onlines'] as const,
     lastOnline: () => ['clients', 'lastOnline'] as const,
     groups: () => ['clients', 'groups'] as const,

+ 2 - 0
frontend/src/api/websocketBridge.ts

@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
 
 import { WebSocketClient } from '@/api/websocket';
 import { keys } from '@/api/queryKeys';
+import { isRecentLocalInvalidate } from '@/api/invalidationTracker';
 
 type Handler = (payload: unknown) => void;
 
@@ -35,6 +36,7 @@ export function useWebSocketBridge() {
       if (invalidateTimer != null) clearTimeout(invalidateTimer);
       invalidateTimer = window.setTimeout(() => {
         invalidateTimer = null;
+        if (isRecentLocalInvalidate()) return;
         if (p.type === 'inbounds') {
           queryClient.invalidateQueries({ queryKey: ['inbounds'] });
         } else {

+ 18 - 0
frontend/src/components/AppSidebar.css

@@ -247,6 +247,24 @@
   }
 }
 
+body.dark .ant-drawer-content,
+body.dark .ant-drawer-body {
+  background-color: #15161a;
+}
+
+html[data-theme="ultra-dark"] body.dark .ant-drawer-content,
+html[data-theme="ultra-dark"] body.dark .ant-drawer-body {
+  background-color: #050507;
+}
+
+body.dark .ant-drawer-body .drawer-menu,
+body.dark .ant-drawer-body .drawer-menu.ant-menu-dark,
+body.dark .ant-drawer-body .drawer-menu .ant-menu-item,
+body.dark .ant-drawer-body .drawer-menu .ant-menu-sub,
+body.dark .ant-drawer-body .drawer-menu .ant-menu-item-group-list {
+  background-color: transparent;
+}
+
 .sider-nav .ant-menu-item-selected,
 .sider-utility .ant-menu-item-selected,
 .drawer-menu .ant-menu-item-selected {

+ 5 - 3
frontend/src/components/PromptModal.tsx

@@ -1,6 +1,7 @@
 import { useEffect, useRef, useState } from 'react';
 import { Input, Modal } from 'antd';
 import type { InputRef } from 'antd';
+import { useTranslation } from 'react-i18next';
 
 interface PromptModalProps {
   open: boolean;
@@ -17,12 +18,13 @@ export default function PromptModal({
   open,
   onClose,
   title,
-  okText = 'OK',
+  okText,
   type = 'input',
   initialValue = '',
   loading = false,
   onConfirm,
 }: PromptModalProps) {
+  const { t } = useTranslation();
   const [value, setValue] = useState('');
   const textareaRef = useRef<HTMLTextAreaElement | null>(null);
   const inputRef = useRef<InputRef | null>(null);
@@ -53,8 +55,8 @@ export default function PromptModal({
     <Modal
       open={open}
       title={title}
-      okText={okText}
-      cancelText="Cancel"
+      okText={okText ?? t('confirm')}
+      cancelText={t('cancel')}
       mask={{ closable: false }}
       confirmLoading={loading}
       onOk={() => onConfirm(value)}

+ 4 - 2
frontend/src/components/TextModal.tsx

@@ -1,5 +1,6 @@
 import { Button, Input, Modal, message } from 'antd';
 import { CopyOutlined, DownloadOutlined } from '@ant-design/icons';
+import { useTranslation } from 'react-i18next';
 
 import { ClipboardManager, FileManager } from '@/utils';
 
@@ -12,11 +13,12 @@ interface TextModalProps {
 }
 
 export default function TextModal({ open, onClose, title, content, fileName = '' }: TextModalProps) {
+  const { t } = useTranslation();
   const [messageApi, messageContextHolder] = message.useMessage();
   async function copy() {
     const ok = await ClipboardManager.copyText(content || '');
     if (ok) {
-      messageApi.success('Copied');
+      messageApi.success(t('copied'));
       onClose();
     }
   }
@@ -39,7 +41,7 @@ export default function TextModal({ open, onClose, title, content, fileName = ''
           {fileName && (
             <Button icon={<DownloadOutlined />} onClick={download}>{fileName}</Button>
           )}
-          <Button type="primary" icon={<CopyOutlined />} onClick={copy}>Copy</Button>
+          <Button type="primary" icon={<CopyOutlined />} onClick={copy}>{t('copy')}</Button>
         </>
       )}
     >

+ 57 - 10
frontend/src/hooks/useClients.ts

@@ -4,14 +4,17 @@ import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tansta
 import { HttpUtil, Msg } from '@/utils';
 import { parseMsg } from '@/utils/zodValidate';
 import { keys } from '@/api/queryKeys';
+import { markLocalInvalidate } from '@/api/invalidationTracker';
 import {
   ClientHydrateSchema,
   ClientPageResponseSchema,
   InboundOptionsSchema,
   OnlinesSchema,
   BulkAdjustResultSchema,
+  BulkAttachResultSchema,
   BulkCreateResultSchema,
   BulkDeleteResultSchema,
+  BulkDetachResultSchema,
   DelDepletedResultSchema,
   type ClientHydrate,
   type ClientRecord,
@@ -20,8 +23,10 @@ import {
   type ClientPageResponse,
   type InboundOption,
   type BulkAdjustResult,
+  type BulkAttachResult,
   type BulkCreateResult,
   type BulkDeleteResult,
+  type BulkDetachResult,
 } from '@/schemas/client';
 import { DefaultsPayloadSchema } from '@/schemas/defaults';
 
@@ -209,10 +214,13 @@ export function useClients() {
   // Inbounds page and any open edit modal pick up the new shape without
   // a manual reload.
   const invalidateAll = useCallback(
-    () => Promise.all([
-      queryClient.invalidateQueries({ queryKey: keys.clients.root() }),
-      queryClient.invalidateQueries({ queryKey: keys.inbounds.root() }),
-    ]),
+    () => {
+      markLocalInvalidate();
+      return Promise.all([
+        queryClient.invalidateQueries({ queryKey: keys.clients.root() }),
+        queryClient.invalidateQueries({ queryKey: keys.inbounds.root() }),
+      ]);
+    },
     [queryClient],
   );
 
@@ -234,9 +242,15 @@ export function useClients() {
     onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
   });
 
-  const bulkAssignGroupMut = useMutation({
+  const bulkAddToGroupMut = useMutation({
     mutationFn: (body: { emails: string[]; group: string }) =>
-      HttpUtil.post('/panel/api/clients/bulkAssignGroup', body, JSON_HEADERS),
+      HttpUtil.post('/panel/api/clients/groups/bulkAdd', body, JSON_HEADERS),
+    onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
+  });
+
+  const bulkRemoveFromGroupMut = useMutation({
+    mutationFn: (body: { emails: string[] }) =>
+      HttpUtil.post('/panel/api/clients/groups/bulkRemove', body, JSON_HEADERS),
     onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
   });
 
@@ -286,12 +300,28 @@ export function useClients() {
     onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
   });
 
+  const bulkAttachMut = useMutation({
+    mutationFn: async (payload: { emails: string[]; inboundIds: number[] }): Promise<Msg<BulkAttachResult>> => {
+      const raw = await HttpUtil.post('/panel/api/clients/bulkAttach', payload, JSON_HEADERS);
+      return parseMsg(raw, BulkAttachResultSchema, 'clients/bulkAttach');
+    },
+    onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
+  });
+
   const detachMut = useMutation({
     mutationFn: ({ email, inboundIds }: { email: string; inboundIds: number[] }) =>
       HttpUtil.post(`/panel/api/clients/${encodeURIComponent(email)}/detach`, { inboundIds }, JSON_HEADERS),
     onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
   });
 
+  const bulkDetachMut = useMutation({
+    mutationFn: async (payload: { emails: string[]; inboundIds: number[] }): Promise<Msg<BulkDetachResult>> => {
+      const raw = await HttpUtil.post('/panel/api/clients/bulkDetach', payload, JSON_HEADERS);
+      return parseMsg(raw, BulkDetachResultSchema, 'clients/bulkDetach');
+    },
+    onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
+  });
+
   const resetTrafficMut = useMutation({
     mutationFn: (email: string) =>
       HttpUtil.post(`/panel/api/clients/resetTraffic/${encodeURIComponent(email)}`),
@@ -332,18 +362,32 @@ export function useClients() {
     if (!Array.isArray(emails) || emails.length === 0) return Promise.resolve(null);
     return bulkAdjustMut.mutateAsync({ emails, addDays, addBytes });
   }, [bulkAdjustMut]);
-  const bulkAssignGroup = useCallback((emails: string[], group: string) => {
+  const bulkAddToGroup = useCallback((emails: string[], group: string) => {
+    if (!Array.isArray(emails) || emails.length === 0) return Promise.resolve(null);
+    return bulkAddToGroupMut.mutateAsync({ emails, group });
+  }, [bulkAddToGroupMut]);
+  const bulkRemoveFromGroup = useCallback((emails: string[]) => {
     if (!Array.isArray(emails) || emails.length === 0) return Promise.resolve(null);
-    return bulkAssignGroupMut.mutateAsync({ emails, group });
-  }, [bulkAssignGroupMut]);
+    return bulkRemoveFromGroupMut.mutateAsync({ emails });
+  }, [bulkRemoveFromGroupMut]);
   const attach = useCallback((email: string, inboundIds: number[]) => {
     if (!email) return Promise.resolve(null as unknown as Msg<unknown>);
     return attachMut.mutateAsync({ email, inboundIds });
   }, [attachMut]);
+  const bulkAttach = useCallback((emails: string[], inboundIds: number[]) => {
+    if (!Array.isArray(emails) || emails.length === 0) return Promise.resolve(null as unknown as Msg<BulkAttachResult>);
+    if (!Array.isArray(inboundIds) || inboundIds.length === 0) return Promise.resolve(null as unknown as Msg<BulkAttachResult>);
+    return bulkAttachMut.mutateAsync({ emails, inboundIds });
+  }, [bulkAttachMut]);
   const detach = useCallback((email: string, inboundIds: number[]) => {
     if (!email) return Promise.resolve(null as unknown as Msg<unknown>);
     return detachMut.mutateAsync({ email, inboundIds });
   }, [detachMut]);
+  const bulkDetach = useCallback((emails: string[], inboundIds: number[]) => {
+    if (!Array.isArray(emails) || emails.length === 0) return Promise.resolve(null as unknown as Msg<BulkDetachResult>);
+    if (!Array.isArray(inboundIds) || inboundIds.length === 0) return Promise.resolve(null as unknown as Msg<BulkDetachResult>);
+    return bulkDetachMut.mutateAsync({ emails, inboundIds });
+  }, [bulkDetachMut]);
   const resetTraffic = useCallback((client: ClientRecord) => {
     if (!client?.email) return Promise.resolve(null as unknown as Msg<unknown>);
     return resetTrafficMut.mutateAsync(client.email);
@@ -442,9 +486,12 @@ export function useClients() {
     remove,
     bulkDelete,
     bulkAdjust,
-    bulkAssignGroup,
+    bulkAddToGroup,
+    bulkRemoveFromGroup,
     attach,
+    bulkAttach,
     detach,
+    bulkDetach,
     resetTraffic,
     resetAllTraffics,
     delDepleted,

+ 1 - 1
frontend/src/lib/xray/inbound-defaults.ts

@@ -46,7 +46,7 @@ interface ClientBase {
 
 function clientBase(seed: ClientBaseSeed = {}): ClientBase {
   return {
-    email: seed.email ?? RandomUtil.randomLowerAndNum(8),
+    email: seed.email ?? RandomUtil.randomLowerAndNum(10),
     limitIp: seed.limitIp ?? 0,
     totalGB: seed.totalGB ?? 0,
     expiryTime: seed.expiryTime ?? 0,

+ 27 - 9
frontend/src/lib/xray/inbound-form-adapter.ts

@@ -112,10 +112,26 @@ function healStreamNetworkKey(stream: Record<string, unknown>): void {
   }
 }
 
-// Map a raw DB row (settings/streamSettings/sniffing as string OR object)
-// into the typed InboundFormValues. Does NOT validate against the schema —
-// callers that want a hard guarantee should follow up with
-// InboundFormSchema.safeParse(...).
+function tlsCerts(stream: Record<string, unknown>): Record<string, unknown>[] {
+  const tls = stream.tlsSettings as { certificates?: unknown } | undefined;
+  return Array.isArray(tls?.certificates) ? tls.certificates as Record<string, unknown>[] : [];
+}
+
+function synthesizeTlsCertUseFile(stream: Record<string, unknown>): void {
+  for (const c of tlsCerts(stream)) {
+    if (typeof c.useFile === 'boolean') continue;
+    const hasFile = !!c.certificateFile || !!c.keyFile;
+    const hasInline =
+      (Array.isArray(c.certificate) && c.certificate.length > 0) ||
+      (Array.isArray(c.key) && c.key.length > 0);
+    c.useFile = hasFile || !hasInline;
+  }
+}
+
+function stripTlsCertUseFile(stream: Record<string, unknown>): void {
+  for (const c of tlsCerts(stream)) delete c.useFile;
+}
+
 export function rawInboundToFormValues(row: RawInboundRow): InboundFormValues {
   const protocol = (row.protocol || 'vless') as InboundSettings['protocol'];
   const settings = coerceJsonObject(row.settings) as InboundSettings['settings'];
@@ -125,6 +141,7 @@ export function rawInboundToFormValues(row: RawInboundRow): InboundFormValues {
     : undefined;
   if (streamSettings) {
     healStreamNetworkKey(streamSettings as unknown as Record<string, unknown>);
+    synthesizeTlsCertUseFile(streamSettings as unknown as Record<string, unknown>);
   }
   const sniffing = coerceJsonObject(row.sniffing) as unknown as Sniffing;
 
@@ -181,12 +198,12 @@ export function pruneEmpty(value: unknown): unknown {
 // gives us the canonical projection.
 function clientSchemaForProtocol(protocol: string): z.ZodType | null {
   switch (protocol) {
-    case 'vless':       return VlessClientSchema;
-    case 'vmess':       return VmessClientSchema;
-    case 'trojan':      return TrojanClientSchema;
+    case 'vless': return VlessClientSchema;
+    case 'vmess': return VmessClientSchema;
+    case 'trojan': return TrojanClientSchema;
     case 'shadowsocks': return ShadowsocksClientSchema;
-    case 'hysteria':    return HysteriaClientSchema;
-    default:            return null;
+    case 'hysteria': return HysteriaClientSchema;
+    default: return null;
   }
 }
 
@@ -265,6 +282,7 @@ export function formValuesToWirePayload(values: InboundFormValues): WireInboundP
   const streamPruned = values.streamSettings
     ? ((pruneEmpty(values.streamSettings) ?? {}) as Record<string, unknown>)
     : undefined;
+  if (streamPruned) stripTlsCertUseFile(streamPruned);
   dropLegacyOptionalEmpties(settingsPruned, streamPruned);
   const payload: WireInboundPayload = {
     up: values.up,

+ 34 - 10
frontend/src/lib/xray/inbound-link.ts

@@ -184,7 +184,9 @@ export function genVmessLink(input: GenVmessLinkInput): string {
         const request = header.request;
         if (request) {
           obj.path = request.path.join(',');
-          const host = getHeaderValue(request.headers, 'host');
+          const host =
+            getHeaderValue(header.response?.headers, 'host')
+            || getHeaderValue(request.headers, 'host');
           if (host) obj.host = host;
         }
       }
@@ -223,6 +225,9 @@ export function genVmessLink(input: GenVmessLinkInput): string {
     if (tlsSettings.serverName.length > 0) obj.sni = tlsSettings.serverName;
     if (tlsSettings.settings.fingerprint.length > 0) obj.fp = tlsSettings.settings.fingerprint;
     if (tlsSettings.alpn.length > 0) obj.alpn = tlsSettings.alpn.join(',');
+    if (tlsSettings.settings.pinnedPeerCertSha256.length > 0) {
+      obj.pcs = tlsSettings.settings.pinnedPeerCertSha256.join(',');
+    }
   }
 
   applyExternalProxyTLSObj(externalProxy, obj, tls);
@@ -309,7 +314,9 @@ export function genVlessLink(input: GenVlessLinkInput): string {
       const request = tcp.header.request;
       if (request) {
         params.set('path', request.path.join(','));
-        const host = getHeaderValue(request.headers, 'host');
+        const host =
+          getHeaderValue(tcp.header.response?.headers, 'host')
+          || getHeaderValue(request.headers, 'host');
         if (host) params.set('host', host);
         params.set('headerType', 'http');
       }
@@ -345,6 +352,9 @@ export function genVlessLink(input: GenVlessLinkInput): string {
       params.set('alpn', tls.alpn.join(','));
       if (tls.serverName.length > 0) params.set('sni', tls.serverName);
       if (tls.settings.echConfigList.length > 0) params.set('ech', tls.settings.echConfigList);
+      if (tls.settings.pinnedPeerCertSha256.length > 0) {
+        params.set('pcs', tls.settings.pinnedPeerCertSha256.join(','));
+      }
       if (stream.network === 'tcp' && flow.length > 0) params.set('flow', flow);
     }
     applyExternalProxyTLSParams(externalProxy, params, security);
@@ -354,13 +364,14 @@ export function genVlessLink(input: GenVlessLinkInput): string {
       const reality = stream.realitySettings;
       params.set('pbk', reality.settings.publicKey);
       params.set('fp', reality.settings.fingerprint);
-      // Legacy parity quirk: the old class stored realitySettings.serverNames
-      // as a comma-joined string and gated SNI on `!ObjectUtil.isArrEmpty(s)`
-      // — which returns true for any string, so SNI was never written into
-      // Reality share links. Existing deployed clients rely on receiving
-      // the SNI from realitySettings.target instead; we keep the omission
-      // here so this extraction stays byte-stable with the legacy URL.
-      // Fixing the bug is a separate intentional commit.
+
+      const sni =
+        reality.settings.serverName ||
+        reality.serverNames?.[0] ||
+        reality.target?.split(':')[0];
+
+      if (sni && sni.length > 0) params.set('sni', sni);
+
       if (reality.shortIds.length > 0) params.set('sid', reality.shortIds[0]);
       if (reality.settings.spiderX.length > 0) params.set('spx', reality.settings.spiderX);
       if (reality.settings.mldsa65Verify.length > 0) params.set('pqv', reality.settings.mldsa65Verify);
@@ -387,7 +398,9 @@ function writeNetworkParams(stream: NonNullable<Inbound['streamSettings']>, para
       const request = tcp.header.request;
       if (request) {
         params.set('path', request.path.join(','));
-        const host = getHeaderValue(request.headers, 'host');
+        const host =
+          getHeaderValue(tcp.header.response?.headers, 'host')
+          || getHeaderValue(request.headers, 'host');
         if (host) params.set('host', host);
         params.set('headerType', 'http');
       }
@@ -421,6 +434,9 @@ function writeTlsParams(stream: NonNullable<Inbound['streamSettings']>, params:
   params.set('alpn', tls.alpn.join(','));
   if (tls.settings.echConfigList.length > 0) params.set('ech', tls.settings.echConfigList);
   if (tls.serverName.length > 0) params.set('sni', tls.serverName);
+  if (tls.settings.pinnedPeerCertSha256.length > 0) {
+    params.set('pcs', tls.settings.pinnedPeerCertSha256.join(','));
+  }
 }
 
 // Reality query-string writer shared by VLESS and Trojan. Preserves the
@@ -430,6 +446,14 @@ function writeRealityParams(stream: NonNullable<Inbound['streamSettings']>, para
   const reality = stream.realitySettings;
   params.set('pbk', reality.settings.publicKey);
   params.set('fp', reality.settings.fingerprint);
+
+  const sni =
+    reality.settings.serverName ||
+    reality.serverNames?.[0] ||
+    reality.target?.split(':')[0];
+
+  if (sni && sni.length > 0) params.set('sni', sni);
+
   if (reality.shortIds.length > 0) params.set('sid', reality.shortIds[0]);
   if (reality.settings.spiderX.length > 0) params.set('spx', reality.settings.spiderX);
   if (reality.settings.mldsa65Verify.length > 0) params.set('pqv', reality.settings.mldsa65Verify);

+ 21 - 3
frontend/src/pages/api-docs/endpoints.ts

@@ -546,11 +546,18 @@ export const sections: readonly Section[] = [
       },
       {
         method: 'POST',
-        path: '/panel/api/clients/bulkAssignGroup',
-        summary: 'Assign the given group label to many clients in one call. Updates clients.group_name and patches the matching client entry inside every owning inbound\'s settings JSON in a single transaction. Pass an empty group to clear the label. If the group name does not yet exist (in client_groups or as a derived label), it is auto-created as a persistent group.',
+        path: '/panel/api/clients/groups/bulkAdd',
+        summary: 'Add many clients to a group in one call. Updates clients.group_name and patches the matching client entry inside every owning inbound\'s settings JSON in a single transaction. If the group name does not yet exist (in client_groups or as a derived label), it is auto-created as a persistent group. To clear the group label, use /groups/bulkRemove instead.',
         body: '{\n  "emails": ["alice", "bob"],\n  "group": "customer-a"\n}',
         response: '{\n  "success": true,\n  "obj": {\n    "affected": 2\n  }\n}',
       },
+      {
+        method: 'POST',
+        path: '/panel/api/clients/groups/bulkRemove',
+        summary: 'Clear the group label on many clients in one call. Inverse of /groups/bulkAdd. Clients themselves are kept — only the group label is cleared from clients.group_name and from each owning inbound\'s settings JSON. Groups become empty if all their members are removed.',
+        body: '{\n  "emails": ["alice", "bob"]\n}',
+        response: '{\n  "success": true,\n  "obj": {\n    "affected": 2\n  }\n}',
+      },
       {
         method: 'POST',
         path: '/panel/api/clients/bulkAttach',
@@ -562,6 +569,17 @@ export const sections: readonly Section[] = [
         body: '{\n  "emails": ["alice", "bob"],\n  "inboundIds": [7, 9]\n}',
         response: '{\n  "success": true,\n  "obj": {\n    "attached": ["alice", "bob"],\n    "skipped": ["bob"],\n    "errors": []\n  }\n}',
       },
+      {
+        method: 'POST',
+        path: '/panel/api/clients/bulkDetach',
+        summary: 'Mirror of bulkAttach: detach many existing clients from many inbounds in one call. For each email, intersects the client\'s current inbounds with the requested set and detaches from those only; (email, inbound) pairs where the client is not currently attached are silently no-ops. Emails not attached to any of the requested inbounds are reported under skipped. Client records are kept even if they become orphaned — use bulkDel for full removal. Returns per-email detached/skipped/errors lists and triggers a single Xray restart if any target inbound was running.',
+        params: [
+          { name: 'emails', in: 'body (json)', type: 'array', desc: 'Emails of existing clients to detach.' },
+          { name: 'inboundIds', in: 'body (json)', type: 'integer[]', desc: 'Inbound IDs to detach the clients from.' },
+        ],
+        body: '{\n  "emails": ["alice", "bob"],\n  "inboundIds": [7, 9]\n}',
+        response: '{\n  "success": true,\n  "obj": {\n    "detached": ["alice", "bob"],\n    "skipped": [],\n    "errors": []\n  }\n}',
+      },
       {
         method: 'POST',
         path: '/panel/api/clients/bulkResetTraffic',
@@ -587,7 +605,7 @@ export const sections: readonly Section[] = [
       {
         method: 'POST',
         path: '/panel/api/clients/groups/create',
-        summary: 'Create a new empty (placeholder) group. The group becomes selectable in client forms and the filter drawer even before any client is assigned to it. Errors if a group with the same name already exists.',
+        summary: 'Create a new empty (placeholder) group. The group becomes selectable in client forms and the filter drawer even before any client is added to it. Errors if a group with the same name already exists.',
         body: '{\n  "name": "customer-a"\n}',
         response: '{\n  "success": true,\n  "obj": {\n    "name": "customer-a"\n  }\n}',
       },

+ 10 - 12
frontend/src/pages/clients/BulkAssignGroupModal.tsx → frontend/src/pages/clients/BulkAddToGroupModal.tsx

@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { AutoComplete, Form, Modal, message } from 'antd';
 
-interface BulkAssignGroupModalProps {
+interface BulkAddToGroupModalProps {
   open: boolean;
   count: number;
   groups: string[];
@@ -10,13 +10,13 @@ interface BulkAssignGroupModalProps {
   onSubmit: (group: string) => Promise<{ affected?: number } | null>;
 }
 
-export default function BulkAssignGroupModal({
+export default function BulkAddToGroupModal({
   open,
   count,
   groups,
   onOpenChange,
   onSubmit,
-}: BulkAssignGroupModalProps) {
+}: BulkAddToGroupModalProps) {
   const { t } = useTranslation();
   const [messageApi, messageContextHolder] = message.useMessage();
   const [value, setValue] = useState('');
@@ -28,16 +28,13 @@ export default function BulkAssignGroupModal({
 
   async function submit() {
     const next = value.trim();
+    if (!next) return;
     setSubmitting(true);
     try {
       const result = await onSubmit(next);
       if (result) {
         const affected = result.affected ?? 0;
-        if (next === '') {
-          messageApi.success(t('pages.clients.assignGroupClearedToast', { count: affected }));
-        } else {
-          messageApi.success(t('pages.clients.assignGroupAssignedToast', { count: affected, group: next }));
-        }
+        messageApi.success(t('pages.clients.addToGroupSuccessToast', { count: affected, group: next }));
         onOpenChange(false);
       }
     } finally {
@@ -50,10 +47,11 @@ export default function BulkAssignGroupModal({
       {messageContextHolder}
       <Modal
         open={open}
-        title={t('pages.clients.assignGroupTitle', { count })}
-        okText={t('save')}
+        title={t('pages.clients.addToGroupTitle', { count })}
+        okText={t('add')}
         cancelText={t('cancel')}
         confirmLoading={submitting}
+        okButtonProps={{ disabled: !value.trim() }}
         onCancel={() => onOpenChange(false)}
         onOk={submit}
         destroyOnHidden
@@ -61,11 +59,11 @@ export default function BulkAssignGroupModal({
         <Form layout="vertical">
           <Form.Item
             label={t('pages.clients.group')}
-            tooltip={t('pages.clients.assignGroupTooltip')}
+            tooltip={t('pages.clients.addToGroupTooltip')}
           >
             <AutoComplete
               value={value}
-              placeholder={t('pages.clients.assignGroupPlaceholder')}
+              placeholder={t('pages.clients.addToGroupPlaceholder')}
               options={groups.map((g) => ({ value: g }))}
               onChange={(v) => setValue(v ?? '')}
               filterOption={(input, option) =>

+ 98 - 0
frontend/src/pages/clients/BulkAttachInboundsModal.tsx

@@ -0,0 +1,98 @@
+import { useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Alert, Modal, Select, Typography, message } from 'antd';
+
+import type { InboundOption } from '@/hooks/useClients';
+import type { BulkAttachResult } from '@/schemas/client';
+
+const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks']);
+
+interface BulkAttachInboundsModalProps {
+  open: boolean;
+  count: number;
+  inbounds: InboundOption[];
+  onOpenChange: (open: boolean) => void;
+  onSubmit: (inboundIds: number[]) => Promise<BulkAttachResult | null>;
+}
+
+export default function BulkAttachInboundsModal({
+  open,
+  count,
+  inbounds,
+  onOpenChange,
+  onSubmit,
+}: BulkAttachInboundsModalProps) {
+  const { t } = useTranslation();
+  const [messageApi, messageContextHolder] = message.useMessage();
+  const [targetIds, setTargetIds] = useState<number[]>([]);
+  const [submitting, setSubmitting] = useState(false);
+
+  useEffect(() => {
+    if (open) setTargetIds([]);
+  }, [open]);
+
+  const targetOptions = useMemo(() => {
+    return (inbounds || [])
+      .filter((ib) => MULTI_USER_PROTOCOLS.has((ib.protocol || '').toLowerCase()))
+      .map((ib) => ({
+        value: ib.id,
+        label: `${ib.remark ?? ''} (${ib.protocol ?? ''}@${ib.port ?? ''})`,
+      }));
+  }, [inbounds]);
+
+  async function submit() {
+    if (targetIds.length === 0 || count === 0) return;
+    setSubmitting(true);
+    try {
+      const result = await onSubmit(targetIds);
+      if (!result) return;
+      const attached = result.attached?.length ?? 0;
+      const skipped = result.skipped?.length ?? 0;
+      const errors = result.errors?.length ?? 0;
+      if (errors > 0) {
+        messageApi.warning(
+          t('pages.inbounds.attachClientsResultMixed', { attached, skipped, errors }),
+        );
+      } else {
+        messageApi.success(t('pages.inbounds.attachClientsResult', { attached, skipped }));
+      }
+      onOpenChange(false);
+    } finally {
+      setSubmitting(false);
+    }
+  }
+
+  return (
+    <>
+      {messageContextHolder}
+      <Modal
+        open={open}
+        title={t('pages.clients.attachToInboundsTitle', { count })}
+        okText={t('pages.inbounds.attachClients')}
+        cancelText={t('cancel')}
+        okButtonProps={{ disabled: targetIds.length === 0, loading: submitting }}
+        onCancel={() => onOpenChange(false)}
+        onOk={submit}
+        destroyOnHidden
+      >
+        <Typography.Paragraph type="secondary">
+          {t('pages.clients.attachToInboundsDesc', { count })}
+        </Typography.Paragraph>
+        {targetOptions.length === 0 ? (
+          <Alert type="info" showIcon message={t('pages.clients.attachToInboundsNoTargets')} />
+        ) : (
+          <Select
+            mode="multiple"
+            style={{ width: '100%' }}
+            value={targetIds}
+            onChange={setTargetIds}
+            options={targetOptions}
+            placeholder={t('pages.clients.attachToInboundsTargets')}
+            optionFilterProp="label"
+            autoFocus
+          />
+        )}
+      </Modal>
+    </>
+  );
+}

+ 98 - 0
frontend/src/pages/clients/BulkDetachInboundsModal.tsx

@@ -0,0 +1,98 @@
+import { useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Alert, Modal, Select, Typography, message } from 'antd';
+
+import type { InboundOption } from '@/hooks/useClients';
+import type { BulkDetachResult } from '@/schemas/client';
+
+const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks']);
+
+interface BulkDetachInboundsModalProps {
+  open: boolean;
+  count: number;
+  inbounds: InboundOption[];
+  onOpenChange: (open: boolean) => void;
+  onSubmit: (inboundIds: number[]) => Promise<BulkDetachResult | null>;
+}
+
+export default function BulkDetachInboundsModal({
+  open,
+  count,
+  inbounds,
+  onOpenChange,
+  onSubmit,
+}: BulkDetachInboundsModalProps) {
+  const { t } = useTranslation();
+  const [messageApi, messageContextHolder] = message.useMessage();
+  const [targetIds, setTargetIds] = useState<number[]>([]);
+  const [submitting, setSubmitting] = useState(false);
+
+  useEffect(() => {
+    if (open) setTargetIds([]);
+  }, [open]);
+
+  const targetOptions = useMemo(() => {
+    return (inbounds || [])
+      .filter((ib) => MULTI_USER_PROTOCOLS.has((ib.protocol || '').toLowerCase()))
+      .map((ib) => ({
+        value: ib.id,
+        label: `${ib.remark ?? ''} (${ib.protocol ?? ''}@${ib.port ?? ''})`,
+      }));
+  }, [inbounds]);
+
+  async function submit() {
+    if (targetIds.length === 0 || count === 0) return;
+    setSubmitting(true);
+    try {
+      const result = await onSubmit(targetIds);
+      if (!result) return;
+      const detached = result.detached?.length ?? 0;
+      const skipped = result.skipped?.length ?? 0;
+      const errors = result.errors?.length ?? 0;
+      if (errors > 0) {
+        messageApi.warning(
+          t('pages.clients.detachFromInboundsResultMixed', { detached, skipped, errors }),
+        );
+      } else {
+        messageApi.success(t('pages.clients.detachFromInboundsResult', { detached, skipped }));
+      }
+      onOpenChange(false);
+    } finally {
+      setSubmitting(false);
+    }
+  }
+
+  return (
+    <>
+      {messageContextHolder}
+      <Modal
+        open={open}
+        title={t('pages.clients.detachFromInboundsTitle', { count })}
+        okText={t('pages.clients.detach')}
+        cancelText={t('cancel')}
+        okButtonProps={{ danger: true, disabled: targetIds.length === 0, loading: submitting }}
+        onCancel={() => onOpenChange(false)}
+        onOk={submit}
+        destroyOnHidden
+      >
+        <Typography.Paragraph type="secondary">
+          {t('pages.clients.detachFromInboundsDesc', { count })}
+        </Typography.Paragraph>
+        {targetOptions.length === 0 ? (
+          <Alert type="info" showIcon message={t('pages.clients.detachFromInboundsNoTargets')} />
+        ) : (
+          <Select
+            mode="multiple"
+            style={{ width: '100%' }}
+            value={targetIds}
+            onChange={setTargetIds}
+            options={targetOptions}
+            placeholder={t('pages.clients.detachFromInboundsTargets')}
+            optionFilterProp="label"
+            autoFocus
+          />
+        )}
+      </Modal>
+    </>
+  );
+}

+ 1 - 1
frontend/src/pages/clients/ClientBulkAddModal.tsx

@@ -130,7 +130,7 @@ export default function ClientBulkAddModal({
     const postfix = method > 2 && form.emailPostfix.length > 0 ? form.emailPostfix : '';
     for (let i = start; i < end; i++) {
       let email = '';
-      if (method !== 4) email = RandomUtil.randomLowerAndNum(6);
+      if (method !== 4) email = RandomUtil.randomLowerAndNum(10);
       email += useNum ? prefix + String(i) + postfix : prefix + postfix;
       out.push(email);
     }

+ 1 - 1
frontend/src/pages/clients/ClientFormModal.tsx

@@ -191,7 +191,7 @@ export default function ClientFormModal({
     } else {
       setForm({
         ...emptyForm(),
-        email: RandomUtil.randomLowerAndNum(9),
+        email: RandomUtil.randomLowerAndNum(10),
         uuid: RandomUtil.randomUUID(),
         subId: RandomUtil.randomLowerAndNum(16),
         password: RandomUtil.randomLowerAndNum(16),

+ 13 - 0
frontend/src/pages/clients/ClientsPage.css

@@ -77,6 +77,19 @@
   padding: 6px 0;
 }
 
+@media (min-width: 769px) and (max-width: 920px) {
+  .card-toolbar {
+    gap: 6px;
+  }
+  .card-toolbar .ant-btn .ant-btn-icon + span,
+  .card-toolbar .ant-btn > span:not(.ant-btn-icon):not(.ant-tag):not(.anticon) {
+    display: none;
+  }
+  .card-toolbar .ant-btn {
+    padding-inline: 8px;
+  }
+}
+
 .email-cell {
   display: flex;
   flex-direction: column;

+ 169 - 38
frontend/src/pages/clients/ClientsPage.tsx

@@ -42,6 +42,7 @@ import {
   TagsOutlined,
   TeamOutlined,
   UsergroupAddOutlined,
+  UsergroupDeleteOutlined,
 } from '@ant-design/icons';
 
 import { useTheme } from '@/hooks/useTheme';
@@ -61,13 +62,54 @@ const ClientBulkAddModal = lazy(() => import('./ClientBulkAddModal'));
 const ClientBulkAdjustModal = lazy(() => import('./ClientBulkAdjustModal'));
 const FilterDrawer = lazy(() => import('./FilterDrawer'));
 const SubLinksModal = lazy(() => import('./SubLinksModal'));
-const BulkAssignGroupModal = lazy(() => import('./BulkAssignGroupModal'));
+const BulkAddToGroupModal = lazy(() => import('./BulkAddToGroupModal'));
+const BulkAttachInboundsModal = lazy(() => import('./BulkAttachInboundsModal'));
+const BulkDetachInboundsModal = lazy(() => import('./BulkDetachInboundsModal'));
 import { emptyFilters, activeFilterCount } from './filters';
 import type { ClientFilters } from './filters';
 import './ClientsPage.css';
 
 const FILTER_STATE_KEY = 'clientsFilterState';
 
+function UngroupIcon() {
+  return (
+    <span
+      style={{
+        position: 'relative',
+        display: 'inline-flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+        width: '1em',
+        height: '1em',
+      }}
+    >
+      <TagsOutlined />
+      <span
+        aria-hidden="true"
+        style={{
+          position: 'absolute',
+          inset: 0,
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+          pointerEvents: 'none',
+        }}
+      >
+        <span
+          style={{
+            display: 'block',
+            width: '125%',
+            height: '1.5px',
+            background: 'currentColor',
+            transform: 'rotate(-45deg)',
+            borderRadius: '1px',
+          }}
+        />
+      </span>
+    </span>
+  );
+}
+
 type Bucket = 'active' | 'deactive' | 'depleted' | 'expiring';
 
 interface PersistedFilterState {
@@ -149,7 +191,7 @@ export default function ClientsPage() {
     setQuery,
     inbounds, onlines, loading, fetched, subSettings,
     ipLimitEnable, tgBotEnable, expireDiff, trafficDiff, pageSize,
-    create, update, remove, bulkDelete, bulkAdjust, bulkAssignGroup, attach, detach,
+    create, update, remove, bulkDelete, bulkAdjust, bulkAddToGroup, bulkRemoveFromGroup, attach, bulkAttach, detach, bulkDetach,
     resetTraffic, resetAllTraffics, delDepleted, setEnable,
     applyTrafficEvent, applyClientStatsEvent,
     hydrate,
@@ -173,6 +215,8 @@ export default function ClientsPage() {
   const [bulkAdjustOpen, setBulkAdjustOpen] = useState(false);
   const [subLinksOpen, setSubLinksOpen] = useState(false);
   const [bulkGroupOpen, setBulkGroupOpen] = useState(false);
+  const [bulkAttachOpen, setBulkAttachOpen] = useState(false);
+  const [bulkDetachOpen, setBulkDetachOpen] = useState(false);
   const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
 
   const initial = readFilterState();
@@ -456,6 +500,26 @@ export default function ClientsPage() {
     });
   }
 
+  function onBulkUngroup() {
+    const emails = [...selectedRowKeys];
+    if (emails.length === 0) return;
+    modal.confirm({
+      title: t('pages.clients.ungroupConfirmTitle', { count: emails.length }),
+      content: t('pages.clients.ungroupConfirmContent'),
+      okText: t('confirm'),
+      okType: 'danger',
+      cancelText: t('cancel'),
+      onOk: async () => {
+        const msg = await bulkRemoveFromGroup(emails);
+        if (msg?.success) {
+          setSelectedRowKeys([]);
+          const affected = (msg.obj as { affected?: number } | undefined)?.affected ?? emails.length;
+          messageApi.success(t('pages.clients.ungroupSuccessToast', { count: affected }));
+        }
+      },
+    });
+  }
+
   function onBulkDelete() {
     const emails = [...selectedRowKeys];
     if (emails.length === 0) return;
@@ -581,6 +645,7 @@ export default function ClientsPage() {
       title: t('pages.clients.group'),
       key: 'group',
       width: 130,
+      hidden: allGroups.length === 0,
       render: (_v, record) => {
         if (!record.group) return <span style={{ color: 'rgba(0,0,0,0.45)' }}>—</span>;
         const isActive = filters.groups.includes(record.group);
@@ -665,7 +730,7 @@ export default function ClientsPage() {
       ),
     },
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  ], [t, togglingEmail, clientBucket, isOnline, inboundsById, filters]);
+  ], [t, togglingEmail, clientBucket, isOnline, inboundsById, filters, allGroups]);
 
   const tablePagination = {
     current: currentPage,
@@ -778,22 +843,31 @@ export default function ClientsPage() {
                       hoverable
                       title={
                         <div className="card-toolbar">
-                          <Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
-                            {!isMobile && t('pages.clients.addClients')}
-                          </Button>
-                          {selectedRowKeys.length > 0 && (
+                          {selectedRowKeys.length === 0 ? (
+                            <Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
+                              {!isMobile && t('pages.clients.addClients')}
+                            </Button>
+                          ) : (
                             <>
-                              <Button icon={<ClockCircleOutlined />} onClick={() => setBulkAdjustOpen(true)}>
-                                {t('pages.clients.adjustSelected', { count: selectedRowKeys.length })}
+                              <Tag
+                                color="blue"
+                                closable
+                                onClose={() => setSelectedRowKeys([])}
+                                style={{ marginInlineEnd: 0, padding: '4px 8px', fontSize: 13 }}
+                              >
+                                {t('pages.clients.selectedCount', { count: selectedRowKeys.length })}
+                              </Tag>
+                              <Button icon={<UsergroupAddOutlined />} onClick={() => setBulkAttachOpen(true)}>
+                                {!isMobile && t('pages.clients.attach')}
                               </Button>
-                              <Button icon={<TagsOutlined />} onClick={() => setBulkGroupOpen(true)}>
-                                {t('pages.clients.assignGroupSelected', { count: selectedRowKeys.length })}
+                              <Button danger icon={<UsergroupDeleteOutlined />} onClick={() => setBulkDetachOpen(true)}>
+                                {!isMobile && t('pages.clients.detach')}
                               </Button>
-                              <Button icon={<LinkOutlined />} onClick={() => setSubLinksOpen(true)}>
-                                {t('pages.clients.subLinksSelected', { count: selectedRowKeys.length })}
+                              <Button icon={<TagsOutlined />} onClick={() => setBulkGroupOpen(true)}>
+                                {!isMobile && t('pages.clients.addToGroup')}
                               </Button>
-                              <Button danger icon={<DeleteOutlined />} onClick={onBulkDelete}>
-                                {t('pages.clients.deleteSelected', { count: selectedRowKeys.length })}
+                              <Button danger icon={<UngroupIcon />} onClick={onBulkUngroup}>
+                                {!isMobile && t('pages.clients.ungroup')}
                               </Button>
                             </>
                           )}
@@ -801,33 +875,58 @@ export default function ClientsPage() {
                             trigger={['click']}
                             placement="bottomRight"
                             menu={{
-                              items: [
-                                {
-                                  key: 'bulk',
-                                  icon: <UsergroupAddOutlined />,
-                                  label: t('pages.clients.bulk'),
-                                  onClick: () => setBulkAddOpen(true),
-                                },
-                                {
-                                  key: 'resetAll',
-                                  icon: <RetweetOutlined />,
-                                  label: t('pages.clients.resetAllTraffics'),
-                                  onClick: onResetAllTraffics,
-                                },
-                                {
-                                  key: 'delDepleted',
-                                  icon: <RestOutlined />,
-                                  label: t('pages.clients.delDepleted'),
-                                  danger: true,
-                                  onClick: onDelDepleted,
-                                },
-                              ],
+                              items: selectedRowKeys.length > 0
+                                ? [
+                                    {
+                                      key: 'adjust',
+                                      icon: <ClockCircleOutlined />,
+                                      label: t('pages.clients.adjust'),
+                                      onClick: () => setBulkAdjustOpen(true),
+                                    },
+                                    {
+                                      key: 'subLinks',
+                                      icon: <LinkOutlined />,
+                                      label: t('pages.clients.subLinks'),
+                                      onClick: () => setSubLinksOpen(true),
+                                    },
+                                  ]
+                                : [
+                                    {
+                                      key: 'bulk',
+                                      icon: <UsergroupAddOutlined />,
+                                      label: t('pages.clients.bulk'),
+                                      onClick: () => setBulkAddOpen(true),
+                                    },
+                                    {
+                                      key: 'resetAll',
+                                      icon: <RetweetOutlined />,
+                                      label: t('pages.clients.resetAllTraffics'),
+                                      onClick: onResetAllTraffics,
+                                    },
+                                    {
+                                      key: 'delDepleted',
+                                      icon: <RestOutlined />,
+                                      label: t('pages.clients.delDepleted'),
+                                      danger: true,
+                                      onClick: onDelDepleted,
+                                    },
+                                  ],
                             }}
                           >
                             <Button icon={<MoreOutlined />}>
                               {!isMobile && t('more')}
                             </Button>
                           </Dropdown>
+                          {selectedRowKeys.length > 0 && (
+                            <Button
+                              danger
+                              icon={<DeleteOutlined />}
+                              onClick={onBulkDelete}
+                              style={{ marginInlineStart: 'auto' }}
+                            >
+                              {!isMobile && t('delete')}
+                            </Button>
+                          )}
                         </div>
                       }
                     >
@@ -1142,13 +1241,13 @@ export default function ClientsPage() {
           />
         </LazyMount>
         <LazyMount when={bulkGroupOpen}>
-          <BulkAssignGroupModal
+          <BulkAddToGroupModal
             open={bulkGroupOpen}
             count={selectedRowKeys.length}
             groups={allGroups}
             onOpenChange={setBulkGroupOpen}
             onSubmit={async (group) => {
-              const msg = await bulkAssignGroup([...selectedRowKeys], group);
+              const msg = await bulkAddToGroup([...selectedRowKeys], group);
               if (msg?.success) {
                 setSelectedRowKeys([]);
                 return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
@@ -1157,6 +1256,38 @@ export default function ClientsPage() {
             }}
           />
         </LazyMount>
+        <LazyMount when={bulkAttachOpen}>
+          <BulkAttachInboundsModal
+            open={bulkAttachOpen}
+            count={selectedRowKeys.length}
+            inbounds={inbounds}
+            onOpenChange={setBulkAttachOpen}
+            onSubmit={async (inboundIds) => {
+              const msg = await bulkAttach([...selectedRowKeys], inboundIds);
+              if (msg?.success) {
+                setSelectedRowKeys([]);
+                return msg.obj ?? { attached: [], skipped: [], errors: [] };
+              }
+              return null;
+            }}
+          />
+        </LazyMount>
+        <LazyMount when={bulkDetachOpen}>
+          <BulkDetachInboundsModal
+            open={bulkDetachOpen}
+            count={selectedRowKeys.length}
+            inbounds={inbounds}
+            onOpenChange={setBulkDetachOpen}
+            onSubmit={async (inboundIds) => {
+              const msg = await bulkDetach([...selectedRowKeys], inboundIds);
+              if (msg?.success) {
+                setSelectedRowKeys([]);
+                return msg.obj ?? { detached: [], skipped: [], errors: [] };
+              }
+              return null;
+            }}
+          />
+        </LazyMount>
         <LazyMount when={filterDrawerOpen}>
           <FilterDrawer
             open={filterDrawerOpen}

+ 161 - 0
frontend/src/pages/groups/GroupAddClientsModal.tsx

@@ -0,0 +1,161 @@
+import { useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Alert, Input, Modal, Space, Table, Tag, Typography, message } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+
+import type { ClientRecord } from '@/hooks/useClients';
+
+interface GroupAddClientsModalProps {
+  open: boolean;
+  groupName: string | null;
+  candidates: ClientRecord[];
+  onClose: () => void;
+  onSubmit: (emails: string[]) => Promise<{ affected?: number } | null>;
+}
+
+interface ClientRow {
+  email: string;
+  comment: string;
+  enable: boolean;
+  currentGroup: string;
+}
+
+export default function GroupAddClientsModal({
+  open,
+  groupName,
+  candidates,
+  onClose,
+  onSubmit,
+}: GroupAddClientsModalProps) {
+  const { t } = useTranslation();
+  const [messageApi, messageContextHolder] = message.useMessage();
+  const [saving, setSaving] = useState(false);
+  const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
+  const [search, setSearch] = useState('');
+
+  const rows = useMemo<ClientRow[]>(
+    () =>
+      (candidates || [])
+        .map((c) => ({
+          email: (c.email || '').trim(),
+          comment: (c.comment || '').trim(),
+          enable: c.enable !== false,
+          currentGroup: (c.group || '').trim(),
+        }))
+        .filter((r) => r.email),
+    [candidates],
+  );
+
+  useEffect(() => {
+    if (!open) return;
+    setSelectedEmails([]);
+    setSearch('');
+  }, [open, rows]);
+
+  const filteredRows = useMemo(() => {
+    const q = search.trim().toLowerCase();
+    if (!q) return rows;
+    return rows.filter(
+      (r) =>
+        r.email.toLowerCase().includes(q) ||
+        r.comment.toLowerCase().includes(q) ||
+        r.currentGroup.toLowerCase().includes(q),
+    );
+  }, [rows, search]);
+
+  const columns: ColumnsType<ClientRow> = useMemo(
+    () => [
+      { title: t('pages.inbounds.email'), dataIndex: 'email', key: 'email', ellipsis: true },
+      { title: t('comment'), dataIndex: 'comment', key: 'comment', ellipsis: true },
+      {
+        title: t('pages.clients.group'),
+        dataIndex: 'currentGroup',
+        key: 'currentGroup',
+        width: 140,
+        ellipsis: true,
+        render: (g: string) =>
+          g ? <Tag color="geekblue">{g}</Tag> : <span style={{ color: 'rgba(0,0,0,0.45)' }}>—</span>,
+      },
+      {
+        title: t('enable'),
+        dataIndex: 'enable',
+        key: 'enable',
+        width: 90,
+        render: (enabled: boolean) =>
+          enabled ? (
+            <Tag color="success">{t('enable')}</Tag>
+          ) : (
+            <Tag>{t('pages.inbounds.attachClientsStatusDisabled')}</Tag>
+          ),
+      },
+    ],
+    [t],
+  );
+
+  async function submit() {
+    if (!groupName || selectedEmails.length === 0) return;
+    setSaving(true);
+    try {
+      const result = await onSubmit(selectedEmails);
+      if (!result) return;
+      const affected = result.affected ?? selectedEmails.length;
+      messageApi.success(t('pages.groups.addToGroupResult', { count: affected, name: groupName }));
+      onClose();
+    } finally {
+      setSaving(false);
+    }
+  }
+
+  return (
+    <Modal
+      open={open}
+      onCancel={onClose}
+      onOk={submit}
+      okButtonProps={{ disabled: selectedEmails.length === 0, loading: saving }}
+      okText={t('add')}
+      cancelText={t('cancel')}
+      title={t('pages.groups.addToGroupTitle', { name: groupName ?? '' })}
+      width={720}
+    >
+      {messageContextHolder}
+      <Typography.Paragraph type="secondary">
+        {t('pages.groups.addToGroupDesc')}
+      </Typography.Paragraph>
+
+      <Space direction="vertical" size="small" style={{ width: '100%' }}>
+        <Space style={{ width: '100%', justifyContent: 'space-between' }} wrap>
+          <Input.Search
+            allowClear
+            value={search}
+            onChange={(e) => setSearch(e.target.value)}
+            placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
+            style={{ maxWidth: 320 }}
+          />
+          <Typography.Text type="secondary">
+            {t('pages.inbounds.attachClientsSelectedCount', {
+              selected: selectedEmails.length,
+              total: rows.length,
+            })}
+          </Typography.Text>
+        </Space>
+        {rows.length === 0 ? (
+          <Alert type="info" showIcon message={t('pages.groups.addToGroupEmpty')} />
+        ) : (
+          <Table<ClientRow>
+            size="small"
+            rowKey="email"
+            columns={columns}
+            dataSource={filteredRows}
+            pagination={false}
+            scroll={{ y: 320 }}
+            rowSelection={{
+              selectedRowKeys: selectedEmails,
+              onChange: (keys) => setSelectedEmails(keys as string[]),
+              preserveSelectedRowKeys: true,
+            }}
+          />
+        )}
+      </Space>
+    </Modal>
+  );
+}

+ 145 - 0
frontend/src/pages/groups/GroupRemoveClientsModal.tsx

@@ -0,0 +1,145 @@
+import { useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Input, Modal, Space, Table, Tag, Typography, message } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+
+import type { ClientRecord } from '@/hooks/useClients';
+
+interface GroupRemoveClientsModalProps {
+  open: boolean;
+  groupName: string | null;
+  members: ClientRecord[];
+  onClose: () => void;
+  onSubmit: (emails: string[]) => Promise<{ affected?: number } | null>;
+}
+
+interface ClientRow {
+  email: string;
+  comment: string;
+  enable: boolean;
+}
+
+export default function GroupRemoveClientsModal({
+  open,
+  groupName,
+  members,
+  onClose,
+  onSubmit,
+}: GroupRemoveClientsModalProps) {
+  const { t } = useTranslation();
+  const [messageApi, messageContextHolder] = message.useMessage();
+  const [saving, setSaving] = useState(false);
+  const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
+  const [search, setSearch] = useState('');
+
+  const rows = useMemo<ClientRow[]>(
+    () =>
+      (members || [])
+        .map((c) => ({
+          email: (c.email || '').trim(),
+          comment: (c.comment || '').trim(),
+          enable: c.enable !== false,
+        }))
+        .filter((r) => r.email),
+    [members],
+  );
+
+  useEffect(() => {
+    if (!open) return;
+    setSelectedEmails([]);
+    setSearch('');
+  }, [open, rows]);
+
+  const filteredRows = useMemo(() => {
+    const q = search.trim().toLowerCase();
+    if (!q) return rows;
+    return rows.filter(
+      (r) => r.email.toLowerCase().includes(q) || r.comment.toLowerCase().includes(q),
+    );
+  }, [rows, search]);
+
+  const columns: ColumnsType<ClientRow> = useMemo(
+    () => [
+      { title: t('pages.inbounds.email'), dataIndex: 'email', key: 'email', ellipsis: true },
+      { title: t('comment'), dataIndex: 'comment', key: 'comment', ellipsis: true },
+      {
+        title: t('enable'),
+        dataIndex: 'enable',
+        key: 'enable',
+        width: 90,
+        render: (enabled: boolean) =>
+          enabled ? (
+            <Tag color="success">{t('enable')}</Tag>
+          ) : (
+            <Tag>{t('pages.inbounds.attachClientsStatusDisabled')}</Tag>
+          ),
+      },
+    ],
+    [t],
+  );
+
+  async function submit() {
+    if (!groupName || selectedEmails.length === 0) return;
+    setSaving(true);
+    try {
+      const result = await onSubmit(selectedEmails);
+      if (!result) return;
+      const affected = result.affected ?? selectedEmails.length;
+      messageApi.success(
+        t('pages.groups.removeFromGroupResult', { count: affected, name: groupName }),
+      );
+      onClose();
+    } finally {
+      setSaving(false);
+    }
+  }
+
+  return (
+    <Modal
+      open={open}
+      onCancel={onClose}
+      onOk={submit}
+      okButtonProps={{ danger: true, disabled: selectedEmails.length === 0, loading: saving }}
+      okText={t('remove')}
+      cancelText={t('cancel')}
+      title={t('pages.groups.removeFromGroupTitle', { name: groupName ?? '' })}
+      width={680}
+    >
+      {messageContextHolder}
+      <Typography.Paragraph type="secondary">
+        {t('pages.groups.removeFromGroupDesc')}
+      </Typography.Paragraph>
+
+      <Space direction="vertical" size="small" style={{ width: '100%' }}>
+        <Space style={{ width: '100%', justifyContent: 'space-between' }} wrap>
+          <Input.Search
+            allowClear
+            value={search}
+            onChange={(e) => setSearch(e.target.value)}
+            placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
+            style={{ maxWidth: 320 }}
+          />
+          <Typography.Text type="secondary">
+            {t('pages.inbounds.attachClientsSelectedCount', {
+              selected: selectedEmails.length,
+              total: rows.length,
+            })}
+          </Typography.Text>
+        </Space>
+        <Table<ClientRow>
+          size="small"
+          rowKey="email"
+          columns={columns}
+          dataSource={filteredRows}
+          pagination={false}
+          scroll={{ y: 280 }}
+          rowSelection={{
+            selectedRowKeys: selectedEmails,
+            onChange: (keys) => setSelectedEmails(keys as string[]),
+            preserveSelectedRowKeys: true,
+          }}
+        />
+      </Space>
+    </Modal>
+  );
+}

+ 92 - 5
frontend/src/pages/groups/GroupsPage.tsx

@@ -30,8 +30,11 @@ import {
   RetweetOutlined,
   TagsOutlined,
   TeamOutlined,
+  UsergroupAddOutlined,
+  UsergroupDeleteOutlined,
 } from '@ant-design/icons';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { z } from 'zod';
 
 import { useTheme } from '@/hooks/useTheme';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
@@ -42,11 +45,20 @@ import { setMessageInstance } from '@/utils/messageBus';
 import AppSidebar from '@/components/AppSidebar';
 import LazyMount from '@/components/LazyMount';
 import { keys } from '@/api/queryKeys';
-import { GroupSummaryListSchema, type GroupSummary } from '@/schemas/client';
+import {
+  ClientRecordSchema,
+  GroupSummaryListSchema,
+  type ClientRecord,
+  type GroupSummary,
+} from '@/schemas/client';
 import { parseMsg } from '@/utils/zodValidate';
 
+const ClientRecordListSchema = z.array(ClientRecordSchema).nullable().transform((v) => v ?? []);
+
 const SubLinksModal = lazy(() => import('../clients/SubLinksModal'));
 const ClientBulkAdjustModal = lazy(() => import('../clients/ClientBulkAdjustModal'));
+const GroupAddClientsModal = lazy(() => import('./GroupAddClientsModal'));
+const GroupRemoveClientsModal = lazy(() => import('./GroupRemoveClientsModal'));
 
 const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } } as const;
 
@@ -77,7 +89,7 @@ export default function GroupsPage() {
   useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
   const queryClient = useQueryClient();
 
-  const { clients, subSettings, bulkAdjust, bulkDelete } = useClients();
+  const { subSettings, bulkAdjust, bulkAddToGroup, bulkRemoveFromGroup, bulkDelete } = useClients();
 
   const groupsQuery = useQuery({
     queryKey: keys.clients.groups(),
@@ -124,9 +136,24 @@ export default function GroupsPage() {
 
   const [subLinksOpen, setSubLinksOpen] = useState(false);
   const [adjustOpen, setAdjustOpen] = useState(false);
+  const [addClientsOpen, setAddClientsOpen] = useState(false);
+  const [removeClientsOpen, setRemoveClientsOpen] = useState(false);
   const [groupEmails, setGroupEmails] = useState<string[]>([]);
   const [groupForAction, setGroupForAction] = useState<GroupSummary | null>(null);
 
+  const allClientsQuery = useQuery<ClientRecord[]>({
+    queryKey: keys.clients.all(),
+    queryFn: async () => {
+      const msg = await HttpUtil.get('/panel/api/clients/list', undefined, { silent: true });
+      if (!msg?.success) throw new Error(msg?.msg || 'Failed to load clients');
+      const validated = parseMsg(msg, ClientRecordListSchema, 'clients/list');
+      return validated.obj ?? [];
+    },
+    enabled: addClientsOpen || removeClientsOpen || subLinksOpen,
+    staleTime: 30_000,
+  });
+  const allClients = allClientsQuery.data ?? [];
+
   const totalGroups = groups.length;
   const totalClients = useMemo(
     () => groups.reduce((acc, g) => acc + (g.clientCount || 0), 0),
@@ -228,6 +255,20 @@ export default function GroupsPage() {
     setAdjustOpen(true);
   }
 
+  function openAddClientsFor(g: GroupSummary) {
+    setGroupForAction(g);
+    setAddClientsOpen(true);
+  }
+
+  function openRemoveClientsFor(g: GroupSummary) {
+    if (!g.clientCount) {
+      messageApi.info(t('pages.groups.emptyForAction'));
+      return;
+    }
+    setGroupForAction(g);
+    setRemoveClientsOpen(true);
+  }
+
   function onDeleteClients(g: GroupSummary) {
     if (!g.clientCount) {
       messageApi.info(t('pages.groups.emptyForAction'));
@@ -306,13 +347,27 @@ export default function GroupsPage() {
         disabled: !row.clientCount,
         onClick: () => onResetTraffic(row),
       },
-      { type: 'divider' },
+      {
+        key: 'addClients',
+        icon: <UsergroupAddOutlined />,
+        label: t('pages.groups.addToGroup'),
+        onClick: () => openAddClientsFor(row),
+      },
       {
         key: 'rename',
         icon: <EditOutlined />,
         label: t('pages.groups.rename'),
         onClick: () => openRename(row),
       },
+      { type: 'divider' },
+      {
+        key: 'removeClients',
+        icon: <UsergroupDeleteOutlined />,
+        label: t('pages.groups.removeFromGroup'),
+        danger: true,
+        disabled: !row.clientCount,
+        onClick: () => openRemoveClientsFor(row),
+      },
       {
         key: 'deleteClients',
         icon: <DeleteOutlined />,
@@ -377,7 +432,7 @@ export default function GroupsPage() {
         <AppSidebar />
         <Layout className="content-shell">
           <Layout.Content id="content-layout" className="content-area">
-            <Spin spinning={!fetched} delay={200} description="Loading…" size="large">
+            <Spin spinning={!fetched} delay={200} description={t('loading')} size="large">
               {!fetched ? (
                 <div className="loading-spacer" />
               ) : (
@@ -495,7 +550,7 @@ export default function GroupsPage() {
           <SubLinksModal
             open={subLinksOpen}
             emails={groupEmails}
-            clients={clients}
+            clients={allClients}
             subSettings={subSettings}
             onOpenChange={setSubLinksOpen}
           />
@@ -522,6 +577,38 @@ export default function GroupsPage() {
             }}
           />
         </LazyMount>
+
+        <LazyMount when={addClientsOpen}>
+          <GroupAddClientsModal
+            open={addClientsOpen}
+            groupName={groupForAction?.name ?? null}
+            candidates={allClients.filter((c) => c.group !== groupForAction?.name)}
+            onClose={() => setAddClientsOpen(false)}
+            onSubmit={async (emails) => {
+              const msg = await bulkAddToGroup(emails, groupForAction?.name ?? '');
+              if (msg?.success) {
+                return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
+              }
+              return null;
+            }}
+          />
+        </LazyMount>
+
+        <LazyMount when={removeClientsOpen}>
+          <GroupRemoveClientsModal
+            open={removeClientsOpen}
+            groupName={groupForAction?.name ?? null}
+            members={allClients.filter((c) => c.group === groupForAction?.name)}
+            onClose={() => setRemoveClientsOpen(false)}
+            onSubmit={async (emails) => {
+              const msg = await bulkRemoveFromGroup(emails);
+              if (msg?.success) {
+                return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
+              }
+              return null;
+            }}
+          />
+        </LazyMount>
       </Layout>
     </ConfigProvider>
   );

+ 9 - 9
frontend/src/pages/inbounds/AssignClientsGroupModal.tsx → frontend/src/pages/inbounds/AddClientsToGroupModal.tsx

@@ -3,13 +3,13 @@ import { lazy, useEffect, useMemo, useState } from 'react';
 import { HttpUtil } from '@/utils';
 import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
 
-const BulkAssignGroupModal = lazy(() => import('@/pages/clients/BulkAssignGroupModal'));
+const BulkAddToGroupModal = lazy(() => import('@/pages/clients/BulkAddToGroupModal'));
 
-interface AssignClientsGroupModalProps {
+interface AddClientsToGroupModalProps {
   open: boolean;
   source: DBInbound | null;
   onClose: () => void;
-  onAssigned?: () => void;
+  onAdded?: () => void;
 }
 
 function readClientEmails(settings: unknown): string[] {
@@ -18,12 +18,12 @@ function readClientEmails(settings: unknown): string[] {
   return clients.map((c) => (c?.email || '').trim()).filter(Boolean);
 }
 
-export default function AssignClientsGroupModal({
+export default function AddClientsToGroupModal({
   open,
   source,
   onClose,
-  onAssigned,
-}: AssignClientsGroupModalProps) {
+  onAdded,
+}: AddClientsToGroupModalProps) {
   const [groups, setGroups] = useState<string[]>([]);
 
   const emails = useMemo(() => (source ? readClientEmails(source.settings) : []), [source]);
@@ -41,19 +41,19 @@ export default function AssignClientsGroupModal({
   }, [open]);
 
   return (
-    <BulkAssignGroupModal
+    <BulkAddToGroupModal
       open={open}
       count={emails.length}
       groups={groups}
       onOpenChange={(o) => { if (!o) onClose(); }}
       onSubmit={async (group) => {
         const msg = await HttpUtil.post(
-          '/panel/api/clients/bulkAssignGroup',
+          '/panel/api/clients/groups/bulkAdd',
           { emails, group },
           { headers: { 'Content-Type': 'application/json' } },
         );
         if (!msg?.success) return null;
-        onAssigned?.();
+        onAdded?.();
         return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
       }}
     />

+ 112 - 12
frontend/src/pages/inbounds/AttachClientsModal.tsx

@@ -1,6 +1,7 @@
 import { useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Alert, Modal, Select, Typography, message } from 'antd';
+import { Alert, Input, Modal, Select, Space, Table, Tag, Typography, message } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
 
 import { HttpUtil } from '@/utils';
 import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
@@ -20,10 +21,24 @@ interface BulkAttachResult {
   errors?: string[];
 }
 
-function readClientEmails(settings: unknown): string[] {
-  const parsed = coerceInboundJsonField(settings) as { clients?: Array<{ email?: string }> };
+interface ClientRow {
+  email: string;
+  comment: string;
+  enable: boolean;
+}
+
+function readClientRows(settings: unknown): ClientRow[] {
+  const parsed = coerceInboundJsonField(settings) as {
+    clients?: Array<{ email?: string; comment?: string; enable?: boolean }>;
+  };
   const clients = Array.isArray(parsed?.clients) ? parsed.clients : [];
-  return clients.map((c) => (c?.email || '').trim()).filter(Boolean);
+  return clients
+    .map((c) => ({
+      email: (c?.email || '').trim(),
+      comment: (c?.comment || '').trim(),
+      enable: c?.enable !== false,
+    }))
+    .filter((r) => r.email);
 }
 
 export default function AttachClientsModal({
@@ -37,12 +52,18 @@ export default function AttachClientsModal({
   const [messageApi, messageContextHolder] = message.useMessage();
   const [targetIds, setTargetIds] = useState<number[]>([]);
   const [saving, setSaving] = useState(false);
+  const [clientRows, setClientRows] = useState<ClientRow[]>([]);
+  const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
+  const [search, setSearch] = useState('');
 
   useEffect(() => {
-    if (open) setTargetIds([]);
-  }, [open]);
-
-  const emails = useMemo(() => (source ? readClientEmails(source.settings) : []), [source]);
+    if (!open) return;
+    const rows = source ? readClientRows(source.settings) : [];
+    setClientRows(rows);
+    setSelectedEmails(rows.map((r) => r.email));
+    setTargetIds([]);
+    setSearch('');
+  }, [open, source]);
 
   const targetOptions = useMemo(() => {
     if (!source) return [];
@@ -51,11 +72,53 @@ export default function AttachClientsModal({
       .map((ib) => ({ value: ib.id, label: `${ib.remark} (${ib.protocol}@${ib.port})` }));
   }, [dbInbounds, source]);
 
+  const filteredRows = useMemo(() => {
+    const q = search.trim().toLowerCase();
+    if (!q) return clientRows;
+    return clientRows.filter(
+      (r) => r.email.toLowerCase().includes(q) || r.comment.toLowerCase().includes(q),
+    );
+  }, [clientRows, search]);
+
+  const columns: ColumnsType<ClientRow> = useMemo(
+    () => [
+      {
+        title: t('pages.inbounds.email'),
+        dataIndex: 'email',
+        key: 'email',
+        ellipsis: true,
+      },
+      {
+        title: t('comment'),
+        dataIndex: 'comment',
+        key: 'comment',
+        ellipsis: true,
+      },
+      {
+        title: t('enable'),
+        dataIndex: 'enable',
+        key: 'enable',
+        width: 90,
+        render: (enabled: boolean) =>
+          enabled ? (
+            <Tag color="success">{t('enable')}</Tag>
+          ) : (
+            <Tag>{t('pages.inbounds.attachClientsStatusDisabled')}</Tag>
+          ),
+      },
+    ],
+    [t],
+  );
+
   async function submit() {
-    if (!source || targetIds.length === 0 || emails.length === 0) return;
+    if (!source || targetIds.length === 0 || selectedEmails.length === 0) return;
     setSaving(true);
     try {
-      const msg = await HttpUtil.post('/panel/api/clients/bulkAttach', { emails, inboundIds: targetIds }, { headers: { 'Content-Type': 'application/json' } });
+      const msg = await HttpUtil.post(
+        '/panel/api/clients/bulkAttach',
+        { emails: selectedEmails, inboundIds: targetIds },
+        { headers: { 'Content-Type': 'application/json' } },
+      );
       if (!msg?.success) {
         messageApi.error(msg?.msg || t('somethingWentWrong'));
         return;
@@ -81,15 +144,52 @@ export default function AttachClientsModal({
       open={open}
       onCancel={onClose}
       onOk={submit}
-      okButtonProps={{ disabled: targetIds.length === 0 || emails.length === 0, loading: saving }}
+      okButtonProps={{
+        disabled: targetIds.length === 0 || selectedEmails.length === 0,
+        loading: saving,
+      }}
       okText={t('pages.inbounds.attachClients')}
       cancelText={t('cancel')}
       title={t('pages.inbounds.attachClientsTitle', { remark: source?.remark ?? '' })}
+      width={680}
     >
       {messageContextHolder}
       <Typography.Paragraph type="secondary">
-        {t('pages.inbounds.attachClientsDesc', { count: emails.length })}
+        {t('pages.inbounds.attachClientsDesc', { count: clientRows.length })}
       </Typography.Paragraph>
+
+      <Space direction="vertical" size="small" style={{ width: '100%', marginBottom: 12 }}>
+        <Typography.Text strong>{t('pages.inbounds.attachClientsSelectLabel')}</Typography.Text>
+        <Space style={{ width: '100%', justifyContent: 'space-between' }} wrap>
+          <Input.Search
+            allowClear
+            value={search}
+            onChange={(e) => setSearch(e.target.value)}
+            placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
+            style={{ maxWidth: 320 }}
+          />
+          <Typography.Text type="secondary">
+            {t('pages.inbounds.attachClientsSelectedCount', {
+              selected: selectedEmails.length,
+              total: clientRows.length,
+            })}
+          </Typography.Text>
+        </Space>
+        <Table<ClientRow>
+          size="small"
+          rowKey="email"
+          columns={columns}
+          dataSource={filteredRows}
+          pagination={false}
+          scroll={{ y: 280 }}
+          rowSelection={{
+            selectedRowKeys: selectedEmails,
+            onChange: (keys) => setSelectedEmails(keys as string[]),
+            preserveSelectedRowKeys: true,
+          }}
+        />
+      </Space>
+
       {targetOptions.length === 0 ? (
         <Alert type="info" showIcon message={t('pages.inbounds.attachClientsNoTargets')} />
       ) : (

+ 183 - 0
frontend/src/pages/inbounds/DetachClientsModal.tsx

@@ -0,0 +1,183 @@
+import { useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Input, Modal, Space, Table, Tag, Typography, message } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+
+import { HttpUtil } from '@/utils';
+import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
+
+interface DetachClientsModalProps {
+  open: boolean;
+  source: DBInbound | null;
+  onClose: () => void;
+  onDetached?: () => void;
+}
+
+interface BulkDetachResult {
+  detached?: string[];
+  skipped?: string[];
+  errors?: string[];
+}
+
+interface ClientRow {
+  email: string;
+  comment: string;
+  enable: boolean;
+}
+
+function readClientRows(settings: unknown): ClientRow[] {
+  const parsed = coerceInboundJsonField(settings) as {
+    clients?: Array<{ email?: string; comment?: string; enable?: boolean }>;
+  };
+  const clients = Array.isArray(parsed?.clients) ? parsed.clients : [];
+  return clients
+    .map((c) => ({
+      email: (c?.email || '').trim(),
+      comment: (c?.comment || '').trim(),
+      enable: c?.enable !== false,
+    }))
+    .filter((r) => r.email);
+}
+
+export default function DetachClientsModal({
+  open,
+  source,
+  onClose,
+  onDetached,
+}: DetachClientsModalProps) {
+  const { t } = useTranslation();
+  const [messageApi, messageContextHolder] = message.useMessage();
+  const [saving, setSaving] = useState(false);
+  const [clientRows, setClientRows] = useState<ClientRow[]>([]);
+  const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
+  const [search, setSearch] = useState('');
+
+  useEffect(() => {
+    if (!open) return;
+    const rows = source ? readClientRows(source.settings) : [];
+    setClientRows(rows);
+    setSelectedEmails([]);
+    setSearch('');
+  }, [open, source]);
+
+  const filteredRows = useMemo(() => {
+    const q = search.trim().toLowerCase();
+    if (!q) return clientRows;
+    return clientRows.filter(
+      (r) => r.email.toLowerCase().includes(q) || r.comment.toLowerCase().includes(q),
+    );
+  }, [clientRows, search]);
+
+  const columns: ColumnsType<ClientRow> = useMemo(
+    () => [
+      {
+        title: t('pages.inbounds.email'),
+        dataIndex: 'email',
+        key: 'email',
+        ellipsis: true,
+      },
+      {
+        title: t('comment'),
+        dataIndex: 'comment',
+        key: 'comment',
+        ellipsis: true,
+      },
+      {
+        title: t('enable'),
+        dataIndex: 'enable',
+        key: 'enable',
+        width: 90,
+        render: (enabled: boolean) =>
+          enabled ? (
+            <Tag color="success">{t('enable')}</Tag>
+          ) : (
+            <Tag>{t('pages.inbounds.attachClientsStatusDisabled')}</Tag>
+          ),
+      },
+    ],
+    [t],
+  );
+
+  async function submit() {
+    if (!source || selectedEmails.length === 0) return;
+    setSaving(true);
+    try {
+      const msg = await HttpUtil.post(
+        '/panel/api/clients/bulkDetach',
+        { emails: selectedEmails, inboundIds: [source.id] },
+        { headers: { 'Content-Type': 'application/json' } },
+      );
+      if (!msg?.success) {
+        messageApi.error(msg?.msg || t('somethingWentWrong'));
+        return;
+      }
+      const result = (msg.obj || {}) as BulkDetachResult;
+      const detached = result.detached?.length ?? 0;
+      const skipped = result.skipped?.length ?? 0;
+      const errors = result.errors?.length ?? 0;
+      if (errors > 0) {
+        messageApi.warning(t('pages.inbounds.detachClientsResultMixed', { detached, skipped, errors }));
+      } else {
+        messageApi.success(t('pages.inbounds.detachClientsResult', { detached, skipped }));
+      }
+      onDetached?.();
+      onClose();
+    } finally {
+      setSaving(false);
+    }
+  }
+
+  return (
+    <Modal
+      open={open}
+      onCancel={onClose}
+      onOk={submit}
+      okButtonProps={{
+        danger: true,
+        disabled: selectedEmails.length === 0,
+        loading: saving,
+      }}
+      okText={t('pages.inbounds.detachClients')}
+      cancelText={t('cancel')}
+      title={t('pages.inbounds.detachClientsTitle', { remark: source?.remark ?? '' })}
+      width={680}
+    >
+      {messageContextHolder}
+      <Typography.Paragraph type="secondary">
+        {t('pages.inbounds.detachClientsDesc', { count: clientRows.length })}
+      </Typography.Paragraph>
+
+      <Space direction="vertical" size="small" style={{ width: '100%' }}>
+        <Typography.Text strong>{t('pages.inbounds.detachClientsSelectLabel')}</Typography.Text>
+        <Space style={{ width: '100%', justifyContent: 'space-between' }} wrap>
+          <Input.Search
+            allowClear
+            value={search}
+            onChange={(e) => setSearch(e.target.value)}
+            placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
+            style={{ maxWidth: 320 }}
+          />
+          <Typography.Text type="secondary">
+            {t('pages.inbounds.attachClientsSelectedCount', {
+              selected: selectedEmails.length,
+              total: clientRows.length,
+            })}
+          </Typography.Text>
+        </Space>
+        <Table<ClientRow>
+          size="small"
+          rowKey="email"
+          columns={columns}
+          dataSource={filteredRows}
+          pagination={false}
+          scroll={{ y: 280 }}
+          rowSelection={{
+            selectedRowKeys: selectedEmails,
+            onChange: (keys) => setSelectedEmails(keys as string[]),
+            preserveSelectedRowKeys: true,
+          }}
+        />
+      </Space>
+    </Modal>
+  );
+}

文件差異過大導致無法顯示
+ 178 - 121
frontend/src/pages/inbounds/InboundFormModal.tsx


+ 22 - 22
frontend/src/pages/inbounds/InboundInfoModal.tsx

@@ -506,7 +506,7 @@ export default function InboundInfoModal({
           )}
           {inbound.isVlessTlsFlow && (
             <tr>
-              <td>Flow</td>
+              <td>{t('pages.clients.flow')}</td>
               <td>
                 {clientSettings?.flow ? <Tag>{clientSettings.flow}</Tag> : <Tag color="orange">{t('none')}</Tag>}
               </td>
@@ -729,18 +729,18 @@ export default function InboundInfoModal({
             )}
             {inbound.isXHTTP && (
               <div className="info-row">
-                <dt>Mode</dt>
+                <dt>{t('pages.inbounds.info.mode')}</dt>
                 <dd><Tag>{inbound.stream?.xhttp?.mode}</Tag></dd>
               </div>
             )}
             {inbound.isGrpc && (
               <>
                 <div className="info-row">
-                  <dt>grpc serviceName</dt>
+                  <dt>{t('pages.inbounds.info.grpcServiceName')}</dt>
                   <dd><Tag className="value-tag">{inbound.serviceName}</Tag></dd>
                 </div>
                 <div className="info-row">
-                  <dt>grpc multiMode</dt>
+                  <dt>{t('pages.inbounds.info.grpcMultiMode')}</dt>
                   <dd><Tag>{String(inbound.stream?.grpc?.multiMode)}</Tag></dd>
                 </div>
               </>
@@ -805,16 +805,16 @@ export default function InboundInfoModal({
       {inbound.protocol === Protocols.TUN && inbound.settings && (
         <dl className="info-list info-list-block">
           <div className="info-row">
-            <dt>Interface name</dt>
+            <dt>{t('pages.inbounds.info.interfaceName')}</dt>
             <dd><Tag color="green" className="value-tag">{inbound.settings.name as string}</Tag></dd>
           </div>
           <div className="info-row">
-            <dt>MTU</dt>
+            <dt>{t('pages.inbounds.info.mtu')}</dt>
             <dd><Tag color="green">{inbound.settings.mtu as number}</Tag></dd>
           </div>
           {Array.isArray(inbound.settings.gateway) && (inbound.settings.gateway as string[]).length > 0 && (
             <div className="info-row">
-              <dt>Gateway</dt>
+              <dt>{t('pages.inbounds.info.gateway')}</dt>
               <dd>
                 {(inbound.settings.gateway as string[]).map((ip, j) => (
                   <Tag key={`tun-gw-${j}`} color="green" className="value-tag">{ip}</Tag>
@@ -824,7 +824,7 @@ export default function InboundInfoModal({
           )}
           {Array.isArray(inbound.settings.dns) && (inbound.settings.dns as string[]).length > 0 && (
             <div className="info-row">
-              <dt>DNS</dt>
+              <dt>{t('pages.inbounds.info.dns')}</dt>
               <dd>
                 {(inbound.settings.dns as string[]).map((ip, j) => (
                   <Tag key={`tun-dns-${j}`} color="green">{ip}</Tag>
@@ -833,12 +833,12 @@ export default function InboundInfoModal({
             </div>
           )}
           <div className="info-row">
-            <dt>Outbounds interface</dt>
+            <dt>{t('pages.inbounds.info.outboundsInterface')}</dt>
             <dd><Tag color="green">{(inbound.settings.autoOutboundsInterface as string) || 'auto'}</Tag></dd>
           </div>
           {Array.isArray(inbound.settings.autoSystemRoutingTable) && (inbound.settings.autoSystemRoutingTable as string[]).length > 0 && (
             <div className="info-row">
-              <dt>Auto system routes</dt>
+              <dt>{t('pages.inbounds.info.autoSystemRoutes')}</dt>
               <dd>
                 {(inbound.settings.autoSystemRoutingTable as string[]).map((cidr, j) => (
                   <Tag key={`tun-rt-${j}`} color="green">{cidr}</Tag>
@@ -864,7 +864,7 @@ export default function InboundInfoModal({
             <dd><Tag color="green">{inbound.settings.allowedNetwork as string}</Tag></dd>
           </div>
           <div className="info-row">
-            <dt>FollowRedirect</dt>
+            <dt>{t('pages.inbounds.info.followRedirect')}</dt>
             <dd>
               <Tag color={inbound.settings.followRedirect ? 'green' : 'red'}>
                 {inbound.settings.followRedirect ? t('enabled') : t('disabled')}
@@ -877,7 +877,7 @@ export default function InboundInfoModal({
       {dbInbound.isMixed && inbound.settings && (
         <dl className="info-list info-list-block">
           <div className="info-row">
-            <dt>Auth</dt>
+            <dt>{t('pages.inbounds.info.auth')}</dt>
             <dd>
               <Tag color={inbound.settings.auth === 'password' ? 'green' : 'orange'}>
                 {inbound.settings.auth as string}
@@ -969,19 +969,19 @@ export default function InboundInfoModal({
         <>
           <dl className="info-list info-list-block">
             <div className="info-row">
-              <dt>Secret key</dt>
+              <dt>{t('pages.xray.wireguard.secretKey')}</dt>
               <dd><Tag className="value-tag">{inbound.settings.secretKey as string}</Tag></dd>
             </div>
             <div className="info-row">
-              <dt>Public key</dt>
+              <dt>{t('pages.xray.wireguard.publicKey')}</dt>
               <dd><Tag className="value-tag">{inbound.settings.pubKey as string}</Tag></dd>
             </div>
             <div className="info-row">
-              <dt>MTU</dt>
+              <dt>{t('pages.inbounds.info.mtu')}</dt>
               <dd><Tag>{inbound.settings.mtu as number}</Tag></dd>
             </div>
             <div className="info-row">
-              <dt>No-kernel TUN</dt>
+              <dt>{t('pages.inbounds.info.noKernelTun')}</dt>
               <dd>
                 <Tag color={inbound.settings.noKernelTun ? 'green' : 'default'}>
                   {String(inbound.settings.noKernelTun)}
@@ -991,14 +991,14 @@ export default function InboundInfoModal({
           </dl>
           {Array.isArray(inbound.settings.peers) && (inbound.settings.peers as { privateKey: string; publicKey: string; psk: string; allowedIPs?: string[]; keepAlive?: number }[]).map((peer, idx) => (
             <Fragment key={idx}>
-              <Divider>Peer {idx + 1}</Divider>
+              <Divider>{t('pages.inbounds.info.peerNumber', { n: idx + 1 })}</Divider>
               <dl className="info-list info-list-block">
                 <div className="info-row">
-                  <dt>Secret key</dt>
+                  <dt>{t('pages.xray.wireguard.secretKey')}</dt>
                   <dd><Tag className="value-tag">{peer.privateKey}</Tag></dd>
                 </div>
                 <div className="info-row">
-                  <dt>Public key</dt>
+                  <dt>{t('pages.xray.wireguard.publicKey')}</dt>
                   <dd><Tag className="value-tag">{peer.publicKey}</Tag></dd>
                 </div>
                 <div className="info-row">
@@ -1006,7 +1006,7 @@ export default function InboundInfoModal({
                   <dd><Tag className="value-tag">{peer.psk}</Tag></dd>
                 </div>
                 <div className="info-row">
-                  <dt>Allowed IPs</dt>
+                  <dt>{t('pages.xray.wireguard.allowedIPs')}</dt>
                   <dd>
                     {(peer.allowedIPs || []).map((ip, j) => (
                       <Tag key={`wg-ip-${idx}-${j}`} className="value-tag">{ip}</Tag>
@@ -1014,14 +1014,14 @@ export default function InboundInfoModal({
                   </dd>
                 </div>
                 <div className="info-row">
-                  <dt>Keep alive</dt>
+                  <dt>{t('pages.inbounds.info.keepAlive')}</dt>
                   <dd><Tag>{peer.keepAlive}</Tag></dd>
                 </div>
               </dl>
               {wireguardConfigs[idx] && (
                 <div className="link-panel">
                   <div className="link-panel-header">
-                    <Tag color="green">Peer {idx + 1} config</Tag>
+                    <Tag color="green">{t('pages.inbounds.info.peerNumberConfig', { n: idx + 1 })}</Tag>
                     <Tooltip title={t('copy')}>
                       <Button size="small" icon={<CopyOutlined />} onClick={() => copyText(wireguardConfigs[idx], t)} />
                     </Tooltip>

+ 5 - 1
frontend/src/pages/inbounds/InboundList.tsx

@@ -262,8 +262,12 @@ function buildRowActionsMenu({ record, subEnable, t, isMobile, hasClients }: { r
   items.push({ key: 'clone', icon: <BlockOutlined />, label: t('pages.inbounds.clone') });
   if (isInboundMultiUser(record) && hasClients) {
     items.push({ key: 'attachClients', icon: <UsergroupAddOutlined />, label: t('pages.inbounds.attachClients') });
-    items.push({ key: 'assignGroup', icon: <TagsOutlined />, label: t('pages.inbounds.assignClientsGroup') });
+    items.push({ key: 'detachClients', icon: <UsergroupDeleteOutlined />, label: t('pages.inbounds.detachClients') });
+    items.push({ key: 'addToGroup', icon: <TagsOutlined />, label: t('pages.inbounds.addClientsToGroup') });
+    items.push({ type: 'divider' });
     items.push({ key: 'delAllClients', icon: <UsergroupDeleteOutlined />, danger: true, label: t('pages.inbounds.delAllClients') });
+  } else {
+    items.push({ type: 'divider' });
   }
   items.push({ key: 'delete', icon: <DeleteOutlined />, danger: true, label: t('delete') });
   return items;

+ 31 - 15
frontend/src/pages/inbounds/InboundsPage.tsx

@@ -39,7 +39,8 @@ const InboundFormModal = lazy(() => import('./InboundFormModal'));
 const InboundInfoModal = lazy(() => import('./InboundInfoModal'));
 const QrCodeModal = lazy(() => import('./QrCodeModal'));
 const AttachClientsModal = lazy(() => import('./AttachClientsModal'));
-const AssignClientsGroupModal = lazy(() => import('./AssignClientsGroupModal'));
+const DetachClientsModal = lazy(() => import('./DetachClientsModal'));
+const AddClientsToGroupModal = lazy(() => import('./AddClientsToGroupModal'));
 
 type RowAction =
   | 'edit'
@@ -52,7 +53,8 @@ type RowAction =
   | 'resetTraffic'
   | 'delAllClients'
   | 'attachClients'
-  | 'assignGroup'
+  | 'detachClients'
+  | 'addToGroup'
   | 'clone';
 
 type GeneralAction = 'import' | 'export' | 'subs' | 'resetInbounds';
@@ -127,6 +129,8 @@ export default function InboundsPage() {
 
   const [attachOpen, setAttachOpen] = useState(false);
   const [attachSource, setAttachSource] = useState<DBInbound | null>(null);
+  const [detachOpen, setDetachOpen] = useState(false);
+  const [detachSource, setDetachSource] = useState<DBInbound | null>(null);
 
   const [groupOpen, setGroupOpen] = useState(false);
   const [groupSource, setGroupSource] = useState<DBInbound | null>(null);
@@ -167,7 +171,7 @@ export default function InboundsPage() {
     confirm: (value: string) => Promise<boolean | void> | boolean | void;
   }) => {
     setPromptTitle(opts.title);
-    setPromptOkText(opts.okText || 'OK');
+    setPromptOkText(opts.okText || t('confirm'));
     setPromptType(opts.type || 'textarea');
     setPromptInitial(opts.value || '');
     setPromptHandler(() => opts.confirm);
@@ -290,7 +294,7 @@ export default function InboundsPage() {
         fallbackHostname: window.location.hostname,
       }));
     }
-    openText({ title: t('pages.inbounds.exportAllLinksTitle'), content: out.join('\r\n'), fileName: 'All-Inbounds' });
+    openText({ title: t('pages.inbounds.exportAllLinksTitle'), content: out.join('\r\n'), fileName: t('pages.inbounds.exportAllLinksFileName') });
   }, [dbInbounds, hydrateInbound, checkFallback, remarkModel, hostOverrideFor, openText, t]);
 
   const exportAllSubs = useCallback(async () => {
@@ -307,13 +311,13 @@ export default function InboundsPage() {
         }
       }
     }
-    openText({ title: t('pages.inbounds.exportAllSubsTitle'), content: [...new Set(out)].join('\r\n'), fileName: 'All-Inbounds-Subs' });
+    openText({ title: t('pages.inbounds.exportAllSubsTitle'), content: [...new Set(out)].join('\r\n'), fileName: t('pages.inbounds.exportAllSubsFileName') });
   }, [dbInbounds, hydrateInbound, subSettings, openText, t]);
 
   const importInbound = useCallback(() => {
     openPrompt({
-      title: 'Import inbound',
-      okText: 'Import',
+      title: t('pages.inbounds.importInbound'),
+      okText: t('pages.inbounds.import'),
       type: 'textarea',
       value: '',
       confirm: async (value) => {
@@ -430,9 +434,9 @@ export default function InboundsPage() {
       case 'subs': exportAllSubs(); break;
       case 'resetInbounds':
         modal.confirm({
-          title: 'Reset all inbound traffic?',
-          okText: 'Reset',
-          cancelText: 'Cancel',
+          title: t('pages.inbounds.resetAllTrafficTitle'),
+          okText: t('reset'),
+          cancelText: t('cancel'),
           onOk: async () => {
             const msg = await HttpUtil.post('/panel/api/inbounds/resetAllTraffics');
             if (msg?.success) await refresh();
@@ -448,7 +452,7 @@ export default function InboundsPage() {
     // Actions that touch per-client secrets (uuid, password, flow, ...) need
     // the full payload that the slim list view does not ship. Hydrate first
     // and then operate on the rehydrated record.
-    const hydratingKeys: RowAction[] = ['edit', 'showInfo', 'qrcode', 'export', 'subs', 'clipboard', 'clone', 'attachClients', 'assignGroup'];
+    const hydratingKeys: RowAction[] = ['edit', 'showInfo', 'qrcode', 'export', 'subs', 'clipboard', 'clone', 'attachClients', 'addToGroup'];
     let target = dbInbound;
     if (hydratingKeys.includes(key)) {
       const hydrated = await hydrateInbound(dbInbound.id);
@@ -489,7 +493,11 @@ export default function InboundsPage() {
         setAttachSource(target);
         setAttachOpen(true);
         break;
-      case 'assignGroup':
+      case 'detachClients':
+        setDetachSource(target);
+        setDetachOpen(true);
+        break;
+      case 'addToGroup':
         setGroupSource(target);
         setGroupOpen(true);
         break;
@@ -510,7 +518,7 @@ export default function InboundsPage() {
 
         <Layout className="content-shell">
           <Layout.Content id="content-layout" className="content-area">
-            <Spin spinning={!fetched} delay={200} description="Loading…" size="large">
+            <Spin spinning={!fetched} delay={200} description={t('loading')} size="large">
               {!fetched ? (
                 <div className="loading-spacer" />
               ) : (
@@ -614,11 +622,19 @@ export default function InboundsPage() {
             dbInbounds={dbInbounds}
           />
         </LazyMount>
+        <LazyMount when={detachOpen}>
+          <DetachClientsModal
+            open={detachOpen}
+            onClose={() => setDetachOpen(false)}
+            onDetached={refresh}
+            source={detachSource}
+          />
+        </LazyMount>
         <LazyMount when={groupOpen}>
-          <AssignClientsGroupModal
+          <AddClientsToGroupModal
             open={groupOpen}
             onClose={() => setGroupOpen(false)}
-            onAssigned={refresh}
+            onAdded={refresh}
             source={groupSource}
           />
         </LazyMount>

+ 1 - 1
frontend/src/pages/nodes/NodesPage.tsx

@@ -97,7 +97,7 @@ export default function NodesPage() {
 
         <Layout className="content-shell">
           <Layout.Content id="content-layout" className="content-area">
-            <Spin spinning={!fetched} delay={200} description="Loading…" size="large">
+            <Spin spinning={!fetched} delay={200} description={t('loading')} size="large">
               {!fetched ? (
                 <div className="loading-spacer" />
               ) : (

+ 24 - 24
frontend/src/pages/settings/GeneralTab.tsx

@@ -160,8 +160,8 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
 
             <SettingListItem
               paddings="small"
-              title="Trusted proxy CIDRs"
-              description="Comma-separated IPs/CIDRs allowed to set forwarded host, proto, and client IP headers."
+              title={t('pages.settings.trustedProxyCidrs')}
+              description={t('pages.settings.trustedProxyCidrsDesc')}
             >
               <Input
                 value={allSetting.trustedProxyCIDRs}
@@ -271,58 +271,58 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
         label: 'LDAP',
         children: (
           <>
-            <SettingListItem paddings="small" title="Enable LDAP sync">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.enable')}>
               <Switch checked={allSetting.ldapEnable} onChange={(v) => updateSetting({ ldapEnable: v })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="LDAP host">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.host')}>
               <Input value={allSetting.ldapHost} onChange={(e) => updateSetting({ ldapHost: e.target.value })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="LDAP port">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.port')}>
               <InputNumber value={allSetting.ldapPort} min={1} max={65535} style={{ width: '100%' }}
                 onChange={(v) => updateSetting({ ldapPort: Number(v) || 0 })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Use TLS (LDAPS)">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.useTls')}>
               <Switch checked={allSetting.ldapUseTLS} onChange={(v) => updateSetting({ ldapUseTLS: v })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Bind DN">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.bindDn')}>
               <Input value={allSetting.ldapBindDN} onChange={(e) => updateSetting({ ldapBindDN: e.target.value })} />
             </SettingListItem>
             <SettingListItem
               paddings="small"
               title={t('password')}
-              description={allSetting.hasLdapPassword ? 'Configured; leave blank to keep current password.' : 'Not configured.'}
+              description={allSetting.hasLdapPassword ? t('pages.settings.ldap.passwordConfigured') : t('pages.settings.ldap.passwordUnconfigured')}
             >
               <Input.Password
                 value={allSetting.ldapPassword}
-                placeholder={allSetting.hasLdapPassword ? 'Configured - enter a new value to replace' : ''}
+                placeholder={allSetting.hasLdapPassword ? t('pages.settings.ldap.passwordPlaceholder') : ''}
                 onChange={(e) => updateSetting({ ldapPassword: e.target.value })}
               />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Base DN">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.baseDn')}>
               <Input value={allSetting.ldapBaseDN} onChange={(e) => updateSetting({ ldapBaseDN: e.target.value })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="User filter">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.userFilter')}>
               <Input value={allSetting.ldapUserFilter} onChange={(e) => updateSetting({ ldapUserFilter: e.target.value })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="User attribute (username/email)">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.userAttr')}>
               <Input value={allSetting.ldapUserAttr} onChange={(e) => updateSetting({ ldapUserAttr: e.target.value })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="VLESS flag attribute">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.vlessField')}>
               <Input value={allSetting.ldapVlessField} onChange={(e) => updateSetting({ ldapVlessField: e.target.value })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Generic flag attribute (optional)" description="If set, overrides VLESS flag — e.g. shadowInactive.">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.flagField')} description={t('pages.settings.ldap.flagFieldDesc')}>
               <Input value={allSetting.ldapFlagField} onChange={(e) => updateSetting({ ldapFlagField: e.target.value })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Truthy values" description="Comma-separated; default: true,1,yes,on">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.truthyValues')} description={t('pages.settings.ldap.truthyValuesDesc')}>
               <Input value={allSetting.ldapTruthyValues} onChange={(e) => updateSetting({ ldapTruthyValues: e.target.value })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Invert flag" description="Enable when the attribute means disabled (e.g. shadowInactive).">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.invertFlag')} description={t('pages.settings.ldap.invertFlagDesc')}>
               <Switch checked={allSetting.ldapInvertFlag} onChange={(v) => updateSetting({ ldapInvertFlag: v })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Sync schedule" description="Cron-like string, e.g. @every 1m">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.syncSchedule')} description={t('pages.settings.ldap.syncScheduleDesc')}>
               <Input value={allSetting.ldapSyncCron} onChange={(e) => updateSetting({ ldapSyncCron: e.target.value })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Inbound tags" description="Inbounds that LDAP sync may auto-create or auto-delete clients on.">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.inboundTags')} description={t('pages.settings.ldap.inboundTagsDesc')}>
               <>
                 <Select
                   mode="multiple"
@@ -332,25 +332,25 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
                   options={inboundOptions}
                 />
                 {inboundOptions.length === 0 && (
-                  <div className="ldap-no-inbounds">No inbounds found. Create one in Inbounds first.</div>
+                  <div className="ldap-no-inbounds">{t('pages.settings.ldap.noInbounds')}</div>
                 )}
               </>
             </SettingListItem>
-            <SettingListItem paddings="small" title="Auto create clients">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.autoCreate')}>
               <Switch checked={allSetting.ldapAutoCreate} onChange={(v) => updateSetting({ ldapAutoCreate: v })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Auto delete clients">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.autoDelete')}>
               <Switch checked={allSetting.ldapAutoDelete} onChange={(v) => updateSetting({ ldapAutoDelete: v })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Default total (GB)">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.defaultTotalGb')}>
               <InputNumber value={allSetting.ldapDefaultTotalGB} min={0} style={{ width: '100%' }}
                 onChange={(v) => updateSetting({ ldapDefaultTotalGB: Number(v) || 0 })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Default expiry (days)">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.defaultExpiryDays')}>
               <InputNumber value={allSetting.ldapDefaultExpiryDays} min={0} style={{ width: '100%' }}
                 onChange={(v) => updateSetting({ ldapDefaultExpiryDays: Number(v) || 0 })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Default IP limit">
+            <SettingListItem paddings="small" title={t('pages.settings.ldap.defaultIpLimit')}>
               <InputNumber value={allSetting.ldapDefaultLimitIP} min={0} style={{ width: '100%' }}
                 onChange={(v) => updateSetting({ ldapDefaultLimitIP: Number(v) || 0 })} />
             </SettingListItem>

+ 1 - 1
frontend/src/pages/settings/SettingsPage.tsx

@@ -284,7 +284,7 @@ export default function SettingsPage() {
 
         <Layout className="content-shell">
           <Layout.Content id="content-layout" className="content-area">
-            <Spin spinning={spinning || !fetched} delay={200} description="Loading…" size="large">
+            <Spin spinning={spinning || !fetched} delay={200} description={t('loading')} size="large">
               {!fetched ? (
                 <div className="loading-spacer" />
               ) : (

+ 15 - 15
frontend/src/pages/settings/SubscriptionFormatsTab.tsx

@@ -264,19 +264,19 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
                     label: t('pages.settings.fragmentSett'),
                     children: (
                       <>
-                        <SettingListItem paddings="small" title="Packets">
+                        <SettingListItem paddings="small" title={t('pages.settings.subFormats.packets')}>
                           <Input value={fragmentObj.packets} placeholder="1-1 | 1-3 | tlshello | …"
                             onChange={(e) => setFragmentField('packets', e.target.value)} />
                         </SettingListItem>
-                        <SettingListItem paddings="small" title="Length">
+                        <SettingListItem paddings="small" title={t('pages.settings.subFormats.length')}>
                           <Input value={fragmentObj.length} placeholder="100-200"
                             onChange={(e) => setFragmentField('length', e.target.value)} />
                         </SettingListItem>
-                        <SettingListItem paddings="small" title="Interval">
+                        <SettingListItem paddings="small" title={t('pages.settings.subFormats.interval')}>
                           <Input value={fragmentObj.interval} placeholder="10-20"
                             onChange={(e) => setFragmentField('interval', e.target.value)} />
                         </SettingListItem>
-                        <SettingListItem paddings="small" title="Max split">
+                        <SettingListItem paddings="small" title={t('pages.settings.subFormats.maxSplit')}>
                           <Input value={fragmentObj.maxSplit} placeholder="300-400"
                             onChange={(e) => setFragmentField('maxSplit', e.target.value)} />
                         </SettingListItem>
@@ -291,20 +291,20 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
       },
       {
         key: '3',
-        label: 'Noises',
+        label: t('pages.settings.subFormats.noises'),
         children: (
           <>
-            <SettingListItem paddings="small" title="Noises" description={t('pages.settings.noisesDesc')}>
+            <SettingListItem paddings="small" title={t('pages.settings.subFormats.noises')} description={t('pages.settings.noisesDesc')}>
               <Switch checked={noisesEnabled} onChange={setNoisesEnabled} />
             </SettingListItem>
             {noisesEnabled && (
               <div className="nested-block">
                 <Collapse items={noisesArray.map((noise, index) => ({
                   key: String(index),
-                  label: `Noise №${index + 1}`,
+                  label: t('pages.settings.subFormats.noiseItem', { n: index + 1 }),
                   children: (
                     <>
-                      <SettingListItem paddings="small" title="Type">
+                      <SettingListItem paddings="small" title={t('pages.settings.subFormats.type')}>
                         <Select
                           value={noise.type}
                           style={{ width: '100%' }}
@@ -312,15 +312,15 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
                           options={['rand', 'base64', 'str', 'hex'].map((p) => ({ value: p, label: p }))}
                         />
                       </SettingListItem>
-                      <SettingListItem paddings="small" title="Packet">
+                      <SettingListItem paddings="small" title={t('pages.settings.subFormats.packet')}>
                         <Input value={noise.packet} placeholder="5-10"
                           onChange={(e) => updateNoiseField(index, 'packet', e.target.value)} />
                       </SettingListItem>
-                      <SettingListItem paddings="small" title="Delay (ms)">
+                      <SettingListItem paddings="small" title={t('pages.settings.subFormats.delayMs')}>
                         <Input value={noise.delay} placeholder="10-20"
                           onChange={(e) => updateNoiseField(index, 'delay', e.target.value)} />
                       </SettingListItem>
-                      <SettingListItem paddings="small" title="Apply to">
+                      <SettingListItem paddings="small" title={t('pages.settings.subFormats.applyTo')}>
                         <Select
                           value={noise.applyTo}
                           style={{ width: '100%' }}
@@ -338,7 +338,7 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
                     </>
                   ),
                 }))} />
-                <Button type="primary" style={{ marginTop: 10 }} onClick={addNoise}>+ Noise</Button>
+                <Button type="primary" style={{ marginTop: 10 }} onClick={addNoise}>{t('pages.settings.subFormats.addNoise')}</Button>
               </div>
             )}
           </>
@@ -360,15 +360,15 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
                     label: t('pages.settings.muxSett'),
                     children: (
                       <>
-                        <SettingListItem paddings="small" title="Concurrency">
+                        <SettingListItem paddings="small" title={t('pages.settings.subFormats.concurrency')}>
                           <InputNumber value={muxObj.concurrency} min={-1} max={1024} style={{ width: '100%' }}
                             onChange={(v) => setMuxField('concurrency', Number(v) || 0)} />
                         </SettingListItem>
-                        <SettingListItem paddings="small" title="xudp concurrency">
+                        <SettingListItem paddings="small" title={t('pages.settings.subFormats.xudpConcurrency')}>
                           <InputNumber value={muxObj.xudpConcurrency} min={-1} max={1024} style={{ width: '100%' }}
                             onChange={(v) => setMuxField('xudpConcurrency', Number(v) || 0)} />
                         </SettingListItem>
-                        <SettingListItem paddings="small" title="xudp UDP 443">
+                        <SettingListItem paddings="small" title={t('pages.settings.subFormats.xudpUdp443')}>
                           <Select
                             value={muxObj.xudpProxyUDP443}
                             style={{ width: '100%' }}

+ 2 - 2
frontend/src/pages/settings/SubscriptionGeneralTab.tsx

@@ -33,10 +33,10 @@ export default function SubscriptionGeneralTab({ allSetting, updateSetting }: Su
             <SettingListItem paddings="small" title={t('pages.settings.subEnable')} description={t('pages.settings.subEnableDesc')}>
               <Switch checked={allSetting.subEnable} onChange={(v) => updateSetting({ subEnable: v })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="JSON subscription" description={t('pages.settings.subJsonEnable')}>
+            <SettingListItem paddings="small" title={t('pages.settings.subJsonEnableTitle')} description={t('pages.settings.subJsonEnable')}>
               <Switch checked={allSetting.subJsonEnable} onChange={(v) => updateSetting({ subJsonEnable: v })} />
             </SettingListItem>
-            <SettingListItem paddings="small" title="Clash / Mihomo subscription">
+            <SettingListItem paddings="small" title={t('pages.settings.subClashEnableTitle')}>
               <Switch checked={allSetting.subClashEnable} onChange={(v) => updateSetting({ subClashEnable: v })} />
             </SettingListItem>
             <SettingListItem paddings="small" title={t('pages.settings.subListen')} description={t('pages.settings.subListenDesc')}>

+ 12 - 12
frontend/src/pages/xray/BalancerFormModal.tsx

@@ -135,19 +135,19 @@ export default function BalancerFormModal({
     >
       <Form colon={false} labelCol={{ md: { span: 8 } }} wrapperCol={{ md: { span: 14 } }}>
         <Form.Item
-          label="Tag"
+          label={t('pages.xray.balancer.tag')}
           required
           validateStatus={issues.tag ? 'error' : duplicateTag ? 'warning' : ''}
-          help={issues.tag || (duplicateTag ? 'Tag already used by another balancer' : '')}
+          help={issues.tag || (duplicateTag ? t('pages.xray.balancer.tagDuplicate') : '')}
           hasFeedback
         >
           <Input
             value={state.tag}
             onChange={(e) => update('tag', e.target.value)}
-            placeholder="unique balancer tag"
+            placeholder={t('pages.xray.balancer.tagPlaceholder')}
           />
         </Form.Item>
-        <Form.Item label="Strategy">
+        <Form.Item label={t('pages.xray.balancer.balancerStrategy')}>
           <Select
             value={state.strategy}
             onChange={(v) => update('strategy', v)}
@@ -155,7 +155,7 @@ export default function BalancerFormModal({
           />
         </Form.Item>
         <Form.Item
-          label="Selector"
+          label={t('pages.xray.balancer.selector')}
           required
           validateStatus={issues.selector ? 'error' : ''}
           help={issues.selector || ''}
@@ -169,7 +169,7 @@ export default function BalancerFormModal({
             options={outboundTags.map((tg) => ({ value: tg, label: tg }))}
           />
         </Form.Item>
-        <Form.Item label="Fallback">
+        <Form.Item label={t('pages.xray.balancer.fallback')}>
           <Select
             value={state.fallbackTag}
             onChange={(v) => update('fallbackTag', v ?? '')}
@@ -180,23 +180,23 @@ export default function BalancerFormModal({
 
         {state.strategy === 'leastLoad' && (
           <>
-            <Form.Item label="Expected">
+            <Form.Item label={t('pages.xray.balancer.expected')}>
               <InputNumber
                 value={settings?.expected}
                 onChange={(v) => updateSetting('expected', typeof v === 'number' ? v : undefined)}
                 min={0}
-                placeholder="optimal node count"
+                placeholder={t('pages.xray.balancer.expectedPlaceholder')}
                 style={{ width: '100%' }}
               />
             </Form.Item>
-            <Form.Item label="Max RTT">
+            <Form.Item label={t('pages.xray.balancer.maxRtt')}>
               <Input
                 value={settings?.maxRTT ?? ''}
                 onChange={(e) => updateSetting('maxRTT', e.target.value || undefined)}
                 placeholder="e.g. 1s"
               />
             </Form.Item>
-            <Form.Item label="Tolerance">
+            <Form.Item label={t('pages.xray.balancer.tolerance')}>
               <InputNumber
                 value={settings?.tolerance}
                 onChange={(v) => updateSetting('tolerance', typeof v === 'number' ? v : undefined)}
@@ -207,7 +207,7 @@ export default function BalancerFormModal({
                 style={{ width: '100%' }}
               />
             </Form.Item>
-            <Form.Item label="Baselines">
+            <Form.Item label={t('pages.xray.balancer.baselines')}>
               <Button
                 size="small"
                 type="primary"
@@ -227,7 +227,7 @@ export default function BalancerFormModal({
                 </Space.Compact>
               ))}
             </Form.Item>
-            <Form.Item label="Costs">
+            <Form.Item label={t('pages.xray.balancer.costs')}>
               <Button
                 size="small"
                 type="primary"

+ 27 - 25
frontend/src/pages/xray/NordModal.tsx

@@ -1,4 +1,5 @@
 import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
 import { Button, Divider, Form, Input, message, Modal, Select, Tabs, Tag } from 'antd';
 import { LoginOutlined, SaveOutlined } from '@ant-design/icons';
 
@@ -58,6 +59,7 @@ export default function NordModal({
   onRemoveOutbound,
   onRemoveRoutingRules,
 }: NordModalProps) {
+  const { t } = useTranslation();
   const [messageApi, messageContextHolder] = message.useMessage();
   const [loading, setLoading] = useState(false);
   const [nordData, setNordData] = useState<NordData | null>(null);
@@ -185,7 +187,7 @@ export default function NordModal({
         })
         .sort((a: NordServer, b: NordServer) => a.load - b.load);
       setServers(next);
-      if (next.length === 0) messageApi.warning('No servers found for the selected country');
+      if (next.length === 0) messageApi.warning(t('pages.xray.nord.noServers'));
     } finally {
       setLoading(false);
     }
@@ -197,7 +199,7 @@ export default function NordModal({
     const tech = server.technologies?.find((tt) => tt.id === 35);
     const publicKey = tech?.metadata?.find((m) => m.name === 'public_key')?.value;
     if (!publicKey) {
-      messageApi.error('Selected server does not advertise a NordLynx public key.');
+      messageApi.error(t('pages.xray.nord.noPublicKey'));
       return null;
     }
     return {
@@ -216,7 +218,7 @@ export default function NordModal({
     const ob = buildNordOutbound();
     if (!ob) return;
     onAddOutbound(ob);
-    messageApi.success('NordVPN outbound added');
+    messageApi.success(t('pages.xray.nord.outboundAdded'));
     onClose();
   }
 
@@ -231,7 +233,7 @@ export default function NordModal({
       oldTag,
       newTag: ob.tag as string,
     });
-    messageApi.success('NordVPN outbound updated');
+    messageApi.success(t('pages.xray.nord.outboundUpdated'));
     onClose();
   }
 
@@ -245,7 +247,7 @@ export default function NordModal({
           items={[
             {
               key: 'token',
-              label: 'Access token',
+              label: t('pages.xray.nord.accessToken'),
               children: (
                 <Form
                   colon={false}
@@ -253,14 +255,14 @@ export default function NordModal({
                   wrapperCol={{ md: { span: 18 } }}
                   className="mt-20"
                 >
-                  <Form.Item label="Access token">
+                  <Form.Item label={t('pages.xray.nord.accessToken')}>
                     <Input
                       value={token}
-                      placeholder="Access token"
+                      placeholder={t('pages.xray.nord.accessToken')}
                       onChange={(e) => setToken(e.target.value)}
                     />
                     <Button type="primary" className="mt-10" loading={loading} icon={<LoginOutlined />} onClick={login}>
-                      Login
+                      {t('login')}
                     </Button>
                   </Form.Item>
                 </Form>
@@ -268,7 +270,7 @@ export default function NordModal({
             },
             {
               key: 'key',
-              label: 'Private key',
+              label: t('pages.xray.nord.privateKey'),
               children: (
                 <Form
                   colon={false}
@@ -276,14 +278,14 @@ export default function NordModal({
                   wrapperCol={{ md: { span: 18 } }}
                   className="mt-20"
                 >
-                  <Form.Item label="Private key">
+                  <Form.Item label={t('pages.xray.nord.privateKey')}>
                     <Input
                       value={manualKey}
-                      placeholder="Private key"
+                      placeholder={t('pages.xray.nord.privateKey')}
                       onChange={(e) => setManualKey(e.target.value)}
                     />
                     <Button type="primary" className="mt-10" loading={loading} icon={<SaveOutlined />} onClick={saveKey}>
-                      Save
+                      {t('save')}
                     </Button>
                   </Form.Item>
                 </Form>
@@ -297,25 +299,25 @@ export default function NordModal({
             <tbody>
               {nordData.token && (
                 <tr className="row-odd">
-                  <td>Access token</td>
+                  <td>{t('pages.xray.nord.accessToken')}</td>
                   <td>{nordData.token}</td>
                 </tr>
               )}
               <tr>
-                <td>Private key</td>
+                <td>{t('pages.xray.nord.privateKey')}</td>
                 <td>{nordData.private_key}</td>
               </tr>
             </tbody>
           </table>
 
           <Button loading={loading} type="primary" danger className="mt-8" onClick={logout}>
-            Logout
+            {t('logout')}
           </Button>
 
-          <Divider className="zero-margin">Settings</Divider>
+          <Divider className="zero-margin">{t('pages.xray.warp.settings')}</Divider>
 
           <Form colon={false} labelCol={{ md: { span: 6 } }} wrapperCol={{ md: { span: 18 } }} className="mt-10">
-            <Form.Item label="Country">
+            <Form.Item label={t('pages.xray.outbound.country')}>
               <Select
                 value={countryId ?? undefined}
                 showSearch={{ optionFilterProp: 'label' }}
@@ -328,18 +330,18 @@ export default function NordModal({
             </Form.Item>
 
             {cities.length > 0 && (
-              <Form.Item label="City">
+              <Form.Item label={t('pages.xray.outbound.city')}>
                 <Select
                   value={cityId}
                   showSearch={{ optionFilterProp: 'label' }}
                   onChange={setCityId}
-                  options={[{ value: null, label: 'All cities' }, ...cities.map((c) => ({ value: c.id, label: c.name }))]}
+                  options={[{ value: null, label: t('pages.xray.outbound.allCities') }, ...cities.map((c) => ({ value: c.id, label: c.name }))]}
                 />
               </Form.Item>
             )}
 
             {filteredServers.length > 0 && (
-              <Form.Item label="Server">
+              <Form.Item label={t('pages.xray.outbound.server')}>
                 <Select
                   value={serverId}
                   showSearch={{ optionFilterProp: 'label' }}
@@ -363,17 +365,17 @@ export default function NordModal({
             )}
           </Form>
 
-          <Divider className="my-10">Outbound status</Divider>
+          <Divider className="my-10">{t('pages.xray.outbound.outboundStatus')}</Divider>
           {nordOutboundIndex >= 0 ? (
             <>
-              <Tag color="green">Enabled</Tag>
+              <Tag color="green">{t('enabled')}</Tag>
               <Button type="primary" danger loading={loading} className="ml-8" onClick={resetOutbound}>
-                Reset
+                {t('reset')}
               </Button>
             </>
           ) : (
             <>
-              <Tag color="orange">Disabled</Tag>
+              <Tag color="orange">{t('disabled')}</Tag>
               <Button
                 type="primary"
                 className="ml-8"
@@ -381,7 +383,7 @@ export default function NordModal({
                 loading={loading}
                 onClick={addOutbound}
               >
-                Add outbound
+                {t('pages.xray.warp.addOutbound')}
               </Button>
             </>
           )}

文件差異過大導致無法顯示
+ 135 - 135
frontend/src/pages/xray/OutboundFormModal.tsx


+ 4 - 4
frontend/src/pages/xray/OutboundsTab.tsx

@@ -258,7 +258,7 @@ export default function OutboundsTab({
         ),
       },
       {
-        title: 'Tag',
+        title: t('pages.xray.outbound.tag'),
         key: 'identity',
         align: 'left',
         render: (_v, record) => (
@@ -316,7 +316,7 @@ export default function OutboundsTab({
         },
       },
       {
-        title: 'Latency',
+        title: t('pages.nodes.latency'),
         key: 'testResult',
         align: 'left',
         width: 140,
@@ -398,14 +398,14 @@ export default function OutboundsTab({
           </Col>
           <Col xs={24} sm={12} className="toolbar-right">
             <Space size="small" wrap>
-              <Tooltip title="TCP: fast dial-only probe. HTTP: full request through xray.">
+              <Tooltip title={t('pages.xray.outbound.testModeTooltip')}>
                 <Radio.Group value={testMode} onChange={(e) => setTestMode(e.target.value)} buttonStyle="solid" size="small">
                   <Radio.Button value="tcp">TCP</Radio.Button>
                   <Radio.Button value="http">HTTP</Radio.Button>
                 </Radio.Group>
               </Tooltip>
               <Button type="primary" loading={testingAll} icon={<PlayCircleOutlined />} onClick={() => onTestAll(testMode)}>
-                {!isMobile && 'Test all'}
+                {!isMobile && t('pages.xray.outbound.testAll')}
               </Button>
               <Popconfirm
                 placement="topRight"

+ 15 - 2
frontend/src/pages/xray/RoutingTab.css

@@ -99,7 +99,7 @@
 .rule-list {
   display: flex;
   flex-direction: column;
-  gap: 8px;
+  gap: 14px;
 }
 
 .rule-card {
@@ -109,11 +109,24 @@
   gap: 8px;
   padding: 10px 12px;
   background: var(--bg-card, #fff);
-  border: 1px solid var(--ant-color-border-secondary);
+  border: 1px solid var(--ant-color-border);
   border-radius: 8px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
   transition: opacity 0.15s, box-shadow 0.15s;
 }
 
+.rule-list > .rule-card:not(:last-child)::after {
+  content: '';
+  position: absolute;
+  left: 50%;
+  bottom: -8px;
+  width: 32px;
+  height: 1px;
+  background: var(--ant-color-border);
+  transform: translateX(-50%);
+  opacity: 0.7;
+}
+
 .rule-card.row-dragging {
   opacity: 0.4;
 }

+ 6 - 4
frontend/src/pages/xray/RoutingTab.tsx

@@ -88,6 +88,8 @@ export default function RoutingTab({
     () => (templateSettings?.routing?.rules || []) as RoutingRule[],
     [templateSettings?.routing?.rules],
   );
+  const rulesRef = useRef(rules);
+  rulesRef.current = rules;
 
   const rows: RuleRow[] = useMemo(
     () =>
@@ -171,7 +173,7 @@ export default function RoutingTab({
     setRuleModalOpen(true);
   }
   function openEdit(idx: number) {
-    setEditingRule(rules[idx]);
+    setEditingRule(rulesRef.current[idx]);
     setEditingIndex(idx);
     setRuleModalOpen(true);
   }
@@ -291,7 +293,7 @@ export default function RoutingTab({
           <div className="action-cell">
             <HolderOutlined
               className="drag-handle"
-              title="Drag to reorder"
+              title={t('pages.xray.routing.dragToReorder')}
               onPointerDown={(ev: React.PointerEvent) => onHandlePointerDown(index, ev)}
             />
             <span className="row-index">{index + 1}</span>
@@ -324,7 +326,7 @@ export default function RoutingTab({
         ),
       },
       {
-        title: 'Source',
+        title: t('pages.xray.rules.source'),
         align: 'left',
         width: 180,
         key: 'source',
@@ -352,7 +354,7 @@ export default function RoutingTab({
         ),
       },
       {
-        title: 'Destination',
+        title: t('pages.xray.rules.dest'),
         align: 'left',
         key: 'destination',
         render: (_v, record) => (

+ 22 - 22
frontend/src/pages/xray/RuleFormModal.tsx

@@ -148,8 +148,8 @@ export default function RuleFormModal({
       <Form colon={false} labelCol={{ md: { span: 8 } }} wrapperCol={{ md: { span: 14 } }}>
         <Form.Item
           label={
-            <Tooltip title="Comma-separated list">
-              Source IPs <QuestionCircleOutlined />
+            <Tooltip title={t('pages.xray.rules.useComma')}>
+              {t('pages.xray.ruleForm.sourceIps')} <QuestionCircleOutlined />
             </Tooltip>
           }
         >
@@ -158,8 +158,8 @@ export default function RuleFormModal({
 
         <Form.Item
           label={
-            <Tooltip title="Comma-separated list">
-              Source port <QuestionCircleOutlined />
+            <Tooltip title={t('pages.xray.rules.useComma')}>
+              {t('pages.xray.ruleForm.sourcePort')} <QuestionCircleOutlined />
             </Tooltip>
           }
         >
@@ -168,15 +168,15 @@ export default function RuleFormModal({
 
         <Form.Item
           label={
-            <Tooltip title="Comma-separated list">
-              VLESS route <QuestionCircleOutlined />
+            <Tooltip title={t('pages.xray.rules.useComma')}>
+              {t('pages.xray.ruleForm.vlessRoute')} <QuestionCircleOutlined />
             </Tooltip>
           }
         >
           <Input value={form.vlessRoute} onChange={(e) => update('vlessRoute', e.target.value)} placeholder="53,443,1000-2000" />
         </Form.Item>
 
-        <Form.Item label="Network">
+        <Form.Item label={t('pages.inbounds.network')}>
           <Select
             value={form.network}
             onChange={(v) => update('network', v)}
@@ -184,7 +184,7 @@ export default function RuleFormModal({
           />
         </Form.Item>
 
-        <Form.Item label="Protocol">
+        <Form.Item label={t('pages.inbounds.protocol')}>
           <Select
             mode="multiple"
             value={form.protocol}
@@ -193,7 +193,7 @@ export default function RuleFormModal({
           />
         </Form.Item>
 
-        <Form.Item label="Attributes">
+        <Form.Item label={t('pages.xray.ruleForm.attributes')}>
           <Button size="small" icon={<PlusOutlined />} onClick={() => update('attrs', [...form.attrs, ['', '']])} />
         </Form.Item>
         <Form.Item wrapperCol={{ span: 24 }}>
@@ -202,7 +202,7 @@ export default function RuleFormModal({
               <InputAddon>{`${idx + 1}`}</InputAddon>
               <Input
                 value={attr[0]}
-                placeholder="Name"
+                placeholder={t('pages.nodes.name')}
                 onChange={(e) => {
                   const next = form.attrs.map((a, i) => (i === idx ? ([e.target.value, a[1]] as [string, string]) : a));
                   update('attrs', next);
@@ -210,7 +210,7 @@ export default function RuleFormModal({
               />
               <Input
                 value={attr[1]}
-                placeholder="Value"
+                placeholder={t('pages.xray.ruleForm.value')}
                 onChange={(e) => {
                   const next = form.attrs.map((a, i) => (i === idx ? ([a[0], e.target.value] as [string, string]) : a));
                   update('attrs', next);
@@ -226,7 +226,7 @@ export default function RuleFormModal({
 
         <Form.Item
           label={
-            <Tooltip title="Comma-separated list">
+            <Tooltip title={t('pages.xray.rules.useComma')}>
               IP <QuestionCircleOutlined />
             </Tooltip>
           }
@@ -236,8 +236,8 @@ export default function RuleFormModal({
 
         <Form.Item
           label={
-            <Tooltip title="Comma-separated list">
-              Domain <QuestionCircleOutlined />
+            <Tooltip title={t('pages.xray.rules.useComma')}>
+              {t('domainName')} <QuestionCircleOutlined />
             </Tooltip>
           }
         >
@@ -246,8 +246,8 @@ export default function RuleFormModal({
 
         <Form.Item
           label={
-            <Tooltip title="Comma-separated list">
-              User <QuestionCircleOutlined />
+            <Tooltip title={t('pages.xray.rules.useComma')}>
+              {t('pages.xray.ruleForm.user')} <QuestionCircleOutlined />
             </Tooltip>
           }
         >
@@ -256,15 +256,15 @@ export default function RuleFormModal({
 
         <Form.Item
           label={
-            <Tooltip title="Comma-separated list">
-              Port <QuestionCircleOutlined />
+            <Tooltip title={t('pages.xray.rules.useComma')}>
+              {t('pages.inbounds.port')} <QuestionCircleOutlined />
             </Tooltip>
           }
         >
           <Input value={form.port} onChange={(e) => update('port', e.target.value)} placeholder="53,443,1000-2000" />
         </Form.Item>
 
-        <Form.Item label="Inbound tags">
+        <Form.Item label={t('pages.xray.ruleForm.inboundTags')}>
           <Select
             mode="multiple"
             value={form.inboundTag}
@@ -273,7 +273,7 @@ export default function RuleFormModal({
           />
         </Form.Item>
 
-        <Form.Item label="Outbound tag">
+        <Form.Item label={t('pages.xray.ruleForm.outboundTag')}>
           <Select
             value={form.outboundTag}
             onChange={(v) => update('outboundTag', v)}
@@ -283,8 +283,8 @@ export default function RuleFormModal({
 
         <Form.Item
           label={
-            <Tooltip title="Routes traffic through one of the configured load balancers">
-              Balancer tag <QuestionCircleOutlined />
+            <Tooltip title={t('pages.xray.ruleForm.balancerTagTooltip')}>
+              {t('pages.xray.ruleForm.balancerTag')} <QuestionCircleOutlined />
             </Tooltip>
           }
         >

+ 30 - 28
frontend/src/pages/xray/WarpModal.tsx

@@ -1,4 +1,5 @@
 import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
 import {
   Alert,
   Button,
@@ -72,6 +73,7 @@ export default function WarpModal({
   onResetOutbound,
   onRemoveOutbound,
 }: WarpModalProps) {
+  const { t } = useTranslation();
   const [messageApi, messageContextHolder] = message.useMessage();
   const [loading, setLoading] = useState(false);
   const [warpData, setWarpData] = useState<WarpData | null>(null);
@@ -167,7 +169,7 @@ export default function WarpModal({
         setWarpConfig(null);
         setWarpPlus('');
       } else {
-        setLicenseError(msg?.msg || 'Failed to set WARP license.');
+        setLicenseError(msg?.msg || t('pages.xray.warp.licenseError'));
       }
     } finally {
       setLoading(false);
@@ -192,7 +194,7 @@ export default function WarpModal({
 
   function addOutbound() {
     if (!stagedOutbound) {
-      messageApi.warning('Fetch the WARP config first.');
+      messageApi.warning(t('pages.xray.warp.fetchFirst'));
       return;
     }
     onAddOutbound(stagedOutbound);
@@ -213,49 +215,49 @@ export default function WarpModal({
       <Modal open={open} title="Cloudflare WARP" footer={null} onCancel={onClose}>
       {!hasWarp ? (
         <Button type="primary" loading={loading} icon={<ApiOutlined />} onClick={register}>
-          Create WARP account
+          {t('pages.xray.warp.createAccount')}
         </Button>
       ) : (
         <>
           <table className="warp-data-table">
             <tbody>
               <tr className="row-odd">
-                <td>Access token</td>
+                <td>{t('pages.xray.warp.accessToken')}</td>
                 <td>{warpData?.access_token}</td>
               </tr>
               <tr>
-                <td>Device ID</td>
+                <td>{t('pages.xray.warp.deviceId')}</td>
                 <td>{warpData?.device_id}</td>
               </tr>
               <tr className="row-odd">
-                <td>License key</td>
+                <td>{t('pages.xray.warp.licenseKey')}</td>
                 <td>{warpData?.license_key}</td>
               </tr>
               <tr>
-                <td>Private key</td>
+                <td>{t('pages.xray.warp.privateKey')}</td>
                 <td>{warpData?.private_key}</td>
               </tr>
             </tbody>
           </table>
 
           <Button loading={loading} type="primary" danger className="mt-8" icon={<DeleteOutlined />} onClick={delConfig}>
-            Delete account
+            {t('pages.xray.warp.deleteAccount')}
           </Button>
 
-          <Divider className="zero-margin">Settings</Divider>
+          <Divider className="zero-margin">{t('pages.xray.warp.settings')}</Divider>
 
           <Collapse
             className="my-10"
             items={[
               {
                 key: '1',
-                label: 'WARP / WARP+ license key',
+                label: t('pages.xray.warp.licenseKeyLabel'),
                 children: (
                   <Form colon={false} labelCol={{ md: { span: 6 } }} wrapperCol={{ md: { span: 14 } }}>
-                    <Form.Item label="Key">
+                    <Form.Item label={t('pages.xray.warp.key')}>
                       <Input
                         value={warpPlus}
-                        placeholder="26-char WARP+ key"
+                        placeholder={t('pages.xray.warp.keyPlaceholder')}
                         onChange={(e) => {
                           setWarpPlus(e.target.value);
                           setLicenseError('');
@@ -268,7 +270,7 @@ export default function WarpModal({
                           loading={loading}
                           onClick={updateLicense}
                         >
-                          Update
+                          {t('update')}
                         </Button>
                         {licenseError && (
                           <Alert title={licenseError} type="error" showIcon className="license-error" />
@@ -281,9 +283,9 @@ export default function WarpModal({
             ]}
           />
 
-          <Divider className="zero-margin">Account info</Divider>
+          <Divider className="zero-margin">{t('pages.xray.warp.accountInfo')}</Divider>
           <Button className="my-8" loading={loading} type="primary" icon={<SyncOutlined />} onClick={getConfig}>
-            Refresh
+            {t('refresh')}
           </Button>
 
           {hasConfig && (
@@ -291,38 +293,38 @@ export default function WarpModal({
               <table className="warp-data-table">
                 <tbody>
                   <tr className="row-odd">
-                    <td>Device name</td>
+                    <td>{t('pages.xray.warp.deviceName')}</td>
                     <td>{warpConfig?.name}</td>
                   </tr>
                   <tr>
-                    <td>Device model</td>
+                    <td>{t('pages.xray.warp.deviceModel')}</td>
                     <td>{warpConfig?.model}</td>
                   </tr>
                   <tr className="row-odd">
-                    <td>Device enabled</td>
+                    <td>{t('pages.xray.warp.deviceEnabled')}</td>
                     <td>{String(warpConfig?.enabled)}</td>
                   </tr>
                   {warpConfig?.account && (
                     <>
                       <tr>
-                        <td>Account type</td>
+                        <td>{t('pages.xray.warp.accountType')}</td>
                         <td>{warpConfig.account.account_type}</td>
                       </tr>
                       <tr className="row-odd">
-                        <td>Role</td>
+                        <td>{t('pages.xray.warp.role')}</td>
                         <td>{warpConfig.account.role}</td>
                       </tr>
                       <tr>
-                        <td>WARP+ data</td>
+                        <td>{t('pages.xray.warp.warpPlusData')}</td>
                         <td>{SizeFormatter.sizeFormat(warpConfig.account.premium_data)}</td>
                       </tr>
                       <tr className="row-odd">
-                        <td>Quota</td>
+                        <td>{t('pages.xray.warp.quota')}</td>
                         <td>{SizeFormatter.sizeFormat(warpConfig.account.quota)}</td>
                       </tr>
                       {warpConfig.account.usage != null && (
                         <tr>
-                          <td>Usage</td>
+                          <td>{t('pages.xray.warp.usage')}</td>
                           <td>{SizeFormatter.sizeFormat(warpConfig.account.usage)}</td>
                         </tr>
                       )}
@@ -331,19 +333,19 @@ export default function WarpModal({
                 </tbody>
               </table>
 
-              <Divider className="my-10">Outbound status</Divider>
+              <Divider className="my-10">{t('pages.xray.outbound.outboundStatus')}</Divider>
               {warpOutboundIndex >= 0 ? (
                 <>
-                  <Tag color="green">Enabled</Tag>
+                  <Tag color="green">{t('enabled')}</Tag>
                   <Button type="primary" danger loading={loading} className="ml-8" onClick={resetOutbound}>
-                    Reset
+                    {t('reset')}
                   </Button>
                 </>
               ) : (
                 <>
-                  <Tag color="orange">Disabled</Tag>
+                  <Tag color="orange">{t('disabled')}</Tag>
                   <Button type="primary" loading={loading} className="ml-8" icon={<PlusOutlined />} onClick={addOutbound}>
-                    Add outbound
+                    {t('pages.xray.warp.addOutbound')}
                   </Button>
                 </>
               )}

+ 6 - 6
frontend/src/pages/xray/XrayPage.tsx

@@ -223,10 +223,10 @@ export default function XrayPage() {
 
   function confirmRestart() {
     modal.confirm({
-      title: 'Restart xray?',
-      content: 'Reloads the xray service with the saved configuration.',
-      okText: 'Restart',
-      cancelText: 'Cancel',
+      title: t('pages.xray.restartConfirmTitle'),
+      content: t('pages.xray.restartConfirmContent'),
+      okText: t('pages.xray.restart'),
+      cancelText: t('cancel'),
       onOk: () => restartXray(),
     });
   }
@@ -255,7 +255,7 @@ export default function XrayPage() {
 
         <Layout className="content-shell">
           <Layout.Content id="content-layout" className="content-area">
-            <Spin spinning={spinning || !fetched} delay={200} description="Loading…" size="large">
+            <Spin spinning={spinning || !fetched} delay={200} description={t('loading')} size="large">
               {!fetched ? (
                 <div className="loading-spacer" />
               ) : fetchError ? (
@@ -281,7 +281,7 @@ export default function XrayPage() {
                             {restartResult && (
                               <Popover
                                 placement="rightTop"
-                                title="Xray restart output"
+                                title={t('pages.xray.restartOutputTitle')}
                                 content={<pre className="restart-result">{restartResult}</pre>}
                               >
                                 <QuestionCircleOutlined className="restart-icon" />

+ 14 - 0
frontend/src/schemas/client.ts

@@ -97,6 +97,18 @@ export const DelDepletedResultSchema = z.object({
   deleted: z.number().optional(),
 });
 
+export const BulkAttachResultSchema = z.object({
+  attached: z.array(z.string()).nullable().transform((v) => v ?? []),
+  skipped: z.array(z.string()).nullable().transform((v) => v ?? []),
+  errors: z.array(z.string()).nullable().transform((v) => v ?? []),
+});
+
+export const BulkDetachResultSchema = z.object({
+  detached: z.array(z.string()).nullable().transform((v) => v ?? []),
+  skipped: z.array(z.string()).nullable().transform((v) => v ?? []),
+  errors: z.array(z.string()).nullable().transform((v) => v ?? []),
+});
+
 export const OnlinesSchema = nullableStringArray;
 
 export const GroupSummarySchema = z.object({
@@ -167,6 +179,8 @@ export type ClientHydrate = z.infer<typeof ClientHydrateSchema>;
 export type BulkAdjustResult = z.infer<typeof BulkAdjustResultSchema>;
 export type BulkDeleteResult = z.infer<typeof BulkDeleteResultSchema>;
 export type BulkCreateResult = z.infer<typeof BulkCreateResultSchema>;
+export type BulkAttachResult = z.infer<typeof BulkAttachResultSchema>;
+export type BulkDetachResult = z.infer<typeof BulkDetachResultSchema>;
 export type ClientBulkAddFormValues = z.infer<typeof ClientBulkAddFormSchema>;
 export type ClientBulkAdjustFormValues = z.infer<typeof ClientBulkAdjustFormSchema>;
 export type ClientFormValues = z.infer<typeof ClientFormSchema>;

+ 2 - 1
frontend/src/schemas/protocols/security/tls.ts

@@ -51,6 +51,7 @@ export type TlsCert = z.infer<typeof TlsCertSchema>;
 export const TlsClientSettingsSchema = z.object({
   fingerprint: UtlsFingerprintSchema.default('chrome'),
   echConfigList: z.string().default(''),
+  pinnedPeerCertSha256: z.array(z.string()).default([]),
 });
 export type TlsClientSettings = z.infer<typeof TlsClientSettingsSchema>;
 
@@ -67,6 +68,6 @@ export const TlsStreamSettingsSchema = z.object({
   certificates: z.array(TlsCertSchema).default([]),
   alpn: z.array(AlpnSchema).default(['h2', 'http/1.1']),
   echServerKeys: z.string().default(''),
-  settings: TlsClientSettingsSchema.default({ fingerprint: 'chrome', echConfigList: '' }),
+  settings: TlsClientSettingsSchema.default({ fingerprint: 'chrome', echConfigList: '', pinnedPeerCertSha256: [] }),
 });
 export type TlsStreamSettings = z.infer<typeof TlsStreamSettingsSchema>;

+ 18 - 0
frontend/src/styles/page-shell.css

@@ -89,6 +89,24 @@
   min-height: calc(100vh - 120px);
 }
 
+.ant-dropdown-menu-item:not(.ant-dropdown-menu-item-disabled):not(.ant-dropdown-menu-item-danger):hover,
+.ant-dropdown-menu-item:not(.ant-dropdown-menu-item-disabled):not(.ant-dropdown-menu-item-danger):hover .ant-dropdown-menu-title-content,
+.ant-dropdown-menu-item:not(.ant-dropdown-menu-item-disabled):not(.ant-dropdown-menu-item-danger):hover > .anticon {
+  color: var(--ant-color-primary) !important;
+}
+
+.ant-dropdown-menu-item:not(.ant-dropdown-menu-item-disabled):not(.ant-dropdown-menu-item-danger):hover {
+  background-color: color-mix(in srgb, var(--ant-color-primary) 14%, transparent) !important;
+}
+
+.ant-dropdown-menu-item-divider {
+  background-color: var(--ant-color-border) !important;
+}
+
+body.dark .ant-dropdown-menu-item-divider {
+  background-color: rgba(255, 255, 255, 0.12) !important;
+}
+
 .settings-page .header-row,
 .xray-page .header-row {
   display: flex;

+ 95 - 0
frontend/src/test/__snapshots__/inbound-full.test.ts.snap

@@ -70,6 +70,7 @@ exports[`InboundSchema (full) fixtures > parses hysteria-v1-tls byte-stably 1`]
       "settings": {
         "echConfigList": "",
         "fingerprint": "chrome",
+        "pinnedPeerCertSha256": [],
       },
     },
   },
@@ -207,6 +208,7 @@ exports[`InboundSchema (full) fixtures > parses trojan-ws-tls byte-stably 1`] =
       "settings": {
         "echConfigList": "",
         "fingerprint": "chrome",
+        "pinnedPeerCertSha256": [],
       },
     },
     "wsSettings": {
@@ -378,6 +380,7 @@ exports[`InboundSchema (full) fixtures > parses vless-ws-tls byte-stably 1`] = `
       "settings": {
         "echConfigList": "",
         "fingerprint": "chrome",
+        "pinnedPeerCertSha256": [],
       },
     },
     "wsSettings": {
@@ -394,6 +397,97 @@ exports[`InboundSchema (full) fixtures > parses vless-ws-tls byte-stably 1`] = `
 }
 `;
 
+exports[`InboundSchema (full) fixtures > parses vless-ws-tls-pinned byte-stably 1`] = `
+{
+  "down": 0,
+  "enable": true,
+  "expiryTime": 0,
+  "id": 43,
+  "listen": "",
+  "port": 443,
+  "protocol": "vless",
+  "remark": "alice-vless-ws-tls-pinned",
+  "settings": {
+    "clients": [
+      {
+        "comment": "",
+        "email": "[email protected]",
+        "enable": true,
+        "expiryTime": 0,
+        "flow": "",
+        "id": "8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02",
+        "limitIp": 0,
+        "reset": 0,
+        "subId": "abc123def",
+        "tgId": 0,
+        "totalGB": 0,
+      },
+    ],
+    "decryption": "none",
+    "encryption": "none",
+    "fallbacks": [],
+  },
+  "sniffing": {
+    "destOverride": [
+      "http",
+      "tls",
+      "quic",
+      "fakedns",
+    ],
+    "domainsExcluded": [],
+    "enabled": true,
+    "ipsExcluded": [],
+    "metadataOnly": false,
+    "routeOnly": false,
+  },
+  "streamSettings": {
+    "network": "ws",
+    "security": "tls",
+    "tlsSettings": {
+      "alpn": [
+        "h2",
+        "http/1.1",
+      ],
+      "certificates": [
+        {
+          "buildChain": false,
+          "certificateFile": "/etc/ssl/certs/cdn.example.test.crt",
+          "keyFile": "/etc/ssl/private/cdn.example.test.key",
+          "oneTimeLoading": false,
+          "usage": "encipherment",
+        },
+      ],
+      "cipherSuites": "",
+      "disableSystemRoot": false,
+      "echServerKeys": "",
+      "enableSessionResumption": false,
+      "maxVersion": "1.3",
+      "minVersion": "1.2",
+      "rejectUnknownSni": false,
+      "serverName": "cdn.example.test",
+      "settings": {
+        "echConfigList": "",
+        "fingerprint": "chrome",
+        "pinnedPeerCertSha256": [
+          "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+          "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=",
+        ],
+      },
+    },
+    "wsSettings": {
+      "acceptProxyProtocol": false,
+      "headers": {},
+      "heartbeatPeriod": 0,
+      "host": "cdn.example.test",
+      "path": "/ws",
+    },
+  },
+  "tag": "inbound-vless-pinned-1",
+  "total": 0,
+  "up": 0,
+}
+`;
+
 exports[`InboundSchema (full) fixtures > parses vmess-tcp-tls byte-stably 1`] = `
 {
   "down": 0,
@@ -468,6 +562,7 @@ exports[`InboundSchema (full) fixtures > parses vmess-tcp-tls byte-stably 1`] =
       "settings": {
         "echConfigList": "",
         "fingerprint": "chrome",
+        "pinnedPeerCertSha256": [],
       },
     },
   },

+ 6 - 2
frontend/src/test/__snapshots__/inbound-link.test.ts.snap

@@ -8,10 +8,12 @@ exports[`genInboundLinks orchestrator > shadowsocks-tcp-2022: byte-stable 1`] =
 
 exports[`genInboundLinks orchestrator > trojan-ws-tls: byte-stable 1`] = `"trojan://[email protected]:443?type=ws&path=%2Ftrojan&host=trojan.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=trojan.example.test#parity-test"`;
 
-exports[`genInboundLinks orchestrator > vless-tcp-reality: byte-stable 1`] = `"vless://[email protected]:443?type=tcp&encryption=none&security=reality&pbk=Tx5yj1bRcOPHkdvT2pIAQ2zh0gQ8m4OPdnzqXJxxV3o&fp=chrome&sid=a3f1&spx=%2F&flow=xtls-rprx-vision#parity-test"`;
+exports[`genInboundLinks orchestrator > vless-tcp-reality: byte-stable 1`] = `"vless://[email protected]:443?type=tcp&encryption=none&security=reality&pbk=Tx5yj1bRcOPHkdvT2pIAQ2zh0gQ8m4OPdnzqXJxxV3o&fp=chrome&sni=yahoo.com&sid=a3f1&spx=%2F&flow=xtls-rprx-vision#parity-test"`;
 
 exports[`genInboundLinks orchestrator > vless-ws-tls: byte-stable 1`] = `"vless://[email protected]:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test#parity-test"`;
 
+exports[`genInboundLinks orchestrator > vless-ws-tls-pinned: byte-stable 1`] = `"vless://[email protected]:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test&pcs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%3D%2CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB%3D#parity-test"`;
+
 exports[`genInboundLinks orchestrator > vmess-tcp-tls: byte-stable 1`] = `"vmess://ewogICJ2IjogIjIiLAogICJwcyI6ICJwYXJpdHktdGVzdCIsCiAgImFkZCI6ICJvdmVycmlkZS50ZXN0IiwKICAicG9ydCI6IDg0NDMsCiAgImlkIjogIjExMTExMTExLTIyMjItNDMzMy04NDQ0LTU1NTU1NTU1NTU1NSIsCiAgInNjeSI6ICJhdXRvIiwKICAibmV0IjogInRjcCIsCiAgInRscyI6ICJ0bHMiLAogICJ0eXBlIjogIm5vbmUiLAogICJzbmkiOiAidm1lc3MuZXhhbXBsZS50ZXN0IiwKICAiZnAiOiAiY2hyb21lIiwKICAiYWxwbiI6ICJoMixodHRwLzEuMSIKfQ=="`;
 
 exports[`genInboundLinks orchestrator > wireguard-server: byte-stable 1`] = `
@@ -34,10 +36,12 @@ exports[`genShadowsocksLink > shadowsocks-tcp-2022: byte-stable 1`] = `"ss://MjA
 
 exports[`genTrojanLink > trojan-ws-tls: byte-stable 1`] = `"trojan://[email protected]:443?type=ws&path=%2Ftrojan&host=trojan.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=trojan.example.test#parity-test"`;
 
-exports[`genVlessLink > vless-tcp-reality: byte-stable 1`] = `"vless://[email protected]:443?type=tcp&encryption=none&security=reality&pbk=Tx5yj1bRcOPHkdvT2pIAQ2zh0gQ8m4OPdnzqXJxxV3o&fp=chrome&sid=a3f1&spx=%2F&flow=xtls-rprx-vision#parity-test"`;
+exports[`genVlessLink > vless-tcp-reality: byte-stable 1`] = `"vless://[email protected]:443?type=tcp&encryption=none&security=reality&pbk=Tx5yj1bRcOPHkdvT2pIAQ2zh0gQ8m4OPdnzqXJxxV3o&fp=chrome&sni=yahoo.com&sid=a3f1&spx=%2F&flow=xtls-rprx-vision#parity-test"`;
 
 exports[`genVlessLink > vless-ws-tls: byte-stable 1`] = `"vless://[email protected]:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test#parity-test"`;
 
+exports[`genVlessLink > vless-ws-tls-pinned: byte-stable 1`] = `"vless://[email protected]:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test&pcs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%3D%2CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB%3D#parity-test"`;
+
 exports[`genVmessLink > vmess-tcp-tls: byte-stable 1`] = `"vmess://ewogICJ2IjogIjIiLAogICJwcyI6ICJwYXJpdHktdGVzdCIsCiAgImFkZCI6ICJleGFtcGxlLnRlc3QiLAogICJwb3J0IjogODQ0MywKICAiaWQiOiAiMTExMTExMTEtMjIyMi00MzMzLTg0NDQtNTU1NTU1NTU1NTU1IiwKICAic2N5IjogImF1dG8iLAogICJuZXQiOiAidGNwIiwKICAidGxzIjogInRscyIsCiAgInR5cGUiOiAibm9uZSIsCiAgInNuaSI6ICJ2bWVzcy5leGFtcGxlLnRlc3QiLAogICJmcCI6ICJjaHJvbWUiLAogICJhbHBuIjogImgyLGh0dHAvMS4xIgp9"`;
 
 exports[`genWireguardLink + genWireguardConfig > wireguard-server: byte-stable 1`] = `

+ 1 - 0
frontend/src/test/__snapshots__/security.test.ts.snap

@@ -66,6 +66,7 @@ exports[`SecuritySettingsSchema fixtures > parses tls-cert-file byte-stably 1`]
     "settings": {
       "echConfigList": "",
       "fingerprint": "chrome",
+      "pinnedPeerCertSha256": [],
     },
   },
 }

+ 80 - 0
frontend/src/test/golden/fixtures/inbound-full/vless-ws-tls-pinned.json

@@ -0,0 +1,80 @@
+{
+  "id": 43,
+  "up": 0,
+  "down": 0,
+  "total": 0,
+  "remark": "alice-vless-ws-tls-pinned",
+  "enable": true,
+  "expiryTime": 0,
+  "listen": "",
+  "port": 443,
+  "tag": "inbound-vless-pinned-1",
+  "sniffing": {
+    "enabled": true,
+    "destOverride": ["http", "tls", "quic", "fakedns"],
+    "metadataOnly": false,
+    "routeOnly": false,
+    "ipsExcluded": [],
+    "domainsExcluded": []
+  },
+  "protocol": "vless",
+  "settings": {
+    "clients": [
+      {
+        "id": "8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02",
+        "email": "[email protected]",
+        "flow": "",
+        "limitIp": 0,
+        "totalGB": 0,
+        "expiryTime": 0,
+        "enable": true,
+        "tgId": 0,
+        "subId": "abc123def",
+        "comment": "",
+        "reset": 0
+      }
+    ],
+    "decryption": "none",
+    "encryption": "none",
+    "fallbacks": []
+  },
+  "streamSettings": {
+    "network": "ws",
+    "wsSettings": {
+      "acceptProxyProtocol": false,
+      "path": "/ws",
+      "host": "cdn.example.test",
+      "headers": {},
+      "heartbeatPeriod": 0
+    },
+    "security": "tls",
+    "tlsSettings": {
+      "serverName": "cdn.example.test",
+      "minVersion": "1.2",
+      "maxVersion": "1.3",
+      "cipherSuites": "",
+      "rejectUnknownSni": false,
+      "disableSystemRoot": false,
+      "enableSessionResumption": false,
+      "certificates": [
+        {
+          "certificateFile": "/etc/ssl/certs/cdn.example.test.crt",
+          "keyFile": "/etc/ssl/private/cdn.example.test.key",
+          "oneTimeLoading": false,
+          "usage": "encipherment",
+          "buildChain": false
+        }
+      ],
+      "alpn": ["h2", "http/1.1"],
+      "echServerKeys": "",
+      "settings": {
+        "fingerprint": "chrome",
+        "echConfigList": "",
+        "pinnedPeerCertSha256": [
+          "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+          "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
+        ]
+      }
+    }
+  }
+}

+ 88 - 13
install.sh

@@ -111,10 +111,11 @@ gen_random_string() {
 }
 
 install_postgres_local() {
-    local pg_user="xui"
-    local pg_db="xui"
-    local pg_pass
+    local pg_user pg_pass
     pg_pass=$(gen_random_string 24)
+    local pg_db="xui"
+    local pg_host="127.0.0.1"
+    local pg_port="5432"
 
     case "${release}" in
         ubuntu | debian | armbian)
@@ -170,20 +171,50 @@ install_postgres_local() {
         sleep 1
     done
 
-    # Idempotent role/db creation.
+    local existing_owner=""
+    existing_owner=$(sudo -u postgres psql -tAc \
+        "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_database WHERE datname='${pg_db}'" 2> /dev/null \
+        | tr -d '[:space:]')
+    if [[ -n "${existing_owner}" && "${existing_owner}" != "postgres" ]]; then
+        pg_user="${existing_owner}"
+    else
+        pg_user=$(gen_random_string 8)
+    fi
+
+    # Idempotent role/db creation. Identifiers are double-quoted because a
+    # random username may start with a digit, which Postgres rejects unquoted.
     sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${pg_user}'" 2> /dev/null \
         | grep -q 1 \
-        || sudo -u postgres psql -c "CREATE USER ${pg_user} WITH PASSWORD '${pg_pass}';" >&2 || return 1
+        || sudo -u postgres psql -c "CREATE USER \"${pg_user}\" WITH PASSWORD '${pg_pass}';" >&2 || return 1
 
     sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${pg_db}'" 2> /dev/null \
         | grep -q 1 \
-        || sudo -u postgres psql -c "CREATE DATABASE ${pg_db} OWNER ${pg_user};" >&2 || return 1
+        || sudo -u postgres psql -c "CREATE DATABASE \"${pg_db}\" OWNER \"${pg_user}\";" >&2 || return 1
 
-    sudo -u postgres psql -c "ALTER USER ${pg_user} WITH PASSWORD '${pg_pass}';" >&2 || return 1
+    sudo -u postgres psql -c "ALTER USER \"${pg_user}\" WITH PASSWORD '${pg_pass}';" >&2 || return 1
 
     local pg_pass_enc
     pg_pass_enc=$(printf '%s' "${pg_pass}" | sed -e 's/%/%25/g' -e 's/:/%3A/g' -e 's/@/%40/g' -e 's|/|%2F|g' -e 's/?/%3F/g' -e 's/#/%23/g')
-    echo "postgres://${pg_user}:${pg_pass_enc}@127.0.0.1:5432/${pg_db}?sslmode=disable"
+
+    if [[ -n "${PG_CRED_FILE:-}" ]]; then
+        local prev_umask
+        prev_umask=$(umask)
+        umask 077
+        if ! cat > "${PG_CRED_FILE}" << EOF; then
+PG_USER=${pg_user}
+PG_PASS=${pg_pass}
+PG_HOST=${pg_host}
+PG_PORT=${pg_port}
+PG_DB=${pg_db}
+EOF
+            umask "${prev_umask}"
+            echo -e "${red}Failed to write PostgreSQL credentials to ${PG_CRED_FILE}${plain}" >&2
+            return 1
+        fi
+        umask "${prev_umask}"
+    fi
+
+    echo "postgres://${pg_user}:${pg_pass_enc}@${pg_host}:${pg_port}/${pg_db}?sslmode=disable"
     return 0
 }
 
@@ -823,7 +854,7 @@ config_after_install() {
             echo -e "${green}═══════════════════════════════════════════${plain}"
             echo -e "${green}     Database Selection                    ${plain}"
             echo -e "${green}═══════════════════════════════════════════${plain}"
-            echo -e "  1) SQLite     (default — recommended for < 1000 clients)"
+            echo -e "  1) SQLite     (default — recommended for < 500 clients)"
             echo -e "  2) PostgreSQL (recommended for high client counts / many nodes)"
             read -rp "Choose [1]: " db_choice
             db_choice="${db_choice:-1}"
@@ -843,6 +874,7 @@ config_after_install() {
 
                 local xui_dsn=""
                 local pg_mode=""
+                local pg_local_installed=0
                 while [[ -z "$xui_dsn" ]]; do
                     echo ""
                     echo -e "  1) Install PostgreSQL locally and create a dedicated user/db (recommended)"
@@ -857,9 +889,23 @@ config_after_install() {
                         db_label="PostgreSQL (external)"
                     else
                         echo -e "${yellow}Installing PostgreSQL — this may take a moment...${plain}"
-                        if xui_dsn=$(install_postgres_local); then
-                            db_label="PostgreSQL ([email protected]:5432/xui)"
+                        local pg_cred_file
+                        pg_cred_file=$(mktemp 2> /dev/null) || pg_cred_file=$(mktemp -t x-ui-pg-creds.XXXXXXXX)
+                        if [[ -z "${pg_cred_file}" ]]; then
+                            echo -e "${red}Failed to create temporary credentials file.${plain}"
+                            xui_dsn=""
+                            continue
+                        fi
+                        if xui_dsn=$(PG_CRED_FILE="${pg_cred_file}" install_postgres_local); then
+                            pg_local_installed=1
+                            if [[ -r "${pg_cred_file}" ]]; then
+                                # shellcheck disable=SC1090
+                                source "${pg_cred_file}"
+                            fi
+                            rm -f "${pg_cred_file}"
+                            db_label="PostgreSQL (${PG_USER}@${PG_HOST}:${PG_PORT}/${PG_DB})"
                         else
+                            rm -f "${pg_cred_file}"
                             echo ""
                             echo -e "${red}PostgreSQL installation failed.${plain}"
                             echo -e "  1) Retry local install"
@@ -870,8 +916,15 @@ config_after_install() {
                             pg_fail="${pg_fail:-1}"
                             case "$pg_fail" in
                                 2) pg_mode="2" ;;
-                                3) echo -e "${red}Install aborted.${plain}"; exit 1 ;;
-                                4) db_choice="1"; xui_dsn=""; break ;;
+                                3)
+                                    echo -e "${red}Install aborted.${plain}"
+                                    exit 1
+                                    ;;
+                                4)
+                                    db_choice="1"
+                                    xui_dsn=""
+                                    break
+                                    ;;
                                 *) xui_dsn="" ;;
                             esac
                         fi
@@ -935,6 +988,28 @@ EOF
             else
                 echo -e "${yellow}⚠ SSL Certificate: Skipped — panel is HTTP-only. Use a reverse proxy or SSH tunnel.${plain}"
             fi
+
+            if [[ "$db_choice" == "2" && "$pg_local_installed" == "1" ]]; then
+                echo ""
+                echo -e "${green}═══════════════════════════════════════════${plain}"
+                echo -e "${green}     PostgreSQL Credentials               ${plain}"
+                echo -e "${green}═══════════════════════════════════════════${plain}"
+                echo -e "${green}DB Name:    ${PG_DB}${plain}"
+                echo -e "${green}Username:   ${PG_USER}${plain}"
+                echo -e "${green}Password:   ${PG_PASS}${plain}"
+                echo -e "${green}Host:       ${PG_HOST}${plain}"
+                echo -e "${green}Port:       ${PG_PORT}${plain}"
+                echo -e "${green}DSN:        ${xui_dsn}${plain}"
+                echo -e "${green}Env file:   ${xui_env_file}${plain}"
+                echo -e "${green}-------------------------------------------${plain}"
+                echo -e "${green}Connect from this server:${plain}"
+                echo -e "  ${blue}sudo -u postgres psql -d ${PG_DB}${plain}      (as the postgres superuser)"
+                echo -e "  ${blue}PGPASSWORD='${PG_PASS}' psql -h ${PG_HOST} -p ${PG_PORT} -U ${PG_USER} -d ${PG_DB}${plain}"
+                echo -e "${green}═══════════════════════════════════════════${plain}"
+                echo -e "${yellow}⚠ The panel reads these credentials from ${xui_env_file}.${plain}"
+                echo -e "${yellow}⚠ Save the password — it is not stored anywhere else in plain text.${plain}"
+                unset PG_USER PG_PASS PG_HOST PG_PORT PG_DB
+            fi
         else
             local config_webBasePath=$(gen_random_string 18)
             echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"

+ 3 - 0
sub/subClashService.go

@@ -482,6 +482,9 @@ func (s *SubClashService) tlsData(tData map[string]any) map[string]any {
 	if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
 		tlsData["fingerprint"] = fingerprint
 	}
+	if pins, ok := tlsClientSettings["pinnedPeerCertSha256"].([]any); ok && len(pins) > 0 {
+		tlsData["pin-sha256"] = pins
+	}
 	return tlsData
 }
 

+ 3 - 0
sub/subJsonService.go

@@ -272,6 +272,9 @@ func (s *SubJsonService) tlsData(tData map[string]any) map[string]any {
 	if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
 		tlsData["fingerprint"] = fingerprint
 	}
+	if pins, ok := tlsClientSettings["pinnedPeerCertSha256"].([]any); ok && len(pins) > 0 {
+		tlsData["pinnedPeerCertSha256"] = pins
+	}
 	return tlsData
 }
 

+ 55 - 4
sub/subService.go

@@ -696,8 +696,17 @@ func applyShareNetworkParams(stream map[string]any, streamNetwork string, params
 			request := header["request"].(map[string]any)
 			requestPath, _ := request["path"].([]any)
 			params["path"] = requestPath[0].(string)
-			headers, _ := request["headers"].(map[string]any)
-			params["host"] = searchHost(headers)
+			host := ""
+			if response, ok := header["response"].(map[string]any); ok {
+				if respHeaders, ok := response["headers"].(map[string]any); ok {
+					host = searchHost(respHeaders)
+				}
+			}
+			if host == "" {
+				headers, _ := request["headers"].(map[string]any)
+				host = searchHost(headers)
+			}
+			params["host"] = host
 			params["headerType"] = "http"
 		}
 	case "kcp":
@@ -743,8 +752,17 @@ func applyVmessNetworkParams(stream map[string]any, network string, obj map[stri
 			request := header["request"].(map[string]any)
 			requestPath, _ := request["path"].([]any)
 			obj["path"] = requestPath[0].(string)
-			headers, _ := request["headers"].(map[string]any)
-			obj["host"] = searchHost(headers)
+			host := ""
+			if response, ok := header["response"].(map[string]any); ok {
+				if respHeaders, ok := response["headers"].(map[string]any); ok {
+					host = searchHost(respHeaders)
+				}
+			}
+			if host == "" {
+				headers, _ := request["headers"].(map[string]any)
+				host = searchHost(headers)
+			}
+			obj["host"] = host
 		}
 	case "kcp":
 		applyKcpShareObj(stream, obj)
@@ -791,6 +809,9 @@ func applyShareTLSParams(stream map[string]any, params map[string]string) {
 		if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
 			params["fp"], _ = fpValue.(string)
 		}
+		if pins, ok := pinnedSha256List(tlsSettings); ok {
+			params["pcs"] = strings.Join(pins, ",")
+		}
 	}
 }
 
@@ -813,7 +834,37 @@ func applyVmessTLSParams(stream map[string]any, obj map[string]any) {
 		if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
 			obj["fp"], _ = fpValue.(string)
 		}
+		if pins, ok := pinnedSha256List(tlsSettings); ok {
+			obj["pcs"] = strings.Join(pins, ",")
+		}
+	}
+}
+
+// pinnedSha256List extracts tlsSettings.settings.pinnedPeerCertSha256 as a
+// []string. The field is panel-only (stripped before the run-config reaches
+// xray-core via web/service/xray.go) but flows into share links so clients
+// can pin the server's certificate hash.
+func pinnedSha256List(tlsClientSettings any) ([]string, bool) {
+	raw, ok := searchKey(tlsClientSettings, "pinnedPeerCertSha256")
+	if !ok {
+		return nil, false
+	}
+	arr, ok := raw.([]any)
+	if !ok || len(arr) == 0 {
+		return nil, false
+	}
+	out := make([]string, 0, len(arr))
+	for _, v := range arr {
+		s, ok := v.(string)
+		if !ok || s == "" {
+			continue
+		}
+		out = append(out, s)
+	}
+	if len(out) == 0 {
+		return nil, false
 	}
+	return out, true
 }
 
 func applyShareRealityParams(stream map[string]any, params map[string]string) {

+ 13 - 0
util/random/random.go

@@ -51,6 +51,19 @@ func Seq(n int) string {
 	return string(runes)
 }
 
+// NumLower generates a random string of length n containing digits and lowercase letters only.
+func NumLower(n int) string {
+	runes := make([]rune, n)
+	for i := range n {
+		idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(numLowerSeq))))
+		if err != nil {
+			panic("crypto/rand failed: " + err.Error())
+		}
+		runes[i] = numLowerSeq[idx.Int64()]
+	}
+	return string(runes)
+}
+
 // Num generates a random integer between 0 and n-1.
 func Num(n int) int {
 	bn := big.NewInt(int64(n))

+ 1 - 0
web/controller/api.go

@@ -67,6 +67,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup, customGeo *service.Custom
 
 	clients := api.Group("/clients")
 	NewClientController(clients)
+	NewGroupController(clients)
 
 	// Server API
 	server := api.Group("/server")

+ 2 - 0
web/controller/api_docs_test.go

@@ -89,6 +89,8 @@ func TestAPIRoutesDocumented(t *testing.T) {
 			basePath = "/panel/api/inbounds"
 		case "client.go":
 			basePath = "/panel/api/clients"
+		case "group.go":
+			basePath = "/panel/api/clients"
 		case "server.go":
 			basePath = "/panel/api/server"
 		case "node.go":

+ 16 - 98
web/controller/client.go

@@ -47,21 +47,15 @@ func (a *ClientController) initRouter(g *gin.RouterGroup) {
 	g.POST("/bulkAdjust", a.bulkAdjust)
 	g.POST("/bulkDel", a.bulkDelete)
 	g.POST("/bulkCreate", a.bulkCreate)
-	g.POST("/bulkAssignGroup", a.bulkAssignGroup)
 	g.POST("/bulkAttach", a.bulkAttach)
+	g.POST("/bulkDetach", a.bulkDetach)
+	g.POST("/bulkResetTraffic", a.bulkResetTraffic)
 	g.POST("/resetTraffic/:email", a.resetTrafficByEmail)
 	g.POST("/updateTraffic/:email", a.updateTrafficByEmail)
 	g.POST("/ips/:email", a.getIps)
 	g.POST("/clearIps/:email", a.clearIps)
 	g.POST("/onlines", a.onlines)
 	g.POST("/lastOnline", a.lastOnline)
-
-	g.GET("/groups", a.listGroups)
-	g.GET("/groups/:name/emails", a.groupEmails)
-	g.POST("/groups/create", a.createGroup)
-	g.POST("/groups/rename", a.renameGroup)
-	g.POST("/groups/delete", a.deleteGroup)
-	g.POST("/bulkResetTraffic", a.bulkResetTraffic)
 }
 
 func (a *ClientController) list(c *gin.Context) {
@@ -219,39 +213,41 @@ type bulkDeleteRequest struct {
 	KeepTraffic bool     `json:"keepTraffic"`
 }
 
-type bulkAssignGroupRequest struct {
-	Emails []string `json:"emails"`
-	Group  string   `json:"group"`
+type bulkAttachRequest struct {
+	Emails     []string `json:"emails"`
+	InboundIds []int    `json:"inboundIds"`
 }
 
-func (a *ClientController) bulkAssignGroup(c *gin.Context) {
-	var req bulkAssignGroupRequest
+func (a *ClientController) bulkAttach(c *gin.Context) {
+	var req bulkAttachRequest
 	if err := c.ShouldBindJSON(&req); err != nil {
 		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
 		return
 	}
-	affected, err := a.clientService.AssignGroup(req.Emails, req.Group)
+	result, needRestart, err := a.clientService.BulkAttach(&a.inboundService, req.Emails, req.InboundIds)
 	if err != nil {
 		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
 		return
 	}
-	jsonObj(c, gin.H{"affected": affected}, nil)
-	a.xrayService.SetToNeedRestart()
+	jsonObj(c, result, nil)
+	if needRestart {
+		a.xrayService.SetToNeedRestart()
+	}
 	notifyClientsChanged()
 }
 
-type bulkAttachRequest struct {
+type bulkDetachRequest struct {
 	Emails     []string `json:"emails"`
 	InboundIds []int    `json:"inboundIds"`
 }
 
-func (a *ClientController) bulkAttach(c *gin.Context) {
-	var req bulkAttachRequest
+func (a *ClientController) bulkDetach(c *gin.Context) {
+	var req bulkDetachRequest
 	if err := c.ShouldBindJSON(&req); err != nil {
 		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
 		return
 	}
-	result, needRestart, err := a.clientService.BulkAttach(&a.inboundService, req.Emails, req.InboundIds)
+	result, needRestart, err := a.clientService.BulkDetach(&a.inboundService, req.Emails, req.InboundIds)
 	if err != nil {
 		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
 		return
@@ -447,25 +443,6 @@ func (a *ClientController) detach(c *gin.Context) {
 	notifyClientsChanged()
 }
 
-func (a *ClientController) listGroups(c *gin.Context) {
-	rows, err := a.clientService.ListGroups()
-	if err != nil {
-		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
-		return
-	}
-	jsonObj(c, rows, nil)
-}
-
-func (a *ClientController) groupEmails(c *gin.Context) {
-	name := c.Param("name")
-	emails, err := a.clientService.EmailsByGroup(name)
-	if err != nil {
-		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
-		return
-	}
-	jsonObj(c, emails, nil)
-}
-
 type bulkResetRequest struct {
 	Emails []string `json:"emails"`
 }
@@ -485,62 +462,3 @@ func (a *ClientController) bulkResetTraffic(c *gin.Context) {
 	a.xrayService.SetToNeedRestart()
 	notifyClientsChanged()
 }
-
-type groupCreateBody struct {
-	Name string `json:"name"`
-}
-
-func (a *ClientController) createGroup(c *gin.Context) {
-	var body groupCreateBody
-	if err := c.ShouldBindJSON(&body); err != nil {
-		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
-		return
-	}
-	if err := a.clientService.CreateGroup(body.Name); err != nil {
-		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
-		return
-	}
-	jsonObj(c, gin.H{"name": body.Name}, nil)
-	notifyClientsChanged()
-}
-
-type groupRenameBody struct {
-	OldName string `json:"oldName"`
-	NewName string `json:"newName"`
-}
-
-func (a *ClientController) renameGroup(c *gin.Context) {
-	var body groupRenameBody
-	if err := c.ShouldBindJSON(&body); err != nil {
-		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
-		return
-	}
-	affected, err := a.clientService.RenameGroup(body.OldName, body.NewName)
-	if err != nil {
-		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
-		return
-	}
-	a.xrayService.SetToNeedRestart()
-	jsonObj(c, gin.H{"affected": affected}, nil)
-	notifyClientsChanged()
-}
-
-type groupDeleteBody struct {
-	Name string `json:"name"`
-}
-
-func (a *ClientController) deleteGroup(c *gin.Context) {
-	var body groupDeleteBody
-	if err := c.ShouldBindJSON(&body); err != nil {
-		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
-		return
-	}
-	affected, err := a.clientService.DeleteGroup(body.Name)
-	if err != nil {
-		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
-		return
-	}
-	a.xrayService.SetToNeedRestart()
-	jsonObj(c, gin.H{"affected": affected}, nil)
-	notifyClientsChanged()
-}

+ 154 - 0
web/controller/group.go

@@ -0,0 +1,154 @@
+package controller
+
+import (
+	"strings"
+
+	"github.com/mhsanaei/3x-ui/v3/util/common"
+	"github.com/mhsanaei/3x-ui/v3/web/service"
+
+	"github.com/gin-gonic/gin"
+)
+
+type GroupController struct {
+	clientService service.ClientService
+	xrayService   service.XrayService
+}
+
+func NewGroupController(g *gin.RouterGroup) *GroupController {
+	a := &GroupController{}
+	a.initRouter(g)
+	return a
+}
+
+func (a *GroupController) initRouter(g *gin.RouterGroup) {
+	g.GET("/groups", a.list)
+	g.GET("/groups/:name/emails", a.emails)
+	g.POST("/groups/create", a.create)
+	g.POST("/groups/rename", a.rename)
+	g.POST("/groups/delete", a.delete)
+	g.POST("/groups/bulkAdd", a.bulkAdd)
+	g.POST("/groups/bulkRemove", a.bulkRemove)
+}
+
+func (a *GroupController) list(c *gin.Context) {
+	rows, err := a.clientService.ListGroups()
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	jsonObj(c, rows, nil)
+}
+
+func (a *GroupController) emails(c *gin.Context) {
+	name := c.Param("name")
+	emails, err := a.clientService.EmailsByGroup(name)
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	jsonObj(c, emails, nil)
+}
+
+type groupCreateBody struct {
+	Name string `json:"name"`
+}
+
+func (a *GroupController) create(c *gin.Context) {
+	var body groupCreateBody
+	if err := c.ShouldBindJSON(&body); err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	if err := a.clientService.CreateGroup(body.Name); err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	jsonObj(c, gin.H{"name": body.Name}, nil)
+	notifyClientsChanged()
+}
+
+type groupRenameBody struct {
+	OldName string `json:"oldName"`
+	NewName string `json:"newName"`
+}
+
+func (a *GroupController) rename(c *gin.Context) {
+	var body groupRenameBody
+	if err := c.ShouldBindJSON(&body); err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	affected, err := a.clientService.RenameGroup(body.OldName, body.NewName)
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	a.xrayService.SetToNeedRestart()
+	jsonObj(c, gin.H{"affected": affected}, nil)
+	notifyClientsChanged()
+}
+
+type groupDeleteBody struct {
+	Name string `json:"name"`
+}
+
+func (a *GroupController) delete(c *gin.Context) {
+	var body groupDeleteBody
+	if err := c.ShouldBindJSON(&body); err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	affected, err := a.clientService.DeleteGroup(body.Name)
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	a.xrayService.SetToNeedRestart()
+	jsonObj(c, gin.H{"affected": affected}, nil)
+	notifyClientsChanged()
+}
+
+type bulkAddToGroupRequest struct {
+	Emails []string `json:"emails"`
+	Group  string   `json:"group"`
+}
+
+func (a *GroupController) bulkAdd(c *gin.Context) {
+	var req bulkAddToGroupRequest
+	if err := c.ShouldBindJSON(&req); err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	if strings.TrimSpace(req.Group) == "" {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("group name is required"))
+		return
+	}
+	affected, err := a.clientService.AddToGroup(req.Emails, req.Group)
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	jsonObj(c, gin.H{"affected": affected}, nil)
+	a.xrayService.SetToNeedRestart()
+	notifyClientsChanged()
+}
+
+type bulkRemoveFromGroupRequest struct {
+	Emails []string `json:"emails"`
+}
+
+func (a *GroupController) bulkRemove(c *gin.Context) {
+	var req bulkRemoveFromGroupRequest
+	if err := c.ShouldBindJSON(&req); err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	affected, err := a.clientService.RemoveFromGroup(req.Emails)
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	jsonObj(c, gin.H{"affected": affected}, nil)
+	a.xrayService.SetToNeedRestart()
+	notifyClientsChanged()
+}

+ 126 - 24
web/service/client.go

@@ -687,7 +687,7 @@ func (s *ClientService) Delete(inboundSvc *InboundService, id int, keepTraffic b
 		if key == "" {
 			continue
 		}
-		nr, delErr := s.DelInboundClient(inboundSvc, ibId, key)
+		nr, delErr := s.DelInboundClient(inboundSvc, ibId, key, false)
 		if delErr != nil {
 			return needRestart, delErr
 		}
@@ -808,6 +808,12 @@ func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string,
 		return result, false, nil
 	}
 
+	recordErr := func(format string, args ...any) {
+		msg := fmt.Sprintf(format, args...)
+		result.Errors = append(result.Errors, msg)
+		logger.Warningf("[BulkAttach] %s", msg)
+	}
+
 	records := make([]*model.ClientRecord, 0, len(emails))
 	seenEmail := make(map[string]struct{}, len(emails))
 	for _, email := range emails {
@@ -821,7 +827,7 @@ func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string,
 		seenEmail[key] = struct{}{}
 		rec, err := s.GetRecordByEmail(nil, email)
 		if err != nil {
-			result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", email, err))
+			recordErr("%s: %v", email, err)
 			continue
 		}
 		records = append(records, rec)
@@ -831,12 +837,12 @@ func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string,
 	for _, ibId := range inboundIds {
 		inbound, err := inboundSvc.GetInbound(ibId)
 		if err != nil {
-			result.Errors = append(result.Errors, fmt.Sprintf("inbound %d: %v", ibId, err))
+			recordErr("inbound %d: %v", ibId, err)
 			continue
 		}
 		existingClients, err := inboundSvc.GetClients(inbound)
 		if err != nil {
-			result.Errors = append(result.Errors, fmt.Sprintf("inbound %d: %v", ibId, err))
+			recordErr("inbound %d: %v", ibId, err)
 			continue
 		}
 		have := make(map[string]struct{}, len(existingClients))
@@ -853,7 +859,7 @@ func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string,
 			client := *rec.ToClient()
 			client.UpdatedAt = time.Now().UnixMilli()
 			if err := s.fillProtocolDefaults(&client, inbound); err != nil {
-				result.Errors = append(result.Errors, fmt.Sprintf("%s -> inbound %d: %v", rec.Email, ibId, err))
+				recordErr("%s -> inbound %d: %v", rec.Email, ibId, err)
 				continue
 			}
 			clientsToAdd = append(clientsToAdd, client)
@@ -865,12 +871,12 @@ func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string,
 
 		payload, err := json.Marshal(map[string][]model.Client{"clients": clientsToAdd})
 		if err != nil {
-			result.Errors = append(result.Errors, fmt.Sprintf("inbound %d: %v", ibId, err))
+			recordErr("inbound %d: %v", ibId, err)
 			continue
 		}
 		nr, err := s.AddInboundClient(inboundSvc, &model.Inbound{Id: ibId, Settings: string(payload)})
 		if err != nil {
-			result.Errors = append(result.Errors, fmt.Sprintf("inbound %d: %v", ibId, err))
+			recordErr("inbound %d: %v", ibId, err)
 			continue
 		}
 		if nr {
@@ -884,6 +890,81 @@ func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string,
 	return result, needRestart, nil
 }
 
+// BulkDetachResult reports the outcome of a bulk detach across target inbounds.
+type BulkDetachResult struct {
+	Detached []string `json:"detached"`
+	Skipped  []string `json:"skipped"`
+	Errors   []string `json:"errors"`
+}
+
+// BulkDetach detaches the given existing clients (by email) from each target inbound.
+// (email, inbound) pairs where the client is not currently attached are silently skipped
+// at the inbound level; emails that aren't attached to any of the requested inbounds
+// are reported under skipped. ClientRecord rows are kept even when they become orphaned
+// (matches single-client detach semantics); callers should use bulkDelete for full removal.
+func (s *ClientService) BulkDetach(inboundSvc *InboundService, emails []string, inboundIds []int) (*BulkDetachResult, bool, error) {
+	result := &BulkDetachResult{}
+	if len(emails) == 0 || len(inboundIds) == 0 {
+		return result, false, nil
+	}
+
+	recordErr := func(format string, args ...any) {
+		msg := fmt.Sprintf(format, args...)
+		result.Errors = append(result.Errors, msg)
+		logger.Warningf("[BulkDetach] %s", msg)
+	}
+
+	requested := make(map[int]struct{}, len(inboundIds))
+	for _, id := range inboundIds {
+		requested[id] = struct{}{}
+	}
+
+	needRestart := false
+	seenEmail := make(map[string]struct{}, len(emails))
+	for _, email := range emails {
+		if email == "" {
+			continue
+		}
+		key := strings.ToLower(email)
+		if _, ok := seenEmail[key]; ok {
+			continue
+		}
+		seenEmail[key] = struct{}{}
+
+		rec, err := s.GetRecordByEmail(nil, email)
+		if err != nil {
+			recordErr("%s: %v", email, err)
+			continue
+		}
+		currentIds, err := s.GetInboundIdsForRecord(rec.Id)
+		if err != nil {
+			recordErr("%s: %v", email, err)
+			continue
+		}
+		intersection := make([]int, 0, len(currentIds))
+		for _, id := range currentIds {
+			if _, ok := requested[id]; ok {
+				intersection = append(intersection, id)
+			}
+		}
+		if len(intersection) == 0 {
+			result.Skipped = append(result.Skipped, rec.Email)
+			continue
+		}
+		nr, err := s.Detach(inboundSvc, rec.Id, intersection)
+		if err != nil {
+			recordErr("%s: %v", rec.Email, err)
+			continue
+		}
+		if nr {
+			needRestart = true
+		}
+		result.Detached = append(result.Detached, rec.Email)
+	}
+
+	return result, needRestart, nil
+}
+
 func (s *ClientService) DetachByEmailMany(inboundSvc *InboundService, email string, inboundIds []int) (bool, error) {
 	if email == "" {
 		return false, common.NewError("client email is required")
@@ -915,7 +996,7 @@ func (s *ClientService) DeleteByEmail(inboundSvc *InboundService, email string,
 	}
 	needRestart := false
 	for _, ibId := range inboundIds {
-		nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, email)
+		nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, email, false)
 		if delErr != nil {
 			return needRestart, delErr
 		}
@@ -1333,7 +1414,11 @@ func (s *ClientService) DeleteGroup(name string) (int, error) {
 	return s.replaceGroupValue(name, "")
 }
 
-func (s *ClientService) AssignGroup(emails []string, group string) (int, error) {
+func (s *ClientService) RemoveFromGroup(emails []string) (int, error) {
+	return s.AddToGroup(emails, "")
+}
+
+func (s *ClientService) AddToGroup(emails []string, group string) (int, error) {
 	group = strings.TrimSpace(group)
 	if len(emails) == 0 {
 		return 0, nil
@@ -2320,7 +2405,7 @@ func (s *ClientService) BulkDelete(inboundSvc *InboundService, emails []string,
 
 	needRestart := false
 	for inboundId, ibEmails := range emailsByInbound {
-		ibResult := s.bulkDelInboundClients(inboundSvc, inboundId, ibEmails, recordsByEmail)
+		ibResult := s.bulkDelInboundClients(inboundSvc, inboundId, ibEmails, recordsByEmail, false)
 		if ibResult.needRestart {
 			needRestart = true
 		}
@@ -2380,6 +2465,7 @@ func (s *ClientService) bulkDelInboundClients(
 	inboundId int,
 	emails []string,
 	records map[string]*model.ClientRecord,
+	keepTraffic bool,
 ) bulkInboundDeleteResult {
 	res := bulkInboundDeleteResult{perEmailSkipped: map[string]string{}}
 
@@ -2501,7 +2587,7 @@ func (s *ClientService) bulkDelInboundClients(
 			delete(foundEmails, email)
 			continue
 		}
-		if shared {
+		if shared || keepTraffic {
 			continue
 		}
 		if delErr := inboundSvc.DelClientIPs(db, email); delErr != nil {
@@ -2734,7 +2820,7 @@ func (s *ClientService) Detach(inboundSvc *InboundService, id int, inboundIds []
 		if key == "" {
 			continue
 		}
-		nr, delErr := s.DelInboundClient(inboundSvc, ibId, key)
+		nr, delErr := s.DelInboundClient(inboundSvc, ibId, key, true)
 		if delErr != nil {
 			return needRestart, delErr
 		}
@@ -2794,6 +2880,10 @@ func (s *ClientService) AddInboundClient(inboundSvc *InboundService, data *model
 				cm["created_at"] = nowTs
 			}
 			cm["updated_at"] = nowTs
+			existingSub, _ := cm["subId"].(string)
+			if strings.TrimSpace(existingSub) == "" {
+				cm["subId"] = random.NumLower(16)
+			}
 			interfaceClients[i] = cm
 		}
 	}
@@ -3032,11 +3122,13 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
 	}
 	settingsClients := oldSettings["clients"].([]any)
 	var preservedCreated any
+	var preservedSubID string
 	if clientIndex >= 0 && clientIndex < len(settingsClients) {
 		if oldMap, ok := settingsClients[clientIndex].(map[string]any); ok {
 			if v, ok2 := oldMap["created_at"]; ok2 {
 				preservedCreated = v
 			}
+			preservedSubID, _ = oldMap["subId"].(string)
 		}
 	}
 	if len(interfaceClients) > 0 {
@@ -3046,6 +3138,14 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
 			}
 			newMap["created_at"] = preservedCreated
 			newMap["updated_at"] = time.Now().Unix() * 1000
+			newSub, _ := newMap["subId"].(string)
+			if strings.TrimSpace(newSub) == "" {
+				if strings.TrimSpace(preservedSubID) != "" {
+					newMap["subId"] = preservedSubID
+				} else {
+					newMap["subId"] = random.NumLower(16)
+				}
+			}
 			interfaceClients[0] = newMap
 		}
 	}
@@ -3209,7 +3309,7 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
 	return needRestart, nil
 }
 
-func (s *ClientService) DelInboundClient(inboundSvc *InboundService, inboundId int, clientId string) (bool, error) {
+func (s *ClientService) DelInboundClient(inboundSvc *InboundService, inboundId int, clientId string, keepTraffic bool) (bool, error) {
 	defer lockInbound(inboundId).Unlock()
 
 	oldInbound, err := inboundSvc.GetInbound(inboundId)
@@ -3272,7 +3372,7 @@ func (s *ClientService) DelInboundClient(inboundSvc *InboundService, inboundId i
 		return false, err
 	}
 
-	if !emailShared {
+	if !emailShared && !keepTraffic {
 		err = inboundSvc.DelClientIPs(db, email)
 		if err != nil {
 			logger.Error("Error in delete client IPs")
@@ -3289,7 +3389,7 @@ func (s *ClientService) DelInboundClient(inboundSvc *InboundService, inboundId i
 			return false, err
 		}
 		notDepleted := len(enables) > 0 && enables[0]
-		if !emailShared {
+		if !emailShared && !keepTraffic {
 			err = inboundSvc.DelClientStat(db, email)
 			if err != nil {
 				logger.Error("Delete stats Data Error")
@@ -3336,7 +3436,7 @@ func (s *ClientService) DelInboundClient(inboundSvc *InboundService, inboundId i
 	return needRestart, nil
 }
 
-func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inboundId int, email string) (bool, error) {
+func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inboundId int, email string, keepTraffic bool) (bool, error) {
 	defer lockInbound(inboundId).Unlock()
 
 	oldInbound, err := inboundSvc.GetInbound(inboundId)
@@ -3393,7 +3493,7 @@ func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inbo
 		return false, err
 	}
 
-	if !emailShared {
+	if !emailShared && !keepTraffic {
 		if err := inboundSvc.DelClientIPs(db, email); err != nil {
 			logger.Error("Error in delete client IPs")
 			return false, err
@@ -3403,15 +3503,17 @@ func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inbo
 	needRestart := false
 
 	if len(email) > 0 && !emailShared {
-		traffic, err := inboundSvc.GetClientTrafficByEmail(email)
-		if err != nil {
-			return false, err
-		}
-		if traffic != nil {
-			if err := inboundSvc.DelClientStat(db, email); err != nil {
-				logger.Error("Delete stats Data Error")
+		if !keepTraffic {
+			traffic, err := inboundSvc.GetClientTrafficByEmail(email)
+			if err != nil {
 				return false, err
 			}
+			if traffic != nil {
+				if err := inboundSvc.DelClientStat(db, email); err != nil {
+					logger.Error("Delete stats Data Error")
+					return false, err
+				}
+			}
 		}
 
 		if needApiDel {

+ 4 - 1
web/service/inbound.go

@@ -2988,10 +2988,13 @@ func (s *InboundService) MigrationRequirements() {
 	for inbound_index := range inbounds {
 		settings := map[string]any{}
 		json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
+		if raw, exists := settings["clients"]; exists && raw == nil {
+			settings["clients"] = []any{}
+		}
 		clients, ok := settings["clients"].([]any)
 		if ok {
 			// Fix Client configuration problems
-			var newClients []any
+			newClients := make([]any, 0, len(clients))
 			hasVisionFlow := false
 			for client_index := range clients {
 				c := clients[client_index].(map[string]any)

+ 3 - 111
web/service/port_conflict.go

@@ -10,9 +10,6 @@ import (
 	"github.com/mhsanaei/3x-ui/v3/util/common"
 )
 
-// transportBits is a bitmask of L4 transports an inbound listens on.
-// 0.0.0.0:443/tcp and 0.0.0.0:443/udp are independent sockets in linux,
-// so the conflict check needs more than just the port number.
 type transportBits uint8
 
 const (
@@ -20,19 +17,6 @@ const (
 	transportUDP
 )
 
-// inboundTransports returns the L4 transports the given inbound listens on.
-// always returns at least one bit (falls back to tcp on parse errors), so
-// no parse failure can silently let a real socket collision through.
-//
-// the rules:
-//   - hysteria, wireguard: udp regardless of streamSettings
-//   - streamSettings.network=kcp or quic: udp (both ride on udp at L4)
-//   - shadowsocks: settings.network ("tcp" / "udp" / "tcp,udp"), overrides
-//     the streamSettings-derived bit when present
-//   - tunnel (xray dokodemo-door): same shape via settings.allowedNetwork
-//     (3x-ui's wrapper renames the field)
-//   - mixed (socks/http combo): tcp + udp when settings.udp is true
-//   - everything else: tcp
 func inboundTransports(protocol model.Protocol, streamSettings, settings string) transportBits {
 	// protocols that ignore streamSettings entirely.
 	switch protocol {
@@ -69,11 +53,6 @@ func inboundTransports(protocol model.Protocol, streamSettings, settings string)
 		if json.Unmarshal([]byte(settings), &st) == nil {
 			switch protocol {
 			case model.Shadowsocks, model.Tunnel:
-				// shadowsocks exposes settings.network, tunnel exposes
-				// settings.allowedNetwork (3x-ui's wrapper around xray's
-				// dokodemo-door). both carry "tcp" / "udp" / "tcp,udp"
-				// and, when present, win outright over the streamSettings-
-				// derived default; absent/empty keeps the inferred bit (tcp).
 				key := "network"
 				if protocol == model.Tunnel {
 					key = "allowedNetwork"
@@ -106,10 +85,6 @@ func inboundTransports(protocol model.Protocol, streamSettings, settings string)
 	return bits
 }
 
-// listenOverlaps reports whether two listen addresses can collide on the
-// same port. preserves the rule from the original checkPortExist:
-// any-address (empty / 0.0.0.0 / :: / ::0) overlaps with everything,
-// otherwise only identical specific addresses overlap.
 func listenOverlaps(a, b string) bool {
 	if isAnyListen(a) || isAnyListen(b) {
 		return true
@@ -121,12 +96,6 @@ func isAnyListen(s string) bool {
 	return s == "" || s == "0.0.0.0" || s == "::" || s == "::0"
 }
 
-// portConflictDetail describes the existing inbound that an add/update
-// would collide with. it carries enough context for the API layer to
-// render a user-actionable error ("port 443 (tcp) already used by
-// inbound 'my-vless' (#7) on *") instead of the historical opaque
-// "Port exists". Transports holds only the bits the two inbounds
-// actually share, not the existing inbound's full transport mask.
 type portConflictDetail struct {
 	InboundID  int
 	Remark     string
@@ -155,22 +124,6 @@ func (d *portConflictDetail) String() string {
 		d.Port, transportTagSuffix(d.Transports), name, listen)
 }
 
-// checkPortConflict reports the existing inbound (if any) that adding
-// or updating an inbound on (listen, port) would clash with. nil result
-// means no conflict.
-//
-// the check understands that tcp/443 and udp/443 are independent
-// sockets in linux and may coexist on the same address (see
-// inboundTransports for the per-protocol L4 mapping).
-//
-// node scope: inbounds with different NodeID run on different physical
-// machines (local panel xray vs a remote node, or two remote nodes),
-// so their sockets can't collide. only candidates with the same NodeID
-// participate in the listen/transport overlap check.
-//
-// listen overlap: a specific listen address conflicts with any-address
-// on the same port (both directions), otherwise only identical specific
-// addresses overlap.
 func (s *InboundService) checkPortConflict(inbound *model.Inbound, ignoreId int) (*portConflictDetail, error) {
 	db := database.GetDB()
 
@@ -208,11 +161,6 @@ func (s *InboundService) checkPortConflict(inbound *model.Inbound, ignoreId int)
 	return nil, nil
 }
 
-// sameNode reports whether two NodeID pointers refer to the same xray
-// process. nil/nil means both inbounds run on the local panel; non-nil
-// with equal value means they share the same remote node. any mix
-// (local vs remote, remote-A vs remote-B) is "different node" and
-// can't produce a real socket collision.
 func sameNode(a, b *int) bool {
 	if a == nil && b == nil {
 		return true
@@ -223,9 +171,6 @@ func sameNode(a, b *int) bool {
 	return *a == *b
 }
 
-// baseInboundTag is the "in-<port>" / "in-<listen>:<port>" core used
-// by composeInboundTag and as a probe shape in setRemoteTrafficLocked
-// for node-side xray imports that pre-date the canonical naming.
 func baseInboundTag(listen string, port int) string {
 	if isAnyListen(listen) {
 		return fmt.Sprintf("in-%v", port)
@@ -255,53 +200,13 @@ func nodeTagPrefix(nodeID *int) string {
 	return fmt.Sprintf("n%d-", *nodeID)
 }
 
-// protocolShortName collapses the full protocol identifier into a 2–4
-// char tag-friendly token (shadowsocks → ss, wireguard → wg, …). Falls
-// back to the raw identifier for anything not in the table so future
-// protocols don't need a code change just to get a tag.
-func protocolShortName(p model.Protocol) string {
-	switch p {
-	case model.VMESS:
-		return "vm"
-	case model.VLESS:
-		return "vl"
-	case model.Trojan:
-		return "tr"
-	case model.Shadowsocks:
-		return "ss"
-	case model.Mixed:
-		return "mx"
-	case model.WireGuard:
-		return "wg"
-	case model.Hysteria:
-		return "hy"
-	case model.Tunnel:
-		return "tn"
-	case model.HTTP:
-		return "http"
-	}
-	if p == "" {
-		return "any"
-	}
-	return string(p)
-}
-
-// composeInboundTag returns the canonical
-// "[n<id>-]inbound-[<listen>:]<port>-<protocol>-<network>" shape used
-// for every newly created inbound. The protocol + network segments
-// disambiguate tcp/443 and udp/443 sharing a listener; the node prefix
-// lets the same port live on local + node.
-func composeInboundTag(listen string, port int, protocol model.Protocol, nodeID *int, bits transportBits) string {
-	return nodeTagPrefix(nodeID) + baseInboundTag(listen, port) + "-" + protocolShortName(protocol) + "-" + transportTagSuffix(bits)
+func composeInboundTag(listen string, port int, nodeID *int, bits transportBits) string {
+	return nodeTagPrefix(nodeID) + baseInboundTag(listen, port) + "-" + transportTagSuffix(bits)
 }
 
-// generateInboundTag returns a free tag in the canonical shape. ignoreId
-// is the inbound's own id on update so it doesn't see itself as taken;
-// pass 0 on add. Numeric suffix fallback is defensive — the port check
-// should have already blocked an exact-collision insert.
 func (s *InboundService) generateInboundTag(inbound *model.Inbound, ignoreId int) (string, error) {
 	bits := inboundTransports(inbound.Protocol, inbound.StreamSettings, inbound.Settings)
-	candidate := composeInboundTag(inbound.Listen, inbound.Port, inbound.Protocol, inbound.NodeID, bits)
+	candidate := composeInboundTag(inbound.Listen, inbound.Port, inbound.NodeID, bits)
 	exists, err := s.tagExists(candidate, ignoreId)
 	if err != nil {
 		return "", err
@@ -323,19 +228,6 @@ func (s *InboundService) generateInboundTag(inbound *model.Inbound, ignoreId int
 	return "", common.NewError("could not pick a unique inbound tag for port:", inbound.Port)
 }
 
-// resolveInboundTag chooses a tag for an Add or Update. when the caller
-// supplied a non-empty Tag (e.g. the central panel pushed its picked
-// tag to a node during a multi-node sync) and that tag is free in the
-// local DB, it's used verbatim so the two panels stay in agreement —
-// otherwise the node would regenerate (often back to bare
-// "inbound-<port>") and the eventual traffic sync-back would try to
-// INSERT a row whose tag already exists, hitting the UNIQUE constraint
-// on inbounds.tag and rolling the node-side row right back out.
-// when Tag is empty (the common UI path) or collides, fall back to the
-// transport-aware generateInboundTag.
-//
-// ignoreId mirrors generateInboundTag: pass 0 on add, the inbound's
-// own id on update so a row doesn't see its own current tag as taken.
 func (s *InboundService) resolveInboundTag(inbound *model.Inbound, ignoreId int) (string, error) {
 	if inbound.Tag != "" {
 		taken, err := s.tagExists(inbound.Tag, ignoreId)

+ 23 - 23
web/service/port_conflict_test.go

@@ -285,13 +285,13 @@ func TestGenerateInboundTag_DisambiguatesByTransportOnSamePort(t *testing.T) {
 	if err != nil {
 		t.Fatalf("generateInboundTag: %v", err)
 	}
-	if got != "in-443-hy-udp" {
-		t.Fatalf("expected in-443-hy-udp, got %q", got)
+	if got != "in-443-udp" {
+		t.Fatalf("expected in-443-udp, got %q", got)
 	}
 }
 
-// when the port is free, the canonical tag carries protocol + transport
-// so tcp/8443 and udp/8443 get distinct tags out of the box.
+// when the port is free, the canonical tag carries the transport so
+// tcp/8443 and udp/8443 get distinct tags out of the box.
 func TestGenerateInboundTag_KeepsBaseTagWhenFree(t *testing.T) {
 	setupConflictDB(t)
 
@@ -305,8 +305,8 @@ func TestGenerateInboundTag_KeepsBaseTagWhenFree(t *testing.T) {
 	if err != nil {
 		t.Fatalf("generateInboundTag: %v", err)
 	}
-	if got != "in-8443-vl-tcp" {
-		t.Fatalf("expected in-8443-vl-tcp, got %q", got)
+	if got != "in-8443-tcp" {
+		t.Fatalf("expected in-8443-tcp, got %q", got)
 	}
 }
 
@@ -314,10 +314,10 @@ func TestGenerateInboundTag_KeepsBaseTagWhenFree(t *testing.T) {
 // that's what ignoreId is for.
 func TestGenerateInboundTag_IgnoresSelfOnUpdate(t *testing.T) {
 	setupConflictDB(t)
-	seedInboundConflict(t, "in-443-vl-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`)
+	seedInboundConflict(t, "in-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`)
 
 	var existing model.Inbound
-	if err := database.GetDB().Where("tag = ?", "in-443-vl-tcp").First(&existing).Error; err != nil {
+	if err := database.GetDB().Where("tag = ?", "in-443-tcp").First(&existing).Error; err != nil {
 		t.Fatalf("read seeded row: %v", err)
 	}
 
@@ -326,7 +326,7 @@ func TestGenerateInboundTag_IgnoresSelfOnUpdate(t *testing.T) {
 	if err != nil {
 		t.Fatalf("generateInboundTag: %v", err)
 	}
-	if got != "in-443-vl-tcp" {
+	if got != "in-443-tcp" {
 		t.Fatalf("self-update must keep base tag, got %q", got)
 	}
 }
@@ -346,8 +346,8 @@ func TestGenerateInboundTag_SpecificListenSameDisambiguation(t *testing.T) {
 	if err != nil {
 		t.Fatalf("generateInboundTag: %v", err)
 	}
-	if got != "in-1.2.3.4:443-hy-udp" {
-		t.Fatalf("expected in-1.2.3.4:443-hy-udp, got %q", got)
+	if got != "in-1.2.3.4:443-udp" {
+		t.Fatalf("expected in-1.2.3.4:443-udp, got %q", got)
 	}
 }
 
@@ -399,8 +399,8 @@ func TestCheckPortConflict_NodeScope(t *testing.T) {
 // panels diverged, causing a UNIQUE constraint failure on sync.
 func TestResolveInboundTag_RespectsCallerTagWhenFree(t *testing.T) {
 	setupConflictDB(t)
-	seedInboundConflictNode(t, "in-5000-vl-tcp", "0.0.0.0", 5000, model.VLESS, `{"network":"tcp"}`, `{}`, nil)
-	seedInboundConflictNode(t, "in-5000-hy-udp", "0.0.0.0", 5000, model.Hysteria, ``, ``, nil)
+	seedInboundConflictNode(t, "in-5000-tcp", "0.0.0.0", 5000, model.VLESS, `{"network":"tcp"}`, `{}`, nil)
+	seedInboundConflictNode(t, "in-5000-udp", "0.0.0.0", 5000, model.Hysteria, ``, ``, nil)
 
 	svc := &InboundService{}
 	pushed := &model.Inbound{
@@ -436,8 +436,8 @@ func TestResolveInboundTag_GeneratesWhenTagEmpty(t *testing.T) {
 	if err != nil {
 		t.Fatalf("resolveInboundTag: %v", err)
 	}
-	if got != "in-8443-vl-tcp" {
-		t.Fatalf("expected generated in-8443-vl-tcp, got %q", got)
+	if got != "in-8443-tcp" {
+		t.Fatalf("expected generated in-8443-tcp, got %q", got)
 	}
 }
 
@@ -448,11 +448,11 @@ func TestResolveInboundTag_GeneratesWhenTagEmpty(t *testing.T) {
 // tag that the central will pick up via the AddInbound response.
 func TestResolveInboundTag_RegeneratesOnCollision(t *testing.T) {
 	setupConflictDB(t)
-	seedInboundConflictNode(t, "in-5000-vl-tcp", "0.0.0.0", 5000, model.VLESS, `{"network":"tcp"}`, `{}`, nil)
+	seedInboundConflictNode(t, "in-5000-tcp", "0.0.0.0", 5000, model.VLESS, `{"network":"tcp"}`, `{}`, nil)
 
 	svc := &InboundService{}
 	pushed := &model.Inbound{
-		Tag:            "in-5000-vl-tcp",
+		Tag:            "in-5000-tcp",
 		Listen:         "0.0.0.0",
 		Port:           5000,
 		Protocol:       model.Hysteria,
@@ -463,7 +463,7 @@ func TestResolveInboundTag_RegeneratesOnCollision(t *testing.T) {
 	if err != nil {
 		t.Fatalf("resolveInboundTag: %v", err)
 	}
-	if got == "in-5000-vl-tcp" {
+	if got == "in-5000-tcp" {
 		t.Fatalf("colliding caller tag must be replaced, but resolver kept %q", got)
 	}
 }
@@ -486,8 +486,8 @@ func TestGenerateInboundTag_NodePrefix(t *testing.T) {
 	if err != nil {
 		t.Fatalf("generateInboundTag: %v", err)
 	}
-	if got != "n1-in-443-vl-tcp" {
-		t.Fatalf("expected n1-in-443-vl-tcp, got %q", got)
+	if got != "n1-in-443-tcp" {
+		t.Fatalf("expected n1-in-443-tcp, got %q", got)
 	}
 }
 
@@ -495,7 +495,7 @@ func TestGenerateInboundTag_NodePrefix(t *testing.T) {
 // the prefix scopes the tag to that specific node.
 func TestGenerateInboundTag_NodePrefixedDoesNotCollideWithLocal(t *testing.T) {
 	setupConflictDB(t)
-	seedInboundConflict(t, "in-443-vl-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`)
+	seedInboundConflict(t, "in-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`)
 
 	svc := &InboundService{}
 	in := &model.Inbound{
@@ -508,8 +508,8 @@ func TestGenerateInboundTag_NodePrefixedDoesNotCollideWithLocal(t *testing.T) {
 	if err != nil {
 		t.Fatalf("generateInboundTag: %v", err)
 	}
-	if got != "n1-in-443-vl-tcp" {
-		t.Fatalf("expected n1-in-443-vl-tcp, got %q", got)
+	if got != "n1-in-443-tcp" {
+		t.Fatalf("expected n1-in-443-tcp, got %q", got)
 	}
 }
 

+ 1 - 1
web/service/setting.go

@@ -41,7 +41,7 @@ var defaultValueMap = map[string]string{
 	"pageSize":                    "25",
 	"expireDiff":                  "0",
 	"trafficDiff":                 "0",
-	"remarkModel":                 "-io",
+	"remarkModel":                 "-ieo",
 	"timeLocation":                "Local",
 	"tgBotEnable":                 "false",
 	"tgBotToken":                  "",

+ 1 - 1
web/service/tgbot.go

@@ -2411,7 +2411,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
 	}
 
 	// Inform user
-	t.SendMsgToTgbot(chatId, "QRCode"+":")
+	t.SendMsgToTgbot(chatId, "QRCode for client "+email+":")
 
 	// Send sub URL QR (filename: sub.png)
 	if png, err := createQR(subURL, 320); err == nil {

+ 502 - 79
web/translation/ar-EG.json

@@ -8,15 +8,22 @@
   "save": "حفظ",
   "logout": "تسجيل خروج",
   "create": "إنشاء",
+  "add": "إضافة",
+  "remove": "إزالة",
   "update": "تحديث",
   "copy": "نسخ",
   "copied": "اتنسخ",
+  "more": "المزيد",
   "download": "تحميل",
   "remark": "ملاحظة",
   "enable": "مفعل",
   "protocol": "بروتوكول",
   "search": "بحث",
-  "filter": "فلترة",
+  "filter": "تصفية",
+  "all": "الكل",
+  "from": "من",
+  "to": "إلى",
+  "done": "تم",
   "loading": "جاري التحميل...",
   "refresh": "تحديث",
   "clear": "مسح",
@@ -27,28 +34,28 @@
   "check": "شيك",
   "indefinite": "غير محدد",
   "unlimited": "غير محدود",
-  "none": "مفيش",
+  "none": "لا شيء",
   "qrCode": "كود QR",
   "info": "معلومات أكتر",
-  "edit": "تعديل",
+  "edit": "تحرير",
   "delete": "مسح",
-  "reset": "إعادة ضبط",
+  "reset": "إعادة تعيين",
   "noData": "لا توجد بيانات.",
   "copySuccess": "اتنسخ بنجاح",
   "sure": "متأكد؟",
   "encryption": "تشفير",
   "useIPv4ForHost": "استخدم IPv4 للمضيف",
   "transmission": "نقل",
-  "host": "المستضيف",
-  "path": "مسار",
+  "host": "المضيف",
+  "path": "المسار",
   "camouflage": "تمويه",
   "status": "الحالة",
   "enabled": "مفعل",
   "disabled": "معطل",
   "depleted": "خلص",
   "depletingSoon": "هينتهي قريب",
-  "offline": "أوفلاين",
-  "online": "أونلاين",
+  "offline": "غير متصل",
+  "online": "متصل",
   "domainName": "اسم الدومين",
   "monitor": "المسمع IP",
   "certificate": "شهادة رقمية",
@@ -95,10 +102,11 @@
     "dark": "داكن",
     "ultraDark": "داكن جدًا",
     "dashboard": "نظرة عامة",
-    "inbounds": "الإدخالات",
+    "inbounds": "الواردات",
     "clients": "العملاء",
+    "groups": "المجموعات",
     "nodes": "النودز",
-    "settings": "إعدادات البانل",
+    "settings": "إعدادات اللوحة",
     "xray": "إعدادات Xray",
     "apiDocs": "توثيق API",
     "logout": "تسجيل خروج",
@@ -120,13 +128,13 @@
     },
     "index": {
       "title": "نظرة عامة",
-      "cpu": "المعالج",
+      "cpu": "CPU",
       "logicalProcessors": "المعالجات المنطقية",
       "frequency": "التردد",
       "swap": "Swap",
       "storage": "تخزين",
-      "memory": "رام",
-      "threads": "خيوط المعالجة",
+      "memory": "RAM",
+      "threads": "خيوط",
       "xrayStatus": "Xray",
       "stopXray": "إيقاف",
       "restartXray": "إعادة تشغيل",
@@ -143,7 +151,7 @@
       "xrayStatusUnknown": "مش معروف",
       "xrayStatusRunning": "شغالة",
       "xrayStatusStop": "متوقفة",
-      "xrayStatusError": "فيها غلطة",
+      "xrayStatusError": "خطأ",
       "xrayErrorPopoverTitle": "حصل خطأ أثناء تشغيل Xray",
       "operationHours": "مدة التشغيل",
       "systemHistoryTitle": "تاريخ النظام",
@@ -186,14 +194,14 @@
       "customGeoTitle": "GeoSite / GeoIP مخصص",
       "customGeoAdd": "إضافة",
       "customGeoType": "النوع",
-      "customGeoAlias": "الاسم المستعار",
+      "customGeoAlias": "اسم مستعار",
       "customGeoUrl": "URL",
       "customGeoEnabled": "مفعّل",
       "customGeoLastUpdated": "آخر تحديث",
       "customGeoExtColumn": "التوجيه (ext:…)",
       "customGeoToastUpdateAll": "تم تحديث جميع المصادر المخصصة",
       "customGeoActions": "إجراءات",
-      "customGeoEdit": "تعديل",
+      "customGeoEdit": "تحرير",
       "customGeoDelete": "حذف",
       "customGeoDownload": "تحديث الآن",
       "customGeoModalAdd": "إضافة geo مخصص",
@@ -228,7 +236,7 @@
       "dontRefresh": "التثبيت شغال، متعملش Refresh للصفحة",
       "logs": "السجلات",
       "config": "الإعدادات",
-      "backup": "نسخة احتياطية",
+      "backup": "نسخ احتياطي",
       "backupTitle": "نسخ احتياطي واستعادة",
       "exportDatabase": "اخزن نسخة",
       "exportDatabaseDesc": "اضغط عشان تحمل ملف .db يحتوي على نسخة احتياطية لقاعدة البيانات الحالية على جهازك.",
@@ -241,18 +249,18 @@
       "getConfigError": "حدث خطأ أثناء استرجاع ملف الإعدادات"
     },
     "inbounds": {
-      "title": "الإدخالات",
+      "title": "الواردات",
       "totalDownUp": "إجمالي المرسل/المستقبل",
       "totalUsage": "إجمالي الاستخدام",
       "inboundCount": "عدد الإدخالات",
       "operate": "القائمة",
       "enable": "مفعل",
       "remark": "ملاحظة",
-      "node": "نود",
+      "node": "العقدة",
       "deployTo": "نشر على",
       "localPanel": "بانل محلي",
       "fallbacks": {
-        "title": "الـ Fallbacks",
+        "title": "Fallbacks",
         "help": "عند وصول اتصال إلى هذا الـ inbound لا يطابق أي عميل، يتم توجيهه إلى inbound آخر. اختر فرعًا أدناه وسيتم ملء حقول التوجيه (SNI / ALPN / Path / xver) تلقائيًا من نقل الفرع — في الغالب لا تحتاج إلى أي تعديل إضافي. يجب أن يستمع كل فرع على 127.0.0.1 مع security=none.",
         "empty": "لا توجد fallbacks بعد",
         "add": "إضافة fallback",
@@ -269,15 +277,15 @@
         "defaultCatchAll": "افتراضي — يلتقط أي شيء آخر"
       },
       "protocol": "بروتوكول",
-      "port": "بورت",
-      "portMap": "خريطة البورت",
-      "traffic": "الترافيك",
+      "port": "المنفذ",
+      "portMap": "تعيين المنفذ",
+      "traffic": "حركة المرور",
       "details": "تفاصيل",
-      "transportConfig": "نقل",
+      "transportConfig": "النقل",
       "expireDate": "المدة",
       "createdAt": "تاريخ الإنشاء",
       "updatedAt": "تاريخ التحديث",
-      "resetTraffic": "إعادة ضبط الترافيك",
+      "resetTraffic": "إعادة تعيين حركة المرور",
       "addInbound": "أضف إدخال",
       "generalActions": "إجراءات عامة",
       "modifyInbound": "تعديل الإدخال",
@@ -292,11 +300,31 @@
       "delAllClients": "حذف جميع العملاء",
       "delAllClientsConfirmTitle": "حذف جميع العملاء البالغ عددهم {count} من \"{remark}\"؟",
       "delAllClientsConfirmContent": "يزيل كل عميل من هذا الإدخال ويحذف سجلات حركة المرور الخاصة بهم. يتم الاحتفاظ بالإدخال نفسه. لا يمكن التراجع عن هذا.",
+      "attachClients": "إرفاق عملاء بـ…",
+      "addClientsToGroup": "إضافة عملاء إلى مجموعة…",
+      "attachClientsTitle": "إرفاق عملاء من «{remark}»",
+      "attachClientsDesc": "يربط نفس {count} عميل (UUID/كلمة المرور وحركة المرور المشتركة) بالواردات المحددة. يبقون في هذا الوارد أيضاً.",
+      "attachClientsTargets": "الواردات الهدف",
+      "attachClientsNoTargets": "لا توجد واردات متوافقة أخرى للإرفاق.",
+      "attachClientsResult": "أُرفق {attached}، تم تخطي {skipped}.",
+      "attachClientsResultMixed": "أُرفق {attached}، تخطي {skipped}، أخطاء {errors}.",
+      "attachClientsSelectLabel": "العملاء للإرفاق",
+      "attachClientsSearchPlaceholder": "ابحث بالبريد أو التعليق",
+      "attachClientsStatusDisabled": "معطل",
+      "attachClientsSelectedCount": "{selected} من {total} محدد",
+      "detachClients": "فصل العملاء",
+      "detachClientsTitle": "فصل عملاء من «{remark}»",
+      "detachClientsDesc": "يزيل العميل (العملاء) المحدد من هذا الوارد فقط. تُحفظ سجلات العملاء (استخدم Delete للإزالة الكاملة). المصدر يحتوي على {count} عميل إجمالاً.",
+      "detachClientsResult": "فُصل {detached}، تم تخطي {skipped}.",
+      "detachClientsResultMixed": "فُصل {detached}، تخطي {skipped}، أخطاء {errors}.",
+      "detachClientsSelectLabel": "العملاء للفصل",
       "exportLinksTitle": "تصدير روابط الإدخال",
       "exportSubsTitle": "تصدير روابط الاشتراك",
       "exportAllLinksTitle": "تصدير كل روابط الإدخالات",
       "exportAllSubsTitle": "تصدير كل روابط الاشتراكات",
-      "inboundJsonTitle": "JSON الإدخال",
+      "exportAllLinksFileName": "جميع-الواردات",
+      "exportAllSubsFileName": "جميع-الواردات-Subs",
+      "inboundJsonTitle": "JSON الوارد",
       "deleteClient": "حذف العميل",
       "deleteClientContent": "متأكد إنك عايز تحذف العميل؟",
       "resetTrafficContent": "متأكد إنك عايز تعيد ضبط الترافيك؟",
@@ -306,7 +334,7 @@
       "destinationPort": "بورت الوجهة",
       "targetAddress": "عنوان الهدف",
       "monitorDesc": "سيبها فاضية لو عايز تستمع على كل الـ IPs",
-      "meansNoLimit": "= غير محدود. (الوحدة: جيجابايت)",
+      "meansNoLimit": "= غير محدود. (الوحدة: GB)",
       "totalFlow": "إجمالي التدفق",
       "leaveBlankToNeverExpire": "سيبها فاضية عشان ماتنتهيش",
       "noRecommendKeepDefault": "ننصح باستخدام الافتراضي",
@@ -333,7 +361,7 @@
       "delDepletedClients": "حذف العملاء اللي خلصت",
       "delDepletedClientsTitle": "حذف العملاء اللي خلصت",
       "delDepletedClientsContent": "متأكد إنك عايز تحذف كل العملاء اللي خلصت؟",
-      "email": "الإيميل",
+      "email": "البريد",
       "emailDesc": "ادخل إيميل فريد.",
       "IPLimit": "تحديد IP",
       "IPLimitDesc": "بيعطل الإدخال لو العدد زاد عن القيمة المحددة. (0 = تعطيل)",
@@ -341,9 +369,10 @@
       "IPLimitlogDesc": "سجل تاريخ الـ IPs. (عشان تفعل الإدخال بعد التعطيل، امسح السجل)",
       "IPLimitlogclear": "امسح السجل",
       "setDefaultCert": "استخدم شهادة البانل",
-      "streamTab": "الدفق",
+      "setDefaultCertEmpty": "لا توجد شهادة معدّة للوحة. عينّ واحدة من الإعدادات أولاً.",
+      "streamTab": "تدفق",
       "securityTab": "الأمان",
-      "sniffingTab": "الاستشعار",
+      "sniffingTab": "تنصت",
       "sniffingMetadataOnly": "البيانات الوصفية فقط",
       "sniffingRouteOnly": "التوجيه فقط",
       "sniffingIpsExcluded": "IP المستثناة",
@@ -361,15 +390,14 @@
         "allHelp": "كائن الاتصال الوارد الكامل بكل الحقول في محرر واحد.",
         "settings": "الإعدادات",
         "settingsHelp": "غلاف كتلة settings في Xray:",
-        "sniffing": "الاستشعار",
+        "sniffing": "Sniffing",
         "sniffingHelp": "غلاف كتلة sniffing في Xray:",
-        "stream": "الدفق",
+        "stream": "Stream",
         "streamHelp": "غلاف كتلة stream في Xray:",
         "jsonErrorPrefix": "JSON متقدم"
       },
       "telegramDesc": "ادخل ID شات Telegram. (استخدم '/id' في البوت) أو ({'@'}userinfobot)",
       "subscriptionDesc": "عشان تلاقي رابط الاشتراك، ادخل على 'التفاصيل'. وكمان ممكن تستخدم نفس الاسم لعدة عملاء.",
-      "info": "معلومات",
       "same": "نفسه",
       "inboundData": "بيانات الإدخال",
       "exportInbound": "تصدير الإدخال",
@@ -406,6 +434,143 @@
         "getNewmldsa65Error": "حدث خطاء في الحصول على mldsa65.",
         "getNewVlessEncError": "حدث خطأ أثناء الحصول على VlessEnc."
       },
+      "form": {
+        "moveUp": "أعلى",
+        "moveDown": "أسفل",
+        "addAll": "إضافة الكل",
+        "addAllFallbackTooltip": "أضف صف fallback لكل وارد مؤهل لم يتم ربطه بعد",
+        "peers": "Peers",
+        "addPeer": "إضافة peer",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "ويندوز فقط. تُضاف CIDR تلقائياً إلى جدول التوجيه ليمر المرور المطابق عبر TUN.",
+        "autoOutboundsInterface": "واجهة صادر تلقائية",
+        "autoOutboundsInterfaceTooltip": "الواجهة الفعلية لحركة المرور الصادرة. استخدم 'auto' للاكتشاف؛ يتم تفعيلها تلقائياً عند تعيين Auto system routes.",
+        "rewriteAddress": "إعادة كتابة العنوان",
+        "rewritePort": "إعادة كتابة المنفذ",
+        "allowedNetwork": "الشبكة المسموح بها",
+        "followRedirect": "اتبع إعادة التوجيه",
+        "accounts": "الحسابات",
+        "allowTransparent": "السماح بالشفاف",
+        "encryptionMethod": "طريقة التشفير",
+        "visionTestseed": "Vision testseed",
+        "version": "الإصدار",
+        "udpIdleTimeout": "UDP idle timeout (ثانية)",
+        "masquerade": "Masquerade",
+        "type": "النوع",
+        "upstreamUrl": "Upstream URL",
+        "rewriteHost": "إعادة كتابة Host",
+        "skipTlsVerify": "تخطي التحقق من TLS",
+        "directory": "الدليل",
+        "statusCode": "رمز الحالة",
+        "body": "Body",
+        "headers": "الترويسات",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "إصدار الطلب",
+        "requestMethod": "طريقة الطلب",
+        "requestPath": "مسار الطلب",
+        "requestHeaders": "ترويسات الطلب",
+        "responseVersion": "إصدار الاستجابة",
+        "responseStatus": "حالة الاستجابة",
+        "responseReason": "سبب الاستجابة",
+        "responseHeaders": "ترويسات الاستجابة",
+        "heartbeatPeriod": "فترة Heartbeat",
+        "serviceName": "اسم الخدمة",
+        "authority": "Authority",
+        "multiMode": "Multi Mode",
+        "maxBufferedUpload": "الحد الأقصى للرفع المخزن",
+        "maxUploadSize": "حجم الرفع الأقصى (بايت)",
+        "streamUpServer": "Stream-Up Server",
+        "serverMaxHeaderBytes": "أقصى بايت ترويسة الخادم",
+        "paddingBytes": "بايتات Padding",
+        "uplinkHttpMethod": "Uplink HTTP method",
+        "paddingObfsMode": "وضع تشويش Padding",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "موضع Padding",
+        "paddingMethod": "طريقة Padding",
+        "sessionPlacement": "Session Placement",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence Placement",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink Data Placement",
+        "uplinkDataKey": "Uplink Data Key",
+        "noSseHeader": "بدون ترويسة SSE",
+        "ttiMs": "TTI (ms)",
+        "uplinkMbps": "رفع (MB/s)",
+        "downlinkMbps": "تنزيل (MB/s)",
+        "cwndMultiplier": "معامل CWND",
+        "maxSendingWindow": "أقصى نافذة إرسال",
+        "externalProxy": "وكيل خارجي",
+        "sniPlaceholder": "SNI (افتراضياً host)",
+        "fingerprint": "بصمة",
+        "defaultOption": "افتراضي",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive Interval",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "V6 فقط",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "X-Forwarded-For موثوق",
+        "addressPortStrategy": "استراتيجية العنوان+المنفذ",
+        "tryDelayMs": "تأخير المحاولة (ms)",
+        "prioritizeIPv6": "أولوية IPv6",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "أقصى محاولات متزامنة",
+        "customSockopt": "sockopt مخصص",
+        "addCustomOption": "إضافة خيار مخصص",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "تلقائي",
+        "minMaxVersion": "إصدار أدنى/أقصى",
+        "rejectUnknownSni": "رفض SNI غير معروف",
+        "disableSystemRoot": "تعطيل System Root",
+        "sessionResumption": "استئناف الجلسة",
+        "oneTimeLoading": "تحميل لمرة واحدة",
+        "usageOption": "خيار الاستخدام",
+        "buildChain": "بناء السلسلة",
+        "echKey": "ECH key",
+        "echConfig": "تكوين ECH",
+        "pinnedPeerCertSha256": "SHA-256 لشهادة النظير المثبَّتة",
+        "pinnedPeerCertSha256Tip": "تجزئات SHA-256 المُرمَّزة بـ Base64 لشهادة النظير. للوحة فقط — لا تُكتب في إعدادات xray على الخادم، لكنها تُضمَّن في روابط المشاركة ليتمكَّن العملاء من تثبيت الشهادة.",
+        "pinnedPeerCertSha256Placeholder": "تجزئة (تجزئات) base64، مفصولة بفواصل",
+        "generateRandomPin": "إنشاء تجزئة عشوائية",
+        "getNewEchCert": "احصل على شهادة ECH جديدة",
+        "show": "عرض",
+        "xver": "Xver",
+        "target": "الهدف",
+        "maxTimeDiff": "أقصى فرق زمن (ms)",
+        "minClientVer": "أدنى إصدار للعميل",
+        "maxClientVer": "أقصى إصدار للعميل",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "احصل على شهادة جديدة",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "احصل على Seed جديد"
+      },
+      "info": {
+        "mode": "الوضع",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "اسم الواجهة",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "واجهة الصادر",
+        "autoSystemRoutes": "توجيهات نظام تلقائية",
+        "followRedirect": "FollowRedirect",
+        "auth": "Auth",
+        "noKernelTun": "TUN بدون نواة",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "تكوين Peer {n}"
+      },
       "stream": {
         "general": {
           "request": "طلب",
@@ -416,7 +581,7 @@
         "tcp": {
           "version": "نسخة",
           "method": "طريقة",
-          "path": "مسار",
+          "path": "المسار",
           "status": "الحالة",
           "statusDescription": "وصف الحالة",
           "requestHeader": "رأس الطلب",
@@ -456,6 +621,20 @@
       "days": "يوم",
       "renew": "تجديد تلقائي",
       "renewDesc": "تجديد تلقائي بعد انتهاء الصلاحية. (0 = تعطيل) (الوحدة: يوم)",
+      "searchPlaceholder": "ابحث بالبريد، التعليق، sub ID، UUID، كلمة المرور، auth…",
+      "filterTitle": "تصفية العملاء",
+      "clearAllFilters": "مسح الكل",
+      "sortOldest": "الأقدم أولاً",
+      "sortNewest": "الأحدث أولاً",
+      "sortRecentlyUpdated": "محدّث مؤخراً",
+      "sortRecentlyOnline": "متصل مؤخراً",
+      "sortEmailAZ": "بريد A→Z",
+      "sortEmailZA": "بريد Z→A",
+      "sortMostTraffic": "الأكثر استهلاكاً",
+      "sortHighestRemaining": "الأعلى متبقياً",
+      "sortExpiringSoonest": "الأقرب انتهاءً",
+      "has": "يملك",
+      "hasNot": "لا يملك",
       "title": "العملاء",
       "actions": "الإجراءات",
       "totalGB": "مجموع المرسل/المستقبل (جيجابايت)",
@@ -465,7 +644,10 @@
       "password": "كلمة المرور",
       "subId": "معرّف الاشتراك",
       "online": "متصل",
-      "email": "البريد الإلكتروني",
+      "email": "البريد",
+      "group": "المجموعة",
+      "groupDesc": "تسمية منطقية لتجميع العملاء (مثل فريق، عميل، منطقة). يمكن تصفيتها من شريط الأدوات.",
+      "groupPlaceholder": "مثلاً customer-a",
       "comment": "ملاحظة",
       "traffic": "حركة المرور",
       "offline": "غير متصل",
@@ -483,17 +665,51 @@
       "selectInbound": "حدد اتصالاً واردًا واحدًا أو أكثر",
       "noSubId": "هذا العميل ليس لديه subId، لا يوجد رابط قابل للمشاركة.",
       "noLinks": "لا توجد روابط للمشاركة — قم بإرفاق هذا العميل بأحد الاتصالات الواردة الداعمة للبروتوكول أولاً.",
-      "link": "رابط",
+      "link": "الرابط",
       "resetNotPossible": "قم بإرفاق هذا العميل بأحد الاتصالات الواردة أولاً.",
       "general": "عام",
       "resetAllTraffics": "إعادة ضبط حركة مرور كل العملاء",
       "resetAllTrafficsTitle": "إعادة ضبط حركة مرور كل العملاء؟",
       "resetAllTrafficsContent": "يُعاد ضبط عدّاد الإرسال/الاستقبال لكل عميل إلى الصفر. لا تتأثر الحصص ومواعيد الانتهاء. لا يمكن التراجع.",
-      "empty": "لا يوجد عملاء بعد — أضف واحدًا للبدء.",
       "deleteConfirmTitle": "حذف العميل {email}؟",
       "deleteConfirmContent": "سيؤدي هذا إلى إزالة العميل من جميع الاتصالات الواردة المرتبطة وحذف سجل حركة مروره. لا يمكن التراجع.",
       "deleteSelected": "حذف ({count})",
       "adjustSelected": "تعديل ({count})",
+      "subLinksSelected": "روابط الاشتراك ({count})",
+      "addToGroupTitle": "إضافة {count} عميل إلى مجموعة",
+      "addToGroupTooltip": "اختر مجموعة موجودة أو أدخل اسماً جديداً. استخدم Ungroup لإزالة العملاء من مجموعتهم الحالية.",
+      "addToGroupPlaceholder": "اسم المجموعة",
+      "addToGroupSuccessToast": "تمت إضافة {count} عميل إلى {group}",
+      "ungroupSuccessToast": "تم مسح المجموعة من {count} عميل",
+      "ungroup": "إزالة من المجموعة",
+      "ungroupConfirmTitle": "إزالة {count} عميل من مجموعتهم؟",
+      "ungroupConfirmContent": "يمسح تسمية المجموعة من كل عميل محدد. يُحفظ العملاء (استخدم Delete للإزالة الكاملة).",
+      "addToGroup": "إضافة إلى مجموعة",
+      "attach": "إرفاق",
+      "adjust": "ضبط",
+      "subLinks": "روابط الاشتراك",
+      "selectedCount": "{count} محدد",
+      "attachSelected": "إرفاق ({count})",
+      "attachToInboundsTitle": "إرفاق {count} عميل بالواردات",
+      "attachToInboundsDesc": "يربط {count} عميل المحدد (نفس UUID/كلمة المرور والمرور المشترك) بالواردات المختارة. يحتفظون بارتباطاتهم الحالية.",
+      "attachToInboundsTargets": "الواردات الهدف",
+      "attachToInboundsNoTargets": "لا توجد واردات متعددة المستخدمين للارتباط.",
+      "detachSelected": "فصل ({count})",
+      "detach": "فصل",
+      "detachFromInboundsTitle": "فصل {count} عميل من الواردات",
+      "detachFromInboundsDesc": "يزيل {count} عميل المحدد من الواردات المختارة. الأزواج التي لم يكن العميل مرتبطاً بها يتم تخطيها بصمت. تُحفظ سجلات العملاء (استخدم Delete للإزالة الكاملة).",
+      "detachFromInboundsTargets": "الواردات للفصل",
+      "detachFromInboundsNoTargets": "لا توجد واردات متعددة المستخدمين.",
+      "detachFromInboundsResult": "فُصل {detached}، تم تخطي {skipped}.",
+      "detachFromInboundsResultMixed": "فُصل {detached}، تخطي {skipped}، أخطاء {errors}.",
+      "subLinksTitle": "روابط الاشتراك ({count})",
+      "subLinkColumn": "رابط الاشتراك",
+      "subJsonLinkColumn": "رابط JSON للاشتراك",
+      "subLinksCopyAll": "نسخ الكل",
+      "subLinksCopiedAll": "تم نسخ {count} رابط",
+      "subLinksEmpty": "لا يحتوي أي من العملاء المحددين على معرف اشتراك.",
+      "subLinksDisabled": "خدمة الاشتراك معطلة.",
+      "subLinksDisabledHint": "فعّل الاشتراك من إعدادات اللوحة → الاشتراك لإنشاء الروابط.",
       "bulkDeleteConfirmTitle": "حذف {count} عميل؟",
       "bulkDeleteConfirmContent": "سيتم إزالة كل عميل محدد من جميع الاتصالات الواردة المرتبطة وحذف سجل حركة مروره. لا يمكن التراجع.",
       "bulkAdjustTitle": "تعديل {count} عميل",
@@ -505,10 +721,11 @@
       "delDepletedConfirmTitle": "حذف العملاء المنتهية حصصهم؟",
       "delDepletedConfirmContent": "يُحذف كل عميل استُنفِدت حصة حركة مروره أو انتهت صلاحيته. لا يمكن التراجع.",
       "auth": "Auth",
-      "hysteriaAuth": "Auth (Hysteria)",
+      "hysteriaAuth": "Hysteria Auth",
       "uuid": "UUID",
       "flow": "Flow",
-      "reverseTag": "Reverse tag",
+      "vmessSecurity": "أمان VMess",
+      "reverseTag": "وسم عكسي",
       "reverseTagPlaceholder": "Reverse tag اختياري",
       "telegramId": "معرّف مستخدم تلغرام",
       "telegramIdPlaceholder": "معرّف مستخدم تلغرام رقمي (0 = لا شيء)",
@@ -528,13 +745,51 @@
         "delDepleted": "تم حذف {count} عميل منتهٍ"
       }
     },
+    "groups": {
+      "title": "المجموعات",
+      "name": "الاسم",
+      "clientCount": "عملاء في المجموعة",
+      "totalGroups": "إجمالي المجموعات",
+      "totalGroupedClients": "العملاء بمجموعة",
+      "emptyGroups": "مجموعات فارغة",
+      "addGroup": "إضافة مجموعة",
+      "createSuccess": "تم إنشاء المجموعة «{name}».",
+      "rename": "إعادة تسمية",
+      "renameTitle": "إعادة تسمية {name}",
+      "renameCollision": "مجموعة باسم «{name}» موجودة بالفعل.",
+      "renameSuccess": "تمت إعادة تسمية المجموعة على {count} عميل.",
+      "deleteConfirmTitle": "حذف المجموعة {name}؟",
+      "deleteConfirmContent": "يحذف المجموعة ويمسح تسميتها من {count} عميل. العملاء أنفسهم لا يُحذفون.",
+      "deleteSuccess": "تم مسح المجموعة من {count} عميل.",
+      "resetTraffic": "إعادة تعيين حركة المرور",
+      "resetConfirmTitle": "إعادة تعيين حركة المرور للمجموعة {name}؟",
+      "resetConfirmContent": "يصفر up/down لجميع {count} عميل في هذه المجموعة.",
+      "resetSuccess": "تمت إعادة تعيين حركة المرور لـ {count} عميل.",
+      "adjustSuccess": "تم ضبط {count} عميل في {name}.",
+      "emptyForAction": "هذه المجموعة فارغة.",
+      "deleteGroupOnly": "حذف المجموعة (مع الاحتفاظ بالعملاء)",
+      "deleteClients": "حذف عملاء المجموعة",
+      "deleteClientsConfirmTitle": "حذف جميع العملاء في {name}؟",
+      "deleteClientsConfirmContent": "يحذف {count} عميل نهائياً مع سجلات حركة المرور. تُمسح تسمية المجموعة أيضاً. لا يمكن التراجع.",
+      "deleteClientsSuccess": "تم حذف {count} عميل.",
+      "deleteClientsMixed": "{ok} حُذف، {failed} تم تخطيه",
+      "addToGroup": "إضافة عملاء…",
+      "addToGroupTitle": "إضافة عملاء إلى المجموعة «{name}»",
+      "addToGroupDesc": "اختر العملاء لإضافتهم إلى هذه المجموعة. يحتفظون بارتباطات الواردات الحالية؛ تتغير تسمية المجموعة فقط. لا تُعرض العملاء الذين هم في هذه المجموعة بالفعل.",
+      "addToGroupEmpty": "لا يوجد عملاء آخرون للإضافة.",
+      "addToGroupResult": "تمت إضافة {count} عميل إلى {name}.",
+      "removeFromGroup": "إزالة عملاء…",
+      "removeFromGroupTitle": "إزالة عملاء من المجموعة «{name}»",
+      "removeFromGroupDesc": "اختر الأعضاء لإزالتهم من هذه المجموعة. يُحفظ العملاء (استخدم «حذف عملاء المجموعة» للإزالة الكاملة).",
+      "removeFromGroupResult": "تمت إزالة {count} عميل من {name}."
+    },
     "nodes": {
       "title": "النودز",
       "addNode": "إضافة نود",
-      "editNode": "تعديل نود",
+      "editNode": "تحرير العقدة",
       "totalNodes": "إجمالي النودز",
-      "onlineNodes": "أونلاين",
-      "offlineNodes": "أوفلاين",
+      "onlineNodes": "متصل",
+      "offlineNodes": "غير متصل",
       "avgLatency": "متوسط الكمون",
       "name": "الاسم",
       "namePlaceholder": "مثال: de-frankfurt-1",
@@ -542,9 +797,9 @@
       "remark": "ملاحظة",
       "scheme": "البروتوكول",
       "address": "العنوان",
-      "port": "البورت",
+      "port": "المنفذ",
       "basePath": "المسار الأساسي",
-      "apiToken": "توكن API",
+      "apiToken": "رمز API",
       "apiTokenPlaceholder": "التوكن من صفحة إعدادات البانل البعيد",
       "apiTokenHint": "البانل البعيد بيعرض توكن API بتاعه في الإعدادات → توكن API.",
       "regenerate": "تجديد التوكن",
@@ -553,7 +808,7 @@
       "allowPrivateAddressHint": "التفعيل فقط للعقد على شبكة خاصة أو VPN.",
       "enable": "مفعل",
       "status": "الحالة",
-      "cpu": "المعالج",
+      "cpu": "CPU",
       "mem": "الذاكرة",
       "uptime": "مدة التشغيل",
       "latency": "الكمون",
@@ -570,8 +825,8 @@
       "deleteConfirmTitle": "تحذف النود \"{name}\"؟",
       "deleteConfirmContent": "ده هيوقّف مراقبة النود. البانل البعيد نفسه مش هيتأثر.",
       "statusValues": {
-        "online": "أونلاين",
-        "offline": "أوفلاين",
+        "online": "متصل",
+        "offline": "غير متصل",
         "unknown": "غير معروف"
       },
       "toasts": {
@@ -590,7 +845,7 @@
       "title": "إعدادات البانل",
       "save": "حفظ",
       "infoDesc": "كل تغيير هتعمله هنا لازم يتخزن. ياريت تعيد تشغيل البانل عشان التعديلات تتفعل.",
-      "restartPanel": "إعادة تشغيل البانل",
+      "restartPanel": "إعادة تشغيل اللوحة",
       "restartPanelDesc": "متأكد إنك عايز تعيد تشغيل البانل؟ لو ماقدرتش تدخل بعد إعادة التشغيل، شوف سجل البانل على السيرفر.",
       "restartPanelSuccess": "تم إعادة تشغيل اللوحة بنجاح",
       "actions": "إجراءات",
@@ -604,7 +859,7 @@
       "warnDefaultBasePath": "المسار الأساسي الافتراضي \"/\" معروف — غيّره إلى مسار عشوائي.",
       "warnDefaultSubPath": "مسار الاشتراك الافتراضي \"/sub/\" معروف — قم بتغييره.",
       "warnDefaultJsonPath": "مسار اشتراك JSON الافتراضي \"/json/\" معروف — قم بتغييره.",
-      "TGBotSettings": "بوت Telegram",
+      "TGBotSettings": "بوت تيليجرام",
       "panelListeningIP": "IP الاستماع",
       "panelListeningIPDesc": "عنوان IP للبانل. (سيبه فاضي عشان يستمع على كل الـ IPs)",
       "panelListeningDomain": "دومين الاستماع",
@@ -619,6 +874,8 @@
       "panelUrlPathDesc": "مسار URI للبانل. (يبدأ بـ '/' وبينتهي بـ '/')",
       "pageSize": "حجم الصفحة",
       "pageSizeDesc": "حدد حجم الصفحة لجدول الإدخالات. (0 = تعطيل)",
+      "panelProxy": "وكيل شبكة اللوحة",
+      "panelProxyDesc": "يوجه طلبات اللوحة الصادرة (تحديثات geo، فحص إصدارات Xray/اللوحة، تيليجرام) عبر هذا الوكيل لتجاوز فلترة GitHub/تيليجرام على الخادم. يقبل socks5:// أو http(s)://، مثل وارد SOCKS محلي لـ Xray. اتركه فارغاً للاتصال المباشر.",
       "remarkModel": "نموذج الملاحظة وحرف الفصل",
       "datepicker": "نوع التقويم",
       "datepickerPlaceholder": "اختار التاريخ",
@@ -630,11 +887,11 @@
       "newPassword": "الباسورد الجديد",
       "telegramBotEnable": "تفعيل بوت Telegram",
       "telegramBotEnableDesc": "يفعل بوت Telegram.",
-      "telegramToken": "توكن Telegram",
+      "telegramToken": "رمز تيليجرام",
       "telegramTokenDesc": "توكن البوت اللي جبت من '{'@'}BotFather'.",
-      "telegramProxy": "بروكسي SOCKS",
+      "telegramProxy": "وكيل SOCKS",
       "telegramProxyDesc": "يفعل بروكسي SOCKS5 للاتصال بـ Telegram. (اضبط الإعدادات حسب الدليل)",
-      "telegramAPIServer": "سيرفر Telegram API",
+      "telegramAPIServer": "خادم API لتيليجرام",
       "telegramAPIServerDesc": "سيرفر Telegram API المستخدم. سيبه فاضي لاستخدام الافتراضي.",
       "telegramChatId": "ID شات الأدمن",
       "telegramChatIdDesc": "ID شات الأدمن في Telegram. (مفصول بفواصل)(تقدر تجيبه من {'@'}userinfobot) أو (استخدم '/id' في البوت)",
@@ -658,6 +915,8 @@
       "subEnable": "تفعيل خدمة الاشتراك",
       "subEnableDesc": "يفعل خدمة الاشتراك.",
       "subJsonEnable": "تمكين/تعطيل نقطة نهاية اشتراك JSON بشكل مستقل.",
+      "subJsonEnableTitle": "اشتراك JSON",
+      "subClashEnableTitle": "اشتراك Clash / Mihomo",
       "subTitle": "عنوان الاشتراك",
       "subTitleDesc": "العنوان اللي هيظهر في عميل VPN",
       "subSupportUrl": "رابط الدعم",
@@ -693,7 +952,7 @@
       "subURI": "مسار البروكسي العكسي",
       "subURIDesc": "مسار URI لرابط الاشتراك عشان تستخدمه ورا البروكسي.",
       "externalTrafficInformEnable": "تنبيه الترافيك الخارجي",
-      "externalTrafficInformEnableDesc": "يبعت تنبيه لـ API خارجي مع كل تحديث للترافيك.",
+      "externalTrafficInformEnableDesc": "إخطار واجهة API خارجية بكل تحديث لحركة المرور.",
       "externalTrafficInformURI": "مسار تنبيه الترافيك الخارجي",
       "externalTrafficInformURIDesc": "تحديثات الترافيك هتتبعت للمسار ده.",
       "restartXrayOnClientDisable": "إعادة تشغيل Xray بعد التعطيل التلقائي",
@@ -703,7 +962,55 @@
       "fragmentSett": "إعدادات التجزئة",
       "noisesDesc": "يفعل التشويش.",
       "noisesSett": "إعدادات التشويش",
-      "mux": "MUX",
+      "trustedProxyCidrs": "CIDR وكلاء موثوقين",
+      "trustedProxyCidrsDesc": "IPs/CIDRs مفصولة بفواصل يُسمح لها بتعيين ترويسات host، proto و client IP المعاد توجيهها.",
+      "ldap": {
+        "enable": "تفعيل مزامنة LDAP",
+        "host": "مضيف LDAP",
+        "port": "منفذ LDAP",
+        "useTls": "استخدام TLS (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "مهيأة؛ اترك فارغاً للاحتفاظ بكلمة المرور الحالية.",
+        "passwordUnconfigured": "غير مهيأة.",
+        "passwordPlaceholder": "مهيأة — أدخل قيمة جديدة لاستبدالها",
+        "baseDn": "Base DN",
+        "userFilter": "مرشح المستخدم",
+        "userAttr": "خاصية المستخدم (username/email)",
+        "vlessField": "خاصية VLESS flag",
+        "flagField": "خاصية flag عامة (اختياري)",
+        "flagFieldDesc": "إذا تم تعيينها، تتجاوز VLESS flag — مثل shadowInactive.",
+        "truthyValues": "قيم Truthy",
+        "truthyValuesDesc": "مفصولة بفواصل؛ الافتراضي: true,1,yes,on",
+        "invertFlag": "عكس flag",
+        "invertFlagDesc": "فعّل عندما تعني الخاصية «معطل» (مثل shadowInactive).",
+        "syncSchedule": "جدول المزامنة",
+        "syncScheduleDesc": "سلسلة شبيهة بـ cron، مثل @every 1m",
+        "inboundTags": "وسوم الواردات",
+        "inboundTagsDesc": "الواردات التي يمكن لمزامنة LDAP إنشاء/حذف العملاء فيها تلقائياً.",
+        "noInbounds": "لم يتم العثور على واردات. أنشئ واحداً في الواردات أولاً.",
+        "autoCreate": "إنشاء عملاء تلقائياً",
+        "autoDelete": "حذف عملاء تلقائياً",
+        "defaultTotalGb": "الإجمالي الافتراضي (GB)",
+        "defaultExpiryDays": "الانتهاء الافتراضي (أيام)",
+        "defaultIpLimit": "حد IP الافتراضي"
+      },
+      "subFormats": {
+        "packets": "الحزم",
+        "length": "الطول",
+        "interval": "الفاصل",
+        "maxSplit": "أقصى تقسيم",
+        "noises": "الضوضاء",
+        "noiseItem": "ضوضاء №{n}",
+        "type": "النوع",
+        "packet": "حزمة",
+        "delayMs": "التأخير (ms)",
+        "applyTo": "تطبيق على",
+        "addNoise": "+ ضوضاء",
+        "concurrency": "التزامن",
+        "xudpConcurrency": "تزامن xudp",
+        "xudpUdp443": "xudp UDP 443"
+      },
+      "mux": "Mux",
       "muxDesc": "ينقل أكثر من تيار بيانات مستقل خلال تيار بيانات واحد قائم.",
       "muxSett": "إعدادات MUX",
       "direct": "اتصال مباشر",
@@ -756,8 +1063,11 @@
     "xray": {
       "title": "إعدادات Xray",
       "save": "احفظ",
-      "restart": "أعد تشغيل Xray",
+      "restart": "إعادة تشغيل Xray",
       "restartSuccess": "تم إعادة تشغيل Xray بنجاح",
+      "restartOutputTitle": "مخرجات إعادة تشغيل Xray",
+      "restartConfirmTitle": "إعادة تشغيل xray؟",
+      "restartConfirmContent": "يعيد تحميل خدمة xray بالتكوين المحفوظ.",
       "stopSuccess": "تم إيقاف Xray بنجاح",
       "restartError": "حدث خطأ أثناء إعادة تشغيل Xray.",
       "stopError": "حدث خطأ أثناء إيقاف Xray.",
@@ -765,7 +1075,7 @@
       "advancedTemplate": "متقدم",
       "generalConfigs": "إعدادات عامة",
       "generalConfigsDesc": "الخيارات دي هتحدد التعديلات العامة.",
-      "logConfigs": "السجلات",
+      "logConfigs": "السجل",
       "logConfigsDesc": "السجلات ممكن تأثر على كفاءة السيرفر. ننصح بتفعيلها بحكمة لما تكون محتاجها.",
       "blockConfigsDesc": "الخيارات دي هتحجب الترافيك بناءً على بروتوكولات ومواقع محددة.",
       "basicRouting": "توجيه أساسي",
@@ -790,10 +1100,12 @@
       "outboundTestUrl": "رابط اختبار المخرج",
       "outboundTestUrlDesc": "الرابط المستخدم عند اختبار اتصال المخرج",
       "Torrent": "حظر بروتوكول التورنت",
-      "Inbounds": "الإدخالات",
+      "Inbounds": "الواردات",
       "InboundsDesc": "قبول العملاء المعينين.",
-      "Outbounds": "المخرجات",
+      "Outbounds": "الصادرات",
       "Balancers": "موازنات التحميل",
+      "balancerTagRequired": "الوسم مطلوب",
+      "balancerSelectorRequired": "اختر صادراً واحداً على الأقل",
       "OutboundsDesc": "حدد مسار الترافيك الصادر.",
       "Routings": "قواعد التوجيه",
       "RoutingsDesc": "أولوية كل قاعدة مهمة جداً!",
@@ -832,6 +1144,73 @@
         "edit": "عدل القاعدة",
         "useComma": "عناصر مفصولة بفواصل"
       },
+      "routing": {
+        "dragToReorder": "اسحب لإعادة الترتيب"
+      },
+      "ruleForm": {
+        "sourceIps": "IPs المصدر",
+        "sourcePort": "منفذ المصدر",
+        "vlessRoute": "مسار VLESS",
+        "attributes": "الخصائص",
+        "value": "القيمة",
+        "user": "المستخدم",
+        "inboundTags": "وسوم الواردات",
+        "outboundTag": "وسم الصادر",
+        "balancerTag": "وسم الموازن",
+        "balancerTagTooltip": "يوجه حركة المرور عبر أحد موازنات الحمل المهيأة"
+      },
+      "outboundForm": {
+        "tagDuplicate": "الوسم مستخدم بالفعل من قبل صادر آخر",
+        "tagRequired": "الوسم مطلوب",
+        "tagPlaceholder": "وسم-فريد",
+        "localIpPlaceholder": "IP محلي",
+        "addressRequired": "العنوان مطلوب",
+        "portRequired": "المنفذ مطلوب",
+        "optional": "اختياري",
+        "udpOverTcp": "UDP over TCP",
+        "uotVersion": "إصدار UoT",
+        "inboundTag": "وسم الوارد",
+        "inboundTagPlaceholder": "وسم الوارد المستخدم في قواعد التوجيه",
+        "responseType": "نوع الاستجابة",
+        "rewriteNetwork": "إعادة كتابة الشبكة",
+        "unchanged": "(دون تغيير)",
+        "unchangedAddress": "(دون تغيير) مثل 1.1.1.1",
+        "rules": "القواعد",
+        "ruleN": "القاعدة {n}",
+        "action": "الإجراء",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "القواعد النهائية",
+        "overrideXrayPrivateIp": "تجاوز حظر IP الخاص الافتراضي في Xray",
+        "blockDelay": "تأخير الحظر (ms)",
+        "reverseSniffing": "Sniffing عكسي",
+        "workers": "Workers",
+        "reserved": "محجوز",
+        "minUploadInterval": "أدنى فاصل رفع (ms)",
+        "maxUploadSizeBytes": "حجم الرفع الأقصى (بايت)",
+        "uplinkChunkSize": "حجم chunk الرفع",
+        "noGrpcHeader": "بدون ترويسة gRPC",
+        "maxConcurrency": "أقصى تزامن",
+        "maxConnections": "أقصى اتصالات",
+        "maxReuseTimes": "أقصى مرات إعادة استخدام",
+        "maxRequestTimes": "أقصى طلبات",
+        "maxReusableSecs": "أقصى ثوانٍ قابلة لإعادة الاستخدام",
+        "keepAlivePeriod": "فترة keep alive",
+        "authPassword": "كلمة مرور Auth",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "اسم الخادم",
+        "verifyPeerName": "التحقق من اسم peer",
+        "pinnedSha256": "SHA256 مثبت",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "فاصل keep alive",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "الواجهة",
+        "ipv6Only": "IPv6 فقط",
+        "acceptProxyProtocol": "قبول proxy protocol",
+        "tcpUserTimeoutMs": "TCP user timeout (ms)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (ثانية)"
+      },
       "outbound": {
         "addOutbound": "أضف مخرج",
         "addReverse": "أضف عكسي",
@@ -840,15 +1219,15 @@
         "reverseTag": "وسم العكسي",
         "reverseTagDesc": "وسم الخروج لبروكسي VLESS العكسي البسيط. اتركه فارغاً لتعطيله.",
         "reverseTagPlaceholder": "وسم الخروج (اتركه فارغاً للتعطيل)",
-        "tag": "تاج",
+        "tag": "الوسم",
         "tagDesc": "تاج فريد",
         "address": "العنوان",
         "reverse": "عكسي",
-        "domain": "دومين",
+        "domain": "النطاق",
         "type": "النوع",
-        "bridge": "جسر",
-        "portal": "بوابة",
-        "link": "رابط",
+        "bridge": "Bridge",
+        "portal": "Portal",
+        "link": "الرابط",
         "intercon": "تواصل",
         "settings": "إعدادات",
         "accountInfo": "معلومات الحساب",
@@ -860,6 +1239,8 @@
         "testSuccess": "الاختبار ناجح",
         "testFailed": "فشل الاختبار",
         "testError": "فشل اختبار المخرج",
+        "testModeTooltip": "TCP: فحص dial سريع. HTTP: طلب كامل عبر xray.",
+        "testAll": "اختبار الكل",
         "nordvpn": "NordVPN",
         "accessToken": "رمز الوصول",
         "country": "الدولة",
@@ -874,8 +1255,18 @@
         "editBalancer": "عدل موازن التحميل",
         "balancerStrategy": "استراتيجية الموازن",
         "balancerSelectors": "المحددات",
-        "tag": "تاج",
+        "tag": "الوسم",
         "tagDesc": "تاج فريد",
+        "tagDuplicate": "الوسم مستخدم بالفعل من قبل موازن آخر",
+        "tagPlaceholder": "وسم موازن فريد",
+        "selector": "المحدد",
+        "fallback": "Fallback",
+        "expected": "المتوقع",
+        "expectedPlaceholder": "العدد الأمثل للعقد",
+        "maxRtt": "أقصى RTT",
+        "tolerance": "التحمل",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "ماينفعش تستخدم balancerTag و outboundTag مع بعض. لو اتستخدموا مع بعض، outboundTag هو اللي هيشتغل."
       },
       "wireguard": {
@@ -892,6 +1283,38 @@
         "userLevel": "مستوى المستخدم",
         "userLevelDesc": "ستستخدم جميع الاتصالات المُرسلة عبر هذا الإدخال مستوى المستخدم هذا. القيمة الافتراضية هي 0"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "المفتاح الخاص",
+        "noServers": "لم يتم العثور على خوادم للدولة المحددة",
+        "noPublicKey": "الخادم المحدد لا يُعلن عن مفتاح NordLynx العام.",
+        "outboundAdded": "تمت إضافة صادر NordVPN",
+        "outboundUpdated": "تم تحديث صادر NordVPN"
+      },
+      "warp": {
+        "licenseError": "فشل تعيين رخصة WARP.",
+        "fetchFirst": "احصل على تكوين WARP أولاً.",
+        "createAccount": "إنشاء حساب WARP",
+        "accessToken": "Access token",
+        "deviceId": "معرف الجهاز",
+        "licenseKey": "مفتاح الرخصة",
+        "privateKey": "المفتاح الخاص",
+        "deleteAccount": "حذف الحساب",
+        "settings": "الإعدادات",
+        "licenseKeyLabel": "مفتاح رخصة WARP / WARP+",
+        "key": "المفتاح",
+        "keyPlaceholder": "مفتاح WARP+ مكوّن من 26 حرفاً",
+        "accountInfo": "معلومات الحساب",
+        "deviceName": "اسم الجهاز",
+        "deviceModel": "طراز الجهاز",
+        "deviceEnabled": "الجهاز مفعّل",
+        "accountType": "نوع الحساب",
+        "role": "الدور",
+        "warpPlusData": "بيانات WARP+",
+        "quota": "الحصة",
+        "usage": "الاستخدام",
+        "addOutbound": "إضافة صادر"
+      },
       "dns": {
         "enable": "فعل DNS",
         "enableDesc": "فعل سيرفر DNS المدمج",
@@ -911,7 +1334,7 @@
         "strategyDesc": "الاستراتيجية العامة لحل أسماء الدومين",
         "add": "أضف سيرفر",
         "edit": "عدل السيرفر",
-        "domains": "الدومينات",
+        "domains": "النطاقات",
         "expectIPs": "العناوين المتوقعة",
         "unexpectIPs": "عناوين IP غير متوقعة",
         "useSystemHosts": "استخدام ملف Hosts الخاص بالنظام",
@@ -992,16 +1415,16 @@
       "2faFailed": "فشل 2FA",
       "report": "🕰 التقارير المجدولة: {{ .RunTime }}\r\n",
       "datetime": "⏰ التاريخ والوقت: {{ .DateTime }}\r\n",
-      "hostname": "💻 السيرفر: {{ .Hostname }}\r\n",
+      "hostname": "💻 المضيف: {{ .Hostname }}\r\n",
       "version": "🚀 نسخة 3X-UI: {{ .Version }}\r\n",
       "xrayVersion": "📡 نسخة Xray: {{ .XrayVersion }}\r\n",
       "ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
       "ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
       "ip": "🌐 IP: {{ .IP }}\r\n",
-      "ips": "🔢 عناوين IP:\r\n{{ .IPs }}\r\n",
+      "ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
       "serverUpTime": "⏳ وقت التشغيل: {{ .UpTime }} {{ .Unit }}\r\n",
       "serverLoad": "📈 تحميل النظام: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
-      "serverMemory": "📋 الرام: {{ .Current }}/{{ .Total }}\r\n",
+      "serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
       "tcpCount": "🔹 TCP: {{ .Count }}\r\n",
       "udpCount": "🔸 UDP: {{ .Count }}\r\n",
       "traffic": "🚦 الترافيك: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
@@ -1009,17 +1432,17 @@
       "username": "👤 اسم المستخدم: {{ .Username }}\r\n",
       "reason": "❗️ السبب: {{ .Reason }}\r\n",
       "time": "⏰ الوقت: {{ .Time }}\r\n",
-      "inbound": "📍 الإدخال: {{ .Remark }}\r\n",
-      "port": "🔌 البورت: {{ .Port }}\r\n",
+      "inbound": "📍 الوارد: {{ .Remark }}\r\n",
+      "port": "🔌 المنفذ: {{ .Port }}\r\n",
       "expire": "📅 تاريخ الانتهاء: {{ .Time }}\r\n",
       "expireIn": "📅 هيخلص بعد: {{ .Time }}\r\n",
       "active": "💡 مفعل: {{ .Enable }}\r\n",
       "enabled": "🚨 مفعل: {{ .Enable }}\r\n",
       "online": "🌐 حالة الاتصال: {{ .Status }}\r\n",
       "lastOnline": "🔙 آخر متصل: {{ .Time }}\r\n",
-      "email": "📧 الإيميل: {{ .Email }}\r\n",
-      "upload": "🔼 رفع: ↑{{ .Upload }}\r\n",
-      "download": "🔽 تنزيل: ↓{{ .Download }}\r\n",
+      "email": "📧 البريد: {{ .Email }}\r\n",
+      "upload": "🔼 الرفع: ↑{{ .Upload }}\r\n",
+      "download": "🔽 التنزيل: ↓{{ .Download }}\r\n",
       "total": "📊 الإجمالي: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
       "TGUser": "👤 مستخدم Telegram: {{ .TelegramID }}\r\n",
       "exhaustedMsg": "🚨 نفذ {{ .Type }}:\r\n",
@@ -1030,7 +1453,7 @@
       "backupTime": "🗄 وقت النسخة الاحتياطية: {{ .Time }}\r\n",
       "refreshedOn": "\r\n📋🔄 اتحدّث في: {{ .Time }}\r\n\r\n",
       "yes": "✅ أيوه",
-      "no": "❌ لأ",
+      "no": "❌ لا",
       "received_id": "🔑📥 الـ ID اتحدث.",
       "received_password": "🔑📥 الباسورد اتحدث.",
       "received_email": "📧📥 الإيميل اتحدث.",
@@ -1042,7 +1465,7 @@
       "inbound_client_data_id": "🔄 الدخول: {{ .InboundRemark }}\n\n🔑 المعرف: {{ .ClientId }}\n📧 البريد الإلكتروني: {{ .ClientEmail }}\n📊 الترافيك: {{ .ClientTraffic }}\n📅 تاريخ الانتهاء: {{ .ClientExp }}\n🌐 حدّ IP: {{ .IpLimit }}\n💬 تعليق: {{ .ClientComment }}\n\nدلوقتي تقدر تضيف العميل على الدخول!",
       "inbound_client_data_pass": "🔄 الدخول: {{ .InboundRemark }}\n\n🔑 كلمة المرور: {{ .ClientPass }}\n📧 البريد الإلكتروني: {{ .ClientEmail }}\n📊 الترافيك: {{ .ClientTraffic }}\n📅 تاريخ الانتهاء: {{ .ClientExp }}\n🌐 حدّ IP: {{ .IpLimit }}\n💬 تعليق: {{ .ClientComment }}\n\nدلوقتي تقدر تضيف العميل على الدخول!",
       "cancel": "❌ العملية اتلغت! \n\nممكن تبدأ من /start في أي وقت. 🔄",
-      "error_add_client": "⚠️ حصل خطأ:\n\n {{ .error }}",
+      "error_add_client": "⚠️ خطأ:\n\n {{ .error }}",
       "using_default_value": "تمام، هشيل على القيمة الافتراضية. 😊",
       "incorrect_input": "المدخلات مش صحيحة.\nالكلمات لازم تكون متصلة من غير فراغات.\nمثال صحيح: aaaaaa\nمثال غلط: aaa aaa 🚫",
       "AreYouSure": "إنت متأكد؟ 🤔",
@@ -1087,11 +1510,11 @@
       "submitDisable": "إرسال كمعطّل ☑️",
       "submitEnable": "إرسال كمفعّل ✅",
       "use_default": "🏷️ استخدام الإعدادات الافتراضية",
-      "change_id": "⚙️🔑 المعرّف",
+      "change_id": "⚙️🔑 ID",
       "change_password": "⚙️🔑 كلمة السر",
-      "change_email": "⚙️📧 البريد الإلكتروني",
+      "change_email": "⚙️📧 البريد",
       "change_comment": "⚙️💬 تعليق",
-      "change_flow": "⚙️🚦 التدفق",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "إعادة ضبط جميع الترافيك",
       "SortedTrafficUsageReport": "تقرير استخدام الترافيك المرتب"
     },
@@ -1119,4 +1542,4 @@
       "chooseInbound": "اختار الإدخال"
     }
   }
-}
+}

+ 354 - 11
web/translation/en-US.json

@@ -8,6 +8,8 @@
   "save": "Save",
   "logout": "Log Out",
   "create": "Create",
+  "add": "Add",
+  "remove": "Remove",
   "update": "Update",
   "copy": "Copy",
   "copied": "Copied",
@@ -299,17 +301,29 @@
       "delAllClientsConfirmTitle": "Delete all {count} clients from \"{remark}\"?",
       "delAllClientsConfirmContent": "This removes every client from this inbound and drops their traffic records. The inbound itself is kept. This cannot be undone.",
       "attachClients": "Attach Clients To…",
-      "assignClientsGroup": "Assign Clients To Group…",
+      "addClientsToGroup": "Add Clients To Group…",
       "attachClientsTitle": "Attach clients from \"{remark}\"",
       "attachClientsDesc": "Attaches the same {count} clients (same UUID/password and shared traffic) to the selected inbound(s). They stay on this inbound too.",
       "attachClientsTargets": "Target inbounds",
       "attachClientsNoTargets": "No other compatible inbounds available to attach to.",
       "attachClientsResult": "Attached {attached}, skipped {skipped}.",
       "attachClientsResultMixed": "Attached {attached}, skipped {skipped}, errors {errors}.",
+      "attachClientsSelectLabel": "Clients to attach",
+      "attachClientsSearchPlaceholder": "Search email or comment",
+      "attachClientsStatusDisabled": "Disabled",
+      "attachClientsSelectedCount": "{selected} of {total} selected",
+      "detachClients": "Detach Clients",
+      "detachClientsTitle": "Detach clients of \"{remark}\"",
+      "detachClientsDesc": "Removes the selected client(s) from this inbound only. Client records themselves are kept (use Delete to remove fully). Source has {count} clients in total.",
+      "detachClientsResult": "Detached {detached}, skipped {skipped}.",
+      "detachClientsResultMixed": "Detached {detached}, skipped {skipped}, errors {errors}.",
+      "detachClientsSelectLabel": "Clients to detach",
       "exportLinksTitle": "Export inbound links",
       "exportSubsTitle": "Export subscription links",
       "exportAllLinksTitle": "Export all inbound links",
       "exportAllSubsTitle": "Export all subscription links",
+      "exportAllLinksFileName": "All-Inbounds",
+      "exportAllSubsFileName": "All-Inbounds-Subs",
       "inboundJsonTitle": "Inbound JSON",
       "deleteClient": "Delete Client",
       "deleteClientContent": "Are you sure you want to delete this client?",
@@ -384,7 +398,6 @@
       },
       "telegramDesc": "Please provide Telegram Chat ID. (use '/id' command in the bot) or ({'@'}userinfobot)",
       "subscriptionDesc": "To find your subscription URL, navigate to the 'Details'. Additionally, you can use the same name for several clients.",
-      "info": "Info",
       "same": "Same",
       "inboundData": "Inbound's Data",
       "exportInbound": "Export Inbound",
@@ -421,6 +434,143 @@
         "getNewmldsa65Error": "Error while obtaining mldsa65.",
         "getNewVlessEncError": "Error while obtaining VlessEnc."
       },
+      "form": {
+        "moveUp": "Move up",
+        "moveDown": "Move down",
+        "addAll": "Add all",
+        "addAllFallbackTooltip": "Add a fallback row for every eligible inbound not yet wired up",
+        "peers": "Peers",
+        "addPeer": "Add peer",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "Windows-only. CIDRs added to the system routing table automatically so matching traffic goes through TUN.",
+        "autoOutboundsInterface": "Auto outbounds interface",
+        "autoOutboundsInterfaceTooltip": "Physical interface for outbound traffic. Use 'auto' to detect; auto-enabled when Auto system routes is set.",
+        "rewriteAddress": "Rewrite address",
+        "rewritePort": "Rewrite port",
+        "allowedNetwork": "Allowed network",
+        "followRedirect": "Follow redirect",
+        "accounts": "Accounts",
+        "allowTransparent": "Allow transparent",
+        "encryptionMethod": "Encryption method",
+        "visionTestseed": "Vision testseed",
+        "version": "Version",
+        "udpIdleTimeout": "UDP idle timeout (s)",
+        "masquerade": "Masquerade",
+        "type": "Type",
+        "upstreamUrl": "Upstream URL",
+        "rewriteHost": "Rewrite Host",
+        "skipTlsVerify": "Skip TLS verify",
+        "directory": "Directory",
+        "statusCode": "Status code",
+        "body": "Body",
+        "headers": "Headers",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "Request version",
+        "requestMethod": "Request method",
+        "requestPath": "Request path",
+        "requestHeaders": "Request headers",
+        "responseVersion": "Response version",
+        "responseStatus": "Response status",
+        "responseReason": "Response reason",
+        "responseHeaders": "Response headers",
+        "heartbeatPeriod": "Heartbeat Period",
+        "serviceName": "Service Name",
+        "authority": "Authority",
+        "multiMode": "Multi Mode",
+        "maxBufferedUpload": "Max Buffered Upload",
+        "maxUploadSize": "Max Upload Size (Byte)",
+        "streamUpServer": "Stream-Up Server",
+        "serverMaxHeaderBytes": "Server Max Header Bytes",
+        "paddingBytes": "Padding Bytes",
+        "uplinkHttpMethod": "Uplink HTTP Method",
+        "paddingObfsMode": "Padding Obfs Mode",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "Padding Placement",
+        "paddingMethod": "Padding Method",
+        "sessionPlacement": "Session Placement",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence Placement",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink Data Placement",
+        "uplinkDataKey": "Uplink Data Key",
+        "noSseHeader": "No SSE Header",
+        "ttiMs": "TTI (ms)",
+        "uplinkMbps": "Uplink (MB/s)",
+        "downlinkMbps": "Downlink (MB/s)",
+        "cwndMultiplier": "CWND Multiplier",
+        "maxSendingWindow": "Max Sending Window",
+        "externalProxy": "External Proxy",
+        "sniPlaceholder": "SNI (defaults to host)",
+        "fingerprint": "Fingerprint",
+        "defaultOption": "Default",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive Interval",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "V6 Only",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "Trusted X-Forwarded-For",
+        "addressPortStrategy": "Address+port strategy",
+        "tryDelayMs": "Try delay (ms)",
+        "prioritizeIPv6": "Prioritize IPv6",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "Max concurrent try",
+        "customSockopt": "Custom sockopt",
+        "addCustomOption": "Add custom option",
+        "serverNameIndication": "Server Name Indication",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "Auto",
+        "minMaxVersion": "Min/Max Version",
+        "rejectUnknownSni": "Reject Unknown SNI",
+        "disableSystemRoot": "Disable System Root",
+        "sessionResumption": "Session Resumption",
+        "oneTimeLoading": "One Time Loading",
+        "usageOption": "Usage Option",
+        "buildChain": "Build Chain",
+        "echKey": "ECH key",
+        "echConfig": "ECH config",
+        "pinnedPeerCertSha256": "Pinned Peer Cert SHA-256",
+        "pinnedPeerCertSha256Tip": "Base64-encoded SHA-256 hashes of the peer certificate. Panel-only — not written to the server's xray config, but included in share links so clients can pin the certificate.",
+        "pinnedPeerCertSha256Placeholder": "base64 hash(es), comma-separated",
+        "generateRandomPin": "Generate random hash",
+        "getNewEchCert": "Get New ECH Cert",
+        "show": "Show",
+        "xver": "Xver",
+        "target": "Target",
+        "maxTimeDiff": "Max Time Diff (ms)",
+        "minClientVer": "Min Client Ver",
+        "maxClientVer": "Max Client Ver",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "Get New Cert",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "Get New Seed"
+      },
+      "info": {
+        "mode": "Mode",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "Interface name",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "Outbounds interface",
+        "autoSystemRoutes": "Auto system routes",
+        "followRedirect": "FollowRedirect",
+        "auth": "Auth",
+        "noKernelTun": "No-kernel TUN",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "Peer {n} config"
+      },
       "stream": {
         "general": {
           "request": "Request",
@@ -526,12 +676,32 @@
       "deleteSelected": "Delete ({count})",
       "adjustSelected": "Adjust ({count})",
       "subLinksSelected": "Sub links ({count})",
-      "assignGroupSelected": "Group ({count})",
-      "assignGroupTitle": "Assign group to {count} client(s)",
-      "assignGroupTooltip": "Pick an existing group or type a new name. Leave blank to clear the group on the selected clients.",
-      "assignGroupPlaceholder": "Group name (leave blank to clear)",
-      "assignGroupAssignedToast": "Assigned {count} client(s) to {group}",
-      "assignGroupClearedToast": "Cleared group from {count} client(s)",
+      "addToGroupTitle": "Add {count} client(s) to a group",
+      "addToGroupTooltip": "Pick an existing group or type a new name. Use the Ungroup action to remove clients from their current group.",
+      "addToGroupPlaceholder": "Group name",
+      "addToGroupSuccessToast": "Added {count} client(s) to {group}",
+      "ungroupSuccessToast": "Cleared group from {count} client(s)",
+      "ungroup": "Ungroup",
+      "ungroupConfirmTitle": "Remove {count} client(s) from their group?",
+      "ungroupConfirmContent": "Clears the group label on each selected client. Clients themselves are kept (use Delete to remove them entirely).",
+      "addToGroup": "Add to group",
+      "attach": "Attach",
+      "adjust": "Adjust",
+      "subLinks": "Sub links",
+      "selectedCount": "{count} selected",
+      "attachSelected": "Attach ({count})",
+      "attachToInboundsTitle": "Attach {count} client(s) to inbound(s)",
+      "attachToInboundsDesc": "Attaches the selected {count} client(s) (same UUID/password and shared traffic) to the chosen inbound(s). They keep their existing attachments too.",
+      "attachToInboundsTargets": "Target inbounds",
+      "attachToInboundsNoTargets": "No multi-user inbounds available to attach to.",
+      "detachSelected": "Detach ({count})",
+      "detach": "Detach",
+      "detachFromInboundsTitle": "Detach {count} client(s) from inbound(s)",
+      "detachFromInboundsDesc": "Removes the selected {count} client(s) from the chosen inbound(s). Pairs where the client wasn't attached are silently skipped. Client records are kept (use Delete to remove fully).",
+      "detachFromInboundsTargets": "Inbounds to detach from",
+      "detachFromInboundsNoTargets": "No multi-user inbounds available.",
+      "detachFromInboundsResult": "Detached {detached}, skipped {skipped}.",
+      "detachFromInboundsResultMixed": "Detached {detached}, skipped {skipped}, errors {errors}.",
       "subLinksTitle": "Sub links ({count})",
       "subLinkColumn": "Subscription URL",
       "subJsonLinkColumn": "Subscription JSON URL",
@@ -602,7 +772,16 @@
       "deleteClientsConfirmTitle": "Delete all clients in {name}?",
       "deleteClientsConfirmContent": "This permanently removes {count} client(s) along with their traffic records. The group label is cleared too. This cannot be undone.",
       "deleteClientsSuccess": "Deleted {count} client(s).",
-      "deleteClientsMixed": "{ok} deleted, {failed} skipped"
+      "deleteClientsMixed": "{ok} deleted, {failed} skipped",
+      "addToGroup": "Add clients…",
+      "addToGroupTitle": "Add clients to group \"{name}\"",
+      "addToGroupDesc": "Select clients to add to this group. They keep their existing inbound attachments; only the group label changes. Clients already in this group are not listed.",
+      "addToGroupEmpty": "No other clients available to add.",
+      "addToGroupResult": "Added {count} client(s) to {name}.",
+      "removeFromGroup": "Remove clients…",
+      "removeFromGroupTitle": "Remove clients from group \"{name}\"",
+      "removeFromGroupDesc": "Select members to remove from this group. Clients themselves are kept (use \"Delete clients in group\" to remove them entirely).",
+      "removeFromGroupResult": "Removed {count} client(s) from {name}."
     },
     "nodes": {
       "title": "Nodes",
@@ -736,6 +915,8 @@
       "subEnable": "Subscription Service",
       "subEnableDesc": "Enable/Disable the subscription service.",
       "subJsonEnable": "Enable/Disable the JSON subscription endpoint independently.",
+      "subJsonEnableTitle": "JSON subscription",
+      "subClashEnableTitle": "Clash / Mihomo subscription",
       "subTitle": "Subscription Title",
       "subTitleDesc": "Title shown in VPN client",
       "subSupportUrl": "Support URL",
@@ -781,6 +962,54 @@
       "fragmentSett": "Fragmentation Settings",
       "noisesDesc": "Enable Noises.",
       "noisesSett": "Noises Settings",
+      "trustedProxyCidrs": "Trusted proxy CIDRs",
+      "trustedProxyCidrsDesc": "Comma-separated IPs/CIDRs allowed to set forwarded host, proto, and client IP headers.",
+      "ldap": {
+        "enable": "Enable LDAP sync",
+        "host": "LDAP host",
+        "port": "LDAP port",
+        "useTls": "Use TLS (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "Configured; leave blank to keep current password.",
+        "passwordUnconfigured": "Not configured.",
+        "passwordPlaceholder": "Configured - enter a new value to replace",
+        "baseDn": "Base DN",
+        "userFilter": "User filter",
+        "userAttr": "User attribute (username/email)",
+        "vlessField": "VLESS flag attribute",
+        "flagField": "Generic flag attribute (optional)",
+        "flagFieldDesc": "If set, overrides VLESS flag — e.g. shadowInactive.",
+        "truthyValues": "Truthy values",
+        "truthyValuesDesc": "Comma-separated; default: true,1,yes,on",
+        "invertFlag": "Invert flag",
+        "invertFlagDesc": "Enable when the attribute means disabled (e.g. shadowInactive).",
+        "syncSchedule": "Sync schedule",
+        "syncScheduleDesc": "Cron-like string, e.g. @every 1m",
+        "inboundTags": "Inbound tags",
+        "inboundTagsDesc": "Inbounds that LDAP sync may auto-create or auto-delete clients on.",
+        "noInbounds": "No inbounds found. Create one in Inbounds first.",
+        "autoCreate": "Auto create clients",
+        "autoDelete": "Auto delete clients",
+        "defaultTotalGb": "Default total (GB)",
+        "defaultExpiryDays": "Default expiry (days)",
+        "defaultIpLimit": "Default IP limit"
+      },
+      "subFormats": {
+        "packets": "Packets",
+        "length": "Length",
+        "interval": "Interval",
+        "maxSplit": "Max split",
+        "noises": "Noises",
+        "noiseItem": "Noise №{n}",
+        "type": "Type",
+        "packet": "Packet",
+        "delayMs": "Delay (ms)",
+        "applyTo": "Apply to",
+        "addNoise": "+ Noise",
+        "concurrency": "Concurrency",
+        "xudpConcurrency": "xudp concurrency",
+        "xudpUdp443": "xudp UDP 443"
+      },
       "mux": "Mux",
       "muxDesc": "Transmit multiple independent data streams within an established data stream.",
       "muxSett": "Mux Settings",
@@ -836,6 +1065,9 @@
       "save": "Save",
       "restart": "Restart Xray",
       "restartSuccess": "Xray has been successfully relaunched.",
+      "restartOutputTitle": "Xray restart output",
+      "restartConfirmTitle": "Restart xray?",
+      "restartConfirmContent": "Reloads the xray service with the saved configuration.",
       "stopSuccess": "Xray has been successfully stopped.",
       "restartError": "There was an error when rebooting the Xray.",
       "stopError": "There was an error when stopping the Xray.",
@@ -910,7 +1142,74 @@
         "info": "Info",
         "add": "Add Rule",
         "edit": "Edit Rule",
-        "useComma": "Comma-separated items"
+        "useComma": "Comma-separated list"
+      },
+      "routing": {
+        "dragToReorder": "Drag to reorder"
+      },
+      "ruleForm": {
+        "sourceIps": "Source IPs",
+        "sourcePort": "Source port",
+        "vlessRoute": "VLESS route",
+        "attributes": "Attributes",
+        "value": "Value",
+        "user": "User",
+        "inboundTags": "Inbound tags",
+        "outboundTag": "Outbound tag",
+        "balancerTag": "Balancer tag",
+        "balancerTagTooltip": "Routes traffic through one of the configured load balancers"
+      },
+      "outboundForm": {
+        "tagDuplicate": "Tag already used by another outbound",
+        "tagRequired": "Tag is required",
+        "tagPlaceholder": "unique-tag",
+        "localIpPlaceholder": "local IP",
+        "addressRequired": "Address is required",
+        "portRequired": "Port is required",
+        "optional": "optional",
+        "udpOverTcp": "UDP over TCP",
+        "uotVersion": "UoT version",
+        "inboundTag": "Inbound tag",
+        "inboundTagPlaceholder": "inbound tag used in routing rules",
+        "responseType": "Response type",
+        "rewriteNetwork": "Rewrite network",
+        "unchanged": "(unchanged)",
+        "unchangedAddress": "(unchanged) e.g. 1.1.1.1",
+        "rules": "Rules",
+        "ruleN": "Rule {n}",
+        "action": "Action",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "Final Rules",
+        "overrideXrayPrivateIp": "Override Xray's default private-IP block",
+        "blockDelay": "Block delay (ms)",
+        "reverseSniffing": "Reverse Sniffing",
+        "workers": "Workers",
+        "reserved": "Reserved",
+        "minUploadInterval": "Min upload interval (ms)",
+        "maxUploadSizeBytes": "Max upload size (bytes)",
+        "uplinkChunkSize": "Uplink chunk size",
+        "noGrpcHeader": "No gRPC header",
+        "maxConcurrency": "Max concurrency",
+        "maxConnections": "Max connections",
+        "maxReuseTimes": "Max reuse times",
+        "maxRequestTimes": "Max request times",
+        "maxReusableSecs": "Max reusable secs",
+        "keepAlivePeriod": "Keep alive period",
+        "authPassword": "Auth password",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "server name",
+        "verifyPeerName": "Verify peer name",
+        "pinnedSha256": "Pinned SHA256",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "Keep alive interval",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "Interface",
+        "ipv6Only": "IPv6 only",
+        "acceptProxyProtocol": "Accept proxy protocol",
+        "tcpUserTimeoutMs": "TCP user timeout (ms)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
       },
       "outbound": {
         "addOutbound": "Add Outbound",
@@ -940,6 +1239,8 @@
         "testSuccess": "Test successful",
         "testFailed": "Test failed",
         "testError": "Failed to test outbound",
+        "testModeTooltip": "TCP: fast dial-only probe. HTTP: full request through xray.",
+        "testAll": "Test all",
         "nordvpn": "NordVPN",
         "accessToken": "Access Token",
         "country": "Country",
@@ -956,6 +1257,16 @@
         "balancerSelectors": "Selectors",
         "tag": "Tag",
         "tagDesc": "Unique Tag",
+        "tagDuplicate": "Tag already used by another balancer",
+        "tagPlaceholder": "unique balancer tag",
+        "selector": "Selector",
+        "fallback": "Fallback",
+        "expected": "Expected",
+        "expectedPlaceholder": "optimal node count",
+        "maxRtt": "Max RTT",
+        "tolerance": "Tolerance",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "It is not possible to use balancerTag and outboundTag at the same time. If used at the same time, only outboundTag will work."
       },
       "wireguard": {
@@ -972,6 +1283,38 @@
         "userLevel": "User Level",
         "userLevelDesc": "All connections made through this inbound will use this user level. Default is 0"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "Private key",
+        "noServers": "No servers found for the selected country",
+        "noPublicKey": "Selected server does not advertise a NordLynx public key.",
+        "outboundAdded": "NordVPN outbound added",
+        "outboundUpdated": "NordVPN outbound updated"
+      },
+      "warp": {
+        "licenseError": "Failed to set WARP license.",
+        "fetchFirst": "Fetch the WARP config first.",
+        "createAccount": "Create WARP account",
+        "accessToken": "Access token",
+        "deviceId": "Device ID",
+        "licenseKey": "License key",
+        "privateKey": "Private key",
+        "deleteAccount": "Delete account",
+        "settings": "Settings",
+        "licenseKeyLabel": "WARP / WARP+ license key",
+        "key": "Key",
+        "keyPlaceholder": "26-char WARP+ key",
+        "accountInfo": "Account info",
+        "deviceName": "Device name",
+        "deviceModel": "Device model",
+        "deviceEnabled": "Device enabled",
+        "accountType": "Account type",
+        "role": "Role",
+        "warpPlusData": "WARP+ data",
+        "quota": "Quota",
+        "usage": "Usage",
+        "addOutbound": "Add outbound"
+      },
       "dns": {
         "enable": "Enable DNS",
         "enableDesc": "Enable built-in DNS server",
@@ -1199,4 +1542,4 @@
       "chooseInbound": "Choose an Inbound"
     }
   }
-}
+}

+ 474 - 51
web/translation/es-ES.json

@@ -8,15 +8,22 @@
   "save": "Guardar",
   "logout": "Cerrar Sesión",
   "create": "Crear",
+  "add": "Añadir",
+  "remove": "Quitar",
   "update": "Actualizar",
   "copy": "Copiar",
   "copied": "Copiado",
+  "more": "más",
   "download": "Descargar",
   "remark": "Notas",
   "enable": "Habilitar",
   "protocol": "Protocolo",
   "search": "Buscar",
   "filter": "Filtrar",
+  "all": "Todos",
+  "from": "Desde",
+  "to": "Hasta",
+  "done": "Hecho",
   "loading": "Cargando...",
   "refresh": "Actualizar",
   "clear": "Borrar",
@@ -27,7 +34,7 @@
   "check": "Verificar",
   "indefinite": "Indefinido",
   "unlimited": "Ilimitado",
-  "none": "None",
+  "none": "Ninguno",
   "qrCode": "Código QR",
   "info": "Más Información",
   "edit": "Editar",
@@ -40,15 +47,15 @@
   "useIPv4ForHost": "Usar IPv4 para el host",
   "transmission": "Transmisión",
   "host": "Host",
-  "path": "Path",
-  "camouflage": "Camuflaje",
+  "path": "Ruta",
+  "camouflage": "Ofuscación",
   "status": "Estado",
   "enabled": "Habilitado",
   "disabled": "Deshabilitado",
   "depleted": "Agotado",
   "depletingSoon": "Agotándose",
-  "offline": "fuera de línea",
-  "online": "en línea",
+  "offline": "Sin conexión",
+  "online": "En línea",
   "domainName": "Nombre de dominio",
   "monitor": "Listening IP",
   "certificate": "Certificado Digital",
@@ -97,9 +104,10 @@
     "dashboard": "Estado del Sistema",
     "inbounds": "Entradas",
     "clients": "Clientes",
+    "groups": "Grupos",
     "nodes": "Nodos",
-    "settings": "Configuraciones",
-    "xray": "Ajustes Xray",
+    "settings": "Ajustes del panel",
+    "xray": "Configuración Xray",
     "apiDocs": "Documentación de la API",
     "logout": "Cerrar Sesión",
     "link": "Gestionar",
@@ -123,7 +131,7 @@
       "cpu": "CPU",
       "logicalProcessors": "Procesadores lógicos",
       "frequency": "Frecuencia",
-      "swap": "Memoria Virtual",
+      "swap": "Swap",
       "storage": "Almacenamiento",
       "memory": "RAM",
       "threads": "Hilos",
@@ -166,7 +174,7 @@
       "toggleIpVisibility": "Alternar visibilidad de la IP",
       "overallSpeed": "Velocidad general",
       "upload": "Subida",
-      "download": "Descarga",
+      "download": "Descargar",
       "totalData": "Datos totales",
       "sent": "Enviado",
       "received": "Recibido",
@@ -228,7 +236,7 @@
       "dontRefresh": "La instalación está en progreso, por favor no actualices esta página.",
       "logs": "Registros",
       "config": "Configuración",
-      "backup": "Сopia de Seguridad",
+      "backup": "Copia de seguridad",
       "backupTitle": "Copia & Restauración",
       "exportDatabase": "Copia de seguridad",
       "exportDatabaseDesc": "Haz clic para descargar un archivo .db que contiene una copia de seguridad de tu base de datos actual en tu dispositivo.",
@@ -270,14 +278,14 @@
       },
       "protocol": "Protocolo",
       "port": "Puerto",
-      "portMap": "Puertos de Destino",
+      "portMap": "Asignación de puertos",
       "traffic": "Tráfico",
       "details": "Detalles",
       "transportConfig": "Transporte",
       "expireDate": "Fecha de Expiración",
       "createdAt": "Creado",
       "updatedAt": "Actualizado",
-      "resetTraffic": "Restablecer Tráfico",
+      "resetTraffic": "Restablecer tráfico",
       "addInbound": "Agregar Entrada",
       "generalActions": "Acciones Generales",
       "modifyInbound": "Modificar Entrada",
@@ -292,11 +300,31 @@
       "delAllClients": "Eliminar todos los clientes",
       "delAllClientsConfirmTitle": "¿Eliminar los {count} clientes de \"{remark}\"?",
       "delAllClientsConfirmContent": "Elimina todos los clientes de este inbound y sus registros de tráfico. El inbound se mantiene. Esto no se puede deshacer.",
+      "attachClients": "Asociar clientes a…",
+      "addClientsToGroup": "Añadir clientes al grupo…",
+      "attachClientsTitle": "Asociar clientes desde «{remark}»",
+      "attachClientsDesc": "Asocia los mismos {count} cliente(s) (mismo UUID/contraseña y tráfico compartido) a las entradas seleccionadas. Permanecen también en esta entrada.",
+      "attachClientsTargets": "Entradas objetivo",
+      "attachClientsNoTargets": "No hay otras entradas compatibles disponibles para asociar.",
+      "attachClientsResult": "Asociados {attached}, omitidos {skipped}.",
+      "attachClientsResultMixed": "Asociados {attached}, omitidos {skipped}, errores {errors}.",
+      "attachClientsSelectLabel": "Clientes para asociar",
+      "attachClientsSearchPlaceholder": "Buscar email o comentario",
+      "attachClientsStatusDisabled": "Deshabilitado",
+      "attachClientsSelectedCount": "{selected} de {total} seleccionado(s)",
+      "detachClients": "Desasociar clientes",
+      "detachClientsTitle": "Desasociar clientes de «{remark}»",
+      "detachClientsDesc": "Quita el cliente o clientes seleccionados solo de esta entrada. Los registros se conservan (usa Delete para eliminar por completo). El origen tiene {count} cliente(s) en total.",
+      "detachClientsResult": "Desasociados {detached}, omitidos {skipped}.",
+      "detachClientsResultMixed": "Desasociados {detached}, omitidos {skipped}, errores {errors}.",
+      "detachClientsSelectLabel": "Clientes para desasociar",
       "exportLinksTitle": "Exportar enlaces del inbound",
       "exportSubsTitle": "Exportar enlaces de suscripción",
       "exportAllLinksTitle": "Exportar todos los enlaces de inbound",
       "exportAllSubsTitle": "Exportar todos los enlaces de suscripción",
-      "inboundJsonTitle": "JSON del inbound",
+      "exportAllLinksFileName": "Todas-las-entradas",
+      "exportAllSubsFileName": "Todas-las-entradas-Subs",
+      "inboundJsonTitle": "JSON de entrada",
       "deleteClient": "Eliminar cliente",
       "deleteClientContent": "¿Está seguro de que desea eliminar el cliente?",
       "resetTrafficContent": "¿Confirmar restablecimiento de tráfico?",
@@ -306,7 +334,7 @@
       "destinationPort": "Puerto de Destino",
       "targetAddress": "Dirección de Destino",
       "monitorDesc": "Dejar en blanco por defecto",
-      "meansNoLimit": " = illimitata. (unidad: GB)",
+      "meansNoLimit": "= Ilimitado. (unidad: GB)",
       "totalFlow": "Flujo Total",
       "leaveBlankToNeverExpire": "Dejar en Blanco para Nunca Expirar",
       "noRecommendKeepDefault": "No hay requisitos especiales para mantener la configuración predeterminada",
@@ -341,9 +369,10 @@
       "IPLimitlogDesc": "Registro de historial de IPs (antes de habilitar la entrada después de que haya sido desactivada por el límite de IP, debes borrar el registro).",
       "IPLimitlogclear": "Limpiar el Registro",
       "setDefaultCert": "Establecer certificado desde el panel",
-      "streamTab": "Stream",
+      "setDefaultCertEmpty": "No hay certificado configurado para el panel. Configura uno en Ajustes primero.",
+      "streamTab": "Transmisión",
       "securityTab": "Seguridad",
-      "sniffingTab": "Sniffing",
+      "sniffingTab": "Inspección",
       "sniffingMetadataOnly": "Solo metadatos",
       "sniffingRouteOnly": "Solo enrutamiento",
       "sniffingIpsExcluded": "IPs excluidas",
@@ -369,7 +398,6 @@
       },
       "telegramDesc": "Por favor, proporciona el ID de Chat de Telegram. (usa el comando '/id' en el bot) o ({'@'}userinfobot)",
       "subscriptionDesc": "Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones.",
-      "info": "Info",
       "same": "misma",
       "inboundData": "Datos de entrada",
       "exportInbound": "Exportación entrante",
@@ -406,6 +434,143 @@
         "getNewmldsa65Error": "Error al obtener el certificado mldsa65.",
         "getNewVlessEncError": "Error al obtener el certificado VlessEnc."
       },
+      "form": {
+        "moveUp": "Subir",
+        "moveDown": "Bajar",
+        "addAll": "Añadir todo",
+        "addAllFallbackTooltip": "Añade una fila de fallback para cada entrada elegible aún no conectada",
+        "peers": "Peers",
+        "addPeer": "Añadir peer",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "Solo Windows. Los CIDR se añaden automáticamente a la tabla de enrutamiento del sistema para que el tráfico coincidente pase por TUN.",
+        "autoOutboundsInterface": "Interfaz de salidas automática",
+        "autoOutboundsInterfaceTooltip": "Interfaz física para tráfico de salida. Usa 'auto' para detectar; se habilita automáticamente cuando se establece Auto system routes.",
+        "rewriteAddress": "Reescribir dirección",
+        "rewritePort": "Reescribir puerto",
+        "allowedNetwork": "Red permitida",
+        "followRedirect": "Seguir redirección",
+        "accounts": "Cuentas",
+        "allowTransparent": "Permitir transparente",
+        "encryptionMethod": "Método de cifrado",
+        "visionTestseed": "Vision testseed",
+        "version": "Versión",
+        "udpIdleTimeout": "UDP idle timeout (s)",
+        "masquerade": "Masquerade",
+        "type": "Tipo",
+        "upstreamUrl": "URL Upstream",
+        "rewriteHost": "Reescribir Host",
+        "skipTlsVerify": "Saltar verificación TLS",
+        "directory": "Directorio",
+        "statusCode": "Código de estado",
+        "body": "Body",
+        "headers": "Cabeceras",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "Versión de petición",
+        "requestMethod": "Método de petición",
+        "requestPath": "Ruta de petición",
+        "requestHeaders": "Cabeceras de petición",
+        "responseVersion": "Versión de respuesta",
+        "responseStatus": "Estado de respuesta",
+        "responseReason": "Razón de respuesta",
+        "responseHeaders": "Cabeceras de respuesta",
+        "heartbeatPeriod": "Periodo de heartbeat",
+        "serviceName": "Nombre de servicio",
+        "authority": "Authority",
+        "multiMode": "Multi Mode",
+        "maxBufferedUpload": "Máx. subida en búfer",
+        "maxUploadSize": "Tamaño máx. de subida (Byte)",
+        "streamUpServer": "Stream-Up Server",
+        "serverMaxHeaderBytes": "Máx. bytes cabecera servidor",
+        "paddingBytes": "Bytes de Padding",
+        "uplinkHttpMethod": "Método HTTP Uplink",
+        "paddingObfsMode": "Modo obfs de Padding",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "Ubicación de Padding",
+        "paddingMethod": "Método de Padding",
+        "sessionPlacement": "Session Placement",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence Placement",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink Data Placement",
+        "uplinkDataKey": "Uplink Data Key",
+        "noSseHeader": "Sin cabecera SSE",
+        "ttiMs": "TTI (ms)",
+        "uplinkMbps": "Subida (MB/s)",
+        "downlinkMbps": "Bajada (MB/s)",
+        "cwndMultiplier": "Multiplicador CWND",
+        "maxSendingWindow": "Máx. ventana de envío",
+        "externalProxy": "Proxy externo",
+        "sniPlaceholder": "SNI (por defecto = host)",
+        "fingerprint": "Fingerprint",
+        "defaultOption": "Por defecto",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive Interval",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "Solo V6",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "X-Forwarded-For de confianza",
+        "addressPortStrategy": "Estrategia dirección+puerto",
+        "tryDelayMs": "Retraso de intento (ms)",
+        "prioritizeIPv6": "Priorizar IPv6",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "Máx. intentos simultáneos",
+        "customSockopt": "Sockopt personalizado",
+        "addCustomOption": "Añadir opción personalizada",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "Auto",
+        "minMaxVersion": "Versión mín/máx",
+        "rejectUnknownSni": "Rechazar SNI desconocido",
+        "disableSystemRoot": "Deshabilitar System Root",
+        "sessionResumption": "Reanudación de sesión",
+        "oneTimeLoading": "Carga única",
+        "usageOption": "Opción de uso",
+        "buildChain": "Construir cadena",
+        "echKey": "ECH key",
+        "echConfig": "Config ECH",
+        "pinnedPeerCertSha256": "SHA-256 del cert. del par fijado",
+        "pinnedPeerCertSha256Tip": "Hashes SHA-256 codificados en Base64 del certificado del par. Solo en el panel — no se escribe en la config xray del servidor, pero se incluye en los enlaces para que los clientes puedan fijar el certificado.",
+        "pinnedPeerCertSha256Placeholder": "hash(es) base64, separados por comas",
+        "generateRandomPin": "Generar hash aleatorio",
+        "getNewEchCert": "Obtener nuevo cert ECH",
+        "show": "Mostrar",
+        "xver": "Xver",
+        "target": "Objetivo",
+        "maxTimeDiff": "Máx. diferencia de tiempo (ms)",
+        "minClientVer": "Mín. versión cliente",
+        "maxClientVer": "Máx. versión cliente",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "Obtener nuevo cert",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "Obtener nuevo Seed"
+      },
+      "info": {
+        "mode": "Modo",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "Nombre de interfaz",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "Interfaz de salidas",
+        "autoSystemRoutes": "Rutas del sistema automáticas",
+        "followRedirect": "FollowRedirect",
+        "auth": "Auth",
+        "noKernelTun": "TUN sin kernel",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "Config Peer {n}"
+      },
       "stream": {
         "general": {
           "request": "Pedido",
@@ -416,7 +581,7 @@
         "tcp": {
           "version": "Versión",
           "method": "Método",
-          "path": "Camino",
+          "path": "Ruta",
           "status": "Estado",
           "statusDescription": "Descripción de la Situación",
           "requestHeader": "Encabezado de solicitud",
@@ -456,6 +621,20 @@
       "days": "Día(s)",
       "renew": "Renovación automática",
       "renewDesc": "Renovación automática tras la expiración. (0 = desactivado) (unidad: día)",
+      "searchPlaceholder": "Buscar email, comentario, sub ID, UUID, contraseña, auth…",
+      "filterTitle": "Filtrar clientes",
+      "clearAllFilters": "Limpiar todo",
+      "sortOldest": "Más antiguos",
+      "sortNewest": "Más recientes",
+      "sortRecentlyUpdated": "Recientemente actualizados",
+      "sortRecentlyOnline": "Recientemente en línea",
+      "sortEmailAZ": "Email A→Z",
+      "sortEmailZA": "Email Z→A",
+      "sortMostTraffic": "Mayor tráfico",
+      "sortHighestRemaining": "Mayor restante",
+      "sortExpiringSoonest": "Caducidad más próxima",
+      "has": "Tiene",
+      "hasNot": "No tiene",
       "title": "Clientes",
       "actions": "Acciones",
       "totalGB": "Total enviado/recibido (GB)",
@@ -465,10 +644,13 @@
       "password": "Contraseña",
       "subId": "ID de suscripción",
       "online": "En línea",
-      "email": "Correo",
+      "email": "Email",
+      "group": "Grupo",
+      "groupDesc": "Etiqueta lógica para agrupar clientes relacionados (p. ej. equipo, cliente, región). Filtrable desde la barra de herramientas.",
+      "groupPlaceholder": "p. ej. customer-a",
       "comment": "Comentario",
       "traffic": "Tráfico",
-      "offline": "Desconectado",
+      "offline": "Sin conexión",
       "addTitle": "Añadir cliente",
       "qrCode": "Código QR",
       "moreInformation": "Más información",
@@ -489,11 +671,45 @@
       "resetAllTraffics": "Restablecer tráfico de todos los clientes",
       "resetAllTrafficsTitle": "¿Restablecer tráfico de todos los clientes?",
       "resetAllTrafficsContent": "El contador de subida/bajada de cada cliente vuelve a cero. Las cuotas y la expiración no se modifican. Esta acción no se puede deshacer.",
-      "empty": "Aún no hay clientes — añade uno para empezar.",
       "deleteConfirmTitle": "¿Eliminar al cliente {email}?",
       "deleteConfirmContent": "Esto elimina al cliente de cada inbound asociado y descarta su registro de tráfico. No se puede deshacer.",
       "deleteSelected": "Eliminar ({count})",
       "adjustSelected": "Ajustar ({count})",
+      "subLinksSelected": "Enlaces sub ({count})",
+      "addToGroupTitle": "Añadir {count} cliente(s) a un grupo",
+      "addToGroupTooltip": "Selecciona un grupo existente o escribe un nombre nuevo. Usa Ungroup para quitar clientes de su grupo actual.",
+      "addToGroupPlaceholder": "Nombre del grupo",
+      "addToGroupSuccessToast": "Se añadieron {count} cliente(s) a {group}",
+      "ungroupSuccessToast": "Grupo limpiado de {count} cliente(s)",
+      "ungroup": "Desagrupar",
+      "ungroupConfirmTitle": "¿Quitar {count} cliente(s) de su grupo?",
+      "ungroupConfirmContent": "Limpia la etiqueta de grupo en cada cliente seleccionado. Los clientes se conservan (usa Delete para eliminarlos por completo).",
+      "addToGroup": "Añadir al grupo",
+      "attach": "Asociar",
+      "adjust": "Ajustar",
+      "subLinks": "Enlaces sub",
+      "selectedCount": "{count} seleccionado(s)",
+      "attachSelected": "Asociar ({count})",
+      "attachToInboundsTitle": "Asociar {count} cliente(s) a entrada(s)",
+      "attachToInboundsDesc": "Asocia los {count} cliente(s) seleccionados (mismo UUID/contraseña y tráfico compartido) a las entradas elegidas. Mantienen sus asociaciones existentes.",
+      "attachToInboundsTargets": "Entradas objetivo",
+      "attachToInboundsNoTargets": "No hay entradas multiusuario disponibles para asociar.",
+      "detachSelected": "Desasociar ({count})",
+      "detach": "Desasociar",
+      "detachFromInboundsTitle": "Desasociar {count} cliente(s) de entrada(s)",
+      "detachFromInboundsDesc": "Quita los {count} cliente(s) seleccionados de las entradas elegidas. Las parejas donde el cliente no estaba asociado se omiten silenciosamente. Los registros de los clientes se conservan (usa Delete para eliminar por completo).",
+      "detachFromInboundsTargets": "Entradas para desasociar",
+      "detachFromInboundsNoTargets": "No hay entradas multiusuario disponibles.",
+      "detachFromInboundsResult": "Desasociados {detached}, omitidos {skipped}.",
+      "detachFromInboundsResultMixed": "Desasociados {detached}, omitidos {skipped}, errores {errors}.",
+      "subLinksTitle": "Enlaces sub ({count})",
+      "subLinkColumn": "URL de suscripción",
+      "subJsonLinkColumn": "URL JSON de suscripción",
+      "subLinksCopyAll": "Copiar todo",
+      "subLinksCopiedAll": "Copiados {count} enlace(s)",
+      "subLinksEmpty": "Ninguno de los clientes seleccionados tiene ID de suscripción.",
+      "subLinksDisabled": "El servicio de suscripción está deshabilitado.",
+      "subLinksDisabledHint": "Habilita la suscripción en Ajustes del panel → Suscripción para generar enlaces.",
       "bulkDeleteConfirmTitle": "¿Eliminar {count} clientes?",
       "bulkDeleteConfirmContent": "Cada cliente seleccionado se elimina de los inbounds asociados y se descarta su registro de tráfico. No se puede deshacer.",
       "bulkAdjustTitle": "Ajustar {count} clientes",
@@ -505,10 +721,11 @@
       "delDepletedConfirmTitle": "¿Eliminar clientes agotados?",
       "delDepletedConfirmContent": "Elimina todos los clientes con cuota agotada o expirados. No se puede deshacer.",
       "auth": "Auth",
-      "hysteriaAuth": "Auth de Hysteria",
+      "hysteriaAuth": "Hysteria Auth",
       "uuid": "UUID",
       "flow": "Flow",
-      "reverseTag": "Reverse tag",
+      "vmessSecurity": "Seguridad VMess",
+      "reverseTag": "Etiqueta inversa",
       "reverseTagPlaceholder": "Reverse tag opcional",
       "telegramId": "ID de usuario de Telegram",
       "telegramIdPlaceholder": "ID numérico de usuario de Telegram (0 = ninguno)",
@@ -528,13 +745,51 @@
         "delDepleted": "{count} clientes agotados eliminados"
       }
     },
+    "groups": {
+      "title": "Grupos",
+      "name": "Nombre",
+      "clientCount": "Clientes en el grupo",
+      "totalGroups": "Total de grupos",
+      "totalGroupedClients": "Clientes con grupo",
+      "emptyGroups": "Grupos vacíos",
+      "addGroup": "Añadir grupo",
+      "createSuccess": "Grupo «{name}» creado.",
+      "rename": "Renombrar",
+      "renameTitle": "Renombrar {name}",
+      "renameCollision": "Ya existe un grupo llamado «{name}».",
+      "renameSuccess": "Grupo renombrado en {count} cliente(s).",
+      "deleteConfirmTitle": "¿Eliminar el grupo {name}?",
+      "deleteConfirmContent": "Esto elimina el grupo y limpia su etiqueta de {count} cliente(s). Los clientes en sí no se eliminan.",
+      "deleteSuccess": "Grupo limpiado de {count} cliente(s).",
+      "resetTraffic": "Restablecer tráfico",
+      "resetConfirmTitle": "¿Restablecer tráfico del grupo {name}?",
+      "resetConfirmContent": "Esto pone a cero up/down para los {count} cliente(s) de este grupo.",
+      "resetSuccess": "Tráfico restablecido en {count} cliente(s).",
+      "adjustSuccess": "Ajustados {count} cliente(s) en {name}.",
+      "emptyForAction": "Este grupo aún no tiene clientes.",
+      "deleteGroupOnly": "Eliminar grupo (conservar clientes)",
+      "deleteClients": "Eliminar clientes del grupo",
+      "deleteClientsConfirmTitle": "¿Eliminar todos los clientes en {name}?",
+      "deleteClientsConfirmContent": "Esto elimina permanentemente {count} cliente(s) junto con sus registros de tráfico. La etiqueta de grupo también se limpia. Esto no se puede deshacer.",
+      "deleteClientsSuccess": "Eliminados {count} cliente(s).",
+      "deleteClientsMixed": "{ok} eliminados, {failed} omitidos",
+      "addToGroup": "Añadir clientes…",
+      "addToGroupTitle": "Añadir clientes al grupo «{name}»",
+      "addToGroupDesc": "Selecciona clientes para añadir a este grupo. Mantienen sus asociaciones de entrada existentes; solo cambia la etiqueta de grupo. Los clientes que ya están en este grupo no se muestran.",
+      "addToGroupEmpty": "No hay otros clientes disponibles para añadir.",
+      "addToGroupResult": "Añadidos {count} cliente(s) a {name}.",
+      "removeFromGroup": "Quitar clientes…",
+      "removeFromGroupTitle": "Quitar clientes del grupo «{name}»",
+      "removeFromGroupDesc": "Selecciona miembros para quitar de este grupo. Los clientes se conservan (usa «Eliminar clientes del grupo» para eliminarlos por completo).",
+      "removeFromGroupResult": "Quitados {count} cliente(s) de {name}."
+    },
     "nodes": {
       "title": "Nodos",
       "addNode": "Agregar nodo",
       "editNode": "Editar nodo",
       "totalNodes": "Total de nodos",
       "onlineNodes": "En línea",
-      "offlineNodes": "Desconectado",
+      "offlineNodes": "Sin conexión",
       "avgLatency": "Latencia media",
       "name": "Nombre",
       "namePlaceholder": "p. ej. de-frankfurt-1",
@@ -544,7 +799,7 @@
       "address": "Dirección",
       "port": "Puerto",
       "basePath": "Ruta base",
-      "apiToken": "Token de API",
+      "apiToken": "Token API",
       "apiTokenPlaceholder": "Token desde la página de Configuración del panel remoto",
       "apiTokenHint": "El panel remoto expone su token de API en Configuración → Token de API.",
       "regenerate": "Regenerar token",
@@ -571,7 +826,7 @@
       "deleteConfirmContent": "Esto detiene la monitorización del nodo. El panel remoto en sí no se ve afectado.",
       "statusValues": {
         "online": "En línea",
-        "offline": "Desconectado",
+        "offline": "Sin conexión",
         "unknown": "Desconocido"
       },
       "toasts": {
@@ -590,7 +845,7 @@
       "title": "Configuraciones",
       "save": "Guardar",
       "infoDesc": "Cada cambio realizado aquí debe ser guardado. Por favor, reinicie el panel para aplicar los cambios.",
-      "restartPanel": "Reiniciar Panel",
+      "restartPanel": "Reiniciar panel",
       "restartPanelDesc": "¿Está seguro de que desea reiniciar el panel? Haga clic en Aceptar para reiniciar después de 3 segundos. Si no puede acceder al panel después de reiniciar, por favor, consulte la información de registro del panel en el servidor.",
       "restartPanelSuccess": "El panel se reinició correctamente",
       "actions": "Acciones",
@@ -604,7 +859,7 @@
       "warnDefaultBasePath": "La ruta base por defecto \"/\" es conocida — cámbiela a una ruta aleatoria.",
       "warnDefaultSubPath": "La ruta de suscripción por defecto \"/sub/\" es conocida — cámbiela.",
       "warnDefaultJsonPath": "La ruta de suscripción JSON por defecto \"/json/\" es conocida — cámbiela.",
-      "TGBotSettings": "Configuraciones de Bot de Telegram",
+      "TGBotSettings": "Bot de Telegram",
       "panelListeningIP": "IP de Escucha del Panel",
       "panelListeningIPDesc": "Dejar en blanco por defecto para monitorear todas las IPs.",
       "panelListeningDomain": "Dominio de Escucha del Panel",
@@ -615,10 +870,12 @@
       "publicKeyPathDesc": "Complete con una ruta absoluta que comience con.",
       "privateKeyPath": "Ruta del Archivo de Clave Privada del Certificado del Panel",
       "privateKeyPathDesc": "Complete con una ruta absoluta que comience con.",
-      "panelUrlPath": "Ruta Raíz de la URL del Panel",
+      "panelUrlPath": "Ruta URI",
       "panelUrlPathDesc": "Debe empezar con '/' y terminar con.",
       "pageSize": "Tamaño de paginación",
       "pageSizeDesc": "Defina el tamaño de página para la tabla de entradas. Establezca 0 para desactivar",
+      "panelProxy": "Proxy de red del panel",
+      "panelProxyDesc": "Enruta las peticiones salientes del propio panel (actualizaciones de geo, comprobaciones de versión de Xray/panel, Telegram) a través de este proxy para sortear el filtrado de GitHub/Telegram en el servidor. Acepta socks5:// o http(s)://, p. ej. una entrada SOCKS local de Xray. Deja vacío para conexión directa.",
       "remarkModel": "Modelo de observación y carácter de separación",
       "datepicker": "selector de fechas",
       "datepickerPlaceholder": "Seleccionar fecha",
@@ -632,9 +889,9 @@
       "telegramBotEnableDesc": "Conéctese a las funciones de este panel a través del bot de Telegram.",
       "telegramToken": "Token de Telegram",
       "telegramTokenDesc": "Debe obtener el token del administrador de bots de Telegram {'@'}botfather.",
-      "telegramProxy": "Socks5 Proxy",
+      "telegramProxy": "Proxy SOCKS",
       "telegramProxyDesc": "Si necesita el proxy Socks5 para conectarse a Telegram. Ajuste su configuración según la guía.",
-      "telegramAPIServer": "API Server de Telegram",
+      "telegramAPIServer": "Servidor API de Telegram",
       "telegramAPIServerDesc": "El servidor API de Telegram a utilizar. Déjelo en blanco para utilizar el servidor predeterminado.",
       "telegramChatId": "IDs de Chat de Telegram para Administradores",
       "telegramChatIdDesc": "IDs de Chat múltiples separados por comas. Use {'@'}userinfobot o use el comando '/id' en el bot para obtener sus IDs de Chat.",
@@ -658,6 +915,8 @@
       "subEnable": "Habilitar Servicio",
       "subEnableDesc": "Función de suscripción con configuración separada.",
       "subJsonEnable": "Habilitar/Deshabilitar el endpoint de suscripción JSON de forma independiente.",
+      "subJsonEnableTitle": "Suscripción JSON",
+      "subClashEnableTitle": "Suscripción Clash / Mihomo",
       "subTitle": "Título de la Suscripción",
       "subTitleDesc": "Título mostrado en el cliente VPN",
       "subSupportUrl": "URL de soporte",
@@ -678,13 +937,13 @@
       "subCertPathDesc": "Complete con una ruta absoluta que comience con '/'",
       "subKeyPath": "Ruta del Archivo de Clave Privada del Certificado de Suscripción",
       "subKeyPathDesc": "Complete con una ruta absoluta que comience con '/'",
-      "subPath": "Ruta Raíz de la URL de Suscripción",
+      "subPath": "Ruta URI",
       "subPathDesc": "Debe empezar con '/' y terminar con '/'",
       "subDomain": "Dominio de Escucha",
       "subDomainDesc": "Dejar en blanco por defecto para monitorear todos los dominios e IPs.",
       "subUpdates": "Intervalos de Actualización de Suscripción",
       "subUpdatesDesc": "Horas de intervalo entre actualizaciones en la aplicación del cliente.",
-      "subEncrypt": "Encriptar configuraciones",
+      "subEncrypt": "Codificar",
       "subEncryptDesc": "Encriptar las configuraciones devueltas en la suscripción.",
       "subShowInfo": "Mostrar información de uso",
       "subShowInfoDesc": "Mostrar tráfico restante y fecha después del nombre de configuración.",
@@ -693,7 +952,7 @@
       "subURI": "URI de proxy inverso",
       "subURIDesc": "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy",
       "externalTrafficInformEnable": "Informe de tráfico externo",
-      "externalTrafficInformEnableDesc": "Informar a la API externa sobre cada actualización de tráfico.",
+      "externalTrafficInformEnableDesc": "Informar a una API externa en cada actualización de tráfico.",
       "externalTrafficInformURI": "URI de información de tráfico externo",
       "externalTrafficInformURIDesc": "Las actualizaciones de tráfico se envían a este URI.",
       "restartXrayOnClientDisable": "Reiniciar Xray tras desactivación automática",
@@ -703,6 +962,54 @@
       "fragmentSett": "Configuración de Fragmentación",
       "noisesDesc": "Activar Sonidos",
       "noisesSett": "Configuración de Sonidos",
+      "trustedProxyCidrs": "CIDR de proxy de confianza",
+      "trustedProxyCidrsDesc": "IP/CIDR separados por coma que pueden establecer las cabeceras de host, proto e IP del cliente reenviadas.",
+      "ldap": {
+        "enable": "Habilitar sincronización LDAP",
+        "host": "Host LDAP",
+        "port": "Puerto LDAP",
+        "useTls": "Usar TLS (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "Configurada; deja en blanco para mantener la contraseña actual.",
+        "passwordUnconfigured": "No configurada.",
+        "passwordPlaceholder": "Configurada — introduce un nuevo valor para reemplazar",
+        "baseDn": "Base DN",
+        "userFilter": "Filtro de usuario",
+        "userAttr": "Atributo de usuario (username/email)",
+        "vlessField": "Atributo flag VLESS",
+        "flagField": "Atributo flag genérico (opcional)",
+        "flagFieldDesc": "Si se establece, sobrescribe el flag VLESS — p. ej. shadowInactive.",
+        "truthyValues": "Valores truthy",
+        "truthyValuesDesc": "Separados por coma; por defecto: true,1,yes,on",
+        "invertFlag": "Invertir flag",
+        "invertFlagDesc": "Habilita cuando el atributo significa «deshabilitado» (p. ej. shadowInactive).",
+        "syncSchedule": "Programación de sincronización",
+        "syncScheduleDesc": "Cadena tipo cron, p. ej. @every 1m",
+        "inboundTags": "Etiquetas de entradas",
+        "inboundTagsDesc": "Entradas en las que la sincronización LDAP puede auto-crear o auto-eliminar clientes.",
+        "noInbounds": "No se encontraron entradas. Crea una en Entradas primero.",
+        "autoCreate": "Crear clientes automáticamente",
+        "autoDelete": "Eliminar clientes automáticamente",
+        "defaultTotalGb": "Total por defecto (GB)",
+        "defaultExpiryDays": "Caducidad por defecto (días)",
+        "defaultIpLimit": "Límite IP por defecto"
+      },
+      "subFormats": {
+        "packets": "Paquetes",
+        "length": "Longitud",
+        "interval": "Intervalo",
+        "maxSplit": "Máx. división",
+        "noises": "Ruidos",
+        "noiseItem": "Ruido №{n}",
+        "type": "Tipo",
+        "packet": "Paquete",
+        "delayMs": "Retraso (ms)",
+        "applyTo": "Aplicar a",
+        "addNoise": "+ Ruido",
+        "concurrency": "Concurrencia",
+        "xudpConcurrency": "Concurrencia xudp",
+        "xudpUdp443": "xudp UDP 443"
+      },
       "mux": "Mux",
       "muxDesc": "Transmite múltiples flujos de datos independientes dentro de un flujo de datos establecido.",
       "muxSett": "Configuración Mux",
@@ -758,6 +1065,9 @@
       "save": "Guardar configuración",
       "restart": "Reiniciar Xray",
       "restartSuccess": "Xray se ha reiniciado correctamente",
+      "restartOutputTitle": "Salida del reinicio de Xray",
+      "restartConfirmTitle": "¿Reiniciar xray?",
+      "restartConfirmContent": "Recarga el servicio xray con la configuración guardada.",
       "stopSuccess": "Xray se ha detenido correctamente",
       "restartError": "Ocurrió un error al reiniciar Xray.",
       "stopError": "Ocurrió un error al detener Xray.",
@@ -790,14 +1100,16 @@
       "outboundTestUrl": "URL de prueba de outbound",
       "outboundTestUrlDesc": "URL usada al probar la conectividad del outbound",
       "Torrent": "Prohibir Uso de BitTorrent",
-      "Inbounds": "Entrante",
+      "Inbounds": "Entradas",
       "InboundsDesc": "Cambia la plantilla de configuración para aceptar clientes específicos.",
       "Outbounds": "Salidas",
       "Balancers": "Equilibradores",
+      "balancerTagRequired": "La etiqueta es obligatoria",
+      "balancerSelectorRequired": "Elige al menos una salida",
       "OutboundsDesc": "Cambia la plantilla de configuración para definir formas de salida para este servidor.",
       "Routings": "Reglas de enrutamiento",
       "RoutingsDesc": "¡La prioridad de cada regla es importante!",
-      "completeTemplate": "Todos",
+      "completeTemplate": "Todo",
       "logLevel": "Nivel de registro",
       "logLevelDesc": "El nivel de registro para registros de errores, que indica la información que debe registrarse.",
       "accessLog": "Registro de acceso",
@@ -827,11 +1139,78 @@
         "inbound": "Entrante",
         "outbound": "Saliente",
         "balancer": "Equilibrador",
-        "info": "Información",
+        "info": "Info",
         "add": "Agregar Regla",
         "edit": "Editar Regla",
         "useComma": "Elementos separados por comas"
       },
+      "routing": {
+        "dragToReorder": "Arrastra para reordenar"
+      },
+      "ruleForm": {
+        "sourceIps": "IPs de origen",
+        "sourcePort": "Puerto de origen",
+        "vlessRoute": "Ruta VLESS",
+        "attributes": "Atributos",
+        "value": "Valor",
+        "user": "Usuario",
+        "inboundTags": "Etiquetas de entradas",
+        "outboundTag": "Etiqueta de salida",
+        "balancerTag": "Etiqueta de balanceador",
+        "balancerTagTooltip": "Enruta el tráfico a través de uno de los balanceadores configurados"
+      },
+      "outboundForm": {
+        "tagDuplicate": "Etiqueta ya usada por otra salida",
+        "tagRequired": "La etiqueta es obligatoria",
+        "tagPlaceholder": "etiqueta-única",
+        "localIpPlaceholder": "IP local",
+        "addressRequired": "La dirección es obligatoria",
+        "portRequired": "El puerto es obligatorio",
+        "optional": "opcional",
+        "udpOverTcp": "UDP sobre TCP",
+        "uotVersion": "Versión UoT",
+        "inboundTag": "Etiqueta de entrada",
+        "inboundTagPlaceholder": "etiqueta de entrada usada en reglas de enrutamiento",
+        "responseType": "Tipo de respuesta",
+        "rewriteNetwork": "Reescribir red",
+        "unchanged": "(sin cambios)",
+        "unchangedAddress": "(sin cambios) p. ej. 1.1.1.1",
+        "rules": "Reglas",
+        "ruleN": "Regla {n}",
+        "action": "Acción",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "Reglas finales",
+        "overrideXrayPrivateIp": "Sobrescribir el bloqueo de IP privada por defecto de Xray",
+        "blockDelay": "Retraso de bloqueo (ms)",
+        "reverseSniffing": "Sniffing inverso",
+        "workers": "Workers",
+        "reserved": "Reservado",
+        "minUploadInterval": "Intervalo mín. de subida (ms)",
+        "maxUploadSizeBytes": "Tamaño máx. de subida (bytes)",
+        "uplinkChunkSize": "Tamaño de chunk Uplink",
+        "noGrpcHeader": "Sin cabecera gRPC",
+        "maxConcurrency": "Máx. concurrencia",
+        "maxConnections": "Máx. conexiones",
+        "maxReuseTimes": "Máx. reutilizaciones",
+        "maxRequestTimes": "Máx. peticiones",
+        "maxReusableSecs": "Máx. segundos reutilizables",
+        "keepAlivePeriod": "Periodo keep alive",
+        "authPassword": "Contraseña de auth",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "nombre del servidor",
+        "verifyPeerName": "Verificar nombre del peer",
+        "pinnedSha256": "SHA256 pinned",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "Intervalo keep alive",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "Interfaz",
+        "ipv6Only": "Solo IPv6",
+        "acceptProxyProtocol": "Aceptar proxy protocol",
+        "tcpUserTimeoutMs": "TCP user timeout (ms)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
+      },
       "outbound": {
         "addOutbound": "Agregar salida",
         "addReverse": "Agregar reverso",
@@ -846,8 +1225,8 @@
         "reverse": "Reverso",
         "domain": "Dominio",
         "type": "Tipo",
-        "bridge": "puente",
-        "portal": "portal",
+        "bridge": "Bridge",
+        "portal": "Portal",
         "link": "Enlace",
         "intercon": "Interconexión",
         "settings": "Configuración",
@@ -860,6 +1239,8 @@
         "testSuccess": "Prueba exitosa",
         "testFailed": "Prueba fallida",
         "testError": "Error al probar la salida",
+        "testModeTooltip": "TCP: sonda rápida solo de dial. HTTP: petición completa a través de xray.",
+        "testAll": "Probar todo",
         "nordvpn": "NordVPN",
         "accessToken": "Token de acceso",
         "country": "País",
@@ -876,6 +1257,16 @@
         "balancerSelectors": "Selectores",
         "tag": "Etiqueta",
         "tagDesc": "etiqueta única",
+        "tagDuplicate": "Etiqueta ya usada por otro balanceador",
+        "tagPlaceholder": "etiqueta única de balanceador",
+        "selector": "Selector",
+        "fallback": "Fallback",
+        "expected": "Esperado",
+        "expectedPlaceholder": "número óptimo de nodos",
+        "maxRtt": "Máx. RTT",
+        "tolerance": "Tolerancia",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "No es posible utilizar balancerTag y outboundTag al mismo tiempo. Si se utilizan al mismo tiempo, sólo funcionará outboundTag."
       },
       "wireguard": {
@@ -892,6 +1283,38 @@
         "userLevel": "Nivel de Usuario",
         "userLevelDesc": "Todas las conexiones realizadas a través de este entrada utilizarán este nivel de usuario. El valor predeterminado es 0"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "Clave privada",
+        "noServers": "No se encontraron servidores para el país seleccionado",
+        "noPublicKey": "El servidor seleccionado no anuncia una clave pública NordLynx.",
+        "outboundAdded": "Salida NordVPN añadida",
+        "outboundUpdated": "Salida NordVPN actualizada"
+      },
+      "warp": {
+        "licenseError": "No se pudo establecer la licencia WARP.",
+        "fetchFirst": "Obtén primero la configuración WARP.",
+        "createAccount": "Crear cuenta WARP",
+        "accessToken": "Access token",
+        "deviceId": "ID del dispositivo",
+        "licenseKey": "Clave de licencia",
+        "privateKey": "Clave privada",
+        "deleteAccount": "Eliminar cuenta",
+        "settings": "Ajustes",
+        "licenseKeyLabel": "Clave de licencia WARP / WARP+",
+        "key": "Clave",
+        "keyPlaceholder": "clave WARP+ de 26 caracteres",
+        "accountInfo": "Información de cuenta",
+        "deviceName": "Nombre del dispositivo",
+        "deviceModel": "Modelo del dispositivo",
+        "deviceEnabled": "Dispositivo habilitado",
+        "accountType": "Tipo de cuenta",
+        "role": "Rol",
+        "warpPlusData": "Datos WARP+",
+        "quota": "Cuota",
+        "usage": "Uso",
+        "addOutbound": "Añadir salida"
+      },
       "dns": {
         "enable": "Habilitar DNS",
         "enableDesc": "Habilitar servidor DNS incorporado",
@@ -961,7 +1384,7 @@
     "unknown": "Desconocido",
     "inbounds": "Entradas",
     "clients": "Clientes",
-    "offline": "🔴 Desconectado",
+    "offline": "🔴 Sin conexión",
     "online": "🟢 En línea",
     "commands": {
       "unknown": "❗ Comando desconocido",
@@ -992,7 +1415,7 @@
       "2faFailed": "Error de 2FA",
       "report": "🕰 Informes programados: {{ .RunTime }}\r\n",
       "datetime": "⏰ Fecha y Hora: {{ .DateTime }}\r\n",
-      "hostname": "💻 Nombre del Host: {{ .Hostname }}\r\n",
+      "hostname": "💻 Host: {{ .Hostname }}\r\n",
       "version": "🚀 Versión de X-UI: {{ .Version }}\r\n",
       "xrayVersion": "📡 Versión de Xray: {{ .XrayVersion }}\r\n",
       "ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
@@ -1001,15 +1424,15 @@
       "ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
       "serverUpTime": "⏳ Tiempo de actividad del servidor: {{ .UpTime }} {{ .Unit }}\r\n",
       "serverLoad": "📈 Carga del servidor: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
-      "serverMemory": "📋 Memoria del servidor: {{ .Current }}/{{ .Total }}\r\n",
-      "tcpCount": "🔹 Conteo de TCP: {{ .Count }}\r\n",
-      "udpCount": "🔸 Conteo de UDP: {{ .Count }}\r\n",
+      "serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
+      "tcpCount": "🔹 TCP: {{ .Count }}\r\n",
+      "udpCount": "🔸 UDP: {{ .Count }}\r\n",
       "traffic": "🚦 Tráfico: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
-      "xrayStatus": "ℹ️ Estado de Xray: {{ .State }}\r\n",
+      "xrayStatus": "ℹ️ Estado: {{ .State }}\r\n",
       "username": "👤 Nombre de usuario: {{ .Username }}\r\n",
       "reason": "❗️ Motivo: {{ .Reason }}\r\n",
       "time": "⏰ Hora: {{ .Time }}\r\n",
-      "inbound": "📍 Inbound: {{ .Remark }}\r\n",
+      "inbound": "📍 Entrada: {{ .Remark }}\r\n",
       "port": "🔌 Puerto: {{ .Port }}\r\n",
       "expire": "📅 Fecha de Vencimiento: {{ .Time }}\r\n",
       "expireIn": "📅 Vence en: {{ .Time }}\r\n",
@@ -1019,7 +1442,7 @@
       "lastOnline": "🔙 Última conexión: {{ .Time }}\r\n",
       "email": "📧 Email: {{ .Email }}\r\n",
       "upload": "🔼 Subida: ↑{{ .Upload }}\r\n",
-      "download": "🔽 Bajada: ↓{{ .Download }}\r\n",
+      "download": "🔽 Descarga: ↓{{ .Download }}\r\n",
       "total": "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
       "TGUser": "👤 Usuario de Telegram: {{ .TelegramID }}\r\n",
       "exhaustedMsg": "🚨 Agotado {{ .Type }}:\r\n",
@@ -1077,7 +1500,7 @@
       "ipLimit": "🔢 Límite de IP",
       "setTGUser": "👤 Establecer Usuario de Telegram",
       "toggle": "🔘 Habilitar / Deshabilitar",
-      "custom": "🔢 Costumbre",
+      "custom": "🔢 Personalizado",
       "confirmNumber": "✅ Confirmar: {{ .Num }}",
       "confirmNumberAdd": "✅ Confirmar agregando: {{ .Num }}",
       "limitTraffic": "🚧 Límite de tráfico",
@@ -1089,9 +1512,9 @@
       "use_default": "🏷️ Usar por defecto",
       "change_id": "⚙️🔑 ID",
       "change_password": "⚙️🔑 Contraseña",
-      "change_email": "⚙️📧 Correo electrónico",
+      "change_email": "⚙️📧 Email",
       "change_comment": "⚙️💬 Comentario",
-      "change_flow": "⚙️🚦 Flujo",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "Reiniciar todo el tráfico",
       "SortedTrafficUsageReport": "Informe de uso de tráfico ordenado"
     },
@@ -1119,4 +1542,4 @@
       "chooseInbound": "Elige un Inbound"
     }
   }
-}
+}

+ 473 - 64
web/translation/fa-IR.json

@@ -8,6 +8,8 @@
   "save": "ذخیره",
   "logout": "خروج",
   "create": "ایجاد",
+  "add": "افزودن",
+  "remove": "حذف",
   "update": "به‌روزرسانی",
   "copy": "کپی",
   "copied": "کپی شد",
@@ -18,6 +20,10 @@
   "protocol": "پروتکل",
   "search": "جستجو",
   "filter": "فیلتر",
+  "all": "همه",
+  "from": "از",
+  "to": "تا",
+  "done": "انجام شد",
   "loading": "...در حال بارگذاری",
   "refresh": "تازه‌سازی",
   "clear": "پاک کردن",
@@ -33,14 +39,14 @@
   "info": "اطلاعات بیشتر",
   "edit": "ویرایش",
   "delete": "حذف",
-  "reset": "ریست",
+  "reset": "بازنشانی",
   "noData": "داده‌ای وجود ندارد.",
   "copySuccess": "باموفقیت کپی‌شد",
   "sure": "مطمئن",
   "encryption": "رمزگذاری",
   "useIPv4ForHost": "از IPv4 برای میزبان استفاده کنید",
   "transmission": "راه‌اتصال",
-  "host": "آدرس",
+  "host": "میزبان",
   "path": "مسیر",
   "camouflage": "مبهم‌سازی",
   "status": "وضعیت",
@@ -98,9 +104,10 @@
     "dashboard": "نمای کلی",
     "inbounds": "ورودی‌ها",
     "clients": "کلاینت‌ها",
+    "groups": "گروه‌ها",
     "nodes": "نودها",
     "settings": "تنظیمات پنل",
-    "xray": "پیکربندی ایکس‌ری",
+    "xray": "پیکربندی Xray",
     "apiDocs": "مستندات API",
     "logout": "خروج",
     "link": "مدیریت",
@@ -121,16 +128,16 @@
     },
     "index": {
       "title": "نمای کلی",
-      "cpu": "پردازنده",
+      "cpu": "CPU",
       "logicalProcessors": "پردازنده‌های منطقی",
       "frequency": "فرکانس",
-      "swap": "سواپ",
+      "swap": "Swap",
       "storage": "ذخیره‌سازی",
-      "memory": "حافظه رم",
-      "threads": "رشته‌ها",
-      "xrayStatus": "ایکس‌ری",
+      "memory": "RAM",
+      "threads": "نخ‌ها",
+      "xrayStatus": "Xray",
       "stopXray": "توقف",
-      "restartXray": "شروع‌مجدد",
+      "restartXray": "راه‌اندازی مجدد",
       "xraySwitch": "‌نسخه",
       "xrayUpdates": "به‌روزرسانی‌های Xray",
       "xraySwitchClick": "نسخه مورد نظر را انتخاب کنید",
@@ -227,7 +234,7 @@
       "customGeoErrUpdateAllIncomplete": "به‌روزرسانی یک یا چند منبع geo سفارشی ناموفق بود",
       "customGeoEmpty": "هنوز منبع geo سفارشی‌ای ثبت نشده — برای ایجاد روی «افزودن» کلیک کنید",
       "dontRefresh": "در حال نصب، لطفا صفحه را رفرش نکنید",
-      "logs": "گزارش‌ها",
+      "logs": "لاگ‌ها",
       "config": "پیکربندی",
       "backup": "پشتیبان‌گیری",
       "backupTitle": "پشتیبان‌گیری و بازیابی",
@@ -242,18 +249,18 @@
       "getConfigError": "خطا در دریافت فایل پیکربندی"
     },
     "inbounds": {
-      "title": "کاربران",
+      "title": "ورودی‌ها",
       "totalDownUp": "دریافت/ارسال کل",
       "totalUsage": "‌‌‌مصرف کل",
       "inboundCount": "کل ورودی‌ها",
-      "operate": "عملیات",
+      "operate": "منو",
       "enable": "فعال",
       "remark": "نام",
       "node": "نود",
       "deployTo": "استقرار روی",
       "localPanel": "پنل لوکال",
       "fallbacks": {
-        "title": "فال‌بک‌ها",
+        "title": "Fallbackها",
         "help": "وقتی اتصالی روی این اینباند با هیچ کلاینتی تطبیق پیدا نمی‌کند، به یک اینباند دیگر ارجاع داده می‌شود. یک فرزند انتخاب کنید، فیلدهای مسیریابی (SNI / ALPN / Path / xver) خودکار از روی transport آن پر می‌شود — برای بیشتر تنظیمات نیازی به ویرایش نیست. هر فرزند باید روی 127.0.0.1 با security=none گوش بدهد.",
         "empty": "هنوز فال‌بکی اضافه نشده",
         "add": "افزودن فال‌بک",
@@ -271,14 +278,14 @@
       },
       "protocol": "پروتکل",
       "port": "پورت",
-      "portMap": "پورت‌های نظیر",
+      "portMap": "نگاشت پورت",
       "traffic": "ترافیک",
       "details": "توضیحات",
-      "transportConfig": "نحوه اتصال",
+      "transportConfig": "انتقال",
       "expireDate": "مدت زمان",
       "createdAt": "ایجاد",
       "updatedAt": "به‌روزرسانی",
-      "resetTraffic": "ریست ترافیک",
+      "resetTraffic": "بازنشانی ترافیک",
       "addInbound": "افزودن ورودی",
       "generalActions": "عملیات کلی",
       "modifyInbound": "ویرایش ورودی",
@@ -293,19 +300,31 @@
       "delAllClients": "حذف همه کلاینت‌ها",
       "delAllClientsConfirmTitle": "حذف هر {count} کلاینت اینباند «{remark}»؟",
       "delAllClientsConfirmContent": "تمام کلاینت‌های این اینباند به همراه رکوردهای ترافیک‌شان حذف می‌شوند. خود اینباند باقی می‌ماند. این عمل غیرقابل بازگشت است.",
-      "attachClients": "اتصال کلاینت‌ها به…",
-      "assignClientsGroup": "افزودن کلاینت‌ها به گروه…",
-      "attachClientsTitle": "اتصال کلاینت‌های «{remark}»",
-      "attachClientsDesc": "همان {count} کلاینت (با همان UUID/پسورد و ترافیک مشترک) را به اینباند(های) انتخاب‌شده هم متصل می‌کند. روی این اینباند هم باقی می‌مانند.",
-      "attachClientsTargets": "اینباندهای مقصد",
-      "attachClientsNoTargets": "اینباند سازگار دیگری برای اتصال وجود ندارد.",
-      "attachClientsResult": "{attached} متصل شد، {skipped} رد شد.",
-      "attachClientsResultMixed": "{attached} متصل شد، {skipped} رد شد، {errors} خطا.",
+      "attachClients": "الصاق کاربران به…",
+      "addClientsToGroup": "افزودن کاربران به گروه…",
+      "attachClientsTitle": "الصاق کاربران از «{remark}»",
+      "attachClientsDesc": "همان {count} کاربر (با UUID/رمز یکسان و ترافیک مشترک) را به ورودی‌(های) انتخابی الصاق می‌کند. در این ورودی هم باقی می‌مانند.",
+      "attachClientsTargets": "ورودی‌های مقصد",
+      "attachClientsNoTargets": "هیچ ورودی سازگار دیگری برای الصاق در دسترس نیست.",
+      "attachClientsResult": "الصاق شد {attached}، نادیده {skipped}.",
+      "attachClientsResultMixed": "الصاق شد {attached}، نادیده {skipped}، خطا {errors}.",
+      "attachClientsSelectLabel": "کاربران برای الصاق",
+      "attachClientsSearchPlaceholder": "جستجوی ایمیل یا توضیح",
+      "attachClientsStatusDisabled": "غیرفعال",
+      "attachClientsSelectedCount": "{selected} از {total} انتخاب‌شده",
+      "detachClients": "جداسازی کاربران",
+      "detachClientsTitle": "جداسازی کاربران از «{remark}»",
+      "detachClientsDesc": "کاربر(های) انتخابی را تنها از این ورودی حذف می‌کند. خود رکورد کاربر حفظ می‌شود (برای حذف کامل از Delete استفاده کنید). مبدا در مجموع {count} کاربر دارد.",
+      "detachClientsResult": "جدا شد {detached}، نادیده {skipped}.",
+      "detachClientsResultMixed": "جدا شد {detached}، نادیده {skipped}، خطا {errors}.",
+      "detachClientsSelectLabel": "کاربران برای جداسازی",
       "exportLinksTitle": "خروجی لینک‌های اینباند",
       "exportSubsTitle": "خروجی لینک‌های ساب",
       "exportAllLinksTitle": "خروجی لینک‌های همه اینباندها",
       "exportAllSubsTitle": "خروجی لینک‌های ساب همه اینباندها",
-      "inboundJsonTitle": "JSON اینباند",
+      "exportAllLinksFileName": "همه-ورودی‌ها",
+      "exportAllSubsFileName": "همه-ورودی‌ها-Subs",
+      "inboundJsonTitle": "JSON ورودی",
       "deleteClient": "حذف کاربر",
       "deleteClientContent": "آیا مطمئن به حذف کاربر هستید؟",
       "resetTrafficContent": "آیا مطمئن به ریست ترافیک هستید؟",
@@ -315,7 +334,7 @@
       "destinationPort": "پورت مقصد",
       "targetAddress": "آدرس مقصد",
       "monitorDesc": "به‌طور پیش‌فرض خالی‌بگذارید",
-      "meansNoLimit": "0 = واحد: گیگابایت) نامحدود)",
+      "meansNoLimit": "= نامحدود. (واحد: GB)",
       "totalFlow": "ترافیک کل",
       "leaveBlankToNeverExpire": "برای منقضی‌نشدن خالی‌بگذارید",
       "noRecommendKeepDefault": "توصیه‌می‌شود به‌طور پیش‌فرض حفظ‌شود",
@@ -350,9 +369,10 @@
       "IPLimitlogDesc": "گزارش تاریخچه آی‌پی. برای فعال کردن ورودی پس از غیرفعال شدن، گزارش را پاک کنید",
       "IPLimitlogclear": "پاک کردن گزارش‌ها",
       "setDefaultCert": "استفاده از گواهی پنل",
-      "streamTab": "استریم",
+      "setDefaultCertEmpty": "هیچ گواهی‌ای برای پنل پیکربندی نشده. ابتدا از تنظیمات یکی تعیین کنید.",
+      "streamTab": "انتقال",
       "securityTab": "امنیت",
-      "sniffingTab": "اسنیفینگ",
+      "sniffingTab": "شنود",
       "sniffingMetadataOnly": "فقط متادیتا",
       "sniffingRouteOnly": "فقط مسیریابی",
       "sniffingIpsExcluded": "IPهای مستثنا",
@@ -370,15 +390,14 @@
         "allHelp": "شیء کامل اینباند با همه فیلدها در یک ویرایشگر.",
         "settings": "تنظیمات",
         "settingsHelp": "ساختار بلوک settings در Xray:",
-        "sniffing": "اسنیفینگ",
+        "sniffing": "Sniffing",
         "sniffingHelp": "ساختار بلوک sniffing در Xray:",
-        "stream": "استریم",
+        "stream": "Stream",
         "streamHelp": "ساختار بلوک stream در Xray:",
         "jsonErrorPrefix": "JSON پیشرفته"
       },
       "telegramDesc": "لطفا شناسه گفتگوی تلگرام را وارد کنید. (از دستور '/id' در ربات استفاده کنید) یا ({'@'}userinfobot)",
       "subscriptionDesc": "شما می‌توانید لینک سابسکربپشن خودرا در 'جزئیات' پیدا کنید، همچنین می‌توانید از همین نام برای چندین کاربر استفاده‌کنید",
-      "info": "اطلاعات",
       "same": "همسان",
       "inboundData": "داده‌های ورودی",
       "exportInbound": "استخراج ورودی",
@@ -415,6 +434,143 @@
         "getNewmldsa65Error": "خطا در دریافت گواهی mldsa65.",
         "getNewVlessEncError": "خطا در دریافت گواهی VlessEnc."
       },
+      "form": {
+        "moveUp": "بالا",
+        "moveDown": "پایین",
+        "addAll": "افزودن همه",
+        "addAllFallbackTooltip": "برای هر ورودی واجد شرایط که هنوز متصل نشده یک ردیف fallback اضافه می‌کند",
+        "peers": "Peers",
+        "addPeer": "افزودن peer",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "فقط ویندوز. CIDRها به‌صورت خودکار به جدول مسیریابی سیستم اضافه می‌شوند تا ترافیک مطابق از TUN عبور کند.",
+        "autoOutboundsInterface": "رابط خروجی خودکار",
+        "autoOutboundsInterfaceTooltip": "رابط فیزیکی برای ترافیک خروجی. از auto برای تشخیص استفاده کنید؛ زمانی که Auto system routes فعال باشد، به‌صورت خودکار فعال می‌شود.",
+        "rewriteAddress": "بازنویسی آدرس",
+        "rewritePort": "بازنویسی پورت",
+        "allowedNetwork": "شبکه مجاز",
+        "followRedirect": "دنبال‌کردن Redirect",
+        "accounts": "حساب‌ها",
+        "allowTransparent": "اجازه شفاف",
+        "encryptionMethod": "روش رمزنگاری",
+        "visionTestseed": "Vision testseed",
+        "version": "نسخه",
+        "udpIdleTimeout": "UDP idle timeout (s)",
+        "masquerade": "استتار",
+        "type": "نوع",
+        "upstreamUrl": "آدرس Upstream",
+        "rewriteHost": "بازنویسی Host",
+        "skipTlsVerify": "رد تایید TLS",
+        "directory": "دایرکتوری",
+        "statusCode": "کد وضعیت",
+        "body": "Body",
+        "headers": "هدرها",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "نسخه درخواست",
+        "requestMethod": "متد درخواست",
+        "requestPath": "مسیر درخواست",
+        "requestHeaders": "هدرهای درخواست",
+        "responseVersion": "نسخه پاسخ",
+        "responseStatus": "وضعیت پاسخ",
+        "responseReason": "دلیل پاسخ",
+        "responseHeaders": "هدرهای پاسخ",
+        "heartbeatPeriod": "دوره Heartbeat",
+        "serviceName": "نام سرویس",
+        "authority": "Authority",
+        "multiMode": "حالت چندگانه",
+        "maxBufferedUpload": "حداکثر آپلود بافرشده",
+        "maxUploadSize": "حداکثر اندازه آپلود (بایت)",
+        "streamUpServer": "سرور Stream-Up",
+        "serverMaxHeaderBytes": "حداکثر بایت هدر سرور",
+        "paddingBytes": "بایت‌های Padding",
+        "uplinkHttpMethod": "متد HTTP آپلینک",
+        "paddingObfsMode": "حالت ابهام Padding",
+        "paddingKey": "کلید Padding",
+        "paddingHeader": "هدر Padding",
+        "paddingPlacement": "محل Padding",
+        "paddingMethod": "روش Padding",
+        "sessionPlacement": "محل نشست",
+        "sessionKey": "کلید نشست",
+        "sequencePlacement": "محل Sequence",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "محل داده Uplink",
+        "uplinkDataKey": "کلید داده Uplink",
+        "noSseHeader": "بدون هدر SSE",
+        "ttiMs": "TTI (ms)",
+        "uplinkMbps": "آپلود (MB/s)",
+        "downlinkMbps": "دانلود (MB/s)",
+        "cwndMultiplier": "ضریب CWND",
+        "maxSendingWindow": "حداکثر پنجره ارسال",
+        "externalProxy": "پراکسی خارجی",
+        "sniPlaceholder": "SNI (پیش‌فرض همان host)",
+        "fingerprint": "اثرانگشت",
+        "defaultOption": "پیش‌فرض",
+        "routeMark": "علامت مسیر",
+        "tcpKeepAliveInterval": "بازه TCP Keep Alive",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "فقط IPv6",
+        "tcpCongestion": "تراکم TCP",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "X-Forwarded-For مورد اعتماد",
+        "addressPortStrategy": "استراتژی آدرس+پورت",
+        "tryDelayMs": "تأخیر تلاش (ms)",
+        "prioritizeIPv6": "اولویت IPv6",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "حداکثر تلاش هم‌زمان",
+        "customSockopt": "Sockopt دلخواه",
+        "addCustomOption": "افزودن گزینه دلخواه",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "مجموعه‌های رمز",
+        "autoOption": "خودکار",
+        "minMaxVersion": "نسخه حداقل/حداکثر",
+        "rejectUnknownSni": "رد SNI ناشناخته",
+        "disableSystemRoot": "غیرفعال‌سازی System Root",
+        "sessionResumption": "ازسرگیری نشست",
+        "oneTimeLoading": "بارگذاری یک‌بار",
+        "usageOption": "گزینه استفاده",
+        "buildChain": "ساخت زنجیره",
+        "echKey": "کلید ECH",
+        "echConfig": "پیکربندی ECH",
+        "pinnedPeerCertSha256": "SHA-256 پین‌شدهٔ گواهی همتا",
+        "pinnedPeerCertSha256Tip": "هش‌های SHA-256 با کدگذاری Base64 از گواهی همتا. فقط در پنل — در پیکربندی xray سرور نوشته نمی‌شود، اما در لینک‌های اشتراک‌گذاری گنجانده می‌شود تا کلاینت‌ها بتوانند گواهی را پین کنند.",
+        "pinnedPeerCertSha256Placeholder": "هش(های) base64، با کاما جدا شوند",
+        "generateRandomPin": "تولید هش تصادفی",
+        "getNewEchCert": "دریافت گواهی ECH جدید",
+        "show": "نمایش",
+        "xver": "Xver",
+        "target": "هدف",
+        "maxTimeDiff": "حداکثر اختلاف زمان (ms)",
+        "minClientVer": "حداقل نسخه کلاینت",
+        "maxClientVer": "حداکثر نسخه کلاینت",
+        "shortIds": "Short IDها",
+        "spiderX": "SpiderX",
+        "getNewCert": "دریافت گواهی جدید",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "دریافت Seed جدید"
+      },
+      "info": {
+        "mode": "حالت",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "نام رابط",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "رابط خروجی",
+        "autoSystemRoutes": "مسیریابی خودکار سیستم",
+        "followRedirect": "FollowRedirect",
+        "auth": "احراز",
+        "noKernelTun": "TUN غیرکرنل",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "پیکربندی Peer {n}"
+      },
       "stream": {
         "general": {
           "request": "درخواست",
@@ -465,6 +621,20 @@
       "days": "روز",
       "renew": "تمدید خودکار",
       "renewDesc": "تمدید خودکار پس از انقضا. (۰ = غیرفعال) (واحد: روز)",
+      "searchPlaceholder": "جستجوی ایمیل، توضیح، Sub ID، UUID، رمز، احراز...",
+      "filterTitle": "فیلتر کاربران",
+      "clearAllFilters": "پاک کردن همه",
+      "sortOldest": "قدیمی‌ترین",
+      "sortNewest": "جدیدترین",
+      "sortRecentlyUpdated": "اخیراً به‌روزشده",
+      "sortRecentlyOnline": "اخیراً آنلاین",
+      "sortEmailAZ": "ایمیل ا→ی",
+      "sortEmailZA": "ایمیل ی→ا",
+      "sortMostTraffic": "بیشترین ترافیک",
+      "sortHighestRemaining": "بیشترین باقی‌مانده",
+      "sortExpiringSoonest": "نزدیک‌ترین انقضا",
+      "has": "دارد",
+      "hasNot": "ندارد",
       "title": "کلاینت‌ها",
       "actions": "عملیات",
       "totalGB": "مجموع ارسال/دریافت (گیگابایت)",
@@ -475,6 +645,9 @@
       "subId": "شناسه اشتراک",
       "online": "آنلاین",
       "email": "ایمیل",
+      "group": "گروه",
+      "groupDesc": "برچسبی منطقی برای دسته‌بندی کاربران مرتبط (مثل تیم، مشتری، منطقه). از نوار ابزار قابل فیلتر است.",
+      "groupPlaceholder": "مثلاً customer-a",
       "comment": "توضیحات",
       "traffic": "ترافیک",
       "offline": "آفلاین",
@@ -498,11 +671,45 @@
       "resetAllTraffics": "بازنشانی ترافیک همه کلاینت‌ها",
       "resetAllTrafficsTitle": "بازنشانی ترافیک همه کلاینت‌ها؟",
       "resetAllTrafficsContent": "شمارنده ارسال/دریافت همه کلاینت‌ها به صفر می‌رسد. سهمیه و تاریخ انقضا تغییری نمی‌کند. این عمل غیرقابل بازگشت است.",
-      "empty": "هنوز کلاینتی نیست — برای شروع یکی اضافه کنید.",
       "deleteConfirmTitle": "حذف کلاینت {email}؟",
       "deleteConfirmContent": "این کلاینت از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک می‌شود. این عمل غیرقابل بازگشت است.",
       "deleteSelected": "حذف ({count})",
       "adjustSelected": "تنظیم ({count})",
+      "subLinksSelected": "لینک‌های اشتراک ({count})",
+      "addToGroupTitle": "افزودن {count} کاربر به یک گروه",
+      "addToGroupTooltip": "یک گروه موجود را انتخاب کنید یا نام جدیدی تایپ کنید. برای حذف کاربران از گروه فعلی، از Ungroup استفاده کنید.",
+      "addToGroupPlaceholder": "نام گروه",
+      "addToGroupSuccessToast": "{count} کاربر به {group} اضافه شد",
+      "ungroupSuccessToast": "گروه از {count} کاربر پاک شد",
+      "ungroup": "خارج از گروه",
+      "ungroupConfirmTitle": "حذف {count} کاربر از گروهشان؟",
+      "ungroupConfirmContent": "برچسب گروه را روی هر کاربر انتخابی پاک می‌کند. کاربران حفظ می‌شوند (برای حذف کامل از Delete استفاده کنید).",
+      "addToGroup": "افزودن به گروه",
+      "attach": "الصاق",
+      "adjust": "تنظیم",
+      "subLinks": "لینک‌های اشتراک",
+      "selectedCount": "{count} انتخاب‌شده",
+      "attachSelected": "الصاق ({count})",
+      "attachToInboundsTitle": "الصاق {count} کاربر به ورودی‌(ها)",
+      "attachToInboundsDesc": "{count} کاربر انتخاب‌شده (همان UUID/رمز و ترافیک مشترک) را به ورودی‌های انتخابی الصاق می‌کند. الصاق‌های قبلی حفظ می‌شوند.",
+      "attachToInboundsTargets": "ورودی‌های مقصد",
+      "attachToInboundsNoTargets": "هیچ ورودی چندکاربره‌ای برای الصاق در دسترس نیست.",
+      "detachSelected": "جداسازی ({count})",
+      "detach": "جداسازی",
+      "detachFromInboundsTitle": "جداسازی {count} کاربر از ورودی‌(ها)",
+      "detachFromInboundsDesc": "{count} کاربر انتخاب‌شده را از ورودی‌های انتخابی حذف می‌کند. در مواردی که کاربر الصاق نبوده، نادیده گرفته می‌شود. رکورد کاربر حفظ می‌شود (برای حذف کامل از Delete استفاده کنید).",
+      "detachFromInboundsTargets": "ورودی‌هایی برای جداسازی",
+      "detachFromInboundsNoTargets": "هیچ ورودی چندکاربره‌ای در دسترس نیست.",
+      "detachFromInboundsResult": "جدا شد {detached}، نادیده گرفته شد {skipped}.",
+      "detachFromInboundsResultMixed": "جدا شد {detached}، نادیده {skipped}، خطا {errors}.",
+      "subLinksTitle": "لینک‌های اشتراک ({count})",
+      "subLinkColumn": "آدرس اشتراک",
+      "subJsonLinkColumn": "آدرس JSON اشتراک",
+      "subLinksCopyAll": "کپی همه",
+      "subLinksCopiedAll": "{count} لینک کپی شد",
+      "subLinksEmpty": "هیچ‌کدام از کاربران انتخابی شناسه اشتراک ندارند.",
+      "subLinksDisabled": "سرویس اشتراک غیرفعال است.",
+      "subLinksDisabledHint": "برای ساخت لینک، اشتراک را در تنظیمات پنل ← اشتراک فعال کنید.",
       "bulkDeleteConfirmTitle": "حذف {count} کلاینت؟",
       "bulkDeleteConfirmContent": "هر کلاینت انتخاب‌شده از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک می‌شود. این عمل غیرقابل بازگشت است.",
       "bulkAdjustTitle": "تنظیم {count} کلاینت",
@@ -513,12 +720,12 @@
       "delDepleted": "حذف اتمام‌یافته‌ها",
       "delDepletedConfirmTitle": "حذف کلاینت‌های اتمام‌یافته؟",
       "delDepletedConfirmContent": "هر کلاینتی که سهمیه ترافیک‌اش تمام شده یا تاریخ انقضایش گذشته است حذف می‌شود. این عمل غیرقابل بازگشت است.",
-      "auth": "Auth",
-      "hysteriaAuth": "Auth (هیستریا)",
+      "auth": "احراز",
+      "hysteriaAuth": "احراز Hysteria",
       "uuid": "UUID",
       "flow": "Flow",
       "vmessSecurity": "امنیت VMess",
-      "reverseTag": "Reverse tag",
+      "reverseTag": "تگ معکوس",
       "reverseTagPlaceholder": "Reverse tag اختیاری",
       "telegramId": "شناسه کاربر تلگرام",
       "telegramIdPlaceholder": "شناسه عددی کاربر تلگرام (۰ = هیچ)",
@@ -538,6 +745,44 @@
         "delDepleted": "{count} کلاینت اتمام‌یافته حذف شد"
       }
     },
+    "groups": {
+      "title": "گروه‌ها",
+      "name": "نام",
+      "clientCount": "کاربران در گروه",
+      "totalGroups": "تعداد گروه‌ها",
+      "totalGroupedClients": "کاربران دارای گروه",
+      "emptyGroups": "گروه‌های خالی",
+      "addGroup": "افزودن گروه",
+      "createSuccess": "گروه «{name}» ایجاد شد.",
+      "rename": "تغییر نام",
+      "renameTitle": "تغییر نام {name}",
+      "renameCollision": "گروهی به نام «{name}» از قبل وجود دارد.",
+      "renameSuccess": "گروه روی {count} کاربر تغییر نام داده شد.",
+      "deleteConfirmTitle": "حذف گروه {name}؟",
+      "deleteConfirmContent": "این عمل گروه را حذف می‌کند و برچسب آن را از {count} کاربر پاک می‌کند. خود کاربران حذف نمی‌شوند.",
+      "deleteSuccess": "گروه از {count} کاربر پاک شد.",
+      "resetTraffic": "بازنشانی ترافیک",
+      "resetConfirmTitle": "بازنشانی ترافیک گروه {name}؟",
+      "resetConfirmContent": "این عمل آپلود/دانلود تمام {count} کاربر این گروه را صفر می‌کند.",
+      "resetSuccess": "ترافیک {count} کاربر بازنشانی شد.",
+      "adjustSuccess": "{count} کاربر در {name} تنظیم شد.",
+      "emptyForAction": "این گروه هنوز کاربری ندارد.",
+      "deleteGroupOnly": "حذف گروه (نگه داشتن کاربران)",
+      "deleteClients": "حذف کاربران گروه",
+      "deleteClientsConfirmTitle": "حذف همه کاربران در {name}؟",
+      "deleteClientsConfirmContent": "این عمل {count} کاربر را به همراه رکورد ترافیک‌شان برای همیشه حذف می‌کند. برچسب گروه نیز پاک می‌شود. این عمل قابل بازگشت نیست.",
+      "deleteClientsSuccess": "{count} کاربر حذف شد.",
+      "deleteClientsMixed": "{ok} حذف شد، {failed} نادیده گرفته شد",
+      "addToGroup": "افزودن کاربران…",
+      "addToGroupTitle": "افزودن کاربران به گروه «{name}»",
+      "addToGroupDesc": "کاربرانی را برای افزودن به این گروه انتخاب کنید. الصاق‌های ورودی فعلی حفظ می‌شود؛ تنها برچسب گروه تغییر می‌کند. کاربرانی که از قبل در این گروه هستند نشان داده نمی‌شوند.",
+      "addToGroupEmpty": "کاربر دیگری برای افزودن در دسترس نیست.",
+      "addToGroupResult": "{count} کاربر به {name} اضافه شد.",
+      "removeFromGroup": "حذف کاربران…",
+      "removeFromGroupTitle": "حذف کاربران از گروه «{name}»",
+      "removeFromGroupDesc": "اعضایی را برای حذف از این گروه انتخاب کنید. خود کاربران حفظ می‌شوند (برای حذف کامل از «حذف کاربران گروه» استفاده کنید).",
+      "removeFromGroupResult": "{count} کاربر از {name} حذف شد."
+    },
     "nodes": {
       "title": "نودها",
       "addNode": "افزودن نود",
@@ -563,9 +808,9 @@
       "allowPrivateAddressHint": "فقط برای نودهای روی شبکه خصوصی یا VPN فعال شود.",
       "enable": "فعال",
       "status": "وضعیت",
-      "cpu": "پردازنده",
+      "cpu": "CPU",
       "mem": "حافظه",
-      "uptime": "زمان کارکرد",
+      "uptime": "مدت فعالیت",
       "latency": "تاخیر",
       "lastHeartbeat": "آخرین ضربان",
       "xrayVersion": "نسخه Xray",
@@ -600,7 +845,7 @@
       "title": "تنظیمات پنل",
       "save": "ذخیره",
       "infoDesc": "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید",
-      "restartPanel": "ریستارت پنل",
+      "restartPanel": "راه‌اندازی مجدد پنل",
       "restartPanelDesc": "آیا مطمئن به ریستارت پنل هستید؟ اگر پس‌از ریستارت نمی‌توانید به پنل دسترسی پیدا کنید، لطفاً گزارش‌های موجود در اسکریپت پنل را بررسی کنید",
       "restartPanelSuccess": "پنل با موفقیت راه‌اندازی مجدد شد",
       "actions": "عملیات ها",
@@ -625,12 +870,12 @@
       "publicKeyPathDesc": "مسیر فایل کلیدعمومی برای وب پنل. با '/' شروع‌می‌شود",
       "privateKeyPath": "مسیر کلید خصوصی",
       "privateKeyPathDesc": "مسیر فایل کلیدخصوصی برای وب پنل. با '/' شروع‌می‌شود",
-      "panelUrlPath": "URI مسیر",
+      "panelUrlPath": "مسیر URI",
       "panelUrlPathDesc": "برای وب پنل. با '/' شروع‌ و با '/' خاتمه‌ می‌یابد URI مسیر",
       "pageSize": "اندازه صفحه بندی جدول",
       "pageSizeDesc": "(اندازه صفحه برای جدول ورودی‌ها.(0 = غیرفعال",
-      "panelProxy": "پراکسی شبکه‌ی پنل",
-      "panelProxyDesc": "درخواست‌های خروجیِ خودِ پنل (آپدیت geo، چک نسخه‌ی Xray و پنل، تلگرام) را از این پراکسی عبور می‌دهد تا فیلترینگ سروری گیت‌هاب/تلگرام دور زده شود. پشتیبانی از socks5:// و http(s)://، برای نمونه یک اینباند SOCKS لوکالِ Xray. برای اتصال مستقیم خالی بگذارید.",
+      "panelProxy": "پراکسی شبکه پنل",
+      "panelProxyDesc": "درخواست‌های خروجی خود پنل (به‌روزرسانی geo، بررسی نسخه Xray/پنل، تلگرام) را از این پراکسی عبور می‌دهد تا فیلترینگ GitHub/تلگرام در سرور دور زده شود. socks5:// یا http(s):// قابل قبول است، مثل ورودی SOCKS محلی Xray. برای اتصال مستقیم خالی بگذارید.",
       "remarkModel": "نام‌کانفیگ و جداکننده",
       "datepicker": "نوع تقویم",
       "datepickerPlaceholder": "انتخاب تاریخ",
@@ -644,7 +889,7 @@
       "telegramBotEnableDesc": "ربات تلگرام را فعال می‌کند",
       "telegramToken": "توکن تلگرام",
       "telegramTokenDesc": "دریافت کنید {'@'}botfather توکن را می‌توانید از",
-      "telegramProxy": "SOCKS پراکسی",
+      "telegramProxy": "پراکسی SOCKS",
       "telegramProxyDesc": "را برای اتصال به تلگرام فعال می کند SOCKS5 پراکسی",
       "telegramAPIServer": "سرور API تلگرام",
       "telegramAPIServerDesc": "API سرور تلگرام برای اتصال را تغییر میدهد. برای استفاده از سرور پیش فرض خالی بگذارید",
@@ -670,6 +915,8 @@
       "subEnable": "فعال‌سازی سرویس سابسکریپشن",
       "subEnableDesc": "سرویس سابسکریپشن‌ را فعال‌می‌کند",
       "subJsonEnable": "فعال/غیرفعال‌سازی مستقل نقطه دسترسی سابسکریپشن JSON.",
+      "subJsonEnableTitle": "اشتراک JSON",
+      "subClashEnableTitle": "اشتراک Clash / Mihomo",
       "subTitle": "عنوان اشتراک",
       "subTitleDesc": "عنوان نمایش داده شده در کلاینت VPN",
       "subSupportUrl": "آدرس پشتیبانی",
@@ -690,13 +937,13 @@
       "subCertPathDesc": "مسیر فایل کلیدعمومی برای سرویس سابیکریپشن. با '/' شروع‌می‌شود",
       "subKeyPath": "مسیر کلید خصوصی",
       "subKeyPathDesc": "مسیر فایل کلیدخصوصی برای سرویس سابسکریپشن. با '/' شروع‌می‌شود",
-      "subPath": "URI مسیر",
+      "subPath": "مسیر URI",
       "subPathDesc": "برای سرویس سابسکریپشن. با '/' شروع‌ و با '/' خاتمه‌ می‌یابد URI مسیر",
       "subDomain": "نام دامنه",
       "subDomainDesc": "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید‌",
       "subUpdates": "فاصله بروزرسانی‌ سابسکریپشن",
       "subUpdatesDesc": "(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت",
-      "subEncrypt": "کدگذاری",
+      "subEncrypt": "انکود",
       "subEncryptDesc": "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه",
       "subShowInfo": "نمایش اطلاعات مصرف",
       "subShowInfoDesc": "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد",
@@ -705,7 +952,7 @@
       "subURI": "پروکسی معکوس URI مسیر",
       "subURIDesc": "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر",
       "externalTrafficInformEnable": "اطلاع رسانی خارجی مصرف ترافیک",
-      "externalTrafficInformEnableDesc": "مصرف ترافیک به سرویس خارجی ارسال می شود",
+      "externalTrafficInformEnableDesc": "به API خارجی در هر به‌روزرسانی ترافیک اطلاع بده.",
       "externalTrafficInformURI": "لینک اطلاع رسانی خارجی مصرف ترافیک",
       "externalTrafficInformURIDesc": "ترافیک های مصرفی به این لینک هم ارسال می شود",
       "restartXrayOnClientDisable": "ری‌استارت Xray بعد از غیرفعال‌سازی خودکار",
@@ -715,7 +962,55 @@
       "fragmentSett": "تنظیمات فرگمنت",
       "noisesDesc": "فعال کردن Noises.",
       "noisesSett": "تنظیمات Noises",
-      "mux": "ماکس",
+      "trustedProxyCidrs": "CIDRهای پراکسی مورد اعتماد",
+      "trustedProxyCidrsDesc": "IPها/CIDRها (با کاما) که مجازند هدرهای host، proto و client IP فوروارد را تنظیم کنند.",
+      "ldap": {
+        "enable": "فعال‌سازی همگام‌سازی LDAP",
+        "host": "میزبان LDAP",
+        "port": "پورت LDAP",
+        "useTls": "استفاده از TLS (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "تنظیم‌شده؛ برای حفظ رمز فعلی خالی بگذارید.",
+        "passwordUnconfigured": "تنظیم نشده.",
+        "passwordPlaceholder": "تنظیم‌شده – برای جایگزینی مقدار جدید وارد کنید",
+        "baseDn": "Base DN",
+        "userFilter": "فیلتر کاربر",
+        "userAttr": "صفت کاربر (username/email)",
+        "vlessField": "صفت پرچم VLESS",
+        "flagField": "صفت پرچم عمومی (اختیاری)",
+        "flagFieldDesc": "اگر تعیین شود، پرچم VLESS را override می‌کند — مثل shadowInactive.",
+        "truthyValues": "مقادیر صحیح",
+        "truthyValuesDesc": "با کاما جدا شده؛ پیش‌فرض: true,1,yes,on",
+        "invertFlag": "وارونگی پرچم",
+        "invertFlagDesc": "وقتی صفت به معنی «غیرفعال» است فعال کنید (مثل shadowInactive).",
+        "syncSchedule": "زمان‌بندی همگام‌سازی",
+        "syncScheduleDesc": "رشته شبیه cron، مثل @every 1m",
+        "inboundTags": "تگ‌های ورودی",
+        "inboundTagsDesc": "ورودی‌هایی که همگام‌سازی LDAP اجازه دارد روی آن‌ها کاربر بسازد یا حذف کند.",
+        "noInbounds": "هیچ ورودی یافت نشد. ابتدا از بخش ورودی‌ها یکی بسازید.",
+        "autoCreate": "ساخت خودکار کاربران",
+        "autoDelete": "حذف خودکار کاربران",
+        "defaultTotalGb": "حجم پیش‌فرض (GB)",
+        "defaultExpiryDays": "انقضای پیش‌فرض (روز)",
+        "defaultIpLimit": "محدودیت IP پیش‌فرض"
+      },
+      "subFormats": {
+        "packets": "بسته‌ها",
+        "length": "طول",
+        "interval": "بازه",
+        "maxSplit": "حداکثر تقسیم",
+        "noises": "نویزها",
+        "noiseItem": "نویز №{n}",
+        "type": "نوع",
+        "packet": "بسته",
+        "delayMs": "تأخیر (ms)",
+        "applyTo": "اعمال بر",
+        "addNoise": "+ نویز",
+        "concurrency": "هم‌زمانی",
+        "xudpConcurrency": "هم‌زمانی xudp",
+        "xudpUdp443": "xudp UDP 443"
+      },
+      "mux": "Mux",
       "muxDesc": "چندین جریان داده مستقل را در یک جریان داده ثابت منتقل می کند",
       "muxSett": "تنظیمات ماکس",
       "direct": "اتصال مستقیم",
@@ -768,8 +1063,11 @@
     "xray": {
       "title": "پیکربندی ایکس‌ری",
       "save": "ذخیره",
-      "restart": "ریستارت ایکس‌ری",
+      "restart": "راه‌اندازی مجدد Xray",
       "restartSuccess": "Xray با موفقیت راه‌اندازی مجدد شد",
+      "restartOutputTitle": "خروجی راه‌اندازی مجدد Xray",
+      "restartConfirmTitle": "راه‌اندازی مجدد xray؟",
+      "restartConfirmContent": "سرویس xray با پیکربندی ذخیره‌شده دوباره بارگذاری می‌شود.",
       "stopSuccess": "Xray با موفقیت متوقف شد",
       "restartError": "خطا در راه‌اندازی مجدد Xray.",
       "stopError": "خطا در توقف Xray.",
@@ -777,7 +1075,7 @@
       "advancedTemplate": "پیشرفته",
       "generalConfigs": "استراتژی‌ کلی",
       "generalConfigsDesc": "این گزینه‌ها استراتژی کلی ترافیک را تعیین می‌کنند",
-      "logConfigs": "گزارش",
+      "logConfigs": "لاگ",
       "logConfigsDesc": "گزارش‌ها ممکن است بر کارایی سرور شما تأثیر بگذارد. توصیه می شود فقط در صورت نیاز آن را عاقلانه فعال کنید",
       "blockConfigsDesc": "این گزینه‌ها ترافیک را بر اساس پروتکل‌های درخواستی خاص، و وب سایت‌ها مسدود می‌کند",
       "basicRouting": "مسیریابی پایه",
@@ -807,11 +1105,11 @@
       "Outbounds": "خروجی‌ها",
       "Balancers": "بالانسرها",
       "balancerTagRequired": "تگ الزامی است",
-      "balancerSelectorRequired": "حداقل یک outbound انتخاب کنید",
+      "balancerSelectorRequired": "حداقل یک خروجی انتخاب کنید",
       "OutboundsDesc": "مسیر ترافیک خروجی را تنظیم کنید",
       "Routings": "قوانین مسیریابی",
       "RoutingsDesc": "اولویت هر قانون مهم است",
-      "completeTemplate": "کامل",
+      "completeTemplate": "همه",
       "logLevel": "سطح گزارش",
       "logLevelDesc": "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند.",
       "accessLog": "مسیر گزارش",
@@ -846,6 +1144,73 @@
         "edit": "ویرایش قانون",
         "useComma": "موارد جدا شده با کاما"
       },
+      "routing": {
+        "dragToReorder": "برای تغییر ترتیب بکشید"
+      },
+      "ruleForm": {
+        "sourceIps": "IPهای مبدا",
+        "sourcePort": "پورت مبدا",
+        "vlessRoute": "مسیر VLESS",
+        "attributes": "صفت‌ها",
+        "value": "مقدار",
+        "user": "کاربر",
+        "inboundTags": "تگ‌های ورودی",
+        "outboundTag": "تگ خروجی",
+        "balancerTag": "تگ بالانسر",
+        "balancerTagTooltip": "ترافیک را از یکی از بالانسرهای پیکربندی‌شده عبور می‌دهد"
+      },
+      "outboundForm": {
+        "tagDuplicate": "این تگ توسط خروجی دیگری استفاده شده است",
+        "tagRequired": "تگ الزامی است",
+        "tagPlaceholder": "تگ-منحصربه‌فرد",
+        "localIpPlaceholder": "IP محلی",
+        "addressRequired": "آدرس الزامی است",
+        "portRequired": "پورت الزامی است",
+        "optional": "اختیاری",
+        "udpOverTcp": "UDP over TCP",
+        "uotVersion": "نسخه UoT",
+        "inboundTag": "تگ ورودی",
+        "inboundTagPlaceholder": "تگ ورودی استفاده‌شده در قوانین مسیریابی",
+        "responseType": "نوع پاسخ",
+        "rewriteNetwork": "بازنویسی شبکه",
+        "unchanged": "(بدون تغییر)",
+        "unchangedAddress": "(بدون تغییر) مثل 1.1.1.1",
+        "rules": "قوانین",
+        "ruleN": "قانون {n}",
+        "action": "عمل",
+        "redirect": "بازهدایت",
+        "fragment": "Fragment",
+        "finalRules": "قوانین نهایی",
+        "overrideXrayPrivateIp": "override بلاک پیش‌فرض IP خصوصی Xray",
+        "blockDelay": "تأخیر بلاک (ms)",
+        "reverseSniffing": "Sniffing معکوس",
+        "workers": "Workerها",
+        "reserved": "رزرو شده",
+        "minUploadInterval": "حداقل بازه آپلود (ms)",
+        "maxUploadSizeBytes": "حداکثر اندازه آپلود (بایت)",
+        "uplinkChunkSize": "اندازه قطعه آپلینک",
+        "noGrpcHeader": "بدون هدر gRPC",
+        "maxConcurrency": "حداکثر هم‌زمانی",
+        "maxConnections": "حداکثر اتصال‌ها",
+        "maxReuseTimes": "حداکثر استفاده مجدد",
+        "maxRequestTimes": "حداکثر تعداد درخواست",
+        "maxReusableSecs": "حداکثر ثانیه قابل استفاده مجدد",
+        "keepAlivePeriod": "دوره Keep alive",
+        "authPassword": "رمز احراز",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "نام سرور",
+        "verifyPeerName": "تایید نام Peer",
+        "pinnedSha256": "SHA256 پین‌شده",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "بازه Keep alive",
+        "markFwmark": "علامت (fwmark)",
+        "interface": "رابط",
+        "ipv6Only": "فقط IPv6",
+        "acceptProxyProtocol": "پذیرش Proxy Protocol",
+        "tcpUserTimeoutMs": "TCP user timeout (ms)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
+      },
       "outbound": {
         "addOutbound": "افزودن خروجی",
         "addReverse": "افزودن معکوس",
@@ -854,7 +1219,7 @@
         "reverseTag": "تگ معکوس",
         "reverseTagDesc": "تگ خروجی پروکسی معکوس ساده VLESS. برای غیرفعال کردن خالی بگذارید. در صورت تنظیم، اتصالات این کلاینت می‌توانند به عنوان تونل پروکسی معکوس استفاده شوند.",
         "reverseTagPlaceholder": "تگ خروجی (خالی = غیرفعال)",
-        "tag": "برچسب",
+        "tag": "تگ",
         "tagDesc": "برچسب یگانه",
         "address": "آدرس",
         "reverse": "معکوس",
@@ -874,6 +1239,8 @@
         "testSuccess": "تست موفقیت‌آمیز",
         "testFailed": "تست ناموفق",
         "testError": "خطا در تست خروجی",
+        "testModeTooltip": "TCP: فقط dial سریع. HTTP: درخواست کامل از طریق xray.",
+        "testAll": "تست همه",
         "nordvpn": "NordVPN",
         "accessToken": "توکن دسترسی",
         "country": "کشور",
@@ -888,8 +1255,18 @@
         "editBalancer": "ویرایش بالانسر",
         "balancerStrategy": "استراتژی",
         "balancerSelectors": "انتخاب‌گرها",
-        "tag": "برچسب",
+        "tag": "تگ",
         "tagDesc": "برچسب یگانه",
+        "tagDuplicate": "این تگ توسط بالانسر دیگری استفاده شده است",
+        "tagPlaceholder": "تگ منحصربه‌فرد بالانسر",
+        "selector": "انتخابگر",
+        "fallback": "Fallback",
+        "expected": "مورد انتظار",
+        "expectedPlaceholder": "تعداد نود بهینه",
+        "maxRtt": "حداکثر RTT",
+        "tolerance": "تحمل",
+        "baselines": "خطوط پایه",
+        "costs": "هزینه‌ها",
         "balancerDesc": "امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد."
       },
       "wireguard": {
@@ -906,6 +1283,38 @@
         "userLevel": "سطح کاربر",
         "userLevelDesc": "تمام اتصالات انجام‌شده از طریق این ورودی از این سطح کاربری استفاده خواهند کرد. مقدار پیش‌فرض 0 است"
       },
+      "nord": {
+        "accessToken": "توکن دسترسی",
+        "privateKey": "کلید خصوصی",
+        "noServers": "سروری برای کشور انتخابی پیدا نشد",
+        "noPublicKey": "سرور انتخابی کلید عمومی NordLynx اعلام نمی‌کند.",
+        "outboundAdded": "خروجی NordVPN اضافه شد",
+        "outboundUpdated": "خروجی NordVPN به‌روزرسانی شد"
+      },
+      "warp": {
+        "licenseError": "تنظیم لایسنس WARP ناموفق بود.",
+        "fetchFirst": "ابتدا پیکربندی WARP را دریافت کنید.",
+        "createAccount": "ایجاد حساب WARP",
+        "accessToken": "توکن دسترسی",
+        "deviceId": "شناسه دستگاه",
+        "licenseKey": "کلید لایسنس",
+        "privateKey": "کلید خصوصی",
+        "deleteAccount": "حذف حساب",
+        "settings": "تنظیمات",
+        "licenseKeyLabel": "کلید لایسنس WARP / WARP+",
+        "key": "کلید",
+        "keyPlaceholder": "کلید ۲۶ کاراکتری WARP+",
+        "accountInfo": "اطلاعات حساب",
+        "deviceName": "نام دستگاه",
+        "deviceModel": "مدل دستگاه",
+        "deviceEnabled": "دستگاه فعال",
+        "accountType": "نوع حساب",
+        "role": "نقش",
+        "warpPlusData": "داده WARP+",
+        "quota": "سهمیه",
+        "usage": "مصرف",
+        "addOutbound": "افزودن خروجی"
+      },
       "dns": {
         "enable": "فعال کردن حل دامنه",
         "enableDesc": "سرور حل دامنه داخلی را فعال کنید",
@@ -973,7 +1382,7 @@
     "hours": "ساعت",
     "minutes": "دقیقه",
     "unknown": "نامشخص",
-    "inbounds": "ورودی ها",
+    "inbounds": "ورودیها",
     "clients": "کاربران",
     "offline": "🔴 آفلاین",
     "online": "🟢 آنلاین",
@@ -1006,24 +1415,24 @@
       "2faFailed": "خطای 2FA",
       "report": "🕰 گزارشات‌زمان‌بندی‌شده: {{ .RunTime }}\r\n",
       "datetime": "⏰ تاریخ‌وزمان: {{ .DateTime }}\r\n",
-      "hostname": "💻 نام‌میزبان: {{ .Hostname }}\r\n",
+      "hostname": "💻 میزبان: {{ .Hostname }}\r\n",
       "version": "🚀 نسخه‌پنل: {{ .Version }}\r\n",
       "xrayVersion": "📡 نسخه‌هسته: {{ .XrayVersion }}\r\n",
       "ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
       "ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
-      "ip": "🌐 آدرس‌آی‌پی: {{ .IP }}\r\n",
-      "ips": "🔢 آدرس‌های آی‌پی:\r\n{{ .IPs }}\r\n",
+      "ip": "🌐 IP: {{ .IP }}\r\n",
+      "ips": "🔢 IPها:\r\n{{ .IPs }}\r\n",
       "serverUpTime": "⏳ مدت‌کارکردسیستم: {{ .UpTime }} {{ .Unit }}\r\n",
       "serverLoad": "📈 بارسیستم: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
       "serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
       "tcpCount": "🔹 TCP: {{ .Count }}\r\n",
       "udpCount": "🔸 UDP: {{ .Count }}\r\n",
       "traffic": "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
-      "xrayStatus": "ℹ️ وضعیت‌ایکس‌ری: {{ .State }}\r\n",
+      "xrayStatus": "ℹ️ وضعیت: {{ .State }}\r\n",
       "username": "👤 نام‌کاربری: {{ .Username }}\r\n",
       "reason": "❗️ دلیل: {{ .Reason }}\r\n",
       "time": "⏰ زمان: {{ .Time }}\r\n",
-      "inbound": "📍 نام‌ورودی: {{ .Remark }}\r\n",
+      "inbound": "📍 ورودی: {{ .Remark }}\r\n",
       "port": "🔌 پورت: {{ .Port }}\r\n",
       "expire": "📅 تاریخ‌انقضا: {{ .Time }}\r\n\r\n",
       "expireIn": "📅 باقی‌ مانده‌ تا انقضا: {{ .Time }}\r\n\r\n",
@@ -1032,9 +1441,9 @@
       "online": "🌐 وضعیت اتصال: {{ .Status }}\r\n",
       "lastOnline": "🔙 آخرین فعالیت: {{ .Time }}\r\n",
       "email": "📧 ایمیل: {{ .Email }}\r\n",
-      "upload": "🔼 آپلود: {{ .Upload }}\r\n",
-      "download": "🔽 دانلود: {{ .Download }}\r\n",
-      "total": "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n",
+      "upload": "🔼 آپلود: {{ .Upload }}\r\n",
+      "download": "🔽 دانلود: {{ .Download }}\r\n",
+      "total": "📊 کل: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
       "TGUser": "👤 کاربر تلگرام: {{ .TelegramID }}\r\n",
       "exhaustedMsg": "🚨 {{ .Type }} به‌اتمام‌رسیده‌است:\r\n",
       "exhaustedCount": "🚨 تعداد {{ .Type }} به‌اتمام‌رسیده‌است:\r\n",
@@ -1101,11 +1510,11 @@
       "submitDisable": "ارسال به عنوان غیرفعال ☑️",
       "submitEnable": "ارسال به عنوان فعال ✅",
       "use_default": "🏷️ استفاده از پیش‌فرض",
-      "change_id": "⚙️🔑 شناسه",
+      "change_id": "⚙️🔑 ID",
       "change_password": "⚙️🔑 گذرواژه",
       "change_email": "⚙️📧 ایمیل",
       "change_comment": "⚙️💬 نظر",
-      "change_flow": "⚙️🚦 جریان",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "بازنشانی همه ترافیک‌ها",
       "SortedTrafficUsageReport": "گزارش استفاده از ترافیک مرتب‌شده"
     },
@@ -1133,4 +1542,4 @@
       "chooseInbound": "یک ورودی انتخاب کنید"
     }
   }
-}
+}

+ 455 - 32
web/translation/id-ID.json

@@ -8,15 +8,22 @@
   "save": "Simpan",
   "logout": "Keluar",
   "create": "Buat",
+  "add": "Tambah",
+  "remove": "Hapus",
   "update": "Perbarui",
   "copy": "Salin",
   "copied": "Tersalin",
+  "more": "lainnya",
   "download": "Unduh",
   "remark": "Catatan",
   "enable": "Aktifkan",
   "protocol": "Protokol",
   "search": "Cari",
   "filter": "Filter",
+  "all": "Semua",
+  "from": "Dari",
+  "to": "Ke",
+  "done": "Selesai",
   "loading": "Memuat...",
   "refresh": "Segarkan",
   "clear": "Bersihkan",
@@ -27,7 +34,7 @@
   "check": "Centang",
   "indefinite": "Tak Terbatas",
   "unlimited": "Tanpa Batas",
-  "none": "None",
+  "none": "Tidak ada",
   "qrCode": "Kode QR",
   "info": "Informasi Lebih Lanjut",
   "edit": "Edit",
@@ -40,8 +47,8 @@
   "useIPv4ForHost": "Gunakan IPv4 untuk host",
   "transmission": "Transmisi",
   "host": "Host",
-  "path": "Jalur",
-  "camouflage": "Obfuscation",
+  "path": "Path",
+  "camouflage": "Obfuskasi",
   "status": "Status",
   "enabled": "Aktif",
   "disabled": "Nonaktif",
@@ -95,8 +102,9 @@
     "dark": "Gelap",
     "ultraDark": "Sangat Gelap",
     "dashboard": "Ikhtisar",
-    "inbounds": "Masuk",
+    "inbounds": "Inbound",
     "clients": "Klien",
+    "groups": "Grup",
     "nodes": "Node",
     "settings": "Pengaturan Panel",
     "xray": "Konfigurasi Xray",
@@ -128,8 +136,8 @@
       "memory": "RAM",
       "threads": "Thread",
       "xrayStatus": "Xray",
-      "stopXray": "Stop",
-      "restartXray": "Restart",
+      "stopXray": "Hentikan",
+      "restartXray": "Mulai ulang",
       "xraySwitch": "Versi",
       "xrayUpdates": "Pembaruan Xray",
       "xraySwitchClick": "Pilih versi yang ingin Anda pindah.",
@@ -143,7 +151,7 @@
       "xrayStatusUnknown": "Tidak diketahui",
       "xrayStatusRunning": "Berjalan",
       "xrayStatusStop": "Berhenti",
-      "xrayStatusError": "Kesalahan",
+      "xrayStatusError": "Error",
       "xrayErrorPopoverTitle": "Terjadi kesalahan saat menjalankan Xray",
       "operationHours": "Waktu Aktif",
       "systemHistoryTitle": "Riwayat Sistem",
@@ -241,7 +249,7 @@
       "getConfigError": "Terjadi kesalahan saat mengambil file konfigurasi"
     },
     "inbounds": {
-      "title": "Masuk",
+      "title": "Inbound",
       "totalDownUp": "Total Terkirim/Diterima",
       "totalUsage": "Penggunaan Total",
       "inboundCount": "Total Masuk",
@@ -270,14 +278,14 @@
       },
       "protocol": "Protokol",
       "port": "Port",
-      "portMap": "Port Mapping",
-      "traffic": "Traffic",
+      "portMap": "Pemetaan port",
+      "traffic": "Trafik",
       "details": "Rincian",
       "transportConfig": "Transport",
       "expireDate": "Durasi",
       "createdAt": "Dibuat",
       "updatedAt": "Diperbarui",
-      "resetTraffic": "Reset Traffic",
+      "resetTraffic": "Reset trafik",
       "addInbound": "Tambahkan Masuk",
       "generalActions": "Tindakan Umum",
       "modifyInbound": "Ubah Masuk",
@@ -292,10 +300,30 @@
       "delAllClients": "Hapus Semua Klien",
       "delAllClientsConfirmTitle": "Hapus semua {count} klien dari \"{remark}\"?",
       "delAllClientsConfirmContent": "Menghapus setiap klien dari inbound ini dan menghapus catatan trafiknya. Inbound itu sendiri dipertahankan. Tindakan ini tidak dapat dibatalkan.",
+      "attachClients": "Lampirkan klien ke…",
+      "addClientsToGroup": "Tambah klien ke grup…",
+      "attachClientsTitle": "Lampirkan klien dari «{remark}»",
+      "attachClientsDesc": "Melampirkan {count} klien yang sama (UUID/kata sandi sama dan trafik bersama) ke inbound terpilih. Tetap ada di inbound ini juga.",
+      "attachClientsTargets": "Inbound tujuan",
+      "attachClientsNoTargets": "Tidak ada inbound kompatibel lain untuk dilampirkan.",
+      "attachClientsResult": "Dilampirkan {attached}, dilewati {skipped}.",
+      "attachClientsResultMixed": "Dilampirkan {attached}, dilewati {skipped}, error {errors}.",
+      "attachClientsSelectLabel": "Klien untuk dilampirkan",
+      "attachClientsSearchPlaceholder": "Cari email atau komentar",
+      "attachClientsStatusDisabled": "Dinonaktifkan",
+      "attachClientsSelectedCount": "{selected} dari {total} dipilih",
+      "detachClients": "Lepas klien",
+      "detachClientsTitle": "Lepas klien dari «{remark}»",
+      "detachClientsDesc": "Menghapus klien terpilih hanya dari inbound ini. Catatan klien tetap dipertahankan (gunakan Delete untuk menghapus sepenuhnya). Sumber memiliki total {count} klien.",
+      "detachClientsResult": "Dilepas {detached}, dilewati {skipped}.",
+      "detachClientsResultMixed": "Dilepas {detached}, dilewati {skipped}, error {errors}.",
+      "detachClientsSelectLabel": "Klien untuk dilepas",
       "exportLinksTitle": "Ekspor tautan inbound",
       "exportSubsTitle": "Ekspor tautan langganan",
       "exportAllLinksTitle": "Ekspor semua tautan inbound",
       "exportAllSubsTitle": "Ekspor semua tautan langganan",
+      "exportAllLinksFileName": "Semua-Inbound",
+      "exportAllSubsFileName": "Semua-Inbound-Subs",
       "inboundJsonTitle": "JSON inbound",
       "deleteClient": "Hapus Klien",
       "deleteClientContent": "Apakah Anda yakin ingin menghapus klien?",
@@ -306,7 +334,7 @@
       "destinationPort": "Port Tujuan",
       "targetAddress": "Alamat Target",
       "monitorDesc": "Biarkan kosong untuk mendengarkan semua IP",
-      "meansNoLimit": "= Unlimited. (unit: GB)",
+      "meansNoLimit": "= Tanpa batas. (satuan: GB)",
       "totalFlow": "Total Aliran",
       "leaveBlankToNeverExpire": "Biarkan kosong untuk tidak pernah kedaluwarsa",
       "noRecommendKeepDefault": "Disarankan untuk tetap menggunakan pengaturan default",
@@ -341,7 +369,8 @@
       "IPLimitlogDesc": "Log histori IP. (untuk mengaktifkan masuk setelah menonaktifkan, hapus log)",
       "IPLimitlogclear": "Hapus Log",
       "setDefaultCert": "Atur Sertifikat dari Panel",
-      "streamTab": "Stream",
+      "setDefaultCertEmpty": "Tidak ada sertifikat yang dikonfigurasi untuk panel. Atur dulu di Pengaturan.",
+      "streamTab": "Aliran",
       "securityTab": "Keamanan",
       "sniffingTab": "Sniffing",
       "sniffingMetadataOnly": "Hanya metadata",
@@ -369,7 +398,6 @@
       },
       "telegramDesc": "Harap berikan ID Obrolan Telegram. (gunakan perintah '/id' di bot) atau ({'@'}userinfobot)",
       "subscriptionDesc": "Untuk menemukan URL langganan Anda, buka 'Rincian'. Selain itu, Anda dapat menggunakan nama yang sama untuk beberapa klien.",
-      "info": "Info",
       "same": "Sama",
       "inboundData": "Data Masuk",
       "exportInbound": "Ekspor Masuk",
@@ -406,6 +434,143 @@
         "getNewmldsa65Error": "Terjadi kesalahan saat mendapatkan sertifikat mldsa65.",
         "getNewVlessEncError": "Terjadi kesalahan saat mendapatkan sertifikat VlessEnc."
       },
+      "form": {
+        "moveUp": "Naik",
+        "moveDown": "Turun",
+        "addAll": "Tambah semua",
+        "addAllFallbackTooltip": "Tambahkan baris fallback untuk setiap inbound yang memenuhi syarat dan belum terhubung",
+        "peers": "Peers",
+        "addPeer": "Tambah peer",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "Hanya Windows. CIDR ditambahkan otomatis ke tabel routing sistem agar trafik yang cocok melewati TUN.",
+        "autoOutboundsInterface": "Interface outbound otomatis",
+        "autoOutboundsInterfaceTooltip": "Interface fisik untuk trafik outbound. Gunakan 'auto' untuk deteksi; otomatis aktif saat Auto system routes diatur.",
+        "rewriteAddress": "Tulis ulang alamat",
+        "rewritePort": "Tulis ulang port",
+        "allowedNetwork": "Jaringan yang diizinkan",
+        "followRedirect": "Ikuti redirect",
+        "accounts": "Akun",
+        "allowTransparent": "Izinkan transparan",
+        "encryptionMethod": "Metode enkripsi",
+        "visionTestseed": "Vision testseed",
+        "version": "Versi",
+        "udpIdleTimeout": "UDP idle timeout (d)",
+        "masquerade": "Masquerade",
+        "type": "Tipe",
+        "upstreamUrl": "URL Upstream",
+        "rewriteHost": "Tulis ulang Host",
+        "skipTlsVerify": "Lewati verifikasi TLS",
+        "directory": "Direktori",
+        "statusCode": "Kode status",
+        "body": "Body",
+        "headers": "Header",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "Versi permintaan",
+        "requestMethod": "Metode permintaan",
+        "requestPath": "Path permintaan",
+        "requestHeaders": "Header permintaan",
+        "responseVersion": "Versi respons",
+        "responseStatus": "Status respons",
+        "responseReason": "Alasan respons",
+        "responseHeaders": "Header respons",
+        "heartbeatPeriod": "Periode heartbeat",
+        "serviceName": "Nama layanan",
+        "authority": "Authority",
+        "multiMode": "Multi Mode",
+        "maxBufferedUpload": "Maks. upload ter-buffer",
+        "maxUploadSize": "Ukuran upload maks. (Byte)",
+        "streamUpServer": "Stream-Up Server",
+        "serverMaxHeaderBytes": "Maks. byte header server",
+        "paddingBytes": "Byte Padding",
+        "uplinkHttpMethod": "Metode HTTP Uplink",
+        "paddingObfsMode": "Mode obfs Padding",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "Posisi Padding",
+        "paddingMethod": "Metode Padding",
+        "sessionPlacement": "Session Placement",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence Placement",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink Data Placement",
+        "uplinkDataKey": "Uplink Data Key",
+        "noSseHeader": "Tanpa header SSE",
+        "ttiMs": "TTI (ms)",
+        "uplinkMbps": "Uplink (MB/s)",
+        "downlinkMbps": "Downlink (MB/s)",
+        "cwndMultiplier": "Pengganda CWND",
+        "maxSendingWindow": "Maks. jendela pengiriman",
+        "externalProxy": "Proxy eksternal",
+        "sniPlaceholder": "SNI (default = host)",
+        "fingerprint": "Fingerprint",
+        "defaultOption": "Default",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive Interval",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "Hanya V6",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "X-Forwarded-For tepercaya",
+        "addressPortStrategy": "Strategi alamat+port",
+        "tryDelayMs": "Penundaan percobaan (ms)",
+        "prioritizeIPv6": "Prioritaskan IPv6",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "Maks. percobaan bersamaan",
+        "customSockopt": "Sockopt kustom",
+        "addCustomOption": "Tambah opsi kustom",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "Otomatis",
+        "minMaxVersion": "Versi Min/Maks",
+        "rejectUnknownSni": "Tolak SNI tidak dikenal",
+        "disableSystemRoot": "Nonaktifkan System Root",
+        "sessionResumption": "Lanjutkan sesi",
+        "oneTimeLoading": "Pemuatan sekali",
+        "usageOption": "Opsi penggunaan",
+        "buildChain": "Bangun rantai",
+        "echKey": "ECH key",
+        "echConfig": "Konfig ECH",
+        "pinnedPeerCertSha256": "SHA-256 Sertifikat Peer Tersemat",
+        "pinnedPeerCertSha256Tip": "Hash SHA-256 berenkode Base64 dari sertifikat peer. Hanya panel — tidak ditulis ke konfig xray server, tetapi disertakan dalam link berbagi agar klien dapat menyematkan sertifikat.",
+        "pinnedPeerCertSha256Placeholder": "hash base64, dipisah koma",
+        "generateRandomPin": "Hasilkan hash acak",
+        "getNewEchCert": "Dapatkan sertifikat ECH baru",
+        "show": "Tampilkan",
+        "xver": "Xver",
+        "target": "Target",
+        "maxTimeDiff": "Maks. selisih waktu (ms)",
+        "minClientVer": "Min. versi klien",
+        "maxClientVer": "Maks. versi klien",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "Dapatkan sertifikat baru",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "Dapatkan Seed baru"
+      },
+      "info": {
+        "mode": "Mode",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "Nama interface",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "Interface outbound",
+        "autoSystemRoutes": "Rute sistem otomatis",
+        "followRedirect": "FollowRedirect",
+        "auth": "Auth",
+        "noKernelTun": "TUN tanpa kernel",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "Konfig Peer {n}"
+      },
       "stream": {
         "general": {
           "request": "Permintaan",
@@ -456,6 +621,20 @@
       "days": "Hari",
       "renew": "Perpanjangan otomatis",
       "renewDesc": "Perpanjangan otomatis setelah kedaluwarsa. (0 = nonaktif) (satuan: hari)",
+      "searchPlaceholder": "Cari email, komentar, sub ID, UUID, kata sandi, auth…",
+      "filterTitle": "Filter klien",
+      "clearAllFilters": "Hapus semua",
+      "sortOldest": "Terlama dulu",
+      "sortNewest": "Terbaru dulu",
+      "sortRecentlyUpdated": "Baru saja diperbarui",
+      "sortRecentlyOnline": "Baru saja online",
+      "sortEmailAZ": "Email A→Z",
+      "sortEmailZA": "Email Z→A",
+      "sortMostTraffic": "Trafik terbanyak",
+      "sortHighestRemaining": "Tersisa terbanyak",
+      "sortExpiringSoonest": "Segera kedaluwarsa",
+      "has": "Memiliki",
+      "hasNot": "Tidak memiliki",
       "title": "Klien",
       "actions": "Aksi",
       "totalGB": "Total Kirim/Terima (GB)",
@@ -466,6 +645,9 @@
       "subId": "ID Langganan",
       "online": "Online",
       "email": "Email",
+      "group": "Grup",
+      "groupDesc": "Label logis untuk mengelompokkan klien terkait (mis. tim, pelanggan, wilayah). Dapat difilter dari toolbar.",
+      "groupPlaceholder": "mis. customer-a",
       "comment": "Komentar",
       "traffic": "Lalu lintas",
       "offline": "Offline",
@@ -489,11 +671,45 @@
       "resetAllTraffics": "Reset lalu lintas semua klien",
       "resetAllTrafficsTitle": "Reset lalu lintas semua klien?",
       "resetAllTrafficsContent": "Penghitung kirim/terima setiap klien turun ke nol. Kuota dan kedaluwarsa tidak terpengaruh. Tidak dapat dibatalkan.",
-      "empty": "Belum ada klien — tambahkan satu untuk memulai.",
       "deleteConfirmTitle": "Hapus klien {email}?",
       "deleteConfirmContent": "Tindakan ini menghapus klien dari setiap inbound terlampir dan menghapus catatan lalu lintasnya. Tidak dapat dibatalkan.",
       "deleteSelected": "Hapus ({count})",
       "adjustSelected": "Sesuaikan ({count})",
+      "subLinksSelected": "Tautan sub ({count})",
+      "addToGroupTitle": "Tambahkan {count} klien ke grup",
+      "addToGroupTooltip": "Pilih grup yang ada atau ketik nama baru. Gunakan Ungroup untuk menghapus klien dari grup saat ini.",
+      "addToGroupPlaceholder": "Nama grup",
+      "addToGroupSuccessToast": "{count} klien ditambahkan ke {group}",
+      "ungroupSuccessToast": "Grup dihapus dari {count} klien",
+      "ungroup": "Lepaskan grup",
+      "ungroupConfirmTitle": "Hapus {count} klien dari grupnya?",
+      "ungroupConfirmContent": "Menghapus label grup dari setiap klien terpilih. Klien tetap dipertahankan (gunakan Delete untuk menghapus sepenuhnya).",
+      "addToGroup": "Tambahkan ke grup",
+      "attach": "Lampirkan",
+      "adjust": "Atur",
+      "subLinks": "Tautan sub",
+      "selectedCount": "{count} dipilih",
+      "attachSelected": "Lampirkan ({count})",
+      "attachToInboundsTitle": "Lampirkan {count} klien ke inbound",
+      "attachToInboundsDesc": "Melampirkan {count} klien terpilih (UUID/kata sandi sama dan trafik bersama) ke inbound terpilih. Lampiran yang ada tetap dipertahankan.",
+      "attachToInboundsTargets": "Inbound tujuan",
+      "attachToInboundsNoTargets": "Tidak ada inbound multi-pengguna untuk dilampirkan.",
+      "detachSelected": "Lepas ({count})",
+      "detach": "Lepas",
+      "detachFromInboundsTitle": "Lepas {count} klien dari inbound",
+      "detachFromInboundsDesc": "Menghapus {count} klien terpilih dari inbound terpilih. Pasangan di mana klien tidak terlampir akan dilewati secara diam-diam. Catatan klien dipertahankan (gunakan Delete untuk menghapus sepenuhnya).",
+      "detachFromInboundsTargets": "Inbound untuk dilepas",
+      "detachFromInboundsNoTargets": "Tidak ada inbound multi-pengguna.",
+      "detachFromInboundsResult": "Dilepas {detached}, dilewati {skipped}.",
+      "detachFromInboundsResultMixed": "Dilepas {detached}, dilewati {skipped}, error {errors}.",
+      "subLinksTitle": "Tautan sub ({count})",
+      "subLinkColumn": "URL Langganan",
+      "subJsonLinkColumn": "URL JSON Langganan",
+      "subLinksCopyAll": "Salin semua",
+      "subLinksCopiedAll": "{count} tautan disalin",
+      "subLinksEmpty": "Tidak ada klien terpilih yang memiliki ID langganan.",
+      "subLinksDisabled": "Layanan langganan dinonaktifkan.",
+      "subLinksDisabledHint": "Aktifkan langganan di Pengaturan Panel → Langganan untuk membuat tautan.",
       "bulkDeleteConfirmTitle": "Hapus {count} klien?",
       "bulkDeleteConfirmContent": "Setiap klien yang dipilih dihapus dari semua inbound terlampir dan catatan lalu lintasnya dihapus. Tidak dapat dibatalkan.",
       "bulkAdjustTitle": "Sesuaikan {count} klien",
@@ -505,9 +721,10 @@
       "delDepletedConfirmTitle": "Hapus klien yang habis?",
       "delDepletedConfirmContent": "Hapus setiap klien yang kuota lalu lintasnya habis atau yang masa berlakunya telah berakhir. Tidak dapat dibatalkan.",
       "auth": "Auth",
-      "hysteriaAuth": "Auth Hysteria",
+      "hysteriaAuth": "Hysteria Auth",
       "uuid": "UUID",
       "flow": "Flow",
+      "vmessSecurity": "Keamanan VMess",
       "reverseTag": "Reverse tag",
       "reverseTagPlaceholder": "Reverse tag opsional",
       "telegramId": "ID pengguna Telegram",
@@ -528,10 +745,48 @@
         "delDepleted": "{count} klien habis dihapus"
       }
     },
+    "groups": {
+      "title": "Grup",
+      "name": "Nama",
+      "clientCount": "Klien di grup",
+      "totalGroups": "Total grup",
+      "totalGroupedClients": "Klien dengan grup",
+      "emptyGroups": "Grup kosong",
+      "addGroup": "Tambah grup",
+      "createSuccess": "Grup «{name}» dibuat.",
+      "rename": "Ubah nama",
+      "renameTitle": "Ubah nama {name}",
+      "renameCollision": "Grup bernama «{name}» sudah ada.",
+      "renameSuccess": "Grup diubah namanya pada {count} klien.",
+      "deleteConfirmTitle": "Hapus grup {name}?",
+      "deleteConfirmContent": "Ini menghapus grup dan label-nya dari {count} klien. Klien itu sendiri tidak dihapus.",
+      "deleteSuccess": "Grup dihapus dari {count} klien.",
+      "resetTraffic": "Reset trafik",
+      "resetConfirmTitle": "Reset trafik grup {name}?",
+      "resetConfirmContent": "Ini mengatur ulang up/down ke 0 untuk semua {count} klien di grup ini.",
+      "resetSuccess": "Trafik direset untuk {count} klien.",
+      "adjustSuccess": "{count} klien di {name} disesuaikan.",
+      "emptyForAction": "Grup ini belum memiliki klien.",
+      "deleteGroupOnly": "Hapus grup (pertahankan klien)",
+      "deleteClients": "Hapus klien di grup",
+      "deleteClientsConfirmTitle": "Hapus semua klien di {name}?",
+      "deleteClientsConfirmContent": "Ini akan menghapus {count} klien secara permanen beserta catatan trafiknya. Label grup juga dihapus. Tidak dapat dibatalkan.",
+      "deleteClientsSuccess": "{count} klien dihapus.",
+      "deleteClientsMixed": "{ok} dihapus, {failed} dilewati",
+      "addToGroup": "Tambah klien…",
+      "addToGroupTitle": "Tambah klien ke grup «{name}»",
+      "addToGroupDesc": "Pilih klien untuk ditambahkan ke grup ini. Lampiran inbound yang ada tetap dipertahankan; hanya label grup yang berubah. Klien yang sudah ada di grup ini tidak ditampilkan.",
+      "addToGroupEmpty": "Tidak ada klien lain untuk ditambahkan.",
+      "addToGroupResult": "{count} klien ditambahkan ke {name}.",
+      "removeFromGroup": "Hapus klien…",
+      "removeFromGroupTitle": "Hapus klien dari grup «{name}»",
+      "removeFromGroupDesc": "Pilih anggota untuk dihapus dari grup ini. Klien tetap dipertahankan (gunakan «Hapus klien di grup» untuk menghapus sepenuhnya).",
+      "removeFromGroupResult": "{count} klien dihapus dari {name}."
+    },
     "nodes": {
       "title": "Node",
       "addNode": "Tambah Node",
-      "editNode": "Edit Node",
+      "editNode": "Edit node",
       "totalNodes": "Total Node",
       "onlineNodes": "Online",
       "offlineNodes": "Offline",
@@ -543,7 +798,7 @@
       "scheme": "Skema",
       "address": "Alamat",
       "port": "Port",
-      "basePath": "Base Path",
+      "basePath": "Path dasar",
       "apiToken": "Token API",
       "apiTokenPlaceholder": "Token dari halaman Pengaturan panel jarak jauh",
       "apiTokenHint": "Panel jarak jauh menampilkan token API-nya di Pengaturan → Token API.",
@@ -590,7 +845,7 @@
       "title": "Pengaturan Panel",
       "save": "Simpan",
       "infoDesc": "Setiap perubahan yang dibuat di sini perlu disimpan. Harap restart panel untuk menerapkan perubahan.",
-      "restartPanel": "Restart Panel",
+      "restartPanel": "Mulai ulang panel",
       "restartPanelDesc": "Apakah Anda yakin ingin merestart panel? Jika Anda tidak dapat mengakses panel setelah merestart, lihat info log panel di server.",
       "restartPanelSuccess": "Panel berhasil dimulai ulang",
       "actions": "Tindakan",
@@ -615,10 +870,12 @@
       "publicKeyPathDesc": "Path berkas kunci publik untuk panel web. (dimulai dengan ‘/‘)",
       "privateKeyPath": "Path Kunci Privat",
       "privateKeyPathDesc": "Path berkas kunci privat untuk panel web. (dimulai dengan ‘/‘)",
-      "panelUrlPath": "URI Path",
+      "panelUrlPath": "Path URI",
       "panelUrlPathDesc": "URI path untuk panel web. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)",
       "pageSize": "Ukuran Halaman",
       "pageSizeDesc": "Tentukan ukuran halaman untuk tabel masuk. (0 = nonaktif)",
+      "panelProxy": "Proxy jaringan panel",
+      "panelProxyDesc": "Mengarahkan permintaan keluar panel sendiri (pembaruan geo, pemeriksaan versi Xray/panel, Telegram) melalui proxy ini untuk melewati pemfilteran GitHub/Telegram di sisi server. Menerima socks5:// atau http(s)://, mis. inbound SOCKS lokal Xray. Kosongkan untuk koneksi langsung.",
       "remarkModel": "Model Catatan & Karakter Pemisah",
       "datepicker": "Jenis Kalender",
       "datepickerPlaceholder": "Pilih tanggal",
@@ -634,7 +891,7 @@
       "telegramTokenDesc": "Token bot Telegram yang diperoleh dari '{'@'}BotFather'.",
       "telegramProxy": "Proxy SOCKS",
       "telegramProxyDesc": "Mengaktifkan proxy SOCKS5 untuk terhubung ke Telegram. (sesuaikan pengaturan sesuai panduan)",
-      "telegramAPIServer": "Telegram API Server",
+      "telegramAPIServer": "Server API Telegram",
       "telegramAPIServerDesc": "Server API Telegram yang akan digunakan. Biarkan kosong untuk menggunakan server default.",
       "telegramChatId": "ID Obrolan Admin",
       "telegramChatIdDesc": "ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini {'@'}userinfobot) atau (gunakan perintah '/id' di bot)",
@@ -658,6 +915,8 @@
       "subEnable": "Aktifkan Layanan Langganan",
       "subEnableDesc": "Mengaktifkan layanan langganan.",
       "subJsonEnable": "Aktifkan/Nonaktifkan endpoint langganan JSON secara mandiri.",
+      "subJsonEnableTitle": "Langganan JSON",
+      "subClashEnableTitle": "Langganan Clash / Mihomo",
       "subTitle": "Judul Langganan",
       "subTitleDesc": "Judul yang ditampilkan di klien VPN",
       "subSupportUrl": "URL Dukungan",
@@ -678,7 +937,7 @@
       "subCertPathDesc": "Path berkas kunci publik untuk layanan langganan. (dimulai dengan ‘/‘)",
       "subKeyPath": "Path Kunci Privat",
       "subKeyPathDesc": "Path berkas kunci privat untuk layanan langganan. (dimulai dengan ‘/‘)",
-      "subPath": "URI Path",
+      "subPath": "Path URI",
       "subPathDesc": "URI path untuk layanan langganan. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)",
       "subDomain": "Domain Pendengar",
       "subDomainDesc": "Nama domain untuk layanan langganan. (biarkan kosong untuk mendengarkan semua domain dan IP)",
@@ -693,7 +952,7 @@
       "subURI": "URI Proxy Terbalik",
       "subURIDesc": "Path URI dari URL langganan untuk digunakan di belakang proxy.",
       "externalTrafficInformEnable": "Informasikan API eksternal pada setiap pembaruan lalu lintas.",
-      "externalTrafficInformEnableDesc": "Inform external API on every traffic update.",
+      "externalTrafficInformEnableDesc": "Beritahu API eksternal setiap kali ada pembaruan trafik.",
       "externalTrafficInformURI": "Lalu Lintas Eksternal Menginformasikan URI",
       "externalTrafficInformURIDesc": "Pembaruan lalu lintas dikirim ke URI ini.",
       "restartXrayOnClientDisable": "Nyalakan Ulang Xray Setelah Nonaktif Otomatis",
@@ -703,6 +962,54 @@
       "fragmentSett": "Pengaturan Fragmentasi",
       "noisesDesc": "Aktifkan Noises.",
       "noisesSett": "Pengaturan Noises",
+      "trustedProxyCidrs": "CIDR proxy tepercaya",
+      "trustedProxyCidrsDesc": "IP/CIDR (dipisahkan koma) yang diizinkan mengatur header forwarded host, proto, dan client IP.",
+      "ldap": {
+        "enable": "Aktifkan sinkronisasi LDAP",
+        "host": "LDAP host",
+        "port": "Port LDAP",
+        "useTls": "Gunakan TLS (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "Terkonfigurasi; biarkan kosong untuk mempertahankan kata sandi saat ini.",
+        "passwordUnconfigured": "Tidak terkonfigurasi.",
+        "passwordPlaceholder": "Terkonfigurasi — masukkan nilai baru untuk menggantikan",
+        "baseDn": "Base DN",
+        "userFilter": "Filter pengguna",
+        "userAttr": "Atribut pengguna (username/email)",
+        "vlessField": "Atribut flag VLESS",
+        "flagField": "Atribut flag umum (opsional)",
+        "flagFieldDesc": "Jika diatur, menimpa flag VLESS — mis. shadowInactive.",
+        "truthyValues": "Nilai truthy",
+        "truthyValuesDesc": "Dipisahkan koma; default: true,1,yes,on",
+        "invertFlag": "Balik flag",
+        "invertFlagDesc": "Aktifkan saat atribut berarti «dinonaktifkan» (mis. shadowInactive).",
+        "syncSchedule": "Jadwal sinkronisasi",
+        "syncScheduleDesc": "String mirip cron, mis. @every 1m",
+        "inboundTags": "Tag inbound",
+        "inboundTagsDesc": "Inbound di mana sinkronisasi LDAP dapat membuat/menghapus klien secara otomatis.",
+        "noInbounds": "Tidak ada inbound. Buat dulu di Inbound.",
+        "autoCreate": "Buat klien otomatis",
+        "autoDelete": "Hapus klien otomatis",
+        "defaultTotalGb": "Total default (GB)",
+        "defaultExpiryDays": "Kedaluwarsa default (hari)",
+        "defaultIpLimit": "Batas IP default"
+      },
+      "subFormats": {
+        "packets": "Paket",
+        "length": "Panjang",
+        "interval": "Interval",
+        "maxSplit": "Maks. pembagian",
+        "noises": "Noise",
+        "noiseItem": "Noise №{n}",
+        "type": "Tipe",
+        "packet": "Paket",
+        "delayMs": "Penundaan (ms)",
+        "applyTo": "Terapkan ke",
+        "addNoise": "+ Noise",
+        "concurrency": "Konkurensi",
+        "xudpConcurrency": "Konkurensi xudp",
+        "xudpUdp443": "xudp UDP 443"
+      },
       "mux": "Mux",
       "muxDesc": "Mengirimkan beberapa aliran data independen dalam aliran data yang sudah ada.",
       "muxSett": "Pengaturan Mux",
@@ -756,8 +1063,11 @@
     "xray": {
       "title": "Konfigurasi Xray",
       "save": "Simpan",
-      "restart": "Restart Xray",
+      "restart": "Mulai ulang Xray",
       "restartSuccess": "Xray berhasil diluncurkan ulang",
+      "restartOutputTitle": "Output mulai ulang Xray",
+      "restartConfirmTitle": "Mulai ulang xray?",
+      "restartConfirmContent": "Memuat ulang layanan xray dengan konfigurasi tersimpan.",
       "stopSuccess": "Xray telah berhasil dihentikan",
       "restartError": "Terjadi kesalahan saat memulai ulang Xray.",
       "stopError": "Terjadi kesalahan saat menghentikan Xray.",
@@ -765,7 +1075,7 @@
       "advancedTemplate": "Lanjutan",
       "generalConfigs": "Strategi Umum",
       "generalConfigsDesc": "Opsi ini akan menentukan penyesuaian strategi umum.",
-      "logConfigs": "Catatan",
+      "logConfigs": "Log",
       "logConfigsDesc": "Log dapat mempengaruhi efisiensi server Anda. Disarankan untuk mengaktifkannya dengan bijak hanya jika diperlukan",
       "blockConfigsDesc": "Opsi ini akan memblokir lalu lintas berdasarkan protokol dan situs web yang diminta.",
       "basicRouting": "Perutean Dasar",
@@ -790,10 +1100,12 @@
       "outboundTestUrl": "URL tes outbound",
       "outboundTestUrlDesc": "URL yang digunakan saat menguji konektivitas outbound",
       "Torrent": "Blokir Protokol BitTorrent",
-      "Inbounds": "Masuk",
+      "Inbounds": "Inbound",
       "InboundsDesc": "Menerima klien tertentu.",
-      "Outbounds": "Keluar",
+      "Outbounds": "Outbound",
       "Balancers": "Penyeimbang",
+      "balancerTagRequired": "Tag wajib diisi",
+      "balancerSelectorRequired": "Pilih setidaknya satu outbound",
       "OutboundsDesc": "Atur jalur lalu lintas keluar.",
       "Routings": "Aturan Pengalihan",
       "RoutingsDesc": "Prioritas setiap aturan penting!",
@@ -832,6 +1144,73 @@
         "edit": "Edit Aturan",
         "useComma": "Item yang dipisahkan koma"
       },
+      "routing": {
+        "dragToReorder": "Seret untuk mengurutkan ulang"
+      },
+      "ruleForm": {
+        "sourceIps": "IP sumber",
+        "sourcePort": "Port sumber",
+        "vlessRoute": "Rute VLESS",
+        "attributes": "Atribut",
+        "value": "Nilai",
+        "user": "Pengguna",
+        "inboundTags": "Tag inbound",
+        "outboundTag": "Tag outbound",
+        "balancerTag": "Tag balancer",
+        "balancerTagTooltip": "Mengarahkan trafik melalui salah satu balancer yang dikonfigurasi"
+      },
+      "outboundForm": {
+        "tagDuplicate": "Tag sudah digunakan oleh outbound lain",
+        "tagRequired": "Tag wajib diisi",
+        "tagPlaceholder": "tag-unik",
+        "localIpPlaceholder": "IP lokal",
+        "addressRequired": "Alamat wajib diisi",
+        "portRequired": "Port wajib diisi",
+        "optional": "opsional",
+        "udpOverTcp": "UDP over TCP",
+        "uotVersion": "Versi UoT",
+        "inboundTag": "Tag inbound",
+        "inboundTagPlaceholder": "tag inbound yang digunakan dalam aturan routing",
+        "responseType": "Tipe respons",
+        "rewriteNetwork": "Tulis ulang jaringan",
+        "unchanged": "(tidak berubah)",
+        "unchangedAddress": "(tidak berubah) mis. 1.1.1.1",
+        "rules": "Aturan",
+        "ruleN": "Aturan {n}",
+        "action": "Aksi",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "Aturan akhir",
+        "overrideXrayPrivateIp": "Timpa blok IP privat default Xray",
+        "blockDelay": "Penundaan blokir (ms)",
+        "reverseSniffing": "Sniffing terbalik",
+        "workers": "Workers",
+        "reserved": "Dicadangkan",
+        "minUploadInterval": "Min. interval upload (ms)",
+        "maxUploadSizeBytes": "Ukuran upload maks. (byte)",
+        "uplinkChunkSize": "Ukuran chunk Uplink",
+        "noGrpcHeader": "Tanpa header gRPC",
+        "maxConcurrency": "Maks. konkurensi",
+        "maxConnections": "Maks. koneksi",
+        "maxReuseTimes": "Maks. pemakaian ulang",
+        "maxRequestTimes": "Maks. permintaan",
+        "maxReusableSecs": "Maks. detik dapat dipakai ulang",
+        "keepAlivePeriod": "Periode keep alive",
+        "authPassword": "Kata sandi auth",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "nama server",
+        "verifyPeerName": "Verifikasi nama peer",
+        "pinnedSha256": "SHA256 pinned",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "Interval keep alive",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "Interface",
+        "ipv6Only": "Hanya IPv6",
+        "acceptProxyProtocol": "Terima proxy protocol",
+        "tcpUserTimeoutMs": "TCP user timeout (ms)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (d)"
+      },
       "outbound": {
         "addOutbound": "Tambahkan Keluar",
         "addReverse": "Tambahkan Revers",
@@ -846,7 +1225,7 @@
         "reverse": "Revers",
         "domain": "Domain",
         "type": "Tipe",
-        "bridge": "Jembatan",
+        "bridge": "Bridge",
         "portal": "Portal",
         "link": "Tautan",
         "intercon": "Interkoneksi",
@@ -860,6 +1239,8 @@
         "testSuccess": "Tes berhasil",
         "testFailed": "Tes gagal",
         "testError": "Gagal menguji outbound",
+        "testModeTooltip": "TCP: probe dial-only cepat. HTTP: permintaan penuh via xray.",
+        "testAll": "Tes semua",
         "nordvpn": "NordVPN",
         "accessToken": "Token Akses",
         "country": "Negara",
@@ -874,8 +1255,18 @@
         "editBalancer": "Sunting Penyeimbang",
         "balancerStrategy": "Strategi",
         "balancerSelectors": "Penyeleksi",
-        "tag": "Menandai",
+        "tag": "Tag",
         "tagDesc": "Label Unik",
+        "tagDuplicate": "Tag sudah digunakan oleh balancer lain",
+        "tagPlaceholder": "tag balancer unik",
+        "selector": "Selector",
+        "fallback": "Fallback",
+        "expected": "Diharapkan",
+        "expectedPlaceholder": "jumlah node optimal",
+        "maxRtt": "Maks. RTT",
+        "tolerance": "Toleransi",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "BalancerTag dan outboundTag tidak dapat digunakan secara bersamaan. Jika digunakan secara bersamaan, hanya outboundTag yang akan berfungsi."
       },
       "wireguard": {
@@ -892,6 +1283,38 @@
         "userLevel": "Level Pengguna",
         "userLevelDesc": "Semua koneksi yang dibuat melalui inbound ini akan menggunakan level pengguna ini. Standar adalah 0"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "Kunci privat",
+        "noServers": "Tidak ada server ditemukan untuk negara yang dipilih",
+        "noPublicKey": "Server yang dipilih tidak mengumumkan kunci publik NordLynx.",
+        "outboundAdded": "Outbound NordVPN ditambahkan",
+        "outboundUpdated": "Outbound NordVPN diperbarui"
+      },
+      "warp": {
+        "licenseError": "Gagal mengatur lisensi WARP.",
+        "fetchFirst": "Ambil konfig WARP terlebih dahulu.",
+        "createAccount": "Buat akun WARP",
+        "accessToken": "Access token",
+        "deviceId": "ID perangkat",
+        "licenseKey": "Kunci lisensi",
+        "privateKey": "Kunci privat",
+        "deleteAccount": "Hapus akun",
+        "settings": "Pengaturan",
+        "licenseKeyLabel": "Kunci lisensi WARP / WARP+",
+        "key": "Kunci",
+        "keyPlaceholder": "kunci WARP+ 26 karakter",
+        "accountInfo": "Info akun",
+        "deviceName": "Nama perangkat",
+        "deviceModel": "Model perangkat",
+        "deviceEnabled": "Perangkat aktif",
+        "accountType": "Tipe akun",
+        "role": "Peran",
+        "warpPlusData": "Data WARP+",
+        "quota": "Kuota",
+        "usage": "Penggunaan",
+        "addOutbound": "Tambah outbound"
+      },
       "dns": {
         "enable": "Aktifkan DNS",
         "enableDesc": "Aktifkan server DNS bawaan",
@@ -911,7 +1334,7 @@
         "strategyDesc": "Strategi keseluruhan untuk menyelesaikan nama domain",
         "add": "Tambahkan Server",
         "edit": "Sunting Server",
-        "domains": "Domains",
+        "domains": "Domain",
         "expectIPs": "IP yang Diharapkan",
         "unexpectIPs": "IP tak terduga",
         "useSystemHosts": "Gunakan Hosts Sistem",
@@ -1042,7 +1465,7 @@
       "inbound_client_data_id": "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!",
       "inbound_client_data_pass": "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 Kata sandi: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!",
       "cancel": "❌ Proses Dibatalkan! \n\nAnda dapat /start lagi kapan saja. 🔄",
-      "error_add_client": "⚠️ Kesalahan:\n\n {{ .error }}",
+      "error_add_client": "⚠️ Error:\n\n {{ .error }}",
       "using_default_value": "Oke, saya akan tetap menggunakan nilai default. 😊",
       "incorrect_input": "Masukan Anda tidak valid.\nFrasa harus berlanjut tanpa spasi.\nContoh benar: aaaaaa\nContoh salah: aaa aaa 🚫",
       "AreYouSure": "Apakah kamu yakin? 🤔",
@@ -1119,4 +1542,4 @@
       "chooseInbound": "Pilih Inbound"
     }
   }
-}
+}

+ 473 - 50
web/translation/ja-JP.json

@@ -8,15 +8,22 @@
   "save": "保存",
   "logout": "ログアウト",
   "create": "作成",
+  "add": "追加",
+  "remove": "削除",
   "update": "更新",
   "copy": "コピー",
   "copied": "コピー済み",
+  "more": "もっと",
   "download": "ダウンロード",
   "remark": "備考",
   "enable": "有効化",
   "protocol": "プロトコル",
   "search": "検索",
-  "filter": "フィルター",
+  "filter": "フィルタ",
+  "all": "すべて",
+  "from": "から",
+  "to": "まで",
+  "done": "完了",
   "loading": "読み込み中...",
   "refresh": "更新",
   "clear": "クリア",
@@ -41,7 +48,7 @@
   "transmission": "伝送",
   "host": "ホスト",
   "path": "パス",
-  "camouflage": "偽装",
+  "camouflage": "難読化",
   "status": "ステータス",
   "enabled": "有効",
   "disabled": "無効",
@@ -95,11 +102,12 @@
     "dark": "ダーク",
     "ultraDark": "ウルトラダーク",
     "dashboard": "ダッシュボード",
-    "inbounds": "インバウンド一覧",
+    "inbounds": "インバウンド",
     "clients": "クライアント",
+    "groups": "グループ",
     "nodes": "ノード",
     "settings": "パネル設定",
-    "xray": "Xray設定",
+    "xray": "Xray 設定",
     "apiDocs": "API ドキュメント",
     "logout": "ログアウト",
     "link": "リンク管理",
@@ -123,7 +131,7 @@
       "cpu": "CPU",
       "logicalProcessors": "論理プロセッサ",
       "frequency": "周波数",
-      "swap": "スワップ",
+      "swap": "Swap",
       "storage": "ストレージ",
       "memory": "RAM",
       "threads": "スレッド",
@@ -241,7 +249,7 @@
       "getConfigError": "設定ファイルの取得中にエラーが発生しました"
     },
     "inbounds": {
-      "title": "インバウンド一覧",
+      "title": "インバウンド",
       "totalDownUp": "総アップロード / ダウンロード",
       "totalUsage": "総使用量",
       "inboundCount": "インバウンド数",
@@ -252,7 +260,7 @@
       "deployTo": "デプロイ先",
       "localPanel": "ローカルパネル",
       "fallbacks": {
-        "title": "フォールバック",
+        "title": "Fallbacks",
         "help": "このインバウンドへの接続がどのクライアントにも一致しない場合、別のインバウンドへルーティングします。下から子インバウンドを選ぶと、ルーティング項目(SNI / ALPN / Path / xver)はその子のトランスポートから自動的に埋められます — ほとんどの構成で追加の調整は不要です。各子インバウンドは 127.0.0.1 で security=none をリッスンする必要があります。",
         "empty": "フォールバックはまだありません",
         "add": "フォールバックを追加",
@@ -273,11 +281,11 @@
       "portMap": "ポートマッピング",
       "traffic": "トラフィック",
       "details": "詳細情報",
-      "transportConfig": "トランスポート設定",
+      "transportConfig": "トランスポート",
       "expireDate": "有効期限",
       "createdAt": "作成",
       "updatedAt": "更新",
-      "resetTraffic": "トラフィックリセット",
+      "resetTraffic": "トラフィックリセット",
       "addInbound": "インバウンド追加",
       "generalActions": "一般操作",
       "modifyInbound": "インバウンド修正",
@@ -292,10 +300,30 @@
       "delAllClients": "すべてのクライアントを削除",
       "delAllClientsConfirmTitle": "「{remark}」から {count} 件のクライアントをすべて削除しますか?",
       "delAllClientsConfirmContent": "このインバウンドからすべてのクライアントを削除し、トラフィックレコードも破棄します。インバウンド自体は保持されます。この操作は取り消せません。",
+      "attachClients": "クライアントをアタッチ…",
+      "addClientsToGroup": "クライアントをグループに追加…",
+      "attachClientsTitle": "「{remark}」のクライアントをアタッチ",
+      "attachClientsDesc": "同じ {count} クライアント(同じ UUID/パスワードと共有トラフィック)を選択したインバウンドにアタッチします。このインバウンドにも残ります。",
+      "attachClientsTargets": "ターゲットインバウンド",
+      "attachClientsNoTargets": "アタッチ可能な互換インバウンドがありません。",
+      "attachClientsResult": "アタッチ {attached}、スキップ {skipped}。",
+      "attachClientsResultMixed": "アタッチ {attached}、スキップ {skipped}、エラー {errors}。",
+      "attachClientsSelectLabel": "アタッチするクライアント",
+      "attachClientsSearchPlaceholder": "メールまたはコメントを検索",
+      "attachClientsStatusDisabled": "無効",
+      "attachClientsSelectedCount": "{total} 中 {selected} 選択中",
+      "detachClients": "クライアントをデタッチ",
+      "detachClientsTitle": "「{remark}」のクライアントをデタッチ",
+      "detachClientsDesc": "選択したクライアントをこのインバウンドのみから外します。クライアントレコードは保持されます (完全に削除するには Delete を使用)。ソースには合計 {count} クライアントがあります。",
+      "detachClientsResult": "デタッチ {detached}、スキップ {skipped}。",
+      "detachClientsResultMixed": "デタッチ {detached}、スキップ {skipped}、エラー {errors}。",
+      "detachClientsSelectLabel": "デタッチするクライアント",
       "exportLinksTitle": "インバウンドリンクのエクスポート",
       "exportSubsTitle": "サブスクリプションリンクのエクスポート",
       "exportAllLinksTitle": "全インバウンドリンクのエクスポート",
       "exportAllSubsTitle": "全サブスクリプションリンクのエクスポート",
+      "exportAllLinksFileName": "全インバウンド",
+      "exportAllSubsFileName": "全インバウンド-Subs",
       "inboundJsonTitle": "インバウンド JSON",
       "deleteClient": "クライアント削除",
       "deleteClientContent": "クライアントを削除してもよろしいですか?",
@@ -306,7 +334,7 @@
       "destinationPort": "宛先ポート",
       "targetAddress": "宛先アドレス",
       "monitorDesc": "空白にするとすべてのIPを監視",
-      "meansNoLimit": "= 無制限(単位:GB)",
+      "meansNoLimit": "= 無制限。(単位: GB)",
       "totalFlow": "総トラフィック",
       "leaveBlankToNeverExpire": "空白にすると期限なし",
       "noRecommendKeepDefault": "デフォルト値を保持することをお勧めします",
@@ -333,7 +361,7 @@
       "delDepletedClients": "トラフィックが尽きたクライアントを削除",
       "delDepletedClientsTitle": "トラフィックが尽きたクライアントを削除",
       "delDepletedClientsContent": "トラフィックが尽きたすべてのクライアントを削除してもよろしいですか?",
-      "email": "メールアドレス",
+      "email": "メール",
       "emailDesc": "メールアドレスは一意でなければなりません",
       "IPLimit": "IP制限",
       "IPLimitDesc": "設定値を超えるとインバウンドトラフィックが無効になります。(0 = 無効)",
@@ -341,6 +369,7 @@
       "IPLimitlogDesc": "IP履歴ログ(無効なインバウンドトラフィックを有効にするには、ログをクリアしてください)",
       "IPLimitlogclear": "ログをクリア",
       "setDefaultCert": "パネル設定から証明書を設定",
+      "setDefaultCertEmpty": "パネル用の証明書が設定されていません。先に設定から指定してください。",
       "streamTab": "ストリーム",
       "securityTab": "セキュリティ",
       "sniffingTab": "スニッフィング",
@@ -361,15 +390,14 @@
         "allHelp": "すべてのフィールドを含むインバウンドオブジェクト全体を 1 つのエディターで編集します。",
         "settings": "設定",
         "settingsHelp": "Xray settings ブロックのラッパー:",
-        "sniffing": "スニッフィング",
+        "sniffing": "Sniffing",
         "sniffingHelp": "Xray sniffing ブロックのラッパー:",
-        "stream": "ストリーム",
+        "stream": "Stream",
         "streamHelp": "Xray stream ブロックのラッパー:",
         "jsonErrorPrefix": "高度な JSON"
       },
       "telegramDesc": "TelegramチャットIDを提供してください。(ボットで'/id'コマンドを使用)または({'@'}userinfobot)",
       "subscriptionDesc": "サブスクリプションURLを見つけるには、“詳細情報”に移動してください。また、複数のクライアントに同じ名前を使用することができます。",
-      "info": "情報",
       "same": "同じ",
       "inboundData": "インバウンドデータ",
       "exportInbound": "インバウンドルールをエクスポート",
@@ -406,6 +434,143 @@
         "getNewmldsa65Error": "mldsa65証明書の取得中にエラーが発生しました。",
         "getNewVlessEncError": "VlessEnc証明書の取得中にエラーが発生しました。"
       },
+      "form": {
+        "moveUp": "上へ",
+        "moveDown": "下へ",
+        "addAll": "すべて追加",
+        "addAllFallbackTooltip": "まだ接続されていないすべての対象インバウンドに対し fallback 行を追加",
+        "peers": "Peers",
+        "addPeer": "peer を追加",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "Windows のみ。CIDR はシステムルーティングテーブルに自動追加され、一致するトラフィックは TUN を経由します。",
+        "autoOutboundsInterface": "自動アウトバウンドインターフェース",
+        "autoOutboundsInterfaceTooltip": "アウトバウンドトラフィック用の物理インターフェース。検出には 'auto' を使用; Auto system routes が設定されていると自動的に有効。",
+        "rewriteAddress": "アドレス書き換え",
+        "rewritePort": "ポート書き換え",
+        "allowedNetwork": "許可されたネットワーク",
+        "followRedirect": "リダイレクトに従う",
+        "accounts": "アカウント",
+        "allowTransparent": "透過を許可",
+        "encryptionMethod": "暗号化方式",
+        "visionTestseed": "Vision testseed",
+        "version": "バージョン",
+        "udpIdleTimeout": "UDP idle timeout (秒)",
+        "masquerade": "Masquerade",
+        "type": "種類",
+        "upstreamUrl": "Upstream URL",
+        "rewriteHost": "Host 書き換え",
+        "skipTlsVerify": "TLS 検証をスキップ",
+        "directory": "ディレクトリ",
+        "statusCode": "ステータスコード",
+        "body": "Body",
+        "headers": "ヘッダー",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "リクエストバージョン",
+        "requestMethod": "リクエストメソッド",
+        "requestPath": "リクエストパス",
+        "requestHeaders": "リクエストヘッダー",
+        "responseVersion": "レスポンスバージョン",
+        "responseStatus": "レスポンスステータス",
+        "responseReason": "レスポンス理由",
+        "responseHeaders": "レスポンスヘッダー",
+        "heartbeatPeriod": "ハートビート間隔",
+        "serviceName": "サービス名",
+        "authority": "Authority",
+        "multiMode": "Multi Mode",
+        "maxBufferedUpload": "最大バッファアップロード",
+        "maxUploadSize": "最大アップロードサイズ (バイト)",
+        "streamUpServer": "Stream-Up Server",
+        "serverMaxHeaderBytes": "サーバー最大ヘッダーバイト",
+        "paddingBytes": "Padding バイト",
+        "uplinkHttpMethod": "Uplink HTTP メソッド",
+        "paddingObfsMode": "Padding 難読化モード",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "Padding 配置",
+        "paddingMethod": "Padding 方法",
+        "sessionPlacement": "Session Placement",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence Placement",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink Data Placement",
+        "uplinkDataKey": "Uplink Data Key",
+        "noSseHeader": "SSE ヘッダーなし",
+        "ttiMs": "TTI (ms)",
+        "uplinkMbps": "アップリンク (MB/s)",
+        "downlinkMbps": "ダウンリンク (MB/s)",
+        "cwndMultiplier": "CWND 倍率",
+        "maxSendingWindow": "最大送信ウィンドウ",
+        "externalProxy": "外部プロキシ",
+        "sniPlaceholder": "SNI (デフォルトは host)",
+        "fingerprint": "Fingerprint",
+        "defaultOption": "デフォルト",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive Interval",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "V6 のみ",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "信頼できる X-Forwarded-For",
+        "addressPortStrategy": "アドレス+ポート戦略",
+        "tryDelayMs": "試行遅延 (ms)",
+        "prioritizeIPv6": "IPv6 優先",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "最大同時試行",
+        "customSockopt": "カスタム sockopt",
+        "addCustomOption": "カスタムオプション追加",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "自動",
+        "minMaxVersion": "最小/最大バージョン",
+        "rejectUnknownSni": "未知の SNI を拒否",
+        "disableSystemRoot": "System Root を無効化",
+        "sessionResumption": "セッション再開",
+        "oneTimeLoading": "一度のみ読み込み",
+        "usageOption": "使用オプション",
+        "buildChain": "Build Chain",
+        "echKey": "ECH key",
+        "echConfig": "ECH config",
+        "pinnedPeerCertSha256": "ピン留めピア証明書 SHA-256",
+        "pinnedPeerCertSha256Tip": "ピア証明書の Base64 エンコード SHA-256 ハッシュ。パネルのみ — サーバーの xray 設定には書き込まれませんが、共有リンクには含まれ、クライアントが証明書をピン留めできます。",
+        "pinnedPeerCertSha256Placeholder": "Base64 ハッシュ、カンマ区切り",
+        "generateRandomPin": "ランダムハッシュを生成",
+        "getNewEchCert": "新しい ECH 証明書を取得",
+        "show": "表示",
+        "xver": "Xver",
+        "target": "ターゲット",
+        "maxTimeDiff": "最大時間差 (ms)",
+        "minClientVer": "最小クライアントバージョン",
+        "maxClientVer": "最大クライアントバージョン",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "新しい証明書を取得",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "新しい Seed を取得"
+      },
+      "info": {
+        "mode": "モード",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "インターフェース名",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "アウトバウンドインターフェース",
+        "autoSystemRoutes": "自動システムルート",
+        "followRedirect": "FollowRedirect",
+        "auth": "Auth",
+        "noKernelTun": "非カーネル TUN",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "Peer {n} 設定"
+      },
       "stream": {
         "general": {
           "request": "リクエスト",
@@ -456,6 +621,20 @@
       "days": "日",
       "renew": "自動更新",
       "renewDesc": "有効期限切れ後に自動更新します。(0 = 無効) (単位: 日)",
+      "searchPlaceholder": "メール、コメント、sub ID、UUID、パスワード、auth を検索…",
+      "filterTitle": "クライアントをフィルタ",
+      "clearAllFilters": "すべてクリア",
+      "sortOldest": "古い順",
+      "sortNewest": "新しい順",
+      "sortRecentlyUpdated": "最近更新",
+      "sortRecentlyOnline": "最近オンライン",
+      "sortEmailAZ": "メール A→Z",
+      "sortEmailZA": "メール Z→A",
+      "sortMostTraffic": "トラフィック多い順",
+      "sortHighestRemaining": "残量多い順",
+      "sortExpiringSoonest": "もうすぐ期限切れ",
+      "has": "あり",
+      "hasNot": "なし",
       "title": "クライアント",
       "actions": "操作",
       "totalGB": "送受信合計 (GB)",
@@ -466,6 +645,9 @@
       "subId": "サブスクリプション ID",
       "online": "オンライン",
       "email": "メール",
+      "group": "グループ",
+      "groupDesc": "関連クライアントをまとめる論理ラベル(チーム、顧客、地域など)。ツールバーからフィルタ可能。",
+      "groupPlaceholder": "例: customer-a",
       "comment": "コメント",
       "traffic": "トラフィック",
       "offline": "オフライン",
@@ -489,11 +671,45 @@
       "resetAllTraffics": "すべてのクライアントのトラフィックをリセット",
       "resetAllTrafficsTitle": "すべてのクライアントのトラフィックをリセットしますか?",
       "resetAllTrafficsContent": "すべてのクライアントの送受信カウンターがゼロにリセットされます。クォータと有効期限には影響しません。元に戻せません。",
-      "empty": "クライアントはまだいません — 1 つ追加して始めましょう。",
       "deleteConfirmTitle": "クライアント {email} を削除しますか?",
       "deleteConfirmContent": "クライアントを関連付けされたすべてのインバウンドから削除し、トラフィック記録も破棄します。元に戻せません。",
       "deleteSelected": "削除 ({count})",
       "adjustSelected": "調整 ({count})",
+      "subLinksSelected": "サブリンク ({count})",
+      "addToGroupTitle": "{count} クライアントをグループに追加",
+      "addToGroupTooltip": "既存のグループを選ぶか新しい名前を入力してください。Ungroup で現在のグループから外せます。",
+      "addToGroupPlaceholder": "グループ名",
+      "addToGroupSuccessToast": "{count} クライアントを {group} に追加しました",
+      "ungroupSuccessToast": "{count} クライアントのグループをクリアしました",
+      "ungroup": "グループ解除",
+      "ungroupConfirmTitle": "{count} クライアントをグループから外しますか?",
+      "ungroupConfirmContent": "選択したクライアントのグループラベルをクリアします。クライアント自体は保持されます (完全に削除するには Delete を使用)。",
+      "addToGroup": "グループに追加",
+      "attach": "アタッチ",
+      "adjust": "調整",
+      "subLinks": "サブリンク",
+      "selectedCount": "{count} 選択中",
+      "attachSelected": "アタッチ ({count})",
+      "attachToInboundsTitle": "{count} クライアントをインバウンドにアタッチ",
+      "attachToInboundsDesc": "選択した {count} クライアント(同じ UUID/パスワードと共有トラフィック)を選択したインバウンドにアタッチします。既存のアタッチは維持されます。",
+      "attachToInboundsTargets": "ターゲットインバウンド",
+      "attachToInboundsNoTargets": "アタッチ可能なマルチユーザーインバウンドがありません。",
+      "detachSelected": "デタッチ ({count})",
+      "detach": "デタッチ",
+      "detachFromInboundsTitle": "{count} クライアントをインバウンドからデタッチ",
+      "detachFromInboundsDesc": "選択した {count} クライアントを選択したインバウンドから外します。アタッチされていなかったペアは黙ってスキップされます。クライアントレコードは保持されます (完全に削除するには Delete を使用)。",
+      "detachFromInboundsTargets": "デタッチ対象のインバウンド",
+      "detachFromInboundsNoTargets": "マルチユーザーインバウンドがありません。",
+      "detachFromInboundsResult": "デタッチ {detached}、スキップ {skipped}。",
+      "detachFromInboundsResultMixed": "デタッチ {detached}、スキップ {skipped}、エラー {errors}。",
+      "subLinksTitle": "サブリンク ({count})",
+      "subLinkColumn": "サブスクリプション URL",
+      "subJsonLinkColumn": "サブスクリプション JSON URL",
+      "subLinksCopyAll": "すべてコピー",
+      "subLinksCopiedAll": "{count} リンクをコピーしました",
+      "subLinksEmpty": "選択したクライアントにはサブスクリプション ID がありません。",
+      "subLinksDisabled": "サブスクリプションサービスは無効です。",
+      "subLinksDisabledHint": "リンクを生成するにはパネル設定 → サブスクリプションで有効にしてください。",
       "bulkDeleteConfirmTitle": "{count} 件のクライアントを削除しますか?",
       "bulkDeleteConfirmContent": "選択された各クライアントを関連付けされたすべてのインバウンドから削除し、トラフィック記録も破棄します。元に戻せません。",
       "bulkAdjustTitle": "{count} 件のクライアントを調整",
@@ -505,9 +721,10 @@
       "delDepletedConfirmTitle": "使い切ったクライアントを削除しますか?",
       "delDepletedConfirmContent": "トラフィック上限に達したか有効期限が切れたクライアントをすべて削除します。元に戻せません。",
       "auth": "Auth",
-      "hysteriaAuth": "Auth (Hysteria)",
+      "hysteriaAuth": "Hysteria Auth",
       "uuid": "UUID",
       "flow": "Flow",
+      "vmessSecurity": "VMess セキュリティ",
       "reverseTag": "Reverse tag",
       "reverseTagPlaceholder": "任意の Reverse tag",
       "telegramId": "Telegram ユーザー ID",
@@ -528,10 +745,48 @@
         "delDepleted": "使い切った {count} 件のクライアントを削除しました"
       }
     },
+    "groups": {
+      "title": "グループ",
+      "name": "名前",
+      "clientCount": "グループ内のクライアント",
+      "totalGroups": "グループ合計",
+      "totalGroupedClients": "グループのあるクライアント",
+      "emptyGroups": "空のグループ",
+      "addGroup": "グループ追加",
+      "createSuccess": "グループ「{name}」を作成しました。",
+      "rename": "名前変更",
+      "renameTitle": "{name} の名前を変更",
+      "renameCollision": "「{name}」という名前のグループは既に存在します。",
+      "renameSuccess": "{count} クライアントのグループ名を変更しました。",
+      "deleteConfirmTitle": "グループ {name} を削除?",
+      "deleteConfirmContent": "これはグループを削除し、{count} クライアントのラベルをクリアします。クライアント自体は削除されません。",
+      "deleteSuccess": "{count} クライアントのグループをクリアしました。",
+      "resetTraffic": "トラフィックをリセット",
+      "resetConfirmTitle": "グループ {name} のトラフィックをリセット?",
+      "resetConfirmContent": "このグループ内のすべての {count} クライアントの up/down をゼロにします。",
+      "resetSuccess": "{count} クライアントのトラフィックをリセットしました。",
+      "adjustSuccess": "{name} 内の {count} クライアントを調整しました。",
+      "emptyForAction": "このグループにはまだクライアントがありません。",
+      "deleteGroupOnly": "グループ削除 (クライアントは保持)",
+      "deleteClients": "グループのクライアントを削除",
+      "deleteClientsConfirmTitle": "{name} 内のすべてのクライアントを削除?",
+      "deleteClientsConfirmContent": "これは {count} クライアントとそのトラフィック記録を永久に削除します。グループラベルもクリアされます。取り消せません。",
+      "deleteClientsSuccess": "{count} クライアントを削除しました。",
+      "deleteClientsMixed": "{ok} 削除、{failed} スキップ",
+      "addToGroup": "クライアントを追加…",
+      "addToGroupTitle": "グループ「{name}」にクライアントを追加",
+      "addToGroupDesc": "このグループに追加するクライアントを選択してください。既存のインバウンドアタッチは保持され、グループラベルのみ変更されます。すでにこのグループにいるクライアントは表示されません。",
+      "addToGroupEmpty": "追加可能な他のクライアントはありません。",
+      "addToGroupResult": "{count} クライアントを {name} に追加しました。",
+      "removeFromGroup": "クライアントを削除…",
+      "removeFromGroupTitle": "グループ「{name}」からクライアントを削除",
+      "removeFromGroupDesc": "このグループから外すメンバーを選択してください。クライアント自体は保持されます (完全に削除するには「グループのクライアントを削除」を使用)。",
+      "removeFromGroupResult": "{count} クライアントを {name} から外しました。"
+    },
     "nodes": {
       "title": "ノード",
       "addNode": "ノードを追加",
-      "editNode": "ノードを編集",
+      "editNode": "ノード編集",
       "totalNodes": "ノード総数",
       "onlineNodes": "オンライン",
       "offlineNodes": "オフライン",
@@ -544,7 +799,7 @@
       "address": "アドレス",
       "port": "ポート",
       "basePath": "ベースパス",
-      "apiToken": "APIトークン",
+      "apiToken": "API トークン",
       "apiTokenPlaceholder": "リモートパネルの設定ページから取得したトークン",
       "apiTokenHint": "リモートパネルでは、設定 → APIトークン でAPIトークンを確認できます。",
       "regenerate": "トークンを再生成",
@@ -590,7 +845,7 @@
       "title": "パネル設定",
       "save": "保存",
       "infoDesc": "ここでのすべての変更は、保存してパネルを再起動する必要があります",
-      "restartPanel": "パネル再起動",
+      "restartPanel": "パネル再起動",
       "restartPanelDesc": "パネルを再起動してもよろしいですか?再起動後にパネルにアクセスできない場合は、サーバーでパネルログを確認してください",
       "restartPanelSuccess": "パネルの再起動に成功しました",
       "actions": "操作",
@@ -604,7 +859,7 @@
       "warnDefaultBasePath": "デフォルトのベースパス \"/\" はよく知られています — ランダムなパスに変更してください。",
       "warnDefaultSubPath": "デフォルトのサブスクリプションパス \"/sub/\" はよく知られています — 変更してください。",
       "warnDefaultJsonPath": "デフォルトの JSON サブスクリプションパス \"/json/\" はよく知られています — 変更してください。",
-      "TGBotSettings": "Telegramボット設定",
+      "TGBotSettings": "Telegram Bot",
       "panelListeningIP": "パネル監視IP",
       "panelListeningIPDesc": "デフォルトではすべてのIPを監視する",
       "panelListeningDomain": "パネル監視ドメイン",
@@ -615,10 +870,12 @@
       "publicKeyPathDesc": "'/'で始まる絶対パスを入力",
       "privateKeyPath": "パネル証明書秘密鍵ファイルパス",
       "privateKeyPathDesc": "'/'で始まる絶対パスを入力",
-      "panelUrlPath": "パネルURLルートパス",
+      "panelUrlPath": "URI パス",
       "panelUrlPathDesc": "'/'で始まり、'/'で終わる必要があります",
       "pageSize": "ページサイズ",
       "pageSizeDesc": "インバウンドテーブルのページサイズを定義します。0を設定すると無効化されます",
+      "panelProxy": "パネルネットワークプロキシ",
+      "panelProxyDesc": "パネル自体のアウトバウンドリクエスト (geo 更新、Xray/パネルバージョンチェック、Telegram) をこのプロキシ経由でルーティングし、サーバー側の GitHub/Telegram フィルタリングを回避します。socks5:// または http(s):// を受け付けます。例: ローカルの Xray SOCKS インバウンド。直接接続するには空のままにします。",
       "remarkModel": "備考モデルと区切り記号",
       "datepicker": "日付ピッカー",
       "datepickerPlaceholder": "日付を選択",
@@ -630,11 +887,11 @@
       "newPassword": "新しいパスワード",
       "telegramBotEnable": "Telegramボットを有効にする",
       "telegramBotEnableDesc": "Telegramボット機能を有効にする",
-      "telegramToken": "Telegramボットトークン",
+      "telegramToken": "Telegram トークン",
       "telegramTokenDesc": "'{'@'}BotFather'から取得したTelegramボットトークン",
-      "telegramProxy": "SOCKS5プロキシ",
+      "telegramProxy": "SOCKS プロキシ",
       "telegramProxyDesc": "SOCKS5プロキシを有効にしてTelegramに接続する(ガイドに従って設定を調整)",
-      "telegramAPIServer": "Telegram APIサーバー",
+      "telegramAPIServer": "Telegram API サーバー",
       "telegramAPIServerDesc": "使用するTelegram APIサーバー。空白の場合はデフォルトサーバーを使用する",
       "telegramChatId": "管理者チャットID",
       "telegramChatIdDesc": "Telegram管理者チャットID(複数の場合はカンマで区切る){'@'}userinfobotで取得するか、ボットで'/id'コマンドを使用して取得する",
@@ -658,6 +915,8 @@
       "subEnable": "サブスクリプションサービスを有効にする",
       "subEnableDesc": "サブスクリプションサービス機能を有効にする",
       "subJsonEnable": "JSON サブスクリプションのエンドポイントを個別に有効/無効にする。",
+      "subJsonEnableTitle": "JSON サブスクリプション",
+      "subClashEnableTitle": "Clash / Mihomo サブスクリプション",
       "subTitle": "サブスクリプションタイトル",
       "subTitleDesc": "VPNクライアントに表示されるタイトル",
       "subSupportUrl": "サポートURL",
@@ -678,7 +937,7 @@
       "subCertPathDesc": "サブスクリプションサービスで使用する公開鍵ファイルのパス('/'で始まる)",
       "subKeyPath": "秘密鍵パス",
       "subKeyPathDesc": "サブスクリプションサービスで使用する秘密鍵ファイルのパス('/'で始まる)",
-      "subPath": "URIパス",
+      "subPath": "URI パス",
       "subPathDesc": "サブスクリプションサービスで使用するURIパス('/'で始まり、'/'で終わる)",
       "subDomain": "監視ドメイン",
       "subDomainDesc": "サブスクリプションサービスが監視するドメイン(空白にするとすべてのドメインとIPを監視)",
@@ -693,7 +952,7 @@
       "subURI": "リバースプロキシURI",
       "subURIDesc": "プロキシ後ろのサブスクリプションURLのURIパスに使用する",
       "externalTrafficInformEnable": "外部トラフィック情報",
-      "externalTrafficInformEnableDesc": "トラフィック更新ごとに外部 API に通知します。",
+      "externalTrafficInformEnableDesc": "トラフィック更新ごとに外部 API に通知。",
       "externalTrafficInformURI": "外部トラフィック通知 URI",
       "externalTrafficInformURIDesc": "トラフィックの更新ごとに外部 API に通知します。",
       "restartXrayOnClientDisable": "自動無効化後に Xray を再起動",
@@ -703,7 +962,55 @@
       "fragmentSett": "設定",
       "noisesDesc": "Noisesを有効にする",
       "noisesSett": "Noises設定",
-      "mux": "マルチプレクサ",
+      "trustedProxyCidrs": "信頼できるプロキシ CIDR",
+      "trustedProxyCidrsDesc": "転送される host、proto、クライアント IP ヘッダーを設定可能な IP/CIDR (カンマ区切り)。",
+      "ldap": {
+        "enable": "LDAP 同期を有効化",
+        "host": "LDAP host",
+        "port": "LDAP ポート",
+        "useTls": "TLS (LDAPS) を使用",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "設定済み;現在のパスワードを保持するには空のままにします。",
+        "passwordUnconfigured": "未設定。",
+        "passwordPlaceholder": "設定済み — 置き換えるには新しい値を入力",
+        "baseDn": "Base DN",
+        "userFilter": "ユーザーフィルター",
+        "userAttr": "ユーザー属性 (username/email)",
+        "vlessField": "VLESS flag 属性",
+        "flagField": "汎用 flag 属性 (任意)",
+        "flagFieldDesc": "設定すると VLESS flag を上書きします — 例: shadowInactive。",
+        "truthyValues": "Truthy 値",
+        "truthyValuesDesc": "カンマ区切り;デフォルト: true,1,yes,on",
+        "invertFlag": "flag を反転",
+        "invertFlagDesc": "属性が「無効」を意味する場合に有効化 (例: shadowInactive)。",
+        "syncSchedule": "同期スケジュール",
+        "syncScheduleDesc": "cron 風の文字列、例 @every 1m",
+        "inboundTags": "インバウンドタグ",
+        "inboundTagsDesc": "LDAP 同期がクライアントを自動作成/削除できるインバウンド。",
+        "noInbounds": "インバウンドが見つかりません。先にインバウンドで作成してください。",
+        "autoCreate": "クライアントを自動作成",
+        "autoDelete": "クライアントを自動削除",
+        "defaultTotalGb": "デフォルト合計 (GB)",
+        "defaultExpiryDays": "デフォルト有効期限 (日)",
+        "defaultIpLimit": "デフォルト IP 制限"
+      },
+      "subFormats": {
+        "packets": "パケット",
+        "length": "長さ",
+        "interval": "間隔",
+        "maxSplit": "最大分割",
+        "noises": "ノイズ",
+        "noiseItem": "ノイズ №{n}",
+        "type": "種類",
+        "packet": "パケット",
+        "delayMs": "遅延 (ms)",
+        "applyTo": "適用先",
+        "addNoise": "+ ノイズ",
+        "concurrency": "並行数",
+        "xudpConcurrency": "xudp 並行数",
+        "xudpUdp443": "xudp UDP 443"
+      },
+      "mux": "Mux",
       "muxDesc": "確立されたストリーム内で複数の独立したストリームを伝送する",
       "muxSett": "マルチプレクサ設定",
       "direct": "直接接続",
@@ -756,8 +1063,11 @@
     "xray": {
       "title": "Xray 設定",
       "save": "保存",
-      "restart": "Xray 再起動",
+      "restart": "Xray 再起動",
       "restartSuccess": "Xrayの再起動に成功しました",
+      "restartOutputTitle": "Xray 再起動の出力",
+      "restartConfirmTitle": "xray を再起動?",
+      "restartConfirmContent": "保存された構成で xray サービスを再ロードします。",
       "stopSuccess": "Xrayが正常に停止しました",
       "restartError": "Xrayの再起動中にエラーが発生しました。",
       "stopError": "Xrayの停止中にエラーが発生しました。",
@@ -790,10 +1100,12 @@
       "outboundTestUrl": "アウトバウンドテスト URL",
       "outboundTestUrlDesc": "アウトバウンド接続テストに使用する URL。既定値",
       "Torrent": "BitTorrent プロトコルをブロック",
-      "Inbounds": "インバウンドルール",
+      "Inbounds": "インバウンド",
       "InboundsDesc": "特定のクライアントからのトラフィックを受け入れる",
-      "Outbounds": "アウトバウンドルール",
+      "Outbounds": "アウトバウンド",
       "Balancers": "負荷分散",
+      "balancerTagRequired": "タグは必須です",
+      "balancerSelectorRequired": "アウトバウンドを少なくとも1つ選んでください",
       "OutboundsDesc": "アウトバウンドトラフィックの送信方法を設定する",
       "Routings": "ルーティングルール",
       "RoutingsDesc": "各ルールの優先順位が重要です",
@@ -832,6 +1144,73 @@
         "edit": "ルール編集",
         "useComma": "カンマ区切りの項目"
       },
+      "routing": {
+        "dragToReorder": "ドラッグして並べ替え"
+      },
+      "ruleForm": {
+        "sourceIps": "送信元 IP",
+        "sourcePort": "送信元ポート",
+        "vlessRoute": "VLESS ルート",
+        "attributes": "属性",
+        "value": "値",
+        "user": "ユーザー",
+        "inboundTags": "インバウンドタグ",
+        "outboundTag": "アウトバウンドタグ",
+        "balancerTag": "バランサータグ",
+        "balancerTagTooltip": "設定済みのロードバランサーの1つを通じてトラフィックをルーティング"
+      },
+      "outboundForm": {
+        "tagDuplicate": "このタグは他のアウトバウンドで使用されています",
+        "tagRequired": "タグは必須です",
+        "tagPlaceholder": "一意のタグ",
+        "localIpPlaceholder": "ローカル IP",
+        "addressRequired": "アドレスは必須です",
+        "portRequired": "ポートは必須です",
+        "optional": "任意",
+        "udpOverTcp": "UDP over TCP",
+        "uotVersion": "UoT バージョン",
+        "inboundTag": "インバウンドタグ",
+        "inboundTagPlaceholder": "ルーティングルールで使うインバウンドタグ",
+        "responseType": "レスポンスタイプ",
+        "rewriteNetwork": "ネットワーク書き換え",
+        "unchanged": "(変更なし)",
+        "unchangedAddress": "(変更なし) 例: 1.1.1.1",
+        "rules": "ルール",
+        "ruleN": "ルール {n}",
+        "action": "アクション",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "最終ルール",
+        "overrideXrayPrivateIp": "Xray のデフォルトプライベート IP ブロックを上書き",
+        "blockDelay": "ブロック遅延 (ms)",
+        "reverseSniffing": "逆 sniffing",
+        "workers": "Workers",
+        "reserved": "予約",
+        "minUploadInterval": "最小アップロード間隔 (ms)",
+        "maxUploadSizeBytes": "最大アップロードサイズ (バイト)",
+        "uplinkChunkSize": "Uplink チャンクサイズ",
+        "noGrpcHeader": "gRPC ヘッダーなし",
+        "maxConcurrency": "最大同時実行数",
+        "maxConnections": "最大接続数",
+        "maxReuseTimes": "最大再利用回数",
+        "maxRequestTimes": "最大リクエスト回数",
+        "maxReusableSecs": "最大再利用秒数",
+        "keepAlivePeriod": "keep alive 周期",
+        "authPassword": "Auth パスワード",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "サーバー名",
+        "verifyPeerName": "peer 名を検証",
+        "pinnedSha256": "Pinned SHA256",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "keep alive 間隔",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "インターフェース",
+        "ipv6Only": "IPv6 のみ",
+        "acceptProxyProtocol": "proxy protocol を受け入れる",
+        "tcpUserTimeoutMs": "TCP user timeout (ms)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (秒)"
+      },
       "outbound": {
         "addOutbound": "アウトバウンド追加",
         "addReverse": "リバース追加",
@@ -846,8 +1225,8 @@
         "reverse": "リバース",
         "domain": "ドメイン",
         "type": "タイプ",
-        "bridge": "ブリッジ",
-        "portal": "ポータル",
+        "bridge": "Bridge",
+        "portal": "Portal",
         "link": "リンク",
         "intercon": "インターコネクション",
         "settings": "設定",
@@ -860,6 +1239,8 @@
         "testSuccess": "テスト成功",
         "testFailed": "テスト失敗",
         "testError": "アウトバウンドのテストに失敗しました",
+        "testModeTooltip": "TCP: 高速 dial-only プローブ。HTTP: xray を経由した完全リクエスト。",
+        "testAll": "すべてテスト",
         "nordvpn": "NordVPN",
         "accessToken": "アクセストークン",
         "country": "国",
@@ -876,6 +1257,16 @@
         "balancerSelectors": "セレクター",
         "tag": "タグ",
         "tagDesc": "一意のタグ",
+        "tagDuplicate": "このタグは他のバランサーで使用されています",
+        "tagPlaceholder": "一意のバランサータグ",
+        "selector": "セレクター",
+        "fallback": "Fallback",
+        "expected": "期待値",
+        "expectedPlaceholder": "最適ノード数",
+        "maxRtt": "最大 RTT",
+        "tolerance": "許容範囲",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "balancerTagとoutboundTagは同時に使用できません。同時に使用された場合、outboundTagのみが有効になります。"
       },
       "wireguard": {
@@ -892,6 +1283,38 @@
         "userLevel": "ユーザーレベル",
         "userLevelDesc": "このインバウンドを通じて確立されたすべての接続は、このユーザーレベルを使用します。デフォルトは 0 です"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "秘密鍵",
+        "noServers": "選択した国のサーバーが見つかりません",
+        "noPublicKey": "選択したサーバーは NordLynx 公開鍵を公開していません。",
+        "outboundAdded": "NordVPN アウトバウンドを追加しました",
+        "outboundUpdated": "NordVPN アウトバウンドを更新しました"
+      },
+      "warp": {
+        "licenseError": "WARP ライセンスの設定に失敗しました。",
+        "fetchFirst": "先に WARP 構成を取得してください。",
+        "createAccount": "WARP アカウントを作成",
+        "accessToken": "Access token",
+        "deviceId": "デバイス ID",
+        "licenseKey": "ライセンスキー",
+        "privateKey": "秘密鍵",
+        "deleteAccount": "アカウントを削除",
+        "settings": "設定",
+        "licenseKeyLabel": "WARP / WARP+ ライセンスキー",
+        "key": "キー",
+        "keyPlaceholder": "26文字の WARP+ キー",
+        "accountInfo": "アカウント情報",
+        "deviceName": "デバイス名",
+        "deviceModel": "デバイスモデル",
+        "deviceEnabled": "デバイス有効",
+        "accountType": "アカウントタイプ",
+        "role": "役割",
+        "warpPlusData": "WARP+ データ",
+        "quota": "クォータ",
+        "usage": "使用量",
+        "addOutbound": "アウトバウンドを追加"
+      },
       "dns": {
         "enable": "DNSを有効にする",
         "enableDesc": "組み込みDNSサーバーを有効にする",
@@ -992,35 +1415,35 @@
       "2faFailed": "2FAエラー",
       "report": "🕰 定期報告:{{ .RunTime }}\r\n",
       "datetime": "⏰ 日時:{{ .DateTime }}\r\n",
-      "hostname": "💻 ホスト名:{{ .Hostname }}\r\n",
+      "hostname": "💻 ホスト: {{ .Hostname }}\r\n",
       "version": "🚀 X-UI バージョン:{{ .Version }}\r\n",
       "xrayVersion": "📡 Xray バージョン: {{ .XrayVersion }}\r\n",
-      "ipv6": "🌐 IPv6{{ .IPv6 }}\r\n",
-      "ipv4": "🌐 IPv4{{ .IPv4 }}\r\n",
-      "ip": "🌐 IP{{ .IP }}\r\n",
-      "ips": "🔢 IPアドレス:\r\n{{ .IPs }}\r\n",
+      "ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
+      "ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
+      "ip": "🌐 IP: {{ .IP }}\r\n",
+      "ips": "🔢 IP:\r\n{{ .IPs }}\r\n",
       "serverUpTime": "⏳ サーバー稼働時間:{{ .UpTime }} {{ .Unit }}\r\n",
       "serverLoad": "📈 サーバー負荷:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
-      "serverMemory": "📋 サーバーメモリ:{{ .Current }}/{{ .Total }}\r\n",
-      "tcpCount": "🔹 TCP接続数:{{ .Count }}\r\n",
-      "udpCount": "🔸 UDP接続数:{{ .Count }}\r\n",
+      "serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
+      "tcpCount": "🔹 TCP: {{ .Count }}\r\n",
+      "udpCount": "🔸 UDP: {{ .Count }}\r\n",
       "traffic": "🚦 トラフィック:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
-      "xrayStatus": "ℹ️ Xrayステータス:{{ .State }}\r\n",
+      "xrayStatus": "ℹ️ ステータス: {{ .State }}\r\n",
       "username": "👤 ユーザー名:{{ .Username }}\r\n",
       "reason": "❗️ 理由:{{ .Reason }}\r\n",
       "time": "⏰ 時間:{{ .Time }}\r\n",
-      "inbound": "📍 インバウンド{{ .Remark }}\r\n",
-      "port": "🔌 ポート{{ .Port }}\r\n",
+      "inbound": "📍 インバウンド: {{ .Remark }}\r\n",
+      "port": "🔌 ポート: {{ .Port }}\r\n",
       "expire": "📅 有効期限:{{ .Time }}\r\n",
       "expireIn": "📅 残り時間:{{ .Time }}\r\n",
       "active": "💡 有効:{{ .Enable }}\r\n",
       "enabled": "🚨 有効化済み:{{ .Enable }}\r\n",
       "online": "🌐 接続ステータス:{{ .Status }}\r\n",
       "lastOnline": "🔙 最終オンライン: {{ .Time }}\r\n",
-      "email": "📧 メール{{ .Email }}\r\n",
-      "upload": "🔼 アップロード↑{{ .Upload }}\r\n",
-      "download": "🔽 ダウンロード↓{{ .Download }}\r\n",
-      "total": "📊 合計{{ .UpDown }} / {{ .Total }}\r\n",
+      "email": "📧 メール: {{ .Email }}\r\n",
+      "upload": "🔼 アップロード: ↑{{ .Upload }}\r\n",
+      "download": "🔽 ダウンロード: ↓{{ .Download }}\r\n",
+      "total": "📊 合計: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
       "TGUser": "👤 Telegramユーザー:{{ .TelegramID }}\r\n",
       "exhaustedMsg": "🚨 消耗済みの {{ .Type }}:\r\n",
       "exhaustedCount": "🚨 消耗済みの {{ .Type }} 数量:\r\n",
@@ -1089,9 +1512,9 @@
       "use_default": "🏷️ デフォルトを使用",
       "change_id": "⚙️🔑 ID",
       "change_password": "⚙️🔑 パスワード",
-      "change_email": "⚙️📧 メールアドレス",
+      "change_email": "⚙️📧 メール",
       "change_comment": "⚙️💬 コメント",
-      "change_flow": "⚙️🚦 フロー",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "すべてのトラフィックをリセット",
       "SortedTrafficUsageReport": "ソートされたトラフィック使用レポート"
     },
@@ -1119,4 +1542,4 @@
       "chooseInbound": "インバウンドを選択"
     }
   }
-}
+}

+ 452 - 29
web/translation/pt-BR.json

@@ -8,15 +8,22 @@
   "save": "Salvar",
   "logout": "Sair",
   "create": "Criar",
+  "add": "Adicionar",
+  "remove": "Remover",
   "update": "Atualizar",
   "copy": "Copiar",
   "copied": "Copiado",
+  "more": "mais",
   "download": "Baixar",
   "remark": "Observação",
   "enable": "Ativado",
   "protocol": "Protocolo",
   "search": "Pesquisar",
   "filter": "Filtrar",
+  "all": "Todos",
+  "from": "De",
+  "to": "Até",
+  "done": "Concluído",
   "loading": "Carregando...",
   "refresh": "Atualizar",
   "clear": "Limpar",
@@ -27,7 +34,7 @@
   "check": "Verificar",
   "indefinite": "Indeterminado",
   "unlimited": "Ilimitado",
-  "none": "Nada",
+  "none": "Nenhum",
   "qrCode": "Código QR",
   "info": "Mais Informações",
   "edit": "Editar",
@@ -39,7 +46,7 @@
   "encryption": "Criptografia",
   "useIPv4ForHost": "Usar IPv4 para o host",
   "transmission": "Transmissão",
-  "host": "Servidor",
+  "host": "Host",
   "path": "Caminho",
   "camouflage": "Ofuscação",
   "status": "Status",
@@ -95,11 +102,12 @@
     "dark": "Escuro",
     "ultraDark": "Ultra Escuro",
     "dashboard": "Visão Geral",
-    "inbounds": "Inbounds",
+    "inbounds": "Entradas",
     "clients": "Clientes",
+    "groups": "Grupos",
     "nodes": "Nós",
-    "settings": "Panel Settings",
-    "xray": "Xray Configs",
+    "settings": "Configurações do Painel",
+    "xray": "Configurações Xray",
     "apiDocs": "Documentação da API",
     "logout": "Sair",
     "link": "Gerenciar",
@@ -241,7 +249,7 @@
       "getConfigError": "Ocorreu um erro ao recuperar o arquivo de configuração"
     },
     "inbounds": {
-      "title": "Inbounds",
+      "title": "Entradas",
       "totalDownUp": "Total Enviado/Recebido",
       "totalUsage": "Uso Total",
       "inboundCount": "Total de Inbounds",
@@ -270,14 +278,14 @@
       },
       "protocol": "Protocolo",
       "port": "Porta",
-      "portMap": "Porta Mapeada",
+      "portMap": "Mapeamento de portas",
       "traffic": "Tráfego",
       "details": "Detalhes",
       "transportConfig": "Transporte",
       "expireDate": "Duração",
       "createdAt": "Criado",
       "updatedAt": "Atualizado",
-      "resetTraffic": "Redefinir Tráfego",
+      "resetTraffic": "Redefinir tráfego",
       "addInbound": "Adicionar Inbound",
       "generalActions": "Ações Gerais",
       "modifyInbound": "Modificar Inbound",
@@ -292,11 +300,31 @@
       "delAllClients": "Excluir todos os clientes",
       "delAllClientsConfirmTitle": "Excluir todos os {count} clientes de \"{remark}\"?",
       "delAllClientsConfirmContent": "Remove todos os clientes deste inbound e descarta seus registros de tráfego. O inbound em si é mantido. Esta ação não pode ser desfeita.",
+      "attachClients": "Associar clientes a…",
+      "addClientsToGroup": "Adicionar clientes ao grupo…",
+      "attachClientsTitle": "Associar clientes de «{remark}»",
+      "attachClientsDesc": "Associa os mesmos {count} cliente(s) (mesmo UUID/senha e tráfego compartilhado) à(s) entrada(s) selecionada(s). Permanecem nesta entrada também.",
+      "attachClientsTargets": "Entradas de destino",
+      "attachClientsNoTargets": "Não há outras entradas compatíveis disponíveis para associação.",
+      "attachClientsResult": "Associados {attached}, ignorados {skipped}.",
+      "attachClientsResultMixed": "Associados {attached}, ignorados {skipped}, erros {errors}.",
+      "attachClientsSelectLabel": "Clientes para associar",
+      "attachClientsSearchPlaceholder": "Buscar email ou comentário",
+      "attachClientsStatusDisabled": "Desabilitado",
+      "attachClientsSelectedCount": "{selected} de {total} selecionado(s)",
+      "detachClients": "Desassociar clientes",
+      "detachClientsTitle": "Desassociar clientes de «{remark}»",
+      "detachClientsDesc": "Remove o(s) cliente(s) selecionado(s) apenas desta entrada. Os registros são mantidos (use Delete para remover completamente). A origem tem {count} cliente(s) no total.",
+      "detachClientsResult": "Desassociados {detached}, ignorados {skipped}.",
+      "detachClientsResultMixed": "Desassociados {detached}, ignorados {skipped}, erros {errors}.",
+      "detachClientsSelectLabel": "Clientes para desassociar",
       "exportLinksTitle": "Exportar links do inbound",
       "exportSubsTitle": "Exportar links de assinatura",
       "exportAllLinksTitle": "Exportar todos os links de inbound",
       "exportAllSubsTitle": "Exportar todos os links de assinatura",
-      "inboundJsonTitle": "JSON do inbound",
+      "exportAllLinksFileName": "Todas-as-entradas",
+      "exportAllSubsFileName": "Todas-as-entradas-Subs",
+      "inboundJsonTitle": "JSON da entrada",
       "deleteClient": "Excluir Cliente",
       "deleteClientContent": "Tem certeza de que deseja excluir o cliente?",
       "resetTrafficContent": "Tem certeza de que deseja redefinir o tráfego?",
@@ -341,9 +369,10 @@
       "IPLimitlogDesc": "O histórico de IPs. (para ativar o inbound após a desativação, limpe o log)",
       "IPLimitlogclear": "Limpar o Log",
       "setDefaultCert": "Definir Certificado pelo Painel",
-      "streamTab": "Stream",
+      "setDefaultCertEmpty": "Nenhum certificado configurado para o painel. Configure um em Configurações primeiro.",
+      "streamTab": "Transmissão",
       "securityTab": "Segurança",
-      "sniffingTab": "Sniffing",
+      "sniffingTab": "Inspeção",
       "sniffingMetadataOnly": "Apenas metadados",
       "sniffingRouteOnly": "Apenas roteamento",
       "sniffingIpsExcluded": "IPs excluídos",
@@ -369,7 +398,6 @@
       },
       "telegramDesc": "Por favor, forneça o ID do Chat do Telegram. (use o comando '/id' no bot) ou ({'@'}userinfobot)",
       "subscriptionDesc": "Para encontrar seu URL de assinatura, navegue até 'Detalhes'. Além disso, você pode usar o mesmo nome para vários clientes.",
-      "info": "Informações",
       "same": "Igual",
       "inboundData": "Dados do Inbound",
       "exportInbound": "Exportar Inbound",
@@ -406,6 +434,143 @@
         "getNewmldsa65Error": "Erro ao obter o certificado mldsa65.",
         "getNewVlessEncError": "Erro ao obter o certificado VlessEnc."
       },
+      "form": {
+        "moveUp": "Mover para cima",
+        "moveDown": "Mover para baixo",
+        "addAll": "Adicionar todos",
+        "addAllFallbackTooltip": "Adiciona uma linha de fallback para cada entrada elegível ainda não conectada",
+        "peers": "Peers",
+        "addPeer": "Adicionar peer",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "Apenas Windows. CIDRs são adicionados à tabela de roteamento do sistema automaticamente para que o tráfego correspondente passe pelo TUN.",
+        "autoOutboundsInterface": "Interface de saída automática",
+        "autoOutboundsInterfaceTooltip": "Interface física para tráfego de saída. Use 'auto' para detecção; auto-habilitado quando Auto system routes está ativo.",
+        "rewriteAddress": "Reescrever endereço",
+        "rewritePort": "Reescrever porta",
+        "allowedNetwork": "Rede permitida",
+        "followRedirect": "Seguir redirect",
+        "accounts": "Contas",
+        "allowTransparent": "Permitir transparente",
+        "encryptionMethod": "Método de criptografia",
+        "visionTestseed": "Vision testseed",
+        "version": "Versão",
+        "udpIdleTimeout": "UDP idle timeout (s)",
+        "masquerade": "Masquerade",
+        "type": "Tipo",
+        "upstreamUrl": "URL Upstream",
+        "rewriteHost": "Reescrever Host",
+        "skipTlsVerify": "Pular verificação TLS",
+        "directory": "Diretório",
+        "statusCode": "Código de status",
+        "body": "Body",
+        "headers": "Cabeçalhos",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "Versão da requisição",
+        "requestMethod": "Método da requisição",
+        "requestPath": "Caminho da requisição",
+        "requestHeaders": "Cabeçalhos de requisição",
+        "responseVersion": "Versão da resposta",
+        "responseStatus": "Status da resposta",
+        "responseReason": "Motivo da resposta",
+        "responseHeaders": "Cabeçalhos de resposta",
+        "heartbeatPeriod": "Período de heartbeat",
+        "serviceName": "Nome do serviço",
+        "authority": "Authority",
+        "multiMode": "Multi Mode",
+        "maxBufferedUpload": "Máx. upload em buffer",
+        "maxUploadSize": "Tamanho máx. de upload (Byte)",
+        "streamUpServer": "Stream-Up Server",
+        "serverMaxHeaderBytes": "Máx. bytes cabeçalho servidor",
+        "paddingBytes": "Bytes de Padding",
+        "uplinkHttpMethod": "Método HTTP Uplink",
+        "paddingObfsMode": "Modo obfs de Padding",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "Posição de Padding",
+        "paddingMethod": "Método de Padding",
+        "sessionPlacement": "Session Placement",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence Placement",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink Data Placement",
+        "uplinkDataKey": "Uplink Data Key",
+        "noSseHeader": "Sem cabeçalho SSE",
+        "ttiMs": "TTI (ms)",
+        "uplinkMbps": "Uplink (MB/s)",
+        "downlinkMbps": "Downlink (MB/s)",
+        "cwndMultiplier": "Multiplicador CWND",
+        "maxSendingWindow": "Máx. janela de envio",
+        "externalProxy": "Proxy externo",
+        "sniPlaceholder": "SNI (padrão = host)",
+        "fingerprint": "Fingerprint",
+        "defaultOption": "Padrão",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive Interval",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "Apenas V6",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "X-Forwarded-For confiável",
+        "addressPortStrategy": "Estratégia endereço+porta",
+        "tryDelayMs": "Atraso de tentativa (ms)",
+        "prioritizeIPv6": "Priorizar IPv6",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "Máx. tentativas simultâneas",
+        "customSockopt": "Sockopt personalizado",
+        "addCustomOption": "Adicionar opção personalizada",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "Auto",
+        "minMaxVersion": "Versão mín/máx",
+        "rejectUnknownSni": "Rejeitar SNI desconhecido",
+        "disableSystemRoot": "Desabilitar System Root",
+        "sessionResumption": "Retomada de sessão",
+        "oneTimeLoading": "Carregamento único",
+        "usageOption": "Opção de uso",
+        "buildChain": "Construir cadeia",
+        "echKey": "ECH key",
+        "echConfig": "Config ECH",
+        "pinnedPeerCertSha256": "SHA-256 do cert. do par fixado",
+        "pinnedPeerCertSha256Tip": "Hashes SHA-256 codificados em Base64 do certificado do par. Apenas no painel — não é gravado na config xray do servidor, mas é incluído nos links de compartilhamento para que clientes possam fixar o certificado.",
+        "pinnedPeerCertSha256Placeholder": "hash(es) base64, separados por vírgula",
+        "generateRandomPin": "Gerar hash aleatório",
+        "getNewEchCert": "Obter novo certificado ECH",
+        "show": "Mostrar",
+        "xver": "Xver",
+        "target": "Alvo",
+        "maxTimeDiff": "Máx. diferença de tempo (ms)",
+        "minClientVer": "Mín. versão cliente",
+        "maxClientVer": "Máx. versão cliente",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "Obter novo certificado",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "Obter novo Seed"
+      },
+      "info": {
+        "mode": "Modo",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "Nome da interface",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "Interface de saída",
+        "autoSystemRoutes": "Rotas do sistema automáticas",
+        "followRedirect": "FollowRedirect",
+        "auth": "Auth",
+        "noKernelTun": "TUN sem kernel",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "Config Peer {n}"
+      },
       "stream": {
         "general": {
           "request": "Requisição",
@@ -456,6 +621,20 @@
       "days": "Dia(s)",
       "renew": "Renovação automática",
       "renewDesc": "Renovação automática após a expiração. (0 = desativar) (unidade: dia)",
+      "searchPlaceholder": "Buscar email, comentário, sub ID, UUID, senha, auth…",
+      "filterTitle": "Filtrar clientes",
+      "clearAllFilters": "Limpar tudo",
+      "sortOldest": "Mais antigos primeiro",
+      "sortNewest": "Mais novos primeiro",
+      "sortRecentlyUpdated": "Atualizados recentemente",
+      "sortRecentlyOnline": "Online recentemente",
+      "sortEmailAZ": "Email A→Z",
+      "sortEmailZA": "Email Z→A",
+      "sortMostTraffic": "Mais tráfego",
+      "sortHighestRemaining": "Maior restante",
+      "sortExpiringSoonest": "Expira em breve",
+      "has": "Tem",
+      "hasNot": "Não tem",
       "title": "Clientes",
       "actions": "Ações",
       "totalGB": "Total enviado/recebido (GB)",
@@ -465,7 +644,10 @@
       "password": "Senha",
       "subId": "ID da assinatura",
       "online": "Online",
-      "email": "E-mail",
+      "email": "Email",
+      "group": "Grupo",
+      "groupDesc": "Rótulo lógico para agrupar clientes relacionados (ex.: equipe, cliente, região). Filtrável pela barra de ferramentas.",
+      "groupPlaceholder": "ex.: customer-a",
       "comment": "Comentário",
       "traffic": "Tráfego",
       "offline": "Offline",
@@ -489,11 +671,45 @@
       "resetAllTraffics": "Redefinir o tráfego de todos os clientes",
       "resetAllTrafficsTitle": "Redefinir o tráfego de todos os clientes?",
       "resetAllTrafficsContent": "Os contadores de envio/recebimento de cada cliente vão a zero. Cota e expiração não são afetadas. Não é possível desfazer.",
-      "empty": "Ainda não há clientes — adicione um para começar.",
       "deleteConfirmTitle": "Excluir o cliente {email}?",
       "deleteConfirmContent": "Isto remove o cliente de cada inbound associado e descarta o registro de tráfego. Não é possível desfazer.",
       "deleteSelected": "Excluir ({count})",
       "adjustSelected": "Ajustar ({count})",
+      "subLinksSelected": "Links sub ({count})",
+      "addToGroupTitle": "Adicionar {count} cliente(s) a um grupo",
+      "addToGroupTooltip": "Escolha um grupo existente ou digite um novo nome. Use Ungroup para remover clientes do grupo atual.",
+      "addToGroupPlaceholder": "Nome do grupo",
+      "addToGroupSuccessToast": "{count} cliente(s) adicionado(s) a {group}",
+      "ungroupSuccessToast": "Grupo limpo de {count} cliente(s)",
+      "ungroup": "Desagrupar",
+      "ungroupConfirmTitle": "Remover {count} cliente(s) do grupo?",
+      "ungroupConfirmContent": "Limpa o rótulo de grupo de cada cliente selecionado. Os clientes em si são mantidos (use Delete para remover completamente).",
+      "addToGroup": "Adicionar ao grupo",
+      "attach": "Associar",
+      "adjust": "Ajustar",
+      "subLinks": "Links de assinatura",
+      "selectedCount": "{count} selecionado(s)",
+      "attachSelected": "Associar ({count})",
+      "attachToInboundsTitle": "Associar {count} cliente(s) a entrada(s)",
+      "attachToInboundsDesc": "Associa os {count} cliente(s) selecionados (mesmo UUID/senha e tráfego compartilhado) às entradas escolhidas. Mantêm suas associações existentes.",
+      "attachToInboundsTargets": "Entradas de destino",
+      "attachToInboundsNoTargets": "Não há entradas multiusuário disponíveis para associação.",
+      "detachSelected": "Desassociar ({count})",
+      "detach": "Desassociar",
+      "detachFromInboundsTitle": "Desassociar {count} cliente(s) de entrada(s)",
+      "detachFromInboundsDesc": "Remove os {count} cliente(s) selecionados das entradas escolhidas. Pares onde o cliente não estava associado são ignorados silenciosamente. Os registros dos clientes são mantidos (use Delete para remover completamente).",
+      "detachFromInboundsTargets": "Entradas para desassociar",
+      "detachFromInboundsNoTargets": "Não há entradas multiusuário disponíveis.",
+      "detachFromInboundsResult": "Desassociados {detached}, ignorados {skipped}.",
+      "detachFromInboundsResultMixed": "Desassociados {detached}, ignorados {skipped}, erros {errors}.",
+      "subLinksTitle": "Links sub ({count})",
+      "subLinkColumn": "URL da assinatura",
+      "subJsonLinkColumn": "URL JSON da assinatura",
+      "subLinksCopyAll": "Copiar tudo",
+      "subLinksCopiedAll": "Copiados {count} link(s)",
+      "subLinksEmpty": "Nenhum dos clientes selecionados tem ID de assinatura.",
+      "subLinksDisabled": "O serviço de assinatura está desabilitado.",
+      "subLinksDisabledHint": "Habilite a assinatura em Configurações do Painel → Assinatura para gerar links.",
       "bulkDeleteConfirmTitle": "Excluir {count} clientes?",
       "bulkDeleteConfirmContent": "Cada cliente selecionado é removido dos inbounds associados e o registro de tráfego é descartado. Não é possível desfazer.",
       "bulkAdjustTitle": "Ajustar {count} clientes",
@@ -505,10 +721,11 @@
       "delDepletedConfirmTitle": "Excluir clientes esgotados?",
       "delDepletedConfirmContent": "Remove todos os clientes cuja cota de tráfego foi esgotada ou cuja expiração já passou. Não é possível desfazer.",
       "auth": "Auth",
-      "hysteriaAuth": "Auth do Hysteria",
+      "hysteriaAuth": "Hysteria Auth",
       "uuid": "UUID",
       "flow": "Flow",
-      "reverseTag": "Reverse tag",
+      "vmessSecurity": "Segurança VMess",
+      "reverseTag": "Tag reversa",
       "reverseTagPlaceholder": "Reverse tag opcional",
       "telegramId": "ID de usuário do Telegram",
       "telegramIdPlaceholder": "ID numérico de usuário do Telegram (0 = nenhum)",
@@ -528,6 +745,44 @@
         "delDepleted": "{count} clientes esgotados excluídos"
       }
     },
+    "groups": {
+      "title": "Grupos",
+      "name": "Nome",
+      "clientCount": "Clientes no grupo",
+      "totalGroups": "Total de grupos",
+      "totalGroupedClients": "Clientes com grupo",
+      "emptyGroups": "Grupos vazios",
+      "addGroup": "Adicionar grupo",
+      "createSuccess": "Grupo «{name}» criado.",
+      "rename": "Renomear",
+      "renameTitle": "Renomear {name}",
+      "renameCollision": "Já existe um grupo chamado «{name}».",
+      "renameSuccess": "Grupo renomeado em {count} cliente(s).",
+      "deleteConfirmTitle": "Excluir o grupo {name}?",
+      "deleteConfirmContent": "Isso remove o grupo e limpa seu rótulo de {count} cliente(s). Os clientes em si não são excluídos.",
+      "deleteSuccess": "Grupo limpo de {count} cliente(s).",
+      "resetTraffic": "Redefinir tráfego",
+      "resetConfirmTitle": "Redefinir tráfego do grupo {name}?",
+      "resetConfirmContent": "Isso zera up/down para todos os {count} cliente(s) deste grupo.",
+      "resetSuccess": "Tráfego redefinido para {count} cliente(s).",
+      "adjustSuccess": "Ajustados {count} cliente(s) em {name}.",
+      "emptyForAction": "Este grupo ainda não tem clientes.",
+      "deleteGroupOnly": "Excluir grupo (manter clientes)",
+      "deleteClients": "Excluir clientes do grupo",
+      "deleteClientsConfirmTitle": "Excluir todos os clientes em {name}?",
+      "deleteClientsConfirmContent": "Isso remove permanentemente {count} cliente(s) junto com seus registros de tráfego. O rótulo de grupo também é limpo. Isso não pode ser desfeito.",
+      "deleteClientsSuccess": "Excluídos {count} cliente(s).",
+      "deleteClientsMixed": "{ok} excluídos, {failed} ignorados",
+      "addToGroup": "Adicionar clientes…",
+      "addToGroupTitle": "Adicionar clientes ao grupo «{name}»",
+      "addToGroupDesc": "Selecione clientes para adicionar a este grupo. Mantêm suas associações de entrada atuais; apenas o rótulo de grupo muda. Clientes já neste grupo não são listados.",
+      "addToGroupEmpty": "Não há outros clientes disponíveis para adicionar.",
+      "addToGroupResult": "Adicionados {count} cliente(s) a {name}.",
+      "removeFromGroup": "Remover clientes…",
+      "removeFromGroupTitle": "Remover clientes do grupo «{name}»",
+      "removeFromGroupDesc": "Selecione membros para remover deste grupo. Os clientes em si são mantidos (use «Excluir clientes do grupo» para removê-los por completo).",
+      "removeFromGroupResult": "Removidos {count} cliente(s) de {name}."
+    },
     "nodes": {
       "title": "Nós",
       "addNode": "Adicionar nó",
@@ -544,7 +799,7 @@
       "address": "Endereço",
       "port": "Porta",
       "basePath": "Caminho base",
-      "apiToken": "Token da API",
+      "apiToken": "Token API",
       "apiTokenPlaceholder": "Token da página de Configurações do painel remoto",
       "apiTokenHint": "O painel remoto exibe o token da API em Configurações → Token da API.",
       "regenerate": "Regenerar token",
@@ -590,7 +845,7 @@
       "title": "Configurações do Painel",
       "save": "Salvar",
       "infoDesc": "Toda alteração feita aqui precisa ser salva. Reinicie o painel para aplicar as alterações.",
-      "restartPanel": "Reiniciar Painel",
+      "restartPanel": "Reiniciar painel",
       "restartPanelDesc": "Tem certeza de que deseja reiniciar o painel? Se não conseguir acessar o painel após reiniciar, consulte os logs do painel no servidor.",
       "restartPanelSuccess": "O painel foi reiniciado com sucesso",
       "actions": "Ações",
@@ -619,6 +874,8 @@
       "panelUrlPathDesc": "O caminho URI para o painel web. (começa com ‘/‘ e termina com ‘/‘)",
       "pageSize": "Tamanho da Paginação",
       "pageSizeDesc": "Definir o tamanho da página para a tabela de entradas. (0 = desativado)",
+      "panelProxy": "Proxy de rede do painel",
+      "panelProxyDesc": "Encaminha as requisições de saída do próprio painel (atualizações de geo, verificações de versão do Xray/painel, Telegram) por este proxy para contornar a filtragem de GitHub/Telegram no servidor. Aceita socks5:// ou http(s)://, ex. uma entrada SOCKS local do Xray. Deixe vazio para conexão direta.",
       "remarkModel": "Modelo de Observação & Caractere de Separação",
       "datepicker": "Tipo de Calendário",
       "datepickerPlaceholder": "Selecionar data",
@@ -634,7 +891,7 @@
       "telegramTokenDesc": "O token do bot do Telegram obtido de '{'@'}BotFather'.",
       "telegramProxy": "Proxy SOCKS",
       "telegramProxyDesc": "Ativa o proxy SOCKS5 para conectar ao Telegram. (ajuste as configurações conforme o guia)",
-      "telegramAPIServer": "API Server do Telegram",
+      "telegramAPIServer": "Servidor API do Telegram",
       "telegramAPIServerDesc": "O servidor API do Telegram a ser usado. Deixe em branco para usar o servidor padrão.",
       "telegramChatId": "ID de Chat do Administrador",
       "telegramChatIdDesc": "O(s) ID(s) de Chat do Administrador no Telegram. (separado por vírgulas)(obtenha aqui {'@'}userinfobot) ou (use o comando '/id' no bot)",
@@ -658,6 +915,8 @@
       "subEnable": "Ativar Serviço de Assinatura",
       "subEnableDesc": "Ativa o serviço de assinatura.",
       "subJsonEnable": "Ativar/Desativar o endpoint de assinatura JSON de forma independente.",
+      "subJsonEnableTitle": "Assinatura JSON",
+      "subClashEnableTitle": "Assinatura Clash / Mihomo",
       "subTitle": "Título da Assinatura",
       "subTitleDesc": "Título exibido no cliente VPN",
       "subSupportUrl": "URL de Suporte",
@@ -693,7 +952,7 @@
       "subURI": "URI de Proxy Reverso",
       "subURIDesc": "O caminho URI da URL de assinatura para uso por trás de proxies.",
       "externalTrafficInformEnable": "Informações de tráfego externo",
-      "externalTrafficInformEnableDesc": "Informar a API externa sobre cada atualização de tráfego.",
+      "externalTrafficInformEnableDesc": "Informar API externa a cada atualização de tráfego.",
       "externalTrafficInformURI": "URI de informação de tráfego externo",
       "externalTrafficInformURIDesc": "As atualizações de tráfego são enviadas para este URI.",
       "restartXrayOnClientDisable": "Reiniciar Xray Após Desativação Automática",
@@ -703,6 +962,54 @@
       "fragmentSett": "Configurações de Fragmentação",
       "noisesDesc": "Ativar Noises.",
       "noisesSett": "Configurações de Noises",
+      "trustedProxyCidrs": "CIDRs de proxy confiável",
+      "trustedProxyCidrsDesc": "IPs/CIDRs separados por vírgula que podem definir os cabeçalhos host, proto e IP do cliente encaminhados.",
+      "ldap": {
+        "enable": "Habilitar sincronização LDAP",
+        "host": "Host LDAP",
+        "port": "Porta LDAP",
+        "useTls": "Usar TLS (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "Configurada; deixe em branco para manter a senha atual.",
+        "passwordUnconfigured": "Não configurada.",
+        "passwordPlaceholder": "Configurada — digite um novo valor para substituir",
+        "baseDn": "Base DN",
+        "userFilter": "Filtro de usuário",
+        "userAttr": "Atributo de usuário (username/email)",
+        "vlessField": "Atributo flag VLESS",
+        "flagField": "Atributo flag genérico (opcional)",
+        "flagFieldDesc": "Se definido, sobrescreve o flag VLESS — ex. shadowInactive.",
+        "truthyValues": "Valores truthy",
+        "truthyValuesDesc": "Separados por vírgula; padrão: true,1,yes,on",
+        "invertFlag": "Inverter flag",
+        "invertFlagDesc": "Habilite quando o atributo significar «desabilitado» (ex. shadowInactive).",
+        "syncSchedule": "Agendamento da sincronização",
+        "syncScheduleDesc": "String tipo cron, ex. @every 1m",
+        "inboundTags": "Tags de entradas",
+        "inboundTagsDesc": "Entradas nas quais a sincronização LDAP pode auto-criar ou auto-excluir clientes.",
+        "noInbounds": "Nenhuma entrada encontrada. Crie uma em Entradas primeiro.",
+        "autoCreate": "Criar clientes automaticamente",
+        "autoDelete": "Excluir clientes automaticamente",
+        "defaultTotalGb": "Total padrão (GB)",
+        "defaultExpiryDays": "Expiração padrão (dias)",
+        "defaultIpLimit": "Limite de IP padrão"
+      },
+      "subFormats": {
+        "packets": "Pacotes",
+        "length": "Comprimento",
+        "interval": "Intervalo",
+        "maxSplit": "Máx. divisão",
+        "noises": "Ruídos",
+        "noiseItem": "Ruído №{n}",
+        "type": "Tipo",
+        "packet": "Pacote",
+        "delayMs": "Atraso (ms)",
+        "applyTo": "Aplicar a",
+        "addNoise": "+ Ruído",
+        "concurrency": "Concorrência",
+        "xudpConcurrency": "Concorrência xudp",
+        "xudpUdp443": "xudp UDP 443"
+      },
       "mux": "Mux",
       "muxDesc": "Transmitir múltiplos fluxos de dados independentes dentro de um fluxo de dados estabelecido.",
       "muxSett": "Configurações de Mux",
@@ -758,6 +1065,9 @@
       "save": "Salvar",
       "restart": "Reiniciar Xray",
       "restartSuccess": "Xray foi reiniciado com sucesso",
+      "restartOutputTitle": "Saída do reinício do Xray",
+      "restartConfirmTitle": "Reiniciar xray?",
+      "restartConfirmContent": "Recarrega o serviço xray com a configuração salva.",
       "stopSuccess": "Xray foi interrompido com sucesso",
       "restartError": "Ocorreu um erro ao reiniciar o Xray.",
       "stopError": "Ocorreu um erro ao parar o Xray.",
@@ -790,14 +1100,16 @@
       "outboundTestUrl": "URL de teste de outbound",
       "outboundTestUrlDesc": "URL usada ao testar conectividade do outbound",
       "Torrent": "Bloquear Protocolo BitTorrent",
-      "Inbounds": "Inbounds",
+      "Inbounds": "Entradas",
       "InboundsDesc": "Aceitar clientes específicos.",
-      "Outbounds": "Outbounds",
+      "Outbounds": "Saídas",
       "Balancers": "Balanceadores",
+      "balancerTagRequired": "A tag é obrigatória",
+      "balancerSelectorRequired": "Selecione pelo menos uma saída",
       "OutboundsDesc": "Definir o caminho de saída do tráfego.",
       "Routings": "Regras de Roteamento",
       "RoutingsDesc": "A prioridade de cada regra é importante!",
-      "completeTemplate": "Todos",
+      "completeTemplate": "Tudo",
       "logLevel": "Nível de Log",
       "logLevelDesc": "O nível de log para erros, indicando a informação que precisa ser registrada.",
       "accessLog": "Log de Acesso",
@@ -832,6 +1144,73 @@
         "edit": "Editar Regra",
         "useComma": "Itens separados por vírgula"
       },
+      "routing": {
+        "dragToReorder": "Arraste para reordenar"
+      },
+      "ruleForm": {
+        "sourceIps": "IPs de origem",
+        "sourcePort": "Porta de origem",
+        "vlessRoute": "Rota VLESS",
+        "attributes": "Atributos",
+        "value": "Valor",
+        "user": "Usuário",
+        "inboundTags": "Tags de entradas",
+        "outboundTag": "Tag de saída",
+        "balancerTag": "Tag de balanceador",
+        "balancerTagTooltip": "Encaminha tráfego por um dos balanceadores configurados"
+      },
+      "outboundForm": {
+        "tagDuplicate": "Tag já usada por outra saída",
+        "tagRequired": "A tag é obrigatória",
+        "tagPlaceholder": "tag-única",
+        "localIpPlaceholder": "IP local",
+        "addressRequired": "Endereço é obrigatório",
+        "portRequired": "Porta é obrigatória",
+        "optional": "opcional",
+        "udpOverTcp": "UDP sobre TCP",
+        "uotVersion": "Versão UoT",
+        "inboundTag": "Tag de entrada",
+        "inboundTagPlaceholder": "tag de entrada usada em regras de roteamento",
+        "responseType": "Tipo de resposta",
+        "rewriteNetwork": "Reescrever rede",
+        "unchanged": "(inalterado)",
+        "unchangedAddress": "(inalterado) ex. 1.1.1.1",
+        "rules": "Regras",
+        "ruleN": "Regra {n}",
+        "action": "Ação",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "Regras finais",
+        "overrideXrayPrivateIp": "Sobrescrever o bloqueio de IP privado padrão do Xray",
+        "blockDelay": "Atraso do bloqueio (ms)",
+        "reverseSniffing": "Sniffing reverso",
+        "workers": "Workers",
+        "reserved": "Reservado",
+        "minUploadInterval": "Intervalo mín. de upload (ms)",
+        "maxUploadSizeBytes": "Tamanho máx. de upload (bytes)",
+        "uplinkChunkSize": "Tamanho do chunk Uplink",
+        "noGrpcHeader": "Sem cabeçalho gRPC",
+        "maxConcurrency": "Máx. concorrência",
+        "maxConnections": "Máx. conexões",
+        "maxReuseTimes": "Máx. reutilizações",
+        "maxRequestTimes": "Máx. requisições",
+        "maxReusableSecs": "Máx. segundos reutilizáveis",
+        "keepAlivePeriod": "Período keep alive",
+        "authPassword": "Senha de auth",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "nome do servidor",
+        "verifyPeerName": "Verificar nome do peer",
+        "pinnedSha256": "SHA256 pinned",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "Intervalo keep alive",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "Interface",
+        "ipv6Only": "Apenas IPv6",
+        "acceptProxyProtocol": "Aceitar proxy protocol",
+        "tcpUserTimeoutMs": "TCP user timeout (ms)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
+      },
       "outbound": {
         "addOutbound": "Adicionar Saída",
         "addReverse": "Adicionar Reverso",
@@ -846,7 +1225,7 @@
         "reverse": "Reverso",
         "domain": "Domínio",
         "type": "Tipo",
-        "bridge": "Ponte",
+        "bridge": "Bridge",
         "portal": "Portal",
         "link": "Link",
         "intercon": "Interconexão",
@@ -860,6 +1239,8 @@
         "testSuccess": "Teste bem-sucedido",
         "testFailed": "Teste falhou",
         "testError": "Falha ao testar saída",
+        "testModeTooltip": "TCP: sondagem rápida apenas de dial. HTTP: requisição completa pelo xray.",
+        "testAll": "Testar todos",
         "nordvpn": "NordVPN",
         "accessToken": "Token de Acesso",
         "country": "País",
@@ -876,6 +1257,16 @@
         "balancerSelectors": "Seletores",
         "tag": "Tag",
         "tagDesc": "Tag Única",
+        "tagDuplicate": "Tag já usada por outro balanceador",
+        "tagPlaceholder": "tag única do balanceador",
+        "selector": "Seletor",
+        "fallback": "Fallback",
+        "expected": "Esperado",
+        "expectedPlaceholder": "número ótimo de nós",
+        "maxRtt": "Máx. RTT",
+        "tolerance": "Tolerância",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "Não é possível usar balancerTag e outboundTag ao mesmo tempo. Se usados simultaneamente, apenas outboundTag funcionará."
       },
       "wireguard": {
@@ -892,6 +1283,38 @@
         "userLevel": "Nível do Usuário",
         "userLevelDesc": "Todas as conexões feitas através deste inbound usarão este nível de usuário. O padrão é 0"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "Chave privada",
+        "noServers": "Nenhum servidor encontrado para o país selecionado",
+        "noPublicKey": "O servidor selecionado não anuncia uma chave pública NordLynx.",
+        "outboundAdded": "Saída NordVPN adicionada",
+        "outboundUpdated": "Saída NordVPN atualizada"
+      },
+      "warp": {
+        "licenseError": "Falha ao definir licença WARP.",
+        "fetchFirst": "Obtenha primeiro a configuração WARP.",
+        "createAccount": "Criar conta WARP",
+        "accessToken": "Access token",
+        "deviceId": "ID do dispositivo",
+        "licenseKey": "Chave de licença",
+        "privateKey": "Chave privada",
+        "deleteAccount": "Excluir conta",
+        "settings": "Configurações",
+        "licenseKeyLabel": "Chave de licença WARP / WARP+",
+        "key": "Chave",
+        "keyPlaceholder": "chave WARP+ de 26 caracteres",
+        "accountInfo": "Informação da conta",
+        "deviceName": "Nome do dispositivo",
+        "deviceModel": "Modelo do dispositivo",
+        "deviceEnabled": "Dispositivo habilitado",
+        "accountType": "Tipo de conta",
+        "role": "Função",
+        "warpPlusData": "Dados WARP+",
+        "quota": "Quota",
+        "usage": "Uso",
+        "addOutbound": "Adicionar saída"
+      },
       "dns": {
         "enable": "Ativar DNS",
         "enableDesc": "Ativar o servidor DNS integrado",
@@ -959,7 +1382,7 @@
     "hours": "Horas",
     "minutes": "Minutos",
     "unknown": "Desconhecido",
-    "inbounds": "Inbounds",
+    "inbounds": "Entradas",
     "clients": "Clientes",
     "offline": "🔴 Offline",
     "online": "🟢 Online",
@@ -1009,7 +1432,7 @@
       "username": "👤 Nome de usuário: {{ .Username }}\r\n",
       "reason": "❗️ Motivo: {{ .Reason }}\r\n",
       "time": "⏰ Hora: {{ .Time }}\r\n",
-      "inbound": "📍 Inbound: {{ .Remark }}\r\n",
+      "inbound": "📍 Entrada: {{ .Remark }}\r\n",
       "port": "🔌 Porta: {{ .Port }}\r\n",
       "expire": "📅 Data de expiração: {{ .Time }}\r\n",
       "expireIn": "📅 Expira em: {{ .Time }}\r\n",
@@ -1089,9 +1512,9 @@
       "use_default": "🏷️ Usar padrão",
       "change_id": "⚙️🔑 ID",
       "change_password": "⚙️🔑 Senha",
-      "change_email": "⚙️📧 E-mail",
+      "change_email": "⚙️📧 Email",
       "change_comment": "⚙️💬 Comentário",
-      "change_flow": "⚙️🚦 Fluxo",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "Redefinir Todo o Tráfego",
       "SortedTrafficUsageReport": "Relatório de Uso de Tráfego Ordenado"
     },
@@ -1119,4 +1542,4 @@
       "chooseInbound": "Escolha um Inbound"
     }
   }
-}
+}

+ 491 - 68
web/translation/ru-RU.json

@@ -8,15 +8,22 @@
   "save": "Сохранить",
   "logout": "Выход",
   "create": "Создать",
+  "add": "Добавить",
+  "remove": "Удалить",
   "update": "Обновить",
   "copy": "Копировать",
   "copied": "Скопировано",
+  "more": "ещё",
   "download": "Скачать",
   "remark": "Примечание",
   "enable": "Включить",
   "protocol": "Протокол",
   "search": "Поиск",
   "filter": "Фильтр",
+  "all": "Все",
+  "from": "От",
+  "to": "До",
+  "done": "Готово",
   "loading": "Загрузка...",
   "refresh": "Обновить",
   "clear": "Очистить",
@@ -27,12 +34,12 @@
   "check": "Проверить",
   "indefinite": "Бесконечно",
   "unlimited": "Безлимит",
-  "none": "Пусто",
+  "none": "Нет",
   "qrCode": "QR-код",
   "info": "Информация",
   "edit": "Изменить",
   "delete": "Удалить",
-  "reset": "Сбросить",
+  "reset": "Сброс",
   "noData": "Нет данных.",
   "copySuccess": "Скопировано",
   "sure": "Да",
@@ -41,14 +48,14 @@
   "transmission": "Транспорт",
   "host": "Хост",
   "path": "Путь",
-  "camouflage": "Маскировка",
+  "camouflage": "Обфускация",
   "status": "Статус",
   "enabled": "Включено",
   "disabled": "Отключено",
   "depleted": "Исчерпано",
   "depletingSoon": "Почти исчерпано",
-  "offline": "Офлайн",
-  "online": "Онлайн",
+  "offline": "Не в сети",
+  "online": "В сети",
   "domainName": "Домен",
   "monitor": "Мониторинг IP",
   "certificate": "SSL-сертификат",
@@ -95,11 +102,12 @@
     "dark": "Темная",
     "ultraDark": "Очень темная",
     "dashboard": "Дашборд",
-    "inbounds": "Подключения",
+    "inbounds": "Входящие",
     "clients": "Клиенты",
+    "groups": "Группы",
     "nodes": "Узлы",
-    "settings": "Настройки",
-    "xray": "Настройки Xray",
+    "settings": "Настройки панели",
+    "xray": "Конфигурации Xray",
     "apiDocs": "Документация API",
     "logout": "Выход",
     "link": "Управление",
@@ -120,16 +128,16 @@
     },
     "index": {
       "title": "Дашборд",
-      "cpu": "ЦП",
+      "cpu": "CPU",
       "logicalProcessors": "Логические процессоры",
       "frequency": "Частота",
-      "swap": "Файл подкачки",
+      "swap": "Swap",
       "storage": "Диск",
-      "memory": "ОЗУ",
+      "memory": "RAM",
       "threads": "Потоки",
       "xrayStatus": "Xray",
-      "stopXray": "Остановить",
-      "restartXray": "Перезапустить",
+      "stopXray": "Стоп",
+      "restartXray": "Перезапуск",
       "xraySwitch": "Выбор версии",
       "xrayUpdates": "Обновления Xray",
       "xraySwitchClick": "Выберите нужную версию",
@@ -165,8 +173,8 @@
       "ipAddresses": "IP-адреса сервера",
       "toggleIpVisibility": "Скрыть или показать IP-адреса сервера",
       "overallSpeed": "Общая скорость передачи трафика",
-      "upload": "Отправка",
-      "download": "Загрузка",
+      "upload": "Загрузка",
+      "download": "Скачать",
       "totalData": "Общий объем трафика",
       "sent": "Отправлено",
       "received": "Получено",
@@ -226,7 +234,7 @@
       "customGeoErrUpdateAllIncomplete": "Не удалось обновить один или несколько пользовательских источников",
       "customGeoEmpty": "Пользовательских источников geo пока нет — нажмите «Добавить», чтобы создать",
       "dontRefresh": "Установка в процессе. Не обновляйте страницу",
-      "logs": "Журнал",
+      "logs": "Логи",
       "config": "Конфигурация",
       "backup": "Резервная копия",
       "backupTitle": "Бэкап и восстановление",
@@ -241,7 +249,7 @@
       "getConfigError": "Произошла ошибка при получении конфигурационного файла"
     },
     "inbounds": {
-      "title": "Подключения",
+      "title": "Входящие",
       "totalDownUp": "Отправлено/получено",
       "totalUsage": "Всего трафика",
       "inboundCount": "Всего подключений",
@@ -252,7 +260,7 @@
       "deployTo": "Развернуть на",
       "localPanel": "Локальная панель",
       "fallbacks": {
-        "title": "Фолбэки",
+        "title": "Fallback'и",
         "help": "Когда соединение на этом инбаунде не совпадает ни с одним клиентом, оно перенаправляется на другой инбаунд. Выберите дочерний инбаунд ниже — поля маршрутизации (SNI / ALPN / Path / xver) заполнятся автоматически из его транспорта, для большинства конфигураций больше ничего менять не нужно. Каждый дочерний должен слушать на 127.0.0.1 с security=none.",
         "empty": "Фолбэков пока нет",
         "add": "Добавить фолбэк",
@@ -270,14 +278,14 @@
       },
       "protocol": "Протокол",
       "port": "Порт",
-      "portMap": "Порт-маппинг",
+      "portMap": "Сопоставление портов",
       "traffic": "Трафик",
       "details": "Подробнее",
       "transportConfig": "Транспорт",
       "expireDate": "Дата окончания",
       "createdAt": "Создано",
       "updatedAt": "Обновлено",
-      "resetTraffic": "Сброс трафика",
+      "resetTraffic": "Сбросить трафик",
       "addInbound": "Создать подключение",
       "generalActions": "Общие действия",
       "modifyInbound": "Изменить подключение",
@@ -292,11 +300,31 @@
       "delAllClients": "Удалить всех клиентов",
       "delAllClientsConfirmTitle": "Удалить всех {count} клиентов из \"{remark}\"?",
       "delAllClientsConfirmContent": "Удаляет всех клиентов этого подключения и сбрасывает их записи трафика. Само подключение сохраняется. Это действие нельзя отменить.",
+      "attachClients": "Привязать клиентов к…",
+      "addClientsToGroup": "Добавить клиентов в группу…",
+      "attachClientsTitle": "Привязать клиентов из «{remark}»",
+      "attachClientsDesc": "Привязывает тех же {count} клиент(ов) (с тем же UUID/паролем и общим трафиком) к выбранным входящим. Они остаются и на этом входящем.",
+      "attachClientsTargets": "Целевые входящие",
+      "attachClientsNoTargets": "Нет других совместимых входящих для привязки.",
+      "attachClientsResult": "Привязано {attached}, пропущено {skipped}.",
+      "attachClientsResultMixed": "Привязано {attached}, пропущено {skipped}, ошибок {errors}.",
+      "attachClientsSelectLabel": "Клиенты для привязки",
+      "attachClientsSearchPlaceholder": "Поиск email или комментария",
+      "attachClientsStatusDisabled": "Отключено",
+      "attachClientsSelectedCount": "{selected} из {total} выбрано",
+      "detachClients": "Отвязать клиентов",
+      "detachClientsTitle": "Отвязать клиентов из «{remark}»",
+      "detachClientsDesc": "Удаляет выбранных клиент(ов) только с этого входящего. Записи клиентов сохраняются (используйте Delete для полного удаления). У источника всего {count} клиент(ов).",
+      "detachClientsResult": "Отвязано {detached}, пропущено {skipped}.",
+      "detachClientsResultMixed": "Отвязано {detached}, пропущено {skipped}, ошибок {errors}.",
+      "detachClientsSelectLabel": "Клиенты для отвязки",
       "exportLinksTitle": "Экспортировать ссылки подключения",
       "exportSubsTitle": "Экспортировать ссылки подписки",
       "exportAllLinksTitle": "Экспортировать все ссылки подключений",
       "exportAllSubsTitle": "Экспортировать все ссылки подписок",
-      "inboundJsonTitle": "JSON подключения",
+      "exportAllLinksFileName": "Все-входящие",
+      "exportAllSubsFileName": "Все-входящие-Subs",
+      "inboundJsonTitle": "JSON входящего",
       "deleteClient": "Удалить клиента",
       "deleteClientContent": "Вы уверены, что хотите удалить клиента?",
       "resetTrafficContent": "Вы уверены, что хотите сбросить трафик?",
@@ -306,7 +334,7 @@
       "destinationPort": "Порт назначения",
       "targetAddress": "Целевой адрес",
       "monitorDesc": "Оставьте пустым для прослушивания всех IP-адресов",
-      "meansNoLimit": "= Без ограничений (значение: ГБ)",
+      "meansNoLimit": "= Безлимит. (единица: ГБ)",
       "totalFlow": "Общий расход",
       "leaveBlankToNeverExpire": "Оставьте пустым, чтобы было бесконечным",
       "noRecommendKeepDefault": "Рекомендуется оставить настройки по умолчанию",
@@ -341,6 +369,7 @@
       "IPLimitlogDesc": "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)",
       "IPLimitlogclear": "Очистить лог",
       "setDefaultCert": "Установить сертификат панели",
+      "setDefaultCertEmpty": "Для панели не настроен сертификат. Сначала установите его в Настройках.",
       "streamTab": "Поток",
       "securityTab": "Безопасность",
       "sniffingTab": "Сниффинг",
@@ -361,15 +390,14 @@
         "allHelp": "Полный объект входящего со всеми полями в одном редакторе.",
         "settings": "Настройки",
         "settingsHelp": "Обёртка блока settings Xray:",
-        "sniffing": "Сниффинг",
+        "sniffing": "Sniffing",
         "sniffingHelp": "Обёртка блока sniffing Xray:",
-        "stream": "Поток",
+        "stream": "Stream",
         "streamHelp": "Обёртка блока stream Xray:",
         "jsonErrorPrefix": "Расширенный JSON"
       },
       "telegramDesc": "Пожалуйста, укажите Chat ID Telegram. (используйте команду '/id' в боте) или ({'@'}userinfobot)",
       "subscriptionDesc": "Вы можете найти свою ссылку подписки в разделе 'Подробнее'",
-      "info": "Информация",
       "same": "Тот же",
       "inboundData": "Данные подключений",
       "exportInbound": "Экспорт подключений",
@@ -406,6 +434,143 @@
         "getNewmldsa65Error": "Ошибка при получении сертификата mldsa65.",
         "getNewVlessEncError": "Ошибка при получении сертификата VlessEnc."
       },
+      "form": {
+        "moveUp": "Вверх",
+        "moveDown": "Вниз",
+        "addAll": "Добавить все",
+        "addAllFallbackTooltip": "Добавляет строку fallback для каждого подходящего входящего, ещё не подключённого",
+        "peers": "Peers",
+        "addPeer": "Добавить peer",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "Только для Windows. CIDR'ы автоматически добавляются в системную таблицу маршрутизации, чтобы соответствующий трафик шёл через TUN.",
+        "autoOutboundsInterface": "Авто-интерфейс исходящих",
+        "autoOutboundsInterfaceTooltip": "Физический интерфейс для исходящего трафика. Используйте 'auto' для автоопределения; включается автоматически при Auto system routes.",
+        "rewriteAddress": "Переписать адрес",
+        "rewritePort": "Переписать порт",
+        "allowedNetwork": "Разрешённая сеть",
+        "followRedirect": "Следовать redirect",
+        "accounts": "Аккаунты",
+        "allowTransparent": "Разрешить прозрачный",
+        "encryptionMethod": "Метод шифрования",
+        "visionTestseed": "Vision testseed",
+        "version": "Версия",
+        "udpIdleTimeout": "UDP idle timeout (с)",
+        "masquerade": "Masquerade",
+        "type": "Тип",
+        "upstreamUrl": "Upstream URL",
+        "rewriteHost": "Переписать Host",
+        "skipTlsVerify": "Пропустить TLS verify",
+        "directory": "Директория",
+        "statusCode": "Код статуса",
+        "body": "Body",
+        "headers": "Заголовки",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "Версия запроса",
+        "requestMethod": "Метод запроса",
+        "requestPath": "Путь запроса",
+        "requestHeaders": "Заголовки запроса",
+        "responseVersion": "Версия ответа",
+        "responseStatus": "Статус ответа",
+        "responseReason": "Причина ответа",
+        "responseHeaders": "Заголовки ответа",
+        "heartbeatPeriod": "Период heartbeat",
+        "serviceName": "Имя сервиса",
+        "authority": "Authority",
+        "multiMode": "Multi Mode",
+        "maxBufferedUpload": "Макс. буферизованная загрузка",
+        "maxUploadSize": "Макс. размер загрузки (байт)",
+        "streamUpServer": "Stream-Up Server",
+        "serverMaxHeaderBytes": "Server Max Header Bytes",
+        "paddingBytes": "Padding Bytes",
+        "uplinkHttpMethod": "HTTP-метод Uplink",
+        "paddingObfsMode": "Padding Obfs Mode",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "Padding Placement",
+        "paddingMethod": "Padding Method",
+        "sessionPlacement": "Session Placement",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence Placement",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink Data Placement",
+        "uplinkDataKey": "Uplink Data Key",
+        "noSseHeader": "Без заголовка SSE",
+        "ttiMs": "TTI (мс)",
+        "uplinkMbps": "Uplink (МБ/с)",
+        "downlinkMbps": "Downlink (МБ/с)",
+        "cwndMultiplier": "Множитель CWND",
+        "maxSendingWindow": "Макс. окно отправки",
+        "externalProxy": "External Proxy",
+        "sniPlaceholder": "SNI (по умолчанию = host)",
+        "fingerprint": "Fingerprint",
+        "defaultOption": "По умолчанию",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive Interval",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "Только V6",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "Доверенный X-Forwarded-For",
+        "addressPortStrategy": "Стратегия адрес+порт",
+        "tryDelayMs": "Задержка попытки (мс)",
+        "prioritizeIPv6": "Приоритет IPv6",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "Макс. одновременных попыток",
+        "customSockopt": "Пользовательский sockopt",
+        "addCustomOption": "Добавить опцию",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "Авто",
+        "minMaxVersion": "Мин/Макс версия",
+        "rejectUnknownSni": "Отклонить неизвестный SNI",
+        "disableSystemRoot": "Отключить System Root",
+        "sessionResumption": "Возобновление сессии",
+        "oneTimeLoading": "Однократная загрузка",
+        "usageOption": "Опция использования",
+        "buildChain": "Build Chain",
+        "echKey": "ECH key",
+        "echConfig": "ECH config",
+        "pinnedPeerCertSha256": "Закреплённый SHA-256 сертификата пира",
+        "pinnedPeerCertSha256Tip": "SHA-256-хеши сертификата пира в кодировке Base64. Только для панели — не записывается в конфиг xray сервера, но включается в ссылки-приглашения, чтобы клиенты могли закрепить сертификат.",
+        "pinnedPeerCertSha256Placeholder": "Base64-хеш(и), через запятую",
+        "generateRandomPin": "Сгенерировать случайный хеш",
+        "getNewEchCert": "Получить новый ECH-сертификат",
+        "show": "Показать",
+        "xver": "Xver",
+        "target": "Цель",
+        "maxTimeDiff": "Макс. разница во времени (мс)",
+        "minClientVer": "Мин. версия клиента",
+        "maxClientVer": "Макс. версия клиента",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "Получить новый сертификат",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "Получить новый Seed"
+      },
+      "info": {
+        "mode": "Режим",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "Имя интерфейса",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "Интерфейс исходящих",
+        "autoSystemRoutes": "Авто-маршруты системы",
+        "followRedirect": "FollowRedirect",
+        "auth": "Auth",
+        "noKernelTun": "TUN без kernel",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "Конфиг Peer {n}"
+      },
       "stream": {
         "general": {
           "request": "Запрос",
@@ -456,6 +621,20 @@
       "days": "Дни",
       "renew": "Автопродление",
       "renewDesc": "Автоматическое продление после окончания. (0 = отключено) (единица: день)",
+      "searchPlaceholder": "Поиск email, комментария, sub ID, UUID, пароля, auth…",
+      "filterTitle": "Фильтр клиентов",
+      "clearAllFilters": "Очистить все",
+      "sortOldest": "Сначала старые",
+      "sortNewest": "Сначала новые",
+      "sortRecentlyUpdated": "Недавно обновлены",
+      "sortRecentlyOnline": "Недавно в сети",
+      "sortEmailAZ": "Email А→Я",
+      "sortEmailZA": "Email Я→А",
+      "sortMostTraffic": "Больше трафика",
+      "sortHighestRemaining": "Больше остатка",
+      "sortExpiringSoonest": "Скорее истекают",
+      "has": "Есть",
+      "hasNot": "Нет",
       "title": "Клиенты",
       "actions": "Действия",
       "totalGB": "Всего отправлено/получено (ГБ)",
@@ -466,6 +645,9 @@
       "subId": "ID подписки",
       "online": "В сети",
       "email": "Email",
+      "group": "Группа",
+      "groupDesc": "Логическая метка для группировки связанных клиентов (например, команда, клиент, регион). Фильтруется из панели инструментов.",
+      "groupPlaceholder": "например, customer-a",
       "comment": "Комментарий",
       "traffic": "Трафик",
       "offline": "Не в сети",
@@ -485,15 +667,49 @@
       "noLinks": "Нет ссылок для общего доступа — сначала привяжите клиента к входящему с поддерживаемым протоколом.",
       "link": "Ссылка",
       "resetNotPossible": "Сначала привяжите этого клиента к входящему.",
-      "general": "Общее",
+      "general": "Общие",
       "resetAllTraffics": "Сбросить трафик всех клиентов",
       "resetAllTrafficsTitle": "Сбросить трафик всех клиентов?",
       "resetAllTrafficsContent": "Счётчики отправки/приёма всех клиентов сбрасываются в ноль. Квоты и срок действия не затрагиваются. Это действие нельзя отменить.",
-      "empty": "Клиентов пока нет — добавьте первого, чтобы начать.",
       "deleteConfirmTitle": "Удалить клиента {email}?",
       "deleteConfirmContent": "Клиент будет удалён из всех привязанных входящих, а его запись трафика будет уничтожена. Это действие нельзя отменить.",
       "deleteSelected": "Удалить ({count})",
       "adjustSelected": "Изменить ({count})",
+      "subLinksSelected": "Sub-ссылки ({count})",
+      "addToGroupTitle": "Добавить {count} клиент(ов) в группу",
+      "addToGroupTooltip": "Выберите существующую группу или введите новое имя. Используйте Ungroup, чтобы удалить клиентов из их текущей группы.",
+      "addToGroupPlaceholder": "Имя группы",
+      "addToGroupSuccessToast": "{count} клиент(ов) добавлено в {group}",
+      "ungroupSuccessToast": "Группа очищена у {count} клиент(ов)",
+      "ungroup": "Разгруппировать",
+      "ungroupConfirmTitle": "Удалить {count} клиент(ов) из их группы?",
+      "ungroupConfirmContent": "Очищает метку группы у каждого выбранного клиента. Сами клиенты сохраняются (используйте Delete для полного удаления).",
+      "addToGroup": "Добавить в группу",
+      "attach": "Привязать",
+      "adjust": "Корректировка",
+      "subLinks": "Sub-ссылки",
+      "selectedCount": "{count} выбрано",
+      "attachSelected": "Привязать ({count})",
+      "attachToInboundsTitle": "Привязать {count} клиент(ов) к входящим",
+      "attachToInboundsDesc": "Привязывает выбранных {count} клиент(ов) (тот же UUID/пароль и общий трафик) к выбранным входящим. Существующие привязки сохраняются.",
+      "attachToInboundsTargets": "Целевые входящие",
+      "attachToInboundsNoTargets": "Нет доступных многопользовательских входящих для привязки.",
+      "detachSelected": "Отвязать ({count})",
+      "detach": "Отвязать",
+      "detachFromInboundsTitle": "Отвязать {count} клиент(ов) от входящих",
+      "detachFromInboundsDesc": "Удаляет выбранных {count} клиент(ов) из выбранных входящих. Пары, где клиент не был привязан, тихо пропускаются. Записи клиентов сохраняются (используйте Delete для полного удаления).",
+      "detachFromInboundsTargets": "Входящие для отвязки",
+      "detachFromInboundsNoTargets": "Нет доступных многопользовательских входящих.",
+      "detachFromInboundsResult": "Отвязано {detached}, пропущено {skipped}.",
+      "detachFromInboundsResultMixed": "Отвязано {detached}, пропущено {skipped}, ошибок {errors}.",
+      "subLinksTitle": "Sub-ссылки ({count})",
+      "subLinkColumn": "URL подписки",
+      "subJsonLinkColumn": "URL JSON-подписки",
+      "subLinksCopyAll": "Копировать все",
+      "subLinksCopiedAll": "Скопировано {count} ссылок",
+      "subLinksEmpty": "Ни у одного из выбранных клиентов нет ID подписки.",
+      "subLinksDisabled": "Сервис подписки отключён.",
+      "subLinksDisabledHint": "Включите подписку в Настройки панели → Подписка для генерации ссылок.",
       "bulkDeleteConfirmTitle": "Удалить {count} клиентов?",
       "bulkDeleteConfirmContent": "Каждый выбранный клиент удаляется из всех привязанных входящих, его запись трафика уничтожается. Это действие нельзя отменить.",
       "bulkAdjustTitle": "Изменить {count} клиентов",
@@ -504,11 +720,12 @@
       "delDepleted": "Удалить исчерпанных",
       "delDepletedConfirmTitle": "Удалить исчерпанных клиентов?",
       "delDepletedConfirmContent": "Удаляются все клиенты, у которых исчерпана квота трафика или истёк срок. Это действие нельзя отменить.",
-      "auth": "Auth",
-      "hysteriaAuth": "Auth для Hysteria",
+      "auth": "Авторизация",
+      "hysteriaAuth": "Hysteria Auth",
       "uuid": "UUID",
       "flow": "Flow",
-      "reverseTag": "Reverse tag",
+      "vmessSecurity": "VMess Security",
+      "reverseTag": "Обратный тег",
       "reverseTagPlaceholder": "Необязательный Reverse tag",
       "telegramId": "ID пользователя Telegram",
       "telegramIdPlaceholder": "Числовой ID пользователя Telegram (0 = нет)",
@@ -528,13 +745,51 @@
         "delDepleted": "Удалено исчерпанных клиентов: {count}"
       }
     },
+    "groups": {
+      "title": "Группы",
+      "name": "Имя",
+      "clientCount": "Клиентов в группе",
+      "totalGroups": "Всего групп",
+      "totalGroupedClients": "Клиенты с группой",
+      "emptyGroups": "Пустые группы",
+      "addGroup": "Добавить группу",
+      "createSuccess": "Группа «{name}» создана.",
+      "rename": "Переименовать",
+      "renameTitle": "Переименовать {name}",
+      "renameCollision": "Группа с именем «{name}» уже существует.",
+      "renameSuccess": "Группа переименована для {count} клиент(ов).",
+      "deleteConfirmTitle": "Удалить группу {name}?",
+      "deleteConfirmContent": "Это удаляет группу и очищает её метку у {count} клиент(ов). Сами клиенты не удаляются.",
+      "deleteSuccess": "Группа очищена у {count} клиент(ов).",
+      "resetTraffic": "Сбросить трафик",
+      "resetConfirmTitle": "Сбросить трафик группы {name}?",
+      "resetConfirmContent": "Это обнулит up/down для всех {count} клиент(ов) в этой группе.",
+      "resetSuccess": "Сброшен трафик у {count} клиент(ов).",
+      "adjustSuccess": "Скорректировано {count} клиент(ов) в {name}.",
+      "emptyForAction": "В этой группе пока нет клиентов.",
+      "deleteGroupOnly": "Удалить группу (сохранить клиентов)",
+      "deleteClients": "Удалить клиентов группы",
+      "deleteClientsConfirmTitle": "Удалить всех клиентов в {name}?",
+      "deleteClientsConfirmContent": "Это безвозвратно удаляет {count} клиент(ов) вместе с их записями трафика. Метка группы также очищается. Это нельзя отменить.",
+      "deleteClientsSuccess": "Удалено {count} клиент(ов).",
+      "deleteClientsMixed": "{ok} удалено, {failed} пропущено",
+      "addToGroup": "Добавить клиентов…",
+      "addToGroupTitle": "Добавить клиентов в группу «{name}»",
+      "addToGroupDesc": "Выберите клиентов для добавления в эту группу. Существующие привязки к входящим сохраняются; меняется только метка группы. Клиенты, уже входящие в эту группу, не показываются.",
+      "addToGroupEmpty": "Нет других клиентов для добавления.",
+      "addToGroupResult": "Добавлено {count} клиент(ов) в {name}.",
+      "removeFromGroup": "Удалить клиентов…",
+      "removeFromGroupTitle": "Удалить клиентов из группы «{name}»",
+      "removeFromGroupDesc": "Выберите участников для удаления из этой группы. Сами клиенты сохраняются (используйте «Удалить клиентов группы» для полного удаления).",
+      "removeFromGroupResult": "Удалено {count} клиент(ов) из {name}."
+    },
     "nodes": {
       "title": "Узлы",
       "addNode": "Добавить узел",
-      "editNode": "Редактировать узел",
+      "editNode": "Изменить узел",
       "totalNodes": "Всего узлов",
-      "onlineNodes": "Онлайн",
-      "offlineNodes": "Офлайн",
+      "onlineNodes": "В сети",
+      "offlineNodes": "Не в сети",
       "avgLatency": "Средняя задержка",
       "name": "Имя",
       "namePlaceholder": "напр. de-frankfurt-1",
@@ -544,7 +799,7 @@
       "address": "Адрес",
       "port": "Порт",
       "basePath": "Базовый путь",
-      "apiToken": "Токен API",
+      "apiToken": "API Токен",
       "apiTokenPlaceholder": "Токен со страницы Настроек удалённой панели",
       "apiTokenHint": "Удалённая панель показывает свой токен API в разделе Настройки → Токен API.",
       "regenerate": "Сгенерировать токен заново",
@@ -555,7 +810,7 @@
       "status": "Статус",
       "cpu": "CPU",
       "mem": "Память",
-      "uptime": "Время работы",
+      "uptime": "Аптайм",
       "latency": "Задержка",
       "lastHeartbeat": "Последний пинг",
       "xrayVersion": "Версия Xray",
@@ -570,8 +825,8 @@
       "deleteConfirmTitle": "Удалить узел \"{name}\"?",
       "deleteConfirmContent": "Это остановит мониторинг узла. Сама удалённая панель не будет затронута.",
       "statusValues": {
-        "online": "Онлайн",
-        "offline": "Офлайн",
+        "online": "В сети",
+        "offline": "Не в сети",
         "unknown": "Неизвестно"
       },
       "toasts": {
@@ -590,7 +845,7 @@
       "title": "Настройки",
       "save": "Сохранить",
       "infoDesc": "Сохраните изменения и перезапустите панель для их применения.",
-      "restartPanel": "Перезапуск панели",
+      "restartPanel": "Перезапустить панель",
       "restartPanelDesc": "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера",
       "restartPanelSuccess": "Панель успешно перезапущена",
       "actions": "Действия",
@@ -604,7 +859,7 @@
       "warnDefaultBasePath": "Базовый путь по умолчанию \"/\" широко известен — измените его на случайный.",
       "warnDefaultSubPath": "Путь подписки по умолчанию \"/sub/\" широко известен — измените его.",
       "warnDefaultJsonPath": "JSON-путь подписки по умолчанию \"/json/\" широко известен — измените его.",
-      "TGBotSettings": "Telegram-Бот",
+      "TGBotSettings": "Telegram-бот",
       "panelListeningIP": "IP-адрес для управления панелью",
       "panelListeningIPDesc": "Оставьте пустым для подключения с любого IP",
       "panelListeningDomain": "Домен панели",
@@ -615,10 +870,12 @@
       "publicKeyPathDesc": "Введите полный путь, начинающийся с '/'",
       "privateKeyPath": "Путь к файлу приватного ключа сертификата панели",
       "privateKeyPathDesc": "Введите полный путь, начинающийся с '/'",
-      "panelUrlPath": "Корневой путь URL адреса панели",
+      "panelUrlPath": "URI-путь",
       "panelUrlPathDesc": "Должен начинаться с '/' и заканчиваться '/'",
       "pageSize": "Размер нумерации страниц",
       "pageSizeDesc": "Определить размер страницы для таблицы подключений. Установите 0, чтобы отключить",
+      "panelProxy": "Сетевой прокси панели",
+      "panelProxyDesc": "Маршрутизирует исходящие запросы самой панели (обновления geo, проверки версий Xray/панели, Telegram) через этот прокси для обхода серверной фильтрации GitHub/Telegram. Принимает socks5:// или http(s)://, напр. локальный SOCKS-входящий Xray. Оставьте пустым для прямого подключения.",
       "remarkModel": "Модель примечания и символ разделения",
       "datepicker": "Тип календаря",
       "datepickerPlaceholder": "Выберите дату",
@@ -630,11 +887,11 @@
       "newPassword": "Новый пароль",
       "telegramBotEnable": "Включить Telegram бота",
       "telegramBotEnableDesc": "Доступ к функциям панели через Telegram-бота",
-      "telegramToken": "Токен Telegram бота",
+      "telegramToken": "Telegram-токен",
       "telegramTokenDesc": "Необходимо получить токен у менеджера ботов Telegram {'@'}botfather",
-      "telegramProxy": "Прокси-сервер Socks5",
+      "telegramProxy": "SOCKS-прокси",
       "telegramProxyDesc": "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству.",
-      "telegramAPIServer": "API-сервер Telegram",
+      "telegramAPIServer": "Telegram API Server",
       "telegramAPIServerDesc": "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию.",
       "telegramChatId": "User ID администратора бота",
       "telegramChatIdDesc": "Один или несколько User ID администратора(-ов) Telegram-бота. Для получения User ID используйте {'@'}userinfobot или команду '/id' в боте.",
@@ -658,6 +915,8 @@
       "subEnable": "Включить подписку",
       "subEnableDesc": "Функция подписки с отдельной конфигурацией",
       "subJsonEnable": "Включить/отключить JSON-эндпоинт подписки независимо.",
+      "subJsonEnableTitle": "JSON-подписка",
+      "subClashEnableTitle": "Подписка Clash / Mihomo",
       "subTitle": "Заголовок подписки",
       "subTitleDesc": "Название подписки, которое видит клиент в VPN-клиенте",
       "subSupportUrl": "URL поддержки",
@@ -678,13 +937,13 @@
       "subCertPathDesc": "Введите полный путь, начинающийся с '/'",
       "subKeyPath": "Путь к файлу приватного ключа сертификата подписки",
       "subKeyPathDesc": "Введите полный путь, начинающийся с '/'",
-      "subPath": "Корневой путь URL-адреса подписки",
+      "subPath": "URI-путь",
       "subPathDesc": "Должен начинаться с '/' и заканчиваться на '/'",
       "subDomain": "Домен прослушивания",
       "subDomainDesc": "Оставьте пустым по умолчанию, чтобы слушать все домены и IP-адреса",
       "subUpdates": "Интервалы обновления подписки",
       "subUpdatesDesc": "Интервал между обновлениями в клиентском приложении (в часах)",
-      "subEncrypt": "Шифровать конфиги",
+      "subEncrypt": "Кодировать",
       "subEncryptDesc": "Шифровать возвращенные конфиги в подписке",
       "subShowInfo": "Показать информацию об использовании",
       "subShowInfoDesc": "Отображать остаток трафика и дату окончания после имени конфигурации",
@@ -693,7 +952,7 @@
       "subURI": "URI обратного прокси",
       "subURIDesc": "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами",
       "externalTrafficInformEnable": "Информация о внешнем трафике",
-      "externalTrafficInformEnableDesc": "Информировать внешний API о каждом обновлении трафика",
+      "externalTrafficInformEnableDesc": "Уведомлять внешний API при каждом обновлении трафика.",
       "externalTrafficInformURI": "URI информации о внешнем трафике",
       "externalTrafficInformURIDesc": "Обновления трафика отправляются на этот URI",
       "restartXrayOnClientDisable": "Перезапускать Xray после автоотключения",
@@ -703,6 +962,54 @@
       "fragmentSett": "Настройки фрагментации",
       "noisesDesc": "Включить Noises.",
       "noisesSett": "Настройки Noises",
+      "trustedProxyCidrs": "Доверенные CIDR прокси",
+      "trustedProxyCidrsDesc": "IP/CIDR через запятую, которым разрешено устанавливать заголовки forwarded host, proto и client IP.",
+      "ldap": {
+        "enable": "Включить LDAP-синхронизацию",
+        "host": "LDAP-хост",
+        "port": "Порт LDAP",
+        "useTls": "Использовать TLS (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "Настроено; оставьте пустым, чтобы сохранить текущий пароль.",
+        "passwordUnconfigured": "Не настроено.",
+        "passwordPlaceholder": "Настроено — введите новое значение для замены",
+        "baseDn": "Base DN",
+        "userFilter": "Фильтр пользователя",
+        "userAttr": "Атрибут пользователя (username/email)",
+        "vlessField": "Атрибут VLESS-флага",
+        "flagField": "Общий атрибут флага (опц.)",
+        "flagFieldDesc": "Если задано, переопределяет флаг VLESS — напр. shadowInactive.",
+        "truthyValues": "Truthy-значения",
+        "truthyValuesDesc": "Через запятую; по умолчанию: true,1,yes,on",
+        "invertFlag": "Инвертировать флаг",
+        "invertFlagDesc": "Включите, когда атрибут означает «отключено» (напр. shadowInactive).",
+        "syncSchedule": "Расписание синхронизации",
+        "syncScheduleDesc": "Строка типа cron, напр. @every 1m",
+        "inboundTags": "Теги входящих",
+        "inboundTagsDesc": "Входящие, на которых LDAP-синхронизация может авто-создавать или авто-удалять клиентов.",
+        "noInbounds": "Входящие не найдены. Сначала создайте входящий.",
+        "autoCreate": "Авто-создание клиентов",
+        "autoDelete": "Авто-удаление клиентов",
+        "defaultTotalGb": "Объём по умолчанию (ГБ)",
+        "defaultExpiryDays": "Срок по умолчанию (дни)",
+        "defaultIpLimit": "Лимит IP по умолчанию"
+      },
+      "subFormats": {
+        "packets": "Пакеты",
+        "length": "Длина",
+        "interval": "Интервал",
+        "maxSplit": "Макс. разбиение",
+        "noises": "Шумы",
+        "noiseItem": "Шум №{n}",
+        "type": "Тип",
+        "packet": "Пакет",
+        "delayMs": "Задержка (мс)",
+        "applyTo": "Применить к",
+        "addNoise": "+ Шум",
+        "concurrency": "Параллелизм",
+        "xudpConcurrency": "Параллелизм xudp",
+        "xudpUdp443": "xudp UDP 443"
+      },
       "mux": "Mux",
       "muxDesc": "Передача нескольких независимых потоков данных в одном соединении.",
       "muxSett": "Настройки Mux",
@@ -758,6 +1065,9 @@
       "save": "Сохранить",
       "restart": "Перезапуск Xray",
       "restartSuccess": "Xray успешно перезапущен",
+      "restartOutputTitle": "Вывод перезапуска Xray",
+      "restartConfirmTitle": "Перезапустить xray?",
+      "restartConfirmContent": "Перезагружает сервис xray с сохранённой конфигурацией.",
       "stopSuccess": "Xray успешно остановлен",
       "restartError": "Произошла ошибка при перезапуске Xray.",
       "stopError": "Произошла ошибка при остановке Xray.",
@@ -765,7 +1075,7 @@
       "advancedTemplate": "Расширенный шаблон",
       "generalConfigs": "Основные настройки",
       "generalConfigsDesc": "Эти параметры описывают общие настройки",
-      "logConfigs": "Логи",
+      "logConfigs": "Лог",
       "logConfigsDesc": "Логи могут замедлять работу сервера. Включайте только нужные вам виды логов при необходимости!",
       "blockConfigsDesc": "Настройте, чтобы клиенты не имели доступа к определенным протоколам",
       "basicRouting": "Базовые соединения",
@@ -790,10 +1100,12 @@
       "outboundTestUrl": "URL для теста исходящего",
       "outboundTestUrlDesc": "URL для проверки подключения исходящего",
       "Torrent": "Заблокировать BitTorrent",
-      "Inbounds": "Входящие подключения",
+      "Inbounds": "Входящие",
       "InboundsDesc": "Изменение шаблона конфигурации для подключения определенных клиентов",
-      "Outbounds": "Исходящие подключения",
+      "Outbounds": "Исходящие",
       "Balancers": "Балансировщик",
+      "balancerTagRequired": "Тег обязателен",
+      "balancerSelectorRequired": "Выберите хотя бы одно исходящее",
       "OutboundsDesc": "Изменение шаблона конфигурации, чтобы определить исходящие подключения для этого сервера",
       "Routings": "Маршрутизация",
       "RoutingsDesc": "Важен приоритет каждого правила!",
@@ -827,11 +1139,78 @@
         "inbound": "Входящее подключение",
         "outbound": "Исходящее подключение",
         "balancer": "Балансировщик",
-        "info": "Информация",
+        "info": "Инфо",
         "add": "Создать правило",
         "edit": "Редактировать правило",
         "useComma": "Элементы, разделённые запятыми"
       },
+      "routing": {
+        "dragToReorder": "Перетащите для изменения порядка"
+      },
+      "ruleForm": {
+        "sourceIps": "IP источника",
+        "sourcePort": "Порт источника",
+        "vlessRoute": "VLESS route",
+        "attributes": "Атрибуты",
+        "value": "Значение",
+        "user": "Пользователь",
+        "inboundTags": "Теги входящих",
+        "outboundTag": "Тег исходящего",
+        "balancerTag": "Тег балансировщика",
+        "balancerTagTooltip": "Направляет трафик через один из настроенных балансировщиков нагрузки"
+      },
+      "outboundForm": {
+        "tagDuplicate": "Тег уже используется другим исходящим",
+        "tagRequired": "Тег обязателен",
+        "tagPlaceholder": "уникальный-тег",
+        "localIpPlaceholder": "локальный IP",
+        "addressRequired": "Адрес обязателен",
+        "portRequired": "Порт обязателен",
+        "optional": "опционально",
+        "udpOverTcp": "UDP over TCP",
+        "uotVersion": "Версия UoT",
+        "inboundTag": "Тег входящего",
+        "inboundTagPlaceholder": "тег входящего в правилах маршрутизации",
+        "responseType": "Тип ответа",
+        "rewriteNetwork": "Переписать сеть",
+        "unchanged": "(без изменений)",
+        "unchangedAddress": "(без изменений) напр. 1.1.1.1",
+        "rules": "Правила",
+        "ruleN": "Правило {n}",
+        "action": "Действие",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "Финальные правила",
+        "overrideXrayPrivateIp": "Переопределить дефолтный блок частных IP в Xray",
+        "blockDelay": "Задержка блока (мс)",
+        "reverseSniffing": "Обратный sniffing",
+        "workers": "Воркеры",
+        "reserved": "Зарезервировано",
+        "minUploadInterval": "Мин. интервал загрузки (мс)",
+        "maxUploadSizeBytes": "Макс. размер загрузки (байт)",
+        "uplinkChunkSize": "Размер chunk Uplink",
+        "noGrpcHeader": "Без gRPC-заголовка",
+        "maxConcurrency": "Макс. параллелизм",
+        "maxConnections": "Макс. соединений",
+        "maxReuseTimes": "Макс. повторных использований",
+        "maxRequestTimes": "Макс. запросов",
+        "maxReusableSecs": "Макс. секунд повторного использования",
+        "keepAlivePeriod": "Период keep alive",
+        "authPassword": "Пароль авторизации",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "имя сервера",
+        "verifyPeerName": "Проверять имя peer",
+        "pinnedSha256": "Pinned SHA256",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "Интервал keep alive",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "Интерфейс",
+        "ipv6Only": "Только IPv6",
+        "acceptProxyProtocol": "Принимать proxy protocol",
+        "tcpUserTimeoutMs": "TCP user timeout (мс)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (с)"
+      },
       "outbound": {
         "addOutbound": "Создать исходящее подключение",
         "addReverse": "Создать реверс-прокси",
@@ -846,8 +1225,8 @@
         "reverse": "Реверс-прокси",
         "domain": "Домен",
         "type": "Тип",
-        "bridge": "Мост",
-        "portal": "Портал",
+        "bridge": "Bridge",
+        "portal": "Portal",
         "link": "Ссылка",
         "intercon": "Соединение",
         "settings": "Настройки",
@@ -860,6 +1239,8 @@
         "testSuccess": "Тест успешен",
         "testFailed": "Тест не пройден",
         "testError": "Не удалось протестировать исходящее подключение",
+        "testModeTooltip": "TCP: быстрый dial-only probe. HTTP: полный запрос через xray.",
+        "testAll": "Тестировать все",
         "nordvpn": "NordVPN",
         "accessToken": "Токен доступа",
         "country": "Страна",
@@ -876,6 +1257,16 @@
         "balancerSelectors": "Селекторы",
         "tag": "Тег",
         "tagDesc": "Уникальный тег",
+        "tagDuplicate": "Тег уже используется другим балансировщиком",
+        "tagPlaceholder": "уникальный тег балансировщика",
+        "selector": "Селектор",
+        "fallback": "Fallback",
+        "expected": "Ожидаемое",
+        "expectedPlaceholder": "оптимальное число узлов",
+        "maxRtt": "Макс. RTT",
+        "tolerance": "Допуск",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag."
       },
       "wireguard": {
@@ -892,6 +1283,38 @@
         "userLevel": "Уровень пользователя",
         "userLevelDesc": "Все соединения, установленные через этот входящий поток, будут использовать этот уровень пользователя. Значение по умолчанию - 0"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "Приватный ключ",
+        "noServers": "Серверов для выбранной страны не найдено",
+        "noPublicKey": "Выбранный сервер не сообщает публичный ключ NordLynx.",
+        "outboundAdded": "Исходящий NordVPN добавлен",
+        "outboundUpdated": "Исходящий NordVPN обновлён"
+      },
+      "warp": {
+        "licenseError": "Не удалось установить лицензию WARP.",
+        "fetchFirst": "Сначала получите WARP-конфиг.",
+        "createAccount": "Создать аккаунт WARP",
+        "accessToken": "Access token",
+        "deviceId": "ID устройства",
+        "licenseKey": "Лицензионный ключ",
+        "privateKey": "Приватный ключ",
+        "deleteAccount": "Удалить аккаунт",
+        "settings": "Настройки",
+        "licenseKeyLabel": "Лицензионный ключ WARP / WARP+",
+        "key": "Ключ",
+        "keyPlaceholder": "26-символьный ключ WARP+",
+        "accountInfo": "Информация об аккаунте",
+        "deviceName": "Имя устройства",
+        "deviceModel": "Модель устройства",
+        "deviceEnabled": "Устройство включено",
+        "accountType": "Тип аккаунта",
+        "role": "Роль",
+        "warpPlusData": "WARP+ data",
+        "quota": "Квота",
+        "usage": "Использование",
+        "addOutbound": "Добавить исходящий"
+      },
       "dns": {
         "enable": "Включить DNS",
         "enableDesc": "Включить встроенный DNS-сервер",
@@ -959,10 +1382,10 @@
     "hours": "Часов",
     "minutes": "Минуты",
     "unknown": "Неизвестно",
-    "inbounds": "Входящие подключения",
+    "inbounds": "Входящие",
     "clients": "Клиенты",
-    "offline": "🔴 Офлайн",
-    "online": "🟢 Онлайн",
+    "offline": "🔴 Не в сети",
+    "online": "🟢 В сети",
     "commands": {
       "unknown": "❗ Неизвестная команда",
       "pleaseChoose": "👇 Пожалуйста, выберите:\r\n",
@@ -992,24 +1415,24 @@
       "2faFailed": "Ошибка 2FA",
       "report": "🕰 Запланированные отчеты: {{ .RunTime }}\r\n",
       "datetime": "⏰ Дата и время: {{ .DateTime }}\r\n",
-      "hostname": "💻 Имя хоста: {{ .Hostname }}\r\n",
+      "hostname": "💻 Хост: {{ .Hostname }}\r\n",
       "version": "🚀 Версия X-UI: {{ .Version }}\r\n",
       "xrayVersion": "📡 Версия Xray: {{ .XrayVersion }}\r\n",
       "ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
       "ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
       "ip": "🌐 IP: {{ .IP }}\r\n",
-      "ips": "🔢 IP-адреса:\r\n{{ .IPs }}\r\n",
+      "ips": "🔢 IP:\r\n{{ .IPs }}\r\n",
       "serverUpTime": "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n",
       "serverLoad": "📈 Нагрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
-      "serverMemory": "📋 ОЗУ сервера: {{ .Current }}/{{ .Total }}\r\n",
-      "tcpCount": "🔹 Количество TCP-соединений: {{ .Count }}\r\n",
-      "udpCount": "🔸 Количество UDP-соединений: {{ .Count }}\r\n",
+      "serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
+      "tcpCount": "🔹 TCP: {{ .Count }}\r\n",
+      "udpCount": "🔸 UDP: {{ .Count }}\r\n",
       "traffic": "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
-      "xrayStatus": "ℹ️ Состояние Xray: {{ .State }}\r\n",
+      "xrayStatus": "ℹ️ Статус: {{ .State }}\r\n",
       "username": "👤 Имя пользователя: {{ .Username }}\r\n",
       "reason": "❗️ Причина: {{ .Reason }}\r\n",
       "time": "⏰ Время: {{ .Time }}\r\n",
-      "inbound": "📍 Входящее подключение: {{ .Remark }}\r\n",
+      "inbound": "📍 Входящий: {{ .Remark }}\r\n",
       "port": "🔌 Порт: {{ .Port }}\r\n",
       "expire": "📅 Дата окончания: {{ .Time }}\r\n",
       "expireIn": "📅 Окончание через: {{ .Time }}\r\n",
@@ -1018,9 +1441,9 @@
       "online": "🌐 Статус соединения: {{ .Status }}\r\n",
       "lastOnline": "🔙 Был(а) в сети: {{ .Time }}\r\n",
       "email": "📧 Email: {{ .Email }}\r\n",
-      "upload": "🔼 Исходящий трафик: ↑{{ .Upload }}\r\n",
-      "download": "🔽 Входящий трафик: ↓{{ .Download }}\r\n",
-      "total": "📊 Всего: ↑↓{{ .UpDown }} из {{ .Total }}\r\n",
+      "upload": "🔼 Загрузка: ↑{{ .Upload }}\r\n",
+      "download": "🔽 Загрузка: ↓{{ .Download }}\r\n",
+      "total": "📊 Всего: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
       "TGUser": "👤 Telegram User ID: {{ .TelegramID }}\r\n",
       "exhaustedMsg": "🚨 Исчерпаны {{ .Type }}:\r\n",
       "exhaustedCount": "🚨 Количество исчерпанных {{ .Type }}:\r\n",
@@ -1077,7 +1500,7 @@
       "ipLimit": "🔢 Лимит IP",
       "setTGUser": "👤 Установить пользователя Telegram",
       "toggle": "🔘 Вкл./Выкл.",
-      "custom": "🔢 Свой",
+      "custom": "🔢 Своё",
       "confirmNumber": "✅ Подтвердить: {{ .Num }}",
       "confirmNumberAdd": "✅ Подтвердить добавление: {{ .Num }}",
       "limitTraffic": "🚧 Лимит трафика",
@@ -1091,7 +1514,7 @@
       "change_password": "⚙️🔑 Пароль",
       "change_email": "⚙️📧 Email",
       "change_comment": "⚙️💬 Комментарий",
-      "change_flow": "⚙️🚦 Поток",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "Сбросить весь трафик",
       "SortedTrafficUsageReport": "Отсортированный отчет об использовании трафика"
     },
@@ -1119,4 +1542,4 @@
       "chooseInbound": "Выберите входящее подключение"
     }
   }
-}
+}

+ 460 - 37
web/translation/tr-TR.json

@@ -8,15 +8,22 @@
   "save": "Kaydet",
   "logout": "Çıkış Yap",
   "create": "Oluştur",
+  "add": "Ekle",
+  "remove": "Kaldır",
   "update": "Güncelle",
   "copy": "Kopyala",
   "copied": "Kopyalandı",
+  "more": "daha",
   "download": "İndir",
   "remark": "Açıklama",
   "enable": "Etkin",
   "protocol": "Protokol",
   "search": "Ara",
-  "filter": "Filtrele",
+  "filter": "Filtre",
+  "all": "Tümü",
+  "from": "Başlangıç",
+  "to": "Bitiş",
+  "done": "Tamam",
   "loading": "Yükleniyor...",
   "refresh": "Yenile",
   "clear": "Temizle",
@@ -27,7 +34,7 @@
   "check": "Kontrol Et",
   "indefinite": "Belirsiz",
   "unlimited": "Sınırsız",
-  "none": "Hiçbiri",
+  "none": "Yok",
   "qrCode": "QR Kod",
   "info": "Daha Fazla Bilgi",
   "edit": "Düzenle",
@@ -39,9 +46,9 @@
   "encryption": "Şifreleme",
   "useIPv4ForHost": "Ana bilgisayar için IPv4 kullan",
   "transmission": "İletim",
-  "host": "Sunucu",
+  "host": "Host",
   "path": "Yol",
-  "camouflage": "Kandırma",
+  "camouflage": "Karartma",
   "status": "Durum",
   "enabled": "Etkin",
   "disabled": "Devre Dışı",
@@ -97,6 +104,7 @@
     "dashboard": "Genel Bakış",
     "inbounds": "Gelenler",
     "clients": "İstemciler",
+    "groups": "Gruplar",
     "nodes": "Düğümler",
     "settings": "Panel Ayarları",
     "xray": "Xray Yapılandırmaları",
@@ -120,16 +128,16 @@
     },
     "index": {
       "title": "Genel Bakış",
-      "cpu": "İşlemci",
+      "cpu": "CPU",
       "logicalProcessors": "Mantıksal işlemciler",
       "frequency": "Frekans",
-      "swap": "Takas",
+      "swap": "Swap",
       "storage": "Depolama",
       "memory": "RAM",
-      "threads": "İş parçacıkları",
+      "threads": "İş parçacığı",
       "xrayStatus": "Xray",
       "stopXray": "Durdur",
-      "restartXray": "Yeniden Başlat",
+      "restartXray": "Yeniden başlat",
       "xraySwitch": "Sürüm",
       "xrayUpdates": "Xray Güncellemeleri",
       "xraySwitchClick": "Geçiş yapmak istediğiniz sürümü seçin.",
@@ -166,7 +174,7 @@
       "toggleIpVisibility": "IP görünürlüğünü değiştir",
       "overallSpeed": "Genel hız",
       "upload": "Yükleme",
-      "download": "İndirme",
+      "download": "İndir",
       "totalData": "Toplam veri",
       "sent": "Gönderilen",
       "received": "Alınan",
@@ -270,14 +278,14 @@
       },
       "protocol": "Protokol",
       "port": "Port",
-      "portMap": "Port Atama",
+      "portMap": "Port eşlemesi",
       "traffic": "Trafik",
       "details": "Detaylar",
       "transportConfig": "Taşıma",
       "expireDate": "Süre",
       "createdAt": "Oluşturuldu",
       "updatedAt": "Güncellendi",
-      "resetTraffic": "Trafiği Sıfırla",
+      "resetTraffic": "Trafiği sıfırla",
       "addInbound": "Gelen Ekle",
       "generalActions": "Genel Eylemler",
       "modifyInbound": "Geleni Düzenle",
@@ -292,11 +300,31 @@
       "delAllClients": "Tüm istemcileri sil",
       "delAllClientsConfirmTitle": "\"{remark}\" içindeki {count} istemcinin tamamı silinsin mi?",
       "delAllClientsConfirmContent": "Bu inbound'a ait tüm istemcileri ve trafik kayıtlarını siler. Inbound'un kendisi korunur. Bu işlem geri alınamaz.",
+      "attachClients": "İstemcileri şuna bağla…",
+      "addClientsToGroup": "İstemcileri gruba ekle…",
+      "attachClientsTitle": "«{remark}» gelenindeki istemcileri bağla",
+      "attachClientsDesc": "Aynı {count} istemciyi (aynı UUID/parola ve paylaşılan trafik) seçilen gelenlere bağlar. Bu gelende de kalırlar.",
+      "attachClientsTargets": "Hedef gelenler",
+      "attachClientsNoTargets": "Bağlanacak uyumlu başka gelen yok.",
+      "attachClientsResult": "Bağlandı {attached}, atlandı {skipped}.",
+      "attachClientsResultMixed": "Bağlandı {attached}, atlandı {skipped}, hata {errors}.",
+      "attachClientsSelectLabel": "Bağlanacak istemciler",
+      "attachClientsSearchPlaceholder": "Email veya yorum ara",
+      "attachClientsStatusDisabled": "Devre dışı",
+      "attachClientsSelectedCount": "{total} içinden {selected} seçildi",
+      "detachClients": "İstemcileri çöz",
+      "detachClientsTitle": "«{remark}» gelenindeki istemcileri çöz",
+      "detachClientsDesc": "Seçilen istemcileri yalnızca bu gelenden kaldırır. İstemci kayıtları korunur (tamamen kaldırmak için Delete kullanın). Kaynakta toplam {count} istemci var.",
+      "detachClientsResult": "Çözüldü {detached}, atlandı {skipped}.",
+      "detachClientsResultMixed": "Çözüldü {detached}, atlandı {skipped}, hata {errors}.",
+      "detachClientsSelectLabel": "Çözülecek istemciler",
       "exportLinksTitle": "Inbound bağlantılarını dışa aktar",
       "exportSubsTitle": "Abonelik bağlantılarını dışa aktar",
       "exportAllLinksTitle": "Tüm inbound bağlantılarını dışa aktar",
       "exportAllSubsTitle": "Tüm abonelik bağlantılarını dışa aktar",
-      "inboundJsonTitle": "Inbound JSON",
+      "exportAllLinksFileName": "Tum-Gelenler",
+      "exportAllSubsFileName": "Tum-Gelenler-Subs",
+      "inboundJsonTitle": "Gelen JSON",
       "deleteClient": "Müşteriyi Sil",
       "deleteClientContent": "Müşteriyi silmek istediğinizden emin misiniz?",
       "resetTrafficContent": "Trafiği sıfırlamak istediğinizden emin misiniz?",
@@ -333,7 +361,7 @@
       "delDepletedClients": "Bitmiş Müşterileri Sil",
       "delDepletedClientsTitle": "Bitmiş Müşterileri Sil",
       "delDepletedClientsContent": "Tüm bitmiş müşterileri silmek istediğinizden emin misiniz?",
-      "email": "E-posta",
+      "email": "Email",
       "emailDesc": "Lütfen benzersiz bir e-posta adresi sağlayın.",
       "IPLimit": "IP Limiti",
       "IPLimitDesc": "Sayının aşılması durumunda gelen devre dışı bırakılır. (0 = devre dışı)",
@@ -341,9 +369,10 @@
       "IPLimitlogDesc": "IP geçmiş günlüğü. (devre dışı bırakıldıktan sonra gelini etkinleştirmek için günlüğü temizleyin)",
       "IPLimitlogclear": "Günlüğü Temizle",
       "setDefaultCert": "Panelden Sertifikayı Ayarla",
+      "setDefaultCertEmpty": "Panel için sertifika yapılandırılmamış. Önce Ayarlar'dan ayarlayın.",
       "streamTab": "Akış",
       "securityTab": "Güvenlik",
-      "sniffingTab": "Sniffing",
+      "sniffingTab": "Dinleme",
       "sniffingMetadataOnly": "Yalnızca üst veri",
       "sniffingRouteOnly": "Yalnızca yönlendirme",
       "sniffingIpsExcluded": "Hariç tutulan IP'ler",
@@ -363,13 +392,12 @@
         "settingsHelp": "Xray settings bloğunun sarmalayıcısı:",
         "sniffing": "Sniffing",
         "sniffingHelp": "Xray sniffing bloğunun sarmalayıcısı:",
-        "stream": "Akış",
+        "stream": "Stream",
         "streamHelp": "Xray stream bloğunun sarmalayıcısı:",
         "jsonErrorPrefix": "Gelişmiş JSON"
       },
       "telegramDesc": "Lütfen Telegram Sohbet Kimliği sağlayın. (botta '/id' komutunu kullanın) veya ({'@'}userinfobot)",
       "subscriptionDesc": "Abonelik URL'inizi bulmak için 'Detaylar'a gidin. Ayrıca, aynı adı birden fazla müşteri için kullanabilirsiniz.",
-      "info": "Bilgi",
       "same": "Aynı",
       "inboundData": "Gelenin Verileri",
       "exportInbound": "Geleni Dışa Aktar",
@@ -406,6 +434,143 @@
         "getNewmldsa65Error": "mldsa65 sertifikası alınırken hata oluştu.",
         "getNewVlessEncError": "VlessEnc sertifikası alınırken hata oluştu."
       },
+      "form": {
+        "moveUp": "Yukarı",
+        "moveDown": "Aşağı",
+        "addAll": "Tümünü ekle",
+        "addAllFallbackTooltip": "Henüz bağlanmamış her uygun gelen için bir fallback satırı ekler",
+        "peers": "Peers",
+        "addPeer": "Peer ekle",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "Yalnızca Windows. CIDR'ler eşleşen trafiğin TUN üzerinden gitmesi için sistem yönlendirme tablosuna otomatik eklenir.",
+        "autoOutboundsInterface": "Otomatik giden arabirimi",
+        "autoOutboundsInterfaceTooltip": "Giden trafiği için fiziksel arabirim. Tespit için 'auto' kullanın; Auto system routes açıkken otomatik etkinleşir.",
+        "rewriteAddress": "Adresi yeniden yaz",
+        "rewritePort": "Port'u yeniden yaz",
+        "allowedNetwork": "İzin verilen ağ",
+        "followRedirect": "Redirect'i takip et",
+        "accounts": "Hesaplar",
+        "allowTransparent": "Şeffafa izin ver",
+        "encryptionMethod": "Şifreleme yöntemi",
+        "visionTestseed": "Vision testseed",
+        "version": "Sürüm",
+        "udpIdleTimeout": "UDP idle timeout (s)",
+        "masquerade": "Masquerade",
+        "type": "Tip",
+        "upstreamUrl": "Upstream URL",
+        "rewriteHost": "Host'u yeniden yaz",
+        "skipTlsVerify": "TLS doğrulamayı atla",
+        "directory": "Dizin",
+        "statusCode": "Durum kodu",
+        "body": "Body",
+        "headers": "Başlıklar",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "İstek sürümü",
+        "requestMethod": "İstek yöntemi",
+        "requestPath": "İstek yolu",
+        "requestHeaders": "İstek başlıkları",
+        "responseVersion": "Yanıt sürümü",
+        "responseStatus": "Yanıt durumu",
+        "responseReason": "Yanıt sebebi",
+        "responseHeaders": "Yanıt başlıkları",
+        "heartbeatPeriod": "Heartbeat periyodu",
+        "serviceName": "Servis adı",
+        "authority": "Authority",
+        "multiMode": "Multi Mode",
+        "maxBufferedUpload": "Maks. tamponlu yükleme",
+        "maxUploadSize": "Maks. yükleme boyutu (Byte)",
+        "streamUpServer": "Stream-Up Server",
+        "serverMaxHeaderBytes": "Sunucu maks. başlık bayt",
+        "paddingBytes": "Padding bayt",
+        "uplinkHttpMethod": "Uplink HTTP yöntemi",
+        "paddingObfsMode": "Padding obfs modu",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "Padding konumu",
+        "paddingMethod": "Padding yöntemi",
+        "sessionPlacement": "Session Placement",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence Placement",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink Data Placement",
+        "uplinkDataKey": "Uplink Data Key",
+        "noSseHeader": "SSE başlığı yok",
+        "ttiMs": "TTI (ms)",
+        "uplinkMbps": "Yükleme (MB/s)",
+        "downlinkMbps": "İndirme (MB/s)",
+        "cwndMultiplier": "CWND çarpanı",
+        "maxSendingWindow": "Maks. gönderme penceresi",
+        "externalProxy": "Harici proxy",
+        "sniPlaceholder": "SNI (varsayılan host)",
+        "fingerprint": "Fingerprint",
+        "defaultOption": "Varsayılan",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive Interval",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "Yalnızca V6",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "Güvenilir X-Forwarded-For",
+        "addressPortStrategy": "Adres+port stratejisi",
+        "tryDelayMs": "Deneme gecikmesi (ms)",
+        "prioritizeIPv6": "IPv6 önceliği",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "Maks. eş zamanlı deneme",
+        "customSockopt": "Özel sockopt",
+        "addCustomOption": "Özel seçenek ekle",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "Otomatik",
+        "minMaxVersion": "Min/Maks sürüm",
+        "rejectUnknownSni": "Bilinmeyen SNI reddet",
+        "disableSystemRoot": "System Root'u devre dışı bırak",
+        "sessionResumption": "Oturum sürdürme",
+        "oneTimeLoading": "Tek seferlik yükleme",
+        "usageOption": "Kullanım seçeneği",
+        "buildChain": "Zincir oluştur",
+        "echKey": "ECH key",
+        "echConfig": "ECH yapılandırması",
+        "pinnedPeerCertSha256": "Sabitlenmiş Peer Sertifikası SHA-256",
+        "pinnedPeerCertSha256Tip": "Peer sertifikasının Base64 kodlu SHA-256 hash'leri. Sadece panel — sunucunun xray yapılandırmasına yazılmaz, ancak istemcilerin sertifikayı sabitleyebilmesi için paylaşım bağlantılarına eklenir.",
+        "pinnedPeerCertSha256Placeholder": "base64 hash(ler), virgülle ayrılmış",
+        "generateRandomPin": "Rastgele hash üret",
+        "getNewEchCert": "Yeni ECH sertifikası al",
+        "show": "Göster",
+        "xver": "Xver",
+        "target": "Hedef",
+        "maxTimeDiff": "Maks. zaman farkı (ms)",
+        "minClientVer": "Min. istemci sürümü",
+        "maxClientVer": "Maks. istemci sürümü",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "Yeni sertifika al",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "Yeni Seed al"
+      },
+      "info": {
+        "mode": "Mod",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "Arabirim adı",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "Giden arabirimi",
+        "autoSystemRoutes": "Otomatik sistem yönlendirmeleri",
+        "followRedirect": "FollowRedirect",
+        "auth": "Auth",
+        "noKernelTun": "Çekirdeksiz TUN",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "Peer {n} yapılandırması"
+      },
       "stream": {
         "general": {
           "request": "İstek",
@@ -456,6 +621,20 @@
       "days": "Gün",
       "renew": "Otomatik yenileme",
       "renewDesc": "Süre dolduktan sonra otomatik yenileme. (0 = devre dışı) (birim: gün)",
+      "searchPlaceholder": "Email, yorum, sub ID, UUID, parola, auth ara…",
+      "filterTitle": "İstemcileri filtrele",
+      "clearAllFilters": "Tümünü temizle",
+      "sortOldest": "Önce en eski",
+      "sortNewest": "Önce en yeni",
+      "sortRecentlyUpdated": "Son güncellenen",
+      "sortRecentlyOnline": "Son zamanlarda çevrimiçi",
+      "sortEmailAZ": "Email A→Z",
+      "sortEmailZA": "Email Z→A",
+      "sortMostTraffic": "En çok trafik",
+      "sortHighestRemaining": "En çok kalan",
+      "sortExpiringSoonest": "Yakında biten",
+      "has": "Var",
+      "hasNot": "Yok",
       "title": "İstemciler",
       "actions": "Eylemler",
       "totalGB": "Toplam Gönderilen/Alınan (GB)",
@@ -465,7 +644,10 @@
       "password": "Şifre",
       "subId": "Abonelik ID'si",
       "online": "Çevrimiçi",
-      "email": "E-posta",
+      "email": "Email",
+      "group": "Grup",
+      "groupDesc": "İlgili istemcileri gruplamak için mantıksal etiket (ekip, müşteri, bölge). Araç çubuğundan filtrelenebilir.",
+      "groupPlaceholder": "örn. customer-a",
       "comment": "Yorum",
       "traffic": "Trafik",
       "offline": "Çevrimdışı",
@@ -489,11 +671,45 @@
       "resetAllTraffics": "Tüm istemcilerin trafiğini sıfırla",
       "resetAllTrafficsTitle": "Tüm istemcilerin trafiği sıfırlansın mı?",
       "resetAllTrafficsContent": "Her istemcinin yükleme/indirme sayaçları sıfırlanır. Kotalar ve son kullanma tarihleri etkilenmez. Geri alınamaz.",
-      "empty": "Henüz istemci yok — başlamak için bir tane ekleyin.",
       "deleteConfirmTitle": "{email} istemcisi silinsin mi?",
       "deleteConfirmContent": "Bu işlem istemciyi bağlı tüm inbound'lardan kaldırır ve trafik kaydını siler. Geri alınamaz.",
       "deleteSelected": "Sil ({count})",
       "adjustSelected": "Ayarla ({count})",
+      "subLinksSelected": "Abonelik bağlantıları ({count})",
+      "addToGroupTitle": "{count} istemciyi bir gruba ekle",
+      "addToGroupTooltip": "Mevcut bir grubu seçin veya yeni ad girin. İstemcileri mevcut gruplarından çıkarmak için Ungroup'u kullanın.",
+      "addToGroupPlaceholder": "Grup adı",
+      "addToGroupSuccessToast": "{count} istemci {group} grubuna eklendi",
+      "ungroupSuccessToast": "{count} istemcinin grubu temizlendi",
+      "ungroup": "Gruptan çıkar",
+      "ungroupConfirmTitle": "{count} istemciyi gruptan çıkar?",
+      "ungroupConfirmContent": "Seçilen her istemcinin grup etiketini temizler. İstemciler korunur (tamamen kaldırmak için Delete kullanın).",
+      "addToGroup": "Gruba ekle",
+      "attach": "Bağla",
+      "adjust": "Ayarla",
+      "subLinks": "Abonelik bağlantıları",
+      "selectedCount": "{count} seçildi",
+      "attachSelected": "Bağla ({count})",
+      "attachToInboundsTitle": "{count} istemciyi gelen(ler)e bağla",
+      "attachToInboundsDesc": "Seçilen {count} istemciyi (aynı UUID/parola ve paylaşılan trafik) seçilen gelene bağlar. Mevcut bağlantılar korunur.",
+      "attachToInboundsTargets": "Hedef gelenler",
+      "attachToInboundsNoTargets": "Bağlanacak çoklu kullanıcılı gelen yok.",
+      "detachSelected": "Çöz ({count})",
+      "detach": "Çöz",
+      "detachFromInboundsTitle": "{count} istemciyi gelen(ler)den çöz",
+      "detachFromInboundsDesc": "Seçilen {count} istemciyi seçilen gelenden kaldırır. İstemcinin bağlı olmadığı çiftler sessizce atlanır. İstemci kayıtları korunur (tamamen kaldırmak için Delete kullanın).",
+      "detachFromInboundsTargets": "Çözülecek gelenler",
+      "detachFromInboundsNoTargets": "Çoklu kullanıcılı gelen yok.",
+      "detachFromInboundsResult": "Çözüldü {detached}, atlandı {skipped}.",
+      "detachFromInboundsResultMixed": "Çözüldü {detached}, atlandı {skipped}, hata {errors}.",
+      "subLinksTitle": "Abonelik bağlantıları ({count})",
+      "subLinkColumn": "Abonelik URL",
+      "subJsonLinkColumn": "Abonelik JSON URL",
+      "subLinksCopyAll": "Tümünü kopyala",
+      "subLinksCopiedAll": "{count} bağlantı kopyalandı",
+      "subLinksEmpty": "Seçilen istemcilerin hiçbirinin abonelik ID'si yok.",
+      "subLinksDisabled": "Abonelik hizmeti devre dışı.",
+      "subLinksDisabledHint": "Bağlantı oluşturmak için Panel Ayarları → Abonelik'ten etkinleştirin.",
       "bulkDeleteConfirmTitle": "{count} istemci silinsin mi?",
       "bulkDeleteConfirmContent": "Seçili her istemci bağlı tüm inbound'lardan kaldırılır ve trafik kaydı silinir. Geri alınamaz.",
       "bulkAdjustTitle": "{count} istemciyi ayarla",
@@ -508,6 +724,7 @@
       "hysteriaAuth": "Hysteria Auth",
       "uuid": "UUID",
       "flow": "Flow",
+      "vmessSecurity": "VMess Güvenlik",
       "reverseTag": "Reverse tag",
       "reverseTagPlaceholder": "İsteğe bağlı Reverse tag",
       "telegramId": "Telegram kullanıcı ID'si",
@@ -528,10 +745,48 @@
         "delDepleted": "{count} tükenmiş istemci silindi"
       }
     },
+    "groups": {
+      "title": "Gruplar",
+      "name": "İsim",
+      "clientCount": "Gruptaki istemciler",
+      "totalGroups": "Toplam grup",
+      "totalGroupedClients": "Grubu olan istemciler",
+      "emptyGroups": "Boş gruplar",
+      "addGroup": "Grup ekle",
+      "createSuccess": "«{name}» grubu oluşturuldu.",
+      "rename": "Yeniden adlandır",
+      "renameTitle": "{name} yeniden adlandır",
+      "renameCollision": "«{name}» adında bir grup zaten var.",
+      "renameSuccess": "{count} istemcinin grubu yeniden adlandırıldı.",
+      "deleteConfirmTitle": "{name} grubunu sil?",
+      "deleteConfirmContent": "Bu, grubu siler ve etiketini {count} istemciden temizler. İstemciler silinmez.",
+      "deleteSuccess": "{count} istemcinin grubu temizlendi.",
+      "resetTraffic": "Trafiği sıfırla",
+      "resetConfirmTitle": "{name} grubunun trafiğini sıfırla?",
+      "resetConfirmContent": "Bu, bu gruptaki tüm {count} istemcinin yukarı/aşağı trafiğini sıfırlar.",
+      "resetSuccess": "{count} istemcinin trafiği sıfırlandı.",
+      "adjustSuccess": "{name} içinde {count} istemci ayarlandı.",
+      "emptyForAction": "Bu grupta henüz istemci yok.",
+      "deleteGroupOnly": "Grubu sil (istemcileri tut)",
+      "deleteClients": "Gruptaki istemcileri sil",
+      "deleteClientsConfirmTitle": "{name} içindeki tüm istemcileri sil?",
+      "deleteClientsConfirmContent": "Bu, {count} istemciyi trafik kayıtlarıyla birlikte kalıcı olarak siler. Grup etiketi de temizlenir. Geri alınamaz.",
+      "deleteClientsSuccess": "{count} istemci silindi.",
+      "deleteClientsMixed": "{ok} silindi, {failed} atlandı",
+      "addToGroup": "İstemci ekle…",
+      "addToGroupTitle": "«{name}» grubuna istemci ekle",
+      "addToGroupDesc": "Bu gruba eklemek için istemcileri seçin. Mevcut gelen bağlantıları korunur; yalnızca grup etiketi değişir. Halihazırda bu grupta olan istemciler listelenmez.",
+      "addToGroupEmpty": "Eklenecek başka istemci yok.",
+      "addToGroupResult": "{count} istemci {name} grubuna eklendi.",
+      "removeFromGroup": "İstemci çıkar…",
+      "removeFromGroupTitle": "«{name}» grubundan istemci çıkar",
+      "removeFromGroupDesc": "Bu gruptan çıkarılacak üyeleri seçin. İstemciler korunur (tamamen kaldırmak için «Gruptaki istemcileri sil» kullanın).",
+      "removeFromGroupResult": "{name} grubundan {count} istemci çıkarıldı."
+    },
     "nodes": {
       "title": "Düğümler",
       "addNode": "Düğüm Ekle",
-      "editNode": "Düğümü Düzenle",
+      "editNode": "Düğümü düzenle",
       "totalNodes": "Toplam Düğüm",
       "onlineNodes": "Çevrimiçi",
       "offlineNodes": "Çevrimdışı",
@@ -543,7 +798,7 @@
       "scheme": "Şema",
       "address": "Adres",
       "port": "Port",
-      "basePath": "Temel Yol",
+      "basePath": "Base Path",
       "apiToken": "API Token",
       "apiTokenPlaceholder": "Uzak panelin Ayarlar sayfasındaki token",
       "apiTokenHint": "Uzak panel API token'ını Ayarlar → API Token altında gösterir.",
@@ -555,7 +810,7 @@
       "status": "Durum",
       "cpu": "CPU",
       "mem": "Bellek",
-      "uptime": "Çalışma Süresi",
+      "uptime": "Çalışma süresi",
       "latency": "Gecikme",
       "lastHeartbeat": "Son Sinyal",
       "xrayVersion": "Xray Sürümü",
@@ -590,7 +845,7 @@
       "title": "Panel Ayarları",
       "save": "Kaydet",
       "infoDesc": "Burada yapılan her değişikliğin kaydedilmesi gerekir. Değişikliklerin uygulanması için paneli yeniden başlatın.",
-      "restartPanel": "Paneli Yeniden Başlat",
+      "restartPanel": "Paneli yeniden başlat",
       "restartPanelDesc": "Paneli yeniden başlatmak istediğinizden emin misiniz? Yeniden başlattıktan sonra panele erişemezseniz, sunucudaki panel günlük bilgilerini görüntüleyin.",
       "restartPanelSuccess": "Panel başarıyla yeniden başlatıldı",
       "actions": "Eylemler",
@@ -615,10 +870,12 @@
       "publicKeyPathDesc": "Web paneli için genel anahtar dosya yolu. ('/' ile başlar)",
       "privateKeyPath": "Özel Anahtar Yolu",
       "privateKeyPathDesc": "Web paneli için özel anahtar dosya yolu. ('/' ile başlar)",
-      "panelUrlPath": "URI Yolu",
+      "panelUrlPath": "URI yolu",
       "panelUrlPathDesc": "Web paneli için URI yolu. ('/' ile başlar ve '/' ile biter)",
       "pageSize": "Sayfa Boyutu",
       "pageSizeDesc": "Gelenler tablosu için sayfa boyutunu belirleyin. (0 = devre dışı)",
+      "panelProxy": "Panel ağ proxy'si",
+      "panelProxyDesc": "Panelin kendi giden istekleri (geo güncellemeleri, Xray/panel sürüm kontrolleri, Telegram) bu proxy üzerinden yönlendirir; sunucu tarafındaki GitHub/Telegram filtrelemesini atlatmak için. socks5:// veya http(s):// kabul eder, örn. yerel bir Xray SOCKS geleni. Doğrudan bağlantı için boş bırakın.",
       "remarkModel": "Açıklama Modeli & Ayırma Karakteri",
       "datepicker": "Takvim Türü",
       "datepickerPlaceholder": "Tarih Seçin",
@@ -634,7 +891,7 @@
       "telegramTokenDesc": "'{'@'}BotFather'dan alınan Telegram bot token.",
       "telegramProxy": "SOCKS Proxy",
       "telegramProxyDesc": "Telegram'a bağlanmak için SOCKS5 proxy'sini etkinleştirir. (ayarları kılavuzda belirtilen şekilde ayarlayın)",
-      "telegramAPIServer": "Telegram API Server",
+      "telegramAPIServer": "Telegram API Sunucusu",
       "telegramAPIServerDesc": "Kullanılacak Telegram API sunucusu. Varsayılan sunucuyu kullanmak için boş bırakın.",
       "telegramChatId": "Yönetici Sohbet Kimliği",
       "telegramChatIdDesc": "Telegram Yönetici Sohbet Kimliği(leri). (virgülle ayrılmış)(buradan alın {'@'}userinfobot) veya (botta '/id' komutunu kullanın)",
@@ -658,6 +915,8 @@
       "subEnable": "Abonelik Hizmetini Etkinleştir",
       "subEnableDesc": "Abonelik hizmetini etkinleştirir.",
       "subJsonEnable": "JSON abonelik uç noktasını bağımsız olarak Etkinleştir/Devre Dışı bırak.",
+      "subJsonEnableTitle": "JSON aboneliği",
+      "subClashEnableTitle": "Clash / Mihomo aboneliği",
       "subTitle": "Abonelik Başlığı",
       "subTitleDesc": "VPN istemcisinde gösterilen başlık",
       "subSupportUrl": "Destek URL'si",
@@ -678,13 +937,13 @@
       "subCertPathDesc": "Abonelik hizmeti için genel anahtar dosya yolu. ('/' ile başlar)",
       "subKeyPath": "Özel Anahtar Yolu",
       "subKeyPathDesc": "Abonelik hizmeti için özel anahtar dosya yolu. ('/' ile başlar)",
-      "subPath": "URI Yolu",
+      "subPath": "URI yolu",
       "subPathDesc": "Abonelik hizmeti için URI yolu. ('/' ile başlar ve '/' ile biter)",
       "subDomain": "Dinleme Alan Adı",
       "subDomainDesc": "Abonelik hizmeti için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın)",
       "subUpdates": "Güncelleme Aralıkları",
       "subUpdatesDesc": "Müşteri uygulamalarındaki abonelik URL'sinin güncelleme aralıkları. (birim: saat)",
-      "subEncrypt": "Şifrele",
+      "subEncrypt": "Kodla",
       "subEncryptDesc": "Abonelik hizmetinin döndürülen içeriği Base64 ile şifrelenir.",
       "subShowInfo": "Kullanım Bilgisini Göster",
       "subShowInfoDesc": "Kalan trafik ve tarih müşteri uygulamalarında görüntülenir.",
@@ -693,7 +952,7 @@
       "subURI": "Ters Proxy URI",
       "subURIDesc": "Proxy arkasında kullanılacak abonelik URL'sinin URI yolu.",
       "externalTrafficInformEnable": "Harici Trafik Bilgisi",
-      "externalTrafficInformEnableDesc": "Her trafik güncellemesinde harici API'yi bilgilendirin.",
+      "externalTrafficInformEnableDesc": "Her trafik güncellemesinde harici API'yi bilgilendir.",
       "externalTrafficInformURI": "Harici Trafik Bilgisi URI'si",
       "externalTrafficInformURIDesc": "Trafik güncellemeleri bu URI'ye gönderildi.",
       "restartXrayOnClientDisable": "Otomatik Devre Dışı Sonrası Xray'i Yeniden Başlat",
@@ -703,6 +962,54 @@
       "fragmentSett": "Parçalama Ayarları",
       "noisesDesc": "Noises'i Etkinleştir.",
       "noisesSett": "Noises Ayarları",
+      "trustedProxyCidrs": "Güvenilir proxy CIDR'leri",
+      "trustedProxyCidrsDesc": "İletilen host, proto ve istemci IP başlıklarını ayarlamasına izin verilen IP'ler/CIDR'ler (virgülle ayrılmış).",
+      "ldap": {
+        "enable": "LDAP senkronizasyonunu etkinleştir",
+        "host": "LDAP host",
+        "port": "LDAP port",
+        "useTls": "TLS kullan (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "Yapılandırıldı; mevcut parolayı korumak için boş bırakın.",
+        "passwordUnconfigured": "Yapılandırılmadı.",
+        "passwordPlaceholder": "Yapılandırıldı — değiştirmek için yeni değer girin",
+        "baseDn": "Base DN",
+        "userFilter": "Kullanıcı filtresi",
+        "userAttr": "Kullanıcı özniteliği (username/email)",
+        "vlessField": "VLESS flag özniteliği",
+        "flagField": "Genel flag özniteliği (opsiyonel)",
+        "flagFieldDesc": "Ayarlanırsa VLESS flag'ini geçersiz kılar — örn. shadowInactive.",
+        "truthyValues": "Truthy değerler",
+        "truthyValuesDesc": "Virgülle ayrılmış; varsayılan: true,1,yes,on",
+        "invertFlag": "Flag'i tersine çevir",
+        "invertFlagDesc": "Öznitelik «devre dışı» anlamına geldiğinde etkinleştirin (örn. shadowInactive).",
+        "syncSchedule": "Senkronizasyon programı",
+        "syncScheduleDesc": "cron benzeri dize, örn. @every 1m",
+        "inboundTags": "Gelen etiketleri",
+        "inboundTagsDesc": "LDAP senkronizasyonunun istemci otomatik oluşturup/silebileceği gelenler.",
+        "noInbounds": "Gelen bulunamadı. Önce Gelenler'de bir tane oluşturun.",
+        "autoCreate": "İstemcileri otomatik oluştur",
+        "autoDelete": "İstemcileri otomatik sil",
+        "defaultTotalGb": "Varsayılan toplam (GB)",
+        "defaultExpiryDays": "Varsayılan son kullanma (gün)",
+        "defaultIpLimit": "Varsayılan IP limiti"
+      },
+      "subFormats": {
+        "packets": "Paketler",
+        "length": "Uzunluk",
+        "interval": "Aralık",
+        "maxSplit": "Maks. bölünme",
+        "noises": "Gürültüler",
+        "noiseItem": "Gürültü №{n}",
+        "type": "Tip",
+        "packet": "Paket",
+        "delayMs": "Gecikme (ms)",
+        "applyTo": "Şuna uygula",
+        "addNoise": "+ Gürültü",
+        "concurrency": "Eşzamanlılık",
+        "xudpConcurrency": "xudp eşzamanlılık",
+        "xudpUdp443": "xudp UDP 443"
+      },
       "mux": "Mux",
       "muxDesc": "Kurulmuş bir veri akışında birden çok bağımsız veri akışını iletir.",
       "muxSett": "Mux Ayarları",
@@ -756,8 +1063,11 @@
     "xray": {
       "title": "Xray Yapılandırmaları",
       "save": "Kaydet",
-      "restart": "Xray'i Yeniden Başlat",
+      "restart": "Xray'i yeniden başlat",
       "restartSuccess": "Xray başarıyla yeniden başlatıldı",
+      "restartOutputTitle": "Xray yeniden başlatma çıktısı",
+      "restartConfirmTitle": "Xray'i yeniden başlat?",
+      "restartConfirmContent": "Xray hizmeti kaydedilmiş yapılandırma ile yeniden yüklenir.",
       "stopSuccess": "Xray başarıyla durduruldu",
       "restartError": "Xray yeniden başlatılırken bir hata oluştu.",
       "stopError": "Xray durdurulurken bir hata oluştu.",
@@ -794,6 +1104,8 @@
       "InboundsDesc": "Belirli müşterileri kabul eder.",
       "Outbounds": "Gidenler",
       "Balancers": "Dengeler",
+      "balancerTagRequired": "Etiket gereklidir",
+      "balancerSelectorRequired": "En az bir giden seçin",
       "OutboundsDesc": "Giden trafiğin yolunu ayarlayın.",
       "Routings": "Yönlendirme Kuralları",
       "RoutingsDesc": "Her kuralın önceliği önemlidir!",
@@ -832,6 +1144,73 @@
         "edit": "Kuralı Düzenle",
         "useComma": "Virgülle ayrılmış öğeler"
       },
+      "routing": {
+        "dragToReorder": "Yeniden sıralamak için sürükleyin"
+      },
+      "ruleForm": {
+        "sourceIps": "Kaynak IP'ler",
+        "sourcePort": "Kaynak port",
+        "vlessRoute": "VLESS rotası",
+        "attributes": "Öznitelikler",
+        "value": "Değer",
+        "user": "Kullanıcı",
+        "inboundTags": "Gelen etiketleri",
+        "outboundTag": "Giden etiketi",
+        "balancerTag": "Dengeleyici etiketi",
+        "balancerTagTooltip": "Trafiği yapılandırılmış yük dengeleyicilerden biri üzerinden yönlendirir"
+      },
+      "outboundForm": {
+        "tagDuplicate": "Etiket başka bir giden tarafından kullanılıyor",
+        "tagRequired": "Etiket gereklidir",
+        "tagPlaceholder": "benzersiz-etiket",
+        "localIpPlaceholder": "yerel IP",
+        "addressRequired": "Adres gereklidir",
+        "portRequired": "Port gereklidir",
+        "optional": "opsiyonel",
+        "udpOverTcp": "UDP over TCP",
+        "uotVersion": "UoT sürümü",
+        "inboundTag": "Gelen etiketi",
+        "inboundTagPlaceholder": "yönlendirme kurallarında kullanılan gelen etiketi",
+        "responseType": "Yanıt tipi",
+        "rewriteNetwork": "Ağı yeniden yaz",
+        "unchanged": "(değişmedi)",
+        "unchangedAddress": "(değişmedi) örn. 1.1.1.1",
+        "rules": "Kurallar",
+        "ruleN": "Kural {n}",
+        "action": "Eylem",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "Nihai kurallar",
+        "overrideXrayPrivateIp": "Xray'in varsayılan özel IP bloğunu geçersiz kıl",
+        "blockDelay": "Engelleme gecikmesi (ms)",
+        "reverseSniffing": "Ters sniffing",
+        "workers": "Workers",
+        "reserved": "Ayrılmış",
+        "minUploadInterval": "Min. yükleme aralığı (ms)",
+        "maxUploadSizeBytes": "Maks. yükleme boyutu (bayt)",
+        "uplinkChunkSize": "Uplink chunk boyutu",
+        "noGrpcHeader": "gRPC başlığı yok",
+        "maxConcurrency": "Maks. eşzamanlılık",
+        "maxConnections": "Maks. bağlantı",
+        "maxReuseTimes": "Maks. yeniden kullanım",
+        "maxRequestTimes": "Maks. istek sayısı",
+        "maxReusableSecs": "Maks. yeniden kullanılabilir saniye",
+        "keepAlivePeriod": "Keep alive periyodu",
+        "authPassword": "Auth parolası",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "sunucu adı",
+        "verifyPeerName": "Peer adını doğrula",
+        "pinnedSha256": "Pinned SHA256",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "Keep alive aralığı",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "Arabirim",
+        "ipv6Only": "Yalnızca IPv6",
+        "acceptProxyProtocol": "Proxy protocol kabul et",
+        "tcpUserTimeoutMs": "TCP user timeout (ms)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
+      },
       "outbound": {
         "addOutbound": "Giden Ekle",
         "addReverse": "Ters Ekle",
@@ -844,9 +1223,9 @@
         "tagDesc": "Benzersiz Etiket",
         "address": "Adres",
         "reverse": "Ters",
-        "domain": "Alan Adı",
+        "domain": "Alan adı",
         "type": "Tür",
-        "bridge": "Köprü",
+        "bridge": "Bridge",
         "portal": "Portal",
         "link": "Bağlantı",
         "intercon": "Bağlantı",
@@ -860,6 +1239,8 @@
         "testSuccess": "Test başarılı",
         "testFailed": "Test başarısız",
         "testError": "Giden test edilemedi",
+        "testModeTooltip": "TCP: hızlı dial-only probe. HTTP: xray üzerinden tam istek.",
+        "testAll": "Tümünü test et",
         "nordvpn": "NordVPN",
         "accessToken": "Erişim Jetonu",
         "country": "Ülke",
@@ -876,6 +1257,16 @@
         "balancerSelectors": "Seçiciler",
         "tag": "Etiket",
         "tagDesc": "Benzersiz Etiket",
+        "tagDuplicate": "Etiket başka bir dengeleyici tarafından kullanılıyor",
+        "tagPlaceholder": "benzersiz dengeleyici etiketi",
+        "selector": "Seçici",
+        "fallback": "Fallback",
+        "expected": "Beklenen",
+        "expectedPlaceholder": "optimal düğüm sayısı",
+        "maxRtt": "Maks. RTT",
+        "tolerance": "Tolerans",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "Dengeleyici Etiketi ve Giden Etiketi aynı anda kullanılamaz. Aynı anda kullanıldığında yalnızca giden etiketi çalışır."
       },
       "wireguard": {
@@ -892,6 +1283,38 @@
         "userLevel": "Kullanıcı Seviyesi",
         "userLevelDesc": "Bu giriş yoluyla yapılan tüm bağlantılar bu kullanıcı seviyesini kullanacaktır. Varsayılan değer 0'dır"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "Özel anahtar",
+        "noServers": "Seçilen ülke için sunucu bulunamadı",
+        "noPublicKey": "Seçilen sunucu NordLynx genel anahtarı yayınlamıyor.",
+        "outboundAdded": "NordVPN giden eklendi",
+        "outboundUpdated": "NordVPN giden güncellendi"
+      },
+      "warp": {
+        "licenseError": "WARP lisansı ayarlanamadı.",
+        "fetchFirst": "Önce WARP yapılandırmasını alın.",
+        "createAccount": "WARP hesabı oluştur",
+        "accessToken": "Access token",
+        "deviceId": "Cihaz ID",
+        "licenseKey": "Lisans anahtarı",
+        "privateKey": "Özel anahtar",
+        "deleteAccount": "Hesabı sil",
+        "settings": "Ayarlar",
+        "licenseKeyLabel": "WARP / WARP+ lisans anahtarı",
+        "key": "Anahtar",
+        "keyPlaceholder": "26 karakterli WARP+ anahtarı",
+        "accountInfo": "Hesap bilgisi",
+        "deviceName": "Cihaz adı",
+        "deviceModel": "Cihaz modeli",
+        "deviceEnabled": "Cihaz etkin",
+        "accountType": "Hesap tipi",
+        "role": "Rol",
+        "warpPlusData": "WARP+ veri",
+        "quota": "Kota",
+        "usage": "Kullanım",
+        "addOutbound": "Giden ekle"
+      },
       "dns": {
         "enable": "DNS'yi Etkinleştir",
         "enableDesc": "Dahili DNS sunucusunu etkinleştir",
@@ -911,7 +1334,7 @@
         "strategyDesc": "Alan adlarını çözmek için genel strateji",
         "add": "Sunucu Ekle",
         "edit": "Sunucuyu Düzenle",
-        "domains": "Alan Adları",
+        "domains": "Alan adları",
         "expectIPs": "Beklenen IP'ler",
         "unexpectIPs": "Beklenmeyen IP'ler",
         "useSystemHosts": "Sistem Hosts'larını Kullan",
@@ -992,7 +1415,7 @@
       "2faFailed": "2FA Hatası",
       "report": "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n",
       "datetime": "⏰ Tarih&Zaman: {{ .DateTime }}\r\n",
-      "hostname": "💻 Sunucu: {{ .Hostname }}\r\n",
+      "hostname": "💻 Host: {{ .Hostname }}\r\n",
       "version": "🚀 3X-UI Sürümü: {{ .Version }}\r\n",
       "xrayVersion": "📡 Xray Sürümü: {{ .XrayVersion }}\r\n",
       "ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
@@ -1017,7 +1440,7 @@
       "enabled": "🚨 Etkin: {{ .Enable }}\r\n",
       "online": "🌐 Bağlantı durumu: {{ .Status }}\r\n",
       "lastOnline": "🔙 Son çevrimiçi: {{ .Time }}\r\n",
-      "email": "📧 E-posta: {{ .Email }}\r\n",
+      "email": "📧 Email: {{ .Email }}\r\n",
       "upload": "🔼 Yükleme: ↑{{ .Upload }}\r\n",
       "download": "🔽 İndirme: ↓{{ .Download }}\r\n",
       "total": "📊 Toplam: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
@@ -1087,11 +1510,11 @@
       "submitDisable": "Devre Dışı Olarak Gönder ☑️",
       "submitEnable": "Etkin Olarak Gönder ✅",
       "use_default": "🏷️ Varsayılanı Kullan",
-      "change_id": "⚙️🔑 Kimlik",
+      "change_id": "⚙️🔑 ID",
       "change_password": "⚙️🔑 Şifre",
-      "change_email": "⚙️📧 E-posta",
+      "change_email": "⚙️📧 Email",
       "change_comment": "⚙️💬 Yorum",
-      "change_flow": "⚙️🚦 Akış",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "Tüm Trafikleri Sıfırla",
       "SortedTrafficUsageReport": "Sıralı Trafik Kullanım Raporu"
     },
@@ -1119,4 +1542,4 @@
       "chooseInbound": "Bir Gelen Seçin"
     }
   }
-}
+}

+ 480 - 57
web/translation/uk-UA.json

@@ -8,15 +8,22 @@
   "save": "Зберегти",
   "logout": "Вийти",
   "create": "Створити",
+  "add": "Додати",
+  "remove": "Видалити",
   "update": "Оновити",
   "copy": "Копіювати",
   "copied": "Скопійовано",
+  "more": "більше",
   "download": "Завантажити",
   "remark": "Примітка",
   "enable": "Увімкнути",
   "protocol": "Протокол",
   "search": "Пошук",
   "filter": "Фільтр",
+  "all": "Усі",
+  "from": "Від",
+  "to": "До",
+  "done": "Готово",
   "loading": "Завантаження...",
   "refresh": "Оновити",
   "clear": "Очистити",
@@ -30,7 +37,7 @@
   "none": "Немає",
   "qrCode": "QR-Код",
   "info": "Більше інформації",
-  "edit": "Редагувати",
+  "edit": "Змінити",
   "delete": "Видалити",
   "reset": "Скидання",
   "noData": "Немає даних.",
@@ -41,14 +48,14 @@
   "transmission": "Протокол передачи",
   "host": "Хост",
   "path": "Шлях",
-  "camouflage": "Маскування",
+  "camouflage": "Обфускація",
   "status": "Статус",
   "enabled": "Увімкнено",
   "disabled": "Вимкнено",
   "depleted": "Вичерпано",
   "depletingSoon": "Вичерпується",
-  "offline": "Офлайн",
-  "online": "Онлайн",
+  "offline": "Не в мережі",
+  "online": "У мережі",
   "domainName": "Доменне ім`я",
   "monitor": "Слухати IP",
   "certificate": "Цифровий сертифікат",
@@ -97,8 +104,9 @@
     "dashboard": "Огляд",
     "inbounds": "Вхідні",
     "clients": "Клієнти",
+    "groups": "Групи",
     "nodes": "Вузли",
-    "settings": "Параметри панелі",
+    "settings": "Налаштування панелі",
     "xray": "Конфігурації Xray",
     "apiDocs": "Документація API",
     "logout": "Вийти",
@@ -120,16 +128,16 @@
     },
     "index": {
       "title": "Огляд",
-      "cpu": "ЦП",
+      "cpu": "CPU",
       "logicalProcessors": "Логічні процесори",
       "frequency": "Частота",
-      "swap": "Своп",
+      "swap": "Swap",
       "storage": "Сховище",
-      "memory": "ОЗП",
+      "memory": "RAM",
       "threads": "Потоки",
       "xrayStatus": "Xray",
-      "stopXray": "Зупинити",
-      "restartXray": "Перезапустити",
+      "stopXray": "Стоп",
+      "restartXray": "Перезапуск",
       "xraySwitch": "Версія",
       "xrayUpdates": "Оновлення Xray",
       "xraySwitchClick": "Виберіть версію, на яку ви хочете перейти.",
@@ -165,8 +173,8 @@
       "ipAddresses": "IP-адреси",
       "toggleIpVisibility": "Перемкнути видимість IP",
       "overallSpeed": "Загальна швидкість",
-      "upload": "Відправка",
-      "download": "Завантаження",
+      "upload": "Завантаження",
+      "download": "Завантажити",
       "totalData": "Загальний обсяг даних",
       "sent": "Відправлено",
       "received": "Отримано",
@@ -226,7 +234,7 @@
       "customGeoErrUpdateAllIncomplete": "Не вдалося оновити один або кілька користувацьких джерел",
       "customGeoEmpty": "Користувацьких джерел geo поки немає — натисніть «Додати», щоб створити",
       "dontRefresh": "Інсталяція триває, будь ласка, не оновлюйте цю сторінку",
-      "logs": "Журнали",
+      "logs": "Логи",
       "config": "Конфігурація",
       "backup": "Резервна копія",
       "backupTitle": "Резервне копіювання та відновлення",
@@ -252,7 +260,7 @@
       "deployTo": "Розгорнути на",
       "localPanel": "Локальна панель",
       "fallbacks": {
-        "title": "Фолбеки",
+        "title": "Fallback'и",
         "help": "Коли з'єднання на цьому інбаунді не збігається з жодним клієнтом, воно перенаправляється на інший інбаунд. Оберіть дочірній інбаунд нижче — поля маршрутизації (SNI / ALPN / Path / xver) заповняться автоматично з його транспорту; для більшості налаштувань більше нічого змінювати не треба. Кожен дочірній має слухати на 127.0.0.1 з security=none.",
         "empty": "Фолбеків поки немає",
         "add": "Додати фолбек",
@@ -270,7 +278,7 @@
       },
       "protocol": "Протокол",
       "port": "Порт",
-      "portMap": "Порт-перехід",
+      "portMap": "Відображення портів",
       "traffic": "Трафік",
       "details": "Деталі",
       "transportConfig": "Транспорт",
@@ -292,11 +300,31 @@
       "delAllClients": "Видалити всіх клієнтів",
       "delAllClientsConfirmTitle": "Видалити всіх {count} клієнтів із \"{remark}\"?",
       "delAllClientsConfirmContent": "Видаляє всіх клієнтів цього вхідного й скидає їхні записи трафіку. Сам вхідний зберігається. Цю дію не можна скасувати.",
+      "attachClients": "Прив'язати клієнтів до…",
+      "addClientsToGroup": "Додати клієнтів до групи…",
+      "attachClientsTitle": "Прив'язати клієнтів з «{remark}»",
+      "attachClientsDesc": "Прив'язує тих самих {count} клієнт(ів) (з тим самим UUID/паролем і спільним трафіком) до обраних вхідних. Вони залишаються і на цьому вхідному.",
+      "attachClientsTargets": "Цільові вхідні",
+      "attachClientsNoTargets": "Немає інших сумісних вхідних для прив'язки.",
+      "attachClientsResult": "Прив'язано {attached}, пропущено {skipped}.",
+      "attachClientsResultMixed": "Прив'язано {attached}, пропущено {skipped}, помилок {errors}.",
+      "attachClientsSelectLabel": "Клієнти для прив'язки",
+      "attachClientsSearchPlaceholder": "Пошук email або коментаря",
+      "attachClientsStatusDisabled": "Вимкнено",
+      "attachClientsSelectedCount": "Обрано {selected} з {total}",
+      "detachClients": "Від'єднати клієнтів",
+      "detachClientsTitle": "Від'єднати клієнтів з «{remark}»",
+      "detachClientsDesc": "Видаляє обраних клієнт(ів) лише з цього вхідного. Записи клієнтів зберігаються (використовуйте Delete для повного видалення). У джерела всього {count} клієнт(ів).",
+      "detachClientsResult": "Від'єднано {detached}, пропущено {skipped}.",
+      "detachClientsResultMixed": "Від'єднано {detached}, пропущено {skipped}, помилок {errors}.",
+      "detachClientsSelectLabel": "Клієнти для від'єднання",
       "exportLinksTitle": "Експортувати посилання вхідних",
       "exportSubsTitle": "Експортувати посилання підписок",
       "exportAllLinksTitle": "Експортувати всі посилання вхідних",
       "exportAllSubsTitle": "Експортувати всі посилання підписок",
-      "inboundJsonTitle": "JSON вхідних",
+      "exportAllLinksFileName": "Усі-вхідні",
+      "exportAllSubsFileName": "Усі-вхідні-Subs",
+      "inboundJsonTitle": "JSON вхідного",
       "deleteClient": "Видалити клієнта",
       "deleteClientContent": "Ви впевнені, що хочете видалити клієнт?",
       "resetTrafficContent": "Ви впевнені, що хочете скинути трафік?",
@@ -306,7 +334,7 @@
       "destinationPort": "Порт призначення",
       "targetAddress": "Цільова адреса",
       "monitorDesc": "Залиште порожнім, щоб слухати всі IP-адреси",
-      "meansNoLimit": "= Необмежено. (одиниця: ГБ)",
+      "meansNoLimit": "= Без обмежень. (одиниця: ГБ)",
       "totalFlow": "Загальна витрата",
       "leaveBlankToNeverExpire": "Залиште порожнім, щоб ніколи не закінчувався",
       "noRecommendKeepDefault": "Рекомендується зберегти значення за замовчуванням",
@@ -333,7 +361,7 @@
       "delDepletedClients": "Видалити вичерпані клієнти",
       "delDepletedClientsTitle": "Видалити вичерпані клієнти",
       "delDepletedClientsContent": "Ви впевнені, що хочете видалити всі вичерпані клієнти?",
-      "email": "Електронна пошта",
+      "email": "Email",
       "emailDesc": "Будь ласка, надайте унікальну адресу електронної пошти.",
       "IPLimit": "Обмеження IP",
       "IPLimitDesc": "Вимикає вхідний, якщо кількість перевищує встановлене значення. (0 = вимкнено)",
@@ -341,6 +369,7 @@
       "IPLimitlogDesc": "Журнал історії IP-адрес. (щоб увімкнути вхідну після вимкнення, очистіть журнал)",
       "IPLimitlogclear": "Очистити журнал",
       "setDefaultCert": "Установити сертифікат з панелі",
+      "setDefaultCertEmpty": "Для панелі не налаштовано сертифікат. Спочатку встановіть його в Налаштуваннях.",
       "streamTab": "Потік",
       "securityTab": "Безпека",
       "sniffingTab": "Сніфінг",
@@ -361,15 +390,14 @@
         "allHelp": "Повний об'єкт вхідного з усіма полями в одному редакторі.",
         "settings": "Налаштування",
         "settingsHelp": "Обгортка блоку settings Xray:",
-        "sniffing": "Сніфінг",
+        "sniffing": "Sniffing",
         "sniffingHelp": "Обгортка блоку sniffing Xray:",
-        "stream": "Потік",
+        "stream": "Stream",
         "streamHelp": "Обгортка блоку stream Xray:",
         "jsonErrorPrefix": "Розширений JSON"
       },
       "telegramDesc": "Будь ласка, вкажіть ID чату Telegram. (використовуйте команду '/id' у боті) або ({'@'}userinfobot)",
       "subscriptionDesc": "Щоб знайти URL-адресу вашої підписки, перейдіть до «Деталі». Крім того, ви можете використовувати одне ім'я для кількох клієнтів.",
-      "info": "Інформація",
       "same": "Те саме",
       "inboundData": "Вхідні дані",
       "exportInbound": "Експортувати вхідні",
@@ -406,6 +434,143 @@
         "getNewmldsa65Error": "Помилка при отриманні сертифіката mldsa65.",
         "getNewVlessEncError": "Помилка при отриманні сертифіката VlessEnc."
       },
+      "form": {
+        "moveUp": "Вгору",
+        "moveDown": "Вниз",
+        "addAll": "Додати всі",
+        "addAllFallbackTooltip": "Додає рядок fallback для кожного придатного вхідного, ще не приєднаного",
+        "peers": "Peers",
+        "addPeer": "Додати peer",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "Лише для Windows. CIDR'и автоматично додаються до системної таблиці маршрутизації, щоб відповідний трафік проходив через TUN.",
+        "autoOutboundsInterface": "Авто-інтерфейс вихідних",
+        "autoOutboundsInterfaceTooltip": "Фізичний інтерфейс для вихідного трафіку. Використовуйте 'auto' для виявлення; вмикається автоматично, коли налаштовано Auto system routes.",
+        "rewriteAddress": "Переписати адресу",
+        "rewritePort": "Переписати порт",
+        "allowedNetwork": "Дозволена мережа",
+        "followRedirect": "Слідувати redirect",
+        "accounts": "Акаунти",
+        "allowTransparent": "Дозволити прозорий",
+        "encryptionMethod": "Метод шифрування",
+        "visionTestseed": "Vision testseed",
+        "version": "Версія",
+        "udpIdleTimeout": "UDP idle timeout (с)",
+        "masquerade": "Masquerade",
+        "type": "Тип",
+        "upstreamUrl": "Upstream URL",
+        "rewriteHost": "Переписати Host",
+        "skipTlsVerify": "Пропустити TLS verify",
+        "directory": "Каталог",
+        "statusCode": "Код статусу",
+        "body": "Body",
+        "headers": "Заголовки",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "Версія запиту",
+        "requestMethod": "Метод запиту",
+        "requestPath": "Шлях запиту",
+        "requestHeaders": "Заголовки запиту",
+        "responseVersion": "Версія відповіді",
+        "responseStatus": "Статус відповіді",
+        "responseReason": "Причина відповіді",
+        "responseHeaders": "Заголовки відповіді",
+        "heartbeatPeriod": "Період heartbeat",
+        "serviceName": "Назва сервісу",
+        "authority": "Authority",
+        "multiMode": "Multi Mode",
+        "maxBufferedUpload": "Макс. буферизоване завантаження",
+        "maxUploadSize": "Макс. розмір завантаження (байт)",
+        "streamUpServer": "Stream-Up Server",
+        "serverMaxHeaderBytes": "Server Max Header Bytes",
+        "paddingBytes": "Padding Bytes",
+        "uplinkHttpMethod": "HTTP-метод Uplink",
+        "paddingObfsMode": "Padding Obfs Mode",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "Padding Placement",
+        "paddingMethod": "Padding Method",
+        "sessionPlacement": "Session Placement",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence Placement",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink Data Placement",
+        "uplinkDataKey": "Uplink Data Key",
+        "noSseHeader": "Без заголовка SSE",
+        "ttiMs": "TTI (мс)",
+        "uplinkMbps": "Uplink (МБ/с)",
+        "downlinkMbps": "Downlink (МБ/с)",
+        "cwndMultiplier": "Множник CWND",
+        "maxSendingWindow": "Макс. вікно відправки",
+        "externalProxy": "External Proxy",
+        "sniPlaceholder": "SNI (за замовчуванням = host)",
+        "fingerprint": "Fingerprint",
+        "defaultOption": "За замовчуванням",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive Interval",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "Лише V6",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "Довірений X-Forwarded-For",
+        "addressPortStrategy": "Стратегія адрес+порт",
+        "tryDelayMs": "Затримка спроби (мс)",
+        "prioritizeIPv6": "Пріоритет IPv6",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "Макс. одночасних спроб",
+        "customSockopt": "Користувацький sockopt",
+        "addCustomOption": "Додати опцію",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "Авто",
+        "minMaxVersion": "Мін/Макс версія",
+        "rejectUnknownSni": "Відхиляти невідомий SNI",
+        "disableSystemRoot": "Вимкнути System Root",
+        "sessionResumption": "Відновлення сесії",
+        "oneTimeLoading": "Одноразове завантаження",
+        "usageOption": "Опція використання",
+        "buildChain": "Build Chain",
+        "echKey": "ECH key",
+        "echConfig": "ECH config",
+        "pinnedPeerCertSha256": "Закріплений SHA-256 сертифіката пира",
+        "pinnedPeerCertSha256Tip": "SHA-256-хеші сертифіката пира в кодуванні Base64. Лише панель — не записується в конфіг xray сервера, але додається до посилань спільного доступу, щоб клієнти могли закріпити сертифікат.",
+        "pinnedPeerCertSha256Placeholder": "Base64-хеш(і), через кому",
+        "generateRandomPin": "Згенерувати випадковий хеш",
+        "getNewEchCert": "Отримати новий ECH-сертифікат",
+        "show": "Показати",
+        "xver": "Xver",
+        "target": "Ціль",
+        "maxTimeDiff": "Макс. різниця в часі (мс)",
+        "minClientVer": "Мін. версія клієнта",
+        "maxClientVer": "Макс. версія клієнта",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "Отримати новий сертифікат",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "Отримати новий Seed"
+      },
+      "info": {
+        "mode": "Режим",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "Назва інтерфейсу",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "Інтерфейс вихідних",
+        "autoSystemRoutes": "Авто-маршрути системи",
+        "followRedirect": "FollowRedirect",
+        "auth": "Auth",
+        "noKernelTun": "TUN без kernel",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "Конфіг Peer {n}"
+      },
       "stream": {
         "general": {
           "request": "Запит",
@@ -456,6 +621,20 @@
       "days": "Дні",
       "renew": "Авто-продовження",
       "renewDesc": "Автоматичне продовження після закінчення. (0 = вимкнено) (одиниця: день)",
+      "searchPlaceholder": "Пошук email, коментаря, sub ID, UUID, паролю, auth…",
+      "filterTitle": "Фільтр клієнтів",
+      "clearAllFilters": "Очистити все",
+      "sortOldest": "Спочатку старі",
+      "sortNewest": "Спочатку нові",
+      "sortRecentlyUpdated": "Нещодавно оновлені",
+      "sortRecentlyOnline": "Нещодавно у мережі",
+      "sortEmailAZ": "Email А→Я",
+      "sortEmailZA": "Email Я→А",
+      "sortMostTraffic": "Більше трафіку",
+      "sortHighestRemaining": "Більше залишку",
+      "sortExpiringSoonest": "Швидше закінчуються",
+      "has": "Має",
+      "hasNot": "Не має",
       "title": "Клієнти",
       "actions": "Дії",
       "totalGB": "Усього надіслано/отримано (ГБ)",
@@ -466,6 +645,9 @@
       "subId": "ID підписки",
       "online": "У мережі",
       "email": "Email",
+      "group": "Група",
+      "groupDesc": "Логічна мітка для групування пов'язаних клієнтів (напр. команда, клієнт, регіон). Фільтрується з панелі інструментів.",
+      "groupPlaceholder": "напр. customer-a",
       "comment": "Коментар",
       "traffic": "Трафік",
       "offline": "Не в мережі",
@@ -489,11 +671,45 @@
       "resetAllTraffics": "Скинути трафік усіх клієнтів",
       "resetAllTrafficsTitle": "Скинути трафік усіх клієнтів?",
       "resetAllTrafficsContent": "Лічильники відправлення/отримання кожного клієнта обнулюються. Квоти й термін дії не змінюються. Цю дію неможливо скасувати.",
-      "empty": "Клієнтів ще немає — додайте першого, щоб почати.",
       "deleteConfirmTitle": "Видалити клієнта {email}?",
       "deleteConfirmContent": "Клієнт буде вилучений з усіх прив'язаних вхідних, його запис трафіку буде знищено. Цю дію неможливо скасувати.",
       "deleteSelected": "Видалити ({count})",
       "adjustSelected": "Змінити ({count})",
+      "subLinksSelected": "Sub-посилання ({count})",
+      "addToGroupTitle": "Додати {count} клієнт(ів) до групи",
+      "addToGroupTooltip": "Виберіть існуючу групу або введіть нову назву. Використовуйте Ungroup, щоб вилучити клієнтів із поточної групи.",
+      "addToGroupPlaceholder": "Назва групи",
+      "addToGroupSuccessToast": "{count} клієнт(ів) додано до {group}",
+      "ungroupSuccessToast": "Групу очищено у {count} клієнт(ів)",
+      "ungroup": "Розгрупувати",
+      "ungroupConfirmTitle": "Видалити {count} клієнт(ів) з їхньої групи?",
+      "ungroupConfirmContent": "Очищує мітку групи у кожного обраного клієнта. Самі клієнти зберігаються (використовуйте Delete для повного видалення).",
+      "addToGroup": "Додати до групи",
+      "attach": "Прив'язати",
+      "adjust": "Коригування",
+      "subLinks": "Sub-посилання",
+      "selectedCount": "Обрано {count}",
+      "attachSelected": "Прив'язати ({count})",
+      "attachToInboundsTitle": "Прив'язати {count} клієнт(ів) до вхідних",
+      "attachToInboundsDesc": "Прив'язує обрані {count} клієнт(ів) (той самий UUID/пароль і спільний трафік) до обраних вхідних. Існуючі прив'язки зберігаються.",
+      "attachToInboundsTargets": "Цільові вхідні",
+      "attachToInboundsNoTargets": "Немає доступних багатокористувацьких вхідних для прив'язки.",
+      "detachSelected": "Від'єднати ({count})",
+      "detach": "Від'єднати",
+      "detachFromInboundsTitle": "Від'єднати {count} клієнт(ів) від вхідних",
+      "detachFromInboundsDesc": "Видаляє обраних {count} клієнт(ів) з обраних вхідних. Пари, де клієнт не був прив'язаний, тихо пропускаються. Записи клієнтів зберігаються (використовуйте Delete для повного видалення).",
+      "detachFromInboundsTargets": "Вхідні для від'єднання",
+      "detachFromInboundsNoTargets": "Немає доступних багатокористувацьких вхідних.",
+      "detachFromInboundsResult": "Від'єднано {detached}, пропущено {skipped}.",
+      "detachFromInboundsResultMixed": "Від'єднано {detached}, пропущено {skipped}, помилок {errors}.",
+      "subLinksTitle": "Sub-посилання ({count})",
+      "subLinkColumn": "URL підписки",
+      "subJsonLinkColumn": "URL JSON-підписки",
+      "subLinksCopyAll": "Копіювати все",
+      "subLinksCopiedAll": "Скопійовано {count} посилань",
+      "subLinksEmpty": "Жоден з обраних клієнтів не має ID підписки.",
+      "subLinksDisabled": "Сервіс підписки вимкнено.",
+      "subLinksDisabledHint": "Увімкніть підписку в Налаштування панелі → Підписка для генерації посилань.",
       "bulkDeleteConfirmTitle": "Видалити {count} клієнтів?",
       "bulkDeleteConfirmContent": "Кожен вибраний клієнт вилучається з усіх прив'язаних вхідних, його запис трафіку знищується. Цю дію неможливо скасувати.",
       "bulkAdjustTitle": "Змінити {count} клієнтів",
@@ -504,11 +720,12 @@
       "delDepleted": "Видалити вичерпаних",
       "delDepletedConfirmTitle": "Видалити вичерпаних клієнтів?",
       "delDepletedConfirmContent": "Видаляються всі клієнти, у яких вичерпана квота трафіку або сплив термін. Цю дію неможливо скасувати.",
-      "auth": "Auth",
-      "hysteriaAuth": "Auth для Hysteria",
+      "auth": "Авторизація",
+      "hysteriaAuth": "Hysteria Auth",
       "uuid": "UUID",
       "flow": "Flow",
-      "reverseTag": "Reverse tag",
+      "vmessSecurity": "Безпека VMess",
+      "reverseTag": "Зворотний тег",
       "reverseTagPlaceholder": "Необов'язковий Reverse tag",
       "telegramId": "ID користувача Telegram",
       "telegramIdPlaceholder": "Числовий ID користувача Telegram (0 = немає)",
@@ -528,13 +745,51 @@
         "delDepleted": "Видалено вичерпаних клієнтів: {count}"
       }
     },
+    "groups": {
+      "title": "Групи",
+      "name": "Назва",
+      "clientCount": "Клієнтів у групі",
+      "totalGroups": "Всього груп",
+      "totalGroupedClients": "Клієнти з групою",
+      "emptyGroups": "Порожні групи",
+      "addGroup": "Додати групу",
+      "createSuccess": "Групу «{name}» створено.",
+      "rename": "Перейменувати",
+      "renameTitle": "Перейменувати {name}",
+      "renameCollision": "Група з назвою «{name}» вже існує.",
+      "renameSuccess": "Групу перейменовано на {count} клієнт(ах).",
+      "deleteConfirmTitle": "Видалити групу {name}?",
+      "deleteConfirmContent": "Це видаляє групу й очищує її мітку у {count} клієнт(ів). Самі клієнти не видаляються.",
+      "deleteSuccess": "Групу очищено у {count} клієнт(ів).",
+      "resetTraffic": "Скинути трафік",
+      "resetConfirmTitle": "Скинути трафік групи {name}?",
+      "resetConfirmContent": "Це обнулить up/down для всіх {count} клієнт(ів) у цій групі.",
+      "resetSuccess": "Скинуто трафік у {count} клієнт(ів).",
+      "adjustSuccess": "Скориговано {count} клієнт(ів) у {name}.",
+      "emptyForAction": "У цій групі ще немає клієнтів.",
+      "deleteGroupOnly": "Видалити групу (зберегти клієнтів)",
+      "deleteClients": "Видалити клієнтів групи",
+      "deleteClientsConfirmTitle": "Видалити всіх клієнтів у {name}?",
+      "deleteClientsConfirmContent": "Це безповоротно видалить {count} клієнт(ів) разом з їхніми записами трафіку. Мітка групи також очищується. Дію не можна скасувати.",
+      "deleteClientsSuccess": "Видалено {count} клієнт(ів).",
+      "deleteClientsMixed": "{ok} видалено, {failed} пропущено",
+      "addToGroup": "Додати клієнтів…",
+      "addToGroupTitle": "Додати клієнтів до групи «{name}»",
+      "addToGroupDesc": "Виберіть клієнтів для додавання в цю групу. Існуючі прив'язки до вхідних зберігаються; змінюється лише мітка групи. Клієнти, які вже в цій групі, не відображаються.",
+      "addToGroupEmpty": "Немає інших клієнтів для додавання.",
+      "addToGroupResult": "Додано {count} клієнт(ів) до {name}.",
+      "removeFromGroup": "Видалити клієнтів…",
+      "removeFromGroupTitle": "Видалити клієнтів з групи «{name}»",
+      "removeFromGroupDesc": "Виберіть учасників для видалення з цієї групи. Самі клієнти зберігаються (використовуйте «Видалити клієнтів групи» для повного видалення).",
+      "removeFromGroupResult": "Видалено {count} клієнт(ів) з {name}."
+    },
     "nodes": {
       "title": "Вузли",
       "addNode": "Додати вузол",
-      "editNode": "Редагувати вузол",
+      "editNode": "Змінити вузол",
       "totalNodes": "Усього вузлів",
-      "onlineNodes": "Онлайн",
-      "offlineNodes": "Офлайн",
+      "onlineNodes": "У мережі",
+      "offlineNodes": "Не в мережі",
       "avgLatency": "Середня затримка",
       "name": "Назва",
       "namePlaceholder": "напр. de-frankfurt-1",
@@ -544,7 +799,7 @@
       "address": "Адреса",
       "port": "Порт",
       "basePath": "Базовий шлях",
-      "apiToken": "Токен API",
+      "apiToken": "API Token",
       "apiTokenPlaceholder": "Токен зі сторінки Налаштувань віддаленої панелі",
       "apiTokenHint": "Віддалена панель показує свій токен API в Налаштуваннях → Токен API.",
       "regenerate": "Перегенерувати токен",
@@ -570,8 +825,8 @@
       "deleteConfirmTitle": "Видалити вузол \"{name}\"?",
       "deleteConfirmContent": "Це зупинить моніторинг вузла. Сама віддалена панель не зазнає змін.",
       "statusValues": {
-        "online": "Онлайн",
-        "offline": "Офлайн",
+        "online": "У мережі",
+        "offline": "Не в мережі",
         "unknown": "Невідомо"
       },
       "toasts": {
@@ -604,7 +859,7 @@
       "warnDefaultBasePath": "Базовий шлях за замовчуванням \"/\" широко відомий — змініть його на випадковий.",
       "warnDefaultSubPath": "Шлях підписки за замовчуванням \"/sub/\" широко відомий — змініть його.",
       "warnDefaultJsonPath": "JSON-шлях підписки за замовчуванням \"/json/\" широко відомий — змініть його.",
-      "TGBotSettings": "Telegram Бот",
+      "TGBotSettings": "Telegramот",
       "panelListeningIP": "Слухати IP",
       "panelListeningIPDesc": "IP-адреса для веб-панелі. (залиште порожнім, щоб слухати всі IP-адреси)",
       "panelListeningDomain": "Домен прослуховування",
@@ -615,10 +870,12 @@
       "publicKeyPathDesc": "Шлях до файлу відкритого ключа для веб-панелі. (починається з ‘/‘)",
       "privateKeyPath": "Шлях приватного ключа",
       "privateKeyPathDesc": "Шлях до файлу приватного ключа для веб-панелі. (починається з ‘/‘)",
-      "panelUrlPath": "Шлях URL",
+      "panelUrlPath": "URI-шлях",
       "panelUrlPathDesc": "Шлях URL для веб-панелі. (починається з ‘/‘ і закінчується ‘/‘)",
       "pageSize": "Розмір сторінки",
       "pageSizeDesc": "Визначити розмір сторінки для вхідної таблиці. (0 = вимкнено)",
+      "panelProxy": "Мережевий проксі панелі",
+      "panelProxyDesc": "Маршрутизує власні вихідні запити панелі (оновлення geo, перевірки версій Xray/панелі, Telegram) через цей проксі для обходу фільтрації GitHub/Telegram на стороні сервера. Приймає socks5:// або http(s)://, напр. локальний SOCKS-вхідний Xray. Залиште порожнім для прямого підключення.",
       "remarkModel": "Модель зауваження та роздільний символ",
       "datepicker": "Тип календаря",
       "datepickerPlaceholder": "Виберіть дату",
@@ -630,11 +887,11 @@
       "newPassword": "Новий пароль",
       "telegramBotEnable": "Увімкнути Telegram Bot",
       "telegramBotEnableDesc": "Вмикає бота Telegram.",
-      "telegramToken": "Telegram Токен",
+      "telegramToken": "Telegramокен",
       "telegramTokenDesc": "Токен бота Telegram, отриманий від '{'@'}BotFather'.",
-      "telegramProxy": "SOCKS Проксі",
+      "telegramProxy": "SOCKS-проксі",
       "telegramProxyDesc": "Вмикає проксі-сервер SOCKS5 для підключення до Telegram. (відкоригуйте параметри відповідно до посібника)",
-      "telegramAPIServer": "Сервер Telegram API",
+      "telegramAPIServer": "Telegram API сервер",
       "telegramAPIServerDesc": "Сервер Telegram API для використання. Залиште поле порожнім, щоб використовувати сервер за умовчанням.",
       "telegramChatId": "Ідентифікатор чату адміністратора",
       "telegramChatIdDesc": "Ідентифікатори чату адміністратора Telegram. (розділені комами) (отримайте тут {'@'}userinfobot) або (використовуйте команду '/id' у боті)",
@@ -658,6 +915,8 @@
       "subEnable": "Увімкнути службу підписки",
       "subEnableDesc": "Вмикає службу підписки.",
       "subJsonEnable": "Увімкнути/вимкнути JSON-кінець підписки незалежно.",
+      "subJsonEnableTitle": "JSON-підписка",
+      "subClashEnableTitle": "Підписка Clash / Mihomo",
       "subTitle": "Назва Підписки",
       "subTitleDesc": "Назва, яка відображається у VPN-клієнті",
       "subSupportUrl": "URL підтримки",
@@ -678,13 +937,13 @@
       "subCertPathDesc": "Шлях до файлу відкритого ключа для служби підписки. (починається з ‘/‘)",
       "subKeyPath": "Шлях приватного ключа",
       "subKeyPathDesc": "Шлях до файлу приватного ключа для служби підписки. (починається з ‘/‘)",
-      "subPath": "Шлях URI",
+      "subPath": "URI-шлях",
       "subPathDesc": "Шлях URI для служби підписки. (починається з ‘/‘ і закінчується ‘/‘)",
       "subDomain": "Домен прослуховування",
       "subDomainDesc": "Ім'я домену для служби підписки. (залиште порожнім, щоб слухати всі домени та IP-адреси)",
       "subUpdates": "Інтервали оновлення",
       "subUpdatesDesc": "Інтервали оновлення URL-адреси підписки в клієнтських програмах. (одиниця: година)",
-      "subEncrypt": "Закодувати",
+      "subEncrypt": "Кодувати",
       "subEncryptDesc": "Повернений вміст послуги підписки матиме кодування Base64.",
       "subShowInfo": "Показати інформацію про використання",
       "subShowInfoDesc": "Залишок трафіку та дата відображатимуться в клієнтських програмах.",
@@ -693,7 +952,7 @@
       "subURI": "URI зворотного проксі",
       "subURIDesc": "URI до URL-адреси підписки для використання за проксі.",
       "externalTrafficInformEnable": "Інформація про зовнішній трафік",
-      "externalTrafficInformEnableDesc": "Інформувати зовнішній API про кожне оновлення трафіку.",
+      "externalTrafficInformEnableDesc": "Повідомляти зовнішній API про кожне оновлення трафіку.",
       "externalTrafficInformURI": "Інформаційний URI зовнішнього трафіку",
       "externalTrafficInformURIDesc": "Оновлення трафіку надсилаються на цей URI.",
       "restartXrayOnClientDisable": "Перезапускати Xray після авто-вимкнення",
@@ -703,6 +962,54 @@
       "fragmentSett": "Параметри фрагментації",
       "noisesDesc": "Увімкнути Noises.",
       "noisesSett": "Налаштування Noises",
+      "trustedProxyCidrs": "Довірені CIDR проксі",
+      "trustedProxyCidrsDesc": "IP/CIDR через кому, яким дозволено встановлювати заголовки forwarded host, proto та client IP.",
+      "ldap": {
+        "enable": "Увімкнути LDAP-синхронізацію",
+        "host": "LDAP-хост",
+        "port": "Порт LDAP",
+        "useTls": "Використовувати TLS (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "Налаштовано; залиште порожнім для збереження поточного паролю.",
+        "passwordUnconfigured": "Не налаштовано.",
+        "passwordPlaceholder": "Налаштовано — введіть нове значення для заміни",
+        "baseDn": "Base DN",
+        "userFilter": "Фільтр користувача",
+        "userAttr": "Атрибут користувача (username/email)",
+        "vlessField": "Атрибут VLESS-flag",
+        "flagField": "Загальний атрибут flag (опц.)",
+        "flagFieldDesc": "Якщо задано, перевизначає VLESS flag — напр. shadowInactive.",
+        "truthyValues": "Truthy-значення",
+        "truthyValuesDesc": "Через кому; за замовч.: true,1,yes,on",
+        "invertFlag": "Інвертувати flag",
+        "invertFlagDesc": "Увімкніть, коли атрибут означає «вимкнено» (напр. shadowInactive).",
+        "syncSchedule": "Розклад синхронізації",
+        "syncScheduleDesc": "Рядок типу cron, напр. @every 1m",
+        "inboundTags": "Теги вхідних",
+        "inboundTagsDesc": "Вхідні, на яких LDAP-синхронізація може авто-створювати або авто-видаляти клієнтів.",
+        "noInbounds": "Вхідних не знайдено. Спочатку створіть один у Вхідних.",
+        "autoCreate": "Авто-створення клієнтів",
+        "autoDelete": "Авто-видалення клієнтів",
+        "defaultTotalGb": "Обсяг за замовч. (ГБ)",
+        "defaultExpiryDays": "Термін за замовч. (дні)",
+        "defaultIpLimit": "Ліміт IP за замовч."
+      },
+      "subFormats": {
+        "packets": "Пакети",
+        "length": "Довжина",
+        "interval": "Інтервал",
+        "maxSplit": "Макс. розбиття",
+        "noises": "Шуми",
+        "noiseItem": "Шум №{n}",
+        "type": "Тип",
+        "packet": "Пакет",
+        "delayMs": "Затримка (мс)",
+        "applyTo": "Застосувати до",
+        "addNoise": "+ Шум",
+        "concurrency": "Паралельність",
+        "xudpConcurrency": "Паралельність xudp",
+        "xudpUdp443": "xudp UDP 443"
+      },
       "mux": "Mux",
       "muxDesc": "Передавати кілька незалежних потоків даних у межах встановленого потоку даних.",
       "muxSett": "Налаштування Mux",
@@ -756,8 +1063,11 @@
     "xray": {
       "title": "Xray конфігурації",
       "save": "Зберегти",
-      "restart": "Перезапустити Xray",
+      "restart": "Перезапуск Xray",
       "restartSuccess": "Xray успішно перезапущено",
+      "restartOutputTitle": "Вивід перезапуску Xray",
+      "restartConfirmTitle": "Перезапустити xray?",
+      "restartConfirmContent": "Перезавантажує сервіс xray зі збереженою конфігурацією.",
       "stopSuccess": "Xray успішно зупинено",
       "restartError": "Виникла помилка під час перезапуску Xray.",
       "stopError": "Виникла помилка під час зупинки Xray.",
@@ -765,7 +1075,7 @@
       "advancedTemplate": "Додатково",
       "generalConfigs": "Загальні конфігурації",
       "generalConfigsDesc": "Ці параметри визначатимуть загальні налаштування.",
-      "logConfigs": "Журнал",
+      "logConfigs": "Лог",
       "logConfigsDesc": "Журнали можуть вплинути на ефективність вашого сервера. Рекомендується вмикати його з розумом лише у випадку ваших потреб",
       "blockConfigsDesc": "Ці параметри блокуватимуть трафік на основі конкретних запитуваних протоколів і веб-сайтів.",
       "basicRouting": "Основна Маршрутизація",
@@ -792,8 +1102,10 @@
       "Torrent": "Блокувати протокол BitTorrent",
       "Inbounds": "Вхідні",
       "InboundsDesc": "Прийняття певних клієнтів.",
-      "Outbounds": "Вихід",
+      "Outbounds": "Вихідні",
       "Balancers": "Балансери",
+      "balancerTagRequired": "Тег обов'язковий",
+      "balancerSelectorRequired": "Виберіть принаймні один вихідний",
       "OutboundsDesc": "Встановити шлях вихідного трафіку.",
       "Routings": "Правила маршрутизації",
       "RoutingsDesc": "Пріоритет кожного правила важливий!",
@@ -827,11 +1139,78 @@
         "inbound": "Вхідний",
         "outbound": "Вихідний",
         "balancer": "Балансувальник",
-        "info": "Інформація",
+        "info": "Інфо",
         "add": "Додати правило",
         "edit": "Редагувати правило",
         "useComma": "Елементи, розділені комами"
       },
+      "routing": {
+        "dragToReorder": "Перетягніть для зміни порядку"
+      },
+      "ruleForm": {
+        "sourceIps": "IP джерела",
+        "sourcePort": "Порт джерела",
+        "vlessRoute": "VLESS route",
+        "attributes": "Атрибути",
+        "value": "Значення",
+        "user": "Користувач",
+        "inboundTags": "Теги вхідних",
+        "outboundTag": "Тег вихідного",
+        "balancerTag": "Тег балансувальника",
+        "balancerTagTooltip": "Спрямовує трафік через один з налаштованих балансувальників навантаження"
+      },
+      "outboundForm": {
+        "tagDuplicate": "Тег уже використовується іншим вихідним",
+        "tagRequired": "Тег обов'язковий",
+        "tagPlaceholder": "унікальний-тег",
+        "localIpPlaceholder": "локальний IP",
+        "addressRequired": "Адреса обов'язкова",
+        "portRequired": "Порт обов'язковий",
+        "optional": "опційно",
+        "udpOverTcp": "UDP over TCP",
+        "uotVersion": "Версія UoT",
+        "inboundTag": "Тег вхідного",
+        "inboundTagPlaceholder": "тег вхідного у правилах маршрутизації",
+        "responseType": "Тип відповіді",
+        "rewriteNetwork": "Переписати мережу",
+        "unchanged": "(без змін)",
+        "unchangedAddress": "(без змін) напр. 1.1.1.1",
+        "rules": "Правила",
+        "ruleN": "Правило {n}",
+        "action": "Дія",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "Фінальні правила",
+        "overrideXrayPrivateIp": "Перевизначити дефолтний блок приватних IP у Xray",
+        "blockDelay": "Затримка блоку (мс)",
+        "reverseSniffing": "Зворотний sniffing",
+        "workers": "Воркери",
+        "reserved": "Зарезервовано",
+        "minUploadInterval": "Мін. інтервал завантаження (мс)",
+        "maxUploadSizeBytes": "Макс. розмір завантаження (байт)",
+        "uplinkChunkSize": "Розмір chunk Uplink",
+        "noGrpcHeader": "Без gRPC-заголовка",
+        "maxConcurrency": "Макс. паралельність",
+        "maxConnections": "Макс. з'єднань",
+        "maxReuseTimes": "Макс. повторних використань",
+        "maxRequestTimes": "Макс. запитів",
+        "maxReusableSecs": "Макс. секунд повторного використання",
+        "keepAlivePeriod": "Період keep alive",
+        "authPassword": "Пароль авторизації",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "ім'я сервера",
+        "verifyPeerName": "Перевіряти ім'я peer",
+        "pinnedSha256": "Pinned SHA256",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "Інтервал keep alive",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "Інтерфейс",
+        "ipv6Only": "Лише IPv6",
+        "acceptProxyProtocol": "Приймати proxy protocol",
+        "tcpUserTimeoutMs": "TCP user timeout (мс)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (с)"
+      },
       "outbound": {
         "addOutbound": "Додати вихідний",
         "addReverse": "Додати реверс",
@@ -846,8 +1225,8 @@
         "reverse": "Зворотний",
         "domain": "Домен",
         "type": "Тип",
-        "bridge": "Міст",
-        "portal": "Портал",
+        "bridge": "Bridge",
+        "portal": "Portal",
         "link": "Посилання",
         "intercon": "Взаємозв'язок",
         "settings": "Налаштування",
@@ -860,6 +1239,8 @@
         "testSuccess": "Тест успішний",
         "testFailed": "Тест не пройдено",
         "testError": "Не вдалося протестувати вихідне з'єднання",
+        "testModeTooltip": "TCP: швидкий dial-only probe. HTTP: повний запит через xray.",
+        "testAll": "Тестувати всі",
         "nordvpn": "NordVPN",
         "accessToken": "Токен доступу",
         "country": "Країна",
@@ -876,6 +1257,16 @@
         "balancerSelectors": "Селектори",
         "tag": "Тег",
         "tagDesc": "Унікальний тег",
+        "tagDuplicate": "Тег уже використовується іншим балансувальником",
+        "tagPlaceholder": "унікальний тег балансувальника",
+        "selector": "Селектор",
+        "fallback": "Fallback",
+        "expected": "Очікуване",
+        "expectedPlaceholder": "оптимальна кількість вузлів",
+        "maxRtt": "Макс. RTT",
+        "tolerance": "Допуск",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "Неможливо використовувати balancerTag і outboundTag одночасно. Якщо використовувати одночасно, працюватиме лише outboundTag."
       },
       "wireguard": {
@@ -892,6 +1283,38 @@
         "userLevel": "Рівень користувача",
         "userLevelDesc": "Всі з'єднання, встановлені через цей вхід, використовуватимуть цей рівень користувача. Значення за замовчуванням - 0"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "Приватний ключ",
+        "noServers": "Серверів для обраної країни не знайдено",
+        "noPublicKey": "Обраний сервер не повідомляє публічного ключа NordLynx.",
+        "outboundAdded": "Вихідний NordVPN додано",
+        "outboundUpdated": "Вихідний NordVPN оновлено"
+      },
+      "warp": {
+        "licenseError": "Не вдалося встановити ліцензію WARP.",
+        "fetchFirst": "Спочатку отримайте WARP-конфіг.",
+        "createAccount": "Створити акаунт WARP",
+        "accessToken": "Access token",
+        "deviceId": "ID пристрою",
+        "licenseKey": "Ключ ліцензії",
+        "privateKey": "Приватний ключ",
+        "deleteAccount": "Видалити акаунт",
+        "settings": "Налаштування",
+        "licenseKeyLabel": "Ключ ліцензії WARP / WARP+",
+        "key": "Ключ",
+        "keyPlaceholder": "26-символьний ключ WARP+",
+        "accountInfo": "Інформація про акаунт",
+        "deviceName": "Назва пристрою",
+        "deviceModel": "Модель пристрою",
+        "deviceEnabled": "Пристрій увімкнено",
+        "accountType": "Тип акаунта",
+        "role": "Роль",
+        "warpPlusData": "WARP+ data",
+        "quota": "Квота",
+        "usage": "Використання",
+        "addOutbound": "Додати вихідний"
+      },
       "dns": {
         "enable": "Увімкнути DNS",
         "enableDesc": "Увімкнути вбудований DNS-сервер",
@@ -961,8 +1384,8 @@
     "unknown": "Невідомо",
     "inbounds": "Вхідні",
     "clients": "Клієнти",
-    "offline": "🔴 Офлайн",
-    "online": "🟢 Онлайн",
+    "offline": "🔴 Не в мережі",
+    "online": "🟢 У мережі",
     "commands": {
       "unknown": "❗ Невідома команда.",
       "pleaseChoose": "👇 Будь ласка, виберіть:\r\n",
@@ -998,7 +1421,7 @@
       "ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
       "ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
       "ip": "🌐 IP: {{ .IP }}\r\n",
-      "ips": "🔢 IP-адреси:\r\n{{ .IPs }}\r\n",
+      "ips": "🔢 IP:\r\n{{ .IPs }}\r\n",
       "serverUpTime": "⏳ Час роботи: {{ .UpTime }} {{ .Unit }}\r\n",
       "serverLoad": "📈 Завантаження системи: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
       "serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
@@ -1009,7 +1432,7 @@
       "username": "👤 Ім'я користувача: {{ .Username }}\r\n",
       "reason": "❗️ Причина: {{ .Reason }}\r\n",
       "time": "⏰ Час: {{ .Time }}\r\n",
-      "inbound": "📍 Inbound: {{ .Remark }}\r\n",
+      "inbound": "📍 Вхідний: {{ .Remark }}\r\n",
       "port": "🔌 Порт: {{ .Port }}\r\n",
       "expire": "📅 Дата закінчення: {{ .Time }}\r\n",
       "expireIn": "📅 Термін дії: {{ .Time }}\r\n",
@@ -1017,10 +1440,10 @@
       "enabled": "🚨 Увімкнено: {{ .Enable }}\r\n",
       "online": "🌐 Стан підключення: {{ .Status }}\r\n",
       "lastOnline": "🔙 Був(ла) онлайн: {{ .Time }}\r\n",
-      "email": "📧 Електронна пошта: {{ .Email }}\r\n",
-      "upload": "🔼 Upload: ↑{{ .Upload }}\r\n",
-      "download": "🔽 Download: ↓{{ .Download }}\r\n",
-      "total": "📊 Всього: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
+      "email": "📧 Email: {{ .Email }}\r\n",
+      "upload": "🔼 Завантаження: ↑{{ .Upload }}\r\n",
+      "download": "🔽 Завантаження: ↓{{ .Download }}\r\n",
+      "total": "📊 Усього: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
       "TGUser": "👤 Користувач Telegram: {{ .TelegramID }}\r\n",
       "exhaustedMsg": "🚨 Вичерпано {{ .Type }}:\r\n",
       "exhaustedCount": "🚨 Вичерпано кількість {{ .Type }} count:\r\n",
@@ -1077,7 +1500,7 @@
       "ipLimit": "🔢 IP Ліміт",
       "setTGUser": "👤 Встановити користувача Telegram",
       "toggle": "🔘 Увімкнути / Вимкнути",
-      "custom": "🔢 Custom",
+      "custom": "🔢 Своє",
       "confirmNumber": "✅ Підтвердити: {{ .Num }}",
       "confirmNumberAdd": "✅ Підтвердити додавання: {{ .Num }}",
       "limitTraffic": "🚧 Ліміт трафіку",
@@ -1089,9 +1512,9 @@
       "use_default": "🏷️ Використати типове",
       "change_id": "⚙️🔑 ID",
       "change_password": "⚙️🔑 Пароль",
-      "change_email": "⚙️📧 Електронна пошта",
+      "change_email": "⚙️📧 Email",
       "change_comment": "⚙️💬 Коментар",
-      "change_flow": "⚙️🚦 Потік",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "Скинути весь трафік",
       "SortedTrafficUsageReport": "Відсортований звіт про використання трафіку"
     },
@@ -1119,4 +1542,4 @@
       "chooseInbound": "Виберіть Вхідний"
     }
   }
-}
+}

+ 465 - 42
web/translation/vi-VN.json

@@ -8,15 +8,22 @@
   "save": "Lưu",
   "logout": "Đăng xuất",
   "create": "Tạo",
+  "add": "Thêm",
+  "remove": "Xóa",
   "update": "Cập nhật",
   "copy": "Sao chép",
   "copied": "Đã sao chép",
+  "more": "thêm",
   "download": "Tải xuống",
   "remark": "Ghi chú",
   "enable": "Kích hoạt",
   "protocol": "Giao thức",
   "search": "Tìm kiếm",
-  "filter": "Bộ lọc",
+  "filter": "Lọc",
+  "all": "Tất cả",
+  "from": "Từ",
+  "to": "Đến",
+  "done": "Xong",
   "loading": "Đang tải",
   "refresh": "Làm mới",
   "clear": "Xóa",
@@ -27,10 +34,10 @@
   "check": "Kiểm tra",
   "indefinite": "Không xác định",
   "unlimited": "Không giới hạn",
-  "none": "None",
+  "none": "Không",
   "qrCode": "Mã QR",
   "info": "Thông tin thêm",
-  "edit": "Chỉnh sửa",
+  "edit": "Sửa",
   "delete": "Xóa",
   "reset": "Đặt lại",
   "noData": "Không có dữ liệu.",
@@ -39,7 +46,7 @@
   "encryption": "Mã hóa",
   "useIPv4ForHost": "Sử dụng IPv4 cho máy chủ",
   "transmission": "Truyền tải",
-  "host": "Máy chủ",
+  "host": "Host",
   "path": "Đường dẫn",
   "camouflage": "Ngụy trang",
   "status": "Trạng thái",
@@ -95,11 +102,12 @@
     "dark": "Tối",
     "ultraDark": "Siêu tối",
     "dashboard": "Trạng thái hệ thống",
-    "inbounds": "Đầu vào khách hàng",
+    "inbounds": "Inbound",
     "clients": "Khách hàng",
+    "groups": "Nhóm",
     "nodes": "Nút",
     "settings": "Cài đặt bảng điều khiển",
-    "xray": "Cài đặt Xray",
+    "xray": "Cấu hình Xray",
     "apiDocs": "Tài liệu API",
     "logout": "Đăng xuất",
     "link": "Quản lý",
@@ -128,7 +136,7 @@
       "memory": "RAM",
       "threads": "Luồng",
       "xrayStatus": "Xray",
-      "stopXray": "Dừng lại",
+      "stopXray": "Dừng",
       "restartXray": "Khởi động lại",
       "xraySwitch": "Phiên bản",
       "xrayUpdates": "Cập nhật Xray",
@@ -241,18 +249,18 @@
       "getConfigError": "Lỗi xảy ra khi truy xuất tệp cấu hình"
     },
     "inbounds": {
-      "title": "Điểm vào (Inbounds)",
+      "title": "Inbound",
       "totalDownUp": "Tổng tải lên/tải xuống",
       "totalUsage": "Tổng sử dụng",
       "inboundCount": "Số lượng điểm vào",
-      "operate": "Thao tác",
+      "operate": "Menu",
       "enable": "Kích hoạt",
       "remark": "Chú thích",
-      "node": "Nút",
+      "node": "Node",
       "deployTo": "Triển khai tới",
       "localPanel": "Panel cục bộ",
       "fallbacks": {
-        "title": "Fallback",
+        "title": "Fallbacks",
         "help": "Khi một kết nối trên inbound này không khớp với client nào, nó sẽ được chuyển hướng tới inbound khác. Chọn một child bên dưới và các trường định tuyến (SNI / ALPN / Path / xver) sẽ được tự động điền từ transport của child — hầu hết cấu hình không cần chỉnh thêm. Mỗi child nên lắng nghe trên 127.0.0.1 với security=none.",
         "empty": "Chưa có fallback nào",
         "add": "Thêm fallback",
@@ -270,10 +278,10 @@
       },
       "protocol": "Giao thức",
       "port": "Cổng",
-      "portMap": "Cổng tạo",
+      "portMap": "Ánh xạ cổng",
       "traffic": "Lưu lượng",
       "details": "Chi tiết",
-      "transportConfig": "Giao vận",
+      "transportConfig": "Truyền dẫn",
       "expireDate": "Ngày hết hạn",
       "createdAt": "Tạo lúc",
       "updatedAt": "Cập nhật",
@@ -292,10 +300,30 @@
       "delAllClients": "Xóa tất cả khách hàng",
       "delAllClientsConfirmTitle": "Xóa toàn bộ {count} khách hàng khỏi \"{remark}\"?",
       "delAllClientsConfirmContent": "Xóa mọi khách hàng khỏi inbound này và hủy bản ghi lưu lượng của họ. Bản thân inbound vẫn được giữ lại. Hành động này không thể hoàn tác.",
+      "attachClients": "Gắn client vào…",
+      "addClientsToGroup": "Thêm client vào nhóm…",
+      "attachClientsTitle": "Gắn client từ «{remark}»",
+      "attachClientsDesc": "Gắn cùng {count} client (cùng UUID/mật khẩu và lưu lượng chung) vào các inbound đã chọn. Họ vẫn ở trên inbound này.",
+      "attachClientsTargets": "Inbound đích",
+      "attachClientsNoTargets": "Không có inbound tương thích khác để gắn.",
+      "attachClientsResult": "Đã gắn {attached}, bỏ qua {skipped}.",
+      "attachClientsResultMixed": "Đã gắn {attached}, bỏ qua {skipped}, lỗi {errors}.",
+      "attachClientsSelectLabel": "Client để gắn",
+      "attachClientsSearchPlaceholder": "Tìm email hoặc ghi chú",
+      "attachClientsStatusDisabled": "Đã tắt",
+      "attachClientsSelectedCount": "Đã chọn {selected}/{total}",
+      "detachClients": "Tách client",
+      "detachClientsTitle": "Tách client của «{remark}»",
+      "detachClientsDesc": "Chỉ xóa client đã chọn khỏi inbound này. Hồ sơ client được giữ lại (dùng Delete để xóa hoàn toàn). Nguồn có tổng cộng {count} client.",
+      "detachClientsResult": "Đã tách {detached}, bỏ qua {skipped}.",
+      "detachClientsResultMixed": "Đã tách {detached}, bỏ qua {skipped}, lỗi {errors}.",
+      "detachClientsSelectLabel": "Client để tách",
       "exportLinksTitle": "Xuất liên kết inbound",
       "exportSubsTitle": "Xuất liên kết đăng ký",
       "exportAllLinksTitle": "Xuất tất cả liên kết inbound",
       "exportAllSubsTitle": "Xuất tất cả liên kết đăng ký",
+      "exportAllLinksFileName": "Tat-ca-Inbound",
+      "exportAllSubsFileName": "Tat-ca-Inbound-Subs",
       "inboundJsonTitle": "JSON inbound",
       "deleteClient": "Xóa người dùng",
       "deleteClientContent": "Bạn có chắc chắn muốn xóa người dùng không?",
@@ -306,7 +334,7 @@
       "destinationPort": "Cổng đích",
       "targetAddress": "Địa chỉ mục tiêu",
       "monitorDesc": "Mặc định để trống",
-      "meansNoLimit": "= Không giới hạn (đơn vị: GB)",
+      "meansNoLimit": "= Không giới hạn. (đơn vị: GB)",
       "totalFlow": "Tổng lưu lượng",
       "leaveBlankToNeverExpire": "Để trống để không bao giờ hết hạn",
       "noRecommendKeepDefault": "Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định",
@@ -341,9 +369,10 @@
       "IPLimitlogDesc": "Lịch sử đăng nhập IP (trước khi kích hoạt điểm vào sau khi bị vô hiệu hóa bởi giới hạn IP, bạn nên xóa lịch sử).",
       "IPLimitlogclear": "Xóa Lịch sử",
       "setDefaultCert": "Đặt chứng chỉ từ bảng điều khiển",
-      "streamTab": "Stream",
+      "setDefaultCertEmpty": "Không có chứng chỉ nào được cấu hình cho bảng điều khiển. Hãy đặt một chứng chỉ trong Cài đặt trước.",
+      "streamTab": "Luồng",
       "securityTab": "Bảo mật",
-      "sniffingTab": "Sniffing",
+      "sniffingTab": "Dò gói",
       "sniffingMetadataOnly": "Chỉ siêu dữ liệu",
       "sniffingRouteOnly": "Chỉ định tuyến",
       "sniffingIpsExcluded": "IP bị loại trừ",
@@ -369,7 +398,6 @@
       },
       "telegramDesc": "Vui lòng cung cấp ID Trò chuyện Telegram. (sử dụng lệnh '/id' trong bot) hoặc ({'@'}userinfobot)",
       "subscriptionDesc": "Bạn có thể tìm liên kết gói đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau",
-      "info": "Thông tin",
       "same": "Giống nhau",
       "inboundData": "Dữ liệu gửi đến",
       "exportInbound": "Xuất nhập khẩu",
@@ -406,6 +434,143 @@
         "getNewmldsa65Error": "Lỗi khi lấy chứng chỉ mldsa65.",
         "getNewVlessEncError": "Lỗi khi lấy chứng chỉ VlessEnc."
       },
+      "form": {
+        "moveUp": "Lên",
+        "moveDown": "Xuống",
+        "addAll": "Thêm tất cả",
+        "addAllFallbackTooltip": "Thêm hàng fallback cho mỗi inbound đủ điều kiện chưa được nối",
+        "peers": "Peers",
+        "addPeer": "Thêm peer",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "Chỉ Windows. CIDR được tự động thêm vào bảng định tuyến hệ thống để lưu lượng khớp đi qua TUN.",
+        "autoOutboundsInterface": "Giao diện outbound tự động",
+        "autoOutboundsInterfaceTooltip": "Giao diện vật lý cho lưu lượng đi. Dùng 'auto' để tự phát hiện; tự bật khi Auto system routes được đặt.",
+        "rewriteAddress": "Viết lại địa chỉ",
+        "rewritePort": "Viết lại cổng",
+        "allowedNetwork": "Mạng cho phép",
+        "followRedirect": "Theo redirect",
+        "accounts": "Tài khoản",
+        "allowTransparent": "Cho phép trong suốt",
+        "encryptionMethod": "Phương thức mã hóa",
+        "visionTestseed": "Vision testseed",
+        "version": "Phiên bản",
+        "udpIdleTimeout": "UDP idle timeout (s)",
+        "masquerade": "Masquerade",
+        "type": "Loại",
+        "upstreamUrl": "Upstream URL",
+        "rewriteHost": "Viết lại Host",
+        "skipTlsVerify": "Bỏ qua xác minh TLS",
+        "directory": "Thư mục",
+        "statusCode": "Mã trạng thái",
+        "body": "Body",
+        "headers": "Header",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "Phiên bản yêu cầu",
+        "requestMethod": "Phương thức yêu cầu",
+        "requestPath": "Đường dẫn yêu cầu",
+        "requestHeaders": "Header yêu cầu",
+        "responseVersion": "Phiên bản phản hồi",
+        "responseStatus": "Trạng thái phản hồi",
+        "responseReason": "Lý do phản hồi",
+        "responseHeaders": "Header phản hồi",
+        "heartbeatPeriod": "Chu kỳ heartbeat",
+        "serviceName": "Tên dịch vụ",
+        "authority": "Authority",
+        "multiMode": "Multi Mode",
+        "maxBufferedUpload": "Upload buffered tối đa",
+        "maxUploadSize": "Kích thước upload tối đa (Byte)",
+        "streamUpServer": "Stream-Up Server",
+        "serverMaxHeaderBytes": "Byte header máy chủ tối đa",
+        "paddingBytes": "Byte Padding",
+        "uplinkHttpMethod": "Uplink HTTP method",
+        "paddingObfsMode": "Chế độ obfs Padding",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "Vị trí Padding",
+        "paddingMethod": "Phương thức Padding",
+        "sessionPlacement": "Session Placement",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence Placement",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink Data Placement",
+        "uplinkDataKey": "Uplink Data Key",
+        "noSseHeader": "Không có header SSE",
+        "ttiMs": "TTI (ms)",
+        "uplinkMbps": "Uplink (MB/s)",
+        "downlinkMbps": "Downlink (MB/s)",
+        "cwndMultiplier": "Hệ số CWND",
+        "maxSendingWindow": "Cửa sổ gửi tối đa",
+        "externalProxy": "Proxy ngoài",
+        "sniPlaceholder": "SNI (mặc định = host)",
+        "fingerprint": "Fingerprint",
+        "defaultOption": "Mặc định",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive Interval",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "Chỉ V6",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "X-Forwarded-For tin cậy",
+        "addressPortStrategy": "Chiến lược địa chỉ+cổng",
+        "tryDelayMs": "Độ trễ thử (ms)",
+        "prioritizeIPv6": "Ưu tiên IPv6",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "Số thử đồng thời tối đa",
+        "customSockopt": "Sockopt tùy chỉnh",
+        "addCustomOption": "Thêm tùy chọn",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "Tự động",
+        "minMaxVersion": "Phiên bản Min/Max",
+        "rejectUnknownSni": "Từ chối SNI lạ",
+        "disableSystemRoot": "Tắt System Root",
+        "sessionResumption": "Khôi phục phiên",
+        "oneTimeLoading": "Tải một lần",
+        "usageOption": "Tùy chọn sử dụng",
+        "buildChain": "Tạo chuỗi",
+        "echKey": "ECH key",
+        "echConfig": "Cấu hình ECH",
+        "pinnedPeerCertSha256": "SHA-256 chứng chỉ peer đã ghim",
+        "pinnedPeerCertSha256Tip": "Hash SHA-256 mã hóa Base64 của chứng chỉ peer. Chỉ panel — không ghi vào cấu hình xray máy chủ, nhưng được đưa vào liên kết chia sẻ để client có thể ghim chứng chỉ.",
+        "pinnedPeerCertSha256Placeholder": "hash base64, phân tách bằng dấu phẩy",
+        "generateRandomPin": "Tạo hash ngẫu nhiên",
+        "getNewEchCert": "Lấy chứng chỉ ECH mới",
+        "show": "Hiện",
+        "xver": "Xver",
+        "target": "Mục tiêu",
+        "maxTimeDiff": "Chênh lệch thời gian tối đa (ms)",
+        "minClientVer": "Phiên bản client tối thiểu",
+        "maxClientVer": "Phiên bản client tối đa",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "Lấy chứng chỉ mới",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "Lấy Seed mới"
+      },
+      "info": {
+        "mode": "Chế độ",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "Tên giao diện",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "Giao diện outbound",
+        "autoSystemRoutes": "Định tuyến hệ thống tự động",
+        "followRedirect": "FollowRedirect",
+        "auth": "Auth",
+        "noKernelTun": "TUN không kernel",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "Cấu hình Peer {n}"
+      },
       "stream": {
         "general": {
           "request": "Lời yêu cầu",
@@ -456,6 +621,20 @@
       "days": "Ngày",
       "renew": "Tự động gia hạn",
       "renewDesc": "Tự động gia hạn sau khi hết hạn. (0 = tắt) (đơn vị: ngày)",
+      "searchPlaceholder": "Tìm email, ghi chú, sub ID, UUID, mật khẩu, auth…",
+      "filterTitle": "Lọc client",
+      "clearAllFilters": "Xóa tất cả",
+      "sortOldest": "Cũ nhất trước",
+      "sortNewest": "Mới nhất trước",
+      "sortRecentlyUpdated": "Gần đây cập nhật",
+      "sortRecentlyOnline": "Gần đây trực tuyến",
+      "sortEmailAZ": "Email A→Z",
+      "sortEmailZA": "Email Z→A",
+      "sortMostTraffic": "Nhiều lưu lượng nhất",
+      "sortHighestRemaining": "Còn nhiều nhất",
+      "sortExpiringSoonest": "Sắp hết hạn",
+      "has": "Có",
+      "hasNot": "Không có",
       "title": "Khách hàng",
       "actions": "Hành động",
       "totalGB": "Tổng gửi/nhận (GB)",
@@ -466,6 +645,9 @@
       "subId": "ID đăng ký",
       "online": "Trực tuyến",
       "email": "Email",
+      "group": "Nhóm",
+      "groupDesc": "Nhãn logic để gom các client liên quan (nhóm, khách hàng, khu vực). Có thể lọc từ thanh công cụ.",
+      "groupPlaceholder": "ví dụ customer-a",
       "comment": "Ghi chú",
       "traffic": "Lưu lượng",
       "offline": "Ngoại tuyến",
@@ -489,11 +671,45 @@
       "resetAllTraffics": "Đặt lại lưu lượng của tất cả khách hàng",
       "resetAllTrafficsTitle": "Đặt lại lưu lượng của tất cả khách hàng?",
       "resetAllTrafficsContent": "Bộ đếm gửi/nhận của mỗi khách hàng về 0. Hạn mức và thời hạn không bị ảnh hưởng. Không thể hoàn tác.",
-      "empty": "Chưa có khách hàng nào — thêm một để bắt đầu.",
       "deleteConfirmTitle": "Xóa khách hàng {email}?",
       "deleteConfirmContent": "Hành động này gỡ khách hàng khỏi mọi inbound đã gắn và xóa bản ghi lưu lượng. Không thể hoàn tác.",
       "deleteSelected": "Xóa ({count})",
       "adjustSelected": "Điều chỉnh ({count})",
+      "subLinksSelected": "Liên kết sub ({count})",
+      "addToGroupTitle": "Thêm {count} client vào một nhóm",
+      "addToGroupTooltip": "Chọn nhóm có sẵn hoặc nhập tên mới. Dùng Ungroup để xóa client khỏi nhóm hiện tại.",
+      "addToGroupPlaceholder": "Tên nhóm",
+      "addToGroupSuccessToast": "Đã thêm {count} client vào {group}",
+      "ungroupSuccessToast": "Đã xóa nhóm khỏi {count} client",
+      "ungroup": "Bỏ nhóm",
+      "ungroupConfirmTitle": "Xóa {count} client khỏi nhóm của họ?",
+      "ungroupConfirmContent": "Xóa nhãn nhóm trên mỗi client đã chọn. Bản thân client được giữ lại (dùng Delete để xóa hoàn toàn).",
+      "addToGroup": "Thêm vào nhóm",
+      "attach": "Gắn",
+      "adjust": "Điều chỉnh",
+      "subLinks": "Liên kết sub",
+      "selectedCount": "Đã chọn {count}",
+      "attachSelected": "Gắn ({count})",
+      "attachToInboundsTitle": "Gắn {count} client vào inbound",
+      "attachToInboundsDesc": "Gắn {count} client đã chọn (cùng UUID/mật khẩu và lưu lượng chung) vào các inbound đã chọn. Các gắn kết hiện tại được giữ nguyên.",
+      "attachToInboundsTargets": "Inbound đích",
+      "attachToInboundsNoTargets": "Không có inbound đa người dùng nào để gắn.",
+      "detachSelected": "Tách ({count})",
+      "detach": "Tách",
+      "detachFromInboundsTitle": "Tách {count} client khỏi inbound",
+      "detachFromInboundsDesc": "Xóa {count} client đã chọn khỏi các inbound đã chọn. Các cặp client chưa gắn sẽ được bỏ qua. Hồ sơ client được giữ lại (dùng Delete để xóa hoàn toàn).",
+      "detachFromInboundsTargets": "Inbound để tách",
+      "detachFromInboundsNoTargets": "Không có inbound đa người dùng nào.",
+      "detachFromInboundsResult": "Đã tách {detached}, bỏ qua {skipped}.",
+      "detachFromInboundsResultMixed": "Đã tách {detached}, bỏ qua {skipped}, lỗi {errors}.",
+      "subLinksTitle": "Liên kết sub ({count})",
+      "subLinkColumn": "URL đăng ký",
+      "subJsonLinkColumn": "URL JSON đăng ký",
+      "subLinksCopyAll": "Sao chép tất cả",
+      "subLinksCopiedAll": "Đã sao chép {count} liên kết",
+      "subLinksEmpty": "Không client nào trong các client đã chọn có ID đăng ký.",
+      "subLinksDisabled": "Dịch vụ đăng ký đã tắt.",
+      "subLinksDisabledHint": "Bật đăng ký tại Cài đặt bảng điều khiển → Đăng ký để tạo liên kết.",
       "bulkDeleteConfirmTitle": "Xóa {count} khách hàng?",
       "bulkDeleteConfirmContent": "Mỗi khách hàng được chọn sẽ bị gỡ khỏi tất cả inbound đã gắn và bản ghi lưu lượng cũng bị xóa. Không thể hoàn tác.",
       "bulkAdjustTitle": "Điều chỉnh {count} khách hàng",
@@ -505,9 +721,10 @@
       "delDepletedConfirmTitle": "Xóa khách hàng hết hạn mức?",
       "delDepletedConfirmContent": "Gỡ tất cả khách hàng đã dùng hết hạn mức lưu lượng hoặc đã quá hạn. Không thể hoàn tác.",
       "auth": "Auth",
-      "hysteriaAuth": "Auth Hysteria",
+      "hysteriaAuth": "Hysteria Auth",
       "uuid": "UUID",
       "flow": "Flow",
+      "vmessSecurity": "Bảo mật VMess",
       "reverseTag": "Reverse tag",
       "reverseTagPlaceholder": "Reverse tag tùy chọn",
       "telegramId": "ID người dùng Telegram",
@@ -528,10 +745,48 @@
         "delDepleted": "Đã xóa {count} khách hàng hết hạn mức"
       }
     },
+    "groups": {
+      "title": "Nhóm",
+      "name": "Tên",
+      "clientCount": "Client trong nhóm",
+      "totalGroups": "Tổng số nhóm",
+      "totalGroupedClients": "Client có nhóm",
+      "emptyGroups": "Nhóm trống",
+      "addGroup": "Thêm nhóm",
+      "createSuccess": "Đã tạo nhóm «{name}».",
+      "rename": "Đổi tên",
+      "renameTitle": "Đổi tên {name}",
+      "renameCollision": "Nhóm có tên «{name}» đã tồn tại.",
+      "renameSuccess": "Đã đổi tên nhóm trên {count} client.",
+      "deleteConfirmTitle": "Xóa nhóm {name}?",
+      "deleteConfirmContent": "Việc này xóa nhóm và xóa nhãn khỏi {count} client. Bản thân client không bị xóa.",
+      "deleteSuccess": "Đã xóa nhóm khỏi {count} client.",
+      "resetTraffic": "Đặt lại lưu lượng",
+      "resetConfirmTitle": "Đặt lại lưu lượng nhóm {name}?",
+      "resetConfirmContent": "Việc này đưa up/down về 0 cho tất cả {count} client trong nhóm.",
+      "resetSuccess": "Đã đặt lại lưu lượng cho {count} client.",
+      "adjustSuccess": "Đã điều chỉnh {count} client trong {name}.",
+      "emptyForAction": "Nhóm này chưa có client.",
+      "deleteGroupOnly": "Xóa nhóm (giữ client)",
+      "deleteClients": "Xóa client trong nhóm",
+      "deleteClientsConfirmTitle": "Xóa tất cả client trong {name}?",
+      "deleteClientsConfirmContent": "Việc này xóa vĩnh viễn {count} client cùng với hồ sơ lưu lượng. Nhãn nhóm cũng được xóa. Không thể hoàn tác.",
+      "deleteClientsSuccess": "Đã xóa {count} client.",
+      "deleteClientsMixed": "{ok} đã xóa, {failed} bỏ qua",
+      "addToGroup": "Thêm client…",
+      "addToGroupTitle": "Thêm client vào nhóm «{name}»",
+      "addToGroupDesc": "Chọn client để thêm vào nhóm này. Giữ nguyên gắn kết inbound hiện tại; chỉ thay đổi nhãn nhóm. Client đã ở trong nhóm này sẽ không được liệt kê.",
+      "addToGroupEmpty": "Không có client khác để thêm.",
+      "addToGroupResult": "Đã thêm {count} client vào {name}.",
+      "removeFromGroup": "Xóa client…",
+      "removeFromGroupTitle": "Xóa client khỏi nhóm «{name}»",
+      "removeFromGroupDesc": "Chọn thành viên để xóa khỏi nhóm này. Bản thân client được giữ lại (dùng «Xóa client trong nhóm» để xóa hoàn toàn).",
+      "removeFromGroupResult": "Đã xóa {count} client khỏi {name}."
+    },
     "nodes": {
       "title": "Nút",
       "addNode": "Thêm nút",
-      "editNode": "Chỉnh sửa nút",
+      "editNode": "Sửa node",
       "totalNodes": "Tổng số nút",
       "onlineNodes": "Trực tuyến",
       "offlineNodes": "Ngoại tuyến",
@@ -615,10 +870,12 @@
       "publicKeyPathDesc": "Điền vào đường dẫn đầy đủ (bắt đầu từ '/')",
       "privateKeyPath": "Đường dẫn file khóa của chứng chỉ bảng điều khiển",
       "privateKeyPathDesc": "Điền vào đường dẫn đầy đủ (bắt đầu từ '/')",
-      "panelUrlPath": "Đường dẫn gốc URL bảng điều khiển",
+      "panelUrlPath": "Đường dẫn URI",
       "panelUrlPathDesc": "Phải bắt đầu và kết thúc bằng '/'",
       "pageSize": "Kích thước phân trang",
       "pageSizeDesc": "Xác định kích thước trang cho bảng gửi đến. Đặt 0 để tắt",
+      "panelProxy": "Proxy mạng của bảng điều khiển",
+      "panelProxyDesc": "Định tuyến các yêu cầu đi của chính bảng điều khiển (cập nhật geo, kiểm tra phiên bản Xray/panel, Telegram) qua proxy này để vượt qua lọc GitHub/Telegram phía máy chủ. Chấp nhận socks5:// hoặc http(s)://, ví dụ inbound SOCKS cục bộ của Xray. Để trống để kết nối trực tiếp.",
       "remarkModel": "Ghi chú mô hình và ký tự phân tách",
       "datepicker": "Kiểu lịch",
       "datepickerPlaceholder": "Chọn ngày",
@@ -632,9 +889,9 @@
       "telegramBotEnableDesc": "Kết nối với các tính năng của bảng điều khiển này thông qua bot Telegram",
       "telegramToken": "Token Telegram",
       "telegramTokenDesc": "Bạn phải nhận token từ quản lý bot Telegram {'@'}botfather",
-      "telegramProxy": "Socks5 Proxy",
+      "telegramProxy": "SOCKS Proxy",
       "telegramProxyDesc": "Nếu bạn cần socks5 proxy để kết nối với Telegram. Điều chỉnh cài đặt của nó theo hướng dẫn.",
-      "telegramAPIServer": "Telegram API Server",
+      "telegramAPIServer": "Máy chủ API Telegram",
       "telegramAPIServerDesc": "Máy chủ API Telegram để sử dụng. Để trống để sử dụng máy chủ mặc định.",
       "telegramChatId": "Chat ID Telegram của quản trị viên",
       "telegramChatIdDesc": "Nhiều Chat ID phân tách bằng dấu phẩy. Sử dụng {'@'}userinfobot hoặc sử dụng lệnh '/id' trong bot để lấy Chat ID của bạn.",
@@ -658,6 +915,8 @@
       "subEnable": "Bật dịch vụ",
       "subEnableDesc": "Tính năng gói đăng ký với cấu hình riêng",
       "subJsonEnable": "Bật/Tắt điểm cuối đăng ký JSON độc lập.",
+      "subJsonEnableTitle": "Đăng ký JSON",
+      "subClashEnableTitle": "Đăng ký Clash / Mihomo",
       "subTitle": "Tiêu đề Đăng ký",
       "subTitleDesc": "Tiêu đề hiển thị trong ứng dụng VPN",
       "subSupportUrl": "URL Hỗ trợ",
@@ -678,13 +937,13 @@
       "subCertPathDesc": "Điền vào đường dẫn đầy đủ (bắt đầu với '/')",
       "subKeyPath": "Đường dẫn file khóa của chứng chỉ gói đăng ký",
       "subKeyPathDesc": "Điền vào đường dẫn đầy đủ (bắt đầu với '/')",
-      "subPath": "Đường dẫn gốc URL gói đăng ký",
+      "subPath": "Đường dẫn URI",
       "subPathDesc": "Phải bắt đầu và kết thúc bằng '/'",
       "subDomain": "Tên miền con",
       "subDomainDesc": "Mặc định để trống để nghe tất cả các tên miền và IP",
       "subUpdates": "Khoảng thời gian cập nhật gói đăng ký",
       "subUpdatesDesc": "Số giờ giữa các cập nhật trong ứng dụng khách",
-      "subEncrypt": "Mã hóa cấu hình",
+      "subEncrypt": "Mã hóa",
       "subEncryptDesc": "Mã hóa các cấu hình được trả về trong gói đăng ký",
       "subShowInfo": "Hiển thị thông tin sử dụng",
       "subShowInfoDesc": "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình",
@@ -693,7 +952,7 @@
       "subURI": "URI proxy trung gian",
       "subURIDesc": "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian",
       "externalTrafficInformEnable": "Thông báo giao thông bên ngoài",
-      "externalTrafficInformEnableDesc": "Thông báo cho API bên ngoài về mọi cập nhật lưu lượng truy cập.",
+      "externalTrafficInformEnableDesc": "Thông báo API ngoài mỗi khi cập nhật lưu lượng.",
       "externalTrafficInformURI": "URI thông báo lưu lượng truy cập bên ngoài",
       "externalTrafficInformURIDesc": "Cập nhật lưu lượng truy cập được gửi tới URI này.",
       "restartXrayOnClientDisable": "Khởi Động Lại Xray Sau Khi Tự Động Vô Hiệu Hóa",
@@ -703,6 +962,54 @@
       "fragmentSett": "Cài đặt phân mảnh",
       "noisesDesc": "Bật Noises.",
       "noisesSett": "Cài đặt Noises",
+      "trustedProxyCidrs": "CIDR proxy tin cậy",
+      "trustedProxyCidrsDesc": "IPs/CIDRs cách nhau bằng dấu phẩy được phép đặt header host, proto và IP client chuyển tiếp.",
+      "ldap": {
+        "enable": "Bật đồng bộ LDAP",
+        "host": "LDAP host",
+        "port": "Cổng LDAP",
+        "useTls": "Dùng TLS (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "Đã cấu hình; để trống để giữ mật khẩu hiện tại.",
+        "passwordUnconfigured": "Chưa cấu hình.",
+        "passwordPlaceholder": "Đã cấu hình — nhập giá trị mới để thay thế",
+        "baseDn": "Base DN",
+        "userFilter": "Bộ lọc user",
+        "userAttr": "Thuộc tính user (username/email)",
+        "vlessField": "Thuộc tính flag VLESS",
+        "flagField": "Thuộc tính flag chung (tùy chọn)",
+        "flagFieldDesc": "Nếu đặt, sẽ ghi đè VLESS flag — ví dụ shadowInactive.",
+        "truthyValues": "Giá trị truthy",
+        "truthyValuesDesc": "Cách nhau bằng dấu phẩy; mặc định: true,1,yes,on",
+        "invertFlag": "Đảo flag",
+        "invertFlagDesc": "Bật khi thuộc tính có nghĩa «đã tắt» (ví dụ shadowInactive).",
+        "syncSchedule": "Lịch đồng bộ",
+        "syncScheduleDesc": "Chuỗi kiểu cron, ví dụ @every 1m",
+        "inboundTags": "Tag inbound",
+        "inboundTagsDesc": "Các inbound mà đồng bộ LDAP có thể tự tạo hoặc tự xóa client.",
+        "noInbounds": "Không tìm thấy inbound. Hãy tạo một inbound trong mục Inbound trước.",
+        "autoCreate": "Tự động tạo client",
+        "autoDelete": "Tự động xóa client",
+        "defaultTotalGb": "Tổng mặc định (GB)",
+        "defaultExpiryDays": "Hết hạn mặc định (ngày)",
+        "defaultIpLimit": "Giới hạn IP mặc định"
+      },
+      "subFormats": {
+        "packets": "Gói",
+        "length": "Độ dài",
+        "interval": "Khoảng",
+        "maxSplit": "Chia tối đa",
+        "noises": "Nhiễu",
+        "noiseItem": "Nhiễu №{n}",
+        "type": "Loại",
+        "packet": "Gói",
+        "delayMs": "Trễ (ms)",
+        "applyTo": "Áp dụng cho",
+        "addNoise": "+ Nhiễu",
+        "concurrency": "Đồng thời",
+        "xudpConcurrency": "Đồng thời xudp",
+        "xudpUdp443": "xudp UDP 443"
+      },
       "mux": "Mux",
       "muxDesc": "Truyền nhiều luồng dữ liệu độc lập trong luồng dữ liệu đã thiết lập.",
       "muxSett": "Mux Cài đặt",
@@ -758,6 +1065,9 @@
       "save": "Lưu cài đặt",
       "restart": "Khởi động lại Xray",
       "restartSuccess": "Đã khởi động lại Xray thành công",
+      "restartOutputTitle": "Đầu ra khởi động lại Xray",
+      "restartConfirmTitle": "Khởi động lại xray?",
+      "restartConfirmContent": "Tải lại dịch vụ xray với cấu hình đã lưu.",
       "stopSuccess": "Xray đã được dừng thành công",
       "restartError": "Đã xảy ra lỗi khi khởi động lại Xray.",
       "stopError": "Đã xảy ra lỗi khi dừng Xray.",
@@ -790,14 +1100,16 @@
       "outboundTestUrl": "URL kiểm tra outbound",
       "outboundTestUrlDesc": "URL dùng khi kiểm tra kết nối outbound",
       "Torrent": "Cấu hình sử dụng BitTorrent",
-      "Inbounds": "Đầu vào",
+      "Inbounds": "Inbound",
       "InboundsDesc": "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể.",
-      "Outbounds": "Đầu ra",
+      "Outbounds": "Outbound",
       "Balancers": "Cân bằng",
+      "balancerTagRequired": "Tag là bắt buộc",
+      "balancerSelectorRequired": "Chọn ít nhất một outbound",
       "OutboundsDesc": "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này.",
       "Routings": "Quy tắc định tuyến",
       "RoutingsDesc": "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!",
-      "completeTemplate": "All",
+      "completeTemplate": "Tất cả",
       "logLevel": "Mức đăng nhập",
       "logLevelDesc": "Cấp độ nhật ký cho nhật ký lỗi, cho biết thông tin cần được ghi lại.",
       "accessLog": "Nhật ký truy cập",
@@ -832,6 +1144,73 @@
         "edit": "Chỉnh sửa quy tắc",
         "useComma": "Các mục được phân tách bằng dấu phẩy"
       },
+      "routing": {
+        "dragToReorder": "Kéo để sắp xếp lại"
+      },
+      "ruleForm": {
+        "sourceIps": "IP nguồn",
+        "sourcePort": "Cổng nguồn",
+        "vlessRoute": "Đường VLESS",
+        "attributes": "Thuộc tính",
+        "value": "Giá trị",
+        "user": "Người dùng",
+        "inboundTags": "Tag inbound",
+        "outboundTag": "Tag outbound",
+        "balancerTag": "Tag balancer",
+        "balancerTagTooltip": "Định tuyến lưu lượng qua một trong các bộ cân bằng tải đã cấu hình"
+      },
+      "outboundForm": {
+        "tagDuplicate": "Tag đã được dùng bởi outbound khác",
+        "tagRequired": "Tag là bắt buộc",
+        "tagPlaceholder": "tag-duy-nhất",
+        "localIpPlaceholder": "IP nội bộ",
+        "addressRequired": "Địa chỉ là bắt buộc",
+        "portRequired": "Cổng là bắt buộc",
+        "optional": "tùy chọn",
+        "udpOverTcp": "UDP over TCP",
+        "uotVersion": "Phiên bản UoT",
+        "inboundTag": "Tag inbound",
+        "inboundTagPlaceholder": "tag inbound dùng trong quy tắc định tuyến",
+        "responseType": "Loại phản hồi",
+        "rewriteNetwork": "Viết lại mạng",
+        "unchanged": "(không đổi)",
+        "unchangedAddress": "(không đổi) ví dụ 1.1.1.1",
+        "rules": "Quy tắc",
+        "ruleN": "Quy tắc {n}",
+        "action": "Hành động",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "Quy tắc cuối",
+        "overrideXrayPrivateIp": "Ghi đè chặn IP riêng mặc định của Xray",
+        "blockDelay": "Trễ chặn (ms)",
+        "reverseSniffing": "Sniffing ngược",
+        "workers": "Workers",
+        "reserved": "Đã đặt trước",
+        "minUploadInterval": "Khoảng upload tối thiểu (ms)",
+        "maxUploadSizeBytes": "Kích thước upload tối đa (byte)",
+        "uplinkChunkSize": "Kích thước chunk Uplink",
+        "noGrpcHeader": "Không có header gRPC",
+        "maxConcurrency": "Đồng thời tối đa",
+        "maxConnections": "Kết nối tối đa",
+        "maxReuseTimes": "Số lần tái sử dụng tối đa",
+        "maxRequestTimes": "Số yêu cầu tối đa",
+        "maxReusableSecs": "Số giây tái sử dụng tối đa",
+        "keepAlivePeriod": "Chu kỳ keep alive",
+        "authPassword": "Mật khẩu auth",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "tên máy chủ",
+        "verifyPeerName": "Xác minh tên peer",
+        "pinnedSha256": "SHA256 pinned",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "Khoảng keep alive",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "Giao diện",
+        "ipv6Only": "Chỉ IPv6",
+        "acceptProxyProtocol": "Chấp nhận proxy protocol",
+        "tcpUserTimeoutMs": "TCP user timeout (ms)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
+      },
       "outbound": {
         "addOutbound": "Thêm thư đi",
         "addReverse": "Thêm đảo ngược",
@@ -840,14 +1219,14 @@
         "reverseTag": "Thẻ Ngược",
         "reverseTagDesc": "Thẻ outbound của proxy ngược đơn giản VLESS. Để trống để vô hiệu hóa.",
         "reverseTagPlaceholder": "thẻ outbound (để trống để vô hiệu hóa)",
-        "tag": "Thẻ",
+        "tag": "Tag",
         "tagDesc": "thẻ duy nhất",
         "address": "Địa chỉ",
         "reverse": "Đảo ngược",
-        "domain": "Miền",
+        "domain": "Tên miền",
         "type": "Loại",
-        "bridge": "Cầu",
-        "portal": "Cổng thông tin",
+        "bridge": "Bridge",
+        "portal": "Portal",
         "link": "Liên kết",
         "intercon": "Kết nối",
         "settings": "cài đặt",
@@ -860,6 +1239,8 @@
         "testSuccess": "Kiểm tra thành công",
         "testFailed": "Kiểm tra thất bại",
         "testError": "Không thể kiểm tra đầu ra",
+        "testModeTooltip": "TCP: probe dial nhanh. HTTP: yêu cầu đầy đủ qua xray.",
+        "testAll": "Kiểm tra tất cả",
         "nordvpn": "NordVPN",
         "accessToken": "Mã truy cập",
         "country": "Quốc gia",
@@ -874,8 +1255,18 @@
         "editBalancer": "Chỉnh sửa cân bằng",
         "balancerStrategy": "Chiến lược",
         "balancerSelectors": "Bộ chọn",
-        "tag": "Thẻ",
+        "tag": "Tag",
         "tagDesc": "thẻ duy nhất",
+        "tagDuplicate": "Tag đã được dùng bởi balancer khác",
+        "tagPlaceholder": "tag balancer duy nhất",
+        "selector": "Selector",
+        "fallback": "Fallback",
+        "expected": "Kỳ vọng",
+        "expectedPlaceholder": "số node tối ưu",
+        "maxRtt": "RTT tối đa",
+        "tolerance": "Dung sai",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "Không thể sử dụng balancerTag và outboundTag cùng một lúc. Nếu sử dụng cùng lúc thì chỉ outboundTag mới hoạt động."
       },
       "wireguard": {
@@ -892,6 +1283,38 @@
         "userLevel": "Mức Người Dùng",
         "userLevelDesc": "Tất cả các kết nối được thực hiện thông qua inbound này sẽ sử dụng mức người dùng này. Giá trị mặc định là 0"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "Khóa riêng",
+        "noServers": "Không tìm thấy máy chủ cho quốc gia đã chọn",
+        "noPublicKey": "Máy chủ đã chọn không công bố khóa công khai NordLynx.",
+        "outboundAdded": "Đã thêm outbound NordVPN",
+        "outboundUpdated": "Đã cập nhật outbound NordVPN"
+      },
+      "warp": {
+        "licenseError": "Không thiết lập được giấy phép WARP.",
+        "fetchFirst": "Hãy lấy cấu hình WARP trước.",
+        "createAccount": "Tạo tài khoản WARP",
+        "accessToken": "Access token",
+        "deviceId": "ID thiết bị",
+        "licenseKey": "Khóa giấy phép",
+        "privateKey": "Khóa riêng",
+        "deleteAccount": "Xóa tài khoản",
+        "settings": "Cài đặt",
+        "licenseKeyLabel": "Khóa giấy phép WARP / WARP+",
+        "key": "Khóa",
+        "keyPlaceholder": "khóa WARP+ 26 ký tự",
+        "accountInfo": "Thông tin tài khoản",
+        "deviceName": "Tên thiết bị",
+        "deviceModel": "Kiểu thiết bị",
+        "deviceEnabled": "Thiết bị đã bật",
+        "accountType": "Loại tài khoản",
+        "role": "Vai trò",
+        "warpPlusData": "Dữ liệu WARP+",
+        "quota": "Hạn ngạch",
+        "usage": "Sử dụng",
+        "addOutbound": "Thêm outbound"
+      },
       "dns": {
         "enable": "Kích hoạt DNS",
         "enableDesc": "Kích hoạt máy chủ DNS tích hợp",
@@ -992,20 +1415,20 @@
       "2faFailed": "Lỗi 2FA",
       "report": "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n",
       "datetime": "⏰ Ngày-Giờ: {{ .DateTime }}\r\n",
-      "hostname": "💻 Tên máy chủ: {{ .Hostname }}\r\n",
+      "hostname": "💻 Host: {{ .Hostname }}\r\n",
       "version": "🚀 Phiên bản X-UI: {{ .Version }}\r\n",
       "xrayVersion": "📡 Phiên bản Xray: {{ .XrayVersion }}\r\n",
       "ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
       "ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
       "ip": "🌐 IP: {{ .IP }}\r\n",
-      "ips": "🔢 Các IP:\r\n{{ .IPs }}\r\n",
+      "ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
       "serverUpTime": "⏳ Thời gian hoạt động của máy chủ: {{ .UpTime }} {{ .Unit }}\r\n",
       "serverLoad": "📈 Tải máy chủ: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
-      "serverMemory": "📋 Bộ nhớ máy chủ: {{ .Current }}/{{ .Total }}\r\n",
-      "tcpCount": "🔹 Số lượng kết nối TCP: {{ .Count }}\r\n",
-      "udpCount": "🔸 Số lượng kết nối UDP: {{ .Count }}\r\n",
+      "serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
+      "tcpCount": "🔹 TCP: {{ .Count }}\r\n",
+      "udpCount": "🔸 UDP: {{ .Count }}\r\n",
       "traffic": "🚦 Lưu lượng: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
-      "xrayStatus": "ℹ️ Trạng thái Xray: {{ .State }}\r\n",
+      "xrayStatus": "ℹ️ Trạng thái: {{ .State }}\r\n",
       "username": "👤 Tên người dùng: {{ .Username }}\r\n",
       "reason": "❗️ Lý do: {{ .Reason }}\r\n",
       "time": "⏰ Thời gian: {{ .Time }}\r\n",
@@ -1020,7 +1443,7 @@
       "email": "📧 Email: {{ .Email }}\r\n",
       "upload": "🔼 Tải lên: ↑{{ .Upload }}\r\n",
       "download": "🔽 Tải xuống: ↓{{ .Download }}\r\n",
-      "total": "📊 Tổng cộng: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
+      "total": "📊 Tổng: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
       "TGUser": "👤 Người dùng Telegram: {{ .TelegramID }}\r\n",
       "exhaustedMsg": "🚨 Sự cạn kiệt {{ .Type }}:\r\n",
       "exhaustedCount": "🚨 Số lần cạn kiệt {{ .Type }}:\r\n",
@@ -1119,4 +1542,4 @@
       "chooseInbound": "Chọn một Inbound"
     }
   }
-}
+}

+ 475 - 52
web/translation/zh-CN.json

@@ -8,15 +8,22 @@
   "save": "保存",
   "logout": "登出",
   "create": "创建",
+  "add": "添加",
+  "remove": "移除",
   "update": "更新",
   "copy": "复制",
   "copied": "已复制",
+  "more": "更多",
   "download": "下载",
   "remark": "备注",
   "enable": "启用",
   "protocol": "协议",
   "search": "搜索",
   "filter": "筛选",
+  "all": "全部",
+  "from": "从",
+  "to": "到",
+  "done": "完成",
   "loading": "加载中...",
   "refresh": "刷新",
   "clear": "清除",
@@ -41,7 +48,7 @@
   "transmission": "传输",
   "host": "主机",
   "path": "路径",
-  "camouflage": "伪装",
+  "camouflage": "混淆",
   "status": "状态",
   "enabled": "开启",
   "disabled": "关闭",
@@ -95,11 +102,12 @@
     "dark": "暗色",
     "ultraDark": "超暗色",
     "dashboard": "系统状态",
-    "inbounds": "入站列表",
+    "inbounds": "入站",
     "clients": "客户端",
+    "groups": "分组",
     "nodes": "节点",
     "settings": "面板设置",
-    "xray": "Xray 置",
+    "xray": "Xray 置",
     "apiDocs": "API 文档",
     "logout": "退出登录",
     "link": "管理",
@@ -123,9 +131,9 @@
       "cpu": "CPU",
       "logicalProcessors": "逻辑处理器",
       "frequency": "频率",
-      "swap": "交换分区",
+      "swap": "Swap",
       "storage": "存储",
-      "memory": "内存",
+      "memory": "RAM",
       "threads": "线程",
       "xrayStatus": "Xray",
       "stopXray": "停止",
@@ -241,7 +249,7 @@
       "getConfigError": "检索配置文件时出错"
     },
     "inbounds": {
-      "title": "入站列表",
+      "title": "入站",
       "totalDownUp": "总上传 / 下载",
       "totalUsage": "总用量",
       "inboundCount": "入站数量",
@@ -252,7 +260,7 @@
       "deployTo": "部署到",
       "localPanel": "本地面板",
       "fallbacks": {
-        "title": "回落",
+        "title": "Fallbacks",
         "help": "当此入站的连接未匹配任何客户端时,将其路由到另一个入站。在下方选择一个子入站,路由字段(SNI / ALPN / Path / xver)会从子入站的传输方式中自动填充——大多数场景无需再调整。每个子入站应监听 127.0.0.1,security=none。",
         "empty": "暂无回落",
         "add": "添加回落",
@@ -273,7 +281,7 @@
       "portMap": "端口映射",
       "traffic": "流量",
       "details": "详细信息",
-      "transportConfig": "传输配置",
+      "transportConfig": "传输",
       "expireDate": "到期时间",
       "createdAt": "创建时间",
       "updatedAt": "更新时间",
@@ -292,10 +300,30 @@
       "delAllClients": "删除所有客户端",
       "delAllClientsConfirmTitle": "从 \"{remark}\" 中删除全部 {count} 个客户端?",
       "delAllClientsConfirmContent": "从此入站中移除每个客户端并丢弃其流量记录。入站本身将保留。此操作无法撤销。",
+      "attachClients": "附加客户端到…",
+      "addClientsToGroup": "将客户端添加到分组…",
+      "attachClientsTitle": "从 “{remark}” 附加客户端",
+      "attachClientsDesc": "将相同的 {count} 个客户端(相同 UUID/密码和共享流量)附加到选定的入站。它们仍保留在此入站中。",
+      "attachClientsTargets": "目标入站",
+      "attachClientsNoTargets": "没有可附加的其他兼容入站。",
+      "attachClientsResult": "已附加 {attached},已跳过 {skipped}。",
+      "attachClientsResultMixed": "已附加 {attached},已跳过 {skipped},错误 {errors}。",
+      "attachClientsSelectLabel": "要附加的客户端",
+      "attachClientsSearchPlaceholder": "搜索邮箱或备注",
+      "attachClientsStatusDisabled": "已禁用",
+      "attachClientsSelectedCount": "已选 {selected}/{total}",
+      "detachClients": "分离客户端",
+      "detachClientsTitle": "从 “{remark}” 分离客户端",
+      "detachClientsDesc": "仅从此入站移除选中的客户端。客户端记录保留(使用 Delete 完全移除)。源共有 {count} 个客户端。",
+      "detachClientsResult": "已分离 {detached},已跳过 {skipped}。",
+      "detachClientsResultMixed": "已分离 {detached},已跳过 {skipped},错误 {errors}。",
+      "detachClientsSelectLabel": "要分离的客户端",
       "exportLinksTitle": "导出入站链接",
       "exportSubsTitle": "导出订阅链接",
       "exportAllLinksTitle": "导出所有入站链接",
       "exportAllSubsTitle": "导出所有订阅链接",
+      "exportAllLinksFileName": "所有入站",
+      "exportAllSubsFileName": "所有入站-Subs",
       "inboundJsonTitle": "入站 JSON",
       "deleteClient": "删除客户端",
       "deleteClientContent": "确定要删除客户端吗?",
@@ -306,7 +334,7 @@
       "destinationPort": "目标端口",
       "targetAddress": "目标地址",
       "monitorDesc": "留空表示监听所有 IP",
-      "meansNoLimit": "= 无限制(单位:GB)",
+      "meansNoLimit": "= 无限制。(单位: GB)",
       "totalFlow": "总流量",
       "leaveBlankToNeverExpire": "留空表示永不过期",
       "noRecommendKeepDefault": "建议保留默认值",
@@ -333,7 +361,7 @@
       "delDepletedClients": "删除流量耗尽的客户端",
       "delDepletedClientsTitle": "删除流量耗尽的客户端",
       "delDepletedClientsContent": "确定要删除所有流量耗尽的客户端吗?",
-      "email": "电子邮件",
+      "email": "邮箱",
       "emailDesc": "电子邮件必须完全唯一",
       "IPLimit": "IP 限制",
       "IPLimitDesc": "如果数量超过设置值,则禁用入站流量。(0 = 禁用)",
@@ -341,7 +369,8 @@
       "IPLimitlogDesc": "IP 历史日志(要启用被禁用的入站流量,请清除日志)",
       "IPLimitlogclear": "清除日志",
       "setDefaultCert": "从面板设置证书",
-      "streamTab": "流",
+      "setDefaultCertEmpty": "面板尚未配置证书。请先在“设置”中设置。",
+      "streamTab": "传输",
       "securityTab": "安全",
       "sniffingTab": "嗅探",
       "sniffingMetadataOnly": "仅元数据",
@@ -361,15 +390,14 @@
         "allHelp": "在单个编辑器中编辑包含所有字段的完整入站对象。",
         "settings": "设置",
         "settingsHelp": "Xray settings 块包装:",
-        "sniffing": "嗅探",
+        "sniffing": "Sniffing",
         "sniffingHelp": "Xray sniffing 块包装:",
-        "stream": "",
+        "stream": "Stream",
         "streamHelp": "Xray stream 块包装:",
         "jsonErrorPrefix": "高级 JSON"
       },
       "telegramDesc": "请提供Telegram聊天ID。(在机器人中使用'/id'命令)或({'@'}userinfobot",
       "subscriptionDesc": "要找到你的订阅 URL,请导航到“详细信息”。此外,你可以为多个客户端使用相同的名称。",
-      "info": "信息",
       "same": "相同",
       "inboundData": "入站数据",
       "exportInbound": "导出入站规则",
@@ -406,6 +434,143 @@
         "getNewmldsa65Error": "获取mldsa65证书时出错。",
         "getNewVlessEncError": "获取VlessEnc证书时出错。"
       },
+      "form": {
+        "moveUp": "上移",
+        "moveDown": "下移",
+        "addAll": "全部添加",
+        "addAllFallbackTooltip": "为尚未连接的每个符合条件的入站添加一个 fallback 行",
+        "peers": "Peers",
+        "addPeer": "添加 peer",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "仅 Windows。CIDR 会自动添加到系统路由表,以便匹配的流量通过 TUN。",
+        "autoOutboundsInterface": "自动出站接口",
+        "autoOutboundsInterfaceTooltip": "出站流量的物理接口。使用 'auto' 进行检测;设置 Auto system routes 时自动启用。",
+        "rewriteAddress": "重写地址",
+        "rewritePort": "重写端口",
+        "allowedNetwork": "允许的网络",
+        "followRedirect": "跟随重定向",
+        "accounts": "账户",
+        "allowTransparent": "允许透明",
+        "encryptionMethod": "加密方法",
+        "visionTestseed": "Vision testseed",
+        "version": "版本",
+        "udpIdleTimeout": "UDP 空闲超时 (s)",
+        "masquerade": "伪装",
+        "type": "类型",
+        "upstreamUrl": "Upstream URL",
+        "rewriteHost": "重写 Host",
+        "skipTlsVerify": "跳过 TLS 验证",
+        "directory": "目录",
+        "statusCode": "状态码",
+        "body": "Body",
+        "headers": "请求头",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "请求版本",
+        "requestMethod": "请求方法",
+        "requestPath": "请求路径",
+        "requestHeaders": "请求头",
+        "responseVersion": "响应版本",
+        "responseStatus": "响应状态",
+        "responseReason": "响应原因",
+        "responseHeaders": "响应头",
+        "heartbeatPeriod": "心跳周期",
+        "serviceName": "服务名",
+        "authority": "Authority",
+        "multiMode": "多模式",
+        "maxBufferedUpload": "最大缓冲上传",
+        "maxUploadSize": "最大上传大小 (字节)",
+        "streamUpServer": "Stream-Up 服务器",
+        "serverMaxHeaderBytes": "服务器最大头字节",
+        "paddingBytes": "Padding 字节",
+        "uplinkHttpMethod": "Uplink HTTP 方法",
+        "paddingObfsMode": "Padding 混淆模式",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "Padding 位置",
+        "paddingMethod": "Padding 方法",
+        "sessionPlacement": "Session 位置",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence 位置",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink 数据位置",
+        "uplinkDataKey": "Uplink 数据 Key",
+        "noSseHeader": "无 SSE 头",
+        "ttiMs": "TTI (ms)",
+        "uplinkMbps": "上行 (MB/s)",
+        "downlinkMbps": "下行 (MB/s)",
+        "cwndMultiplier": "CWND 倍数",
+        "maxSendingWindow": "最大发送窗口",
+        "externalProxy": "外部代理",
+        "sniPlaceholder": "SNI (默认为 host)",
+        "fingerprint": "指纹",
+        "defaultOption": "默认",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive 间隔",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "仅 V6",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "可信 X-Forwarded-For",
+        "addressPortStrategy": "地址+端口策略",
+        "tryDelayMs": "尝试延迟 (ms)",
+        "prioritizeIPv6": "IPv6 优先",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "最大并发尝试",
+        "customSockopt": "自定义 sockopt",
+        "addCustomOption": "添加自定义选项",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "自动",
+        "minMaxVersion": "最小/最大版本",
+        "rejectUnknownSni": "拒绝未知 SNI",
+        "disableSystemRoot": "禁用系统根",
+        "sessionResumption": "会话恢复",
+        "oneTimeLoading": "一次性加载",
+        "usageOption": "使用选项",
+        "buildChain": "构建证书链",
+        "echKey": "ECH key",
+        "echConfig": "ECH 配置",
+        "pinnedPeerCertSha256": "固定对端证书 SHA-256",
+        "pinnedPeerCertSha256Tip": "对端证书的 Base64 编码 SHA-256 哈希。仅面板使用 — 不写入服务器的 xray 配置,但会包含在分享链接中,以便客户端固定证书。",
+        "pinnedPeerCertSha256Placeholder": "base64 哈希,逗号分隔",
+        "generateRandomPin": "生成随机哈希",
+        "getNewEchCert": "获取新 ECH 证书",
+        "show": "显示",
+        "xver": "Xver",
+        "target": "目标",
+        "maxTimeDiff": "最大时间差 (ms)",
+        "minClientVer": "最小客户端版本",
+        "maxClientVer": "最大客户端版本",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "获取新证书",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "获取新 Seed"
+      },
+      "info": {
+        "mode": "模式",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "接口名称",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "出站接口",
+        "autoSystemRoutes": "自动系统路由",
+        "followRedirect": "FollowRedirect",
+        "auth": "认证",
+        "noKernelTun": "非内核 TUN",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "Peer {n} 配置"
+      },
       "stream": {
         "general": {
           "request": "请求",
@@ -456,6 +621,20 @@
       "days": "天",
       "renew": "自动续期",
       "renewDesc": "到期后自动续期。(0 = 禁用) (单位: 天)",
+      "searchPlaceholder": "搜索邮箱、备注、sub ID、UUID、密码、auth…",
+      "filterTitle": "筛选客户端",
+      "clearAllFilters": "清除全部",
+      "sortOldest": "最旧优先",
+      "sortNewest": "最新优先",
+      "sortRecentlyUpdated": "最近更新",
+      "sortRecentlyOnline": "最近在线",
+      "sortEmailAZ": "邮箱 A→Z",
+      "sortEmailZA": "邮箱 Z→A",
+      "sortMostTraffic": "流量最多",
+      "sortHighestRemaining": "剩余最多",
+      "sortExpiringSoonest": "即将过期",
+      "has": "拥有",
+      "hasNot": "不拥有",
       "title": "客户端",
       "actions": "操作",
       "totalGB": "总上传/下载 (GB)",
@@ -466,6 +645,9 @@
       "subId": "订阅 ID",
       "online": "在线",
       "email": "邮箱",
+      "group": "分组",
+      "groupDesc": "用于对相关客户端进行分桶的逻辑标签(如团队、客户、地区)。可从工具栏筛选。",
+      "groupPlaceholder": "如 customer-a",
       "comment": "备注",
       "traffic": "流量",
       "offline": "离线",
@@ -485,15 +667,49 @@
       "noLinks": "没有可共享的链接 — 请先将此客户端关联到支持协议的入站。",
       "link": "链接",
       "resetNotPossible": "请先将此客户端关联到入站。",
-      "general": "通用",
+      "general": "常规",
       "resetAllTraffics": "重置所有客户端流量",
       "resetAllTrafficsTitle": "重置所有客户端流量?",
       "resetAllTrafficsContent": "所有客户端的上下行计数器将归零。配额与过期时间不受影响。该操作不可撤销。",
-      "empty": "尚无客户端 — 添加一个开始使用。",
       "deleteConfirmTitle": "删除客户端 {email}?",
       "deleteConfirmContent": "将从所有关联入站中移除该客户端并删除其流量记录。该操作不可撤销。",
       "deleteSelected": "删除 ({count})",
       "adjustSelected": "调整 ({count})",
+      "subLinksSelected": "订阅链接 ({count})",
+      "addToGroupTitle": "将 {count} 个客户端添加到分组",
+      "addToGroupTooltip": "选择现有分组或输入新名称。使用 Ungroup 操作从当前分组移除客户端。",
+      "addToGroupPlaceholder": "分组名称",
+      "addToGroupSuccessToast": "已将 {count} 个客户端添加到 {group}",
+      "ungroupSuccessToast": "已清除 {count} 个客户端的分组",
+      "ungroup": "取消分组",
+      "ungroupConfirmTitle": "将 {count} 个客户端从其分组中移除?",
+      "ungroupConfirmContent": "清除每个选中客户端的分组标签。客户端本身保留(使用 Delete 完全移除)。",
+      "addToGroup": "添加到分组",
+      "attach": "附加",
+      "adjust": "调整",
+      "subLinks": "订阅链接",
+      "selectedCount": "已选 {count} 项",
+      "attachSelected": "附加 ({count})",
+      "attachToInboundsTitle": "将 {count} 个客户端附加到入站",
+      "attachToInboundsDesc": "将选中的 {count} 个客户端(相同 UUID/密码和共享流量)附加到选定的入站。它们保留现有的附加关系。",
+      "attachToInboundsTargets": "目标入站",
+      "attachToInboundsNoTargets": "没有可用于附加的多用户入站。",
+      "detachSelected": "分离 ({count})",
+      "detach": "分离",
+      "detachFromInboundsTitle": "从入站分离 {count} 个客户端",
+      "detachFromInboundsDesc": "从选定的入站中移除选中的 {count} 个客户端。客户端未附加的配对将被静默跳过。客户端记录保留(使用 Delete 完全移除)。",
+      "detachFromInboundsTargets": "要分离的入站",
+      "detachFromInboundsNoTargets": "没有可用的多用户入站。",
+      "detachFromInboundsResult": "已分离 {detached},已跳过 {skipped}。",
+      "detachFromInboundsResultMixed": "已分离 {detached},已跳过 {skipped},错误 {errors}。",
+      "subLinksTitle": "订阅链接 ({count})",
+      "subLinkColumn": "订阅 URL",
+      "subJsonLinkColumn": "订阅 JSON URL",
+      "subLinksCopyAll": "全部复制",
+      "subLinksCopiedAll": "已复制 {count} 条链接",
+      "subLinksEmpty": "选中的客户端均无订阅 ID。",
+      "subLinksDisabled": "订阅服务已禁用。",
+      "subLinksDisabledHint": "在面板设置 → 订阅中启用订阅以生成链接。",
       "bulkDeleteConfirmTitle": "删除 {count} 个客户端?",
       "bulkDeleteConfirmContent": "每个所选客户端都会从关联的入站中被移除,其流量记录也会被删除。该操作不可撤销。",
       "bulkAdjustTitle": "调整 {count} 个客户端",
@@ -504,11 +720,12 @@
       "delDepleted": "删除已耗尽",
       "delDepletedConfirmTitle": "删除已耗尽的客户端?",
       "delDepletedConfirmContent": "删除所有流量配额已用尽或已过期的客户端。该操作不可撤销。",
-      "auth": "Auth",
-      "hysteriaAuth": "Hysteria Auth",
+      "auth": "认证",
+      "hysteriaAuth": "Hysteria 认证",
       "uuid": "UUID",
       "flow": "Flow",
-      "reverseTag": "Reverse tag",
+      "vmessSecurity": "VMess 加密",
+      "reverseTag": "反向标签",
       "reverseTagPlaceholder": "可选 Reverse tag",
       "telegramId": "Telegram 用户 ID",
       "telegramIdPlaceholder": "数字形式的 Telegram 用户 ID (0 = 无)",
@@ -528,6 +745,44 @@
         "delDepleted": "已删除 {count} 个已耗尽的客户端"
       }
     },
+    "groups": {
+      "title": "分组",
+      "name": "名称",
+      "clientCount": "分组中的客户端",
+      "totalGroups": "分组总数",
+      "totalGroupedClients": "有分组的客户端",
+      "emptyGroups": "空分组",
+      "addGroup": "添加分组",
+      "createSuccess": "已创建分组 “{name}”。",
+      "rename": "重命名",
+      "renameTitle": "重命名 {name}",
+      "renameCollision": "已存在名为 “{name}” 的分组。",
+      "renameSuccess": "已为 {count} 个客户端重命名分组。",
+      "deleteConfirmTitle": "删除分组 {name}?",
+      "deleteConfirmContent": "这将删除分组并清除 {count} 个客户端的标签。客户端本身不会被删除。",
+      "deleteSuccess": "已清除 {count} 个客户端的分组。",
+      "resetTraffic": "重置流量",
+      "resetConfirmTitle": "重置分组 {name} 的流量?",
+      "resetConfirmContent": "这将清零此分组中所有 {count} 个客户端的上行/下行流量。",
+      "resetSuccess": "已重置 {count} 个客户端的流量。",
+      "adjustSuccess": "已调整 {name} 中的 {count} 个客户端。",
+      "emptyForAction": "此分组尚无客户端。",
+      "deleteGroupOnly": "删除分组(保留客户端)",
+      "deleteClients": "删除分组中的客户端",
+      "deleteClientsConfirmTitle": "删除 {name} 中的所有客户端?",
+      "deleteClientsConfirmContent": "这将永久删除 {count} 个客户端及其流量记录。分组标签也会被清除。此操作无法撤销。",
+      "deleteClientsSuccess": "已删除 {count} 个客户端。",
+      "deleteClientsMixed": "已删除 {ok},已跳过 {failed}",
+      "addToGroup": "添加客户端…",
+      "addToGroupTitle": "添加客户端到分组 “{name}”",
+      "addToGroupDesc": "选择要添加到此分组的客户端。保留其现有入站附加;仅更改分组标签。已在此分组中的客户端不会列出。",
+      "addToGroupEmpty": "没有其他可添加的客户端。",
+      "addToGroupResult": "已将 {count} 个客户端添加到 {name}。",
+      "removeFromGroup": "移除客户端…",
+      "removeFromGroupTitle": "从分组 “{name}” 移除客户端",
+      "removeFromGroupDesc": "选择要从此分组中移除的成员。客户端本身保留(使用 “删除分组中的客户端” 完全移除)。",
+      "removeFromGroupResult": "已从 {name} 移除 {count} 个客户端。"
+    },
     "nodes": {
       "title": "节点",
       "addNode": "添加节点",
@@ -543,8 +798,8 @@
       "scheme": "协议",
       "address": "地址",
       "port": "端口",
-      "basePath": "基础路径",
-      "apiToken": "API 令牌",
+      "basePath": "Base Path",
+      "apiToken": "API Token",
       "apiTokenPlaceholder": "远程面板设置页中的令牌",
       "apiTokenHint": "远程面板在 设置 → API 令牌 中显示其 API 令牌。",
       "regenerate": "重新生成令牌",
@@ -555,7 +810,7 @@
       "status": "状态",
       "cpu": "CPU",
       "mem": "内存",
-      "uptime": "运行时",
+      "uptime": "运行时",
       "latency": "延迟",
       "lastHeartbeat": "上次心跳",
       "xrayVersion": "Xray 版本",
@@ -604,7 +859,7 @@
       "warnDefaultBasePath": "默认根路径 \"/\" 众所周知 — 请更改为随机路径。",
       "warnDefaultSubPath": "默认订阅路径 \"/sub/\" 众所周知 — 请更改。",
       "warnDefaultJsonPath": "默认 JSON 订阅路径 \"/json/\" 众所周知 — 请更改。",
-      "TGBotSettings": "Telegram 机器人配置",
+      "TGBotSettings": "Telegram 机器人",
       "panelListeningIP": "面板监听 IP",
       "panelListeningIPDesc": "默认留空监听所有 IP",
       "panelListeningDomain": "面板监听域名",
@@ -615,10 +870,12 @@
       "publicKeyPathDesc": "填写一个 '/' 开头的绝对路径",
       "privateKeyPath": "面板证书密钥文件路径",
       "privateKeyPathDesc": "填写一个 '/' 开头的绝对路径",
-      "panelUrlPath": "面板 url 根路径",
+      "panelUrlPath": "URI 路径",
       "panelUrlPathDesc": "必须以 '/' 开头,以 '/' 结尾",
       "pageSize": "分页大小",
       "pageSizeDesc": "定义入站表的页面大小。设置 0 表示禁用",
+      "panelProxy": "面板网络代理",
+      "panelProxyDesc": "通过此代理路由面板自身的出站请求(geo 更新、Xray/面板版本检查、Telegram),以绕过服务端对 GitHub/Telegram 的过滤。接受 socks5:// 或 http(s)://,如本地 Xray SOCKS 入站。留空表示直连。",
       "remarkModel": "备注模型和分隔符",
       "datepicker": "日期选择器",
       "datepickerPlaceholder": "选择日期",
@@ -630,11 +887,11 @@
       "newPassword": "新密码",
       "telegramBotEnable": "启用 Telegram 机器人",
       "telegramBotEnableDesc": "启用 Telegram 机器人功能",
-      "telegramToken": "Telegram 机器人令牌(token)",
+      "telegramToken": "Telegram Token",
       "telegramTokenDesc": "从 '{'@'}BotFather' 获取的 Telegram 机器人令牌",
-      "telegramProxy": "SOCKS5 Proxy",
+      "telegramProxy": "SOCKS 代理",
       "telegramProxyDesc": "启用 SOCKS5 代理连接到 Telegram(根据指南调整设置)",
-      "telegramAPIServer": "Telegram API Server",
+      "telegramAPIServer": "Telegram API 服务器",
       "telegramAPIServerDesc": "要使用的 Telegram API 服务器。留空以使用默认服务器。",
       "telegramChatId": "管理员聊天 ID",
       "telegramChatIdDesc": "Telegram 管理员聊天 ID (多个以逗号分隔)(可通过 {'@'}userinfobot 获取,或在机器人中使用 '/id' 命令获取)",
@@ -658,6 +915,8 @@
       "subEnable": "启用订阅服务",
       "subEnableDesc": "启用订阅服务功能",
       "subJsonEnable": "单独启用/禁用 JSON 订阅端点。",
+      "subJsonEnableTitle": "JSON 订阅",
+      "subClashEnableTitle": "Clash / Mihomo 订阅",
       "subTitle": "订阅标题",
       "subTitleDesc": "在VPN客户端中显示的标题",
       "subSupportUrl": "支持链接",
@@ -693,7 +952,7 @@
       "subURI": "反向代理 URI",
       "subURIDesc": "用于代理后面的订阅 URL 的 URI 路径",
       "externalTrafficInformEnable": "外部交通通知",
-      "externalTrafficInformEnableDesc": "每次流量更新时通知外部 API",
+      "externalTrafficInformEnableDesc": "每次流量更新时通知外部 API",
       "externalTrafficInformURI": "外部流量通知 URI",
       "externalTrafficInformURIDesc": "流量更新将发送到此 URI",
       "restartXrayOnClientDisable": "客户端自动禁用后重启 Xray",
@@ -703,7 +962,55 @@
       "fragmentSett": "设置",
       "noisesDesc": "启用 Noises.",
       "noisesSett": "Noises 设置",
-      "mux": "多路复用器",
+      "trustedProxyCidrs": "可信代理 CIDR",
+      "trustedProxyCidrsDesc": "允许设置转发 host、proto 和客户端 IP 标头的 IP/CIDR(逗号分隔)。",
+      "ldap": {
+        "enable": "启用 LDAP 同步",
+        "host": "LDAP host",
+        "port": "LDAP 端口",
+        "useTls": "使用 TLS (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "已配置;留空以保留当前密码。",
+        "passwordUnconfigured": "未配置。",
+        "passwordPlaceholder": "已配置 - 输入新值以替换",
+        "baseDn": "Base DN",
+        "userFilter": "用户筛选",
+        "userAttr": "用户属性 (username/email)",
+        "vlessField": "VLESS flag 属性",
+        "flagField": "通用 flag 属性 (可选)",
+        "flagFieldDesc": "如设置,将覆盖 VLESS flag — 如 shadowInactive。",
+        "truthyValues": "Truthy 值",
+        "truthyValuesDesc": "逗号分隔;默认: true,1,yes,on",
+        "invertFlag": "反转 flag",
+        "invertFlagDesc": "当属性表示已禁用时启用 (如 shadowInactive)。",
+        "syncSchedule": "同步计划",
+        "syncScheduleDesc": "类 cron 字符串,如 @every 1m",
+        "inboundTags": "入站标签",
+        "inboundTagsDesc": "允许 LDAP 同步自动创建或删除客户端的入站。",
+        "noInbounds": "未找到入站。请先在“入站”中创建。",
+        "autoCreate": "自动创建客户端",
+        "autoDelete": "自动删除客户端",
+        "defaultTotalGb": "默认总流量 (GB)",
+        "defaultExpiryDays": "默认到期 (天)",
+        "defaultIpLimit": "默认 IP 限制"
+      },
+      "subFormats": {
+        "packets": "数据包",
+        "length": "长度",
+        "interval": "间隔",
+        "maxSplit": "最大分割",
+        "noises": "噪声",
+        "noiseItem": "噪声 №{n}",
+        "type": "类型",
+        "packet": "数据包",
+        "delayMs": "延迟 (ms)",
+        "applyTo": "应用于",
+        "addNoise": "+ 噪声",
+        "concurrency": "并发",
+        "xudpConcurrency": "xudp 并发",
+        "xudpUdp443": "xudp UDP 443"
+      },
+      "mux": "Mux",
       "muxDesc": "在已建立的数据流内传输多个独立的数据流",
       "muxSett": "复用器设置",
       "direct": "直接连接",
@@ -756,8 +1063,11 @@
     "xray": {
       "title": "Xray 配置",
       "save": "保存",
-      "restart": "重 Xray",
+      "restart": "重启 Xray",
       "restartSuccess": "Xray 已成功重新启动",
+      "restartOutputTitle": "Xray 重启输出",
+      "restartConfirmTitle": "重启 xray?",
+      "restartConfirmContent": "使用已保存的配置重新加载 xray 服务。",
       "stopSuccess": "Xray 已成功停止",
       "restartError": "重启Xray时发生错误。",
       "stopError": "停止Xray时发生错误。",
@@ -790,10 +1100,12 @@
       "outboundTestUrl": "出站测试 URL",
       "outboundTestUrlDesc": "测试出站连接时使用的 URL",
       "Torrent": "屏蔽 BitTorrent 协议",
-      "Inbounds": "入站规则",
+      "Inbounds": "入站",
       "InboundsDesc": "接受来自特定客户端的流量",
-      "Outbounds": "出站规则",
+      "Outbounds": "出站",
       "Balancers": "负载均衡",
+      "balancerTagRequired": "标签为必填项",
+      "balancerSelectorRequired": "至少选择一个出站",
       "OutboundsDesc": "设置出站流量传出方式",
       "Routings": "路由规则",
       "RoutingsDesc": "每条规则的优先级都很重要",
@@ -832,6 +1144,73 @@
         "edit": "编辑规则",
         "useComma": "逗号分隔的项目"
       },
+      "routing": {
+        "dragToReorder": "拖动以重新排序"
+      },
+      "ruleForm": {
+        "sourceIps": "源 IP",
+        "sourcePort": "源端口",
+        "vlessRoute": "VLESS 路由",
+        "attributes": "属性",
+        "value": "值",
+        "user": "用户",
+        "inboundTags": "入站标签",
+        "outboundTag": "出站标签",
+        "balancerTag": "均衡器标签",
+        "balancerTagTooltip": "通过其中一个已配置的负载均衡器路由流量"
+      },
+      "outboundForm": {
+        "tagDuplicate": "该标签已被其他出站使用",
+        "tagRequired": "标签为必填项",
+        "tagPlaceholder": "唯一标签",
+        "localIpPlaceholder": "本地 IP",
+        "addressRequired": "地址为必填项",
+        "portRequired": "端口为必填项",
+        "optional": "可选",
+        "udpOverTcp": "UDP over TCP",
+        "uotVersion": "UoT 版本",
+        "inboundTag": "入站标签",
+        "inboundTagPlaceholder": "用于路由规则的入站标签",
+        "responseType": "响应类型",
+        "rewriteNetwork": "重写网络",
+        "unchanged": "(未更改)",
+        "unchangedAddress": "(未更改) 如 1.1.1.1",
+        "rules": "规则",
+        "ruleN": "规则 {n}",
+        "action": "操作",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "最终规则",
+        "overrideXrayPrivateIp": "覆盖 Xray 默认的私有 IP 阻止",
+        "blockDelay": "阻塞延迟 (ms)",
+        "reverseSniffing": "反向 sniffing",
+        "workers": "Workers",
+        "reserved": "保留",
+        "minUploadInterval": "最小上传间隔 (ms)",
+        "maxUploadSizeBytes": "最大上传大小 (字节)",
+        "uplinkChunkSize": "Uplink chunk 大小",
+        "noGrpcHeader": "无 gRPC 头",
+        "maxConcurrency": "最大并发",
+        "maxConnections": "最大连接",
+        "maxReuseTimes": "最大复用次数",
+        "maxRequestTimes": "最大请求次数",
+        "maxReusableSecs": "最大可复用秒数",
+        "keepAlivePeriod": "keep alive 周期",
+        "authPassword": "认证密码",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "服务器名",
+        "verifyPeerName": "验证 peer 名称",
+        "pinnedSha256": "Pinned SHA256",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "keep alive 间隔",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "接口",
+        "ipv6Only": "仅 IPv6",
+        "acceptProxyProtocol": "接受 proxy protocol",
+        "tcpUserTimeoutMs": "TCP user timeout (ms)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
+      },
       "outbound": {
         "addOutbound": "添加出站",
         "addReverse": "添加反向",
@@ -860,6 +1239,8 @@
         "testSuccess": "测试成功",
         "testFailed": "测试失败",
         "testError": "测试出站失败",
+        "testModeTooltip": "TCP: 快速 dial-only 探测。HTTP: 通过 xray 的完整请求。",
+        "testAll": "全部测试",
         "nordvpn": "NordVPN",
         "accessToken": "访问令牌",
         "country": "国家",
@@ -876,6 +1257,16 @@
         "balancerSelectors": "选择器",
         "tag": "标签",
         "tagDesc": "唯一标签",
+        "tagDuplicate": "该标签已被其他均衡器使用",
+        "tagPlaceholder": "唯一均衡器标签",
+        "selector": "选择器",
+        "fallback": "Fallback",
+        "expected": "期望",
+        "expectedPlaceholder": "最佳节点数",
+        "maxRtt": "最大 RTT",
+        "tolerance": "容差",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "无法同时使用 balancerTag 和 outboundTag。如果同时使用,则只有 outboundTag 会生效。"
       },
       "wireguard": {
@@ -892,6 +1283,38 @@
         "userLevel": "用户级别",
         "userLevelDesc": "通过此入站的所有连接都将使用此用户级别。默认值为 0"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "私钥",
+        "noServers": "未找到选定国家/地区的服务器",
+        "noPublicKey": "选定的服务器未公布 NordLynx 公钥。",
+        "outboundAdded": "NordVPN 出站已添加",
+        "outboundUpdated": "NordVPN 出站已更新"
+      },
+      "warp": {
+        "licenseError": "设置 WARP 许可证失败。",
+        "fetchFirst": "请先获取 WARP 配置。",
+        "createAccount": "创建 WARP 账户",
+        "accessToken": "Access token",
+        "deviceId": "设备 ID",
+        "licenseKey": "许可证密钥",
+        "privateKey": "私钥",
+        "deleteAccount": "删除账户",
+        "settings": "设置",
+        "licenseKeyLabel": "WARP / WARP+ 许可证密钥",
+        "key": "密钥",
+        "keyPlaceholder": "26 位 WARP+ 密钥",
+        "accountInfo": "账户信息",
+        "deviceName": "设备名称",
+        "deviceModel": "设备型号",
+        "deviceEnabled": "设备已启用",
+        "accountType": "账户类型",
+        "role": "角色",
+        "warpPlusData": "WARP+ 数据",
+        "quota": "配额",
+        "usage": "使用",
+        "addOutbound": "添加出站"
+      },
       "dns": {
         "enable": "启用 DNS",
         "enableDesc": "启用内置 DNS 服务器",
@@ -911,7 +1334,7 @@
         "strategyDesc": "解析域名的总体策略",
         "add": "添加服务器",
         "edit": "编辑服务器",
-        "domains": "域",
+        "domains": "域",
         "expectIPs": "预期 IP",
         "unexpectIPs": "意外IP",
         "useSystemHosts": "使用系统Hosts",
@@ -992,35 +1415,35 @@
       "2faFailed": "2FA 失败",
       "report": "🕰 定时报告:{{ .RunTime }}\r\n",
       "datetime": "⏰ 日期时间:{{ .DateTime }}\r\n",
-      "hostname": "💻 主机名:{{ .Hostname }}\r\n",
+      "hostname": "💻 主机: {{ .Hostname }}\r\n",
       "version": "🚀 X-UI 版本:{{ .Version }}\r\n",
       "xrayVersion": "📡 Xray 版本: {{ .XrayVersion }}\r\n",
-      "ipv6": "🌐 IPv6{{ .IPv6 }}\r\n",
-      "ipv4": "🌐 IPv4{{ .IPv4 }}\r\n",
-      "ip": "🌐 IP{{ .IP }}\r\n",
-      "ips": "🔢 IP 地址:\r\n{{ .IPs }}\r\n",
+      "ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
+      "ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
+      "ip": "🌐 IP: {{ .IP }}\r\n",
+      "ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
       "serverUpTime": "⏳ 服务器运行时间:{{ .UpTime }} {{ .Unit }}\r\n",
       "serverLoad": "📈 服务器负载:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
-      "serverMemory": "📋 服务器内存:{{ .Current }}/{{ .Total }}\r\n",
-      "tcpCount": "🔹 TCP 连接数:{{ .Count }}\r\n",
-      "udpCount": "🔸 UDP 连接数:{{ .Count }}\r\n",
+      "serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
+      "tcpCount": "🔹 TCP: {{ .Count }}\r\n",
+      "udpCount": "🔸 UDP: {{ .Count }}\r\n",
       "traffic": "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
-      "xrayStatus": "ℹ️ Xray 状态:{{ .State }}\r\n",
+      "xrayStatus": "ℹ️ 状态: {{ .State }}\r\n",
       "username": "👤 用户名:{{ .Username }}\r\n",
       "reason": "❗️ 原因:{{ .Reason }}\r\n",
       "time": "⏰ 时间:{{ .Time }}\r\n",
-      "inbound": "📍 入站{{ .Remark }}\r\n",
-      "port": "🔌 端口{{ .Port }}\r\n",
+      "inbound": "📍 入站: {{ .Remark }}\r\n",
+      "port": "🔌 端口: {{ .Port }}\r\n",
       "expire": "📅 过期日期:{{ .Time }}\r\n",
       "expireIn": "📅 剩余时间:{{ .Time }}\r\n",
       "active": "💡 激活:{{ .Enable }}\r\n",
       "enabled": "🚨 已启用:{{ .Enable }}\r\n",
       "online": "🌐 连接状态:{{ .Status }}\r\n",
       "lastOnline": "🔙 上次在线: {{ .Time }}\r\n",
-      "email": "📧 邮箱{{ .Email }}\r\n",
-      "upload": "🔼 上传↑{{ .Upload }}\r\n",
-      "download": "🔽 下载↓{{ .Download }}\r\n",
-      "total": "📊 总计{{ .UpDown }} / {{ .Total }}\r\n",
+      "email": "📧 邮箱: {{ .Email }}\r\n",
+      "upload": "🔼 上传: ↑{{ .Upload }}\r\n",
+      "download": "🔽 下载: ↓{{ .Download }}\r\n",
+      "total": "📊 总计: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
       "TGUser": "👤 电报用户:{{ .TelegramID }}\r\n",
       "exhaustedMsg": "🚨 耗尽的 {{ .Type }}:\r\n",
       "exhaustedCount": "🚨 耗尽的 {{ .Type }} 数量:\r\n",
@@ -1030,7 +1453,7 @@
       "backupTime": "🗄 备份时间:{{ .Time }}\r\n",
       "refreshedOn": "\r\n📋🔄 刷新时间:{{ .Time }}\r\n\r\n",
       "yes": "✅ 是的",
-      "no": "❌ 没有",
+      "no": "❌ ",
       "received_id": "🔑📥 ID 已更新。",
       "received_password": "🔑📥 密码已更新。",
       "received_email": "📧📥 邮箱已更新。",
@@ -1077,7 +1500,7 @@
       "ipLimit": "🔢 IP 限制",
       "setTGUser": "👤 设置 Telegram 用户",
       "toggle": "🔘 启用/禁用",
-      "custom": "🔢 风俗",
+      "custom": "🔢 自定义",
       "confirmNumber": "✅ 确认: {{ .Num }}",
       "confirmNumberAdd": "✅ 确认添加:{{ .Num }}",
       "limitTraffic": "🚧 流量限制",
@@ -1091,7 +1514,7 @@
       "change_password": "⚙️🔑 密码",
       "change_email": "⚙️📧 邮箱",
       "change_comment": "⚙️💬 评论",
-      "change_flow": "⚙️🚦 流控",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "重置所有流量",
       "SortedTrafficUsageReport": "排序的流量使用报告"
     },
@@ -1119,4 +1542,4 @@
       "chooseInbound": "选择一个入站"
     }
   }
-}
+}

+ 486 - 63
web/translation/zh-TW.json

@@ -8,15 +8,22 @@
   "save": "儲存",
   "logout": "登出",
   "create": "建立",
+  "add": "新增",
+  "remove": "移除",
   "update": "更新",
   "copy": "複製",
   "copied": "已複製",
+  "more": "更多",
   "download": "下載",
   "remark": "備註",
   "enable": "啟用",
   "protocol": "協議",
   "search": "搜尋",
   "filter": "篩選",
+  "all": "全部",
+  "from": "從",
+  "to": "到",
+  "done": "完成",
   "loading": "載入中...",
   "refresh": "重新整理",
   "clear": "清除",
@@ -41,14 +48,14 @@
   "transmission": "傳輸",
   "host": "主機",
   "path": "路徑",
-  "camouflage": "偽裝",
+  "camouflage": "混淆",
   "status": "狀態",
   "enabled": "開啟",
   "disabled": "關閉",
   "depleted": "耗盡",
   "depletingSoon": "即將耗盡",
   "offline": "離線",
-  "online": "上",
+  "online": "上",
   "domainName": "域名",
   "monitor": "監聽",
   "certificate": "憑證",
@@ -95,8 +102,9 @@
     "dark": "深色",
     "ultraDark": "超深色",
     "dashboard": "系統狀態",
-    "inbounds": "入站列表",
+    "inbounds": "入站",
     "clients": "客戶端",
+    "groups": "群組",
     "nodes": "節點",
     "settings": "面板設定",
     "xray": "Xray 設定",
@@ -123,13 +131,13 @@
       "cpu": "CPU",
       "logicalProcessors": "邏輯處理器",
       "frequency": "頻率",
-      "swap": "交換空間",
+      "swap": "Swap",
       "storage": "儲存",
-      "memory": "記憶體",
+      "memory": "RAM",
       "threads": "執行緒",
       "xrayStatus": "Xray",
       "stopXray": "停止",
-      "restartXray": "重啟",
+      "restartXray": "重",
       "xraySwitch": "版本",
       "xrayUpdates": "Xray 更新",
       "xraySwitchClick": "選擇你要切換到的版本",
@@ -226,9 +234,9 @@
       "customGeoErrUpdateAllIncomplete": "有一個或多個自訂 geo 來源更新失敗",
       "customGeoEmpty": "尚無自訂 geo 來源 — 點擊「新增」以建立",
       "dontRefresh": "安裝中,請勿重新整理此頁面",
-      "logs": "日誌",
+      "logs": "記錄",
       "config": "配置",
-      "backup": "備份和恢復",
+      "backup": "備份",
       "backupTitle": "備份和恢復",
       "exportDatabase": "備份",
       "exportDatabaseDesc": "點擊下載包含當前資料庫備份的 .db 文件到您的設備。",
@@ -241,7 +249,7 @@
       "getConfigError": "檢索設定檔時發生錯誤"
     },
     "inbounds": {
-      "title": "入站列表",
+      "title": "入站",
       "totalDownUp": "總上傳 / 下載",
       "totalUsage": "總用量",
       "inboundCount": "入站數量",
@@ -252,7 +260,7 @@
       "deployTo": "部署到",
       "localPanel": "本機面板",
       "fallbacks": {
-        "title": "回落",
+        "title": "Fallbacks",
         "help": "當此入站的連線未匹配任何用戶時,將其路由到另一個入站。在下方選擇一個子入站,路由欄位(SNI / ALPN / Path / xver)會自動從子入站的傳輸方式填入——大多數情境不需要再調整。每個子入站應監聽 127.0.0.1,security=none。",
         "empty": "尚未新增回落",
         "add": "新增回落",
@@ -269,11 +277,11 @@
         "defaultCatchAll": "預設 — 兜底匹配其餘"
       },
       "protocol": "協議",
-      "port": "埠",
-      "portMap": "埠映射",
+      "port": "連接埠",
+      "portMap": "連接埠對應",
       "traffic": "流量",
       "details": "詳細資訊",
-      "transportConfig": "傳輸配置",
+      "transportConfig": "傳輸",
       "expireDate": "到期時間",
       "createdAt": "建立時間",
       "updatedAt": "更新時間",
@@ -292,10 +300,30 @@
       "delAllClients": "刪除所有客戶端",
       "delAllClientsConfirmTitle": "從「{remark}」中刪除全部 {count} 個客戶端?",
       "delAllClientsConfirmContent": "從此入站中移除每個客戶端並捨棄其流量記錄。入站本身將保留。此操作無法復原。",
+      "attachClients": "附加客戶端到…",
+      "addClientsToGroup": "將客戶端加入群組…",
+      "attachClientsTitle": "從「{remark}」附加客戶端",
+      "attachClientsDesc": "將相同的 {count} 個客戶端(相同 UUID/密碼與共享流量)附加到選定入站。它們仍保留於此入站。",
+      "attachClientsTargets": "目標入站",
+      "attachClientsNoTargets": "沒有可附加的其他相容入站。",
+      "attachClientsResult": "已附加 {attached},已略過 {skipped}。",
+      "attachClientsResultMixed": "已附加 {attached},已略過 {skipped},錯誤 {errors}。",
+      "attachClientsSelectLabel": "要附加的客戶端",
+      "attachClientsSearchPlaceholder": "搜尋電子郵件或備註",
+      "attachClientsStatusDisabled": "已停用",
+      "attachClientsSelectedCount": "已選 {selected}/{total}",
+      "detachClients": "分離客戶端",
+      "detachClientsTitle": "從「{remark}」分離客戶端",
+      "detachClientsDesc": "僅從此入站移除選取的客戶端。客戶端記錄保留(用 Delete 完全移除)。來源共有 {count} 個客戶端。",
+      "detachClientsResult": "已分離 {detached},已略過 {skipped}。",
+      "detachClientsResultMixed": "已分離 {detached},已略過 {skipped},錯誤 {errors}。",
+      "detachClientsSelectLabel": "要分離的客戶端",
       "exportLinksTitle": "匯出入站連結",
       "exportSubsTitle": "匯出訂閱連結",
       "exportAllLinksTitle": "匯出所有入站連結",
       "exportAllSubsTitle": "匯出所有訂閱連結",
+      "exportAllLinksFileName": "所有入站",
+      "exportAllSubsFileName": "所有入站-Subs",
       "inboundJsonTitle": "入站 JSON",
       "deleteClient": "刪除客戶端",
       "deleteClientContent": "確定要刪除客戶端嗎?",
@@ -306,7 +334,7 @@
       "destinationPort": "目標埠",
       "targetAddress": "目標地址",
       "monitorDesc": "留空表示監聽所有 IP",
-      "meansNoLimit": "= 無限制(單位:GB)",
+      "meansNoLimit": "= 無限制。(單位: GB)",
       "totalFlow": "總流量",
       "leaveBlankToNeverExpire": "留空表示永不過期",
       "noRecommendKeepDefault": "建議保留預設值",
@@ -341,7 +369,8 @@
       "IPLimitlogDesc": "IP 歷史日誌(要啟用被禁用的入站流量,請清除日誌)",
       "IPLimitlogclear": "清除日誌",
       "setDefaultCert": "從面板設定證書",
-      "streamTab": "串流",
+      "setDefaultCertEmpty": "面板尚未設定憑證。請先在「設定」中設定。",
+      "streamTab": "傳輸",
       "securityTab": "安全",
       "sniffingTab": "嗅探",
       "sniffingMetadataOnly": "僅中繼資料",
@@ -361,15 +390,14 @@
         "allHelp": "在單一編輯器中編輯包含所有欄位的完整入站物件。",
         "settings": "設定",
         "settingsHelp": "Xray settings 區塊包裝:",
-        "sniffing": "嗅探",
+        "sniffing": "Sniffing",
         "sniffingHelp": "Xray sniffing 區塊包裝:",
-        "stream": "串流",
+        "stream": "Stream",
         "streamHelp": "Xray stream 區塊包裝:",
         "jsonErrorPrefix": "進階 JSON"
       },
       "telegramDesc": "請提供Telegram聊天ID。(在機器人中使用'/id'命令)或({'@'}userinfobot",
       "subscriptionDesc": "要找到你的訂閱 URL,請導航到“詳細資訊”。此外,你可以為多個客戶端使用相同的名稱。",
-      "info": "資訊",
       "same": "相同",
       "inboundData": "入站資料",
       "exportInbound": "匯出入站規則",
@@ -406,6 +434,143 @@
         "getNewmldsa65Error": "取得mldsa65憑證時發生錯誤。",
         "getNewVlessEncError": "取得VlessEnc憑證時發生錯誤。"
       },
+      "form": {
+        "moveUp": "上移",
+        "moveDown": "下移",
+        "addAll": "全部新增",
+        "addAllFallbackTooltip": "為每個尚未連線的符合條件入站新增一個 fallback 列",
+        "peers": "Peers",
+        "addPeer": "新增 peer",
+        "keepAlive": "Keep-alive",
+        "autoSystemRoutesTooltip": "僅 Windows。CIDR 會自動加入系統路由表,使匹配的流量通過 TUN。",
+        "autoOutboundsInterface": "自動出站介面",
+        "autoOutboundsInterfaceTooltip": "出站流量的實體介面。使用 'auto' 進行偵測;設定 Auto system routes 時自動啟用。",
+        "rewriteAddress": "改寫地址",
+        "rewritePort": "改寫連接埠",
+        "allowedNetwork": "允許的網路",
+        "followRedirect": "跟隨重新導向",
+        "accounts": "帳號",
+        "allowTransparent": "允許透明",
+        "encryptionMethod": "加密方法",
+        "visionTestseed": "Vision testseed",
+        "version": "版本",
+        "udpIdleTimeout": "UDP 閒置逾時 (s)",
+        "masquerade": "偽裝",
+        "type": "類型",
+        "upstreamUrl": "Upstream URL",
+        "rewriteHost": "改寫 Host",
+        "skipTlsVerify": "略過 TLS 驗證",
+        "directory": "目錄",
+        "statusCode": "狀態碼",
+        "body": "Body",
+        "headers": "標頭",
+        "proxyProtocol": "Proxy Protocol",
+        "requestVersion": "請求版本",
+        "requestMethod": "請求方法",
+        "requestPath": "請求路徑",
+        "requestHeaders": "請求標頭",
+        "responseVersion": "回應版本",
+        "responseStatus": "回應狀態",
+        "responseReason": "回應原因",
+        "responseHeaders": "回應標頭",
+        "heartbeatPeriod": "心跳週期",
+        "serviceName": "服務名稱",
+        "authority": "Authority",
+        "multiMode": "多模式",
+        "maxBufferedUpload": "最大緩衝上傳",
+        "maxUploadSize": "最大上傳大小 (位元組)",
+        "streamUpServer": "Stream-Up 伺服器",
+        "serverMaxHeaderBytes": "伺服器最大標頭位元組",
+        "paddingBytes": "Padding 位元組",
+        "uplinkHttpMethod": "Uplink HTTP 方法",
+        "paddingObfsMode": "Padding 混淆模式",
+        "paddingKey": "Padding Key",
+        "paddingHeader": "Padding Header",
+        "paddingPlacement": "Padding 位置",
+        "paddingMethod": "Padding 方法",
+        "sessionPlacement": "Session 位置",
+        "sessionKey": "Session Key",
+        "sequencePlacement": "Sequence 位置",
+        "sequenceKey": "Sequence Key",
+        "uplinkDataPlacement": "Uplink 資料位置",
+        "uplinkDataKey": "Uplink 資料 Key",
+        "noSseHeader": "無 SSE 標頭",
+        "ttiMs": "TTI (ms)",
+        "uplinkMbps": "上行 (MB/s)",
+        "downlinkMbps": "下行 (MB/s)",
+        "cwndMultiplier": "CWND 倍數",
+        "maxSendingWindow": "最大發送視窗",
+        "externalProxy": "外部代理",
+        "sniPlaceholder": "SNI (預設為 host)",
+        "fingerprint": "指紋",
+        "defaultOption": "預設",
+        "routeMark": "Route Mark",
+        "tcpKeepAliveInterval": "TCP Keep Alive 間隔",
+        "tcpKeepAliveIdle": "TCP Keep Alive Idle",
+        "tcpMaxSeg": "TCP Max Seg",
+        "tcpUserTimeout": "TCP User Timeout",
+        "tcpWindowClamp": "TCP Window Clamp",
+        "tcpFastOpen": "TCP Fast Open",
+        "multipathTcp": "Multipath TCP",
+        "penetrate": "Penetrate",
+        "v6Only": "僅 V6",
+        "tcpCongestion": "TCP Congestion",
+        "dialerProxy": "Dialer Proxy",
+        "trustedXForwardedFor": "信任的 X-Forwarded-For",
+        "addressPortStrategy": "地址+連接埠策略",
+        "tryDelayMs": "嘗試延遲 (ms)",
+        "prioritizeIPv6": "IPv6 優先",
+        "interleave": "Interleave",
+        "maxConcurrentTry": "最大並發嘗試",
+        "customSockopt": "自訂 sockopt",
+        "addCustomOption": "新增自訂選項",
+        "serverNameIndication": "SNI",
+        "cipherSuites": "Cipher Suites",
+        "autoOption": "自動",
+        "minMaxVersion": "最小/最大版本",
+        "rejectUnknownSni": "拒絕未知 SNI",
+        "disableSystemRoot": "停用系統根",
+        "sessionResumption": "工作階段恢復",
+        "oneTimeLoading": "一次性載入",
+        "usageOption": "使用選項",
+        "buildChain": "建立憑證鏈",
+        "echKey": "ECH key",
+        "echConfig": "ECH 設定",
+        "pinnedPeerCertSha256": "釘選對端憑證 SHA-256",
+        "pinnedPeerCertSha256Tip": "對端憑證的 Base64 編碼 SHA-256 雜湊。僅面板使用 — 不寫入伺服器的 xray 設定,但會包含在分享連結中,以便用戶端釘選憑證。",
+        "pinnedPeerCertSha256Placeholder": "base64 雜湊,以逗號分隔",
+        "generateRandomPin": "產生隨機雜湊",
+        "getNewEchCert": "取得新 ECH 憑證",
+        "show": "顯示",
+        "xver": "Xver",
+        "target": "目標",
+        "maxTimeDiff": "最大時間差 (ms)",
+        "minClientVer": "最小客戶端版本",
+        "maxClientVer": "最大客戶端版本",
+        "shortIds": "Short IDs",
+        "spiderX": "SpiderX",
+        "getNewCert": "取得新憑證",
+        "mldsa65Seed": "mldsa65 Seed",
+        "mldsa65Verify": "mldsa65 Verify",
+        "getNewSeed": "取得新 Seed"
+      },
+      "info": {
+        "mode": "模式",
+        "grpcServiceName": "grpc serviceName",
+        "grpcMultiMode": "grpc multiMode",
+        "interfaceName": "介面名稱",
+        "mtu": "MTU",
+        "gateway": "Gateway",
+        "dns": "DNS",
+        "outboundsInterface": "出站介面",
+        "autoSystemRoutes": "自動系統路由",
+        "followRedirect": "FollowRedirect",
+        "auth": "認證",
+        "noKernelTun": "非核心 TUN",
+        "keepAlive": "Keep alive",
+        "peerNumber": "Peer {n}",
+        "peerNumberConfig": "Peer {n} 設定"
+      },
       "stream": {
         "general": {
           "request": "請求",
@@ -456,6 +621,20 @@
       "days": "天",
       "renew": "自動續期",
       "renewDesc": "到期後自動續期。(0 = 停用) (單位: 天)",
+      "searchPlaceholder": "搜尋電子郵件、備註、sub ID、UUID、密碼、auth…",
+      "filterTitle": "篩選客戶端",
+      "clearAllFilters": "清除全部",
+      "sortOldest": "最舊優先",
+      "sortNewest": "最新優先",
+      "sortRecentlyUpdated": "最近更新",
+      "sortRecentlyOnline": "最近上線",
+      "sortEmailAZ": "電子郵件 A→Z",
+      "sortEmailZA": "電子郵件 Z→A",
+      "sortMostTraffic": "流量最多",
+      "sortHighestRemaining": "剩餘最多",
+      "sortExpiringSoonest": "即將到期",
+      "has": "擁有",
+      "hasNot": "不擁有",
       "title": "客戶端",
       "actions": "操作",
       "totalGB": "總上傳/下載 (GB)",
@@ -465,7 +644,10 @@
       "password": "密碼",
       "subId": "訂閱 ID",
       "online": "上線",
-      "email": "信箱",
+      "email": "電子郵件",
+      "group": "群組",
+      "groupDesc": "用於將相關客戶端歸類的邏輯標籤(如團隊、客戶、地區)。可從工具列篩選。",
+      "groupPlaceholder": "如 customer-a",
       "comment": "備註",
       "traffic": "流量",
       "offline": "離線",
@@ -485,15 +667,49 @@
       "noLinks": "沒有可共享的連結 — 請先將此客戶端關聯至支援協定的入站。",
       "link": "連結",
       "resetNotPossible": "請先將此客戶端關聯至入站。",
-      "general": "通用",
+      "general": "一般",
       "resetAllTraffics": "重設所有客戶端流量",
       "resetAllTrafficsTitle": "重設所有客戶端流量?",
       "resetAllTrafficsContent": "所有客戶端的上下行計數器將歸零。配額與到期時間不受影響。此操作無法復原。",
-      "empty": "尚無客戶端 — 新增一個開始使用。",
       "deleteConfirmTitle": "刪除客戶端 {email}?",
       "deleteConfirmContent": "將從所有關聯入站中移除該客戶端並刪除其流量紀錄。此操作無法復原。",
       "deleteSelected": "刪除 ({count})",
       "adjustSelected": "調整 ({count})",
+      "subLinksSelected": "訂閱連結 ({count})",
+      "addToGroupTitle": "將 {count} 個客戶端加入群組",
+      "addToGroupTooltip": "選擇現有群組或輸入新名稱。使用 Ungroup 操作從當前群組移除客戶端。",
+      "addToGroupPlaceholder": "群組名稱",
+      "addToGroupSuccessToast": "已將 {count} 個客戶端加入 {group}",
+      "ungroupSuccessToast": "已清除 {count} 個客戶端的群組",
+      "ungroup": "取消群組",
+      "ungroupConfirmTitle": "將 {count} 個客戶端從其群組中移除?",
+      "ungroupConfirmContent": "清除每個選取客戶端的群組標籤。客戶端本身保留(用 Delete 完全移除)。",
+      "addToGroup": "加入群組",
+      "attach": "附加",
+      "adjust": "調整",
+      "subLinks": "訂閱連結",
+      "selectedCount": "已選 {count} 項",
+      "attachSelected": "附加 ({count})",
+      "attachToInboundsTitle": "將 {count} 個客戶端附加到入站",
+      "attachToInboundsDesc": "將選取的 {count} 個客戶端(相同 UUID/密碼與共享流量)附加到選定入站。它們保留現有附加關係。",
+      "attachToInboundsTargets": "目標入站",
+      "attachToInboundsNoTargets": "沒有可供附加的多用戶入站。",
+      "detachSelected": "分離 ({count})",
+      "detach": "分離",
+      "detachFromInboundsTitle": "從入站分離 {count} 個客戶端",
+      "detachFromInboundsDesc": "從選定入站中移除選取的 {count} 個客戶端。客戶端未附加的配對會被靜默略過。客戶端記錄保留(用 Delete 完全移除)。",
+      "detachFromInboundsTargets": "要分離的入站",
+      "detachFromInboundsNoTargets": "沒有可用的多用戶入站。",
+      "detachFromInboundsResult": "已分離 {detached},已略過 {skipped}。",
+      "detachFromInboundsResultMixed": "已分離 {detached},已略過 {skipped},錯誤 {errors}。",
+      "subLinksTitle": "訂閱連結 ({count})",
+      "subLinkColumn": "訂閱 URL",
+      "subJsonLinkColumn": "訂閱 JSON URL",
+      "subLinksCopyAll": "全部複製",
+      "subLinksCopiedAll": "已複製 {count} 條連結",
+      "subLinksEmpty": "選取的客戶端皆無訂閱 ID。",
+      "subLinksDisabled": "訂閱服務已停用。",
+      "subLinksDisabledHint": "在面板設定 → 訂閱中啟用訂閱以產生連結。",
       "bulkDeleteConfirmTitle": "刪除 {count} 個客戶端?",
       "bulkDeleteConfirmContent": "每個所選客戶端都會從關聯的入站中被移除,其流量紀錄也會被刪除。此操作無法復原。",
       "bulkAdjustTitle": "調整 {count} 個客戶端",
@@ -504,11 +720,12 @@
       "delDepleted": "刪除已耗盡",
       "delDepletedConfirmTitle": "刪除已耗盡的客戶端?",
       "delDepletedConfirmContent": "刪除所有流量配額已用盡或已過期的客戶端。此操作無法復原。",
-      "auth": "Auth",
-      "hysteriaAuth": "Hysteria Auth",
+      "auth": "認證",
+      "hysteriaAuth": "Hysteria 認證",
       "uuid": "UUID",
       "flow": "Flow",
-      "reverseTag": "Reverse tag",
+      "vmessSecurity": "VMess 加密",
+      "reverseTag": "反向標籤",
       "reverseTagPlaceholder": "選用 Reverse tag",
       "telegramId": "Telegram 使用者 ID",
       "telegramIdPlaceholder": "數字形式的 Telegram 使用者 ID (0 = 無)",
@@ -528,12 +745,50 @@
         "delDepleted": "已刪除 {count} 個已耗盡的客戶端"
       }
     },
+    "groups": {
+      "title": "群組",
+      "name": "名稱",
+      "clientCount": "群組中的客戶端",
+      "totalGroups": "群組總數",
+      "totalGroupedClients": "有群組的客戶端",
+      "emptyGroups": "空群組",
+      "addGroup": "新增群組",
+      "createSuccess": "已建立群組「{name}」。",
+      "rename": "重新命名",
+      "renameTitle": "重新命名 {name}",
+      "renameCollision": "已存在名為「{name}」的群組。",
+      "renameSuccess": "已為 {count} 個客戶端重新命名群組。",
+      "deleteConfirmTitle": "刪除群組 {name}?",
+      "deleteConfirmContent": "這將刪除群組並清除 {count} 個客戶端的標籤。客戶端本身不會被刪除。",
+      "deleteSuccess": "已清除 {count} 個客戶端的群組。",
+      "resetTraffic": "重置流量",
+      "resetConfirmTitle": "重置群組 {name} 的流量?",
+      "resetConfirmContent": "這將將此群組中所有 {count} 個客戶端的上行/下行流量歸零。",
+      "resetSuccess": "已重置 {count} 個客戶端的流量。",
+      "adjustSuccess": "已調整 {name} 中的 {count} 個客戶端。",
+      "emptyForAction": "此群組尚無客戶端。",
+      "deleteGroupOnly": "刪除群組(保留客戶端)",
+      "deleteClients": "刪除群組中的客戶端",
+      "deleteClientsConfirmTitle": "刪除 {name} 中的所有客戶端?",
+      "deleteClientsConfirmContent": "這將永久刪除 {count} 個客戶端及其流量記錄。群組標籤亦會被清除。此操作無法復原。",
+      "deleteClientsSuccess": "已刪除 {count} 個客戶端。",
+      "deleteClientsMixed": "已刪除 {ok},已略過 {failed}",
+      "addToGroup": "新增客戶端…",
+      "addToGroupTitle": "將客戶端加入群組「{name}」",
+      "addToGroupDesc": "選擇要加入此群組的客戶端。保留其現有入站附加;僅更改群組標籤。已在此群組中的客戶端不會列出。",
+      "addToGroupEmpty": "沒有其他可加入的客戶端。",
+      "addToGroupResult": "已將 {count} 個客戶端加入 {name}。",
+      "removeFromGroup": "移除客戶端…",
+      "removeFromGroupTitle": "從群組「{name}」移除客戶端",
+      "removeFromGroupDesc": "選擇要從此群組移除的成員。客戶端本身保留(用「刪除群組中的客戶端」完全移除)。",
+      "removeFromGroupResult": "已從 {name} 移除 {count} 個客戶端。"
+    },
     "nodes": {
       "title": "節點",
       "addNode": "新增節點",
       "editNode": "編輯節點",
       "totalNodes": "節點總數",
-      "onlineNodes": "線上",
+      "onlineNodes": "上",
       "offlineNodes": "離線",
       "avgLatency": "平均延遲",
       "name": "名稱",
@@ -542,9 +797,9 @@
       "remark": "備註",
       "scheme": "協議",
       "address": "位址",
-      "port": "埠",
-      "basePath": "基礎路徑",
-      "apiToken": "API 權杖",
+      "port": "連接埠",
+      "basePath": "Base Path",
+      "apiToken": "API Token",
       "apiTokenPlaceholder": "遠端面板設定頁中的權杖",
       "apiTokenHint": "遠端面板在 設定 → API 權杖 中顯示其 API 權杖。",
       "regenerate": "重新產生權杖",
@@ -555,7 +810,7 @@
       "status": "狀態",
       "cpu": "CPU",
       "mem": "記憶體",
-      "uptime": "行時間",
+      "uptime": "行時間",
       "latency": "延遲",
       "lastHeartbeat": "上次心跳",
       "xrayVersion": "Xray 版本",
@@ -570,7 +825,7 @@
       "deleteConfirmTitle": "刪除節點「{name}」?",
       "deleteConfirmContent": "這將停止監控該節點。遠端面板本身不受影響。",
       "statusValues": {
-        "online": "上",
+        "online": "上",
         "offline": "離線",
         "unknown": "未知"
       },
@@ -590,7 +845,7 @@
       "title": "面板設定",
       "save": "儲存",
       "infoDesc": "此處的所有更改都需要儲存並重啟面板才能生效",
-      "restartPanel": "重啟面板",
+      "restartPanel": "重面板",
       "restartPanelDesc": "確定要重啟面板嗎?若重啟後無法訪問面板,請前往伺服器檢視面板日誌資訊",
       "restartPanelSuccess": "面板已成功重新啟動",
       "actions": "操作",
@@ -604,7 +859,7 @@
       "warnDefaultBasePath": "預設根路徑 \"/\" 廣為人知 — 請更改為隨機路徑。",
       "warnDefaultSubPath": "預設訂閱路徑 \"/sub/\" 廣為人知 — 請更改。",
       "warnDefaultJsonPath": "預設 JSON 訂閱路徑 \"/json/\" 廣為人知 — 請更改。",
-      "TGBotSettings": "Telegram 機器人配置",
+      "TGBotSettings": "Telegram 機器人",
       "panelListeningIP": "面板監聽 IP",
       "panelListeningIPDesc": "預設留空監聽所有 IP",
       "panelListeningDomain": "面板監聽域名",
@@ -615,10 +870,12 @@
       "publicKeyPathDesc": "填寫一個 '/' 開頭的絕對路徑",
       "privateKeyPath": "面板證書金鑰檔案路徑",
       "privateKeyPathDesc": "填寫一個 '/' 開頭的絕對路徑",
-      "panelUrlPath": "面板 url 根路徑",
+      "panelUrlPath": "URI 路徑",
       "panelUrlPathDesc": "必須以 '/' 開頭,以 '/' 結尾",
       "pageSize": "分頁大小",
       "pageSizeDesc": "定義入站表的頁面大小。設定 0 表示禁用",
+      "panelProxy": "面板網路代理",
+      "panelProxyDesc": "透過此代理路由面板自身的出站請求(geo 更新、Xray/面板版本檢查、Telegram),以繞過伺服器端對 GitHub/Telegram 的過濾。接受 socks5:// 或 http(s)://,如本地 Xray SOCKS 入站。留空表示直連。",
       "remarkModel": "備註模型和分隔符",
       "datepicker": "日期選擇器",
       "datepickerPlaceholder": "選擇日期",
@@ -630,11 +887,11 @@
       "newPassword": "新密碼",
       "telegramBotEnable": "啟用 Telegram 機器人",
       "telegramBotEnableDesc": "啟用 Telegram 機器人功能",
-      "telegramToken": "Telegram 機器人令牌(token)",
+      "telegramToken": "Telegram Token",
       "telegramTokenDesc": "從 '{'@'}BotFather' 獲取的 Telegram 機器人令牌",
-      "telegramProxy": "SOCKS5 Proxy",
+      "telegramProxy": "SOCKS 代理",
       "telegramProxyDesc": "啟用 SOCKS5 代理連線到 Telegram(根據指南調整設定)",
-      "telegramAPIServer": "Telegram API Server",
+      "telegramAPIServer": "Telegram API 伺服器",
       "telegramAPIServerDesc": "要使用的 Telegram API 伺服器。留空以使用預設伺服器。",
       "telegramChatId": "管理員聊天 ID",
       "telegramChatIdDesc": "Telegram 管理員聊天 ID (多個以逗號分隔)(可通過 {'@'}userinfobot 獲取,或在機器人中使用 '/id' 命令獲取)",
@@ -658,6 +915,8 @@
       "subEnable": "啟用訂閱服務",
       "subEnableDesc": "啟用訂閱服務功能",
       "subJsonEnable": "獨立啟用/停用 JSON 訂閱端點。",
+      "subJsonEnableTitle": "JSON 訂閱",
+      "subClashEnableTitle": "Clash / Mihomo 訂閱",
       "subTitle": "訂閱標題",
       "subTitleDesc": "在VPN客戶端中顯示的標題",
       "subSupportUrl": "支援連結",
@@ -693,7 +952,7 @@
       "subURI": "反向代理 URI",
       "subURIDesc": "用於代理後面的訂閱 URL 的 URI 路徑",
       "externalTrafficInformEnable": "外部交通通知",
-      "externalTrafficInformEnableDesc": "每次流量更新時通知外部 API",
+      "externalTrafficInformEnableDesc": "每次流量更新時通知外部 API",
       "externalTrafficInformURI": "外部流量通知 URI",
       "externalTrafficInformURIDesc": "流量更新將會傳送到此 URI",
       "restartXrayOnClientDisable": "用戶自動停用後重新啟動 Xray",
@@ -703,7 +962,55 @@
       "fragmentSett": "設定",
       "noisesDesc": "啟用 Noises.",
       "noisesSett": "Noises 設定",
-      "mux": "多路複用器",
+      "trustedProxyCidrs": "信任代理 CIDR",
+      "trustedProxyCidrsDesc": "允許設定轉發 host、proto 與客戶端 IP 標頭的 IP/CIDR(逗號分隔)。",
+      "ldap": {
+        "enable": "啟用 LDAP 同步",
+        "host": "LDAP host",
+        "port": "LDAP 連接埠",
+        "useTls": "使用 TLS (LDAPS)",
+        "bindDn": "Bind DN",
+        "passwordConfigured": "已設定;留空以保留目前密碼。",
+        "passwordUnconfigured": "未設定。",
+        "passwordPlaceholder": "已設定 - 輸入新值以取代",
+        "baseDn": "Base DN",
+        "userFilter": "使用者篩選",
+        "userAttr": "使用者屬性 (username/email)",
+        "vlessField": "VLESS flag 屬性",
+        "flagField": "通用 flag 屬性 (選用)",
+        "flagFieldDesc": "若設定,將覆寫 VLESS flag — 如 shadowInactive。",
+        "truthyValues": "Truthy 值",
+        "truthyValuesDesc": "以逗號分隔;預設: true,1,yes,on",
+        "invertFlag": "反轉 flag",
+        "invertFlagDesc": "當屬性表示已停用時啟用 (如 shadowInactive)。",
+        "syncSchedule": "同步排程",
+        "syncScheduleDesc": "類 cron 字串,如 @every 1m",
+        "inboundTags": "入站標籤",
+        "inboundTagsDesc": "允許 LDAP 同步自動建立或刪除客戶端的入站。",
+        "noInbounds": "未找到入站。請先在「入站」中建立。",
+        "autoCreate": "自動建立客戶端",
+        "autoDelete": "自動刪除客戶端",
+        "defaultTotalGb": "預設總流量 (GB)",
+        "defaultExpiryDays": "預設到期 (天)",
+        "defaultIpLimit": "預設 IP 限制"
+      },
+      "subFormats": {
+        "packets": "封包",
+        "length": "長度",
+        "interval": "間隔",
+        "maxSplit": "最大分割",
+        "noises": "雜訊",
+        "noiseItem": "雜訊 №{n}",
+        "type": "類型",
+        "packet": "封包",
+        "delayMs": "延遲 (ms)",
+        "applyTo": "套用至",
+        "addNoise": "+ 雜訊",
+        "concurrency": "並發",
+        "xudpConcurrency": "xudp 並發",
+        "xudpUdp443": "xudp UDP 443"
+      },
+      "mux": "Mux",
       "muxDesc": "在已建立的資料流內傳輸多個獨立的資料流",
       "muxSett": "複用器設定",
       "direct": "直接連線",
@@ -758,6 +1065,9 @@
       "save": "儲存",
       "restart": "重新啟動 Xray",
       "restartSuccess": "Xray 已成功重新啟動",
+      "restartOutputTitle": "Xray 重新啟動輸出",
+      "restartConfirmTitle": "重新啟動 xray?",
+      "restartConfirmContent": "使用已儲存的設定重新載入 xray 服務。",
       "stopSuccess": "Xray 已成功停止",
       "restartError": "重新啟動Xray時發生錯誤。",
       "stopError": "停止Xray時發生錯誤。",
@@ -765,7 +1075,7 @@
       "advancedTemplate": "高階配置",
       "generalConfigs": "常規配置",
       "generalConfigsDesc": "這些選項將決定常規配置",
-      "logConfigs": "日誌",
+      "logConfigs": "記錄",
       "logConfigsDesc": "日誌可能會影響伺服器的效能,建議僅在需要時啟用",
       "blockConfigsDesc": "這些選項將阻止使用者連線到特定協議和網站",
       "basicRouting": "基本路由",
@@ -790,10 +1100,12 @@
       "outboundTestUrl": "出站測試 URL",
       "outboundTestUrlDesc": "測試出站連線時使用的 URL",
       "Torrent": "遮蔽 BitTorrent 協議",
-      "Inbounds": "入站規則",
+      "Inbounds": "入站",
       "InboundsDesc": "接受來自特定客戶端的流量",
-      "Outbounds": "出站規則",
+      "Outbounds": "出站",
       "Balancers": "負載均衡",
+      "balancerTagRequired": "標籤為必填",
+      "balancerSelectorRequired": "至少選擇一個出站",
       "OutboundsDesc": "設定出站流量傳出方式",
       "Routings": "路由規則",
       "RoutingsDesc": "每條規則的優先順序都很重要",
@@ -832,6 +1144,73 @@
         "edit": "編輯規則",
         "useComma": "逗號分隔的項目"
       },
+      "routing": {
+        "dragToReorder": "拖曳以重新排序"
+      },
+      "ruleForm": {
+        "sourceIps": "來源 IP",
+        "sourcePort": "來源連接埠",
+        "vlessRoute": "VLESS 路由",
+        "attributes": "屬性",
+        "value": "值",
+        "user": "使用者",
+        "inboundTags": "入站標籤",
+        "outboundTag": "出站標籤",
+        "balancerTag": "均衡器標籤",
+        "balancerTagTooltip": "透過其中一個已設定的負載均衡器路由流量"
+      },
+      "outboundForm": {
+        "tagDuplicate": "該標籤已被其他出站使用",
+        "tagRequired": "標籤為必填",
+        "tagPlaceholder": "唯一標籤",
+        "localIpPlaceholder": "本地 IP",
+        "addressRequired": "地址為必填",
+        "portRequired": "連接埠為必填",
+        "optional": "選用",
+        "udpOverTcp": "UDP over TCP",
+        "uotVersion": "UoT 版本",
+        "inboundTag": "入站標籤",
+        "inboundTagPlaceholder": "用於路由規則的入站標籤",
+        "responseType": "回應類型",
+        "rewriteNetwork": "改寫網路",
+        "unchanged": "(未變更)",
+        "unchangedAddress": "(未變更) 如 1.1.1.1",
+        "rules": "規則",
+        "ruleN": "規則 {n}",
+        "action": "動作",
+        "redirect": "Redirect",
+        "fragment": "Fragment",
+        "finalRules": "最終規則",
+        "overrideXrayPrivateIp": "覆寫 Xray 預設的私有 IP 封鎖",
+        "blockDelay": "阻斷延遲 (ms)",
+        "reverseSniffing": "反向 sniffing",
+        "workers": "Workers",
+        "reserved": "保留",
+        "minUploadInterval": "最小上傳間隔 (ms)",
+        "maxUploadSizeBytes": "最大上傳大小 (位元組)",
+        "uplinkChunkSize": "Uplink chunk 大小",
+        "noGrpcHeader": "無 gRPC 標頭",
+        "maxConcurrency": "最大並發",
+        "maxConnections": "最大連線",
+        "maxReuseTimes": "最大重用次數",
+        "maxRequestTimes": "最大請求次數",
+        "maxReusableSecs": "最大可重用秒數",
+        "keepAlivePeriod": "keep alive 週期",
+        "authPassword": "認證密碼",
+        "visionTestpre": "Vision testpre",
+        "serverNamePlaceholder": "伺服器名稱",
+        "verifyPeerName": "驗證 peer 名稱",
+        "pinnedSha256": "Pinned SHA256",
+        "shortId": "Short ID",
+        "sockopts": "Sockopts",
+        "keepAliveInterval": "keep alive 間隔",
+        "markFwmark": "Mark (fwmark)",
+        "interface": "介面",
+        "ipv6Only": "僅 IPv6",
+        "acceptProxyProtocol": "接受 proxy protocol",
+        "tcpUserTimeoutMs": "TCP user timeout (ms)",
+        "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
+      },
       "outbound": {
         "addOutbound": "新增出站",
         "addReverse": "新增反向",
@@ -844,7 +1223,7 @@
         "tagDesc": "唯一標籤",
         "address": "地址",
         "reverse": "反向",
-        "domain": "域",
+        "domain": "域",
         "type": "類型",
         "bridge": "Bridge",
         "portal": "Portal",
@@ -860,6 +1239,8 @@
         "testSuccess": "測試成功",
         "testFailed": "測試失敗",
         "testError": "測試出站失敗",
+        "testModeTooltip": "TCP: 快速 dial-only 探測。HTTP: 透過 xray 的完整請求。",
+        "testAll": "全部測試",
         "nordvpn": "NordVPN",
         "accessToken": "訪問令牌",
         "country": "國家",
@@ -876,6 +1257,16 @@
         "balancerSelectors": "選擇器",
         "tag": "標籤",
         "tagDesc": "唯一標籤",
+        "tagDuplicate": "該標籤已被其他均衡器使用",
+        "tagPlaceholder": "唯一均衡器標籤",
+        "selector": "選擇器",
+        "fallback": "Fallback",
+        "expected": "期望",
+        "expectedPlaceholder": "最佳節點數",
+        "maxRtt": "最大 RTT",
+        "tolerance": "容差",
+        "baselines": "Baselines",
+        "costs": "Costs",
         "balancerDesc": "無法同時使用 balancerTag 和 outboundTag。如果同時使用,則只有 outboundTag 會生效。"
       },
       "wireguard": {
@@ -892,6 +1283,38 @@
         "userLevel": "用戶級別",
         "userLevelDesc": "通過此入站的所有連接都將使用此用戶級別。預設值為 0"
       },
+      "nord": {
+        "accessToken": "Access token",
+        "privateKey": "私鑰",
+        "noServers": "未找到選定國家/地區的伺服器",
+        "noPublicKey": "選定的伺服器未公布 NordLynx 公鑰。",
+        "outboundAdded": "NordVPN 出站已新增",
+        "outboundUpdated": "NordVPN 出站已更新"
+      },
+      "warp": {
+        "licenseError": "設定 WARP 授權失敗。",
+        "fetchFirst": "請先取得 WARP 設定。",
+        "createAccount": "建立 WARP 帳號",
+        "accessToken": "Access token",
+        "deviceId": "裝置 ID",
+        "licenseKey": "授權金鑰",
+        "privateKey": "私鑰",
+        "deleteAccount": "刪除帳號",
+        "settings": "設定",
+        "licenseKeyLabel": "WARP / WARP+ 授權金鑰",
+        "key": "金鑰",
+        "keyPlaceholder": "26 位 WARP+ 金鑰",
+        "accountInfo": "帳號資訊",
+        "deviceName": "裝置名稱",
+        "deviceModel": "裝置型號",
+        "deviceEnabled": "裝置已啟用",
+        "accountType": "帳號類型",
+        "role": "角色",
+        "warpPlusData": "WARP+ 資料",
+        "quota": "配額",
+        "usage": "使用",
+        "addOutbound": "新增出站"
+      },
       "dns": {
         "enable": "啟用 DNS",
         "enableDesc": "啟用內建 DNS 伺服器",
@@ -911,7 +1334,7 @@
         "strategyDesc": "解析域名的總體策略",
         "add": "新增伺服器",
         "edit": "編輯伺服器",
-        "domains": "域",
+        "domains": "域",
         "expectIPs": "預期 IP",
         "unexpectIPs": "意外IP",
         "useSystemHosts": "使用系統Hosts",
@@ -962,7 +1385,7 @@
     "inbounds": "入站",
     "clients": "客戶端",
     "offline": "🔴 離線",
-    "online": "🟢 線",
+    "online": "🟢 線",
     "commands": {
       "unknown": "❗ 未知命令",
       "pleaseChoose": "👇 請選擇:\r\n",
@@ -992,35 +1415,35 @@
       "2faFailed": "2FA 失敗",
       "report": "🕰 定時報告:{{ .RunTime }}\r\n",
       "datetime": "⏰ 日期時間:{{ .DateTime }}\r\n",
-      "hostname": "💻 主機名:{{ .Hostname }}\r\n",
+      "hostname": "💻 主機: {{ .Hostname }}\r\n",
       "version": "🚀 X-UI 版本:{{ .Version }}\r\n",
       "xrayVersion": "📡 Xray 版本: {{ .XrayVersion }}\r\n",
-      "ipv6": "🌐 IPv6{{ .IPv6 }}\r\n",
-      "ipv4": "🌐 IPv4{{ .IPv4 }}\r\n",
-      "ip": "🌐 IP{{ .IP }}\r\n",
-      "ips": "🔢 IP 地址:\r\n{{ .IPs }}\r\n",
+      "ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
+      "ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
+      "ip": "🌐 IP: {{ .IP }}\r\n",
+      "ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
       "serverUpTime": "⏳ 伺服器執行時間:{{ .UpTime }} {{ .Unit }}\r\n",
       "serverLoad": "📈 伺服器負載:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
-      "serverMemory": "📋 伺服器記憶體:{{ .Current }}/{{ .Total }}\r\n",
-      "tcpCount": "🔹 TCP 連線數:{{ .Count }}\r\n",
-      "udpCount": "🔸 UDP 連線數:{{ .Count }}\r\n",
+      "serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
+      "tcpCount": "🔹 TCP: {{ .Count }}\r\n",
+      "udpCount": "🔸 UDP: {{ .Count }}\r\n",
       "traffic": "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
-      "xrayStatus": "ℹ️ Xray 狀態:{{ .State }}\r\n",
+      "xrayStatus": "ℹ️ 狀態: {{ .State }}\r\n",
       "username": "👤 使用者名稱:{{ .Username }}\r\n",
       "reason": "❗️ 原因:{{ .Reason }}\r\n",
       "time": "⏰ 時間:{{ .Time }}\r\n",
-      "inbound": "📍 入站{{ .Remark }}\r\n",
-      "port": "🔌 埠:{{ .Port }}\r\n",
+      "inbound": "📍 入站: {{ .Remark }}\r\n",
+      "port": "🔌 連接埠: {{ .Port }}\r\n",
       "expire": "📅 過期日期:{{ .Time }}\r\n",
       "expireIn": "📅 剩餘時間:{{ .Time }}\r\n",
       "active": "💡 啟用:{{ .Enable }}\r\n",
       "enabled": "🚨 已啟用:{{ .Enable }}\r\n",
       "online": "🌐 連線狀態:{{ .Status }}\r\n",
       "lastOnline": "🔙 上次上線: {{ .Time }}\r\n",
-      "email": "📧 郵箱:{{ .Email }}\r\n",
-      "upload": "🔼 上傳↑{{ .Upload }}\r\n",
-      "download": "🔽 下載↓{{ .Download }}\r\n",
-      "total": "📊 總計{{ .UpDown }} / {{ .Total }}\r\n",
+      "email": "📧 電子郵件: {{ .Email }}\r\n",
+      "upload": "🔼 上傳: ↑{{ .Upload }}\r\n",
+      "download": "🔽 下載: ↓{{ .Download }}\r\n",
+      "total": "📊 總計: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
       "TGUser": "👤 電報使用者:{{ .TelegramID }}\r\n",
       "exhaustedMsg": "🚨 耗盡的 {{ .Type }}:\r\n",
       "exhaustedCount": "🚨 耗盡的 {{ .Type }} 數量:\r\n",
@@ -1030,7 +1453,7 @@
       "backupTime": "🗄 備份時間:{{ .Time }}\r\n",
       "refreshedOn": "\r\n📋🔄 重新整理時間:{{ .Time }}\r\n\r\n",
       "yes": "✅ 是的",
-      "no": "❌ 沒有",
+      "no": "❌ ",
       "received_id": "🔑📥 ID 已更新。",
       "received_password": "🔑📥 密碼已更新。",
       "received_email": "📧📥 電子郵件已更新。",
@@ -1077,7 +1500,7 @@
       "ipLimit": "🔢 IP 限制",
       "setTGUser": "👤 設定 Telegram 使用者",
       "toggle": "🔘 啟用/禁用",
-      "custom": "🔢 風俗",
+      "custom": "🔢 自訂",
       "confirmNumber": "✅ 確認: {{ .Num }}",
       "confirmNumberAdd": "✅ 確認新增:{{ .Num }}",
       "limitTraffic": "🚧 流量限制",
@@ -1091,7 +1514,7 @@
       "change_password": "⚙️🔑 密碼",
       "change_email": "⚙️📧 電子郵件",
       "change_comment": "⚙️💬 評論",
-      "change_flow": "⚙️🚦 流控",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "重設所有流量",
       "SortedTrafficUsageReport": "排序過的流量使用報告"
     },
@@ -1119,4 +1542,4 @@
       "chooseInbound": "選擇一個入站"
     }
   }
-}
+}

部分文件因文件數量過多而無法顯示