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

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>
<!-- JS -->
<script src="js/app.js?v=4"></script>
<script src="js/app.js?v=5"></script>
</body>
</html>

View File

@@ -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.<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: {
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 сервис.<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>
</div>
<div class="plan-specs">
<span><i data-lucide="database"></i> ${plan.data_limit > 0 ? plan.data_limit + ' GB' : 'Unlimited'}</span>
<span><i data-lucide="clock"></i> ${plan.days} Days</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} ${t('unit_days')}</span>
</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);
});
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 = `
<div class="sub-actions">
<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 class="btn-secondary" onclick="copyConfig()">
<i data-lucide="copy"></i> Copy Link
<i data-lucide="copy"></i> ${t('btn_copy')}
</button>
</div>
`;
@@ -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 = `
<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">
`;
@@ -681,16 +811,16 @@ function openDownloadModal() {
html += `</div></div>`;
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 = `
<div class="support-form">
<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>
<textarea id="support-msg" class="glass-input" style="height:100px; padding-top:10px; resize:none;" placeholder="Your message..."></textarea>
<button class="btn-primary" style="margin-top:16px;" onclick="sendSupport()">Send Message</button>
<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()">${t('adm_send')}</button>
</div>
`;
openModal("Support", html);
openModal(t('nav_support'), html);
}
function openAbout() {
openModal("About", `
openModal(t('btn_about'), `
<div style="text-align:center">
<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>
<p style="color:var(--md-sys-color-outline); margin-top:8px">
Premium V2Ray service.<br>Fast, Secure, Reliable.
${t('about_slogan')}
</p>
<p style="margin-top:16px; font-size:12px; opacity:0.5">© 2026 Stellarisei</p>
</div>
@@ -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 = `
<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>
<p>No users found</p>
<p>${t('adm_no_users')}</p>
</div>
`;
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 = `
<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;">
<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>
`;
} else if (action === 'set_limit') {
title = "Set Traffic Limit";
title = t('title_set_limit');
html = `
<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;">
<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>
`;
} else if (action === 'set_expiry') {
title = "Set Expiration";
title = t('title_set_exp');
html = `
<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;">
<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>
`;
} 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", `
<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;">
<button class="btn-secondary" style="flex:1" onclick="closeModal()">Cancel</button>
<button class="btn-error" style="flex:1" onclick="submitDeletePromo('${code}')">Delete</button>
<button class="btn-secondary" style="flex:1" onclick="closeModal()">${t('btn_back')}</button>
<button class="btn-error" style="flex:1" onclick="submitDeletePromo('${code}')">${t('adm_btn_delete')}</button>
</div>
</div>
`);
@@ -1203,31 +1333,31 @@ async function submitDeletePromo(code) {
}
function openCreatePromoModal() {
openModal("New Promo", `
openModal(t('title_new_promo'), `
<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;">
<input type="number" id="new-promo-discount" placeholder="Discount %" class="glass-input">
<input type="number" id="new-promo-uses" placeholder="Uses Limit" 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="${t('ph_uses')}" class="glass-input">
</div>
<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-bonus" placeholder="Bonus 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="${t('ph_bonus')}" class="glass-input">
</div>
<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;">
</label>
<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;">
</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>
`);
}
@@ -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() {