Ver Fonte

feat(sub): add PROTOCOL, TRANSPORT, SECURITY remark template variables

MHSanaei há 9 horas atrás
pai
commit
11c5b53fac

+ 10 - 2
frontend/src/lib/remark/remarkVariables.ts

@@ -3,7 +3,7 @@
 // per client. This file is the single frontend source of truth for the picker
 // UI and the live preview — keep the token list in sync with remark_vars.go.
 
-export type RemarkVarGroup = 'client' | 'traffic' | 'time';
+export type RemarkVarGroup = 'client' | 'traffic' | 'time' | 'connection';
 
 export interface RemarkVar {
   /** Bare token name, e.g. "TRAFFIC_LEFT" (rendered as {{TRAFFIC_LEFT}}). */
@@ -13,7 +13,7 @@ export interface RemarkVar {
   sample: string;
 }
 
-export const REMARK_VAR_GROUPS: RemarkVarGroup[] = ['client', 'traffic', 'time'];
+export const REMARK_VAR_GROUPS: RemarkVarGroup[] = ['client', 'traffic', 'time', 'connection'];
 
 export const REMARK_VARIABLES: RemarkVar[] = [
   // Client identity
@@ -36,11 +36,19 @@ export const REMARK_VARIABLES: RemarkVar[] = [
   { token: 'DOWN', group: 'traffic', sample: '3.20GB' },
   // Time / status
   { token: 'STATUS', group: 'time', sample: 'active' },
+  { token: 'STATUS_EMOJI', group: 'time', sample: '✅' },
   { token: 'DAYS_LEFT', group: 'time', sample: '12' },
+  { token: 'TIME_LEFT', group: 'time', sample: '12d 4h 30m' },
+  { token: 'USAGE_PERCENTAGE', group: 'time', sample: '52.3%' },
   { token: 'EXPIRE_DATE', group: 'time', sample: '2026-09-01' },
+  { token: 'JALALI_EXPIRE_DATE', group: 'time', sample: '1405/06/10' },
   { token: 'EXPIRE_UNIX', group: 'time', sample: '1788300000' },
   { token: 'CREATED_UNIX', group: 'time', sample: '1700000000' },
   { token: 'RESET_DAYS', group: 'time', sample: '30' },
+  // Connection (inbound config descriptors)
+  { token: 'PROTOCOL', group: 'connection', sample: 'VLESS' },
+  { token: 'TRANSPORT', group: 'connection', sample: 'ws' },
+  { token: 'SECURITY', group: 'connection', sample: 'TLS' },
 ];
 
 const SAMPLE_BY_TOKEN: Record<string, string> = Object.fromEntries(

+ 89 - 30
internal/sub/remark_vars.go

@@ -6,7 +6,6 @@ import (
 	"strconv"
 	"strings"
 	"time"
-	"unicode"
 
 	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 	"github.com/mhsanaei/3x-ui/v3/internal/util/common"
@@ -25,6 +24,7 @@ type remarkContext struct {
 	inbound    *model.Inbound
 	hostRemark string
 	transport  string
+	security   string
 }
 
 // configName is the display name for a link: always the inbound's own remark.
@@ -71,6 +71,7 @@ var uiTokenMap = map[string]string{
 	"USAGE_PERCENTAGE":   "USAGE_PERCENTAGE",
 	"PROTOCOL":           "PROTOCOL",
 	"TRANSPORT":          "TRANSPORT",
+	"SECURITY":           "SECURITY",
 }
 
 // translateUISingleBrackets converts user-friendly single-brace tokens to the
@@ -226,6 +227,8 @@ func remarkVarValue(token string, ctx remarkContext) string {
 		return ""
 	case "TRANSPORT":
 		return ctx.transport
+	case "SECURITY":
+		return strings.ToUpper(ctx.security)
 	case "TIME_LEFT":
 		return timeLeftLabel(st.ExpiryTime)
 	case "JALALI_EXPIRE_DATE":
@@ -458,52 +461,107 @@ func (s *SubService) lookupClient(inbound *model.Inbound, email string) model.Cl
 	return model.Client{Email: email}
 }
 
-// usageInfoTokens are the per-client status tokens. On every link of a
-// subscription except the client's first, these (and the decoration leading
-// into them) are dropped, so the traffic/expiry info shows once instead of on
-// every server.
-var usageInfoTokens = []string{
-	"TRAFFIC_USED", "TRAFFIC_LEFT", "TRAFFIC_TOTAL",
-	"TRAFFIC_USED_BYTES", "TRAFFIC_LEFT_BYTES", "TRAFFIC_TOTAL_BYTES",
-	"UP", "DOWN", "DAYS_LEFT", "EXPIRE_DATE", "EXPIRE_UNIX", "STATUS",
-	"STATUS_EMOJI", "USAGE_PERCENTAGE", "TIME_LEFT", "JALALI_EXPIRE_DATE",
+var usageInfoTokens = map[string]bool{
+	"TRAFFIC_USED": true, "TRAFFIC_LEFT": true, "TRAFFIC_TOTAL": true,
+	"TRAFFIC_USED_BYTES": true, "TRAFFIC_LEFT_BYTES": true, "TRAFFIC_TOTAL_BYTES": true,
+	"UP": true, "DOWN": true, "DAYS_LEFT": true, "EXPIRE_DATE": true, "EXPIRE_UNIX": true,
+	"STATUS": true, "STATUS_EMOJI": true, "USAGE_PERCENTAGE": true, "TIME_LEFT": true,
+	"JALALI_EXPIRE_DATE": true,
 }
 
-// nameOnlyTemplate returns template with the trailing per-client info part
-// removed: everything from the first usage token (and the decoration — emojis,
-// spaces, separators — leading into it) onward is dropped, leaving the config
-// name. Returns "" when the template is info-only.
-func nameOnlyTemplate(template string) string {
-	idx := -1
-	for _, tok := range usageInfoTokens {
-		if i := strings.Index(template, "{{"+tok+"}}"); i >= 0 && (idx < 0 || i < idx) {
-			idx = i
+var connectionTokens = map[string]bool{
+	"PROTOCOL":  true,
+	"TRANSPORT": true,
+	"SECURITY":  true,
+}
+
+var displayRemoveTokens = mergeTokenSets(usageInfoTokens, connectionTokens)
+
+func mergeTokenSets(sets ...map[string]bool) map[string]bool {
+	out := make(map[string]bool)
+	for _, set := range sets {
+		for tok := range set {
+			out[tok] = true
 		}
 	}
-	if idx < 0 {
-		return template
+	return out
+}
+
+func filterRemarkTemplate(template string, remove map[string]bool) string {
+	segments := strings.Split(template, "|")
+	kept := make([]string, 0, len(segments))
+	for _, seg := range segments {
+		if out := filterRemarkSegment(seg, remove); out != "" {
+			kept = append(kept, out)
+		}
 	}
-	return strings.TrimRightFunc(template[:idx], func(r rune) bool {
-		return r != '}' && !unicode.IsLetter(r) && !unicode.IsDigit(r)
-	})
+	return strings.Join(kept, "|")
+}
+
+func filterRemarkSegment(seg string, remove map[string]bool) string {
+	locs := remarkVarRe.FindAllStringSubmatchIndex(seg, -1)
+	hasRemove := false
+	for _, loc := range locs {
+		if remove[seg[loc[2]:loc[3]]] {
+			hasRemove = true
+			break
+		}
+	}
+	if !hasRemove {
+		return strings.TrimSpace(seg)
+	}
+	runs := make([]string, 0, 2)
+	runStart, leftRemoved := 0, false
+	for _, loc := range locs {
+		if !remove[seg[loc[2]:loc[3]]] {
+			continue
+		}
+		runs = appendKeptRun(runs, seg[runStart:loc[0]], leftRemoved, true)
+		runStart, leftRemoved = loc[1], true
+	}
+	runs = appendKeptRun(runs, seg[runStart:], leftRemoved, false)
+	return strings.Join(runs, " ")
+}
+
+func appendKeptRun(runs []string, run string, leftRemoved, rightRemoved bool) []string {
+	locs := remarkVarRe.FindAllStringSubmatchIndex(run, -1)
+	if len(locs) == 0 {
+		return runs
+	}
+	start, end := 0, len(run)
+	if leftRemoved {
+		start = locs[0][0]
+	}
+	if rightRemoved {
+		end = locs[len(locs)-1][1]
+	}
+	if frag := strings.TrimSpace(run[start:end]); frag != "" {
+		runs = append(runs, frag)
+	}
+	return runs
 }
 
-// effectiveTemplate picks which template to expand for one body link: the full
-// template (with the per-client info) for a client's first link, and the
-// name-only template for every link thereafter — so the info shows once. Only
-// called in the subscription-body context (displays render name-only directly).
 func (s *SubService) effectiveTemplate(email string) string {
 	translated := translateUISingleBrackets(s.remarkTemplate)
 	if s.usageShown == nil {
 		s.usageShown = map[string]bool{}
 	}
 	if s.usageShown[email] {
-		return nameOnlyTemplate(translated)
+		return filterRemarkTemplate(translated, usageInfoTokens)
 	}
 	s.usageShown[email] = true
 	return translated
 }
 
+func inboundSecurity(inbound *model.Inbound) string {
+	if inbound == nil {
+		return ""
+	}
+	stream := unmarshalStreamSettings(inbound.StreamSettings)
+	security, _ := stream["security"].(string)
+	return security
+}
+
 // genTemplatedRemark expands the remark template for one client. hostRemark is
 // the host endpoint's remark (empty for a plain inbound); it backs the {{HOST}}
 // token only and never substitutes the inbound remark as the config name.
@@ -514,12 +572,13 @@ func (s *SubService) genTemplatedRemark(inbound *model.Inbound, client model.Cli
 		inbound:    inbound,
 		hostRemark: hostRemark,
 		transport:  transport,
+		security:   inboundSecurity(inbound),
 	}
 	var tmpl string
 	if s.subscriptionBody {
 		tmpl = s.effectiveTemplate(client.Email)
 	} else {
-		tmpl = nameOnlyTemplate(translateUISingleBrackets(s.remarkTemplate))
+		tmpl = filterRemarkTemplate(translateUISingleBrackets(s.remarkTemplate), displayRemoveTokens)
 	}
 	if out := expandRemarkVars(tmpl, ctx); strings.TrimSpace(out) != "" {
 		return out

+ 155 - 9
internal/sub/remark_vars_test.go

@@ -251,22 +251,138 @@ func TestRemarkInDisplayContext(t *testing.T) {
 	}
 }
 
-// nameOnlyTemplate drops the info part (and its leading decoration), keeping name.
-func TestNameOnlyTemplate(t *testing.T) {
+func TestFilterRemarkTemplate_BodyRepeat(t *testing.T) {
 	cases := map[string]string{
-		"{{INBOUND}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D": "{{INBOUND}}",           // usage tail stripped
-		"{{EMAIL}} {{INBOUND}} ⏳{{DAYS_LEFT}}":          "{{EMAIL}} {{INBOUND}}", // multi-token name survives the trim
-		"{{INBOUND}} | {{STATUS}}":                      "{{INBOUND}}",
-		"{{INBOUND}}-{{EMAIL}}":                         "{{INBOUND}}-{{EMAIL}}", // no info tokens → unchanged
-		"{{TRAFFIC_LEFT}}":                              "",                      // info only → empty
+		"{{INBOUND}}|📊{{TRAFFIC_LEFT}}|{{PROTOCOL}}-{{TRANSPORT}}-{{SECURITY}}":              "{{INBOUND}}|{{PROTOCOL}}-{{TRANSPORT}}-{{SECURITY}}",
+		"{{INBOUND}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D":                                      "{{INBOUND}}",
+		"{{INBOUND}} {{PROTOCOL}}|📊{{TRAFFIC_LEFT}}":                                         "{{INBOUND}} {{PROTOCOL}}",
+		"{{INBOUND}}-{{EMAIL}}":                                                              "{{INBOUND}}-{{EMAIL}}",
+		"{{TRAFFIC_LEFT}}|{{SECURITY}}":                                                      "{{SECURITY}}",
+		"{{INBOUND}}|📊{{TRAFFIC_LEFT}} {{PROTOCOL}}":                                         "{{INBOUND}}|{{PROTOCOL}}",
+		"{{INBOUND}}|📊{{TRAFFIC_LEFT}}|{{EMAIL}}":                                            "{{INBOUND}}|{{EMAIL}}",
+		"{{INBOUND}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D{{PROTOCOL}}{{TRANSPORT}}{{SECURITY}}": "{{INBOUND}}|{{PROTOCOL}}{{TRANSPORT}}{{SECURITY}}",
+		"{{EMAIL}} {{TRAFFIC_USED}}5h":                                                       "{{EMAIL}}",
+		"{{PROTOCOL}} {{TRAFFIC_LEFT}}GB":                                                    "{{PROTOCOL}}",
+		"{{EMAIL}}-{{TRAFFIC_LEFT}}D-{{HOST}}":                                               "{{EMAIL}} {{HOST}}",
+		"{{EMAIL}} 📊{{TRAFFIC_LEFT}} {{PROTOCOL}}":                                           "{{EMAIL}} {{PROTOCOL}}",
 	}
 	for tmpl, want := range cases {
-		if got := nameOnlyTemplate(tmpl); got != want {
-			t.Errorf("nameOnlyTemplate(%q) = %q, want %q", tmpl, got, want)
+		if got := filterRemarkTemplate(tmpl, usageInfoTokens); got != want {
+			t.Errorf("filterRemarkTemplate(%q, usage) = %q, want %q", tmpl, got, want)
 		}
 	}
 }
 
+func TestFilterRemarkTemplate_Display(t *testing.T) {
+	cases := map[string]string{
+		"{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|{{PROTOCOL}}": "{{INBOUND}}-{{EMAIL}}",
+		"{{INBOUND}} {{PROTOCOL}}":                             "{{INBOUND}}",
+		"{{EMAIL}} {{INBOUND}} ⏳{{DAYS_LEFT}}":                 "{{EMAIL}} {{INBOUND}}",
+		"{{INBOUND}} | {{STATUS}}":                             "{{INBOUND}}",
+		"{{INBOUND}}-{{EMAIL}}":                                "{{INBOUND}}-{{EMAIL}}",
+		"{{TRAFFIC_LEFT}}":                                     "",
+		"{{INBOUND}}|📊{{TRAFFIC_LEFT}}|{{HOST}}":               "{{INBOUND}}|{{HOST}}",
+		"{{EMAIL}} ⏳{{DAYS_LEFT}}D {{HOST}}":                   "{{EMAIL}} {{HOST}}",
+		"{{INBOUND}} {{TRAFFIC_LEFT}} {{EMAIL}}":               "{{INBOUND}} {{EMAIL}}",
+	}
+	for tmpl, want := range cases {
+		if got := filterRemarkTemplate(tmpl, displayRemoveTokens); got != want {
+			t.Errorf("filterRemarkTemplate(%q, display) = %q, want %q", tmpl, got, want)
+		}
+	}
+}
+
+func TestConnectionTokensOnEveryBodyLink(t *testing.T) {
+	s := &SubService{
+		remarkTemplate:   "{{INBOUND}}|📊{{TRAFFIC_LEFT}}|{{PROTOCOL}} {{TRANSPORT}} {{SECURITY}}",
+		subscriptionBody: true,
+		usageShown:       map[string]bool{},
+	}
+	inbound := &model.Inbound{
+		Remark:         "DE",
+		Protocol:       "vless",
+		StreamSettings: `{"network":"ws","security":"tls"}`,
+		ClientStats:    []xray.ClientTraffic{{Email: "john@x", Enable: true, Total: 100 * gb, Up: 30 * gb}},
+	}
+	client := model.Client{Email: "john@x"}
+	first := s.genTemplatedRemark(inbound, client, "", "ws")
+	second := s.genTemplatedRemark(inbound, client, "", "ws")
+	for _, want := range []string{"VLESS", "ws", "TLS"} {
+		if !strings.Contains(first, want) {
+			t.Fatalf("first body link %q missing %q", first, want)
+		}
+		if !strings.Contains(second, want) {
+			t.Fatalf("repeat body link %q missing connection token %q", second, want)
+		}
+	}
+	if strings.ContainsAny(second, "📊") || strings.Contains(second, "GB") {
+		t.Fatalf("repeat body link must drop the usage block: %q", second)
+	}
+}
+
+func TestConnectionTokensMixedIntoUsageSegment(t *testing.T) {
+	s := &SubService{
+		remarkTemplate:   "{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D {{PROTOCOL}} {{TRANSPORT}} {{SECURITY}}",
+		subscriptionBody: true,
+		usageShown:       map[string]bool{},
+	}
+	inbound := &model.Inbound{
+		Remark:         "DE",
+		Protocol:       "vless",
+		StreamSettings: `{"network":"grpc","security":"reality"}`,
+		ClientStats:    []xray.ClientTraffic{{Email: "john@x", Enable: true, Total: 100 * gb, Up: 30 * gb}},
+	}
+	client := model.Client{Email: "john@x"}
+	_ = s.genTemplatedRemark(inbound, client, "", "grpc")
+	second := s.genTemplatedRemark(inbound, client, "", "grpc")
+	for _, want := range []string{"VLESS", "grpc", "REALITY"} {
+		if !strings.Contains(second, want) {
+			t.Fatalf("repeat body link %q missing connection token %q", second, want)
+		}
+	}
+	if strings.Contains(second, "GB") || strings.ContainsRune(second, '⏳') {
+		t.Fatalf("repeat body link must drop the usage block: %q", second)
+	}
+}
+
+func TestConnectionTokensDisplayContextUnchanged(t *testing.T) {
+	s := &SubService{
+		remarkTemplate:   "{{INBOUND}}|📊{{TRAFFIC_LEFT}}|{{PROTOCOL}}",
+		subscriptionBody: false,
+	}
+	inbound := &model.Inbound{
+		Remark:         "DE",
+		Protocol:       "vless",
+		StreamSettings: `{"network":"ws","security":"tls"}`,
+		ClientStats:    []xray.ClientTraffic{{Email: "john@x", Enable: true, Total: 100 * gb, Up: 30 * gb}},
+	}
+	if got := s.genTemplatedRemark(inbound, model.Client{Email: "john@x"}, "", "ws"); got != "DE" {
+		t.Fatalf("display remark = %q, want DE (connection after usage stripped outside the body)", got)
+	}
+}
+
+func TestIdentityTokensEverywhere(t *testing.T) {
+	const tmpl = "{{INBOUND}}|📊{{TRAFFIC_LEFT}}|{{EMAIL}}"
+	inbound := &model.Inbound{
+		Remark:         "DE",
+		Protocol:       "vless",
+		StreamSettings: `{"network":"ws","security":"tls"}`,
+		ClientStats:    []xray.ClientTraffic{{Email: "john@x", Enable: true, Total: 100 * gb, Up: 30 * gb}},
+	}
+	client := model.Client{Email: "john@x"}
+
+	body := &SubService{remarkTemplate: tmpl, subscriptionBody: true, usageShown: map[string]bool{}}
+	_ = body.genTemplatedRemark(inbound, client, "", "ws") // first link consumes the usage block
+	if second := body.genTemplatedRemark(inbound, client, "", "ws"); !strings.Contains(second, "john@x") {
+		t.Fatalf("repeat body link %q must keep the identity token", second)
+	}
+
+	display := &SubService{remarkTemplate: tmpl, subscriptionBody: false}
+	if got := display.genTemplatedRemark(inbound, client, "", "ws"); !strings.Contains(got, "john@x") {
+		t.Fatalf("display remark %q must keep the identity token", got)
+	}
+}
+
 // statsForClient resolves usage from the per-request statsByEmail map when the
 // link's own inbound doesn't carry the client's (globally unique) traffic row —
 // the multi-inbound case that made {{TRAFFIC_LEFT}} show the full quota (#5443).
@@ -377,6 +493,7 @@ func TestExpandNewTokensInTemplate(t *testing.T) {
 		stats:     stats,
 		inbound:   inbound,
 		transport: "ws",
+		security:  "reality",
 	}
 
 	cases := []struct{ tmpl, want string }{
@@ -384,6 +501,7 @@ func TestExpandNewTokensInTemplate(t *testing.T) {
 		{"{{USAGE_PERCENTAGE}}", "50.0%"},
 		{"{{PROTOCOL}}", "VLESS"},
 		{"{{TRANSPORT}}", "ws"},
+		{"{{SECURITY}}", "REALITY"},
 		{"{{STATUS_EMOJI}} {{INBOUND}}", "✅ DE"},
 	}
 	for _, c := range cases {
@@ -393,6 +511,32 @@ func TestExpandNewTokensInTemplate(t *testing.T) {
 	}
 }
 
+func TestInboundSecurity(t *testing.T) {
+	cases := []struct{ stream, want string }{
+		{`{"network":"ws","security":"tls"}`, "tls"},
+		{`{"network":"tcp","security":"reality"}`, "reality"},
+		{`{"network":"tcp","security":"none"}`, "none"},
+		{`{"network":"tcp"}`, ""},
+		{"", ""},
+	}
+	for _, c := range cases {
+		if got := inboundSecurity(&model.Inbound{StreamSettings: c.stream}); got != c.want {
+			t.Errorf("inboundSecurity(%q) = %q, want %q", c.stream, got, c.want)
+		}
+	}
+	if got := inboundSecurity(nil); got != "" {
+		t.Errorf("inboundSecurity(nil) = %q, want empty", got)
+	}
+}
+
+func TestGenTemplatedRemark_SecurityFromStream(t *testing.T) {
+	s := &SubService{remarkTemplate: "{{INBOUND}} {{SECURITY}}", subscriptionBody: true}
+	inbound := &model.Inbound{Remark: "DE", StreamSettings: `{"network":"tcp","security":"reality"}`}
+	if got := s.genTemplatedRemark(inbound, model.Client{Email: "a@x"}, "", "tcp"); got != "DE REALITY" {
+		t.Fatalf("genTemplatedRemark SECURITY = %q, want %q", got, "DE REALITY")
+	}
+}
+
 func TestTranslateUISingleBrackets(t *testing.T) {
 	cases := []struct{ in, want string }{
 		{"{EMAIL}", "{{EMAIL}}"},
@@ -419,6 +563,7 @@ func TestExpandRemarkVars_SingleBracketUI(t *testing.T) {
 		stats:     stats,
 		inbound:   inbound,
 		transport: "ws",
+		security:  "tls",
 	}
 	cases := []struct{ tmpl, want string }{
 		{"{EMAIL}", "[email protected]"},
@@ -429,6 +574,7 @@ func TestExpandRemarkVars_SingleBracketUI(t *testing.T) {
 		{"{USAGE_PERCENTAGE}", "50.0%"},
 		{"{PROTOCOL}", "VLESS"},
 		{"{TRANSPORT}", "ws"},
+		{"{SECURITY}", "TLS"},
 	}
 	for _, c := range cases {
 		if got := expandRemarkVars(c.tmpl, ctx); got != c.want {

+ 10 - 2
internal/web/translation/ar-EG.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "العميل",
           "traffic": "حركة المرور",
-          "time": "الوقت والحالة"
+          "time": "الوقت والحالة",
+          "connection": "الاتصال"
         },
         "descEMAIL": "بريد العميل",
         "descINBOUND": "ملاحظة الوارد نفسه (اسم الإعداد)",
@@ -1840,11 +1841,18 @@
         "descUP": "حركة مرور الرفع",
         "descDOWN": "حركة مرور التنزيل",
         "descSTATUS": "نشط / منتهٍ / معطّل / مستنفد",
+        "descSTATUS_EMOJI": "الحالة كرمز تعبيري (✅ ⏳ 🚫)",
         "descDAYS_LEFT": "الأيام حتى الانتهاء (مخفية إذا كانت غير محدودة)",
+        "descTIME_LEFT": "الوقت المتبقي (مثال: 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "حركة المرور المستخدمة كنسبة مئوية (مخفية إذا كانت غير محدودة)",
         "descEXPIRE_DATE": "تاريخ الانتهاء (YYYY-MM-DD)",
+        "descJALALI_EXPIRE_DATE": "تاريخ الانتهاء بالتقويم الجلالي (YYYY/MM/DD)",
         "descEXPIRE_UNIX": "الانتهاء كطابع زمني Unix (بالثواني)",
         "descCREATED_UNIX": "وقت الإنشاء كطابع زمني Unix (بالثواني)",
-        "descRESET_DAYS": "فترة إعادة تعيين حركة المرور بالأيام"
+        "descRESET_DAYS": "فترة إعادة تعيين حركة المرور بالأيام",
+        "descPROTOCOL": "بروتوكول الوارد (VLESS، VMess، Trojan، …)",
+        "descTRANSPORT": "شبكة النقل (tcp، ws، grpc، …)",
+        "descSECURITY": "أمان النقل (TLS، REALITY، NONE)"
       },
       "toasts": {
         "list": "فشل تحميل المضيفات",

+ 10 - 2
internal/web/translation/en-US.json

@@ -1000,7 +1000,8 @@
         "groups": {
           "client": "Client",
           "traffic": "Traffic",
-          "time": "Time & status"
+          "time": "Time & status",
+          "connection": "Connection"
         },
         "descEMAIL": "Client email",
         "descINBOUND": "Inbound's own remark (the config name)",
@@ -1019,11 +1020,18 @@
         "descUP": "Upload traffic",
         "descDOWN": "Download traffic",
         "descSTATUS": "active / expired / disabled / depleted",
+        "descSTATUS_EMOJI": "Status as an emoji (✅ ⏳ 🚫)",
         "descDAYS_LEFT": "Days until expiry (hidden if unlimited)",
+        "descTIME_LEFT": "Remaining time (e.g. 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "Used traffic as a percentage (hidden if unlimited)",
         "descEXPIRE_DATE": "Expiry date (YYYY-MM-DD)",
+        "descJALALI_EXPIRE_DATE": "Expiry date in the Jalali calendar (YYYY/MM/DD)",
         "descEXPIRE_UNIX": "Expiry as a Unix timestamp (seconds)",
         "descCREATED_UNIX": "Creation time as a Unix timestamp (seconds)",
-        "descRESET_DAYS": "Traffic reset period in days"
+        "descRESET_DAYS": "Traffic reset period in days",
+        "descPROTOCOL": "Inbound protocol (VLESS, VMess, Trojan, …)",
+        "descTRANSPORT": "Transport network (tcp, ws, grpc, …)",
+        "descSECURITY": "Transport security (TLS, REALITY, NONE)"
       },
       "toasts": {
         "list": "Failed to load hosts",

+ 10 - 2
internal/web/translation/es-ES.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "Cliente",
           "traffic": "Tráfico",
-          "time": "Tiempo y estado"
+          "time": "Tiempo y estado",
+          "connection": "Conexión"
         },
         "descEMAIL": "Email del cliente",
         "descINBOUND": "Notas del propio inbound (nombre de la configuración)",
@@ -1840,11 +1841,18 @@
         "descUP": "Tráfico de subida",
         "descDOWN": "Tráfico de bajada",
         "descSTATUS": "activo / expirado / deshabilitado / agotado",
+        "descSTATUS_EMOJI": "Estado como emoji (✅ ⏳ 🚫)",
         "descDAYS_LEFT": "Días hasta la expiración (oculto si es ilimitado)",
+        "descTIME_LEFT": "Tiempo restante (p. ej. 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "Tráfico usado en porcentaje (oculto si es ilimitado)",
         "descEXPIRE_DATE": "Fecha de expiración (AAAA-MM-DD)",
+        "descJALALI_EXPIRE_DATE": "Fecha de expiración en el calendario Jalali (AAAA/MM/DD)",
         "descEXPIRE_UNIX": "Expiración como marca de tiempo Unix (segundos)",
         "descCREATED_UNIX": "Hora de creación como marca de tiempo Unix (segundos)",
-        "descRESET_DAYS": "Periodo de reinicio de tráfico en días"
+        "descRESET_DAYS": "Periodo de reinicio de tráfico en días",
+        "descPROTOCOL": "Protocolo del inbound (VLESS, VMess, Trojan, …)",
+        "descTRANSPORT": "Red de transporte (tcp, ws, grpc, …)",
+        "descSECURITY": "Seguridad del transporte (TLS, REALITY, NONE)"
       },
       "toasts": {
         "list": "Error al cargar los hosts",

+ 10 - 2
internal/web/translation/fa-IR.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "کاربر",
           "traffic": "ترافیک",
-          "time": "زمان و وضعیت"
+          "time": "زمان و وضعیت",
+          "connection": "اتصال"
         },
         "descEMAIL": "ایمیل کاربر",
         "descINBOUND": "نام خود اینباند (نام کانفیگ)",
@@ -1840,11 +1841,18 @@
         "descUP": "ترافیک آپلود",
         "descDOWN": "ترافیک دانلود",
         "descSTATUS": "فعال / منقضی‌شده / غیرفعال / مصرف‌شده",
+        "descSTATUS_EMOJI": "وضعیت به‌صورت ایموجی (✅ ⏳ 🚫)",
         "descDAYS_LEFT": "روزهای باقی‌مانده تا انقضا (در صورت نامحدود بودن پنهان می‌شود)",
+        "descTIME_LEFT": "زمان باقی‌مانده (مثلاً 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "ترافیک مصرف‌شده به درصد (در صورت نامحدود بودن پنهان می‌شود)",
         "descEXPIRE_DATE": "تاریخ انقضا (YYYY-MM-DD)",
+        "descJALALI_EXPIRE_DATE": "تاریخ انقضا در تقویم جلالی (YYYY/MM/DD)",
         "descEXPIRE_UNIX": "انقضا به‌صورت مهر زمانی Unix (ثانیه)",
         "descCREATED_UNIX": "زمان ایجاد به‌صورت مهر زمانی Unix (ثانیه)",
-        "descRESET_DAYS": "دورهٔ بازنشانی ترافیک به روز"
+        "descRESET_DAYS": "دورهٔ بازنشانی ترافیک به روز",
+        "descPROTOCOL": "پروتکل اینباند (VLESS، VMess، Trojan، …)",
+        "descTRANSPORT": "شبکهٔ انتقال (tcp، ws، grpc، …)",
+        "descSECURITY": "امنیت انتقال (TLS، REALITY، NONE)"
       },
       "toasts": {
         "list": "بارگذاری میزبان‌ها ناموفق",

+ 10 - 2
internal/web/translation/id-ID.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "Klien",
           "traffic": "Trafik",
-          "time": "Waktu & status"
+          "time": "Waktu & status",
+          "connection": "Koneksi"
         },
         "descEMAIL": "Email klien",
         "descINBOUND": "Catatan inbound itu sendiri (nama konfigurasi)",
@@ -1840,11 +1841,18 @@
         "descUP": "Trafik unggah",
         "descDOWN": "Trafik unduh",
         "descSTATUS": "aktif / kedaluwarsa / nonaktif / habis",
+        "descSTATUS_EMOJI": "Status sebagai emoji (✅ ⏳ 🚫)",
         "descDAYS_LEFT": "Hari hingga kedaluwarsa (disembunyikan jika tanpa batas)",
+        "descTIME_LEFT": "Waktu tersisa (mis. 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "Trafik terpakai dalam persentase (disembunyikan jika tanpa batas)",
         "descEXPIRE_DATE": "Tanggal kedaluwarsa (YYYY-MM-DD)",
+        "descJALALI_EXPIRE_DATE": "Tanggal kedaluwarsa dalam kalender Jalali (YYYY/MM/DD)",
         "descEXPIRE_UNIX": "Kedaluwarsa sebagai timestamp Unix (detik)",
         "descCREATED_UNIX": "Waktu pembuatan sebagai timestamp Unix (detik)",
-        "descRESET_DAYS": "Periode reset trafik dalam hari"
+        "descRESET_DAYS": "Periode reset trafik dalam hari",
+        "descPROTOCOL": "Protokol inbound (VLESS, VMess, Trojan, …)",
+        "descTRANSPORT": "Jaringan transport (tcp, ws, grpc, …)",
+        "descSECURITY": "Keamanan transport (TLS, REALITY, NONE)"
       },
       "toasts": {
         "list": "Gagal memuat host",

+ 10 - 2
internal/web/translation/ja-JP.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "クライアント",
           "traffic": "トラフィック",
-          "time": "時刻とステータス"
+          "time": "時刻とステータス",
+          "connection": "接続"
         },
         "descEMAIL": "クライアントのメール",
         "descINBOUND": "インバウンド自身の備考(設定名)",
@@ -1840,11 +1841,18 @@
         "descUP": "アップロードトラフィック",
         "descDOWN": "ダウンロードトラフィック",
         "descSTATUS": "active / expired / disabled / depleted",
+        "descSTATUS_EMOJI": "絵文字で表したステータス(✅ ⏳ 🚫)",
         "descDAYS_LEFT": "有効期限までの日数(無制限の場合は非表示)",
+        "descTIME_LEFT": "残り時間(例:12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "使用済みトラフィックの割合(無制限の場合は非表示)",
         "descEXPIRE_DATE": "有効期限(YYYY-MM-DD)",
+        "descJALALI_EXPIRE_DATE": "ジャラーリー暦の有効期限(YYYY/MM/DD)",
         "descEXPIRE_UNIX": "有効期限の Unix タイムスタンプ(秒)",
         "descCREATED_UNIX": "作成時刻の Unix タイムスタンプ(秒)",
-        "descRESET_DAYS": "トラフィックリセット周期(日数)"
+        "descRESET_DAYS": "トラフィックリセット周期(日数)",
+        "descPROTOCOL": "インバウンドのプロトコル(VLESS、VMess、Trojan など)",
+        "descTRANSPORT": "トランスポートネットワーク(tcp、ws、grpc など)",
+        "descSECURITY": "トランスポートのセキュリティ(TLS、REALITY、NONE)"
       },
       "toasts": {
         "list": "ホストの読み込みに失敗しました",

+ 10 - 2
internal/web/translation/pt-BR.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "Cliente",
           "traffic": "Tráfego",
-          "time": "Tempo e status"
+          "time": "Tempo e status",
+          "connection": "Conexão"
         },
         "descEMAIL": "Email do cliente",
         "descINBOUND": "Observação da própria entrada (nome da configuração)",
@@ -1840,11 +1841,18 @@
         "descUP": "Tráfego de upload",
         "descDOWN": "Tráfego de download",
         "descSTATUS": "ativo / expirado / desativado / esgotado",
+        "descSTATUS_EMOJI": "Status como emoji (✅ ⏳ 🚫)",
         "descDAYS_LEFT": "Dias até a expiração (oculto se ilimitado)",
+        "descTIME_LEFT": "Tempo restante (ex.: 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "Tráfego usado como porcentagem (oculto se ilimitado)",
         "descEXPIRE_DATE": "Data de expiração (AAAA-MM-DD)",
+        "descJALALI_EXPIRE_DATE": "Data de expiração no calendário Jalali (AAAA/MM/DD)",
         "descEXPIRE_UNIX": "Expiração como timestamp Unix (segundos)",
         "descCREATED_UNIX": "Data de criação como timestamp Unix (segundos)",
-        "descRESET_DAYS": "Período de redefinição de tráfego em dias"
+        "descRESET_DAYS": "Período de redefinição de tráfego em dias",
+        "descPROTOCOL": "Protocolo da entrada (VLESS, VMess, Trojan, …)",
+        "descTRANSPORT": "Rede de transporte (tcp, ws, grpc, …)",
+        "descSECURITY": "Segurança do transporte (TLS, REALITY, NONE)"
       },
       "toasts": {
         "list": "Falha ao carregar os hosts",

+ 10 - 2
internal/web/translation/ru-RU.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "Клиент",
           "traffic": "Трафик",
-          "time": "Время и статус"
+          "time": "Время и статус",
+          "connection": "Подключение"
         },
         "descEMAIL": "Email клиента",
         "descINBOUND": "Собственное примечание входящего (имя конфигурации)",
@@ -1840,11 +1841,18 @@
         "descUP": "Исходящий трафик",
         "descDOWN": "Входящий трафик",
         "descSTATUS": "активен / истёк / отключён / исчерпан",
+        "descSTATUS_EMOJI": "Статус в виде эмодзи (✅ ⏳ 🚫)",
         "descDAYS_LEFT": "Дней до окончания (скрыто при безлимите)",
+        "descTIME_LEFT": "Оставшееся время (например, 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "Использованный трафик в процентах (скрыт при безлимите)",
         "descEXPIRE_DATE": "Дата окончания (ГГГГ-ММ-ДД)",
+        "descJALALI_EXPIRE_DATE": "Дата окончания по календарю Jalali (ГГГГ/ММ/ДД)",
         "descEXPIRE_UNIX": "Окончание в виде Unix-метки времени (секунды)",
         "descCREATED_UNIX": "Время создания в виде Unix-метки времени (секунды)",
-        "descRESET_DAYS": "Период сброса трафика в днях"
+        "descRESET_DAYS": "Период сброса трафика в днях",
+        "descPROTOCOL": "Протокол входящего (VLESS, VMess, Trojan, …)",
+        "descTRANSPORT": "Транспортная сеть (tcp, ws, grpc, …)",
+        "descSECURITY": "Безопасность транспорта (TLS, REALITY, NONE)"
       },
       "toasts": {
         "list": "Не удалось загрузить хосты",

+ 10 - 2
internal/web/translation/tr-TR.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "Kullanıcı",
           "traffic": "Trafik",
-          "time": "Zaman ve durum"
+          "time": "Zaman ve durum",
+          "connection": "Bağlantı"
         },
         "descEMAIL": "Kullanıcı e-postası",
         "descINBOUND": "Gelen bağlantının kendi açıklaması (yapılandırma adı)",
@@ -1840,11 +1841,18 @@
         "descUP": "Yükleme trafiği",
         "descDOWN": "İndirme trafiği",
         "descSTATUS": "aktif / süresi dolmuş / devre dışı / tükenmiş",
+        "descSTATUS_EMOJI": "Emoji olarak durum (✅ ⏳ 🚫)",
         "descDAYS_LEFT": "Süre dolana kadar kalan gün (sınırsızsa gizlenir)",
+        "descTIME_LEFT": "Kalan süre (örn. 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "Kullanılan trafik yüzde olarak (sınırsızsa gizlenir)",
         "descEXPIRE_DATE": "Son kullanma tarihi (YYYY-AA-GG)",
+        "descJALALI_EXPIRE_DATE": "Celali takvimine göre son kullanma tarihi (YYYY/MM/DD)",
         "descEXPIRE_UNIX": "Son kullanma Unix zaman damgası olarak (saniye)",
         "descCREATED_UNIX": "Oluşturulma zamanı Unix zaman damgası olarak (saniye)",
-        "descRESET_DAYS": "Trafik sıfırlama periyodu (gün)"
+        "descRESET_DAYS": "Trafik sıfırlama periyodu (gün)",
+        "descPROTOCOL": "Gelen bağlantı protokolü (VLESS, VMess, Trojan, …)",
+        "descTRANSPORT": "Taşıma ağı (tcp, ws, grpc, …)",
+        "descSECURITY": "Taşıma güvenliği (TLS, REALITY, NONE)"
       },
       "toasts": {
         "list": "Host'lar yüklenemedi",

+ 10 - 2
internal/web/translation/uk-UA.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "Клієнт",
           "traffic": "Трафік",
-          "time": "Час і статус"
+          "time": "Час і статус",
+          "connection": "З'єднання"
         },
         "descEMAIL": "Email клієнта",
         "descINBOUND": "Власна примітка вхідного (назва конфігурації)",
@@ -1840,11 +1841,18 @@
         "descUP": "Вихідний трафік",
         "descDOWN": "Вхідний трафік",
         "descSTATUS": "active / expired / disabled / depleted",
+        "descSTATUS_EMOJI": "Статус у вигляді емодзі (✅ ⏳ 🚫)",
         "descDAYS_LEFT": "Днів до закінчення (прихований, якщо безлімітний)",
+        "descTIME_LEFT": "Залишок часу (напр. 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "Використаний трафік у відсотках (прихований, якщо безлімітний)",
         "descEXPIRE_DATE": "Дата закінчення (YYYY-MM-DD)",
+        "descJALALI_EXPIRE_DATE": "Дата закінчення за календарем Jalali (YYYY/MM/DD)",
         "descEXPIRE_UNIX": "Закінчення як мітка часу Unix (секунди)",
         "descCREATED_UNIX": "Час створення як мітка часу Unix (секунди)",
-        "descRESET_DAYS": "Період скидання трафіку в днях"
+        "descRESET_DAYS": "Період скидання трафіку в днях",
+        "descPROTOCOL": "Протокол вхідного (VLESS, VMess, Trojan, …)",
+        "descTRANSPORT": "Транспортна мережа (tcp, ws, grpc, …)",
+        "descSECURITY": "Безпека транспорту (TLS, REALITY, NONE)"
       },
       "toasts": {
         "list": "Не вдалося завантажити хости",

+ 10 - 2
internal/web/translation/vi-VN.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "Khách hàng",
           "traffic": "Lưu lượng",
-          "time": "Thời gian & trạng thái"
+          "time": "Thời gian & trạng thái",
+          "connection": "Kết nối"
         },
         "descEMAIL": "Email khách hàng",
         "descINBOUND": "Ghi chú của chính inbound (tên cấu hình)",
@@ -1840,11 +1841,18 @@
         "descUP": "Lưu lượng tải lên",
         "descDOWN": "Lưu lượng tải xuống",
         "descSTATUS": "active / expired / disabled / depleted",
+        "descSTATUS_EMOJI": "Trạng thái dạng biểu tượng cảm xúc (✅ ⏳ 🚫)",
         "descDAYS_LEFT": "Số ngày đến khi hết hạn (ẩn nếu không giới hạn)",
+        "descTIME_LEFT": "Thời gian còn lại (ví dụ 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "Lưu lượng đã dùng tính theo phần trăm (ẩn nếu không giới hạn)",
         "descEXPIRE_DATE": "Ngày hết hạn (YYYY-MM-DD)",
+        "descJALALI_EXPIRE_DATE": "Ngày hết hạn theo lịch Jalali (YYYY/MM/DD)",
         "descEXPIRE_UNIX": "Hết hạn dạng dấu thời gian Unix (giây)",
         "descCREATED_UNIX": "Thời điểm tạo dạng dấu thời gian Unix (giây)",
-        "descRESET_DAYS": "Chu kỳ đặt lại lưu lượng tính theo ngày"
+        "descRESET_DAYS": "Chu kỳ đặt lại lưu lượng tính theo ngày",
+        "descPROTOCOL": "Giao thức inbound (VLESS, VMess, Trojan, …)",
+        "descTRANSPORT": "Mạng truyền tải (tcp, ws, grpc, …)",
+        "descSECURITY": "Bảo mật truyền tải (TLS, REALITY, NONE)"
       },
       "toasts": {
         "list": "Không tải được danh sách host",

+ 10 - 2
internal/web/translation/zh-CN.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "客户端",
           "traffic": "流量",
-          "time": "时间与状态"
+          "time": "时间与状态",
+          "connection": "连接"
         },
         "descEMAIL": "客户端邮箱",
         "descINBOUND": "入站本身的备注(配置名称)",
@@ -1840,11 +1841,18 @@
         "descUP": "上传流量",
         "descDOWN": "下载流量",
         "descSTATUS": "active / expired / disabled / depleted",
+        "descSTATUS_EMOJI": "以表情符号显示状态(✅ ⏳ 🚫)",
         "descDAYS_LEFT": "距到期天数(无限制时隐藏)",
+        "descTIME_LEFT": "剩余时间(例如 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "已用流量百分比(无限制时隐藏)",
         "descEXPIRE_DATE": "到期日期(YYYY-MM-DD)",
+        "descJALALI_EXPIRE_DATE": "Jalali(波斯)历的到期日期(YYYY/MM/DD)",
         "descEXPIRE_UNIX": "到期时间的 Unix 时间戳(秒)",
         "descCREATED_UNIX": "创建时间的 Unix 时间戳(秒)",
-        "descRESET_DAYS": "流量重置周期(天)"
+        "descRESET_DAYS": "流量重置周期(天)",
+        "descPROTOCOL": "入站协议(VLESS、VMess、Trojan……)",
+        "descTRANSPORT": "传输网络(tcp、ws、grpc……)",
+        "descSECURITY": "传输安全(TLS、REALITY、NONE)"
       },
       "toasts": {
         "list": "加载主机失败",

+ 10 - 2
internal/web/translation/zh-TW.json

@@ -1821,7 +1821,8 @@
         "groups": {
           "client": "客戶端",
           "traffic": "流量",
-          "time": "時間與狀態"
+          "time": "時間與狀態",
+          "connection": "連線"
         },
         "descEMAIL": "客戶端電子郵件",
         "descINBOUND": "入站本身的備註(配置名稱)",
@@ -1840,11 +1841,18 @@
         "descUP": "上傳流量",
         "descDOWN": "下載流量",
         "descSTATUS": "active / expired / disabled / depleted",
+        "descSTATUS_EMOJI": "以表情符號表示的狀態(✅ ⏳ 🚫)",
         "descDAYS_LEFT": "距到期天數(無限制時隱藏)",
+        "descTIME_LEFT": "剩餘時間(例如 12d 4h 30m)",
+        "descUSAGE_PERCENTAGE": "已用流量百分比(無限制時隱藏)",
         "descEXPIRE_DATE": "到期日期(YYYY-MM-DD)",
+        "descJALALI_EXPIRE_DATE": "Jalali 曆的到期日期(YYYY/MM/DD)",
         "descEXPIRE_UNIX": "到期時間(Unix 時間戳記,秒)",
         "descCREATED_UNIX": "建立時間(Unix 時間戳記,秒)",
-        "descRESET_DAYS": "流量重置週期(天)"
+        "descRESET_DAYS": "流量重置週期(天)",
+        "descPROTOCOL": "入站協定(VLESS、VMess、Trojan…)",
+        "descTRANSPORT": "傳輸網路(tcp、ws、grpc…)",
+        "descSECURITY": "傳輸安全(TLS、REALITY、NONE)"
       },
       "toasts": {
         "list": "載入 Host 失敗",