feat: allow to edit remnawave panel settings in admin panel, ensure using i18n
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 чата для логов",
|
||||
|
||||
Reference in New Issue
Block a user