mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat(websocket): add global handler for subscription and balance notifications
- Extend WSMessage interface with balance, subscription, autopay, account, referral fields - Create WebSocketNotifications component to handle all WS events - Add component to Layout for global event listening - Show toasts and invalidate queries on subscription/balance events - Add translations for all notification types (ru, en, zh, fa)
This commit is contained in:
398
src/components/WebSocketNotifications.tsx
Normal file
398
src/components/WebSocketNotifications.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
/**
|
||||
* Global WebSocket notifications handler.
|
||||
* Listens to all WebSocket events and shows appropriate toasts.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useWebSocket, WSMessage } from '../hooks/useWebSocket';
|
||||
import { useToast } from './Toast';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
|
||||
export default function WebSocketNotifications() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { showToast } = useToast();
|
||||
const { refreshUser } = useAuthStore();
|
||||
const { formatAmount, currencySymbol } = useCurrency();
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(message: WSMessage) => {
|
||||
const { type } = message;
|
||||
|
||||
// Skip ticket events - they are handled by TicketNotificationBell
|
||||
if (type.startsWith('ticket.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Balance events
|
||||
if (type === 'balance.topup') {
|
||||
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: t('wsNotifications.balance.topupTitle', 'Balance topped up'),
|
||||
message: t(
|
||||
'wsNotifications.balance.topupMessage',
|
||||
'Your balance has been topped up by {{amount}} {{currency}}',
|
||||
{
|
||||
amount: formatAmount(amount),
|
||||
currency: currencySymbol,
|
||||
},
|
||||
),
|
||||
icon: <span className="text-lg">💰</span>,
|
||||
onClick: () => navigate('/balance'),
|
||||
duration: 6000,
|
||||
});
|
||||
// Refresh data
|
||||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
||||
refreshUser();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'balance.change') {
|
||||
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;
|
||||
const isPositive = amount >= 0;
|
||||
showToast({
|
||||
type: isPositive ? 'success' : 'info',
|
||||
title: t('wsNotifications.balance.changeTitle', 'Balance updated'),
|
||||
message:
|
||||
message.description ||
|
||||
t(
|
||||
'wsNotifications.balance.changeMessage',
|
||||
'Your balance has changed by {{amount}} {{currency}}',
|
||||
{
|
||||
amount: (isPositive ? '+' : '') + formatAmount(amount),
|
||||
currency: currencySymbol,
|
||||
},
|
||||
),
|
||||
icon: <span className="text-lg">{isPositive ? '💵' : '💸'}</span>,
|
||||
onClick: () => navigate('/balance'),
|
||||
duration: 5000,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
||||
refreshUser();
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscription events
|
||||
if (type === 'subscription.activated') {
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: t('wsNotifications.subscription.activatedTitle', 'Subscription activated'),
|
||||
message: message.tariff_name
|
||||
? t(
|
||||
'wsNotifications.subscription.activatedWithTariff',
|
||||
'Your subscription "{{tariff}}" is now active!',
|
||||
{ tariff: message.tariff_name },
|
||||
)
|
||||
: t(
|
||||
'wsNotifications.subscription.activatedMessage',
|
||||
'Your subscription is now active!',
|
||||
),
|
||||
icon: <span className="text-lg">🎉</span>,
|
||||
onClick: () => navigate('/subscription'),
|
||||
duration: 8000,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['subscription'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||||
refreshUser();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'subscription.renewed') {
|
||||
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: t('wsNotifications.subscription.renewedTitle', 'Subscription renewed'),
|
||||
message:
|
||||
amount > 0
|
||||
? t(
|
||||
'wsNotifications.subscription.renewedWithAmount',
|
||||
'Your subscription has been renewed for {{amount}} {{currency}}',
|
||||
{
|
||||
amount: formatAmount(amount),
|
||||
currency: currencySymbol,
|
||||
},
|
||||
)
|
||||
: t(
|
||||
'wsNotifications.subscription.renewedMessage',
|
||||
'Your subscription has been renewed!',
|
||||
),
|
||||
icon: <span className="text-lg">✨</span>,
|
||||
onClick: () => navigate('/subscription'),
|
||||
duration: 8000,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['subscription'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||||
refreshUser();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'subscription.expiring') {
|
||||
showToast({
|
||||
type: 'warning',
|
||||
title: t('wsNotifications.subscription.expiringTitle', 'Subscription expiring soon'),
|
||||
message: t(
|
||||
'wsNotifications.subscription.expiringMessage',
|
||||
'Your subscription expires in {{days}} days',
|
||||
{
|
||||
days: message.days_left ?? 0,
|
||||
},
|
||||
),
|
||||
icon: <span className="text-lg">⏰</span>,
|
||||
onClick: () => navigate('/subscription'),
|
||||
duration: 10000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'subscription.expired') {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: t('wsNotifications.subscription.expiredTitle', 'Subscription expired'),
|
||||
message: t(
|
||||
'wsNotifications.subscription.expiredMessage',
|
||||
'Your subscription has expired. Renew to continue using the service.',
|
||||
),
|
||||
icon: <span className="text-lg">😢</span>,
|
||||
onClick: () => navigate('/subscription'),
|
||||
duration: 10000,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['subscription'] });
|
||||
refreshUser();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'subscription.daily_debit') {
|
||||
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;
|
||||
showToast({
|
||||
type: 'info',
|
||||
title: t('wsNotifications.subscription.dailyDebitTitle', 'Daily charge'),
|
||||
message: t(
|
||||
'wsNotifications.subscription.dailyDebitMessage',
|
||||
'Daily subscription fee: {{amount}} {{currency}}',
|
||||
{
|
||||
amount: formatAmount(amount),
|
||||
currency: currencySymbol,
|
||||
},
|
||||
),
|
||||
icon: <span className="text-lg">📅</span>,
|
||||
duration: 5000,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||||
refreshUser();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'subscription.traffic_reset') {
|
||||
showToast({
|
||||
type: 'info',
|
||||
title: t('wsNotifications.subscription.trafficResetTitle', 'Traffic reset'),
|
||||
message: t(
|
||||
'wsNotifications.subscription.trafficResetMessage',
|
||||
'Your traffic limit has been reset',
|
||||
),
|
||||
icon: <span className="text-lg">🔄</span>,
|
||||
duration: 5000,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['subscription'] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Autopay events
|
||||
if (type === 'autopay.success') {
|
||||
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: t('wsNotifications.autopay.successTitle', 'Auto-renewal successful'),
|
||||
message: t(
|
||||
'wsNotifications.autopay.successMessage',
|
||||
'Your subscription was auto-renewed for {{amount}} {{currency}}',
|
||||
{
|
||||
amount: formatAmount(amount),
|
||||
currency: currencySymbol,
|
||||
},
|
||||
),
|
||||
icon: <span className="text-lg">🔁</span>,
|
||||
onClick: () => navigate('/subscription'),
|
||||
duration: 8000,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['subscription'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||||
refreshUser();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'autopay.failed') {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: t('wsNotifications.autopay.failedTitle', 'Auto-renewal failed'),
|
||||
message:
|
||||
message.reason ||
|
||||
t('wsNotifications.autopay.failedMessage', 'Failed to auto-renew your subscription'),
|
||||
icon: <span className="text-lg">❌</span>,
|
||||
onClick: () => navigate('/subscription'),
|
||||
duration: 10000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'autopay.insufficient_funds') {
|
||||
const required = message.required_rubles ?? (message.required_kopeks ?? 0) / 100;
|
||||
const balance = message.balance_rubles ?? (message.balance_kopeks ?? 0) / 100;
|
||||
showToast({
|
||||
type: 'warning',
|
||||
title: t('wsNotifications.autopay.insufficientTitle', 'Insufficient funds'),
|
||||
message: t(
|
||||
'wsNotifications.autopay.insufficientMessage',
|
||||
'Need {{required}} {{currency}} for renewal, but balance is {{balance}} {{currency}}',
|
||||
{
|
||||
required: formatAmount(required),
|
||||
balance: formatAmount(balance),
|
||||
currency: currencySymbol,
|
||||
},
|
||||
),
|
||||
icon: <span className="text-lg">💳</span>,
|
||||
onClick: () => navigate('/balance'),
|
||||
duration: 10000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Account events
|
||||
if (type === 'account.banned') {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: t('wsNotifications.account.bannedTitle', 'Account blocked'),
|
||||
message:
|
||||
message.reason ||
|
||||
t('wsNotifications.account.bannedMessage', 'Your account has been blocked'),
|
||||
icon: <span className="text-lg">🚫</span>,
|
||||
duration: 15000,
|
||||
});
|
||||
refreshUser();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'account.unbanned') {
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: t('wsNotifications.account.unbannedTitle', 'Account unblocked'),
|
||||
message: t('wsNotifications.account.unbannedMessage', 'Your account has been unblocked'),
|
||||
icon: <span className="text-lg">✅</span>,
|
||||
duration: 8000,
|
||||
});
|
||||
refreshUser();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'account.warning') {
|
||||
showToast({
|
||||
type: 'warning',
|
||||
title: t('wsNotifications.account.warningTitle', 'Warning'),
|
||||
message:
|
||||
message.message ||
|
||||
t('wsNotifications.account.warningMessage', 'You have received a warning'),
|
||||
icon: <span className="text-lg">⚠️</span>,
|
||||
duration: 10000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Referral events
|
||||
if (type === 'referral.bonus') {
|
||||
const bonus = message.bonus_rubles ?? (message.bonus_kopeks ?? 0) / 100;
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: t('wsNotifications.referral.bonusTitle', 'Referral bonus'),
|
||||
message: message.referral_name
|
||||
? t(
|
||||
'wsNotifications.referral.bonusWithName',
|
||||
'You received {{amount}} {{currency}} bonus from {{name}}!',
|
||||
{
|
||||
amount: formatAmount(bonus),
|
||||
currency: currencySymbol,
|
||||
name: message.referral_name,
|
||||
},
|
||||
)
|
||||
: t(
|
||||
'wsNotifications.referral.bonusMessage',
|
||||
'You received {{amount}} {{currency}} referral bonus!',
|
||||
{
|
||||
amount: formatAmount(bonus),
|
||||
currency: currencySymbol,
|
||||
},
|
||||
),
|
||||
icon: <span className="text-lg">🎁</span>,
|
||||
onClick: () => navigate('/referral'),
|
||||
duration: 8000,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['referral-stats'] });
|
||||
refreshUser();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'referral.registered') {
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: t('wsNotifications.referral.registeredTitle', 'New referral'),
|
||||
message: message.referral_name
|
||||
? t(
|
||||
'wsNotifications.referral.registeredWithName',
|
||||
'{{name}} joined using your referral link!',
|
||||
{ name: message.referral_name },
|
||||
)
|
||||
: t(
|
||||
'wsNotifications.referral.registeredMessage',
|
||||
'Someone joined using your referral link!',
|
||||
),
|
||||
icon: <span className="text-lg">👤</span>,
|
||||
onClick: () => navigate('/referral'),
|
||||
duration: 6000,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['referral-stats'] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Payment received
|
||||
if (type === 'payment.received') {
|
||||
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: t('wsNotifications.payment.receivedTitle', 'Payment received'),
|
||||
message: t(
|
||||
'wsNotifications.payment.receivedMessage',
|
||||
'Payment of {{amount}} {{currency}} received',
|
||||
{
|
||||
amount: formatAmount(amount),
|
||||
currency: currencySymbol,
|
||||
},
|
||||
),
|
||||
icon: <span className="text-lg">💳</span>,
|
||||
onClick: () => navigate('/balance'),
|
||||
duration: 6000,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
||||
refreshUser();
|
||||
return;
|
||||
}
|
||||
},
|
||||
[t, showToast, navigate, queryClient, refreshUser, formatAmount, currencySymbol],
|
||||
);
|
||||
|
||||
// Connect to WebSocket and handle messages
|
||||
useWebSocket({
|
||||
onMessage: handleMessage,
|
||||
});
|
||||
|
||||
// This component doesn't render anything
|
||||
return null;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useAuthStore } from '../../store/auth';
|
||||
import LanguageSwitcher from '../LanguageSwitcher';
|
||||
import PromoDiscountBadge from '../PromoDiscountBadge';
|
||||
import TicketNotificationBell from '../TicketNotificationBell';
|
||||
import WebSocketNotifications from '../WebSocketNotifications';
|
||||
import AnimatedBackground from '../AnimatedBackground';
|
||||
import { contestsApi } from '../../api/contests';
|
||||
import { pollsApi } from '../../api/polls';
|
||||
@@ -452,6 +453,9 @@ export default function Layout({ children }: LayoutProps) {
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
{/* Global WebSocket notifications handler */}
|
||||
<WebSocketNotifications />
|
||||
|
||||
{/* Animated Background */}
|
||||
<AnimatedBackground />
|
||||
|
||||
|
||||
@@ -5,11 +5,37 @@ const isDev = import.meta.env.DEV;
|
||||
|
||||
export interface WSMessage {
|
||||
type: string;
|
||||
// Ticket events
|
||||
ticket_id?: number;
|
||||
message?: string;
|
||||
title?: string;
|
||||
// Common
|
||||
message?: string;
|
||||
user_id?: number;
|
||||
is_admin?: boolean;
|
||||
// Balance events
|
||||
amount_kopeks?: number;
|
||||
amount_rubles?: number;
|
||||
new_balance_kopeks?: number;
|
||||
new_balance_rubles?: number;
|
||||
description?: string;
|
||||
// Subscription events
|
||||
expires_at?: string;
|
||||
new_expires_at?: string;
|
||||
tariff_name?: string;
|
||||
days_left?: number;
|
||||
// Autopay events
|
||||
required_kopeks?: number;
|
||||
required_rubles?: number;
|
||||
balance_kopeks?: number;
|
||||
balance_rubles?: number;
|
||||
reason?: string;
|
||||
// Account events (ban/warning)
|
||||
// Referral events
|
||||
bonus_kopeks?: number;
|
||||
bonus_rubles?: number;
|
||||
referral_name?: string;
|
||||
// Payment events
|
||||
payment_method?: string;
|
||||
}
|
||||
|
||||
interface UseWebSocketOptions {
|
||||
|
||||
@@ -68,6 +68,58 @@
|
||||
"newUserReply": "User replied in ticket: {{title}}",
|
||||
"newUserReplyTitle": "User Reply"
|
||||
},
|
||||
"wsNotifications": {
|
||||
"balance": {
|
||||
"topupTitle": "Balance topped up",
|
||||
"topupMessage": "Your balance has been topped up by {{amount}} {{currency}}",
|
||||
"changeTitle": "Balance updated",
|
||||
"changeMessage": "Your balance has changed by {{amount}} {{currency}}"
|
||||
},
|
||||
"subscription": {
|
||||
"activatedTitle": "Subscription activated",
|
||||
"activatedMessage": "Your subscription is now active!",
|
||||
"activatedWithTariff": "Your subscription \"{{tariff}}\" is now active!",
|
||||
"renewedTitle": "Subscription renewed",
|
||||
"renewedMessage": "Your subscription has been renewed!",
|
||||
"renewedWithAmount": "Your subscription has been renewed for {{amount}} {{currency}}",
|
||||
"expiringTitle": "Subscription expiring soon",
|
||||
"expiringMessage": "Your subscription expires in {{days}} days",
|
||||
"expiredTitle": "Subscription expired",
|
||||
"expiredMessage": "Your subscription has expired. Renew to continue using the service.",
|
||||
"dailyDebitTitle": "Daily charge",
|
||||
"dailyDebitMessage": "Daily subscription fee: {{amount}} {{currency}}",
|
||||
"trafficResetTitle": "Traffic reset",
|
||||
"trafficResetMessage": "Your traffic limit has been reset"
|
||||
},
|
||||
"autopay": {
|
||||
"successTitle": "Auto-renewal successful",
|
||||
"successMessage": "Your subscription was auto-renewed for {{amount}} {{currency}}",
|
||||
"failedTitle": "Auto-renewal failed",
|
||||
"failedMessage": "Failed to auto-renew your subscription",
|
||||
"insufficientTitle": "Insufficient funds",
|
||||
"insufficientMessage": "Need {{required}} {{currency}} for renewal, but balance is {{balance}} {{currency}}"
|
||||
},
|
||||
"account": {
|
||||
"bannedTitle": "Account blocked",
|
||||
"bannedMessage": "Your account has been blocked",
|
||||
"unbannedTitle": "Account unblocked",
|
||||
"unbannedMessage": "Your account has been unblocked",
|
||||
"warningTitle": "Warning",
|
||||
"warningMessage": "You have received a warning"
|
||||
},
|
||||
"referral": {
|
||||
"bonusTitle": "Referral bonus",
|
||||
"bonusMessage": "You received {{amount}} {{currency}} referral bonus!",
|
||||
"bonusWithName": "You received {{amount}} {{currency}} bonus from {{name}}!",
|
||||
"registeredTitle": "New referral",
|
||||
"registeredMessage": "Someone joined using your referral link!",
|
||||
"registeredWithName": "{{name}} joined using your referral link!"
|
||||
},
|
||||
"payment": {
|
||||
"receivedTitle": "Payment received",
|
||||
"receivedMessage": "Payment of {{amount}} {{currency}} received"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
|
||||
@@ -62,6 +62,58 @@
|
||||
"newUserReply": "کاربر در تیکت پاسخ داد: {{title}}",
|
||||
"newUserReplyTitle": "پاسخ کاربر"
|
||||
},
|
||||
"wsNotifications": {
|
||||
"balance": {
|
||||
"topupTitle": "موجودی شارژ شد",
|
||||
"topupMessage": "موجودی شما {{amount}} {{currency}} شارژ شد",
|
||||
"changeTitle": "موجودی بهروز شد",
|
||||
"changeMessage": "موجودی شما {{amount}} {{currency}} تغییر کرد"
|
||||
},
|
||||
"subscription": {
|
||||
"activatedTitle": "اشتراک فعال شد",
|
||||
"activatedMessage": "اشتراک شما اکنون فعال است!",
|
||||
"activatedWithTariff": "اشتراک «{{tariff}}» شما اکنون فعال است!",
|
||||
"renewedTitle": "اشتراک تمدید شد",
|
||||
"renewedMessage": "اشتراک شما تمدید شد!",
|
||||
"renewedWithAmount": "اشتراک شما با {{amount}} {{currency}} تمدید شد",
|
||||
"expiringTitle": "اشتراک به زودی منقضی میشود",
|
||||
"expiringMessage": "اشتراک شما {{days}} روز دیگر منقضی میشود",
|
||||
"expiredTitle": "اشتراک منقضی شد",
|
||||
"expiredMessage": "اشتراک شما منقضی شده است. برای ادامه استفاده تمدید کنید.",
|
||||
"dailyDebitTitle": "کسر روزانه",
|
||||
"dailyDebitMessage": "هزینه روزانه اشتراک: {{amount}} {{currency}}",
|
||||
"trafficResetTitle": "ترافیک بازنشانی شد",
|
||||
"trafficResetMessage": "محدودیت ترافیک شما بازنشانی شد"
|
||||
},
|
||||
"autopay": {
|
||||
"successTitle": "تمدید خودکار موفق",
|
||||
"successMessage": "اشتراک شما به صورت خودکار با {{amount}} {{currency}} تمدید شد",
|
||||
"failedTitle": "خطا در تمدید خودکار",
|
||||
"failedMessage": "تمدید خودکار اشتراک شما ناموفق بود",
|
||||
"insufficientTitle": "موجودی ناکافی",
|
||||
"insufficientMessage": "برای تمدید {{required}} {{currency}} لازم است، موجودی فعلی {{balance}} {{currency}}"
|
||||
},
|
||||
"account": {
|
||||
"bannedTitle": "حساب مسدود شد",
|
||||
"bannedMessage": "حساب شما مسدود شده است",
|
||||
"unbannedTitle": "حساب رفع مسدودی شد",
|
||||
"unbannedMessage": "حساب شما رفع مسدودی شد",
|
||||
"warningTitle": "هشدار",
|
||||
"warningMessage": "شما یک هشدار دریافت کردید"
|
||||
},
|
||||
"referral": {
|
||||
"bonusTitle": "پاداش معرفی",
|
||||
"bonusMessage": "شما {{amount}} {{currency}} پاداش معرفی دریافت کردید!",
|
||||
"bonusWithName": "شما از {{name}} پاداش {{amount}} {{currency}} دریافت کردید!",
|
||||
"registeredTitle": "معرفی جدید",
|
||||
"registeredMessage": "کسی با لینک معرفی شما ثبت نام کرد!",
|
||||
"registeredWithName": "{{name}} با لینک معرفی شما ثبت نام کرد!"
|
||||
},
|
||||
"payment": {
|
||||
"receivedTitle": "پرداخت دریافت شد",
|
||||
"receivedMessage": "پرداخت {{amount}} {{currency}} دریافت شد"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"login": "ورود",
|
||||
"register": "ثبت نام",
|
||||
|
||||
@@ -71,6 +71,58 @@
|
||||
"newUserReply": "Ответ пользователя в тикете: {{title}}",
|
||||
"newUserReplyTitle": "Ответ пользователя"
|
||||
},
|
||||
"wsNotifications": {
|
||||
"balance": {
|
||||
"topupTitle": "Баланс пополнен",
|
||||
"topupMessage": "Ваш баланс пополнен на {{amount}} {{currency}}",
|
||||
"changeTitle": "Баланс изменён",
|
||||
"changeMessage": "Ваш баланс изменился на {{amount}} {{currency}}"
|
||||
},
|
||||
"subscription": {
|
||||
"activatedTitle": "Подписка активирована",
|
||||
"activatedMessage": "Ваша подписка теперь активна!",
|
||||
"activatedWithTariff": "Ваша подписка «{{tariff}}» теперь активна!",
|
||||
"renewedTitle": "Подписка продлена",
|
||||
"renewedMessage": "Ваша подписка была продлена!",
|
||||
"renewedWithAmount": "Ваша подписка продлена за {{amount}} {{currency}}",
|
||||
"expiringTitle": "Подписка скоро истекает",
|
||||
"expiringMessage": "Ваша подписка истекает через {{days}} дн.",
|
||||
"expiredTitle": "Подписка истекла",
|
||||
"expiredMessage": "Ваша подписка истекла. Продлите для продолжения использования.",
|
||||
"dailyDebitTitle": "Ежедневное списание",
|
||||
"dailyDebitMessage": "Списание за подписку: {{amount}} {{currency}}",
|
||||
"trafficResetTitle": "Трафик сброшен",
|
||||
"trafficResetMessage": "Ваш лимит трафика был сброшен"
|
||||
},
|
||||
"autopay": {
|
||||
"successTitle": "Автопродление успешно",
|
||||
"successMessage": "Подписка автоматически продлена за {{amount}} {{currency}}",
|
||||
"failedTitle": "Ошибка автопродления",
|
||||
"failedMessage": "Не удалось автоматически продлить подписку",
|
||||
"insufficientTitle": "Недостаточно средств",
|
||||
"insufficientMessage": "Для продления нужно {{required}} {{currency}}, на балансе {{balance}} {{currency}}"
|
||||
},
|
||||
"account": {
|
||||
"bannedTitle": "Аккаунт заблокирован",
|
||||
"bannedMessage": "Ваш аккаунт был заблокирован",
|
||||
"unbannedTitle": "Аккаунт разблокирован",
|
||||
"unbannedMessage": "Ваш аккаунт был разблокирован",
|
||||
"warningTitle": "Предупреждение",
|
||||
"warningMessage": "Вы получили предупреждение"
|
||||
},
|
||||
"referral": {
|
||||
"bonusTitle": "Реферальный бонус",
|
||||
"bonusMessage": "Вы получили реферальный бонус {{amount}} {{currency}}!",
|
||||
"bonusWithName": "Вы получили бонус {{amount}} {{currency}} от {{name}}!",
|
||||
"registeredTitle": "Новый реферал",
|
||||
"registeredMessage": "Кто-то зарегистрировался по вашей ссылке!",
|
||||
"registeredWithName": "{{name}} зарегистрировался по вашей ссылке!"
|
||||
},
|
||||
"payment": {
|
||||
"receivedTitle": "Платёж получен",
|
||||
"receivedMessage": "Получен платёж на сумму {{amount}} {{currency}}"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"login": "Вход",
|
||||
"register": "Регистрация",
|
||||
|
||||
@@ -62,6 +62,58 @@
|
||||
"newUserReply": "用户回复了工单:{{title}}",
|
||||
"newUserReplyTitle": "用户回复"
|
||||
},
|
||||
"wsNotifications": {
|
||||
"balance": {
|
||||
"topupTitle": "余额已充值",
|
||||
"topupMessage": "您的余额已充值 {{amount}} {{currency}}",
|
||||
"changeTitle": "余额已更新",
|
||||
"changeMessage": "您的余额已变动 {{amount}} {{currency}}"
|
||||
},
|
||||
"subscription": {
|
||||
"activatedTitle": "订阅已激活",
|
||||
"activatedMessage": "您的订阅现已激活!",
|
||||
"activatedWithTariff": "您的订阅「{{tariff}}」现已激活!",
|
||||
"renewedTitle": "订阅已续期",
|
||||
"renewedMessage": "您的订阅已续期!",
|
||||
"renewedWithAmount": "您的订阅已续期,费用 {{amount}} {{currency}}",
|
||||
"expiringTitle": "订阅即将到期",
|
||||
"expiringMessage": "您的订阅将在 {{days}} 天后到期",
|
||||
"expiredTitle": "订阅已过期",
|
||||
"expiredMessage": "您的订阅已过期。请续期以继续使用服务。",
|
||||
"dailyDebitTitle": "每日扣费",
|
||||
"dailyDebitMessage": "每日订阅费:{{amount}} {{currency}}",
|
||||
"trafficResetTitle": "流量已重置",
|
||||
"trafficResetMessage": "您的流量限制已重置"
|
||||
},
|
||||
"autopay": {
|
||||
"successTitle": "自动续期成功",
|
||||
"successMessage": "您的订阅已自动续期,费用 {{amount}} {{currency}}",
|
||||
"failedTitle": "自动续期失败",
|
||||
"failedMessage": "自动续期订阅失败",
|
||||
"insufficientTitle": "余额不足",
|
||||
"insufficientMessage": "续期需要 {{required}} {{currency}},当前余额 {{balance}} {{currency}}"
|
||||
},
|
||||
"account": {
|
||||
"bannedTitle": "账户已封禁",
|
||||
"bannedMessage": "您的账户已被封禁",
|
||||
"unbannedTitle": "账户已解封",
|
||||
"unbannedMessage": "您的账户已解封",
|
||||
"warningTitle": "警告",
|
||||
"warningMessage": "您收到了一条警告"
|
||||
},
|
||||
"referral": {
|
||||
"bonusTitle": "推荐奖励",
|
||||
"bonusMessage": "您获得了 {{amount}} {{currency}} 推荐奖励!",
|
||||
"bonusWithName": "您从 {{name}} 获得了 {{amount}} {{currency}} 奖励!",
|
||||
"registeredTitle": "新推荐用户",
|
||||
"registeredMessage": "有人通过您的推荐链接注册了!",
|
||||
"registeredWithName": "{{name}} 通过您的推荐链接注册了!"
|
||||
},
|
||||
"payment": {
|
||||
"receivedTitle": "已收到付款",
|
||||
"receivedMessage": "收到付款 {{amount}} {{currency}}"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
|
||||
Reference in New Issue
Block a user