feat: allow to edit remnawave panel settings in admin panel, ensure using i18n

This commit is contained in:
3252a8
2026-05-21 11:26:03 +03:00
parent b3f67a5398
commit 028f0680c6
6 changed files with 301 additions and 38 deletions
+82 -5
View File
@@ -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
@@ -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);
}
@@ -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,
}));
}
</script>
@@ -191,12 +233,8 @@
{/if}
</strong>
<code>{field.key}</code>
{#if field.description}
<small
>{field.i18n_description_key
? at(field.i18n_description_key, {}, field.description)
: field.description}</small
>
{#if fieldDescriptionText(field)}
<small>{fieldDescriptionText(field)}</small>
{/if}
</div>
<div class="admin-setting-control">
@@ -255,9 +293,9 @@
<AdminSelect
class="admin-setting-select"
value={valueFor(field) || ""}
items={field.choices}
items={choiceItems(field)}
ariaLabel={fieldLabelText(field)}
placeholder={field.placeholder || fieldLabelText(field)}
placeholder={fieldPlaceholderText(field) || fieldLabelText(field)}
onValueChange={(value) => 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 @@
<textarea
class="admin-setting-textarea"
rows="4"
placeholder={field.placeholder}
placeholder={fieldPlaceholderText(field)}
value={valueFor(field) ?? ""}
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
></textarea>
@@ -298,7 +336,7 @@
<input
class="input"
type="text"
placeholder={field.placeholder}
placeholder={fieldPlaceholderText(field)}
value={valueFor(field) ?? ""}
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
/>
@@ -402,7 +440,7 @@
<Accordion.Item value={group.id} class="admin-settings-subsection">
<Accordion.Header class="admin-accordion-header">
<Accordion.Trigger class="admin-settings-subsection-trigger">
<strong>{group.label}</strong>
<strong>{subsectionTitle(group)}</strong>
<span class="admin-settings-subsection-meta">
{at(
"settings_fields_count",
@@ -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 {
+69
View File
@@ -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",
+70 -1
View File
@@ -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 чата для логов",