From 8f49327efb16e528887fcaca97bab60d44c2f048 Mon Sep 17 00:00:00 2001 From: H-TTTTT <36735327+H-TTTTT@users.noreply.github.com> Date: Wed, 29 Jul 2026 04:12:52 +0800 Subject: [PATCH] feat(sub): allow identity tokens on every subscription link (#5935) Keep usage tokens first-link-only while adding an opt-in setting for repeating EMAIL and USERNAME in subscription-body remarks. Co-authored-by: x06579 --- docs/public/openapi.json | 8 ++++ frontend/public/openapi.json | 8 ++++ frontend/src/generated/examples.ts | 2 + frontend/src/generated/schemas.ts | 8 ++++ frontend/src/generated/types.ts | 2 + frontend/src/generated/zod.ts | 2 + frontend/src/models/setting.ts | 1 + .../pages/settings/SubscriptionGeneralTab.tsx | 10 +++++ frontend/src/schemas/setting.ts | 1 + .../src/test/setting-sub-identity.test.ts | 22 +++++++++++ internal/sub/remark_vars.go | 6 ++- internal/sub/remark_vars_test.go | 37 ++++++++++++++++++ internal/sub/service.go | 11 ++++-- internal/web/entity/entity.go | 11 +++--- internal/web/service/setting.go | 5 +++ .../web/service/setting_sub_identity_test.go | 39 +++++++++++++++++++ internal/web/translation/ar-EG.json | 2 + internal/web/translation/en-US.json | 2 + internal/web/translation/es-ES.json | 2 + internal/web/translation/fa-IR.json | 2 + internal/web/translation/id-ID.json | 2 + internal/web/translation/ja-JP.json | 2 + internal/web/translation/pt-BR.json | 2 + internal/web/translation/ru-RU.json | 2 + internal/web/translation/tr-TR.json | 2 + internal/web/translation/uk-UA.json | 2 + internal/web/translation/vi-VN.json | 2 + internal/web/translation/zh-CN.json | 2 + internal/web/translation/zh-TW.json | 2 + 29 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 frontend/src/test/setting-sub-identity.test.ts create mode 100644 internal/web/service/setting_sub_identity_test.go diff --git a/docs/public/openapi.json b/docs/public/openapi.json index 2310f602c..c1f90a539 100644 --- a/docs/public/openapi.json +++ b/docs/public/openapi.json @@ -411,6 +411,9 @@ "maximum": 65535, "minimum": 1, "type": "integer" + }, + "subShowIdentityOnAllLinks": { + "type": "boolean" } }, "required": [ @@ -479,6 +482,7 @@ "subPort", "subProfileUrl", "subRoutingRules", + "subShowIdentityOnAllLinks", "subSupportUrl", "subThemeDir", "subTitle", @@ -916,6 +920,9 @@ "maximum": 65535, "minimum": 1, "type": "integer" + }, + "subShowIdentityOnAllLinks": { + "type": "boolean" } }, "required": [ @@ -991,6 +998,7 @@ "subPort", "subProfileUrl", "subRoutingRules", + "subShowIdentityOnAllLinks", "subSupportUrl", "subThemeDir", "subTitle", diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 38eae4a3d..386323ae1 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -270,6 +270,9 @@ "subRoutingRules": { "type": "string" }, + "subShowIdentityOnAllLinks": { + "type": "boolean" + }, "subSupportUrl": { "type": "string" }, @@ -441,6 +444,7 @@ "subPort", "subProfileUrl", "subRoutingRules", + "subShowIdentityOnAllLinks", "subSupportUrl", "subThemeDir", "subTitle", @@ -737,6 +741,9 @@ "subRoutingRules": { "type": "string" }, + "subShowIdentityOnAllLinks": { + "type": "boolean" + }, "subSupportUrl": { "type": "string" }, @@ -915,6 +922,7 @@ "subPort", "subProfileUrl", "subRoutingRules", + "subShowIdentityOnAllLinks", "subSupportUrl", "subThemeDir", "subTitle", diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index 8b3649907..0af3b7007 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -75,6 +75,7 @@ export const EXAMPLES: Record = { "subPort": 1, "subProfileUrl": "", "subRoutingRules": "", + "subShowIdentityOnAllLinks": false, "subSupportUrl": "", "subThemeDir": "", "subTitle": "", @@ -186,6 +187,7 @@ export const EXAMPLES: Record = { "subPort": 1, "subProfileUrl": "", "subRoutingRules": "", + "subShowIdentityOnAllLinks": false, "subSupportUrl": "", "subThemeDir": "", "subTitle": "", diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index 8ea931c04..087a6c75a 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -244,6 +244,9 @@ export const SCHEMAS: Record = { "subRoutingRules": { "type": "string" }, + "subShowIdentityOnAllLinks": { + "type": "boolean" + }, "subSupportUrl": { "type": "string" }, @@ -415,6 +418,7 @@ export const SCHEMAS: Record = { "subPort", "subProfileUrl", "subRoutingRules", + "subShowIdentityOnAllLinks", "subSupportUrl", "subThemeDir", "subTitle", @@ -711,6 +715,9 @@ export const SCHEMAS: Record = { "subRoutingRules": { "type": "string" }, + "subShowIdentityOnAllLinks": { + "type": "boolean" + }, "subSupportUrl": { "type": "string" }, @@ -889,6 +896,7 @@ export const SCHEMAS: Record = { "subPort", "subProfileUrl", "subRoutingRules", + "subShowIdentityOnAllLinks", "subSupportUrl", "subThemeDir", "subTitle", diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index c93b746fc..e62b9b3ea 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -81,6 +81,7 @@ export interface AllSetting { subPort: number; subProfileUrl: string; subRoutingRules: string; + subShowIdentityOnAllLinks: boolean; subSupportUrl: string; subThemeDir: string; subTitle: string; @@ -193,6 +194,7 @@ export interface AllSettingView { subPort: number; subProfileUrl: string; subRoutingRules: string; + subShowIdentityOnAllLinks: boolean; subSupportUrl: string; subThemeDir: string; subTitle: string; diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index f54d0c5fe..20ccd393a 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -93,6 +93,7 @@ export const AllSettingSchema = z.object({ subPort: z.number().int().min(1).max(65535), subProfileUrl: z.string(), subRoutingRules: z.string(), + subShowIdentityOnAllLinks: z.boolean(), subSupportUrl: z.string(), subThemeDir: z.string(), subTitle: z.string(), @@ -206,6 +207,7 @@ export const AllSettingViewSchema = z.object({ subPort: z.number().int().min(1).max(65535), subProfileUrl: z.string(), subRoutingRules: z.string(), + subShowIdentityOnAllLinks: z.boolean(), subSupportUrl: z.string(), subThemeDir: z.string(), subTitle: z.string(), diff --git a/frontend/src/models/setting.ts b/frontend/src/models/setting.ts index 8d22e7af9..c5f3d5f16 100644 --- a/frontend/src/models/setting.ts +++ b/frontend/src/models/setting.ts @@ -14,6 +14,7 @@ export class AllSetting { expireDiff = 0; trafficDiff = 0; remarkTemplate = '{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D'; + subShowIdentityOnAllLinks = false; datepicker: 'gregorian' | 'jalalian' = 'gregorian'; tgBotEnable = false; tgBotToken = ''; diff --git a/frontend/src/pages/settings/SubscriptionGeneralTab.tsx b/frontend/src/pages/settings/SubscriptionGeneralTab.tsx index c2ba1f833..32e98ce6c 100644 --- a/frontend/src/pages/settings/SubscriptionGeneralTab.tsx +++ b/frontend/src/pages/settings/SubscriptionGeneralTab.tsx @@ -93,6 +93,16 @@ export default function SubscriptionGeneralTab({ allSetting, updateSetting }: Su maxLength={256} /> + + updateSetting({ subShowIdentityOnAllLinks: v })} + /> + { + it('defaults to false on AllSetting', () => { + expect(new AllSetting().subShowIdentityOnAllLinks).toBe(false); + }); + + it('accepts boolean values in the settings schema', () => { + for (const v of [true, false]) { + const r = AllSettingSchema.safeParse({ subShowIdentityOnAllLinks: v }); + expect(r.success, `subShowIdentityOnAllLinks=${v}`).toBe(true); + } + }); + + it('rejects non-boolean values', () => { + for (const v of ['true', 1, null]) { + expect(AllSettingSchema.safeParse({ subShowIdentityOnAllLinks: v }).success).toBe(false); + } + }); +}); diff --git a/internal/sub/remark_vars.go b/internal/sub/remark_vars.go index 029c7eeb1..40fe76c28 100644 --- a/internal/sub/remark_vars.go +++ b/internal/sub/remark_vars.go @@ -560,7 +560,11 @@ func (s *SubService) effectiveTemplate(email string) string { s.usageShown = map[string]bool{} } if s.usageShown[email] { - return filterRemarkTemplate(translated, firstLinkOnlyBodyTokens) + remove := firstLinkOnlyBodyTokens + if s.showIdentityOnAllLinks { + remove = usageInfoTokens + } + return filterRemarkTemplate(translated, remove) } s.usageShown[email] = true return translated diff --git a/internal/sub/remark_vars_test.go b/internal/sub/remark_vars_test.go index b291a0ed5..d1f975170 100644 --- a/internal/sub/remark_vars_test.go +++ b/internal/sub/remark_vars_test.go @@ -652,3 +652,40 @@ func TestEmailOnFirstLinkOnly(t *testing.T) { t.Fatalf("second link should still carry the inbound name: %q", second) } } + +func TestIdentityOnAllLinks(t *testing.T) { + const template = "{{INBOUND}}-{{EMAIL}}|{{USERNAME}}|📊{{TRAFFIC_LEFT}}|{{STATUS_EMOJI}}" + inbound := &model.Inbound{ + Remark: "DE", + ClientStats: []xray.ClientTraffic{{ + Email: "alice@x", + Enable: true, + Total: 100 * gb, + Up: 20 * gb, + }}, + } + tests := []struct { + name string + enabled bool + wantSecond string + }{ + {name: "disabled", wantSecond: "DE"}, + {name: "enabled", enabled: true, wantSecond: "DE-alice@x|alice@x"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &SubService{ + remarkTemplate: template, + subscriptionBody: true, + showIdentityOnAllLinks: tt.enabled, + } + client := model.Client{Email: "alice@x"} + if got := s.genTemplatedRemark(inbound, client, "", "ws"); got != "DE-alice@x|alice@x|📊80.00GB|✅" { + t.Fatalf("first link = %q", got) + } + if got := s.genTemplatedRemark(inbound, client, "", "ws"); got != tt.wantSecond { + t.Fatalf("second link = %q, want %q", got, tt.wantSecond) + } + }) + } +} diff --git a/internal/sub/service.go b/internal/sub/service.go index d1b062240..983cec894 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -40,9 +40,10 @@ type SubService struct { // usageShown tracks, per client email, whether the info part of the template // has already been emitted this request, so it appears on the first body // link only. Per-request state; reset in PrepareForRequest. - usageShown map[string]bool - inboundService service.InboundService - settingService service.SettingService + usageShown map[string]bool + showIdentityOnAllLinks bool + inboundService service.InboundService + settingService service.SettingService // nodesByID is populated per request from the Node table so // resolveInboundAddress can return the node's address for any // inbound whose NodeID is set. Keeps the per-link host derivation @@ -197,6 +198,10 @@ func (s *SubService) loadRemarkSettings() { if err != nil { s.datepicker = "gregorian" } + s.showIdentityOnAllLinks, err = s.settingService.GetSubShowIdentityOnAllLinks() + if err != nil { + s.showIdentityOnAllLinks = false + } } func (s *SubService) configuredPublicHost() string { diff --git a/internal/web/entity/entity.go b/internal/web/entity/entity.go index 9fc9156d3..a500d57c8 100644 --- a/internal/web/entity/entity.go +++ b/internal/web/entity/entity.go @@ -28,11 +28,12 @@ type AllSetting struct { TrustedProxyCIDRs string `json:"trustedProxyCIDRs" form:"trustedProxyCIDRs"` PanelOutbound string `json:"panelOutbound" form:"panelOutbound"` - PageSize int `json:"pageSize" form:"pageSize" validate:"gte=0,lte=1000"` - ExpireDiff int `json:"expireDiff" form:"expireDiff" validate:"gte=0"` - TrafficDiff int `json:"trafficDiff" form:"trafficDiff" validate:"gte=0,lte=100"` - RemarkTemplate string `json:"remarkTemplate" form:"remarkTemplate"` - Datepicker string `json:"datepicker" form:"datepicker"` + PageSize int `json:"pageSize" form:"pageSize" validate:"gte=0,lte=1000"` + ExpireDiff int `json:"expireDiff" form:"expireDiff" validate:"gte=0"` + TrafficDiff int `json:"trafficDiff" form:"trafficDiff" validate:"gte=0,lte=100"` + RemarkTemplate string `json:"remarkTemplate" form:"remarkTemplate"` + SubShowIdentityOnAllLinks bool `json:"subShowIdentityOnAllLinks" form:"subShowIdentityOnAllLinks"` + Datepicker string `json:"datepicker" form:"datepicker"` TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` TgBotToken string `json:"tgBotToken" form:"tgBotToken"` diff --git a/internal/web/service/setting.go b/internal/web/service/setting.go index 5ab52ec29..39a731099 100644 --- a/internal/web/service/setting.go +++ b/internal/web/service/setting.go @@ -66,6 +66,7 @@ var defaultValueMap = map[string]string{ "expireDiff": "0", "trafficDiff": "0", "remarkTemplate": DefaultRemarkTemplate, + "subShowIdentityOnAllLinks": "false", "timeLocation": "Local", "tgBotEnable": "false", "tgBotToken": "", @@ -649,6 +650,10 @@ func (s *SettingService) GetRemarkTemplate() (string, error) { return s.getString("remarkTemplate") } +func (s *SettingService) GetSubShowIdentityOnAllLinks() (bool, error) { + return s.getBool("subShowIdentityOnAllLinks") +} + func (s *SettingService) GetSecret() ([]byte, error) { secret, err := s.getString("secret") if secret == defaultValueMap["secret"] { diff --git a/internal/web/service/setting_sub_identity_test.go b/internal/web/service/setting_sub_identity_test.go new file mode 100644 index 000000000..004c99139 --- /dev/null +++ b/internal/web/service/setting_sub_identity_test.go @@ -0,0 +1,39 @@ +package service + +import "testing" + +func TestSubShowIdentityOnAllLinksDefaultsAndPersists(t *testing.T) { + setupSettingTestDB(t) + s := &SettingService{} + + if got, err := s.GetSubShowIdentityOnAllLinks(); err != nil || got { + t.Fatalf("missing setting = %t, %v; want false, nil", got, err) + } + settings, err := s.GetAllSetting() + if err != nil { + t.Fatal(err) + } + if settings.SubShowIdentityOnAllLinks { + t.Fatal("GetAllSetting returned true for a missing setting") + } + + settings.SubShowIdentityOnAllLinks = true + if err := s.UpdateAllSetting(settings, SecretClears{}); err != nil { + t.Fatal(err) + } + if got, err := s.GetSubShowIdentityOnAllLinks(); err != nil || !got { + t.Fatalf("persisted setting = %t, %v; want true, nil", got, err) + } + + settings, err = s.GetAllSetting() + if err != nil { + t.Fatal(err) + } + settings.SubShowIdentityOnAllLinks = false + if err := s.UpdateAllSetting(settings, SecretClears{}); err != nil { + t.Fatal(err) + } + if got, err := s.GetSubShowIdentityOnAllLinks(); err != nil || got { + t.Fatalf("persisted setting = %t, %v; want false, nil", got, err) + } +} diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index c3f4736ef..5be2eb596 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -1435,6 +1435,8 @@ "eventMemoryHigh": "ارتفاع استخدام الذاكرة (%)", "remarkTemplate": "قالب الملاحظة", "remarkTemplateDesc": "عند تعيينه، يحل هذا محل نموذج الملاحظة لكل رابط اشتراك — اكتب صيغتك الخاصة باستخدام رموز المتغيرات (استخدم الزر لإدراجها). اتركه فارغاً لاستخدام النموذج أعلاه.", + "subShowIdentityOnAllLinks": "إظهار الهوية في كل رابط", + "subShowIdentityOnAllLinksDesc": "عند التفعيل، يبقى {{EMAIL}} و{{USERNAME}} في ملاحظة كل رابط في محتوى الاشتراك. تظل رموز الاستخدام في الرابط الأول فقط.", "validation": { "pathLeadingSlash": "يجب أن يبدأ المسار بالرمز /" }, diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 98d60791c..9a2e5723f 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -1261,6 +1261,8 @@ "panelOutboundPh": "Direct connection", "remarkTemplate": "Remark Template", "remarkTemplateDesc": "When set, this replaces the remark model for every subscription link — write your own format with the variable tokens (use the button to insert them). Leave empty to use the model above.", + "subShowIdentityOnAllLinks": "Show identity on every link", + "subShowIdentityOnAllLinksDesc": "When enabled, {{EMAIL}} and {{USERNAME}} stay on every subscription-body remark. Usage tokens still appear on the first link only.", "datepicker": "Calendar Type", "datepickerPlaceholder": "Select date", "datepickerDescription": "Scheduled tasks will run based on this calendar.", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index 6d1cb3d85..ad6c20b2f 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -1435,6 +1435,8 @@ "eventMemoryHigh": "Uso de memoria alto (%)", "remarkTemplate": "Plantilla de notas", "remarkTemplateDesc": "Cuando se define, esto reemplaza el modelo de notas para cada enlace de suscripción — escribe tu propio formato con los tokens de variable (usa el botón para insertarlos). Déjalo vacío para usar el modelo anterior.", + "subShowIdentityOnAllLinks": "Mostrar identidad en cada enlace", + "subShowIdentityOnAllLinksDesc": "Si está activado, {{EMAIL}} y {{USERNAME}} permanecen en la nota de cada enlace del cuerpo de la suscripción. Los tokens de uso siguen solo en el primer enlace.", "validation": { "pathLeadingSlash": "La ruta debe comenzar con /" }, diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index a16add0d1..76a04190f 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -1144,6 +1144,8 @@ "panelOutboundPh": "اتصال مستقیم", "remarkTemplate": "قالب ریمارک", "remarkTemplateDesc": "اگر پر شود، جای مدلِ ریمارک را برای همه‌ی لینک‌های اشتراک می‌گیرد — فرمت دلخواهت را با توکن‌های متغیر بنویس (از دکمه برای درج استفاده کن). خالی = استفاده از مدلِ بالا.", + "subShowIdentityOnAllLinks": "نمایش هویت در همه لینک‌ها", + "subShowIdentityOnAllLinksDesc": "در صورت فعال بودن، {{EMAIL}} و {{USERNAME}} در یادداشت هر لینک بدنه اشتراک باقی می‌مانند. توکن‌های مصرف همچنان فقط در لینک اول نمایش داده می‌شوند.", "datepicker": "نوع تقویم", "datepickerPlaceholder": "انتخاب تاریخ", "datepickerDescription": "وظایف برنامه ریزی شده بر اساس این تقویم اجرا می‌شود", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 00cce6efa..cd5511ec7 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -1435,6 +1435,8 @@ "eventMemoryHigh": "Penggunaan memori tinggi (%)", "remarkTemplate": "Templat Catatan", "remarkTemplateDesc": "Jika diatur, ini menggantikan model catatan untuk setiap tautan langganan — tulis format Anda sendiri dengan token variabel (gunakan tombol untuk menyisipkannya). Biarkan kosong untuk memakai model di atas.", + "subShowIdentityOnAllLinks": "Tampilkan identitas di setiap tautan", + "subShowIdentityOnAllLinksDesc": "Jika diaktifkan, {{EMAIL}} dan {{USERNAME}} tetap ada di catatan setiap tautan isi langganan. Token penggunaan tetap hanya di tautan pertama.", "validation": { "pathLeadingSlash": "Path harus diawali dengan /" }, diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 9aebf242b..4c9a65d2e 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -1435,6 +1435,8 @@ "eventMemoryHigh": "メモリ使用率が高い (%)", "remarkTemplate": "備考テンプレート", "remarkTemplateDesc": "設定すると、すべてのサブスクリプションリンクの備考モデルを置き換えます — 変数トークンを使って独自の形式を記述してください(ボタンで挿入できます)。空欄にすると上記のモデルが使用されます。", + "subShowIdentityOnAllLinks": "すべてのリンクに識別情報を表示", + "subShowIdentityOnAllLinksDesc": "有効にすると、{{EMAIL}} と {{USERNAME}} がサブスクリプション本文のすべてのリンク備考に残ります。使用量トークンは引き続き最初のリンクのみです。", "validation": { "pathLeadingSlash": "パスは / で始まる必要があります" }, diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index 6cd5d5902..a84cf14e1 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -1435,6 +1435,8 @@ "eventMemoryHigh": "Uso de memória alto (%)", "remarkTemplate": "Modelo de Observação", "remarkTemplateDesc": "Quando definido, isto substitui o modelo de observação de cada link de assinatura — escreva seu próprio formato com os tokens de variáveis (use o botão para inseri-los). Deixe vazio para usar o modelo acima.", + "subShowIdentityOnAllLinks": "Mostrar identidade em todos os links", + "subShowIdentityOnAllLinksDesc": "Quando ativado, {{EMAIL}} e {{USERNAME}} permanecem na observação de cada link do corpo da assinatura. Tokens de uso continuam só no primeiro link.", "validation": { "pathLeadingSlash": "O caminho deve começar com /" }, diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 65c6f0275..2f3339cd4 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -1435,6 +1435,8 @@ "eventMemoryHigh": "Превышение порога памяти (%)", "remarkTemplate": "Шаблон примечания", "remarkTemplateDesc": "Если задан, заменяет модель примечания для каждой ссылки подписки — задайте собственный формат с помощью токенов переменных (используйте кнопку для их вставки). Оставьте пустым, чтобы использовать модель выше.", + "subShowIdentityOnAllLinks": "Показывать идентификатор на каждой ссылке", + "subShowIdentityOnAllLinksDesc": "Если включено, {{EMAIL}} и {{USERNAME}} остаются в примечании каждой ссылки тела подписки. Токены использования по-прежнему только на первой ссылке.", "validation": { "pathLeadingSlash": "Путь должен начинаться с /" }, diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index 6cdb0abca..bb7ae2e4c 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -1435,6 +1435,8 @@ "eventMemoryHigh": "Bellek kullanımı yüksek (%)", "remarkTemplate": "Açıklama Şablonu", "remarkTemplateDesc": "Ayarlandığında, her abonelik bağlantısının açıklama modelinin yerini alır — değişken belirteçleriyle kendi formatınızı yazın (eklemek için düğmeyi kullanın). Yukarıdaki modeli kullanmak için boş bırakın.", + "subShowIdentityOnAllLinks": "Kimliği her bağlantıda göster", + "subShowIdentityOnAllLinksDesc": "Etkinleştirildiğinde {{EMAIL}} ve {{USERNAME}} abonelik gövdesindeki her bağlantı notunda kalır. Kullanım jetonları yine yalnızca ilk bağlantıda görünür.", "validation": { "pathLeadingSlash": "Yol / ile başlamalıdır" }, diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 3911bf1a2..9d18a845b 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -1435,6 +1435,8 @@ "eventMemoryHigh": "Високе використання пам'яті (%)", "remarkTemplate": "Шаблон примітки", "remarkTemplateDesc": "Якщо задано, це замінює модель примітки для кожного посилання підписки — напишіть власний формат із токенами змінних (використовуйте кнопку для їх вставлення). Залиште порожнім, щоб використовувати модель вище.", + "subShowIdentityOnAllLinks": "Показувати ідентичність на кожному посиланні", + "subShowIdentityOnAllLinksDesc": "Якщо увімкнено, {{EMAIL}} і {{USERNAME}} залишаються в примітці кожного посилання тіла підписки. Токени використання й надалі лише на першому посиланні.", "validation": { "pathLeadingSlash": "Шлях має починатися з /" }, diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index d4268169b..7ab9ec6d0 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -1435,6 +1435,8 @@ "eventMemoryHigh": "Sử dụng bộ nhớ cao (%)", "remarkTemplate": "Mẫu ghi chú", "remarkTemplateDesc": "Khi được đặt, mục này thay thế mô hình ghi chú cho mọi liên kết đăng ký — hãy viết định dạng riêng của bạn bằng các token biến (dùng nút để chèn chúng). Để trống để dùng mô hình ở trên.", + "subShowIdentityOnAllLinks": "Hiện danh tính trên mọi liên kết", + "subShowIdentityOnAllLinksDesc": "Khi bật, {{EMAIL}} và {{USERNAME}} vẫn có trong ghi chú mọi liên kết phần thân đăng ký. Token dung lượng vẫn chỉ ở liên kết đầu tiên.", "validation": { "pathLeadingSlash": "Đường dẫn phải bắt đầu bằng /" }, diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index f3be4eb65..02c664992 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -1435,6 +1435,8 @@ "eventMemoryHigh": "内存使用率高 (%)", "remarkTemplate": "备注模板", "remarkTemplateDesc": "设置后,将替换每个订阅链接的备注模型 — 使用变量标记编写您自己的格式(用按钮插入它们)。留空则使用上方的模型。", + "subShowIdentityOnAllLinks": "在每个链接上显示身份", + "subShowIdentityOnAllLinksDesc": "启用后,{{EMAIL}} 和 {{USERNAME}} 会保留在每条订阅正文备注中。用量相关变量仍仅出现在第一条链接。", "validation": { "pathLeadingSlash": "路径必须以 / 开头" }, diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index 59d1b2654..e0cd84c5d 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -1435,6 +1435,8 @@ "eventMemoryHigh": "記憶體使用率高 (%)", "remarkTemplate": "備註範本", "remarkTemplateDesc": "設定後,這將取代每個訂閱連結的備註模型——使用變數標記撰寫您自己的格式(使用按鈕來插入)。留空則使用上方的模型。", + "subShowIdentityOnAllLinks": "在每個連結上顯示身分", + "subShowIdentityOnAllLinksDesc": "啟用後,{{EMAIL}} 與 {{USERNAME}} 會保留在每條訂閱正文備註中。用量相關變數仍僅出現在第一條連結。", "validation": { "pathLeadingSlash": "路徑必須以 / 開頭" },