Полный перевод на русский язык

This commit is contained in:
2026-01-11 08:24:55 +03:00
parent 5cc76f5ed1
commit 511e2da647
2 changed files with 200 additions and 70 deletions

View File

@@ -414,7 +414,7 @@
</template> </template>
<!-- JS --> <!-- JS -->
<script src="js/app.js?v=4"></script> <script src="js/app.js?v=5"></script>
</body> </body>
</html> </html>

View File

@@ -81,7 +81,72 @@ const translations = {
status_inactive: "Inactive", status_inactive: "Inactive",
status_expired: "Expired", status_expired: "Expired",
status_no_sub: "No active subscription", status_no_sub: "No active subscription",
unit_gb: "GB" unit_gb: "GB",
// New Keys
err_plans_load: "Failed to load plans.",
err_open_tg: "Open in Telegram",
btn_creating: "Creating..",
plan_unlimited: "Unlimited",
unit_days: "Days",
msg_paid: "Successful Payment!",
err_network: "Network error",
msg_link_copied: "Link copied!",
err_no_sub: "No subscription found.",
modal_dl_title: "Download App",
modal_dl_subtitle: "Choose a client for your device:",
btn_dl_for: "Download for",
msg_promo_applied: "Promo Applied! {0}% OFF.",
err_promo_invalid: "Invalid Promo Code",
err_promo_check: "Error checking promo",
about_slogan: "Premium V2Ray service.<br>Fast, Secure, Reliable.",
msg_support_sent: "Message sent! Admin will contact you.",
err_send_fail: "Failed to send.",
adm_no_users: "No users found",
confirm_toggle: "Change user active/disabled state?",
confirm_reset: "Reset used traffic to zero?",
confirm_delete_sub: "Are you sure? User will lose VPN access immediately.",
title_add_days: "Add Days",
subtitle_add_days: "Enter days to ADD (negative to subtract):",
btn_apply: "Apply",
title_set_limit: "Set Traffic Limit",
subtitle_set_limit: "Enter new limit in GB (0 for ∞):",
title_set_exp: "Set Expiration",
subtitle_set_exp: "Days from NOW (0 = expire, 36500 = ∞):",
confirm_broadcast: "Send this message to ALL users?",
msg_broadcast_sent: "Broadcast sent to {0} users!",
title_new_promo: "New Promo",
ph_code: "CODE (e.g. SUMMER2024)",
ph_discount: "Discount %",
ph_uses: "Uses Limit",
ph_days: "Validity Days",
ph_bonus: "Bonus Days",
lbl_unlim_usage: "Unlimited Usage",
lbl_sticky: "Permanent (Sticky)",
note_sticky: "* Sticky promo locks the discount for the user forever.",
btn_create_promo: "Create Promo",
msg_promo_created: "Promo Created!",
err_promo_create: "Error creating promo",
msg_promo_deleted: "Promo deleted!",
confirm_delete_promo: "Are you sure you want to delete promo code <b>{0}</b>?",
app_desc_sleek: "Sleek & Fast",
app_desc_premium: "Premium Choice",
app_desc_standard: "Standard Client",
app_desc_modern: "Powerful & Modern",
app_desc_aio: "Universal All-in-One",
app_desc_opensource: "Powerful & Open Source",
app_desc_simple: "Simple & Modern",
app_desc_classic: "Classic Client",
app_desc_native: "Native M1/M2 Support",
app_desc_elegant: "Elegant Desktop Client"
}, },
ru: { ru: {
nav_home: "Главная", nav_home: "Главная",
@@ -153,7 +218,72 @@ const translations = {
status_inactive: "Неактивна", status_inactive: "Неактивна",
status_expired: "Истекла", status_expired: "Истекла",
status_no_sub: "Нет активной подписки", status_no_sub: "Нет активной подписки",
unit_gb: "ГБ" unit_gb: "ГБ",
// New Keys RU
err_plans_load: "Не удалось загрузить планы.",
err_open_tg: "Откройте в Telegram",
btn_creating: "Создание..",
plan_unlimited: "Безлимит",
unit_days: "Дней",
msg_paid: "Успешная оплата!",
err_network: "Ошибка сети",
msg_link_copied: "Ссылка скопирована!",
err_no_sub: "Подписка не найдена.",
modal_dl_title: "Скачать приложение",
modal_dl_subtitle: "Выберите клиент для устройства:",
btn_dl_for: "Скачать для",
msg_promo_applied: "Промо применено! Скидка {0}%.",
err_promo_invalid: "Неверный промокод",
err_promo_check: "Ошибка проверки промо",
about_slogan: "Премиум V2Ray сервис.<br>Быстро, Надежно, Безопасно.",
msg_support_sent: "Отправлено! Админ свяжется с вами.",
err_send_fail: "Ошибка отправки.",
adm_no_users: "Пользователи не найдены",
confirm_toggle: "Изменить статус (вкл/выкл)?",
confirm_reset: "Сбросить трафик в ноль?",
confirm_delete_sub: "Удалить подписку? Пользователь потеряет доступ.",
title_add_days: "Добавить дни",
subtitle_add_days: "Дней для добавления (отрицат. = убавить):",
btn_apply: "Применить",
title_set_limit: "Установить лимит",
subtitle_set_limit: "Лимит в ГБ (0 = безлимит):",
title_set_exp: "Срок действия",
subtitle_set_exp: "Дней от СЕЙЧАС (0 = истекло, 36500 = ∞):",
confirm_broadcast: "Отправить сообщение ВСЕМ пользователям?",
msg_broadcast_sent: "Рассылка отправлена {0} пользователям!",
title_new_promo: "Новый Промокод",
ph_code: "КОД (напр. SUMMER2024)",
ph_discount: "Скидка %",
ph_uses: "Лимит испол.",
ph_days: "Дней действия",
ph_bonus: "Бонусные дни",
lbl_unlim_usage: "Безлимит использований",
lbl_sticky: "Перманентный (Sticky)",
note_sticky: "* Sticky закрепляет скидку за пользователем навсегда.",
btn_create_promo: "Создать",
msg_promo_created: "Промокод создан!",
err_promo_create: "Ошибка создания",
msg_promo_deleted: "Промокод удален!",
confirm_delete_promo: "Удалить промокод <b>{0}</b>?",
app_desc_sleek: "Быстрый и удобный",
app_desc_premium: "Премиум выбор",
app_desc_standard: "Стандартный клиент",
app_desc_modern: "Мощный и современный",
app_desc_aio: "Универсальный комбайн",
app_desc_opensource: "Мощный Open Source",
app_desc_simple: "Простой и современный",
app_desc_classic: "Классика",
app_desc_native: "Нативная поддержка M1/M2",
app_desc_elegant: "Элегантный клиент"
} }
}; };
@@ -523,25 +653,25 @@ async function loadShop() {
<span class="plan-price">${plan.price} ⭐️</span> <span class="plan-price">${plan.price} ⭐️</span>
</div> </div>
<div class="plan-specs"> <div class="plan-specs">
<span><i data-lucide="database"></i> ${plan.data_limit > 0 ? plan.data_limit + ' GB' : 'Unlimited'}</span> <span><i data-lucide="database"></i> ${plan.data_limit > 0 ? plan.data_limit + ' ' + t('unit_gb') : t('plan_unlimited')}</span>
<span><i data-lucide="clock"></i> ${plan.days} Days</span> <span><i data-lucide="clock"></i> ${plan.days} ${t('unit_days')}</span>
</div> </div>
<button class="btn-primary" onclick="initPayment('${plan.id}')">Purchase</button> <button class="btn-primary" onclick="initPayment('${plan.id}')">${t('btn_purchase')}</button>
`; `;
container.appendChild(card); container.appendChild(card);
}); });
try { if (window.lucide) lucide.createIcons(); } catch (e) { } try { if (window.lucide) lucide.createIcons(); } catch (e) { }
} catch (e) { } catch (e) {
container.textContent = "Failed to load plans."; container.textContent = t('err_plans_load');
} }
} }
async function initPayment(planId) { async function initPayment(planId) {
if (!tg) return alert("Open in Telegram"); if (!tg) return alert(t('err_open_tg'));
const btn = event.target; const btn = event.target;
const oldText = btn.innerText; const oldText = btn.innerText;
btn.innerText = 'Creating..'; btn.innerText = t('btn_creating');
btn.disabled = true; btn.disabled = true;
try { try {
@@ -563,7 +693,7 @@ async function initPayment(planId) {
if (data.invoice_link) { if (data.invoice_link) {
tg.openInvoice(data.invoice_link, (status) => { tg.openInvoice(data.invoice_link, (status) => {
if (status === 'paid') { if (status === 'paid') {
showToast('Successful Payment!', 'success'); showToast(t('msg_paid'), 'success');
router('dashboard'); router('dashboard');
} }
}); });
@@ -571,7 +701,7 @@ async function initPayment(planId) {
showToast('Error: ' + (data.error || 'Unknown'), 'error'); showToast('Error: ' + (data.error || 'Unknown'), 'error');
} }
} catch (e) { } catch (e) {
showToast('Network error', 'error'); showToast(t('err_network'), 'error');
} finally { } finally {
btn.innerText = oldText; btn.innerText = oldText;
btn.disabled = false; btn.disabled = false;
@@ -603,7 +733,7 @@ async function loadSubscription() {
}); });
} }
} else { } else {
if (linkEl) linkEl.textContent = "No active subscription"; if (linkEl) linkEl.textContent = t('status_no_sub');
} }
// Dynamic Apps based on Device // Dynamic Apps based on Device
@@ -616,10 +746,10 @@ async function loadSubscription() {
appsContainer.innerHTML = ` appsContainer.innerHTML = `
<div class="sub-actions"> <div class="sub-actions">
<button class="btn-primary" onclick="openDownloadModal()"> <button class="btn-primary" onclick="openDownloadModal()">
<i data-lucide="download"></i> Download for ${myOS} <i data-lucide="download"></i> ${t('btn_dl_for')} ${myOS}
</button> </button>
<button class="btn-secondary" onclick="copyConfig()"> <button class="btn-secondary" onclick="copyConfig()">
<i data-lucide="copy"></i> Copy Link <i data-lucide="copy"></i> ${t('btn_copy')}
</button> </button>
</div> </div>
`; `;
@@ -633,25 +763,25 @@ function openDownloadModal() {
if (device === 'ios') { if (device === 'ios') {
apps = [ apps = [
{ name: 'V2Box', desc: 'Sleek & Fast', link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' }, { name: 'V2Box', desc: t('app_desc_sleek'), link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' },
{ name: 'Streisand', desc: 'Premium Choice', link: 'https://apps.apple.com/us/app/streisand/id6450534064' } { name: 'Streisand', desc: t('app_desc_premium'), link: 'https://apps.apple.com/us/app/streisand/id6450534064' }
]; ];
} else if (device === 'android') { } else if (device === 'android') {
apps = [ apps = [
{ name: 'v2rayNG', desc: 'Standard Client', link: 'https://play.google.com/store/apps/details?id=com.v2ray.ang' }, { name: 'v2rayNG', desc: t('app_desc_standard'), link: 'https://play.google.com/store/apps/details?id=com.v2ray.ang' },
{ name: 'NekoBox', desc: 'Powerful & Modern', link: 'https://github.com/MatsuriDayo/NekoBoxForAndroid/releases' }, { name: 'NekoBox', desc: t('app_desc_modern'), link: 'https://github.com/MatsuriDayo/NekoBoxForAndroid/releases' },
{ name: 'Hiddify', desc: 'Universal All-in-One', link: 'https://play.google.com/store/apps/details?id=app.hiddify.com' } { name: 'Hiddify', desc: t('app_desc_aio'), link: 'https://play.google.com/store/apps/details?id=app.hiddify.com' }
]; ];
} else if (device === 'windows') { } else if (device === 'windows') {
apps = [ apps = [
{ name: 'NekoRay', desc: 'Powerful & Open Source', link: 'https://github.com/MatsuriDayo/nekoray/releases' }, { name: 'NekoRay', desc: t('app_desc_opensource'), link: 'https://github.com/MatsuriDayo/nekoray/releases' },
{ name: 'Hiddify Next', desc: 'Simple & Modern', link: 'https://github.com/hiddify/hiddify-next/releases' }, { name: 'Hiddify Next', desc: t('app_desc_simple'), link: 'https://github.com/hiddify/hiddify-next/releases' },
{ name: 'v2rayN', desc: 'Classic Client', link: 'https://github.com/2many986/v2rayN/releases' } { name: 'v2rayN', desc: t('app_desc_classic'), link: 'https://github.com/2many986/v2rayN/releases' }
]; ];
} else if (device === 'macos') { } else if (device === 'macos') {
apps = [ apps = [
{ name: 'V2Box', desc: 'Native M1/M2 Support', link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' }, { name: 'V2Box', desc: t('app_desc_native'), link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' },
{ name: 'Hiddify Next', desc: 'Elegant Desktop Client', link: 'https://github.com/hiddify/hiddify-next/releases' } { name: 'Hiddify Next', desc: t('app_desc_elegant'), link: 'https://github.com/hiddify/hiddify-next/releases' }
]; ];
} else { } else {
// Linux or fallback // Linux or fallback
@@ -663,7 +793,7 @@ function openDownloadModal() {
let html = ` let html = `
<div class="download-options"> <div class="download-options">
<p class="subtitle" style="margin-bottom:20px;">Choose a client for your device:</p> <p class="subtitle" style="margin-bottom:20px;">${t('modal_dl_subtitle')}</p>
<div class="admin-list"> <div class="admin-list">
`; `;
@@ -681,16 +811,16 @@ function openDownloadModal() {
html += `</div></div>`; html += `</div></div>`;
openModal("Download App", html); openModal(t('modal_dl_title'), html);
lucide.createIcons(); lucide.createIcons();
} }
function copyConfig() { function copyConfig() {
if (currentState.subUrl) { if (currentState.subUrl) {
navigator.clipboard.writeText(currentState.subUrl); navigator.clipboard.writeText(currentState.subUrl);
showToast("Link copied!", "success"); showToast(t('msg_link_copied'), "success");
} else { } else {
showToast("No subscription found.", "error"); showToast(t('err_no_sub'), "error");
} }
} }
@@ -739,13 +869,13 @@ async function checkPromo() {
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
currentState.promoCode = data.code; currentState.promoCode = data.code;
showToast(`Promo Applied! ${data.discount}% OFF.`, "success"); showToast(t('msg_promo_applied').replace('{0}', data.discount), "success");
closeModal(); // Optional: close modal on success closeModal(); // Optional: close modal on success
} else { } else {
showToast("Invalid Promo Code", "error"); showToast(t('err_promo_invalid'), "error");
} }
} catch (e) { } catch (e) {
showToast("Error checking promo", "error"); showToast(t('err_promo_check'), "error");
} }
} }
@@ -792,22 +922,22 @@ function openSupport() {
const html = ` const html = `
<div class="support-form"> <div class="support-form">
<p style="color:var(--md-sys-color-outline); margin-bottom:12px;"> <p style="color:var(--md-sys-color-outline); margin-bottom:12px;">
Describe your issue. We will contact you via Telegram. ${t('msg_support_sent')}
</p> </p>
<textarea id="support-msg" class="glass-input" style="height:100px; padding-top:10px; resize:none;" placeholder="Your message..."></textarea> <textarea id="support-msg" class="glass-input" style="height:100px; padding-top:10px; resize:none;" placeholder="${t('ph_message')}"></textarea>
<button class="btn-primary" style="margin-top:16px;" onclick="sendSupport()">Send Message</button> <button class="btn-primary" style="margin-top:16px;" onclick="sendSupport()">${t('adm_send')}</button>
</div> </div>
`; `;
openModal("Support", html); openModal(t('nav_support'), html);
} }
function openAbout() { function openAbout() {
openModal("About", ` openModal(t('btn_about'), `
<div style="text-align:center"> <div style="text-align:center">
<i data-lucide="rocket" style="width:48px;height:48px;color:var(--md-sys-color-primary)"></i> <i data-lucide="rocket" style="width:48px;height:48px;color:var(--md-sys-color-primary)"></i>
<h3 style="margin-top:12px">Stellarisei VPN v1.2</h3> <h3 style="margin-top:12px">Stellarisei VPN v1.2</h3>
<p style="color:var(--md-sys-color-outline); margin-top:8px"> <p style="color:var(--md-sys-color-outline); margin-top:8px">
Premium V2Ray service.<br>Fast, Secure, Reliable. ${t('about_slogan')}
</p> </p>
<p style="margin-top:16px; font-size:12px; opacity:0.5">© 2026 Stellarisei</p> <p style="margin-top:16px; font-size:12px; opacity:0.5">© 2026 Stellarisei</p>
</div> </div>
@@ -837,12 +967,12 @@ async function sendSupport() {
if (res.ok) { if (res.ok) {
closeModal(); closeModal();
showToast("Message sent! Admin will contact you.", "success"); showToast(t('msg_support_sent'), "success");
} else { } else {
showToast("Failed to send.", "error"); showToast(t('err_send_fail'), "error");
} }
} catch (e) { } catch (e) {
showToast("Error sending message.", "error"); showToast(t('err_network'), "error");
} finally { } finally {
if (btn) { if (btn) {
btn.innerText = oldText; btn.innerText = oldText;
@@ -952,7 +1082,7 @@ async function adminSearchUsers() {
list.innerHTML = ` list.innerHTML = `
<div class="card glass" style="text-align:center; padding:24px; opacity:0.6;"> <div class="card glass" style="text-align:center; padding:24px; opacity:0.6;">
<i data-lucide="search-x" style="width:40px; height:40px; margin-bottom:12px;"></i> <i data-lucide="search-x" style="width:40px; height:40px; margin-bottom:12px;"></i>
<p>No users found</p> <p>${t('adm_no_users')}</p>
</div> </div>
`; `;
lucide.createIcons(); lucide.createIcons();
@@ -1000,7 +1130,7 @@ async function showAdminUserDetail(targetId) {
} }
const m = data.marzban || {}; const m = data.marzban || {};
document.getElementById('adm-user-status').textContent = m.status || 'Inactive'; document.getElementById('adm-user-status').textContent = m.status || t('status_inactive');
const subUntil = data.user.subscription_until ? new Date(data.user.subscription_until) : null; const subUntil = data.user.subscription_until ? new Date(data.user.subscription_until) : null;
let expStr = 'None'; let expStr = 'None';
@@ -1020,9 +1150,9 @@ async function showAdminUserDetail(targetId) {
async function adminUserAction(action) { async function adminUserAction(action) {
if (action === 'toggle_status' || action === 'reset_traffic' || action === 'delete_sub') { if (action === 'toggle_status' || action === 'reset_traffic' || action === 'delete_sub') {
const config = { const config = {
'toggle_status': { title: 'Toggle Status', msg: 'Change user active/disabled state?', btnClass: 'btn-primary' }, 'toggle_status': { title: t('adm_btn_toggle'), msg: t('confirm_toggle'), btnClass: 'btn-primary' },
'reset_traffic': { title: 'Reset Traffic', msg: 'Reset used traffic to zero?', btnClass: 'btn-primary' }, 'reset_traffic': { title: t('adm_btn_reset'), msg: t('confirm_reset'), btnClass: 'btn-primary' },
'delete_sub': { title: 'Delete Subscription', msg: 'Are you sure? User will lose VPN access immediately.', btnClass: 'btn-error' } 'delete_sub': { title: t('adm_btn_delete'), msg: t('confirm_delete_sub'), btnClass: 'btn-error' }
}; };
const cfg = config[action]; const cfg = config[action];
openModal(cfg.title, ` openModal(cfg.title, `
@@ -1041,34 +1171,34 @@ async function adminUserAction(action) {
let html = ""; let html = "";
if (action === 'add_days') { if (action === 'add_days') {
title = "Add Days"; title = t('title_add_days');
html = ` html = `
<div class="modal-form"> <div class="modal-form">
<p class="subtitle" style="margin-bottom:12px;">Enter days to ADD (negative to subtract):</p> <p class="subtitle" style="margin-bottom:12px;">${t('subtitle_add_days')}</p>
<input type="number" id="adm-action-val" class="glass-input" value="30" style="text-align:center;"> <input type="number" id="adm-action-val" class="glass-input" value="30" style="text-align:center;">
<button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('add_days')">Apply</button> <button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('add_days')">${t('btn_apply')}</button>
</div> </div>
`; `;
} else if (action === 'set_limit') { } else if (action === 'set_limit') {
title = "Set Traffic Limit"; title = t('title_set_limit');
html = ` html = `
<div class="modal-form"> <div class="modal-form">
<p class="subtitle" style="margin-bottom:12px;">Enter new limit in GB (0 for ∞):</p> <p class="subtitle" style="margin-bottom:12px;">${t('subtitle_set_limit')}</p>
<input type="number" id="adm-action-val" class="glass-input" value="50" style="text-align:center;"> <input type="number" id="adm-action-val" class="glass-input" value="50" style="text-align:center;">
<button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('set_limit')">Set Limit</button> <button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('set_limit')">${t('title_set_limit')}</button>
</div> </div>
`; `;
} else if (action === 'set_expiry') { } else if (action === 'set_expiry') {
title = "Set Expiration"; title = t('title_set_exp');
html = ` html = `
<div class="modal-form"> <div class="modal-form">
<p class="subtitle" style="margin-bottom:12px;">Days from NOW (0 = expire, 36500 = ∞):</p> <p class="subtitle" style="margin-bottom:12px;">${t('subtitle_set_exp')}</p>
<input type="number" id="adm-action-val" class="glass-input" value="30" style="text-align:center;"> <input type="number" id="adm-action-val" class="glass-input" value="30" style="text-align:center;">
<button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('set_expiry')">Set Expiry</button> <button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('set_expiry')">${t('title_set_exp')}</button>
</div> </div>
`; `;
} else if (action === 'set_plan') { } else if (action === 'set_plan') {
title = "Select Plan"; title = t('shop_title');
const res = await fetch(`${API_BASE}/admin/plans_full?user_id=${currentState.user.id}`); const res = await fetch(`${API_BASE}/admin/plans_full?user_id=${currentState.user.id}`);
const allPlans = await res.json(); const allPlans = await res.json();
@@ -1179,10 +1309,10 @@ async function loadAdminPromos() {
async function deleteAdminPromo(code) { async function deleteAdminPromo(code) {
openModal("Delete Promo", ` openModal("Delete Promo", `
<div class="confirm-modal"> <div class="confirm-modal">
<p class="subtitle" style="margin-bottom:24px;">Are you sure you want to delete promo code <b>${code}</b>?</p> <p class="subtitle" style="margin-bottom:24px;">${t('confirm_delete_promo').replace('{0}', code)}</p>
<div style="display:flex; gap:12px;"> <div style="display:flex; gap:12px;">
<button class="btn-secondary" style="flex:1" onclick="closeModal()">Cancel</button> <button class="btn-secondary" style="flex:1" onclick="closeModal()">${t('btn_back')}</button>
<button class="btn-error" style="flex:1" onclick="submitDeletePromo('${code}')">Delete</button> <button class="btn-error" style="flex:1" onclick="submitDeletePromo('${code}')">${t('adm_btn_delete')}</button>
</div> </div>
</div> </div>
`); `);
@@ -1203,31 +1333,31 @@ async function submitDeletePromo(code) {
} }
function openCreatePromoModal() { function openCreatePromoModal() {
openModal("New Promo", ` openModal(t('title_new_promo'), `
<div class="promo-create-form" style="display:flex; flex-direction:column; gap:12px;"> <div class="promo-create-form" style="display:flex; flex-direction:column; gap:12px;">
<input type="text" id="new-promo-code" placeholder="CODE (e.g. SUMMER2024)" class="glass-input" style="text-transform:uppercase;"> <input type="text" id="new-promo-code" placeholder="${t('ph_code')}" class="glass-input" style="text-transform:uppercase;">
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;"> <div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;">
<input type="number" id="new-promo-discount" placeholder="Discount %" class="glass-input"> <input type="number" id="new-promo-discount" placeholder="${t('ph_discount')}" class="glass-input">
<input type="number" id="new-promo-uses" placeholder="Uses Limit" class="glass-input"> <input type="number" id="new-promo-uses" placeholder="${t('ph_uses')}" class="glass-input">
</div> </div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;"> <div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;">
<input type="number" id="new-promo-days" placeholder="Validity Days" class="glass-input"> <input type="number" id="new-promo-days" placeholder="${t('ph_days')}" class="glass-input">
<input type="number" id="new-promo-bonus" placeholder="Bonus Days" class="glass-input"> <input type="number" id="new-promo-bonus" placeholder="${t('ph_bonus')}" class="glass-input">
</div> </div>
<label class="list-item glass" style="margin:0; padding:8px 12px; cursor:pointer;"> <label class="list-item glass" style="margin:0; padding:8px 12px; cursor:pointer;">
<span style="flex:1">Unlimited Usage</span> <span style="flex:1">${t('lbl_unlim_usage')}</span>
<input type="checkbox" id="new-promo-unlim" style="width:20px; height:20px;"> <input type="checkbox" id="new-promo-unlim" style="width:20px; height:20px;">
</label> </label>
<label class="list-item glass" style="margin:0; padding:8px 12px; cursor:pointer;"> <label class="list-item glass" style="margin:0; padding:8px 12px; cursor:pointer;">
<span style="flex:1">Permanent (Sticky)</span> <span style="flex:1">${t('lbl_sticky')}</span>
<input type="checkbox" id="new-promo-sticky" style="width:20px; height:20px;"> <input type="checkbox" id="new-promo-sticky" style="width:20px; height:20px;">
</label> </label>
<p style="font-size:11px; opacity:0.6; padding:0 4px;">* Sticky promo locks the discount for the user forever.</p> <p style="font-size:11px; opacity:0.6; padding:0 4px;">${t('note_sticky')}</p>
<button class="btn-primary" style="margin-top:8px;" onclick="createPromo()">Create Promo</button> <button class="btn-primary" style="margin-top:8px;" onclick="createPromo()">${t('btn_create_promo')}</button>
</div> </div>
`); `);
} }
@@ -1263,7 +1393,7 @@ async function sendBroadcast() {
const msg = document.getElementById('broadcast-msg').value; const msg = document.getElementById('broadcast-msg').value;
if (!msg) return; if (!msg) return;
if (!confirm("Send this message to ALL users?")) return; if (!confirm(t('confirm_broadcast'))) return;
const res = await fetch(`${API_BASE}/admin/broadcast`, { const res = await fetch(`${API_BASE}/admin/broadcast`, {
method: 'POST', method: 'POST',
@@ -1272,7 +1402,7 @@ async function sendBroadcast() {
}); });
const data = await res.json(); const data = await res.json();
showToast(`Broadcast sent to ${data.sent} users!`); showToast(t('msg_broadcast_sent').replace('{0}', data.sent));
} }
function openPromoModal() { function openPromoModal() {