feat(clients): bulk extend expiry / traffic for selected clients

Adds POST /panel/api/clients/bulkAdjust which shifts ExpiryTime by
addDays and TotalGB by addBytes for every email in one request. The
endpoint is wired into the clients page through a new ClientBulkAdjustModal
that opens from the existing multi-select toolbar.

Clients with unlimited expiry (expiryTime=0) or unlimited traffic
(totalGB=0) are skipped for the corresponding field so bulk extend
never accidentally converts an unlimited client to a limited one.
Negative values are allowed for refunds / corrections.

Translations added for all 13 locales.
This commit is contained in:
MHSanaei 2026-05-23 15:55:04 +02:00
parent 665ac303ea
commit 3ac65b6fe7
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
19 changed files with 349 additions and 4 deletions

View file

@ -146,6 +146,17 @@ export function useClients() {
return results;
}, [refresh]);
const bulkAdjust = useCallback(async (emails: string[], addDays: number, addBytes: number) => {
if (!Array.isArray(emails) || emails.length === 0) return null;
const msg = await HttpUtil.post(
'/panel/api/clients/bulkAdjust',
{ emails, addDays, addBytes },
JSON_HEADERS,
) as ApiMsg<{ adjusted: number; skipped?: { email: string; reason: string }[] }>;
if (msg?.success) await refresh();
return msg;
}, [refresh]);
const attach = useCallback(async (email: string, inboundIds: number[]) => {
if (!email) return null;
const encoded = encodeURIComponent(email);
@ -269,6 +280,7 @@ export function useClients() {
update,
remove,
removeMany,
bulkAdjust,
attach,
detach,
resetTraffic,

View file

@ -461,6 +461,13 @@ export const sections = [
summary: 'Delete every client whose traffic quota is exhausted (used >= total, when reset is disabled) or whose expiry has passed. Returns the deleted count and triggers an Xray restart when any client was on a running inbound.',
response: '{\n "success": true,\n "obj": {\n "deleted": 0\n }\n}',
},
{
method: 'POST',
path: '/panel/api/clients/bulkAdjust',
summary: 'Shift expiry and/or traffic quota for many clients in one call. addDays/addBytes may be negative. Clients with unlimited expiry (expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the corresponding field — bulk extend never converts unlimited to limited. Returns the adjusted count and per-email skip reasons.',
body: '{\n "emails": ["alice", "bob"],\n "addDays": 30,\n "addBytes": 53687091200\n}',
response: '{\n "success": true,\n "obj": {\n "adjusted": 2,\n "skipped": [\n { "email": "carol", "reason": "unlimited expiry" }\n ]\n }\n}',
},
{
method: 'POST',
path: '/panel/api/clients/resetTraffic/:email',

View file

@ -0,0 +1,97 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Form, InputNumber, Modal, message } from 'antd';
const GB = 1024 * 1024 * 1024;
interface ClientBulkAdjustModalProps {
open: boolean;
count: number;
onOpenChange: (open: boolean) => void;
onSubmit: (addDays: number, addBytes: number) => Promise<{ adjusted: number; skipped?: { email: string; reason: string }[] } | null>;
}
export default function ClientBulkAdjustModal({ open, count, onOpenChange, onSubmit }: ClientBulkAdjustModalProps) {
const { t } = useTranslation();
const [messageApi, messageContextHolder] = message.useMessage();
const [addDays, setAddDays] = useState<number>(0);
const [addGB, setAddGB] = useState<number>(0);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (open) {
setAddDays(0);
setAddGB(0);
}
}, [open]);
async function handleOk() {
const days = Math.trunc(Number(addDays) || 0);
const gb = Number(addGB) || 0;
if (days === 0 && gb === 0) {
messageApi.warning(t('pages.clients.bulkAdjustNothing'));
return;
}
setSubmitting(true);
try {
const bytes = Math.trunc(gb * GB);
const result = await onSubmit(days, bytes);
if (!result) return;
const ok = result.adjusted ?? 0;
const skipped = result.skipped?.length ?? 0;
if (skipped === 0) {
messageApi.success(t('pages.clients.toasts.bulkAdjusted', { count: ok }));
} else {
const firstReason = result.skipped?.[0]?.reason ?? '';
messageApi.warning(firstReason
? `${t('pages.clients.toasts.bulkAdjustedMixed', { ok, skipped })} — ${firstReason}`
: t('pages.clients.toasts.bulkAdjustedMixed', { ok, skipped }));
}
onOpenChange(false);
} finally {
setSubmitting(false);
}
}
return (
<>
{messageContextHolder}
<Modal
open={open}
title={t('pages.clients.bulkAdjustTitle', { count })}
okText={t('apply')}
cancelText={t('cancel')}
confirmLoading={submitting}
onOk={handleOk}
onCancel={() => onOpenChange(false)}
destroyOnHidden
>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message={t('pages.clients.bulkAdjustHint')}
/>
<Form layout="vertical">
<Form.Item label={t('pages.clients.addDays')}>
<InputNumber
value={addDays}
onChange={(v) => setAddDays(Number(v) || 0)}
style={{ width: '100%' }}
step={1}
precision={0}
/>
</Form.Item>
<Form.Item label={t('pages.clients.addTrafficGB')}>
<InputNumber
value={addGB}
onChange={(v) => setAddGB(Number(v) || 0)}
style={{ width: '100%' }}
step={1}
/>
</Form.Item>
</Form>
</Modal>
</>
);
}

View file

@ -25,6 +25,7 @@ import {
} from 'antd';
import type { ColumnsType, TableProps } from 'antd/es/table';
import {
ClockCircleOutlined,
DeleteOutlined,
EditOutlined,
FilterOutlined,
@ -54,6 +55,7 @@ import ClientFormModal from './ClientFormModal';
import ClientInfoModal from './ClientInfoModal';
import ClientQrModal from './ClientQrModal';
import ClientBulkAddModal from './ClientBulkAddModal';
import ClientBulkAdjustModal from './ClientBulkAdjustModal';
import '@/styles/page-cards.css';
import './ClientsPage.css';
@ -96,7 +98,7 @@ export default function ClientsPage() {
const {
clients, inbounds, onlines, loading, fetched, subSettings,
ipLimitEnable, tgBotEnable, expireDiff, trafficDiff, pageSize,
create, update, remove, removeMany, attach, detach,
create, update, remove, removeMany, bulkAdjust, attach, detach,
resetTraffic, resetAllTraffics, delDepleted, setEnable,
applyTrafficEvent, applyClientStatsEvent, applyInvalidate,
} = useClients();
@ -117,6 +119,7 @@ export default function ClientsPage() {
const [qrOpen, setQrOpen] = useState(false);
const [qrClient, setQrClient] = useState<ClientRecord | null>(null);
const [bulkAddOpen, setBulkAddOpen] = useState(false);
const [bulkAdjustOpen, setBulkAdjustOpen] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const initial = readFilterState();
@ -700,9 +703,14 @@ export default function ClientsPage() {
{!isMobile && t('pages.clients.bulk')}
</Button>
{selectedRowKeys.length > 0 && (
<Button danger size="small" icon={<DeleteOutlined />} onClick={onBulkDelete}>
{t('pages.clients.deleteSelected', { count: selectedRowKeys.length })}
</Button>
<>
<Button size="small" icon={<ClockCircleOutlined />} onClick={() => setBulkAdjustOpen(true)}>
{t('pages.clients.adjustSelected', { count: selectedRowKeys.length })}
</Button>
<Button danger size="small" icon={<DeleteOutlined />} onClick={onBulkDelete}>
{t('pages.clients.deleteSelected', { count: selectedRowKeys.length })}
</Button>
</>
)}
<Button size="small" icon={<RetweetOutlined />} onClick={onResetAllTraffics}>
{!isMobile && t('pages.clients.resetAllTraffics')}
@ -902,6 +910,19 @@ export default function ClientsPage() {
onOpenChange={setBulkAddOpen}
onSaved={() => setBulkAddOpen(false)}
/>
<ClientBulkAdjustModal
open={bulkAdjustOpen}
count={selectedRowKeys.length}
onOpenChange={setBulkAdjustOpen}
onSubmit={async (addDays, addBytes) => {
const msg = await bulkAdjust([...selectedRowKeys], addDays, addBytes);
if (msg?.success) {
setSelectedRowKeys([]);
return msg.obj ?? { adjusted: 0 };
}
return null;
}}
/>
</Layout>
</ConfigProvider>
);

View file

@ -42,6 +42,7 @@ func (a *ClientController) initRouter(g *gin.RouterGroup) {
g.POST("/:email/detach", a.detach)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/delDepleted", a.delDepleted)
g.POST("/bulkAdjust", a.bulkAdjust)
g.POST("/resetTraffic/:email", a.resetTrafficByEmail)
g.POST("/updateTraffic/:email", a.updateTrafficByEmail)
g.POST("/ips/:email", a.getIps)
@ -162,6 +163,30 @@ func (a *ClientController) resetAllTraffics(c *gin.Context) {
notifyClientsChanged()
}
type bulkAdjustRequest struct {
Emails []string `json:"emails"`
AddDays int `json:"addDays"`
AddBytes int64 `json:"addBytes"`
}
func (a *ClientController) bulkAdjust(c *gin.Context) {
var req bulkAdjustRequest
if err := c.ShouldBindJSON(&req); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
result, needRestart, err := a.clientService.BulkAdjust(&a.inboundService, req.Emails, req.AddDays, req.AddBytes)
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonObj(c, result, nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
notifyClientsChanged()
}
func (a *ClientController) delDepleted(c *gin.Context) {
deleted, needRestart, err := a.clientService.DelDepleted(&a.inboundService)
if err != nil {

View file

@ -803,6 +803,85 @@ func (s *ClientService) ResetTrafficByEmail(inboundSvc *InboundService, email st
return needRestart, nil
}
// BulkAdjustResult is returned by BulkAdjust to report how many clients were
// successfully updated and which were skipped (typically because the field
// being adjusted was unlimited for that client) or failed.
type BulkAdjustResult struct {
Adjusted int `json:"adjusted"`
Skipped []BulkAdjustReport `json:"skipped,omitempty"`
}
type BulkAdjustReport struct {
Email string `json:"email"`
Reason string `json:"reason"`
}
// BulkAdjust shifts ExpiryTime by addDays (days) and TotalGB by addBytes
// for every email in the list. Clients whose corresponding field is
// unlimited (0) are skipped — bulk extend should not accidentally
// limit an unlimited client. addDays and addBytes may be negative.
func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string, addDays int, addBytes int64) (BulkAdjustResult, bool, error) {
result := BulkAdjustResult{}
needRestart := false
if len(emails) == 0 {
return result, needRestart, nil
}
if addDays == 0 && addBytes == 0 {
return result, needRestart, common.NewError("no adjustment specified")
}
addExpiryMs := int64(addDays) * 24 * 60 * 60 * 1000
for _, email := range emails {
email = strings.TrimSpace(email)
if email == "" {
continue
}
rec, err := s.GetRecordByEmail(nil, email)
if err != nil {
result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: err.Error()})
continue
}
client := rec.ToClient()
applied := false
if addDays != 0 {
if rec.ExpiryTime == 0 {
result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: "unlimited expiry"})
} else {
client.ExpiryTime = rec.ExpiryTime + addExpiryMs
applied = true
}
}
if addBytes != 0 {
if rec.TotalGB == 0 {
result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: "unlimited traffic"})
} else {
next := rec.TotalGB + addBytes
if next < 0 {
next = 0
}
client.TotalGB = next
applied = true
}
}
if !applied {
continue
}
nr, err := s.Update(inboundSvc, rec.Id, *client)
if err != nil {
result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: err.Error()})
continue
}
if nr {
needRestart = true
}
result.Adjusted++
}
return result, needRestart, nil
}
func (s *ClientService) DelDepleted(inboundSvc *InboundService) (int, bool, error) {
db := database.GetDB()
now := time.Now().UnixMilli()

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "حذف العميل {email}؟",
"deleteConfirmContent": "سيؤدي هذا إلى إزالة العميل من جميع الاتصالات الواردة المرتبطة وحذف سجل حركة مروره. لا يمكن التراجع.",
"deleteSelected": "حذف ({count})",
"adjustSelected": "تعديل ({count})",
"bulkDeleteConfirmTitle": "حذف {count} عميل؟",
"bulkDeleteConfirmContent": "سيتم إزالة كل عميل محدد من جميع الاتصالات الواردة المرتبطة وحذف سجل حركة مروره. لا يمكن التراجع.",
"bulkAdjustTitle": "تعديل {count} عميل",
"bulkAdjustHint": "القيم الموجبة تزيد، السالبة تنقص. العملاء بصلاحية أو ترافيك غير محدود يُتخطّون لذلك الحقل.",
"bulkAdjustNothing": "حدد الأيام أو الترافيك قبل التطبيق.",
"addDays": "إضافة أيام",
"addTrafficGB": "إضافة ترافيك (GB)",
"delDepleted": "حذف المنتهية",
"delDepletedConfirmTitle": "حذف العملاء المنتهية حصصهم؟",
"delDepletedConfirmContent": "يُحذف كل عميل استُنفِدت حصة حركة مروره أو انتهت صلاحيته. لا يمكن التراجع.",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "تم حذف {ok}, وفشل {failed}",
"bulkCreated": "تم إنشاء {count} عميل",
"bulkCreatedMixed": "تم إنشاء {ok}, وفشل {failed}",
"bulkAdjusted": "تم تعديل {count} عميل",
"bulkAdjustedMixed": "{ok} تم تعديلهم، {skipped} تم تخطيهم",
"delDepleted": "تم حذف {count} عميل منتهٍ"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "Delete client {email}?",
"deleteConfirmContent": "This removes the client from every attached inbound and drops its traffic record. This cannot be undone.",
"deleteSelected": "Delete ({count})",
"adjustSelected": "Adjust ({count})",
"bulkDeleteConfirmTitle": "Delete {count} clients?",
"bulkDeleteConfirmContent": "Each selected client is removed from every attached inbound and its traffic record is dropped. This cannot be undone.",
"bulkAdjustTitle": "Adjust {count} clients",
"bulkAdjustHint": "Positive values extend, negative values reduce. Clients with unlimited expiry or traffic are skipped for that field.",
"bulkAdjustNothing": "Set days or traffic before applying.",
"addDays": "Add days",
"addTrafficGB": "Add traffic (GB)",
"delDepleted": "Delete depleted",
"delDepletedConfirmTitle": "Delete depleted clients?",
"delDepletedConfirmContent": "Removes every client whose traffic quota is exhausted or whose expiry has passed. This cannot be undone.",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} deleted, {failed} failed",
"bulkCreated": "{count} clients created",
"bulkCreatedMixed": "{ok} created, {failed} failed",
"bulkAdjusted": "{count} clients adjusted",
"bulkAdjustedMixed": "{ok} adjusted, {skipped} skipped",
"delDepleted": "{count} depleted clients deleted"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "¿Eliminar al cliente {email}?",
"deleteConfirmContent": "Esto elimina al cliente de cada inbound asociado y descarta su registro de tráfico. No se puede deshacer.",
"deleteSelected": "Eliminar ({count})",
"adjustSelected": "Ajustar ({count})",
"bulkDeleteConfirmTitle": "¿Eliminar {count} clientes?",
"bulkDeleteConfirmContent": "Cada cliente seleccionado se elimina de los inbounds asociados y se descarta su registro de tráfico. No se puede deshacer.",
"bulkAdjustTitle": "Ajustar {count} clientes",
"bulkAdjustHint": "Los valores positivos extienden, los negativos reducen. Los clientes con expiración o tráfico ilimitado se omiten para ese campo.",
"bulkAdjustNothing": "Establece días o tráfico antes de aplicar.",
"addDays": "Añadir días",
"addTrafficGB": "Añadir tráfico (GB)",
"delDepleted": "Eliminar agotados",
"delDepletedConfirmTitle": "¿Eliminar clientes agotados?",
"delDepletedConfirmContent": "Elimina todos los clientes con cuota agotada o expirados. No se puede deshacer.",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} eliminados, {failed} fallidos",
"bulkCreated": "{count} clientes creados",
"bulkCreatedMixed": "{ok} creados, {failed} fallidos",
"bulkAdjusted": "{count} clientes ajustados",
"bulkAdjustedMixed": "{ok} ajustados, {skipped} omitidos",
"delDepleted": "{count} clientes agotados eliminados"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "حذف کلاینت {email}؟",
"deleteConfirmContent": "این کلاینت از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک می‌شود. این عمل غیرقابل بازگشت است.",
"deleteSelected": "حذف ({count})",
"adjustSelected": "تنظیم ({count})",
"bulkDeleteConfirmTitle": "حذف {count} کلاینت؟",
"bulkDeleteConfirmContent": "هر کلاینت انتخاب‌شده از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک می‌شود. این عمل غیرقابل بازگشت است.",
"bulkAdjustTitle": "تنظیم {count} کلاینت",
"bulkAdjustHint": "مقادیر مثبت اضافه و منفی کم می‌کنند. کلاینت‌هایی که زمان یا ترافیک نامحدود دارند برای همان فیلد رد می‌شوند.",
"bulkAdjustNothing": "قبل از اعمال، روز یا ترافیک را تنظیم کنید.",
"addDays": "افزودن روز",
"addTrafficGB": "افزودن ترافیک (گیگابایت)",
"delDepleted": "حذف اتمام‌یافته‌ها",
"delDepletedConfirmTitle": "حذف کلاینت‌های اتمام‌یافته؟",
"delDepletedConfirmContent": "هر کلاینتی که سهمیه ترافیک‌اش تمام شده یا تاریخ انقضایش گذشته است حذف می‌شود. این عمل غیرقابل بازگشت است.",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} حذف، {failed} ناموفق",
"bulkCreated": "{count} کلاینت ساخته شد",
"bulkCreatedMixed": "{ok} ساخته شد، {failed} ناموفق",
"bulkAdjusted": "{count} کلاینت تنظیم شد",
"bulkAdjustedMixed": "{ok} تنظیم، {skipped} رد شد",
"delDepleted": "{count} کلاینت اتمام‌یافته حذف شد"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "Hapus klien {email}?",
"deleteConfirmContent": "Tindakan ini menghapus klien dari setiap inbound terlampir dan menghapus catatan lalu lintasnya. Tidak dapat dibatalkan.",
"deleteSelected": "Hapus ({count})",
"adjustSelected": "Sesuaikan ({count})",
"bulkDeleteConfirmTitle": "Hapus {count} klien?",
"bulkDeleteConfirmContent": "Setiap klien yang dipilih dihapus dari semua inbound terlampir dan catatan lalu lintasnya dihapus. Tidak dapat dibatalkan.",
"bulkAdjustTitle": "Sesuaikan {count} klien",
"bulkAdjustHint": "Nilai positif menambah, negatif mengurangi. Klien dengan masa berlaku atau trafik tak terbatas dilewati untuk bidang tersebut.",
"bulkAdjustNothing": "Setel hari atau trafik sebelum menerapkan.",
"addDays": "Tambah hari",
"addTrafficGB": "Tambah trafik (GB)",
"delDepleted": "Hapus yang habis",
"delDepletedConfirmTitle": "Hapus klien yang habis?",
"delDepletedConfirmContent": "Hapus setiap klien yang kuota lalu lintasnya habis atau yang masa berlakunya telah berakhir. Tidak dapat dibatalkan.",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} dihapus, {failed} gagal",
"bulkCreated": "{count} klien dibuat",
"bulkCreatedMixed": "{ok} dibuat, {failed} gagal",
"bulkAdjusted": "{count} klien disesuaikan",
"bulkAdjustedMixed": "{ok} disesuaikan, {skipped} dilewati",
"delDepleted": "{count} klien habis dihapus"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "クライアント {email} を削除しますか?",
"deleteConfirmContent": "クライアントを関連付けされたすべてのインバウンドから削除し、トラフィック記録も破棄します。元に戻せません。",
"deleteSelected": "削除 ({count})",
"adjustSelected": "調整 ({count})",
"bulkDeleteConfirmTitle": "{count} 件のクライアントを削除しますか?",
"bulkDeleteConfirmContent": "選択された各クライアントを関連付けされたすべてのインバウンドから削除し、トラフィック記録も破棄します。元に戻せません。",
"bulkAdjustTitle": "{count} 件のクライアントを調整",
"bulkAdjustHint": "正の値は延長、負の値は短縮します。無期限の有効期限または無制限のトラフィックを持つクライアントは、その項目についてスキップされます。",
"bulkAdjustNothing": "適用する前に日数またはトラフィックを設定してください。",
"addDays": "日数を追加",
"addTrafficGB": "トラフィックを追加 (GB)",
"delDepleted": "使い切ったクライアントを削除",
"delDepletedConfirmTitle": "使い切ったクライアントを削除しますか?",
"delDepletedConfirmContent": "トラフィック上限に達したか有効期限が切れたクライアントをすべて削除します。元に戻せません。",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} 件削除、{failed} 件失敗",
"bulkCreated": "{count} 件のクライアントを作成しました",
"bulkCreatedMixed": "{ok} 件作成、{failed} 件失敗",
"bulkAdjusted": "{count} 件のクライアントを調整しました",
"bulkAdjustedMixed": "{ok} 件調整、{skipped} 件スキップ",
"delDepleted": "使い切った {count} 件のクライアントを削除しました"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "Excluir o cliente {email}?",
"deleteConfirmContent": "Isto remove o cliente de cada inbound associado e descarta o registro de tráfego. Não é possível desfazer.",
"deleteSelected": "Excluir ({count})",
"adjustSelected": "Ajustar ({count})",
"bulkDeleteConfirmTitle": "Excluir {count} clientes?",
"bulkDeleteConfirmContent": "Cada cliente selecionado é removido dos inbounds associados e o registro de tráfego é descartado. Não é possível desfazer.",
"bulkAdjustTitle": "Ajustar {count} clientes",
"bulkAdjustHint": "Valores positivos estendem, negativos reduzem. Clientes com expiração ou tráfego ilimitado são ignorados para esse campo.",
"bulkAdjustNothing": "Defina dias ou tráfego antes de aplicar.",
"addDays": "Adicionar dias",
"addTrafficGB": "Adicionar tráfego (GB)",
"delDepleted": "Excluir esgotados",
"delDepletedConfirmTitle": "Excluir clientes esgotados?",
"delDepletedConfirmContent": "Remove todos os clientes cuja cota de tráfego foi esgotada ou cuja expiração já passou. Não é possível desfazer.",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} excluídos, {failed} com falha",
"bulkCreated": "{count} clientes criados",
"bulkCreatedMixed": "{ok} criados, {failed} com falha",
"bulkAdjusted": "{count} clientes ajustados",
"bulkAdjustedMixed": "{ok} ajustados, {skipped} ignorados",
"delDepleted": "{count} clientes esgotados excluídos"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "Удалить клиента {email}?",
"deleteConfirmContent": "Клиент будет удалён из всех привязанных входящих, а его запись трафика будет уничтожена. Это действие нельзя отменить.",
"deleteSelected": "Удалить ({count})",
"adjustSelected": "Изменить ({count})",
"bulkDeleteConfirmTitle": "Удалить {count} клиентов?",
"bulkDeleteConfirmContent": "Каждый выбранный клиент удаляется из всех привязанных входящих, его запись трафика уничтожается. Это действие нельзя отменить.",
"bulkAdjustTitle": "Изменить {count} клиентов",
"bulkAdjustHint": "Положительные значения добавляют, отрицательные — уменьшают. Клиенты с неограниченным сроком или трафиком пропускаются для соответствующего поля.",
"bulkAdjustNothing": "Укажите дни или трафик перед применением.",
"addDays": "Добавить дни",
"addTrafficGB": "Добавить трафик (ГБ)",
"delDepleted": "Удалить исчерпанных",
"delDepletedConfirmTitle": "Удалить исчерпанных клиентов?",
"delDepletedConfirmContent": "Удаляются все клиенты, у которых исчерпана квота трафика или истёк срок. Это действие нельзя отменить.",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "Удалено: {ok}, не удалось: {failed}",
"bulkCreated": "Создано клиентов: {count}",
"bulkCreatedMixed": "Создано: {ok}, не удалось: {failed}",
"bulkAdjusted": "Изменено клиентов: {count}",
"bulkAdjustedMixed": "Изменено: {ok}, пропущено: {skipped}",
"delDepleted": "Удалено исчерпанных клиентов: {count}"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "{email} istemcisi silinsin mi?",
"deleteConfirmContent": "Bu işlem istemciyi bağlı tüm inbound'lardan kaldırır ve trafik kaydını siler. Geri alınamaz.",
"deleteSelected": "Sil ({count})",
"adjustSelected": "Ayarla ({count})",
"bulkDeleteConfirmTitle": "{count} istemci silinsin mi?",
"bulkDeleteConfirmContent": "Seçili her istemci bağlı tüm inbound'lardan kaldırılır ve trafik kaydı silinir. Geri alınamaz.",
"bulkAdjustTitle": "{count} istemciyi ayarla",
"bulkAdjustHint": "Pozitif değerler ekler, negatif değerler azaltır. Sınırsız süreli veya trafikli istemciler ilgili alan için atlanır.",
"bulkAdjustNothing": "Uygulamadan önce gün veya trafik belirleyin.",
"addDays": "Gün ekle",
"addTrafficGB": "Trafik ekle (GB)",
"delDepleted": "Tükenmişleri sil",
"delDepletedConfirmTitle": "Tükenmiş istemciler silinsin mi?",
"delDepletedConfirmContent": "Trafik kotası dolan veya süresi geçen tüm istemciler silinir. Geri alınamaz.",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} silindi, {failed} başarısız",
"bulkCreated": "{count} istemci oluşturuldu",
"bulkCreatedMixed": "{ok} oluşturuldu, {failed} başarısız",
"bulkAdjusted": "{count} istemci ayarlandı",
"bulkAdjustedMixed": "{ok} ayarlandı, {skipped} atlandı",
"delDepleted": "{count} tükenmiş istemci silindi"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "Видалити клієнта {email}?",
"deleteConfirmContent": "Клієнт буде вилучений з усіх прив'язаних вхідних, його запис трафіку буде знищено. Цю дію неможливо скасувати.",
"deleteSelected": "Видалити ({count})",
"adjustSelected": "Змінити ({count})",
"bulkDeleteConfirmTitle": "Видалити {count} клієнтів?",
"bulkDeleteConfirmContent": "Кожен вибраний клієнт вилучається з усіх прив'язаних вхідних, його запис трафіку знищується. Цю дію неможливо скасувати.",
"bulkAdjustTitle": "Змінити {count} клієнтів",
"bulkAdjustHint": "Додатні значення подовжують, від'ємні зменшують. Клієнти з необмеженим терміном або трафіком пропускаються для відповідного поля.",
"bulkAdjustNothing": "Вкажіть дні або трафік перед застосуванням.",
"addDays": "Додати дні",
"addTrafficGB": "Додати трафік (ГБ)",
"delDepleted": "Видалити вичерпаних",
"delDepletedConfirmTitle": "Видалити вичерпаних клієнтів?",
"delDepletedConfirmContent": "Видаляються всі клієнти, у яких вичерпана квота трафіку або сплив термін. Цю дію неможливо скасувати.",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "Видалено: {ok}, не вдалось: {failed}",
"bulkCreated": "Створено клієнтів: {count}",
"bulkCreatedMixed": "Створено: {ok}, не вдалось: {failed}",
"bulkAdjusted": "Змінено клієнтів: {count}",
"bulkAdjustedMixed": "Змінено: {ok}, пропущено: {skipped}",
"delDepleted": "Видалено вичерпаних клієнтів: {count}"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "Xóa khách hàng {email}?",
"deleteConfirmContent": "Hành động này gỡ khách hàng khỏi mọi inbound đã gắn và xóa bản ghi lưu lượng. Không thể hoàn tác.",
"deleteSelected": "Xóa ({count})",
"adjustSelected": "Điều chỉnh ({count})",
"bulkDeleteConfirmTitle": "Xóa {count} khách hàng?",
"bulkDeleteConfirmContent": "Mỗi khách hàng được chọn sẽ bị gỡ khỏi tất cả inbound đã gắn và bản ghi lưu lượng cũng bị xóa. Không thể hoàn tác.",
"bulkAdjustTitle": "Điều chỉnh {count} khách hàng",
"bulkAdjustHint": "Giá trị dương kéo dài, giá trị âm rút ngắn. Khách hàng có hạn hoặc lưu lượng không giới hạn sẽ bị bỏ qua cho trường đó.",
"bulkAdjustNothing": "Đặt số ngày hoặc lưu lượng trước khi áp dụng.",
"addDays": "Thêm ngày",
"addTrafficGB": "Thêm lưu lượng (GB)",
"delDepleted": "Xóa hết hạn mức",
"delDepletedConfirmTitle": "Xóa khách hàng hết hạn mức?",
"delDepletedConfirmContent": "Gỡ tất cả khách hàng đã dùng hết hạn mức lưu lượng hoặc đã quá hạn. Không thể hoàn tác.",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "Đã xóa {ok}, thất bại {failed}",
"bulkCreated": "Đã tạo {count} khách hàng",
"bulkCreatedMixed": "Đã tạo {ok}, thất bại {failed}",
"bulkAdjusted": "Đã điều chỉnh {count} khách hàng",
"bulkAdjustedMixed": "Đã điều chỉnh {ok}, bỏ qua {skipped}",
"delDepleted": "Đã xóa {count} khách hàng hết hạn mức"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "删除客户端 {email}",
"deleteConfirmContent": "将从所有关联入站中移除该客户端并删除其流量记录。该操作不可撤销。",
"deleteSelected": "删除 ({count})",
"adjustSelected": "调整 ({count})",
"bulkDeleteConfirmTitle": "删除 {count} 个客户端?",
"bulkDeleteConfirmContent": "每个所选客户端都会从关联的入站中被移除,其流量记录也会被删除。该操作不可撤销。",
"bulkAdjustTitle": "调整 {count} 个客户端",
"bulkAdjustHint": "正值延长,负值减少。具有无限期限或流量的客户端将跳过该字段。",
"bulkAdjustNothing": "应用前请设置天数或流量。",
"addDays": "添加天数",
"addTrafficGB": "添加流量 (GB)",
"delDepleted": "删除已耗尽",
"delDepletedConfirmTitle": "删除已耗尽的客户端?",
"delDepletedConfirmContent": "删除所有流量配额已用尽或已过期的客户端。该操作不可撤销。",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "已删除 {ok} 个,失败 {failed} 个",
"bulkCreated": "已创建 {count} 个客户端",
"bulkCreatedMixed": "已创建 {ok} 个,失败 {failed} 个",
"bulkAdjusted": "已调整 {count} 个客户端",
"bulkAdjustedMixed": "已调整 {ok} 个,跳过 {skipped} 个",
"delDepleted": "已删除 {count} 个已耗尽的客户端"
}
},

