diff --git a/frontend/src/lib/remark/remarkVariables.ts b/frontend/src/lib/remark/remarkVariables.ts index 3effc5507..9f44a9297 100644 --- a/frontend/src/lib/remark/remarkVariables.ts +++ b/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 = Object.fromEntries( diff --git a/internal/sub/remark_vars.go b/internal/sub/remark_vars.go index 98ca7bbc5..32da932a0 100644 --- a/internal/sub/remark_vars.go +++ b/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 strings.TrimRightFunc(template[:idx], func(r rune) bool { - return r != '}' && !unicode.IsLetter(r) && !unicode.IsDigit(r) - }) + 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.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 diff --git a/internal/sub/remark_vars_test.go b/internal/sub/remark_vars_test.go index aa0aad810..3b2b709fe 100644 --- a/internal/sub/remark_vars_test.go +++ b/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}", "alice@test.com"}, @@ -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 { diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index b20d22ede..2ae222b6e 100644 --- a/internal/web/translation/ar-EG.json +++ b/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": "فشل تحميل المضيفات", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index f5373bece..f0d429626 100644 --- a/internal/web/translation/en-US.json +++ b/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", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index a1c7fc58a..3bf142e55 100644 --- a/internal/web/translation/es-ES.json +++ b/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", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index feaf5d3d5..e85e6a028 100644 --- a/internal/web/translation/fa-IR.json +++ b/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": "بارگذاری میزبان‌ها ناموفق", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 77eac90ed..ad9292dbe 100644 --- a/internal/web/translation/id-ID.json +++ b/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", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 44731f570..17a9c17b7 100644 --- a/internal/web/translation/ja-JP.json +++ b/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": "ホストの読み込みに失敗しました", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index 68020c614..a08549a7d 100644 --- a/internal/web/translation/pt-BR.json +++ b/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", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 8cd041c7f..e37e3c2d0 100644 --- a/internal/web/translation/ru-RU.json +++ b/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": "Не удалось загрузить хосты", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index fecf6032c..d664adb8c 100644 --- a/internal/web/translation/tr-TR.json +++ b/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", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index f8401be65..42d8388ea 100644 --- a/internal/web/translation/uk-UA.json +++ b/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": "Не вдалося завантажити хости", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 0a7e99c40..9afa3edaa 100644 --- a/internal/web/translation/vi-VN.json +++ b/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", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 67f3c5415..61461c46d 100644 --- a/internal/web/translation/zh-CN.json +++ b/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": "加载主机失败", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index 62335753e..338d846f6 100644 --- a/internal/web/translation/zh-TW.json +++ b/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 失敗",