mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-31 04:12:13 +03:00
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 <x06579@ai-dashboard>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -75,6 +75,7 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"subPort": 1,
|
||||
"subProfileUrl": "",
|
||||
"subRoutingRules": "",
|
||||
"subShowIdentityOnAllLinks": false,
|
||||
"subSupportUrl": "",
|
||||
"subThemeDir": "",
|
||||
"subTitle": "",
|
||||
@@ -186,6 +187,7 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"subPort": 1,
|
||||
"subProfileUrl": "",
|
||||
"subRoutingRules": "",
|
||||
"subShowIdentityOnAllLinks": false,
|
||||
"subSupportUrl": "",
|
||||
"subThemeDir": "",
|
||||
"subTitle": "",
|
||||
|
||||
@@ -244,6 +244,9 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subRoutingRules": {
|
||||
"type": "string"
|
||||
},
|
||||
"subShowIdentityOnAllLinks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subSupportUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -415,6 +418,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subPort",
|
||||
"subProfileUrl",
|
||||
"subRoutingRules",
|
||||
"subShowIdentityOnAllLinks",
|
||||
"subSupportUrl",
|
||||
"subThemeDir",
|
||||
"subTitle",
|
||||
@@ -711,6 +715,9 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subRoutingRules": {
|
||||
"type": "string"
|
||||
},
|
||||
"subShowIdentityOnAllLinks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subSupportUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -889,6 +896,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subPort",
|
||||
"subProfileUrl",
|
||||
"subRoutingRules",
|
||||
"subShowIdentityOnAllLinks",
|
||||
"subSupportUrl",
|
||||
"subThemeDir",
|
||||
"subTitle",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -93,6 +93,16 @@ export default function SubscriptionGeneralTab({ allSetting, updateSetting }: Su
|
||||
maxLength={256}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.subShowIdentityOnAllLinks')}
|
||||
description={t('pages.settings.subShowIdentityOnAllLinksDesc')}
|
||||
>
|
||||
<Switch
|
||||
checked={allSetting.subShowIdentityOnAllLinks}
|
||||
onChange={(v) => updateSetting({ subShowIdentityOnAllLinks: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subUpdates')} description={t('pages.settings.subUpdatesDesc')}>
|
||||
<InputNumber value={allSetting.subUpdates} min={0} max={525600} style={{ width: '100%' }}
|
||||
|
||||
@@ -18,6 +18,7 @@ export const AllSettingSchema = z.object({
|
||||
expireDiff: nonNegativeInt.optional(),
|
||||
trafficDiff: nonNegativeInt.max(100).optional(),
|
||||
remarkTemplate: z.string().optional(),
|
||||
subShowIdentityOnAllLinks: z.boolean().optional(),
|
||||
datepicker: z.enum(['gregorian', 'jalalian']).optional(),
|
||||
tgBotEnable: z.boolean().optional(),
|
||||
tgBotToken: z.string().optional(),
|
||||
|
||||
22
frontend/src/test/setting-sub-identity.test.ts
Normal file
22
frontend/src/test/setting-sub-identity.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AllSettingSchema } from '@/schemas/setting';
|
||||
import { AllSetting } from '@/models/setting';
|
||||
|
||||
describe('subShowIdentityOnAllLinks', () => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
39
internal/web/service/setting_sub_identity_test.go
Normal file
39
internal/web/service/setting_sub_identity_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1435,6 +1435,8 @@
|
||||
"eventMemoryHigh": "ارتفاع استخدام الذاكرة (%)",
|
||||
"remarkTemplate": "قالب الملاحظة",
|
||||
"remarkTemplateDesc": "عند تعيينه، يحل هذا محل نموذج الملاحظة لكل رابط اشتراك — اكتب صيغتك الخاصة باستخدام رموز المتغيرات (استخدم الزر لإدراجها). اتركه فارغاً لاستخدام النموذج أعلاه.",
|
||||
"subShowIdentityOnAllLinks": "إظهار الهوية في كل رابط",
|
||||
"subShowIdentityOnAllLinksDesc": "عند التفعيل، يبقى {{EMAIL}} و{{USERNAME}} في ملاحظة كل رابط في محتوى الاشتراك. تظل رموز الاستخدام في الرابط الأول فقط.",
|
||||
"validation": {
|
||||
"pathLeadingSlash": "يجب أن يبدأ المسار بالرمز /"
|
||||
},
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 /"
|
||||
},
|
||||
|
||||
@@ -1144,6 +1144,8 @@
|
||||
"panelOutboundPh": "اتصال مستقیم",
|
||||
"remarkTemplate": "قالب ریمارک",
|
||||
"remarkTemplateDesc": "اگر پر شود، جای مدلِ ریمارک را برای همهی لینکهای اشتراک میگیرد — فرمت دلخواهت را با توکنهای متغیر بنویس (از دکمه برای درج استفاده کن). خالی = استفاده از مدلِ بالا.",
|
||||
"subShowIdentityOnAllLinks": "نمایش هویت در همه لینکها",
|
||||
"subShowIdentityOnAllLinksDesc": "در صورت فعال بودن، {{EMAIL}} و {{USERNAME}} در یادداشت هر لینک بدنه اشتراک باقی میمانند. توکنهای مصرف همچنان فقط در لینک اول نمایش داده میشوند.",
|
||||
"datepicker": "نوع تقویم",
|
||||
"datepickerPlaceholder": "انتخاب تاریخ",
|
||||
"datepickerDescription": "وظایف برنامه ریزی شده بر اساس این تقویم اجرا میشود",
|
||||
|
||||
@@ -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 /"
|
||||
},
|
||||
|
||||
@@ -1435,6 +1435,8 @@
|
||||
"eventMemoryHigh": "メモリ使用率が高い (%)",
|
||||
"remarkTemplate": "備考テンプレート",
|
||||
"remarkTemplateDesc": "設定すると、すべてのサブスクリプションリンクの備考モデルを置き換えます — 変数トークンを使って独自の形式を記述してください(ボタンで挿入できます)。空欄にすると上記のモデルが使用されます。",
|
||||
"subShowIdentityOnAllLinks": "すべてのリンクに識別情報を表示",
|
||||
"subShowIdentityOnAllLinksDesc": "有効にすると、{{EMAIL}} と {{USERNAME}} がサブスクリプション本文のすべてのリンク備考に残ります。使用量トークンは引き続き最初のリンクのみです。",
|
||||
"validation": {
|
||||
"pathLeadingSlash": "パスは / で始まる必要があります"
|
||||
},
|
||||
|
||||
@@ -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 /"
|
||||
},
|
||||
|
||||
@@ -1435,6 +1435,8 @@
|
||||
"eventMemoryHigh": "Превышение порога памяти (%)",
|
||||
"remarkTemplate": "Шаблон примечания",
|
||||
"remarkTemplateDesc": "Если задан, заменяет модель примечания для каждой ссылки подписки — задайте собственный формат с помощью токенов переменных (используйте кнопку для их вставки). Оставьте пустым, чтобы использовать модель выше.",
|
||||
"subShowIdentityOnAllLinks": "Показывать идентификатор на каждой ссылке",
|
||||
"subShowIdentityOnAllLinksDesc": "Если включено, {{EMAIL}} и {{USERNAME}} остаются в примечании каждой ссылки тела подписки. Токены использования по-прежнему только на первой ссылке.",
|
||||
"validation": {
|
||||
"pathLeadingSlash": "Путь должен начинаться с /"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -1435,6 +1435,8 @@
|
||||
"eventMemoryHigh": "Високе використання пам'яті (%)",
|
||||
"remarkTemplate": "Шаблон примітки",
|
||||
"remarkTemplateDesc": "Якщо задано, це замінює модель примітки для кожного посилання підписки — напишіть власний формат із токенами змінних (використовуйте кнопку для їх вставлення). Залиште порожнім, щоб використовувати модель вище.",
|
||||
"subShowIdentityOnAllLinks": "Показувати ідентичність на кожному посиланні",
|
||||
"subShowIdentityOnAllLinksDesc": "Якщо увімкнено, {{EMAIL}} і {{USERNAME}} залишаються в примітці кожного посилання тіла підписки. Токени використання й надалі лише на першому посиланні.",
|
||||
"validation": {
|
||||
"pathLeadingSlash": "Шлях має починатися з /"
|
||||
},
|
||||
|
||||
@@ -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 /"
|
||||
},
|
||||
|
||||
@@ -1435,6 +1435,8 @@
|
||||
"eventMemoryHigh": "内存使用率高 (%)",
|
||||
"remarkTemplate": "备注模板",
|
||||
"remarkTemplateDesc": "设置后,将替换每个订阅链接的备注模型 — 使用变量标记编写您自己的格式(用按钮插入它们)。留空则使用上方的模型。",
|
||||
"subShowIdentityOnAllLinks": "在每个链接上显示身份",
|
||||
"subShowIdentityOnAllLinksDesc": "启用后,{{EMAIL}} 和 {{USERNAME}} 会保留在每条订阅正文备注中。用量相关变量仍仅出现在第一条链接。",
|
||||
"validation": {
|
||||
"pathLeadingSlash": "路径必须以 / 开头"
|
||||
},
|
||||
|
||||
@@ -1435,6 +1435,8 @@
|
||||
"eventMemoryHigh": "記憶體使用率高 (%)",
|
||||
"remarkTemplate": "備註範本",
|
||||
"remarkTemplateDesc": "設定後,這將取代每個訂閱連結的備註模型——使用變數標記撰寫您自己的格式(使用按鈕來插入)。留空則使用上方的模型。",
|
||||
"subShowIdentityOnAllLinks": "在每個連結上顯示身分",
|
||||
"subShowIdentityOnAllLinksDesc": "啟用後,{{EMAIL}} 與 {{USERNAME}} 會保留在每條訂閱正文備註中。用量相關變數仍僅出現在第一條連結。",
|
||||
"validation": {
|
||||
"pathLeadingSlash": "路徑必須以 / 開頭"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user