@shoppexio/storefront-react 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/dist/account.css +26 -8
- package/dist/account.js +3217 -1001
- package/dist/account.js.map +1 -1
- package/dist/index.d.ts +188 -14
- package/dist/index.js +43 -20
- package/dist/index.js.map +1 -1
- package/eject/account/account-downloads.ts +89 -1
- package/eject/account/account-payment-method.ts +1 -0
- package/eject/account/account-referrals.ts +70 -1
- package/eject/account/account-reseller.ts +6 -140
- package/eject/account/account-subscriptions.ts +158 -6
- package/eject/account/buyer-invoice-status.ts +100 -0
- package/eject/account/downloads.tsx +118 -17
- package/eject/account/hosted-page.ts +45 -0
- package/eject/account/messages/ar.json +150 -39
- package/eject/account/messages/de.json +150 -39
- package/eject/account/messages/en.json +150 -39
- package/eject/account/messages/es.json +150 -39
- package/eject/account/messages/fi.json +150 -39
- package/eject/account/messages/fr.json +150 -39
- package/eject/account/messages/hi.json +150 -39
- package/eject/account/messages/it.json +150 -39
- package/eject/account/messages/lt.json +150 -39
- package/eject/account/messages/nl.json +150 -39
- package/eject/account/messages/pl.json +150 -39
- package/eject/account/messages/pt.json +150 -39
- package/eject/account/messages/ru.json +150 -39
- package/eject/account/messages/tr.json +150 -39
- package/eject/account/messages/uk.json +150 -39
- package/eject/account/messages/zh.json +150 -39
- package/eject/account/orders.tsx +203 -41
- package/eject/account/overview.tsx +6 -0
- package/eject/account/referrals.tsx +47 -5
- package/eject/account/reseller.tsx +62 -308
- package/eject/account/rewards.tsx +5 -0
- package/eject/account/shared.tsx +60 -4
- package/eject/account/subscriptions.tsx +387 -50
- package/package.json +4 -4
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One buyer status table (Design Batch 9, C2): what an invoice status means to
|
|
3
|
+
* the buyer, for every surface that states it — the hosted checkout, the theme
|
|
4
|
+
* account and the code-lane account.
|
|
5
|
+
*
|
|
6
|
+
* Data only, no strings. `labelKey` names the word; each lane maps it into its
|
|
7
|
+
* own message files (checkout `buyerStatus.*`, theme `account.status.*`, code
|
|
8
|
+
* lane `invoiceStatus.*`), so the same status reads the same word everywhere.
|
|
9
|
+
*
|
|
10
|
+
* tone positive done and good, notice your turn or still moving,
|
|
11
|
+
* danger a dispute, neutral over and nothing to act on
|
|
12
|
+
* paid the buyer's money arrived (AWAITING_STOCK included)
|
|
13
|
+
* receipt a receipt exists: paid, and not under a dispute
|
|
14
|
+
* live the order still moves without the buyer starting over
|
|
15
|
+
* filter the order-list group the status belongs to, or null (All only)
|
|
16
|
+
*
|
|
17
|
+
* Exhaustive over the backend's `InvoiceStatus` enum
|
|
18
|
+
* (apps/backend/drizzle/schema/enums.ts; parity test in apps/backend), so a new
|
|
19
|
+
* status cannot reach a buyer without a row. Nova ports this file
|
|
20
|
+
* (templates/code-storefronts/nova/src/commerce/buyer-invoice-status.ts); the
|
|
21
|
+
* parity test in this package keeps the two equal.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const BUYER_INVOICE_STATUSES = [
|
|
25
|
+
'PENDING',
|
|
26
|
+
'PENDING_PAYMENT',
|
|
27
|
+
'PROCESSING',
|
|
28
|
+
'UNDERPAID',
|
|
29
|
+
'COMPLETED',
|
|
30
|
+
'VOIDED',
|
|
31
|
+
'REFUNDED',
|
|
32
|
+
'PARTIAL_REFUND',
|
|
33
|
+
'DISPUTED',
|
|
34
|
+
'EXPIRED',
|
|
35
|
+
'AWAITING_STOCK',
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
export type BuyerInvoiceStatus = (typeof BUYER_INVOICE_STATUSES)[number];
|
|
39
|
+
|
|
40
|
+
export type BuyerStatusTone = 'positive' | 'notice' | 'danger' | 'neutral';
|
|
41
|
+
|
|
42
|
+
/** The order-list groups, in the order a filter lists them. The id is the wire value of `status_group`. */
|
|
43
|
+
export const BUYER_STATUS_FILTERS = [
|
|
44
|
+
{ id: 'awaiting_payment', labelKey: 'awaitingPayment' },
|
|
45
|
+
{ id: 'paid', labelKey: 'paid' },
|
|
46
|
+
{ id: 'refunded', labelKey: 'refunded' },
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export type BuyerStatusFilter = (typeof BUYER_STATUS_FILTERS)[number]['id'];
|
|
50
|
+
|
|
51
|
+
export interface BuyerInvoiceStatusRow {
|
|
52
|
+
labelKey:
|
|
53
|
+
| 'awaitingPayment'
|
|
54
|
+
| 'processing'
|
|
55
|
+
| 'underpaid'
|
|
56
|
+
| 'awaitingStock'
|
|
57
|
+
| 'completed'
|
|
58
|
+
| 'partialRefund'
|
|
59
|
+
| 'refunded'
|
|
60
|
+
| 'disputed'
|
|
61
|
+
| 'expired'
|
|
62
|
+
| 'voided';
|
|
63
|
+
tone: BuyerStatusTone;
|
|
64
|
+
paid: boolean;
|
|
65
|
+
receipt: boolean;
|
|
66
|
+
live: boolean;
|
|
67
|
+
filter: BuyerStatusFilter | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Every word a lane needs a message for: the status labels and the filter labels. */
|
|
71
|
+
export type BuyerStatusLabelKey = BuyerInvoiceStatusRow['labelKey'] | (typeof BUYER_STATUS_FILTERS)[number]['labelKey'];
|
|
72
|
+
|
|
73
|
+
export const BUYER_INVOICE_STATUS: Readonly<Record<BuyerInvoiceStatus, Readonly<BuyerInvoiceStatusRow>>> = {
|
|
74
|
+
PENDING: { labelKey: 'awaitingPayment', tone: 'notice', paid: false, receipt: false, live: false, filter: 'awaiting_payment' },
|
|
75
|
+
PENDING_PAYMENT: { labelKey: 'awaitingPayment', tone: 'notice', paid: false, receipt: false, live: false, filter: 'awaiting_payment' },
|
|
76
|
+
PROCESSING: { labelKey: 'processing', tone: 'neutral', paid: false, receipt: false, live: true, filter: 'awaiting_payment' },
|
|
77
|
+
UNDERPAID: { labelKey: 'underpaid', tone: 'notice', paid: false, receipt: false, live: true, filter: 'awaiting_payment' },
|
|
78
|
+
// "Paid": the buyer has paid, the shop is restocking.
|
|
79
|
+
AWAITING_STOCK: { labelKey: 'awaitingStock', tone: 'notice', paid: true, receipt: true, live: true, filter: 'paid' },
|
|
80
|
+
COMPLETED: { labelKey: 'completed', tone: 'positive', paid: true, receipt: true, live: false, filter: 'paid' },
|
|
81
|
+
// A refund still needs a document, so the receipt stays.
|
|
82
|
+
PARTIAL_REFUND: { labelKey: 'partialRefund', tone: 'neutral', paid: true, receipt: true, live: false, filter: 'refunded' },
|
|
83
|
+
REFUNDED: { labelKey: 'refunded', tone: 'neutral', paid: true, receipt: true, live: false, filter: 'refunded' },
|
|
84
|
+
// A lost chargeback or a reversed capture: never paid, no receipt.
|
|
85
|
+
DISPUTED: { labelKey: 'disputed', tone: 'danger', paid: false, receipt: false, live: false, filter: null },
|
|
86
|
+
EXPIRED: { labelKey: 'expired', tone: 'neutral', paid: false, receipt: false, live: false, filter: null },
|
|
87
|
+
VOIDED: { labelKey: 'voided', tone: 'neutral', paid: false, receipt: false, live: false, filter: null },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** The row for a producer status, or null for a status this table does not know. */
|
|
91
|
+
export function buyerInvoiceStatus(status: string | null | undefined): Readonly<BuyerInvoiceStatusRow> | null {
|
|
92
|
+
return status != null && Object.prototype.hasOwnProperty.call(BUYER_INVOICE_STATUS, status)
|
|
93
|
+
? BUYER_INVOICE_STATUS[status as BuyerInvoiceStatus]
|
|
94
|
+
: null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The statuses an order-list group holds. */
|
|
98
|
+
export function buyerStatusesInFilter(filter: BuyerStatusFilter): BuyerInvoiceStatus[] {
|
|
99
|
+
return BUYER_INVOICE_STATUSES.filter((status) => BUYER_INVOICE_STATUS[status].filter === filter);
|
|
100
|
+
}
|
|
@@ -15,11 +15,14 @@
|
|
|
15
15
|
* clicked has no cause they could point at.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import type { BuyerInvoiceStatus } from './buyer-invoice-status';
|
|
18
19
|
import { useState } from 'react';
|
|
19
20
|
import { Link } from 'react-router-dom';
|
|
20
21
|
import { accountOrderHref } from '@shoppexio/storefront-react';
|
|
21
22
|
import {
|
|
23
|
+
accessRequestTicket,
|
|
22
24
|
buildDownloadGroups,
|
|
25
|
+
describeFileAccess,
|
|
23
26
|
isEmptyDownloadGroups,
|
|
24
27
|
licenseExpiryDate,
|
|
25
28
|
offersRenewal,
|
|
@@ -43,13 +46,58 @@ import {
|
|
|
43
46
|
CopyButton,
|
|
44
47
|
StatusChip,
|
|
45
48
|
formatAccountDate,
|
|
49
|
+
useAccountResource,
|
|
46
50
|
} from './shared';
|
|
47
51
|
|
|
52
|
+
/** The one status the buyer status table calls paid and still moving: the buyer paid, the shop is restocking. */
|
|
53
|
+
export const WAITING_ORDER_STATUS = 'AWAITING_STOCK' satisfies BuyerInvoiceStatus;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Nothing to download yet. When a paid order waits for stock, the empty state
|
|
57
|
+
* names it (Design Batch 9, C7), asked of the producer by status, never picked
|
|
58
|
+
* out of a page in hand. Without one, or while that read has not answered, the
|
|
59
|
+
* general sentence stands; it is true either way.
|
|
60
|
+
*/
|
|
61
|
+
function EmptyDownloads() {
|
|
62
|
+
const { state } = useAccountResource('downloads:waiting-order', () =>
|
|
63
|
+
callCustomerApi((sdk) => sdk.orders({ page: 1, limit: 1, status: WAITING_ORDER_STATUS })));
|
|
64
|
+
const waiting = state.status === 'ready' ? state.data.invoices[0] : undefined;
|
|
65
|
+
const item = waiting?.lineItems[0]?.productTitle;
|
|
66
|
+
|
|
67
|
+
if (waiting && item) {
|
|
68
|
+
return (
|
|
69
|
+
<AccountEmpty
|
|
70
|
+
title={t('downloads.emptyTitle')}
|
|
71
|
+
body={t('downloads.waitingOrder', { id: waiting.uniqid, item })}
|
|
72
|
+
action={(
|
|
73
|
+
<Link to={accountOrderHref(waiting.uniqid)} className="sx-account-button sxa:card-button sxa:card-button--add">
|
|
74
|
+
{t('downloads.openOrder')}
|
|
75
|
+
</Link>
|
|
76
|
+
)}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<AccountEmpty
|
|
83
|
+
title={t('downloads.emptyTitle')}
|
|
84
|
+
body={t('downloads.emptyBody')}
|
|
85
|
+
action={(
|
|
86
|
+
<Link to="/all-products" className="sx-account-button sxa:card-button sxa:card-button--add">
|
|
87
|
+
{t('common.browseStore')}
|
|
88
|
+
</Link>
|
|
89
|
+
)}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
48
94
|
export function AccountDownloads() {
|
|
49
95
|
const { state, reload } = useAccountDashboard();
|
|
50
96
|
const [notice, setNotice] = useState<string | null>(null);
|
|
51
97
|
const [actionError, setActionError] = useState<string | null>(null);
|
|
52
98
|
const [resettingUniqid, setResettingUniqid] = useState<string | null>(null);
|
|
99
|
+
const [askingId, setAskingId] = useState<string | null>(null);
|
|
100
|
+
const [askedIds, setAskedIds] = useState<ReadonlySet<string>>(() => new Set());
|
|
53
101
|
|
|
54
102
|
if (state.status === 'loading') {
|
|
55
103
|
return <AccountRowsSkeleton rows={4} label={t('downloads.loading')} />;
|
|
@@ -75,19 +123,7 @@ export function AccountDownloads() {
|
|
|
75
123
|
|
|
76
124
|
const { groups } = grouped;
|
|
77
125
|
|
|
78
|
-
if (isEmptyDownloadGroups(groups))
|
|
79
|
-
return (
|
|
80
|
-
<AccountEmpty
|
|
81
|
-
title={t('downloads.emptyTitle')}
|
|
82
|
-
body={t('downloads.emptyBody')}
|
|
83
|
-
action={(
|
|
84
|
-
<Link to="/all-products" className="sx-account-button sxa:card-button sxa:card-button--add">
|
|
85
|
-
{t('common.browseStore')}
|
|
86
|
-
</Link>
|
|
87
|
-
)}
|
|
88
|
-
/>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
126
|
+
if (isEmptyDownloadGroups(groups)) return <EmptyDownloads />;
|
|
91
127
|
|
|
92
128
|
const resetHardwareId = async (uniqid: string) => {
|
|
93
129
|
setResettingUniqid(uniqid);
|
|
@@ -108,6 +144,29 @@ export function AccountDownloads() {
|
|
|
108
144
|
reload();
|
|
109
145
|
};
|
|
110
146
|
|
|
147
|
+
/*
|
|
148
|
+
* "Ask for access" is a support ticket on the order, through the same
|
|
149
|
+
* `createTicket` the Support panel uses, so the shop answers it where it
|
|
150
|
+
* answers everything else. One request in flight; a row that asked once says
|
|
151
|
+
* so instead of offering a second ticket.
|
|
152
|
+
*/
|
|
153
|
+
const askForAccess = async (file: FileDownload, reason: string | null) => {
|
|
154
|
+
if (askingId !== null) return;
|
|
155
|
+
setAskingId(file.id);
|
|
156
|
+
setNotice(null);
|
|
157
|
+
setActionError(null);
|
|
158
|
+
const result = await callCustomerApi((sdk) => sdk.createTicket(accessRequestTicket(file, reason)));
|
|
159
|
+
setAskingId(null);
|
|
160
|
+
|
|
161
|
+
if (!result.success) {
|
|
162
|
+
setActionError(result.message?.trim() || t('downloads.accessRequestFailed'));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setAskedIds((current) => new Set(current).add(file.id));
|
|
167
|
+
setNotice(t('downloads.accessRequestDone'));
|
|
168
|
+
};
|
|
169
|
+
|
|
111
170
|
return (
|
|
112
171
|
<div className="sxa:space-y-8">
|
|
113
172
|
{notice ? <AccountNotice message={notice} /> : null}
|
|
@@ -137,7 +196,13 @@ export function AccountDownloads() {
|
|
|
137
196
|
<ul className="sxa:mt-4 sxa:space-y-3">
|
|
138
197
|
{groups.files.map((file) => (
|
|
139
198
|
<li key={file.id}>
|
|
140
|
-
<FileRow
|
|
199
|
+
<FileRow
|
|
200
|
+
file={file}
|
|
201
|
+
asking={askingId === file.id}
|
|
202
|
+
asked={askedIds.has(file.id)}
|
|
203
|
+
disabled={askingId !== null}
|
|
204
|
+
onAskForAccess={(reason) => void askForAccess(file, reason)}
|
|
205
|
+
/>
|
|
141
206
|
</li>
|
|
142
207
|
))}
|
|
143
208
|
</ul>
|
|
@@ -267,7 +332,19 @@ function LicenseRow({
|
|
|
267
332
|
);
|
|
268
333
|
}
|
|
269
334
|
|
|
270
|
-
function FileRow({
|
|
335
|
+
function FileRow({
|
|
336
|
+
file,
|
|
337
|
+
asking,
|
|
338
|
+
asked,
|
|
339
|
+
disabled,
|
|
340
|
+
onAskForAccess,
|
|
341
|
+
}: {
|
|
342
|
+
file: FileDownload;
|
|
343
|
+
asking: boolean;
|
|
344
|
+
asked: boolean;
|
|
345
|
+
disabled: boolean;
|
|
346
|
+
onAskForAccess: (reason: string | null) => void;
|
|
347
|
+
}) {
|
|
271
348
|
/*
|
|
272
349
|
* A plain link, not a fetch. The path is same-origin under `/api/customer/`,
|
|
273
350
|
* so the browser sends the session cookie the Shoppex worker owns — there is
|
|
@@ -279,21 +356,45 @@ function FileRow({ file }: { file: FileDownload }) {
|
|
|
279
356
|
* because a dead link dressed as a button is worse than a plain refusal.
|
|
280
357
|
*/
|
|
281
358
|
const href = safeExternalHref(file.href);
|
|
359
|
+
/*
|
|
360
|
+
* What is left and until when, or why the file closed. A file the server
|
|
361
|
+
* refuses offers no Download: the link would only answer with that refusal.
|
|
362
|
+
* A missing `access` (older backend) is a file with nothing to say.
|
|
363
|
+
*/
|
|
364
|
+
const access = file.access ? describeFileAccess(file.access) : null;
|
|
282
365
|
|
|
283
366
|
return (
|
|
284
367
|
<AccountCard className="sxa:flex sxa:flex-wrap sxa:items-center sxa:gap-4 sxa:px-5 sxa:py-4">
|
|
285
368
|
<BoxIcon className="sxa:h-4 sxa:w-4 sxa:shrink-0 sxa:text-muted" />
|
|
286
369
|
<div className="sxa:min-w-0 sxa:flex-1">
|
|
287
370
|
<p className="sxa:truncate sxa:text-small sxa:font-semibold">{file.label}</p>
|
|
288
|
-
<p className="sxa:mt-1 sxa:
|
|
371
|
+
<p className="sxa:mt-1 sxa:text-micro sxa:text-muted" data-file-access={file.access?.state}>
|
|
372
|
+
{[file.product_title, access?.text].filter(Boolean).join(' · ')}
|
|
373
|
+
</p>
|
|
289
374
|
</div>
|
|
290
|
-
{href === null ? (
|
|
375
|
+
{access?.refused ? null : href === null ? (
|
|
291
376
|
<span className="sx-account-chip sxa:meta-chip sxa:meta-chip--neutral">{t('common.unavailable')}</span>
|
|
292
377
|
) : (
|
|
293
378
|
<a href={href} download className="sx-account-button sxa:card-button sxa:card-button--add sxa:shrink-0">
|
|
294
379
|
{t('downloads.download')}
|
|
295
380
|
</a>
|
|
296
381
|
)}
|
|
382
|
+
{access?.canAsk ? (
|
|
383
|
+
asked ? (
|
|
384
|
+
<p role="status" className="sxa:shrink-0 sxa:text-micro sxa:text-muted">{t('downloads.accessRequested')}</p>
|
|
385
|
+
) : (
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
onClick={() => onAskForAccess(access.text)}
|
|
389
|
+
disabled={disabled}
|
|
390
|
+
aria-busy={asking || undefined}
|
|
391
|
+
className="sx-account-button sxa:card-button sxa:card-button--add sxa:shrink-0 sxa:disabled:cursor-not-allowed sxa:disabled:opacity-50"
|
|
392
|
+
>
|
|
393
|
+
{asking ? <BusySpinner className="sxa:mr-1.5 sxa:h-3.5 sxa:w-3.5" /> : null}
|
|
394
|
+
{asking ? t('downloads.askingAccess') : t('downloads.askAccess')}
|
|
395
|
+
</button>
|
|
396
|
+
)
|
|
397
|
+
) : null}
|
|
297
398
|
</AccountCard>
|
|
298
399
|
);
|
|
299
400
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The way out of the account to a hosted buyer page on the checkout host
|
|
3
|
+
* (design Batch 7, H2 and A1): `{checkout}/{page}/{shopId}`.
|
|
4
|
+
*
|
|
5
|
+
* Adding funds, changing a card and handing out a reseller key happen there,
|
|
6
|
+
* behind the page's own email code, because the JavaScript on a storefront
|
|
7
|
+
* origin may be the merchant's. So the link carries only the shop, the page to
|
|
8
|
+
* come back to and the page's display hints; never an email or a token. The
|
|
9
|
+
* hosted page reads and checks everything again on the server, so a forged
|
|
10
|
+
* hint can at most open the wrong segment.
|
|
11
|
+
*
|
|
12
|
+
* The same rule as `hostedPageUrl` in
|
|
13
|
+
* `packages/storefront-commerce/src/sx/hosted-page-url.ts`, which the theme
|
|
14
|
+
* lane uses. This package does not depend on storefront-commerce, so it keeps
|
|
15
|
+
* its own copy; the two must build the same URL.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type HostedPage = 'topup' | 'reseller' | 'billing';
|
|
19
|
+
|
|
20
|
+
/** `return` is the account page the buyer comes back to; every other entry is a display hint. */
|
|
21
|
+
export type HostedPageQuery = { return: string } & Record<string, string>;
|
|
22
|
+
|
|
23
|
+
export function hostedPageUrl(
|
|
24
|
+
checkoutBaseUrl: string,
|
|
25
|
+
page: HostedPage,
|
|
26
|
+
shopId: string,
|
|
27
|
+
query: HostedPageQuery,
|
|
28
|
+
): string {
|
|
29
|
+
const url = new URL(`/${page}/${encodeURIComponent(shopId)}`, checkoutBaseUrl);
|
|
30
|
+
for (const [name, value] of Object.entries(query)) url.searchParams.set(name, value);
|
|
31
|
+
return url.toString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The link to `page` for the shop this storefront serves, back to the page the
|
|
36
|
+
* buyer is on now. Null when the worker injected no `shopId` or no
|
|
37
|
+
* `checkoutBaseUrl`: without both there is no page to link to, and the caller
|
|
38
|
+
* renders nothing rather than a guessed host.
|
|
39
|
+
*/
|
|
40
|
+
export function hostedPageHref(page: HostedPage, hints: Record<string, string>): string | null {
|
|
41
|
+
const shopId = typeof window.shopId === 'string' ? window.shopId.trim() : '';
|
|
42
|
+
const checkoutBaseUrl = typeof window.checkoutBaseUrl === 'string' ? window.checkoutBaseUrl.trim() : '';
|
|
43
|
+
if (!shopId || !checkoutBaseUrl) return null;
|
|
44
|
+
return hostedPageUrl(checkoutBaseUrl, page, shopId, { ...hints, return: window.location.href });
|
|
45
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"account": {
|
|
13
13
|
"title": "حسابك",
|
|
14
|
+
"secureLink": "تُفتح صفحة Shoppex آمنة.",
|
|
14
15
|
"sections": "أقسام الحساب",
|
|
15
16
|
"signOut": "تسجيل الخروج",
|
|
16
17
|
"signingOut": "جارٍ تسجيل الخروج…",
|
|
@@ -92,11 +93,25 @@
|
|
|
92
93
|
"PAID": "مدفوع",
|
|
93
94
|
"FAILED": "فشل"
|
|
94
95
|
},
|
|
96
|
+
"invoiceStatus": {
|
|
97
|
+
"awaitingPayment": "بانتظار الدفع",
|
|
98
|
+
"processing": "جارٍ تأكيد الدفع",
|
|
99
|
+
"underpaid": "الدفع غير مكتمل",
|
|
100
|
+
"awaitingStock": "بانتظار توفر المخزون",
|
|
101
|
+
"completed": "مكتمل",
|
|
102
|
+
"partialRefund": "مُسترد جزئيًا",
|
|
103
|
+
"refunded": "مُسترَد",
|
|
104
|
+
"disputed": "متنازع عليه",
|
|
105
|
+
"expired": "منتهي الصلاحية",
|
|
106
|
+
"voided": "ألغاها المتجر",
|
|
107
|
+
"paid": "مدفوع"
|
|
108
|
+
},
|
|
95
109
|
"delivery": {
|
|
96
110
|
"DELIVERED": "تم التسليم",
|
|
97
111
|
"AWAITING_FULFILLMENT": "بانتظار التنفيذ",
|
|
98
112
|
"PENDING": "قيد الانتظار",
|
|
99
|
-
"FAILED": "فشل التسليم"
|
|
113
|
+
"FAILED": "فشل التسليم",
|
|
114
|
+
"WAITING": "في الانتظار"
|
|
100
115
|
},
|
|
101
116
|
"overview": {
|
|
102
117
|
"loading": "جارٍ تحميل حسابك",
|
|
@@ -107,6 +122,7 @@
|
|
|
107
122
|
"statTickets": "تذاكر الدعم",
|
|
108
123
|
"walletLabel": "رصيد المتجر",
|
|
109
124
|
"walletNote": "يُطبَّق عند الدفع",
|
|
125
|
+
"topUp": "شحن الرصيد",
|
|
110
126
|
"recentOrders": "أحدث الطلبات",
|
|
111
127
|
"allOrders": "كل الطلبات"
|
|
112
128
|
},
|
|
@@ -138,12 +154,6 @@
|
|
|
138
154
|
"searchPlaceholder": "البحث برقم الفاتورة",
|
|
139
155
|
"status": "الحالة",
|
|
140
156
|
"statusAll": "كل الحالات",
|
|
141
|
-
"statusCompleted": "مكتمل",
|
|
142
|
-
"statusPending": "قيد الانتظار",
|
|
143
|
-
"statusProcessing": "قيد المعالجة",
|
|
144
|
-
"statusRefunded": "مُسترَد",
|
|
145
|
-
"statusDisputed": "متنازع عليه",
|
|
146
|
-
"statusExpired": "منتهي الصلاحية",
|
|
147
157
|
"emptyFiltered": "لا توجد طلبات مطابقة. جرّب رقم فاتورة أو حالة أخرى.",
|
|
148
158
|
"warrantyTitle": "الضمان",
|
|
149
159
|
"warrantyCoveredUntil": "مغطى حتى {date}",
|
|
@@ -162,7 +172,28 @@
|
|
|
162
172
|
"warrantyClaimTooLong": "لا يمكن أن يتجاوز طلبك {max} حرف.",
|
|
163
173
|
"invoicePdf": "تنزيل الفاتورة (PDF)",
|
|
164
174
|
"invoicePdfLoading": "جارٍ التحضير…",
|
|
165
|
-
"
|
|
175
|
+
"reviewTitle": "تقييمك",
|
|
176
|
+
"reviewPrompt": "ما رأيك في {product}؟",
|
|
177
|
+
"reviewPromptGeneric": "ما رأيك في عملية الشراء؟",
|
|
178
|
+
"reviewPositive": "إيجابي",
|
|
179
|
+
"reviewNeutral": "محايد",
|
|
180
|
+
"reviewNegative": "سلبي",
|
|
181
|
+
"reviewEdit": "تعديل التقييم",
|
|
182
|
+
"reviewWrite": "اكتب تقييمًا",
|
|
183
|
+
"reviewAutomated": "تم نشر تقييم إيجابي تلقائي لهذا الطلب. يمكنك استبداله بتقييمك.",
|
|
184
|
+
"reviewFallbackLabel": "تقييم",
|
|
185
|
+
"nothingToDownload": "تم التسليم {date}. سلّمك البائع هذا مباشرةً، لذلك لا يوجد ما يمكن تنزيله.",
|
|
186
|
+
"detailError": "تعذّر تحميل الطلب {id}",
|
|
187
|
+
"noReceiptUnpaid": "لم يُدفع هذا الطلب قط، لذلك لا يوجد له إيصال.",
|
|
188
|
+
"noReceiptVoided": "ألغى المتجر هذا الطلب، لذلك لا يوجد له إيصال.",
|
|
189
|
+
"pdfSignedOut": "تم تسجيل خروجك. سجّل الدخول مرة أخرى لتنزيل الإيصال.",
|
|
190
|
+
"pdfFailed": "تعذّر إنشاء الإيصال. حاول مرة أخرى بعد دقيقة.",
|
|
191
|
+
"waitingStock": {
|
|
192
|
+
"title": "لقد دفعت. البائع يعيد تعبئة المخزون.",
|
|
193
|
+
"body": "سيظهر «{item}» هنا فور تسليمه.",
|
|
194
|
+
"bodyOrder": "سيظهر طلبك هنا فور تسليمه."
|
|
195
|
+
},
|
|
196
|
+
"paidOn": "تم الدفع {date}"
|
|
166
197
|
},
|
|
167
198
|
"downloads": {
|
|
168
199
|
"loading": "جارٍ تحميل تنزيلاتك",
|
|
@@ -186,7 +217,27 @@
|
|
|
186
217
|
"renewAction": "فتح المتجر",
|
|
187
218
|
"brokenTitle": "تعذّر عرض قائمة تنزيلاتك",
|
|
188
219
|
"brokenBody": "أرسل المتجر عنصر ترخيص ({id}) بدون الترخيص نفسه، لذا ستكون هذه القائمة ناقصة لما تذكره.",
|
|
189
|
-
"brokenFileBody": "أرسل المتجر عنصر ملف ({id}) دون أي شيء يمكن تنزيله، لذا ستذكر هذه القائمة ملفًا لا تستطيع تسليمه."
|
|
220
|
+
"brokenFileBody": "أرسل المتجر عنصر ملف ({id}) دون أي شيء يمكن تنزيله، لذا ستذكر هذه القائمة ملفًا لا تستطيع تسليمه.",
|
|
221
|
+
"accessLeftOne": "تبقّى {count} تنزيل",
|
|
222
|
+
"accessLeftOther": "تبقّى {count} من التنزيلات",
|
|
223
|
+
"accessUntil": "حتى {date}",
|
|
224
|
+
"accessLimitReached": "لقد استخدمت جميع التنزيلات",
|
|
225
|
+
"accessLimitReachedOne": "لقد استخدمت التنزيل الوحيد المتاح",
|
|
226
|
+
"accessLimitReachedOther": "لقد استخدمت جميع التنزيلات البالغة {count}",
|
|
227
|
+
"accessExpired": "انتهت التنزيلات في {date}",
|
|
228
|
+
"accessExpiredUndated": "انتهت التنزيلات",
|
|
229
|
+
"accessRevoked": "تم إيقاف التنزيلات لهذا الطلب",
|
|
230
|
+
"accessRefunded": "انتهت التنزيلات لأنه تم استرداد مبلغ هذا الطلب",
|
|
231
|
+
"askAccess": "طلب الوصول",
|
|
232
|
+
"askingAccess": "جارٍ الإرسال…",
|
|
233
|
+
"accessRequested": "تم إرسال الطلب",
|
|
234
|
+
"accessRequestDone": "تم إرسال الطلب. سيرد المتجر في قسم الدعم.",
|
|
235
|
+
"accessRequestFailed": "تعذّر إرسال طلبك. يُرجى المحاولة مرة أخرى.",
|
|
236
|
+
"accessRequestTitle": "الوصول إلى {file}",
|
|
237
|
+
"accessRequestMessage": "أرغب في تنزيل {file} ({product}) من هذا الطلب مرة أخرى.",
|
|
238
|
+
"accessRequestReason": "تعرض صفحة الحساب: {reason}.",
|
|
239
|
+
"waitingOrder": "الطلب {id} مدفوع وبانتظار توفر المخزون. سيظهر «{item}» هنا بمجرد تسليمه.",
|
|
240
|
+
"openOrder": "فتح الطلب"
|
|
190
241
|
},
|
|
191
242
|
"subscriptions": {
|
|
192
243
|
"loading": "جارٍ تحميل اشتراكاتك",
|
|
@@ -197,6 +248,9 @@
|
|
|
197
248
|
"renews": "يتجدّد في {date}",
|
|
198
249
|
"ends": "ينتهي في {date}",
|
|
199
250
|
"canceled": "أُلغي في {date}",
|
|
251
|
+
"endedOn": "انتهى في {date}",
|
|
252
|
+
"endedHoldRanOut": "انتهت فترة الإيقاف دون دفع. يمكنك الاشتراك مجددًا من المتجر.",
|
|
253
|
+
"endedShopEnded": "لم يُدفع التجديد وأنهى المتجر الاشتراك. يمكنك الاشتراك مجددًا من المتجر.",
|
|
200
254
|
"startedOn": "بدأ في {date}",
|
|
201
255
|
"billingDayOne": "فوترة يومية",
|
|
202
256
|
"billingDayOther": "فوترة كل {count} أيام",
|
|
@@ -211,10 +265,8 @@
|
|
|
211
265
|
"paymentMethod": "طريقة الدفع",
|
|
212
266
|
"cardOnFile": "{brand} المنتهية بالأرقام {last4}",
|
|
213
267
|
"noPaymentMethod": "لا توجد طريقة محفوظة",
|
|
214
|
-
"paymentMethodElsewhere": "رابط تغيير بيانات الدفع يصلك من رسائل المتجر نفسها. لا يُعرض هنا، لأن أي عنصر في هذه الصفحة قد يلتقطه.",
|
|
215
268
|
"pause": "إيقاف مؤقت",
|
|
216
269
|
"pausing": "جارٍ الإيقاف…",
|
|
217
|
-
"resume": "استئناف",
|
|
218
270
|
"resuming": "جارٍ الاستئناف…",
|
|
219
271
|
"cancel": "إلغاء الاشتراك",
|
|
220
272
|
"cancelling": "جارٍ الإلغاء…",
|
|
@@ -223,14 +275,82 @@
|
|
|
223
275
|
"confirmCancelAtPeriodEnd": "يبقى وصولك حتى {date}. لن تُحصَّل أي مبالغ بعد ذلك.",
|
|
224
276
|
"confirmCancelNow": "ينتهي الوصول فور الإلغاء.",
|
|
225
277
|
"actionFailed": "لم تتم العملية. يُرجى المحاولة مرة أخرى.",
|
|
226
|
-
"paused": "هذا الاشتراك متوقف مؤقتًا.",
|
|
227
278
|
"resumed": "هذا الاشتراك يعمل من جديد.",
|
|
228
279
|
"cancelled": "تم إلغاء هذا الاشتراك.",
|
|
229
280
|
"historyTitle": "سجل الفواتير",
|
|
230
281
|
"historyLoading": "جارٍ تحميل سجل الفواتير",
|
|
231
282
|
"historyEmptyTitle": "لا يوجد سجل فواتير بعد",
|
|
232
283
|
"historyEmptyBody": "تظهر فواتير التجديد هنا بعد الدورة الأولى.",
|
|
233
|
-
"trial": "تنتهي الفترة التجريبية في {date}"
|
|
284
|
+
"trial": "تنتهي الفترة التجريبية في {date}",
|
|
285
|
+
"statusOnHold": "معلّق",
|
|
286
|
+
"statusPaymentDue": "الدفع مستحق",
|
|
287
|
+
"holdNote": "تم إيقاف وصولك مؤقتًا. ادفع {amount} قبل {date} وسيعود فورًا، مع {interval} من يوم الدفع.",
|
|
288
|
+
"dueNote": "تجديدك بتاريخ {renewal} غير مدفوع. ادفع قبل {date} للحفاظ على وصولك.",
|
|
289
|
+
"readyNote": "تجديدك بتاريخ {renewal} جاهز. ادفع في أي وقت قبل ذلك؛ {interval} في {renewal} كالمعتاد.",
|
|
290
|
+
"holdIntervalDay": "يوم جديد يبدأ",
|
|
291
|
+
"holdIntervalWeek": "أسبوع جديد يبدأ",
|
|
292
|
+
"holdIntervalMonth": "شهر جديد يبدأ",
|
|
293
|
+
"holdIntervalYear": "سنة جديدة تبدأ",
|
|
294
|
+
"holdIntervalOther": "فترة فوترة جديدة تبدأ",
|
|
295
|
+
"readyIntervalDay": "سيبدأ اليوم التالي",
|
|
296
|
+
"readyIntervalWeek": "سيبدأ الأسبوع التالي",
|
|
297
|
+
"readyIntervalMonth": "سيبدأ الشهر التالي",
|
|
298
|
+
"readyIntervalYear": "ستبدأ السنة التالية",
|
|
299
|
+
"readyIntervalOther": "ستبدأ فترة الفوترة التالية",
|
|
300
|
+
"payAndResume": "ادفع {amount} واستأنف",
|
|
301
|
+
"payAmount": "ادفع {amount} الآن",
|
|
302
|
+
"pauses": "يتوقف مؤقتًا في {date}",
|
|
303
|
+
"payBy": "ادفع قبل {date}",
|
|
304
|
+
"resumes": "يُستأنف في {date}",
|
|
305
|
+
"pausedOpenEnded": "متوقف مؤقتًا حتى تستأنفه",
|
|
306
|
+
"pauseTitle": "إيقاف {plan} مؤقتًا؟",
|
|
307
|
+
"pauseStartsAfter": "تتوقف خطتك بعد {date}، أي في نهاية الفترة التي دفعت مقابلها.",
|
|
308
|
+
"pauseHowLong": "المدة",
|
|
309
|
+
"pauseReasonLabel": "لماذا توقفه مؤقتًا؟",
|
|
310
|
+
"pauseOptional": "اختياري",
|
|
311
|
+
"pauseReasonNone": "اختر سببًا",
|
|
312
|
+
"pauseReasonTakingABreak": "آخذ استراحة",
|
|
313
|
+
"pauseReasonTooExpensive": "مكلف جدًا حاليًا",
|
|
314
|
+
"pauseReasonNotUsing": "لا أستخدمه بما يكفي",
|
|
315
|
+
"pauseReasonNotWorking": "هناك شيء لا يعمل",
|
|
316
|
+
"pauseReasonOther": "سبب آخر",
|
|
317
|
+
"pauseLength7": "أسبوع واحد",
|
|
318
|
+
"pauseLength14": "أسبوعان",
|
|
319
|
+
"pauseLength30": "شهر واحد",
|
|
320
|
+
"pauseLength60": "شهران",
|
|
321
|
+
"pauseLength90": "3 أشهر",
|
|
322
|
+
"pauseSummary": "لا دفع من {start} إلى {end}. يُستأنف تلقائيًا في {end}.",
|
|
323
|
+
"pauseResumeAnyTime": "يمكنك الاستئناف قبل ذلك في أي وقت.",
|
|
324
|
+
"pauseLeftNone": "هذا آخر إيقاف مؤقت لك خلال الأشهر الـ12 القادمة.",
|
|
325
|
+
"pauseLeftOne": "يمكنك الإيقاف المؤقت مرة واحدة أخرى خلال الأشهر الـ12 القادمة.",
|
|
326
|
+
"pauseLeftMany": "عدد مرات الإيقاف المؤقت المتبقية خلال الأشهر الـ12 القادمة: {count}.",
|
|
327
|
+
"pauseConfirm": "إيقاف مؤقت حتى {date}",
|
|
328
|
+
"pauseKeep": "الاحتفاظ بالاشتراك",
|
|
329
|
+
"pauseError": "تعذّر إيقاف اشتراكك مؤقتًا. لم يتغير شيء. حاول مرة أخرى بعد قليل.",
|
|
330
|
+
"pausedUntil": "متوقف مؤقتًا حتى {date}",
|
|
331
|
+
"pausedBody": "لا دفع حتى ذلك الحين.",
|
|
332
|
+
"pauseAccessRole": "يُزال دورك {roles} أثناء الإيقاف المؤقت.",
|
|
333
|
+
"pauseAccessRoles": "تُزال أدوارك {roles} أثناء الإيقاف المؤقت.",
|
|
334
|
+
"pauseAccess": "يُزال وصولك أثناء الإيقاف المؤقت.",
|
|
335
|
+
"pausedPayToday": "إذا استأنفت الآن فستدفع {price} اليوم مقابل فترة فوترة جديدة.",
|
|
336
|
+
"pauseScheduled": "يتوقف مؤقتًا بعد {date}",
|
|
337
|
+
"pauseScheduledBody": "يبقى وصولك حتى ذلك الحين. يُستأنف في {date}.",
|
|
338
|
+
"pauseScheduledBodyOpenEnded": "يبقى وصولك حتى ذلك الحين.",
|
|
339
|
+
"resumeNow": "استئناف الآن",
|
|
340
|
+
"dontPause": "عدم الإيقاف المؤقت",
|
|
341
|
+
"pauseLimit": "استخدمت مرات الإيقاف المؤقت لهذا العام (العدد: {count}). يمكنك الإيقاف المؤقت مجددًا بدءًا من {date}.",
|
|
342
|
+
"pauseLimitOne": "استخدمت الإيقاف المؤقت لهذا العام. يمكنك الإيقاف المؤقت مجددًا بدءًا من {date}.",
|
|
343
|
+
"pauseSavedNotice": "تم ضبط الإيقاف المؤقت.",
|
|
344
|
+
"pauseCalledOff": "لن يتوقف اشتراكك مؤقتًا.",
|
|
345
|
+
"renewalDue": "توجد دفعة تجديد مستحقة لهذا الاشتراك.",
|
|
346
|
+
"payNow": "ادفع الآن",
|
|
347
|
+
"updateCard": "تحديث البطاقة",
|
|
348
|
+
"pastDueCardTitle": "فشلت دفعتك الأخيرة",
|
|
349
|
+
"pastDueCardBody": "حدّث بطاقتك للحفاظ على هذا الاشتراك. تُفتح صفحة Shoppex آمنة.",
|
|
350
|
+
"historyFirstOrder": "الطلب الأول",
|
|
351
|
+
"historyRenewal": "تجديد",
|
|
352
|
+
"historyPlanChange": "تغيير الخطة، من {from} إلى {to}",
|
|
353
|
+
"historyDays": "عن {days} من {total} يومًا"
|
|
234
354
|
},
|
|
235
355
|
"favorites": {
|
|
236
356
|
"loading": "جارٍ تحميل مفضلتك",
|
|
@@ -311,6 +431,18 @@
|
|
|
311
431
|
"primary": "أساسي",
|
|
312
432
|
"commission": "عمولة {percent}%",
|
|
313
433
|
"buyerDiscount": "خصم {percent}% للمشتري",
|
|
434
|
+
"yourRates": "أسعارك",
|
|
435
|
+
"ratesEveryProduct": "{percent}% على كل منتج",
|
|
436
|
+
"ratesMostProducts": "{percent}% على معظم المنتجات",
|
|
437
|
+
"ratesProductEarnsNothing": "{product} لا يحقق عمولة",
|
|
438
|
+
"ratesProductsEarnNothing": "{products} لا تحقق عمولة",
|
|
439
|
+
"ratesCustomRateProducts": "بعض المنتجات لها نسبة خاصة بها",
|
|
440
|
+
"ratesMoreProducts": "{count} أخرى",
|
|
441
|
+
"ratesCap": "حتى {amount} لكل طلب",
|
|
442
|
+
"ratesMinimum": "الطلبات الأقل من {amount} لا تحقق عمولة",
|
|
443
|
+
"ratesBindingDays": "يبقى المشترون لك لمدة {days} يومًا",
|
|
444
|
+
"ratesBindingForever": "يبقى المشترون لك دائمًا",
|
|
445
|
+
"ratesBindingDetail": "بعد أول طلب مدفوع عبر رابطك",
|
|
314
446
|
"awaitingApproval": "بانتظار موافقة المتجر على هذا الرابط.",
|
|
315
447
|
"clicks": "النقرات",
|
|
316
448
|
"sales": "المبيعات",
|
|
@@ -432,14 +564,6 @@
|
|
|
432
564
|
"paymentIssue": "مشكلات الدفع",
|
|
433
565
|
"paymentIssueNote": "رسالة عندما تحتاج عملية دفع إلى انتباهك."
|
|
434
566
|
},
|
|
435
|
-
"walletTopup": {
|
|
436
|
-
"title": "إضافة رصيد المتجر",
|
|
437
|
-
"minimum": "الحد الأدنى {min}",
|
|
438
|
-
"bounds": "من {min} إلى {max}",
|
|
439
|
-
"bonus": "أضف {amount} على الأقل واحصل على {percent}% إضافية",
|
|
440
|
-
"amount": "المبلغ",
|
|
441
|
-
"submit": "المتابعة إلى الدفع"
|
|
442
|
-
},
|
|
443
567
|
"reseller": {
|
|
444
568
|
"disabledTitle": "برنامج الموزعين غير متاح",
|
|
445
569
|
"disabledBody": "لا يقبل هذا المتجر طلبات الجملة حاليًا.",
|
|
@@ -466,19 +590,12 @@
|
|
|
466
590
|
"walletTitle": "رصيد الجملة",
|
|
467
591
|
"balance": "الرصيد",
|
|
468
592
|
"held": "محجوز",
|
|
593
|
+
"topUp": "إضافة رصيد",
|
|
469
594
|
"catalogTitle": "كتالوج الجملة",
|
|
470
595
|
"search": "البحث عن منتجات",
|
|
471
596
|
"catalogEmptyTitle": "لم يتم العثور على منتجات جملة",
|
|
472
597
|
"catalogEmptyBody": "جرّب بحثًا آخر أو عد بعد أن يضيف المتجر منتجات.",
|
|
473
598
|
"volumePricing": "أسعار الكميات متاحة",
|
|
474
|
-
"supplierBacked": "ينفذه المورّد",
|
|
475
|
-
"add": "إضافة",
|
|
476
|
-
"basketTitle": "سلة الجملة",
|
|
477
|
-
"basketEmpty": "أضف منتجًا من كتالوج الجملة لبدء الطلب.",
|
|
478
|
-
"quantity": "الكمية",
|
|
479
|
-
"basketNeedsQuote": "احسب سعر هذه السلة لرؤية الإجمالي الحالي قبل الطلب.",
|
|
480
|
-
"quote": "حساب سعر السلة",
|
|
481
|
-
"place": "تنفيذ الطلب",
|
|
482
599
|
"ordersTitle": "طلبات الجملة",
|
|
483
600
|
"ordersEmptyTitle": "لا توجد طلبات جملة بعد",
|
|
484
601
|
"ordersEmptyBody": "تظهر هنا الطلبات المدفوعة من رصيد الجملة.",
|
|
@@ -486,17 +603,11 @@
|
|
|
486
603
|
"lineQuantity": "الكمية {count}",
|
|
487
604
|
"serials": "الأرقام التسلسلية المسلّمة",
|
|
488
605
|
"apiKeysTitle": "مفاتيح API للموزع",
|
|
489
|
-
"apiKeyName": "اسم المفتاح",
|
|
490
|
-
"createApiKey": "إنشاء مفتاح",
|
|
491
606
|
"apiKeysEmpty": "لم تنشئ أي مفاتيح API للموزع.",
|
|
492
|
-
"
|
|
493
|
-
"
|
|
494
|
-
"
|
|
495
|
-
"apiKeyCreatedBody": "يظهر هذا المفتاح النصي مرة واحدة ولا يمكن استعادته لاحقًا.",
|
|
496
|
-
"dismissKey": "حفظت المفتاح",
|
|
607
|
+
"manageKeys": "إدارة المفاتيح",
|
|
608
|
+
"delivery": "رابط التسليم",
|
|
609
|
+
"deliveryHint": "لحقل Dynamic Delivery في متجرك. تُفتح صفحة Shoppex آمنة.",
|
|
497
610
|
"lastUsed": "آخر استخدام: {date}",
|
|
498
|
-
"neverUsed": "لم يُستخدم"
|
|
499
|
-
"revokeApiKey": "إلغاء"
|
|
611
|
+
"neverUsed": "لم يُستخدم"
|
|
500
612
|
}
|
|
501
613
|
}
|
|
502
|
-
|