mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-03-14 07:03:09 +00:00
Subscription
This commit is contained in:
2836
web/assets/css/custom.min.css
vendored
2836
web/assets/css/custom.min.css
vendored
File diff suppressed because one or more lines are too long
125
web/assets/js/subscription.js
Normal file
125
web/assets/js/subscription.js
Normal file
@@ -0,0 +1,125 @@
|
||||
(function () {
|
||||
// Vue app for Subscription page
|
||||
const el = document.getElementById('subscription-data');
|
||||
if (!el) return;
|
||||
const textarea = document.getElementById('subscription-links');
|
||||
const rawLinks = (textarea?.value || '').split('\n').filter(Boolean);
|
||||
|
||||
const data = {
|
||||
sId: el.getAttribute('data-sid') || '',
|
||||
subUrl: el.getAttribute('data-sub-url') || '',
|
||||
subJsonUrl: el.getAttribute('data-subjson-url') || '',
|
||||
download: el.getAttribute('data-download') || '',
|
||||
upload: el.getAttribute('data-upload') || '',
|
||||
used: el.getAttribute('data-used') || '',
|
||||
total: el.getAttribute('data-total') || '',
|
||||
remained: el.getAttribute('data-remained') || '',
|
||||
expireMs: (parseInt(el.getAttribute('data-expire') || '0', 10) || 0) * 1000,
|
||||
lastOnlineMs: (parseInt(el.getAttribute('data-lastonline') || '0', 10) || 0),
|
||||
downloadByte: parseInt(el.getAttribute('data-downloadbyte') || '0', 10) || 0,
|
||||
uploadByte: parseInt(el.getAttribute('data-uploadbyte') || '0', 10) || 0,
|
||||
totalByte: parseInt(el.getAttribute('data-totalbyte') || '0', 10) || 0,
|
||||
datepicker: el.getAttribute('data-datepicker') || 'gregorian',
|
||||
};
|
||||
|
||||
// Normalize lastOnline to milliseconds if it looks like seconds
|
||||
if (data.lastOnlineMs && data.lastOnlineMs < 10_000_000_000) {
|
||||
data.lastOnlineMs *= 1000;
|
||||
}
|
||||
|
||||
function renderLink(item) {
|
||||
return (
|
||||
Vue.h('a-list-item', {}, [
|
||||
Vue.h('a-space', { props: { size: 'small' } }, [
|
||||
Vue.h('a-button', { props: { size: 'small' }, on: { click: () => copy(item) } }, [Vue.h('a-icon', { props: { type: 'copy' } })]),
|
||||
Vue.h('span', { class: 'break-all' }, item)
|
||||
])
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function copy(text) {
|
||||
ClipboardManager.copyText(text).then(ok => {
|
||||
const messageType = ok ? 'success' : 'error';
|
||||
Vue.prototype.$message[messageType](ok ? 'Copied' : 'Copy failed');
|
||||
});
|
||||
}
|
||||
|
||||
function open(url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function drawQR(value) {
|
||||
try { new QRious({ element: document.getElementById('qrcode'), value, size: 220 }); } catch (e) { console.warn(e); }
|
||||
}
|
||||
|
||||
// Try to extract a human label (email/ps) from different link types
|
||||
function linkName(link, idx) {
|
||||
try {
|
||||
if (link.startsWith('vmess://')) {
|
||||
const json = JSON.parse(atob(link.replace('vmess://', '')));
|
||||
if (json.ps) return json.ps;
|
||||
if (json.add && json.id) return json.add; // fallback host
|
||||
} else if (link.startsWith('vless://') || link.startsWith('trojan://')) {
|
||||
// vless://<id>@host:port?...#name
|
||||
const hashIdx = link.indexOf('#');
|
||||
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
||||
// email sometimes in query params like sni or remark
|
||||
const qIdx = link.indexOf('?');
|
||||
if (qIdx !== -1) {
|
||||
const qs = new URL('http://x/?' + link.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : undefined)).searchParams;
|
||||
if (qs.get('remark')) return qs.get('remark');
|
||||
if (qs.get('email')) return qs.get('email');
|
||||
}
|
||||
// else take user@host
|
||||
const at = link.indexOf('@');
|
||||
const protSep = link.indexOf('://');
|
||||
if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);
|
||||
} else if (link.startsWith('ss://')) {
|
||||
// shadowsocks: label often after #
|
||||
const hashIdx = link.indexOf('#');
|
||||
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
||||
}
|
||||
} catch (e) { /* ignore and fallback */ }
|
||||
return 'Link ' + (idx + 1);
|
||||
}
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
app: data,
|
||||
links: rawLinks,
|
||||
lang: '',
|
||||
viewportWidth: (typeof window !== 'undefined' ? window.innerWidth : 1024),
|
||||
},
|
||||
async mounted() {
|
||||
this.lang = LanguageManager.getLanguage();
|
||||
// Discover subJsonUrl if provided via template bootstrap
|
||||
const tpl = document.getElementById('subscription-data');
|
||||
const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';
|
||||
if (sj) this.app.subJsonUrl = sj;
|
||||
drawQR(this.app.subUrl);
|
||||
// Draw second QR if available
|
||||
try { new QRious({ element: document.getElementById('qrcode-subjson'), value: this.app.subJsonUrl || '', size: 220 }); } catch (e) { /* ignore */ }
|
||||
// Track viewport width for responsive behavior
|
||||
this._onResize = () => { this.viewportWidth = window.innerWidth; };
|
||||
window.addEventListener('resize', this._onResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this._onResize) window.removeEventListener('resize', this._onResize);
|
||||
},
|
||||
computed: {
|
||||
isMobile() { return this.viewportWidth < 576; },
|
||||
isUnlimited() { return !this.app.totalByte; },
|
||||
isActive() {
|
||||
const now = Date.now();
|
||||
const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
|
||||
const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
|
||||
return expiryOk && trafficOk;
|
||||
},
|
||||
},
|
||||
methods: { renderLink, copy, open, linkName, i18nLabel(key) { return '{{ i18n "' + key + '" }}'; } },
|
||||
});
|
||||
})();
|
||||
272
web/html/subscription.html
Normal file
272
web/html/subscription.html
Normal file
@@ -0,0 +1,272 @@
|
||||
{{ template "page/head_start" .}}
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-layout-content class="p-2">
|
||||
<a-row type="flex" justify="center" class="mt-2">
|
||||
<a-col :xs="24" :sm="22" :md="18" :lg="14" :xl="12">
|
||||
<a-card hoverable class="subscription-card">
|
||||
<template #title>
|
||||
<a-space>
|
||||
<span>{{ i18n "subscription.title" }}</span>
|
||||
<a-tag>{{ .sId }}</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-popover
|
||||
:overlay-class-name="themeSwitcher.currentTheme"
|
||||
title='{{ i18n "menu.settings" }}'
|
||||
placement="bottomRight" trigger="click">
|
||||
<template #content>
|
||||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language"
|
||||
}}</span>
|
||||
<a-select ref="selectLang" class="w-100"
|
||||
v-model="lang"
|
||||
@change="LanguageManager.setLanguage(lang)"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value"
|
||||
label="English"
|
||||
v-for="l in LanguageManager.supportedLanguages"
|
||||
:key="l.value">
|
||||
<span role="img"
|
||||
:aria-label="l.name"
|
||||
v-text="l.icon"></span>
|
||||
<span
|
||||
v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-button shape="circle" icon="setting"></a-button>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<a-form layout="vertical">
|
||||
<a-form-item>
|
||||
<a-space direction="vertical" align="center">
|
||||
<a-row type="flex" :gutter="[8,8]"
|
||||
justify="center" style="width:100%">
|
||||
<a-col :xs="24" :sm="12"
|
||||
style="text-align:center;">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="purple"
|
||||
class="qr-tag">
|
||||
<span>{{ i18n
|
||||
"pages.settings.subSettings"}}</span>
|
||||
</a-tag>
|
||||
<tr-qr-bg class="qr-bg-sub">
|
||||
<tr-qr-bg-inner
|
||||
class="qr-bg-sub-inner">
|
||||
<canvas id="qrcode"
|
||||
class="qr-cv"
|
||||
title='{{ i18n "copy" }}'
|
||||
@click="copy(app.subUrl)"></canvas>
|
||||
</tr-qr-bg-inner>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12"
|
||||
style="text-align:center;">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="purple"
|
||||
class="qr-tag">
|
||||
<span>{{ i18n
|
||||
"pages.settings.subSettings"}}
|
||||
Json</span>
|
||||
</a-tag>
|
||||
<tr-qr-bg class="qr-bg-sub">
|
||||
<tr-qr-bg-inner
|
||||
class="qr-bg-sub-inner">
|
||||
<canvas id="qrcode-subjson"
|
||||
class="qr-cv"
|
||||
title='{{ i18n "copy" }}'
|
||||
@click="copy(app.subJsonUrl)"></canvas>
|
||||
</tr-qr-bg-inner>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-descriptions bordered :column="1" size="small">
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.subId" }}'>[[
|
||||
app.sId
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.status" }}'>
|
||||
<template v-if="isUnlimited">
|
||||
<a-tag color="purple">{{ i18n
|
||||
"subscription.unlimited" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag
|
||||
:color="isActive ? 'green' : 'red'">[[
|
||||
isActive ? '{{ i18n
|
||||
"subscription.active" }}' : '{{ i18n
|
||||
"subscription.inactive" }}'
|
||||
]]</a-tag>
|
||||
</template>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.downloaded" }}'>[[
|
||||
app.download
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.uploaded" }}'>[[
|
||||
app.upload
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "usage" }}'>[[ app.used
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.totalQuota" }}'>[[
|
||||
app.total
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item v-if="app.totalByte > 0"
|
||||
label='{{ i18n "remained" }}'>[[
|
||||
app.remained ]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "lastOnline" }}'>
|
||||
<template v-if="app.lastOnlineMs > 0">
|
||||
<template
|
||||
v-if="app.datepicker === 'gregorian'">
|
||||
[[
|
||||
DateUtil.formatMillis(app.lastOnlineMs)
|
||||
]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[
|
||||
DateUtil.convertToJalalian(moment(app.lastOnlineMs))
|
||||
]]
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>-</span>
|
||||
</template>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.expiry" }}'>
|
||||
<template v-if="app.expireMs === 0">
|
||||
{{ i18n "subscription.noExpiry" }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<template
|
||||
v-if="app.datepicker === 'gregorian'">
|
||||
[[
|
||||
DateUtil.formatMillis(app.expireMs)
|
||||
]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[
|
||||
DateUtil.convertToJalalian(moment(app.expireMs))
|
||||
]]
|
||||
</template>
|
||||
</template>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<br />
|
||||
<a-list bordered>
|
||||
<a-list-item v-for="(link, idx) in links" :key="link">
|
||||
<div style="width:100%; text-align:center;">
|
||||
<a-button type="primary" :block="isMobile"
|
||||
@click="copy(link)">[[ linkName(link, idx)
|
||||
]]</a-button>
|
||||
</div>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
<br />
|
||||
|
||||
<a-form layout="vertical">
|
||||
<a-form-item>
|
||||
<a-row type="flex" justify="center" :gutter="[8,8]"
|
||||
style="width:100%">
|
||||
<a-col :xs="24" :sm="12"
|
||||
style="text-align:center;">
|
||||
<!-- Android dropdown -->
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button :block="isMobile"
|
||||
:style="{ marginTop: isMobile ? '6px' : 0 }"
|
||||
size="large" type="primary">
|
||||
Android <a-icon type="down" />
|
||||
</a-button>
|
||||
<a-menu slot="overlay"
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="android-v2box"
|
||||
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
|
||||
<a-menu-item key="android-v2rayng"
|
||||
@click="open('v2rayng://install-config?url=' + encodeURIComponent(app.subUrl))">V2RayNG</a-menu-item>
|
||||
<a-menu-item key="android-singbox"
|
||||
@click="copy(app.subUrl)">Sing-box</a-menu-item>
|
||||
<a-menu-item key="android-v2raytun"
|
||||
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
|
||||
<a-menu-item key="android-npvtunnel"
|
||||
@click="copy(app.subUrl)">NPV
|
||||
Tunnel</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12"
|
||||
style="text-align:center;">
|
||||
<!-- iOS dropdown -->
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button :block="isMobile"
|
||||
:style="{ marginTop: isMobile ? '6px' : 0 }"
|
||||
size="large" type="primary">
|
||||
iOS <a-icon type="down" />
|
||||
</a-button>
|
||||
<a-menu slot="overlay"
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="ios-shadowrocket"
|
||||
@click="open('shadowrocket://add/subscription?url=' + encodeURIComponent(app.subUrl) + '&remark=' + encodeURIComponent(app.sId))">Shadowrocket</a-menu-item>
|
||||
<a-menu-item key="ios-v2box"
|
||||
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
|
||||
<a-menu-item key="ios-streisand"
|
||||
@click="open('streisand://import/' + encodeURIComponent(app.subUrl))">Streisand</a-menu-item>
|
||||
<a-menu-item key="ios-v2raytun"
|
||||
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
|
||||
<a-menu-item key="ios-npvtunnel"
|
||||
@click="copy(app.subUrl)">NPV
|
||||
Tunnel</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
|
||||
<!-- Bootstrap data for external JS -->
|
||||
<template id="subscription-data" data-sid="{{ .sId }}"
|
||||
data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}"
|
||||
data-download="{{ .download }}"
|
||||
data-upload="{{ .upload }}" data-used="{{ .used }}"
|
||||
data-total="{{ .total }}" data-remained="{{ .remained }}"
|
||||
data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}"
|
||||
data-downloadbyte="{{ .downloadByte }}"
|
||||
data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}"
|
||||
data-datepicker="{{ .datepicker }}"></template>
|
||||
<textarea id="subscription-links"
|
||||
style="display:none">{{ range .result }}{{ . }}
|
||||
{{ end }}</textarea>
|
||||
|
||||
{{template "page/body_scripts" .}}
|
||||
<script
|
||||
src="{{ .base_path }}assets/moment/moment-jalali.min.js?{{ .cur_ver }}"></script>
|
||||
<script
|
||||
src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
|
||||
{{template "component/aThemeSwitch" .}}
|
||||
<script src="{{ .base_path }}assets/js/subscription.js?{{ .cur_ver }}"></script>
|
||||
{{ template "page/body_end" .}}
|
||||
@@ -48,6 +48,22 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitLocalizerFS allows initializing i18n from any fs.FS (e.g., disk), rooted at a directory containing a "translation" folder
|
||||
func InitLocalizerFS(fsys fs.FS, settingService SettingService) error {
|
||||
// set default bundle to english
|
||||
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
||||
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
|
||||
if err := parseTranslationFiles(fsys, i18nBundle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := initTGBotLocalizer(settingService); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTemplateData(params []string, seperator ...string) map[string]any {
|
||||
var sep string = "=="
|
||||
if len(seperator) > 0 {
|
||||
@@ -118,8 +134,8 @@ func LocalizerMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
||||
err := fs.WalkDir(i18nFS, "translation",
|
||||
func parseTranslationFiles(fsys fs.FS, i18nBundle *i18n.Bundle) error {
|
||||
err := fs.WalkDir(fsys, "translation",
|
||||
func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -129,7 +145,7 @@ func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := i18nFS.ReadFile(path)
|
||||
data, err := fs.ReadFile(fsys, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "مفيش بروكسي عكسي مضاف."
|
||||
"somethingWentWrong" = "حدث خطأ ما"
|
||||
|
||||
[subscription]
|
||||
"title" = "معلومات الاشتراك"
|
||||
"subId" = "معرّف الاشتراك"
|
||||
"status" = "الحالة"
|
||||
"downloaded" = "التنزيل"
|
||||
"uploaded" = "الرفع"
|
||||
"expiry" = "تاريخ الانتهاء"
|
||||
"totalQuota" = "الحصة الإجمالية"
|
||||
"individualLinks" = "روابط فردية"
|
||||
"active" = "نشط"
|
||||
"inactive" = "غير نشط"
|
||||
"unlimited" = "غير محدود"
|
||||
"noExpiry" = "بدون انتهاء"
|
||||
|
||||
[menu]
|
||||
"theme" = "الثيم"
|
||||
"dark" = "داكن"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "No added reverse proxies."
|
||||
"somethingWentWrong" = "Something went wrong"
|
||||
|
||||
[subscription]
|
||||
"title" = "Subscription info"
|
||||
"subId" = "Subscription ID"
|
||||
"status" = "Status"
|
||||
"downloaded" = "Downloaded"
|
||||
"uploaded" = "Uploaded"
|
||||
"expiry" = "Expiry"
|
||||
"totalQuota" = "Total quota"
|
||||
"individualLinks" = "Individual links"
|
||||
"active" = "Active"
|
||||
"inactive" = "Inactive"
|
||||
"unlimited" = "Unlimited"
|
||||
"noExpiry" = "No expiry"
|
||||
|
||||
[menu]
|
||||
"theme" = "Theme"
|
||||
"dark" = "Dark"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "No hay proxies inversos añadidos."
|
||||
"somethingWentWrong" = "Algo salió mal"
|
||||
|
||||
[subscription]
|
||||
"title" = "Información de suscripción"
|
||||
"subId" = "ID de suscripción"
|
||||
"status" = "Estado"
|
||||
"downloaded" = "Descargado"
|
||||
"uploaded" = "Subido"
|
||||
"expiry" = "Caducidad"
|
||||
"totalQuota" = "Cuota total"
|
||||
"individualLinks" = "Enlaces individuales"
|
||||
"active" = "Activo"
|
||||
"inactive" = "Inactivo"
|
||||
"unlimited" = "Ilimitado"
|
||||
"noExpiry" = "Sin caducidad"
|
||||
|
||||
[menu]
|
||||
"theme" = "Tema"
|
||||
"dark" = "Oscuro"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "هیچ پروکسی معکوس اضافه نشده است."
|
||||
"somethingWentWrong" = "مشکلی پیش آمد"
|
||||
|
||||
[subscription]
|
||||
"title" = "اطلاعات سابسکریپشن"
|
||||
"subId" = "شناسه اشتراک"
|
||||
"status" = "وضعیت"
|
||||
"downloaded" = "دانلود"
|
||||
"uploaded" = "آپلود"
|
||||
"expiry" = "تاریخ پایان"
|
||||
"totalQuota" = "حجم کلی"
|
||||
"individualLinks" = "لینکهای تکی"
|
||||
"active" = "فعال"
|
||||
"inactive" = "غیرفعال"
|
||||
"unlimited" = "نامحدود"
|
||||
"noExpiry" = "بدون انقضا"
|
||||
|
||||
[menu]
|
||||
"theme" = "تم"
|
||||
"dark" = "تیره"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Tidak ada proxy terbalik yang ditambahkan."
|
||||
"somethingWentWrong" = "Terjadi kesalahan"
|
||||
|
||||
[subscription]
|
||||
"title" = "Info langganan"
|
||||
"subId" = "ID langganan"
|
||||
"status" = "Status"
|
||||
"downloaded" = "Diunduh"
|
||||
"uploaded" = "Diunggah"
|
||||
"expiry" = "Kedaluwarsa"
|
||||
"totalQuota" = "Kuota total"
|
||||
"individualLinks" = "Tautan individual"
|
||||
"active" = "Aktif"
|
||||
"inactive" = "Nonaktif"
|
||||
"unlimited" = "Tanpa batas"
|
||||
"noExpiry" = "Tanpa kedaluwarsa"
|
||||
|
||||
[menu]
|
||||
"theme" = "Tema"
|
||||
"dark" = "Gelap"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "追加されたリバースプロキシはありません。"
|
||||
"somethingWentWrong" = "エラーが発生しました"
|
||||
|
||||
[subscription]
|
||||
"title" = "サブスクリプション情報"
|
||||
"subId" = "サブスクリプションID"
|
||||
"status" = "ステータス"
|
||||
"downloaded" = "ダウンロード"
|
||||
"uploaded" = "アップロード"
|
||||
"expiry" = "有効期限"
|
||||
"totalQuota" = "合計クォータ"
|
||||
"individualLinks" = "個別リンク"
|
||||
"active" = "有効"
|
||||
"inactive" = "無効"
|
||||
"unlimited" = "無制限"
|
||||
"noExpiry" = "期限なし"
|
||||
|
||||
[menu]
|
||||
"theme" = "テーマ"
|
||||
"dark" = "ダーク"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Nenhum proxy reverso adicionado."
|
||||
"somethingWentWrong" = "Algo deu errado"
|
||||
|
||||
[subscription]
|
||||
"title" = "Informações da assinatura"
|
||||
"subId" = "ID da assinatura"
|
||||
"status" = "Status"
|
||||
"downloaded" = "Baixado"
|
||||
"uploaded" = "Enviado"
|
||||
"expiry" = "Validade"
|
||||
"totalQuota" = "Cota total"
|
||||
"individualLinks" = "Links individuais"
|
||||
"active" = "Ativo"
|
||||
"inactive" = "Inativo"
|
||||
"unlimited" = "Ilimitado"
|
||||
"noExpiry" = "Sem validade"
|
||||
|
||||
[menu]
|
||||
"theme" = "Tema"
|
||||
"dark" = "Escuro"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Нет добавленных реверс-прокси."
|
||||
"somethingWentWrong" = "Что-то пошло не так"
|
||||
|
||||
[subscription]
|
||||
"title" = "Информация о подписке"
|
||||
"subId" = "ID подписки"
|
||||
"status" = "Статус"
|
||||
"downloaded" = "Загружено"
|
||||
"uploaded" = "Отправлено"
|
||||
"expiry" = "Срок действия"
|
||||
"totalQuota" = "Общий лимит"
|
||||
"individualLinks" = "Индивидуальные ссылки"
|
||||
"active" = "Активна"
|
||||
"inactive" = "Неактивна"
|
||||
"unlimited" = "Безлимит"
|
||||
"noExpiry" = "Без срока"
|
||||
|
||||
[menu]
|
||||
"theme" = "Тема"
|
||||
"dark" = "Темная"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Eklenmiş ters proxy yok."
|
||||
"somethingWentWrong" = "Bir şeyler yanlış gitti"
|
||||
|
||||
[subscription]
|
||||
"title" = "Abonelik Bilgisi"
|
||||
"subId" = "Abonelik Kimliği"
|
||||
"status" = "Durum"
|
||||
"downloaded" = "İndirilen"
|
||||
"uploaded" = "Yüklenen"
|
||||
"expiry" = "Son Kullanma"
|
||||
"totalQuota" = "Toplam Kota"
|
||||
"individualLinks" = "Bireysel Bağlantılar"
|
||||
"active" = "Aktif"
|
||||
"inactive" = "Pasif"
|
||||
"unlimited" = "Sınırsız"
|
||||
"noExpiry" = "Süresiz"
|
||||
|
||||
[menu]
|
||||
"theme" = "Tema"
|
||||
"dark" = "Koyu"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Немає доданих зворотних проксі."
|
||||
"somethingWentWrong" = "Щось пішло не так"
|
||||
|
||||
[subscription]
|
||||
"title" = "Інформація про підписку"
|
||||
"subId" = "ID підписки"
|
||||
"status" = "Статус"
|
||||
"downloaded" = "Завантажено"
|
||||
"uploaded" = "Відвантажено"
|
||||
"expiry" = "Термін дії"
|
||||
"totalQuota" = "Загальна квота"
|
||||
"individualLinks" = "Окремі посилання"
|
||||
"active" = "Активна"
|
||||
"inactive" = "Неактивна"
|
||||
"unlimited" = "Безліміт"
|
||||
"noExpiry" = "Без строку"
|
||||
|
||||
[menu]
|
||||
"theme" = "Тема"
|
||||
"dark" = "Темна"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Không có proxy ngược nào được thêm."
|
||||
"somethingWentWrong" = "Đã xảy ra lỗi"
|
||||
|
||||
[subscription]
|
||||
"title" = "Thông tin đăng ký"
|
||||
"subId" = "ID đăng ký"
|
||||
"status" = "Trạng thái"
|
||||
"downloaded" = "Đã tải xuống"
|
||||
"uploaded" = "Đã tải lên"
|
||||
"expiry" = "Hết hạn"
|
||||
"totalQuota" = "Tổng hạn mức"
|
||||
"individualLinks" = "Liên kết riêng lẻ"
|
||||
"active" = "Hoạt động"
|
||||
"inactive" = "Không hoạt động"
|
||||
"unlimited" = "Không giới hạn"
|
||||
"noExpiry" = "Không hết hạn"
|
||||
|
||||
[menu]
|
||||
"theme" = "Chủ đề"
|
||||
"dark" = "Tối"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "未添加反向代理。"
|
||||
"somethingWentWrong" = "出了点问题"
|
||||
|
||||
[subscription]
|
||||
"title" = "订阅信息"
|
||||
"subId" = "订阅 ID"
|
||||
"status" = "状态"
|
||||
"downloaded" = "已下载"
|
||||
"uploaded" = "已上传"
|
||||
"expiry" = "到期"
|
||||
"totalQuota" = "总配额"
|
||||
"individualLinks" = "单独链接"
|
||||
"active" = "启用"
|
||||
"inactive" = "停用"
|
||||
"unlimited" = "无限制"
|
||||
"noExpiry" = "无到期"
|
||||
|
||||
[menu]
|
||||
"theme" = "主题"
|
||||
"dark" = "暗色"
|
||||
|
||||
@@ -72,6 +72,20 @@
|
||||
"emptyReverseDesc" = "未添加反向代理。"
|
||||
"somethingWentWrong" = "發生錯誤"
|
||||
|
||||
[subscription]
|
||||
"title" = "訂閱資訊"
|
||||
"subId" = "訂閱 ID"
|
||||
"status" = "狀態"
|
||||
"downloaded" = "已下載"
|
||||
"uploaded" = "已上傳"
|
||||
"expiry" = "到期"
|
||||
"totalQuota" = "總配額"
|
||||
"individualLinks" = "個別連結"
|
||||
"active" = "啟用"
|
||||
"inactive" = "停用"
|
||||
"unlimited" = "無限制"
|
||||
"noExpiry" = "無到期"
|
||||
|
||||
[menu]
|
||||
"theme" = "主題"
|
||||
"dark" = "深色"
|
||||
|
||||
Reference in New Issue
Block a user