{{ SizeFormatter.sizeFormat(clientStats.up) }} /
diff --git a/frontend/src/pages/inbounds/ClientRowTable.vue b/frontend/src/pages/inbounds/ClientRowTable.vue
index 6ed33119..c84728a0 100644
--- a/frontend/src/pages/inbounds/ClientRowTable.vue
+++ b/frontend/src/pages/inbounds/ClientRowTable.vue
@@ -361,6 +361,10 @@ function confirmBulkDelete() {
| {{ t('remained') }} |
{{ SizeFormatter.sizeFormat(getRem(client.email)) }} |
+
+ | {{ t('subscription.totalQuota') }} |
+ {{ SizeFormatter.sizeFormat(client.subTotalGB) }} |
+
diff --git a/frontend/src/pages/inbounds/InboundInfoModal.vue b/frontend/src/pages/inbounds/InboundInfoModal.vue
index 61ce2fcf..1cf754de 100644
--- a/frontend/src/pages/inbounds/InboundInfoModal.vue
+++ b/frontend/src/pages/inbounds/InboundInfoModal.vue
@@ -80,6 +80,38 @@ const refreshing = ref(false);
const clientIpsArray = ref([]);
const clientIpsText = ref('');
+// Shared subscription quota state.
+const subTrafficInfo = ref(null);
+
+async function loadSubTraffic(subId) {
+ if (!subId) { subTrafficInfo.value = null; return; }
+ try {
+ const msg = await HttpUtil.post(`/panel/api/inbounds/getSubTraffic/${subId}`);
+ if (msg?.success && msg.obj) {
+ subTrafficInfo.value = msg.obj;
+ } else {
+ subTrafficInfo.value = null;
+ }
+ } catch (_e) {
+ subTrafficInfo.value = null;
+ }
+}
+
+function getSubRemaining() {
+ if (!subTrafficInfo.value || !subTrafficInfo.value.total) return '-';
+ const remained = subTrafficInfo.value.total - subTrafficInfo.value.up - subTrafficInfo.value.down;
+ return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-';
+}
+
+function subStatsColor() {
+ if (!subTrafficInfo.value || !subTrafficInfo.value.total) return 'default';
+ return ColorUtils.usageColor(
+ subTrafficInfo.value.up + subTrafficInfo.value.down,
+ props.trafficDiff,
+ subTrafficInfo.value.total,
+ );
+}
+
// === Status flags shown as tags ====================================
const isEnable = computed(() => {
if (clientSettings.value) return !!clientSettings.value.enable;
@@ -246,6 +278,12 @@ watch(() => props.open, (next) => {
) {
loadClientIps();
}
+
+ // Load shared subscription traffic if client has subTotalGB.
+ subTrafficInfo.value = null;
+ if (clientSettings.value?.subTotalGB > 0 && clientSettings.value?.subId) {
+ loadSubTraffic(clientSettings.value.subId);
+ }
});
function close() {
@@ -380,6 +418,7 @@ const showSubscriptionTab = computed(
{{ t('remained') }} |
{{ t('pages.inbounds.totalUsage') }} |
{{ t('pages.inbounds.expireDate') }} |
+ {{ t('subscription.subRemained') }} |
@@ -409,6 +448,10 @@ const showSubscriptionTab = computed(
+
+ {{ getSubRemaining() }}
+ {{ SizeFormatter.sizeFormat(subTrafficInfo.total) }}
+ |
diff --git a/web/controller/inbound.go b/web/controller/inbound.go
index 541ae449..ac5b4c83 100644
--- a/web/controller/inbound.go
+++ b/web/controller/inbound.go
@@ -64,6 +64,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.GET("/get/:id", a.getInbound)
g.GET("/getClientTraffics/:email", a.getClientTraffics)
g.GET("/getClientTrafficsById/:id", a.getClientTrafficsById)
+ g.GET("/getSubTraffic/:subId", a.getSubTraffic)
g.GET("/getSubLinks/:subId", a.getSubLinks)
g.GET("/getClientLinks/:id/:email", a.getClientLinks)
@@ -143,6 +144,17 @@ func (a *InboundController) getClientTrafficsById(c *gin.Context) {
jsonObj(c, clientTraffics, nil)
}
+// getSubTraffic retrieves aggregated traffic info for all clients sharing a SubID.
+func (a *InboundController) getSubTraffic(c *gin.Context) {
+ subId := c.Param("subId")
+ info, err := a.inboundService.GetSubTrafficInfo(subId)
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err)
+ return
+ }
+ jsonObj(c, info, nil)
+}
+
// addInbound creates a new inbound configuration.
func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{}
diff --git a/web/service/inbound.go b/web/service/inbound.go
index 16bb2528..61c950fa 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -1859,13 +1859,21 @@ func (s *InboundService) addTrafficLocked(inboundTraffics []*xray.Traffic, clien
disabledClientsCount = count
}
+ needRestart3, count, err := s.disableSubQuotaClients(tx)
+ if err != nil {
+ logger.Warning("Error in disabling sub-quota clients:", err)
+ } else if count > 0 {
+ logger.Debugf("%v clients disabled by sub quota", count)
+ disabledClientsCount += count
+ }
+
needRestart2, count, err := s.disableInvalidInbounds(tx)
if err != nil {
logger.Warning("Error in disabling invalid inbounds:", err)
} else if count > 0 {
logger.Debugf("%v inbounds disabled", count)
}
- return needRestart0 || needRestart1 || needRestart2, disabledClientsCount > 0, nil
+ return needRestart0 || needRestart1 || needRestart2 || needRestart3, disabledClientsCount > 0, nil
}
func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
@@ -2338,6 +2346,290 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
return needRestart, count, nil
}
+// SubTrafficInfo holds aggregated traffic data for all clients sharing a SubID.
+type SubTrafficInfo struct {
+ SubId string `json:"subId"`
+ Up int64 `json:"up"`
+ Down int64 `json:"down"`
+ Total int64 `json:"total"` // subTotalGB limit (bytes)
+ Clients int `json:"clients"` // number of client emails sharing this subId
+}
+
+// GetSubTrafficInfo returns aggregated traffic data for all clients sharing
+// the given subId. The Total field is the first non-zero subTotalGB found
+// across any sibling client.
+func (s *InboundService) GetSubTrafficInfo(subId string) (*SubTrafficInfo, error) {
+ if subId == "" {
+ return nil, common.NewError("empty subId")
+ }
+ db := database.GetDB()
+
+ // 1. Find all emails + subTotalGB for this subId.
+ var rows []struct {
+ Email string
+ SubTotalGB int64 `gorm:"column:sub_total_gb"`
+ }
+ err := db.Raw(`
+ SELECT JSON_EXTRACT(client.value, '$.email') AS email,
+ COALESCE(JSON_EXTRACT(client.value, '$.subTotalGB'), 0) AS sub_total_gb
+ FROM inbounds,
+ JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
+ WHERE REPLACE(JSON_EXTRACT(client.value, '$.subId'), '"', '') = ?
+ `, subId).Scan(&rows).Error
+ if err != nil {
+ return nil, err
+ }
+ if len(rows) == 0 {
+ return &SubTrafficInfo{SubId: subId}, nil
+ }
+
+ emails := make([]string, 0, len(rows))
+ seen := make(map[string]struct{}, len(rows))
+ var subTotal int64
+ for _, r := range rows {
+ email := strings.Trim(r.Email, "\"")
+ if email == "" {
+ continue
+ }
+ key := strings.ToLower(email)
+ if _, ok := seen[key]; ok {
+ continue
+ }
+ seen[key] = struct{}{}
+ emails = append(emails, email)
+ if subTotal == 0 && r.SubTotalGB > 0 {
+ subTotal = r.SubTotalGB
+ }
+ }
+
+ // 2. Sum traffic from client_traffics for those emails.
+ var up, down int64
+ for _, batch := range chunkStrings(emails, sqliteMaxVars) {
+ var result struct {
+ Up int64
+ Down int64
+ }
+ if err := db.Model(xray.ClientTraffic{}).
+ Select("COALESCE(SUM(up),0) AS up, COALESCE(SUM(down),0) AS down").
+ Where("email IN ?", batch).Scan(&result).Error; err != nil {
+ return nil, err
+ }
+ up += result.Up
+ down += result.Down
+ }
+
+ return &SubTrafficInfo{
+ SubId: subId,
+ Up: up,
+ Down: down,
+ Total: subTotal,
+ Clients: len(emails),
+ }, nil
+}
+
+// disableSubQuotaClients checks all SubID groups for shared quota violations.
+// If the aggregate traffic (up+down) across all clients sharing a SubID
+// exceeds the subTotalGB configured on any sibling, ALL clients in the group
+// are disabled. Only considers enabled clients on local-node inbounds.
+func (s *InboundService) disableSubQuotaClients(tx *gorm.DB) (bool, int64, error) {
+ // 1. Collect every (email, subId, subTotalGB) tuple from inbound settings.
+ var allRows []struct {
+ InboundId int
+ Tag string
+ Email string
+ SubID string `gorm:"column:sub_id"`
+ SubTotalGB int64 `gorm:"column:sub_total_gb"`
+ }
+ err := tx.Raw(`
+ SELECT inbounds.id AS inbound_id,
+ inbounds.tag AS tag,
+ JSON_EXTRACT(client.value, '$.email') AS email,
+ REPLACE(JSON_EXTRACT(client.value, '$.subId'), '"', '') AS sub_id,
+ COALESCE(JSON_EXTRACT(client.value, '$.subTotalGB'), 0) AS sub_total_gb
+ FROM inbounds,
+ JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
+ WHERE inbounds.node_id IS NULL
+ `).Scan(&allRows).Error
+ if err != nil {
+ return false, 0, err
+ }
+
+ // 2. Group by subId: collect emails and find the first non-zero subTotalGB.
+ type subGroup struct {
+ emails map[string]struct{}
+ subTotalGB int64
+ members []struct{ InboundId int; Tag, Email string }
+ }
+ groups := make(map[string]*subGroup)
+ for _, r := range allRows {
+ subId := strings.TrimSpace(r.SubID)
+ if subId == "" {
+ continue
+ }
+ email := strings.Trim(r.Email, "\"")
+ if email == "" {
+ continue
+ }
+ g, ok := groups[subId]
+ if !ok {
+ g = &subGroup{emails: make(map[string]struct{})}
+ groups[subId] = g
+ }
+ g.emails[strings.ToLower(email)] = struct{}{}
+ if g.subTotalGB == 0 && r.SubTotalGB > 0 {
+ g.subTotalGB = r.SubTotalGB
+ }
+ g.members = append(g.members, struct{ InboundId int; Tag, Email string }{
+ InboundId: r.InboundId, Tag: r.Tag, Email: email,
+ })
+ }
+
+ // 3. For each group with a quota, check if aggregate traffic exceeds it.
+ type disableTarget struct {
+ InboundId int
+ Tag string
+ Email string
+ }
+ var toDisable []disableTarget
+
+ for _, g := range groups {
+ if g.subTotalGB <= 0 {
+ continue
+ }
+
+ // Sum traffic for all emails in this subId group.
+ emails := make([]string, 0, len(g.emails))
+ for e := range g.emails {
+ emails = append(emails, e)
+ }
+
+ var totalUsed int64
+ for _, batch := range chunkStrings(emails, sqliteMaxVars) {
+ var sum struct{ Total int64 }
+ if err := tx.Model(xray.ClientTraffic{}).
+ Select("COALESCE(SUM(up + down), 0) AS total").
+ Where("email IN ? AND enable = ?", batch, true).
+ Scan(&sum).Error; err != nil {
+ continue
+ }
+ totalUsed += sum.Total
+ }
+
+ if totalUsed < g.subTotalGB {
+ continue
+ }
+
+ // Quota exceeded — mark all members for disabling.
+ for _, m := range g.members {
+ toDisable = append(toDisable, disableTarget{
+ InboundId: m.InboundId, Tag: m.Tag, Email: m.Email,
+ })
+ }
+ }
+
+ if len(toDisable) == 0 {
+ return false, 0, nil
+ }
+
+ // 4. Remove users from Xray runtime.
+ needRestart := false
+ if p != nil {
+ s.xrayApi.Init(p.GetAPIPort())
+ for _, t := range toDisable {
+ err1 := s.xrayApi.RemoveUser(t.Tag, t.Email)
+ if err1 == nil {
+ logger.Debug("Sub-quota client disabled by api:", t.Email)
+ } else if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", t.Email)) {
+ logger.Debug("Sub-quota user already disabled:", t.Email)
+ } else {
+ logger.Debug("Error disabling sub-quota client by api:", err1)
+ needRestart = true
+ }
+ }
+ s.xrayApi.Close()
+ }
+
+ // 5. Disable client_traffics rows.
+ disableEmails := make([]string, 0, len(toDisable))
+ for _, t := range toDisable {
+ disableEmails = append(disableEmails, t.Email)
+ }
+ uniqDisable := uniqueNonEmptyStrings(disableEmails)
+ var totalDisabled int64
+ for _, batch := range chunkStrings(uniqDisable, sqliteMaxVars) {
+ result := tx.Model(xray.ClientTraffic{}).
+ Where("email IN ? AND enable = ?", batch, true).
+ Update("enable", false)
+ if result.Error != nil {
+ logger.Warning("disableSubQuotaClients update client_traffics:", result.Error)
+ }
+ totalDisabled += result.RowsAffected
+ }
+
+ // 6. Update inbound settings JSON to set enable=false on affected clients.
+ inboundEmailMap := make(map[int]map[string]struct{})
+ for _, t := range toDisable {
+ if inboundEmailMap[t.InboundId] == nil {
+ inboundEmailMap[t.InboundId] = make(map[string]struct{})
+ }
+ inboundEmailMap[t.InboundId][t.Email] = struct{}{}
+ }
+ inboundIds := make([]int, 0, len(inboundEmailMap))
+ for id := range inboundEmailMap {
+ inboundIds = append(inboundIds, id)
+ }
+ var inbounds []*model.Inbound
+ if err = tx.Model(model.Inbound{}).Where("id IN ?", inboundIds).Find(&inbounds).Error; err != nil {
+ logger.Warning("disableSubQuotaClients fetch inbounds:", err)
+ return needRestart, totalDisabled, nil
+ }
+ now := time.Now().Unix() * 1000
+ dirty := make([]*model.Inbound, 0, len(inbounds))
+ for _, inbound := range inbounds {
+ settings := map[string]any{}
+ if jsonErr := json.Unmarshal([]byte(inbound.Settings), &settings); jsonErr != nil {
+ continue
+ }
+ clientsRaw, ok := settings["clients"].([]any)
+ if !ok {
+ continue
+ }
+ emailSet := inboundEmailMap[inbound.Id]
+ changed := false
+ for i := range clientsRaw {
+ c, ok := clientsRaw[i].(map[string]any)
+ if !ok {
+ continue
+ }
+ email, _ := c["email"].(string)
+ if _, shouldDisable := emailSet[email]; !shouldDisable {
+ continue
+ }
+ c["enable"] = false
+ c["updated_at"] = now
+ clientsRaw[i] = c
+ changed = true
+ }
+ if !changed {
+ continue
+ }
+ settings["clients"] = clientsRaw
+ modifiedSettings, jsonErr := json.MarshalIndent(settings, "", " ")
+ if jsonErr != nil {
+ continue
+ }
+ inbound.Settings = string(modifiedSettings)
+ dirty = append(dirty, inbound)
+ }
+ if len(dirty) > 0 {
+ if err = tx.Save(dirty).Error; err != nil {
+ logger.Warning("disableSubQuotaClients update inbound settings:", err)
+ }
+ }
+
+ return needRestart, totalDisabled, nil
+}
+
func (s *InboundService) GetInboundTags() (string, error) {
db := database.GetDB()
var inboundTags []string
diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json
index ecf1c19d..6b5b37b0 100644
--- a/web/translation/ar-EG.json
+++ b/web/translation/ar-EG.json
@@ -86,7 +86,11 @@
"active": "نشط",
"inactive": "غير نشط",
"unlimited": "غير محدود",
- "noExpiry": "بدون انتهاء"
+ "noExpiry": "بدون انتهاء",
+ "subRemained": "المتبقي (الاشتراك)",
+ "subTotalFlow": "إجمالي حركة الاشتراك",
+ "subUsage": "استخدام الاشتراك",
+ "subDepleted": "الاشتراك منتهي"
},
"menu": {
"theme": "الثيم",
diff --git a/web/translation/en-US.json b/web/translation/en-US.json
index eedd3454..bda62644 100644
--- a/web/translation/en-US.json
+++ b/web/translation/en-US.json
@@ -86,7 +86,11 @@
"active": "Active",
"inactive": "Inactive",
"unlimited": "Unlimited",
- "noExpiry": "No expiry"
+ "noExpiry": "No expiry",
+ "subRemained": "Remained (Sub.)",
+ "subTotalFlow": "Sub. Total Flow",
+ "subUsage": "Sub. Usage",
+ "subDepleted": "Sub. Depleted"
},
"menu": {
"theme": "Theme",
diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json
index 34af89ca..6e54eba1 100644
--- a/web/translation/es-ES.json
+++ b/web/translation/es-ES.json
@@ -86,7 +86,11 @@
"active": "Activo",
"inactive": "Inactivo",
"unlimited": "Ilimitado",
- "noExpiry": "Sin caducidad"
+ "noExpiry": "Sin caducidad",
+ "subRemained": "Restante (Sub.)",
+ "subTotalFlow": "Flujo Total Sub.",
+ "subUsage": "Uso Sub.",
+ "subDepleted": "Sub. Agotada"
},
"menu": {
"theme": "Tema",
diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json
index fd948788..2dfe63bd 100644
--- a/web/translation/fa-IR.json
+++ b/web/translation/fa-IR.json
@@ -86,7 +86,11 @@
"active": "فعال",
"inactive": "غیرفعال",
"unlimited": "نامحدود",
- "noExpiry": "بدون انقضا"
+ "noExpiry": "بدون انقضا",
+ "subRemained": "باقیمانده (اشتراک)",
+ "subTotalFlow": "حجم کلی اشتراک",
+ "subUsage": "مصرف اشتراک",
+ "subDepleted": "اشتراک تمام شده"
},
"menu": {
"theme": "تم",
diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json
index b8f44ac6..504eeba0 100644
--- a/web/translation/id-ID.json
+++ b/web/translation/id-ID.json
@@ -86,7 +86,11 @@
"active": "Aktif",
"inactive": "Nonaktif",
"unlimited": "Tanpa batas",
- "noExpiry": "Tanpa kedaluwarsa"
+ "noExpiry": "Tanpa kedaluwarsa",
+ "subRemained": "Sisa (Langganan)",
+ "subTotalFlow": "Total Aliran Langg.",
+ "subUsage": "Penggunaan Langg.",
+ "subDepleted": "Langg. Habis"
},
"menu": {
"theme": "Tema",
diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json
index 630f5623..601a1d7d 100644
--- a/web/translation/ja-JP.json
+++ b/web/translation/ja-JP.json
@@ -86,7 +86,11 @@
"active": "有効",
"inactive": "無効",
"unlimited": "無制限",
- "noExpiry": "期限なし"
+ "noExpiry": "期限なし",
+ "subRemained": "残り (サブ.)",
+ "subTotalFlow": "サブ. 合計フロー",
+ "subUsage": "サブ. 使用量",
+ "subDepleted": "サブ. 消耗済み"
},
"menu": {
"theme": "テーマ",
diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json
index 9f1b67d7..81393b1d 100644
--- a/web/translation/pt-BR.json
+++ b/web/translation/pt-BR.json
@@ -86,7 +86,11 @@
"active": "Ativo",
"inactive": "Inativo",
"unlimited": "Ilimitado",
- "noExpiry": "Sem validade"
+ "noExpiry": "Sem validade",
+ "subRemained": "Restante (Assin.)",
+ "subTotalFlow": "Fluxo Total Assin.",
+ "subUsage": "Uso Assin.",
+ "subDepleted": "Assin. Esgotada"
},
"menu": {
"theme": "Tema",
diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json
index f7ddafa6..baba8ab3 100644
--- a/web/translation/ru-RU.json
+++ b/web/translation/ru-RU.json
@@ -86,7 +86,11 @@
"active": "Активна",
"inactive": "Неактивна",
"unlimited": "Неограниченно",
- "noExpiry": "Бессрочно"
+ "noExpiry": "Бессрочно",
+ "subRemained": "Осталось (Подп.)",
+ "subTotalFlow": "Общий лимит подписки",
+ "subUsage": "Использование подписки",
+ "subDepleted": "Подписка исчерпана"
},
"menu": {
"theme": "Тема",
diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json
index a8dc2c3c..db0a51fc 100644
--- a/web/translation/tr-TR.json
+++ b/web/translation/tr-TR.json
@@ -86,7 +86,11 @@
"active": "Aktif",
"inactive": "Pasif",
"unlimited": "Sınırsız",
- "noExpiry": "Süresiz"
+ "noExpiry": "Süresiz",
+ "subRemained": "Kalan (Abo.)",
+ "subTotalFlow": "Abo. Toplam Akış",
+ "subUsage": "Abo. Kullanım",
+ "subDepleted": "Abo. Tükendi"
},
"menu": {
"theme": "Tema",
diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json
index 417866d1..e50f018c 100644
--- a/web/translation/uk-UA.json
+++ b/web/translation/uk-UA.json
@@ -86,7 +86,11 @@
"active": "Активна",
"inactive": "Неактивна",
"unlimited": "Безліміт",
- "noExpiry": "Без строку"
+ "noExpiry": "Без строку",
+ "subRemained": "Залишок (Підп.)",
+ "subTotalFlow": "Загальний ліміт підписки",
+ "subUsage": "Використання підписки",
+ "subDepleted": "Підписку вичерпано"
},
"menu": {
"theme": "Тема",
diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json
index b292c058..69877883 100644
--- a/web/translation/vi-VN.json
+++ b/web/translation/vi-VN.json
@@ -86,7 +86,11 @@
"active": "Hoạt động",
"inactive": "Không hoạt động",
"unlimited": "Không giới hạn",
- "noExpiry": "Không hết hạn"
+ "noExpiry": "Không hết hạn",
+ "subRemained": "Còn lại (Đăng ký)",
+ "subTotalFlow": "Tổng lưu lượng đăng ký",
+ "subUsage": "Sử dụng đăng ký",
+ "subDepleted": "Đăng ký đã hết"
},
"menu": {
"theme": "Chủ đề",
diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json
index 3f648cb2..4913c91b 100644
--- a/web/translation/zh-CN.json
+++ b/web/translation/zh-CN.json
@@ -86,7 +86,11 @@
"active": "启用",
"inactive": "停用",
"unlimited": "无限制",
- "noExpiry": "无到期"
+ "noExpiry": "无到期",
+ "subRemained": "剩余 (订阅)",
+ "subTotalFlow": "订阅总流量",
+ "subUsage": "订阅用量",
+ "subDepleted": "订阅已耗尽"
},
"menu": {
"theme": "主题",
diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json
index a28c0e0f..0a9487af 100644
--- a/web/translation/zh-TW.json
+++ b/web/translation/zh-TW.json
@@ -86,7 +86,11 @@
"active": "啟用",
"inactive": "停用",
"unlimited": "無限制",
- "noExpiry": "無到期"
+ "noExpiry": "無到期",
+ "subRemained": "剩餘 (訂閱)",
+ "subTotalFlow": "訂閱總流量",
+ "subUsage": "訂閱用量",
+ "subDepleted": "訂閱已耗盡"
},
"menu": {
"theme": "主題",