View file

@ -479,8 +479,14 @@
"deleteConfirmTitle": "刪除客戶端 {email}",
"deleteConfirmContent": "將從所有關聯入站中移除該客戶端並刪除其流量紀錄。此操作無法復原。",
"deleteSelected": "刪除 ({count})",
"adjustSelected": "調整 ({count})",
"bulkDeleteConfirmTitle": "刪除 {count} 個客戶端?",
"bulkDeleteConfirmContent": "每個所選客戶端都會從關聯的入站中被移除,其流量紀錄也會被刪除。此操作無法復原。",
"bulkAdjustTitle": "調整 {count} 個客戶端",
"bulkAdjustHint": "正值延長,負值減少。具有無限期限或流量的客戶端將跳過該欄位。",
"bulkAdjustNothing": "套用前請設定天數或流量。",
"addDays": "新增天數",
"addTrafficGB": "新增流量 (GB)",
"delDepleted": "刪除已耗盡",
"delDepletedConfirmTitle": "刪除已耗盡的客戶端?",
"delDepletedConfirmContent": "刪除所有流量配額已用盡或已過期的客戶端。此操作無法復原。",
@ -503,6 +509,8 @@
"bulkDeletedMixed": "已刪除 {ok} 個,失敗 {failed} 個",
"bulkCreated": "已建立 {count} 個客戶端",
"bulkCreatedMixed": "已建立 {ok} 個,失敗 {failed} 個",
"bulkAdjusted": "已調整 {count} 個客戶端",
"bulkAdjustedMixed": "已調整 {ok} 個,跳過 {skipped} 個",
"delDepleted": "已刪除 {count} 個已耗盡的客戶端"
}
},