diff --git a/backend/bot/app/web/admin_settings_manifest.py b/backend/bot/app/web/admin_settings_manifest.py index 58fe5f8..447c9fd 100644 --- a/backend/bot/app/web/admin_settings_manifest.py +++ b/backend/bot/app/web/admin_settings_manifest.py @@ -8,6 +8,7 @@ the API, even by an admin. from __future__ import annotations +import re from dataclasses import dataclass from typing import Any, List, Optional, Tuple @@ -28,6 +29,7 @@ class SettingField: subsection: Optional[str] = None # group label inside a section i18n_label_key: Optional[str] = None i18n_description_key: Optional[str] = None + i18n_subsection_key: Optional[str] = None SETTINGS_MANIFEST: List[SettingField] = [ @@ -72,10 +74,59 @@ SETTINGS_MANIFEST: List[SettingField] = [ "Ссылка на канал", "Имя пользователя или invite-link.", ), + SettingField( + "PANEL_API_URL", + "url", + "general", + "URL API Remnawave", + "Например, https://panel.example.com/api.", + subsection="Remnawave", + ), + SettingField( + "PANEL_API_KEY", + "string", + "general", + "API-ключ Remnawave", + "Секретный ключ API панели.", + secret=True, + subsection="Remnawave", + ), + SettingField( + "PANEL_WEBHOOK_SECRET", + "string", + "general", + "Секрет вебхуков Remnawave", + "Используется для проверки входящих вебхуков панели.", + secret=True, + subsection="Remnawave", + ), + SettingField( + "USER_SQUAD_UUIDS", + "string", + "general", + "Internal Squads по умолчанию", + "UUID через запятую для legacy-режима без JSON-каталога тарифов.", + subsection="Remnawave", + ), + SettingField( + "USER_EXTERNAL_SQUAD_UUID", + "string", + "general", + "External Squad по умолчанию", + "Необязательный UUID External Squad для новых пользователей.", + subsection="Remnawave", + ), # ─── Web app appearance ──────────────────────────────────────── SettingField( "WEBAPP_TITLE", "string", "appearance", "Название Web App", placeholder="Моя подписка" ), + SettingField( + "SUBSCRIPTION_MINI_APP_URL", + "url", + "appearance", + "Публичный URL Mini App", + "Например, https://app.example.com/.", + ), SettingField( "WEBAPP_PRIMARY_COLOR", "color", "appearance", "Основной цвет", placeholder="#00fe7a" ), @@ -131,7 +182,7 @@ SETTINGS_MANIFEST: List[SettingField] = [ "string", "pricing", "Порядок методов оплаты", - "Через запятую, например: severpay,freekassa,yookassa", + "Через запятую, например: severpay,freekassa,yookassa,heleket", ), SettingField( "SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED", @@ -156,14 +207,14 @@ SETTINGS_MANIFEST: List[SettingField] = [ ), # ─── Payment providers (toggles) ─────────────────────────────── # Common - SettingField("STARS_ENABLED", "bool", "payments", "Telegram Stars", subsection="Общие"), + SettingField("STARS_ENABLED", "bool", "payments", "Telegram Stars", subsection="common"), SettingField( "PAYMENT_METHODS_ORDER", "string", "payments", "Порядок методов оплаты", - "Через запятую: severpay,freekassa,yookassa,platega,stars,cryptopay", - subsection="Общие", + "Через запятую: severpay,freekassa,yookassa,platega,stars,cryptopay,heleket", + subsection="common", ), # ─── Trial ───────────────────────────────────────────────────── SettingField("TRIAL_ENABLED", "bool", "trial", "Триал включён"), @@ -373,6 +424,9 @@ def _provider_field_to_setting_field(spec: Any, manifest_field: Any) -> SettingF max=manifest_field.max, choices=tuple(manifest_field.choices) if manifest_field.choices else None, subsection=manifest_field.subsection, + i18n_label_key=getattr(manifest_field, "i18n_label_key", None), + i18n_description_key=getattr(manifest_field, "i18n_description_key", None), + i18n_subsection_key=getattr(manifest_field, "i18n_subsection_key", None), ) @@ -439,6 +493,11 @@ def coerce_value(field: SettingField, raw: Any) -> Any: return str(raw) +def _i18n_slug(value: str) -> str: + slug = re.sub(r"[^a-z0-9]+", "_", value.strip().lower()).strip("_") + return slug or "default" + + def manifest_payload() -> List[dict]: """Serialize the manifest for the admin UI. @@ -465,6 +524,11 @@ def manifest_payload() -> List[dict]: for field in aggregated_manifest(): auto_label_i18n_key = f"admin_settings_field_{field.key.lower()}_label" auto_description_i18n_key = f"admin_settings_field_{field.key.lower()}_description" + auto_subsection_i18n_key = ( + f"admin_settings_subsection_{_i18n_slug(field.subsection)}" + if field.subsection + else None + ) default_value: Optional[str] = None owner = find_manifest_owner(field.key) @@ -487,6 +551,10 @@ def manifest_payload() -> List[dict]: "i18n_label_key": field.i18n_label_key or auto_label_i18n_key, "i18n_description_key": field.i18n_description_key or (auto_description_i18n_key if field.description else None), + "i18n_subsection_key": field.i18n_subsection_key or auto_subsection_i18n_key, + "i18n_placeholder_key": ( + f"admin_settings_field_{field.key.lower()}_placeholder" if placeholder else None + ), "placeholder": placeholder, "optional": field.optional, "secret": field.secret, @@ -494,6 +562,15 @@ def manifest_payload() -> List[dict]: if default_value is not None: item["default"] = default_value if field.choices: - item["choices"] = [{"value": v, "label": lbl} for v, lbl in field.choices] + item["choices"] = [ + { + "value": v, + "label": lbl, + "i18n_label_key": ( + f"admin_settings_field_{field.key.lower()}_choice_{_i18n_slug(str(v))}" + ), + } + for v, lbl in field.choices + ] items.append(item) return items diff --git a/frontend/src/admin/sections/AppearanceSection.svelte b/frontend/src/admin/sections/AppearanceSection.svelte index 6d89201..1837d56 100644 --- a/frontend/src/admin/sections/AppearanceSection.svelte +++ b/frontend/src/admin/sections/AppearanceSection.svelte @@ -23,6 +23,7 @@ const themesStore = getContext("themesStore"); const APPEARANCE_SETTING_KEYS = new Set([ "WEBAPP_TITLE", + "SUBSCRIPTION_MINI_APP_URL", "WEBAPP_PRIMARY_COLOR", "WEBAPP_LOGO_URL", "WEBAPP_LOGO_USE_EMOJI", @@ -76,7 +77,7 @@ }; $: emojiFontItems = (fieldMap.get("WEBAPP_LOGO_EMOJI_FONT")?.choices || []).map((item) => ({ value: item.value, - label: item.label, + label: item.i18n_label_key ? adminText(item.i18n_label_key, {}, item.label) : item.label, })); $: dirtyCount = Object.keys(settingsDirty || {}).filter((key) => isAppearanceSettingKey(key) @@ -135,6 +136,15 @@ return Boolean(value); } + function adminLocaleKey(key) { + const raw = String(key || ""); + return raw.startsWith("admin_") ? raw.slice("admin_".length) : raw; + } + + function adminText(key, params = {}, fallback = "") { + return key ? at(adminLocaleKey(key), params, fallback) : fallback; + } + function withLogoCacheBust(url) { return withCacheBust(url, logoPreviewNonce); } diff --git a/frontend/src/admin/sections/SettingsSection.svelte b/frontend/src/admin/sections/SettingsSection.svelte index dce8e3b..1c01d90 100644 --- a/frontend/src/admin/sections/SettingsSection.svelte +++ b/frontend/src/admin/sections/SettingsSection.svelte @@ -80,9 +80,9 @@ } function secretPlaceholder(field) { - if (settingsDirty[field.key]?.deleted) return field.placeholder || "••••••••"; + if (settingsDirty[field.key]?.deleted) return fieldPlaceholderText(field) || "********"; if (field.has_value) return at("settings_secret_configured", {}, "Secret is set"); - return field.placeholder || at("settings_secret_empty", {}, "Not set"); + return fieldPlaceholderText(field) || at("settings_secret_empty", {}, "Not set"); } function iconComponent(name) { @@ -127,29 +127,45 @@ const groups = new Map(); for (const field of section.fields || []) { const key = field.subsection || "_root"; - if (!groups.has(key)) groups.set(key, []); - groups.get(key).push(field); + if (!groups.has(key)) { + groups.set(key, { fields: [], i18nLabelKey: field.i18n_subsection_key || null }); + } + const group = groups.get(key); + group.fields.push(field); + if (!group.i18nLabelKey && field.i18n_subsection_key) { + group.i18nLabelKey = field.i18n_subsection_key; + } } - return Array.from(groups.entries()).map(([id, fields]) => ({ + return Array.from(groups.entries()).map(([id, group]) => ({ id, label: id === "_root" ? null : id, - fields, + i18nLabelKey: group.i18nLabelKey, + fields: group.fields, })); } + function adminLocaleKey(key) { + const raw = String(key || ""); + return raw.startsWith("admin_") ? raw.slice("admin_".length) : raw; + } + + function adminText(key, params = {}, fallback = "") { + return key ? at(adminLocaleKey(key), params, fallback) : fallback; + } + function sectionTitle(id) { const map = { - general: at("admin_settings_section_general", {}, "Общие"), - appearance: at("admin_settings_section_appearance", {}, "Внешний вид"), - pricing: at("admin_settings_section_pricing", {}, "Тарифы и цены"), - payments: at("admin_settings_section_payments", {}, "Платёжные системы"), - trial: at("admin_settings_section_trial", {}, "Триал"), - referral: at("admin_settings_section_referral", {}, "Реферальная программа"), - notifications: at("admin_settings_section_notifications", {}, "Уведомления"), - support: at("admin_settings_section_support", {}, "Поддержка"), - devices: at("admin_settings_section_devices", {}, "Устройства"), + general: "Общие", + appearance: "Внешний вид", + pricing: "Тарифы и цены", + payments: "Платёжные системы", + trial: "Триал", + referral: "Реферальная программа", + notifications: "Уведомления", + support: "Поддержка", + devices: "Устройства", }; - return map[id] || id; + return adminText(`settings_section_${id}`, {}, map[id] || id); } function englishFieldLabelFallback(key, originalLabel) { @@ -173,7 +189,33 @@ .toLowerCase() .startsWith("en"); const fallback = isEnglish ? englishFieldLabelFallback(field.key, field.label) : field.label; - return field.i18n_label_key ? at(field.i18n_label_key, {}, fallback) : fallback; + return field.i18n_label_key ? adminText(field.i18n_label_key, {}, fallback) : fallback; + } + + function fieldDescriptionText(field) { + if (!field.description) return ""; + return field.i18n_description_key + ? adminText(field.i18n_description_key, {}, field.description) + : field.description; + } + + function fieldPlaceholderText(field) { + const fallback = field.placeholder || ""; + return field.i18n_placeholder_key ? adminText(field.i18n_placeholder_key, {}, fallback) : fallback; + } + + function subsectionTitle(group) { + if (!group?.label) return ""; + return group.i18nLabelKey ? adminText(group.i18nLabelKey, {}, group.label) : group.label; + } + + function choiceItems(field) { + return (field.choices || []).map((choice) => ({ + ...choice, + label: choice.i18n_label_key + ? adminText(choice.i18n_label_key, {}, choice.label) + : choice.label, + })); } @@ -191,12 +233,8 @@ {/if} {field.key} - {#if field.description} - {field.i18n_description_key - ? at(field.i18n_description_key, {}, field.description) - : field.description} + {#if fieldDescriptionText(field)} + {fieldDescriptionText(field)} {/if}
@@ -255,9 +293,9 @@ settingsStore.markDirty(field.key, value)} /> {:else if field.type === "int" || field.type === "float"} @@ -265,7 +303,7 @@ class="input" type="number" step={field.type === "float" ? "0.1" : "1"} - placeholder={field.placeholder} + placeholder={fieldPlaceholderText(field)} value={valueFor(field) ?? ""} oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)} /> @@ -273,7 +311,7 @@ @@ -298,7 +336,7 @@ settingsStore.markDirty(field.key, e.currentTarget.value)} /> @@ -402,7 +440,7 @@ - {group.label} + {subsectionTitle(group)} {at( "settings_fields_count", diff --git a/frontend/src/lib/admin/stores/settingsStore.js b/frontend/src/lib/admin/stores/settingsStore.js index e74c3e1..ece62b9 100644 --- a/frontend/src/lib/admin/stores/settingsStore.js +++ b/frontend/src/lib/admin/stores/settingsStore.js @@ -85,9 +85,9 @@ export function createSettingsStore({ api, onToast, at }) { const summary = Object.entries(res.errors) .map(([k, v]) => `${k}: ${v}`) .join("; "); - onToast(`Ошибки: ${summary}`); + onToast(at("settings_validation_errors", { errors: summary }, `Ошибки: ${summary}`)); } else { - onToast(res?.error || "Ошибка"); + onToast(at("settings_save_error", { error: res?.error || "" }, res?.error || "Ошибка")); } return false; } finally { diff --git a/locales/en.json b/locales/en.json index e3e16f6..9b9d698 100644 --- a/locales/en.json +++ b/locales/en.json @@ -946,6 +946,18 @@ "admin_settings_section_notifications": "Notifications", "admin_settings_section_devices": "Devices", "admin_settings_section_support": "Support", + "admin_settings_subsection_common": "Common", + "admin_settings_subsection_remnawave": "Remnawave", + "admin_settings_subsection_telegram_stars": "Telegram Stars", + "admin_settings_subsection_yookassa": "YooKassa", + "admin_settings_subsection_freekassa": "FreeKassa", + "admin_settings_subsection_platega": "Platega", + "admin_settings_subsection_severpay": "SeverPay", + "admin_settings_subsection_cryptopay": "CryptoPay", + "admin_settings_subsection_wata": "Wata", + "admin_settings_subsection_heleket": "Heleket", + "admin_settings_validation_errors": "Errors: {errors}", + "admin_settings_save_error": "Error: {error}", "admin_sync_started": "Synchronization started", "admin_sync_error": "Synchronization error", "admin_error": "Error", @@ -1288,10 +1300,37 @@ "admin_settings_field_required_channel_id_description": "Controls the 'Required Channel ID' setting in admin overrides.", "admin_settings_field_required_channel_link_label": "Required Channel Link", "admin_settings_field_required_channel_link_description": "Controls the 'Required Channel Link' setting in admin overrides.", + "admin_settings_field_panel_api_url_label": "Remnawave API URL", + "admin_settings_field_panel_api_url_description": "For example, https://panel.example.com/api.", + "admin_settings_field_panel_api_key_label": "Remnawave API key", + "admin_settings_field_panel_api_key_description": "Secret API key for the panel.", + "admin_settings_field_panel_webhook_secret_label": "Remnawave webhook secret", + "admin_settings_field_panel_webhook_secret_description": "Used to verify incoming panel webhooks.", + "admin_settings_field_user_squad_uuids_label": "Default Internal Squads", + "admin_settings_field_user_squad_uuids_description": "Comma-separated UUIDs for the legacy mode without a JSON tariff catalog.", + "admin_settings_field_user_external_squad_uuid_label": "Default External Squad", + "admin_settings_field_user_external_squad_uuid_description": "Optional External Squad UUID for new users.", "admin_settings_field_webapp_title_label": "WebApp Title", + "admin_settings_field_subscription_mini_app_url_label": "Public Mini App URL", + "admin_settings_field_subscription_mini_app_url_description": "For example, https://app.example.com/.", "admin_settings_field_webapp_primary_color_label": "WebApp Primary Color", + "admin_settings_field_webapp_logo_use_emoji_label": "Use emoji logo", "admin_settings_field_webapp_logo_url_label": "WebApp Logo URL", "admin_settings_field_webapp_logo_emoji_label": "WebApp Logo Emoji", + "admin_settings_field_webapp_logo_emoji_font_label": "Emoji logo font", + "admin_settings_field_webapp_logo_emoji_font_description": "Choose the font used to render the emoji logo.", + "admin_settings_field_webapp_logo_emoji_font_choice_system": "System (default)", + "admin_settings_field_webapp_logo_emoji_font_choice_noto_color": "Noto Color Emoji", + "admin_settings_field_webapp_logo_emoji_font_choice_noto_color_animated": "Noto Color Emoji Animated", + "admin_settings_field_webapp_logo_emoji_font_choice_noto_emoji": "Noto Emoji", + "admin_settings_field_webapp_logo_emoji_font_choice_twemoji": "Twitter Emoji", + "admin_settings_field_webapp_logo_emoji_font_choice_openmoji": "OpenMoji", + "admin_settings_field_webapp_logo_emoji_font_choice_apple": "Apple Color Emoji (local)", + "admin_settings_field_webapp_logo_emoji_font_choice_segoe": "Segoe UI Emoji (local)", + "admin_settings_field_webapp_logo_emoji_font_choice_noto_local": "Noto Emoji (local)", + "admin_settings_field_webapp_favicon_use_custom_label": "Use separate favicon", + "admin_settings_field_webapp_favicon_url_label": "Separate favicon URL", + "admin_settings_field_webapp_logo_favicon_url_label": "Favicon from logo", "admin_settings_field_webapp_enabled_label": "WebApp Enabled", "admin_settings_field_month_1_enabled_label": "Month 1 Enabled", "admin_settings_field_month_3_enabled_label": "Month 3 Enabled", @@ -1363,6 +1402,35 @@ "admin_settings_field_cryptopay_currency_type_label": "CryptoPay Currency Type", "admin_settings_field_cryptopay_currency_type_description": "Controls the 'CryptoPay Currency Type' setting in admin overrides.", "admin_settings_field_cryptopay_asset_label": "CryptoPay Asset", + "admin_settings_field_wata_enabled_label": "Enabled", + "admin_settings_field_wata_api_token_label": "API token", + "admin_settings_field_wata_base_url_label": "Base URL", + "admin_settings_field_wata_return_url_label": "Return URL", + "admin_settings_field_wata_failed_url_label": "Failed URL", + "admin_settings_field_wata_payment_link_ttl_days_label": "Payment link lifetime (days)", + "admin_settings_field_wata_payment_link_ttl_days_description": "1..30; Wata defaults to 3 days and allows up to 30 days.", + "admin_settings_field_wata_webhook_verify_signature_label": "Verify webhook signature", + "admin_settings_field_wata_public_key_label": "Webhook public key", + "admin_settings_field_wata_public_key_description": "Optional. If empty, the backend fetches it from Wata.", + "admin_settings_field_wata_trusted_ips_label": "Trusted IPs", + "admin_settings_field_wata_trusted_ips_description": "Comma-separated IP addresses accepted for Wata webhooks.", + "admin_settings_field_heleket_enabled_label": "Enabled", + "admin_settings_field_heleket_merchant_id_label": "Merchant ID", + "admin_settings_field_heleket_api_key_label": "Payment API key", + "admin_settings_field_heleket_base_url_label": "Base URL", + "admin_settings_field_heleket_currency_label": "Invoice currency", + "admin_settings_field_heleket_currency_description": "Fiat or crypto code (RUB, USD, USDT).", + "admin_settings_field_heleket_to_currency_label": "Target crypto", + "admin_settings_field_heleket_to_currency_description": "Optional target cryptocurrency for conversion.", + "admin_settings_field_heleket_network_label": "Blockchain network", + "admin_settings_field_heleket_network_description": "Optional blockchain network code (tron, bsc, eth).", + "admin_settings_field_heleket_return_url_label": "Return URL", + "admin_settings_field_heleket_success_url_label": "Success URL", + "admin_settings_field_heleket_lifetime_seconds_label": "Invoice lifetime (seconds)", + "admin_settings_field_heleket_lifetime_seconds_description": "300..43200; Heleket defaults to 3600.", + "admin_settings_field_heleket_verify_webhook_signature_label": "Verify webhook signature", + "admin_settings_field_heleket_trusted_ips_label": "Trusted IPs", + "admin_settings_field_heleket_trusted_ips_description": "Comma-separated IP addresses accepted for Heleket webhooks.", "admin_settings_field_trial_enabled_label": "Trial Enabled", "admin_settings_field_trial_duration_days_label": "Trial Duration Days", "admin_settings_field_trial_traffic_limit_gb_label": "Trial Traffic Limit Gb", @@ -1387,6 +1455,7 @@ "admin_settings_field_log_promo_activations_label": "Log Promo Activations", "admin_settings_field_log_trial_activations_label": "Log Trial Activations", "admin_settings_field_log_suspicious_activity_label": "Log Suspicious Activity", + "admin_settings_field_log_support_label": "Log Support", "admin_settings_field_log_level_label": "Log Level", "admin_settings_field_log_level_description": "Controls the 'Log Level' setting in admin overrides.", "admin_settings_field_log_chat_id_label": "Log Chat ID", diff --git a/locales/ru.json b/locales/ru.json index 0fc7a25..bf7efc2 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -946,6 +946,18 @@ "admin_settings_section_notifications": "Уведомления", "admin_settings_section_devices": "Устройства", "admin_settings_section_support": "Поддержка", + "admin_settings_subsection_common": "Общие", + "admin_settings_subsection_remnawave": "Remnawave", + "admin_settings_subsection_telegram_stars": "Telegram Stars", + "admin_settings_subsection_yookassa": "YooKassa", + "admin_settings_subsection_freekassa": "FreeKassa", + "admin_settings_subsection_platega": "Platega", + "admin_settings_subsection_severpay": "SeverPay", + "admin_settings_subsection_cryptopay": "CryptoPay", + "admin_settings_subsection_wata": "Wata", + "admin_settings_subsection_heleket": "Heleket", + "admin_settings_validation_errors": "Ошибки: {errors}", + "admin_settings_save_error": "Ошибка: {error}", "admin_sync_started": "Синхронизация запущена", "admin_sync_error": "Ошибка синхронизации", "admin_error": "Ошибка", @@ -1288,10 +1300,37 @@ "admin_settings_field_required_channel_id_description": "Telegram ID канала, в котором нужно состоять.", "admin_settings_field_required_channel_link_label": "Ссылка на канал", "admin_settings_field_required_channel_link_description": "Имя пользователя или invite-link.", + "admin_settings_field_panel_api_url_label": "URL API Remnawave", + "admin_settings_field_panel_api_url_description": "Например, https://panel.example.com/api.", + "admin_settings_field_panel_api_key_label": "API-ключ Remnawave", + "admin_settings_field_panel_api_key_description": "Секретный ключ API панели.", + "admin_settings_field_panel_webhook_secret_label": "Секрет вебхуков Remnawave", + "admin_settings_field_panel_webhook_secret_description": "Используется для проверки входящих вебхуков панели.", + "admin_settings_field_user_squad_uuids_label": "Internal Squads по умолчанию", + "admin_settings_field_user_squad_uuids_description": "UUID через запятую для legacy-режима без JSON-каталога тарифов.", + "admin_settings_field_user_external_squad_uuid_label": "External Squad по умолчанию", + "admin_settings_field_user_external_squad_uuid_description": "Необязательный UUID External Squad для новых пользователей.", "admin_settings_field_webapp_title_label": "Название Web App", + "admin_settings_field_subscription_mini_app_url_label": "Публичный URL Mini App", + "admin_settings_field_subscription_mini_app_url_description": "Например, https://app.example.com/.", "admin_settings_field_webapp_primary_color_label": "Основной цвет", + "admin_settings_field_webapp_logo_use_emoji_label": "Использовать эмоджи-логотип", "admin_settings_field_webapp_logo_url_label": "URL логотипа", "admin_settings_field_webapp_logo_emoji_label": "Эмоджи-логотип", + "admin_settings_field_webapp_logo_emoji_font_label": "Шрифт эмоджи-логотипа", + "admin_settings_field_webapp_logo_emoji_font_description": "Выберите шрифт для отображения эмодзи-логотипа.", + "admin_settings_field_webapp_logo_emoji_font_choice_system": "Системный (по умолчанию)", + "admin_settings_field_webapp_logo_emoji_font_choice_noto_color": "Noto Color Emoji", + "admin_settings_field_webapp_logo_emoji_font_choice_noto_color_animated": "Noto Color Emoji Animated", + "admin_settings_field_webapp_logo_emoji_font_choice_noto_emoji": "Noto Emoji", + "admin_settings_field_webapp_logo_emoji_font_choice_twemoji": "Twitter Emoji", + "admin_settings_field_webapp_logo_emoji_font_choice_openmoji": "OpenMoji", + "admin_settings_field_webapp_logo_emoji_font_choice_apple": "Apple Color Emoji (local)", + "admin_settings_field_webapp_logo_emoji_font_choice_segoe": "Segoe UI Emoji (local)", + "admin_settings_field_webapp_logo_emoji_font_choice_noto_local": "Noto Emoji (local)", + "admin_settings_field_webapp_favicon_use_custom_label": "Использовать отдельную favicon", + "admin_settings_field_webapp_favicon_url_label": "URL отдельной favicon", + "admin_settings_field_webapp_logo_favicon_url_label": "Favicon из логотипа", "admin_settings_field_webapp_enabled_label": "Web App включён", "admin_settings_field_month_1_enabled_label": "Тариф 1 месяц", "admin_settings_field_month_3_enabled_label": "Тариф 3 месяца", @@ -1309,7 +1348,7 @@ "admin_settings_field_traffic_packages_description": "Формат: 10:199,50:799 (ГБ:цена)", "admin_settings_field_stars_traffic_packages_label": "Пакеты трафика (Stars)", "admin_settings_field_payment_methods_order_label": "Порядок методов оплаты", - "admin_settings_field_payment_methods_order_description": "Через запятую, например: severpay,freekassa,yookassa", + "admin_settings_field_payment_methods_order_description": "Через запятую, например: severpay,freekassa,yookassa,heleket", "admin_settings_field_subscription_purchase_description_enabled_label": "Показывать описание подписки", "admin_settings_field_subscription_purchase_description_enabled_description": "Текст появится перед выбором срока покупки или продления.", "admin_settings_field_subscription_purchase_description_ru_label": "Описание подписки (RU)", @@ -1363,6 +1402,35 @@ "admin_settings_field_cryptopay_currency_type_label": "Currency type", "admin_settings_field_cryptopay_currency_type_description": "fiat или crypto", "admin_settings_field_cryptopay_asset_label": "Asset", + "admin_settings_field_wata_enabled_label": "Включена", + "admin_settings_field_wata_api_token_label": "API token", + "admin_settings_field_wata_base_url_label": "Base URL", + "admin_settings_field_wata_return_url_label": "Return URL", + "admin_settings_field_wata_failed_url_label": "Failed URL", + "admin_settings_field_wata_payment_link_ttl_days_label": "Срок жизни ссылки (дней)", + "admin_settings_field_wata_payment_link_ttl_days_description": "1..30; по умолчанию Wata использует 3 дня.", + "admin_settings_field_wata_webhook_verify_signature_label": "Проверять подпись вебхука", + "admin_settings_field_wata_public_key_label": "Публичный ключ вебхука", + "admin_settings_field_wata_public_key_description": "Необязательно. Если пусто, бэкенд получит ключ из Wata.", + "admin_settings_field_wata_trusted_ips_label": "Доверенные IP", + "admin_settings_field_wata_trusted_ips_description": "IP-адреса через запятую, с которых принимаются вебхуки Wata.", + "admin_settings_field_heleket_enabled_label": "Включена", + "admin_settings_field_heleket_merchant_id_label": "Merchant ID", + "admin_settings_field_heleket_api_key_label": "Payment API key", + "admin_settings_field_heleket_base_url_label": "Base URL", + "admin_settings_field_heleket_currency_label": "Валюта инвойса", + "admin_settings_field_heleket_currency_description": "Фиатный или криптовалютный код (RUB, USD, USDT).", + "admin_settings_field_heleket_to_currency_label": "Целевая криптовалюта", + "admin_settings_field_heleket_to_currency_description": "Необязательная криптовалюта для конвертации.", + "admin_settings_field_heleket_network_label": "Blockchain network", + "admin_settings_field_heleket_network_description": "Необязательный код сети (tron, bsc, eth).", + "admin_settings_field_heleket_return_url_label": "Return URL", + "admin_settings_field_heleket_success_url_label": "Success URL", + "admin_settings_field_heleket_lifetime_seconds_label": "Срок жизни инвойса (сек)", + "admin_settings_field_heleket_lifetime_seconds_description": "300..43200; по умолчанию Heleket использует 3600.", + "admin_settings_field_heleket_verify_webhook_signature_label": "Проверять подпись вебхука", + "admin_settings_field_heleket_trusted_ips_label": "Доверенные IP", + "admin_settings_field_heleket_trusted_ips_description": "IP-адреса через запятую, с которых принимаются вебхуки Heleket.", "admin_settings_field_trial_enabled_label": "Триал включён", "admin_settings_field_trial_duration_days_label": "Длительность триала (дней)", "admin_settings_field_trial_traffic_limit_gb_label": "Лимит трафика триала (ГБ)", @@ -1387,6 +1455,7 @@ "admin_settings_field_log_promo_activations_label": "Логировать активации промокодов", "admin_settings_field_log_trial_activations_label": "Логировать активации триала", "admin_settings_field_log_suspicious_activity_label": "Логировать подозрительные действия", + "admin_settings_field_log_support_label": "Логировать поддержку", "admin_settings_field_log_level_label": "Глобальный уровень логов", "admin_settings_field_log_level_description": "DEBUG / INFO / WARNING / ERROR", "admin_settings_field_log_chat_id_label": "ID чата для логов",