diff --git a/web_app/static/index.html b/web_app/static/index.html index 4034a08..64977f2 100644 --- a/web_app/static/index.html +++ b/web_app/static/index.html @@ -414,7 +414,7 @@ - + \ No newline at end of file diff --git a/web_app/static/js/app.js b/web_app/static/js/app.js index 0b0fefd..f41d7dc 100644 --- a/web_app/static/js/app.js +++ b/web_app/static/js/app.js @@ -81,7 +81,72 @@ const translations = { status_inactive: "Inactive", status_expired: "Expired", 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.
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 {0}?", + + 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: { nav_home: "Главная", @@ -153,7 +218,72 @@ const translations = { status_inactive: "Неактивна", status_expired: "Истекла", 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 сервис.
Быстро, Надежно, Безопасно.", + + 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: "Удалить промокод {0}?", + + 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() { ${plan.price} ⭐️
- ${plan.data_limit > 0 ? plan.data_limit + ' GB' : 'Unlimited'} - ${plan.days} Days + ${plan.data_limit > 0 ? plan.data_limit + ' ' + t('unit_gb') : t('plan_unlimited')} + ${plan.days} ${t('unit_days')}
- + `; container.appendChild(card); }); try { if (window.lucide) lucide.createIcons(); } catch (e) { } } catch (e) { - container.textContent = "Failed to load plans."; + container.textContent = t('err_plans_load'); } } async function initPayment(planId) { - if (!tg) return alert("Open in Telegram"); + if (!tg) return alert(t('err_open_tg')); const btn = event.target; const oldText = btn.innerText; - btn.innerText = 'Creating..'; + btn.innerText = t('btn_creating'); btn.disabled = true; try { @@ -563,7 +693,7 @@ async function initPayment(planId) { if (data.invoice_link) { tg.openInvoice(data.invoice_link, (status) => { if (status === 'paid') { - showToast('Successful Payment!', 'success'); + showToast(t('msg_paid'), 'success'); router('dashboard'); } }); @@ -571,7 +701,7 @@ async function initPayment(planId) { showToast('Error: ' + (data.error || 'Unknown'), 'error'); } } catch (e) { - showToast('Network error', 'error'); + showToast(t('err_network'), 'error'); } finally { btn.innerText = oldText; btn.disabled = false; @@ -603,7 +733,7 @@ async function loadSubscription() { }); } } else { - if (linkEl) linkEl.textContent = "No active subscription"; + if (linkEl) linkEl.textContent = t('status_no_sub'); } // Dynamic Apps based on Device @@ -616,10 +746,10 @@ async function loadSubscription() { appsContainer.innerHTML = `
`; @@ -633,25 +763,25 @@ function openDownloadModal() { if (device === 'ios') { apps = [ - { name: 'V2Box', desc: 'Sleek & Fast', 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: 'V2Box', desc: t('app_desc_sleek'), link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' }, + { name: 'Streisand', desc: t('app_desc_premium'), link: 'https://apps.apple.com/us/app/streisand/id6450534064' } ]; } else if (device === 'android') { apps = [ - { name: 'v2rayNG', desc: 'Standard Client', 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: 'Hiddify', desc: 'Universal All-in-One', link: 'https://play.google.com/store/apps/details?id=app.hiddify.com' } + { name: 'v2rayNG', desc: t('app_desc_standard'), link: 'https://play.google.com/store/apps/details?id=com.v2ray.ang' }, + { name: 'NekoBox', desc: t('app_desc_modern'), link: 'https://github.com/MatsuriDayo/NekoBoxForAndroid/releases' }, + { name: 'Hiddify', desc: t('app_desc_aio'), link: 'https://play.google.com/store/apps/details?id=app.hiddify.com' } ]; } else if (device === 'windows') { apps = [ - { name: 'NekoRay', desc: 'Powerful & Open Source', link: 'https://github.com/MatsuriDayo/nekoray/releases' }, - { name: 'Hiddify Next', desc: 'Simple & Modern', link: 'https://github.com/hiddify/hiddify-next/releases' }, - { name: 'v2rayN', desc: 'Classic Client', link: 'https://github.com/2many986/v2rayN/releases' } + { name: 'NekoRay', desc: t('app_desc_opensource'), link: 'https://github.com/MatsuriDayo/nekoray/releases' }, + { name: 'Hiddify Next', desc: t('app_desc_simple'), link: 'https://github.com/hiddify/hiddify-next/releases' }, + { name: 'v2rayN', desc: t('app_desc_classic'), link: 'https://github.com/2many986/v2rayN/releases' } ]; } else if (device === 'macos') { apps = [ - { name: 'V2Box', desc: 'Native M1/M2 Support', 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: 'V2Box', desc: t('app_desc_native'), link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' }, + { name: 'Hiddify Next', desc: t('app_desc_elegant'), link: 'https://github.com/hiddify/hiddify-next/releases' } ]; } else { // Linux or fallback @@ -663,7 +793,7 @@ function openDownloadModal() { let html = `
-

Choose a client for your device:

+

${t('modal_dl_subtitle')}

`; @@ -681,16 +811,16 @@ function openDownloadModal() { html += `
`; - openModal("Download App", html); + openModal(t('modal_dl_title'), html); lucide.createIcons(); } function copyConfig() { if (currentState.subUrl) { navigator.clipboard.writeText(currentState.subUrl); - showToast("Link copied!", "success"); + showToast(t('msg_link_copied'), "success"); } else { - showToast("No subscription found.", "error"); + showToast(t('err_no_sub'), "error"); } } @@ -739,13 +869,13 @@ async function checkPromo() { if (res.ok) { const data = await res.json(); 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 } else { - showToast("Invalid Promo Code", "error"); + showToast(t('err_promo_invalid'), "error"); } } catch (e) { - showToast("Error checking promo", "error"); + showToast(t('err_promo_check'), "error"); } } @@ -792,22 +922,22 @@ function openSupport() { const html = `

- Describe your issue. We will contact you via Telegram. + ${t('msg_support_sent')}

- - + +
`; - openModal("Support", html); + openModal(t('nav_support'), html); } function openAbout() { - openModal("About", ` + openModal(t('btn_about'), `

Stellarisei VPN v1.2

- Premium V2Ray service.
Fast, Secure, Reliable. + ${t('about_slogan')}

© 2026 Stellarisei

@@ -837,12 +967,12 @@ async function sendSupport() { if (res.ok) { closeModal(); - showToast("Message sent! Admin will contact you.", "success"); + showToast(t('msg_support_sent'), "success"); } else { - showToast("Failed to send.", "error"); + showToast(t('err_send_fail'), "error"); } } catch (e) { - showToast("Error sending message.", "error"); + showToast(t('err_network'), "error"); } finally { if (btn) { btn.innerText = oldText; @@ -952,7 +1082,7 @@ async function adminSearchUsers() { list.innerHTML = `
-

No users found

+

${t('adm_no_users')}

`; lucide.createIcons(); @@ -1000,7 +1130,7 @@ async function showAdminUserDetail(targetId) { } 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; let expStr = 'None'; @@ -1020,9 +1150,9 @@ async function showAdminUserDetail(targetId) { async function adminUserAction(action) { if (action === 'toggle_status' || action === 'reset_traffic' || action === 'delete_sub') { const config = { - 'toggle_status': { title: 'Toggle Status', msg: 'Change user active/disabled state?', btnClass: 'btn-primary' }, - 'reset_traffic': { title: 'Reset Traffic', msg: 'Reset used traffic to zero?', btnClass: 'btn-primary' }, - 'delete_sub': { title: 'Delete Subscription', msg: 'Are you sure? User will lose VPN access immediately.', btnClass: 'btn-error' } + 'toggle_status': { title: t('adm_btn_toggle'), msg: t('confirm_toggle'), btnClass: 'btn-primary' }, + 'reset_traffic': { title: t('adm_btn_reset'), msg: t('confirm_reset'), btnClass: 'btn-primary' }, + 'delete_sub': { title: t('adm_btn_delete'), msg: t('confirm_delete_sub'), btnClass: 'btn-error' } }; const cfg = config[action]; openModal(cfg.title, ` @@ -1041,34 +1171,34 @@ async function adminUserAction(action) { let html = ""; if (action === 'add_days') { - title = "Add Days"; + title = t('title_add_days'); html = ` `; } else if (action === 'set_limit') { - title = "Set Traffic Limit"; + title = t('title_set_limit'); html = ` `; } else if (action === 'set_expiry') { - title = "Set Expiration"; + title = t('title_set_exp'); html = ` `; } 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 allPlans = await res.json(); @@ -1179,10 +1309,10 @@ async function loadAdminPromos() { async function deleteAdminPromo(code) { openModal("Delete Promo", `
-

Are you sure you want to delete promo code ${code}?

+

${t('confirm_delete_promo').replace('{0}', code)}

- - + +
`); @@ -1203,31 +1333,31 @@ async function submitDeletePromo(code) { } function openCreatePromoModal() { - openModal("New Promo", ` + openModal(t('title_new_promo'), `
- +
- - + +
- - + +
-

* Sticky promo locks the discount for the user forever.

+

${t('note_sticky')}

- +
`); } @@ -1263,7 +1393,7 @@ async function sendBroadcast() { const msg = document.getElementById('broadcast-msg').value; 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`, { method: 'POST', @@ -1272,7 +1402,7 @@ async function sendBroadcast() { }); const data = await res.json(); - showToast(`Broadcast sent to ${data.sent} users!`); + showToast(t('msg_broadcast_sent').replace('{0}', data.sent)); } function openPromoModal() {