feat: polish admin settings layout
This commit is contained in:
@@ -38,6 +38,19 @@
|
|||||||
let copiedWebhookKey = "";
|
let copiedWebhookKey = "";
|
||||||
let copiedWebhookTimer = null;
|
let copiedWebhookTimer = null;
|
||||||
|
|
||||||
|
const PLATEGA_SBP_KEYS = new Set(["PLATEGA_SBP_ENABLED", "PLATEGA_SBP_METHOD"]);
|
||||||
|
const PLATEGA_CRYPTO_KEYS = new Set(["PLATEGA_CRYPTO_ENABLED", "PLATEGA_CRYPTO_METHOD"]);
|
||||||
|
const PLATEGA_LEGACY_KEYS = new Set(["PLATEGA_PAYMENT_METHOD"]);
|
||||||
|
const LEGACY_TARIFF_TRAFFIC_KEYS = new Set(["TRAFFIC_PACKAGES", "STARS_TRAFFIC_PACKAGES"]);
|
||||||
|
const SEMANTIC_FIELD_GROUP_ORDER = {
|
||||||
|
platega_common: 1,
|
||||||
|
platega_sbp: 2,
|
||||||
|
platega_crypto: 3,
|
||||||
|
platega_legacy: 4,
|
||||||
|
legacy_tariff_periods: 1,
|
||||||
|
legacy_tariff_traffic: 2,
|
||||||
|
};
|
||||||
|
|
||||||
$: settingsAllOpen =
|
$: settingsAllOpen =
|
||||||
visibleSettingsSections.length > 0 &&
|
visibleSettingsSections.length > 0 &&
|
||||||
settingsOpenSections.length === visibleSettingsSections.length;
|
settingsOpenSections.length === visibleSettingsSections.length;
|
||||||
@@ -220,6 +233,108 @@
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fieldGroupMeta(
|
||||||
|
id,
|
||||||
|
titleKey,
|
||||||
|
titleFallback,
|
||||||
|
descriptionKey = "",
|
||||||
|
descriptionFallback = ""
|
||||||
|
) {
|
||||||
|
return { id, titleKey, titleFallback, descriptionKey, descriptionFallback };
|
||||||
|
}
|
||||||
|
|
||||||
|
function plategaSemanticGroup(field) {
|
||||||
|
const key = String(field?.key || "");
|
||||||
|
if (PLATEGA_SBP_KEYS.has(key) || key.startsWith("PAYMENT_PLATEGA_SBP_")) {
|
||||||
|
return fieldGroupMeta(
|
||||||
|
"platega_sbp",
|
||||||
|
"settings_group_platega_sbp",
|
||||||
|
"SBP/card button",
|
||||||
|
"settings_group_platega_sbp_hint",
|
||||||
|
"Visibility, method ID, and labels for the SBP/card payment button."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (PLATEGA_CRYPTO_KEYS.has(key) || key.startsWith("PAYMENT_PLATEGA_CRYPTO_")) {
|
||||||
|
return fieldGroupMeta(
|
||||||
|
"platega_crypto",
|
||||||
|
"settings_group_platega_crypto",
|
||||||
|
"Crypto button",
|
||||||
|
"settings_group_platega_crypto_hint",
|
||||||
|
"Visibility, method ID, and labels for the crypto payment button."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (PLATEGA_LEGACY_KEYS.has(key)) {
|
||||||
|
return fieldGroupMeta(
|
||||||
|
"platega_legacy",
|
||||||
|
"settings_group_platega_legacy",
|
||||||
|
"Legacy compatibility",
|
||||||
|
"settings_group_platega_legacy_hint",
|
||||||
|
"Fallback method for old Platega callbacks and deployments."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return fieldGroupMeta(
|
||||||
|
"platega_common",
|
||||||
|
"settings_group_platega_common",
|
||||||
|
"Common settings",
|
||||||
|
"settings_group_platega_common_hint",
|
||||||
|
"Shared merchant credentials, redirects, and API endpoint."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacyTariffSemanticGroup(field) {
|
||||||
|
const key = String(field?.key || "");
|
||||||
|
if (LEGACY_TARIFF_TRAFFIC_KEYS.has(key)) {
|
||||||
|
return fieldGroupMeta(
|
||||||
|
"legacy_tariff_traffic",
|
||||||
|
"settings_group_legacy_tariff_traffic",
|
||||||
|
"Legacy traffic packages",
|
||||||
|
"settings_group_legacy_tariff_traffic_hint",
|
||||||
|
"Packages used only when the JSON tariff catalog is not configured."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return fieldGroupMeta(
|
||||||
|
"legacy_tariff_periods",
|
||||||
|
"settings_group_legacy_tariff_periods",
|
||||||
|
"Legacy subscription periods",
|
||||||
|
"settings_group_legacy_tariff_periods_hint",
|
||||||
|
"Month toggles and prices used only by the old remnawave-tg-shop mode."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function semanticFieldGroup(section, group, field) {
|
||||||
|
if (section?.id === "payments" && group?.id === "Platega") {
|
||||||
|
return plategaSemanticGroup(field);
|
||||||
|
}
|
||||||
|
if (section?.id === "pricing") {
|
||||||
|
return legacyTariffSemanticGroup(field);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function semanticFieldGroups(section, group) {
|
||||||
|
const fields = group?.fields || [];
|
||||||
|
const result = new Map();
|
||||||
|
for (const field of fields) {
|
||||||
|
const meta = semanticFieldGroup(section, group, field) || fieldGroupMeta("_default", "", "");
|
||||||
|
if (!result.has(meta.id)) {
|
||||||
|
result.set(meta.id, { ...meta, fields: [] });
|
||||||
|
}
|
||||||
|
result.get(meta.id).fields.push(field);
|
||||||
|
}
|
||||||
|
return Array.from(result.values()).sort(
|
||||||
|
(a, b) =>
|
||||||
|
(SEMANTIC_FIELD_GROUP_ORDER[a.id] || 999) - (SEMANTIC_FIELD_GROUP_ORDER[b.id] || 999)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldGroupTitle(group) {
|
||||||
|
return group.titleKey ? at(group.titleKey, {}, group.titleFallback) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldGroupDescription(group) {
|
||||||
|
return group.descriptionKey ? at(group.descriptionKey, {}, group.descriptionFallback) : "";
|
||||||
|
}
|
||||||
|
|
||||||
function adminLocaleKey(key) {
|
function adminLocaleKey(key) {
|
||||||
const raw = String(key || "");
|
const raw = String(key || "");
|
||||||
return raw.startsWith("admin_") ? raw.slice("admin_".length) : raw;
|
return raw.startsWith("admin_") ? raw.slice("admin_".length) : raw;
|
||||||
@@ -358,6 +473,35 @@
|
|||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet renderGroupedFields(section, group)}
|
||||||
|
{@const fieldGroups = semanticFieldGroups(section, group)}
|
||||||
|
{#if fieldGroups.length === 1 && !fieldGroups[0].titleKey}
|
||||||
|
{#each fieldGroups[0].fields as field}
|
||||||
|
{@render renderField(field)}
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<div class="admin-settings-field-groups">
|
||||||
|
{#each fieldGroups as fieldGroup}
|
||||||
|
<section class="admin-settings-field-group">
|
||||||
|
{#if fieldGroup.titleKey}
|
||||||
|
<header class="admin-settings-field-group-head">
|
||||||
|
<strong>{fieldGroupTitle(fieldGroup)}</strong>
|
||||||
|
{#if fieldGroupDescription(fieldGroup)}
|
||||||
|
<small>{fieldGroupDescription(fieldGroup)}</small>
|
||||||
|
{/if}
|
||||||
|
</header>
|
||||||
|
{/if}
|
||||||
|
<div class="admin-settings-field-group-body">
|
||||||
|
{#each fieldGroup.fields as field}
|
||||||
|
{@render renderField(field)}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
{#snippet renderField(field)}
|
{#snippet renderField(field)}
|
||||||
{@const revealed = isSecretRevealed(field.key)}
|
{@const revealed = isSecretRevealed(field.key)}
|
||||||
<div class="admin-setting" class:is-overridden={isOverridden(field)}>
|
<div class="admin-setting" class:is-overridden={isOverridden(field)}>
|
||||||
@@ -600,9 +744,7 @@
|
|||||||
{#if rootGroup.webhook}
|
{#if rootGroup.webhook}
|
||||||
{@render renderWebhookHint(rootGroup.webhook)}
|
{@render renderWebhookHint(rootGroup.webhook)}
|
||||||
{/if}
|
{/if}
|
||||||
{#each rootGroup.fields as field}
|
{@render renderGroupedFields(section, rootGroup)}
|
||||||
{@render renderField(field)}
|
|
||||||
{/each}
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if labelGroups.length}
|
{#if labelGroups.length}
|
||||||
<Accordion.Root
|
<Accordion.Root
|
||||||
@@ -646,9 +788,7 @@
|
|||||||
{#if group.webhook}
|
{#if group.webhook}
|
||||||
{@render renderWebhookHint(group.webhook)}
|
{@render renderWebhookHint(group.webhook)}
|
||||||
{/if}
|
{/if}
|
||||||
{#each group.fields as field}
|
{@render renderGroupedFields(section, group)}
|
||||||
{@render renderField(field)}
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
|
|||||||
@@ -1823,7 +1823,6 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--admin-text);
|
color: var(--admin-text);
|
||||||
text-transform: capitalize;
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@@ -1851,8 +1850,43 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-accordion-content[data-state="open"] {
|
||||||
|
animation: admin-accordion-open 140ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
.admin-accordion-content[data-state="closed"] {
|
.admin-accordion-content[data-state="closed"] {
|
||||||
display: none;
|
animation: admin-accordion-close 110ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes admin-accordion-open {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
height: var(--bits-accordion-content-height);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes admin-accordion-close {
|
||||||
|
from {
|
||||||
|
height: var(--bits-accordion-content-height);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.admin-accordion-content[data-state="open"],
|
||||||
|
.admin-accordion-content[data-state="closed"] {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
@@ -1996,6 +2030,50 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-settings-field-groups {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-settings-field-group {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--admin-border) 88%, transparent);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: color-mix(in srgb, var(--admin-bg) 52%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-settings-field-group-head {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
background: color-mix(in srgb, var(--admin-surface-2) 56%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-settings-field-group-head strong {
|
||||||
|
color: var(--admin-text);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 750;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-settings-field-group-head small {
|
||||||
|
color: var(--admin-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-settings-field-group-body .admin-setting {
|
||||||
|
padding-inline: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-settings-field-group-body .admin-setting:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-setting:last-child {
|
.admin-setting:last-child {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
@@ -2762,6 +2840,14 @@
|
|||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-settings-field-groups {
|
||||||
|
padding: 10px 14px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-settings-field-group-body .admin-setting {
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-card-head {
|
.admin-card-head {
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
}
|
}
|
||||||
@@ -3489,8 +3575,7 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--admin-text);
|
color: var(--admin-text);
|
||||||
text-transform: uppercase;
|
letter-spacing: 0;
|
||||||
letter-spacing: 0.06em;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-3
@@ -943,7 +943,7 @@
|
|||||||
"admin_logs_empty": "No entries",
|
"admin_logs_empty": "No entries",
|
||||||
"admin_settings_section_general": "General",
|
"admin_settings_section_general": "General",
|
||||||
"admin_settings_section_appearance": "Appearance",
|
"admin_settings_section_appearance": "Appearance",
|
||||||
"admin_settings_section_pricing": "Tariffs and prices",
|
"admin_settings_section_pricing": "Legacy tariff compatibility",
|
||||||
"admin_settings_section_payments": "Payment systems",
|
"admin_settings_section_payments": "Payment systems",
|
||||||
"admin_settings_section_trial": "Trial",
|
"admin_settings_section_trial": "Trial",
|
||||||
"admin_settings_section_referral": "Referral program",
|
"admin_settings_section_referral": "Referral program",
|
||||||
@@ -951,6 +951,7 @@
|
|||||||
"admin_settings_section_devices": "Devices",
|
"admin_settings_section_devices": "Devices",
|
||||||
"admin_settings_section_support": "Support",
|
"admin_settings_section_support": "Support",
|
||||||
"admin_settings_subsection_common": "Common",
|
"admin_settings_subsection_common": "Common",
|
||||||
|
"admin_settings_subsection_checkout": "Checkout",
|
||||||
"admin_settings_subsection_remnawave": "Remnawave",
|
"admin_settings_subsection_remnawave": "Remnawave",
|
||||||
"admin_settings_subsection_telegram_stars": "Telegram Stars",
|
"admin_settings_subsection_telegram_stars": "Telegram Stars",
|
||||||
"admin_settings_subsection_yookassa": "YooKassa",
|
"admin_settings_subsection_yookassa": "YooKassa",
|
||||||
@@ -1241,9 +1242,21 @@
|
|||||||
"admin_ads_col_status": "Status",
|
"admin_ads_col_status": "Status",
|
||||||
"admin_no_data": "No data",
|
"admin_no_data": "No data",
|
||||||
"admin_settings_hint": "Changes in the admin panel take precedence over .env. The 'Reset' button returns the value from environment variables.",
|
"admin_settings_hint": "Changes in the admin panel take precedence over .env. The 'Reset' button returns the value from environment variables.",
|
||||||
"admin_settings_legacy_tariffs_warning_title": "Legacy tariffs",
|
"admin_settings_legacy_tariffs_warning_title": "remnawave-tg-shop legacy compatibility",
|
||||||
"admin_settings_legacy_tariffs_warning_body": "These settings do not apply when tariffs are configured in the dedicated Tariffs section.",
|
"admin_settings_legacy_tariffs_warning_body": "These fields are only for the old mode without a JSON catalog. They do not apply when tariffs are configured in the dedicated Tariffs section.",
|
||||||
"admin_settings_legacy_tariffs_warning_link": "Open Tariffs",
|
"admin_settings_legacy_tariffs_warning_link": "Open Tariffs",
|
||||||
|
"admin_settings_group_platega_common": "Common settings",
|
||||||
|
"admin_settings_group_platega_common_hint": "Merchant details, API URL, and return pages shared by all Platega buttons.",
|
||||||
|
"admin_settings_group_platega_sbp": "SBP/card button",
|
||||||
|
"admin_settings_group_platega_sbp_hint": "Visibility, method ID, and labels for the card or SBP payment button.",
|
||||||
|
"admin_settings_group_platega_crypto": "Crypto button",
|
||||||
|
"admin_settings_group_platega_crypto_hint": "Visibility, method ID, and labels for the crypto payment button.",
|
||||||
|
"admin_settings_group_platega_legacy": "Legacy compatibility",
|
||||||
|
"admin_settings_group_platega_legacy_hint": "Fallback method for old Platega callbacks and deployments.",
|
||||||
|
"admin_settings_group_legacy_tariff_periods": "Legacy subscription periods",
|
||||||
|
"admin_settings_group_legacy_tariff_periods_hint": "Monthly periods and prices for compatibility with old remnawave-tg-shop mode.",
|
||||||
|
"admin_settings_group_legacy_tariff_traffic": "Legacy traffic packages",
|
||||||
|
"admin_settings_group_legacy_tariff_traffic_hint": "Traffic packages for the old mode without a JSON tariff catalog.",
|
||||||
"admin_collapse_all": "Collapse all",
|
"admin_collapse_all": "Collapse all",
|
||||||
"admin_expand_all": "Expand all",
|
"admin_expand_all": "Expand all",
|
||||||
"admin_settings_params_count": "{count} parameters",
|
"admin_settings_params_count": "{count} parameters",
|
||||||
|
|||||||
+16
-3
@@ -943,7 +943,7 @@
|
|||||||
"admin_logs_empty": "Записей нет",
|
"admin_logs_empty": "Записей нет",
|
||||||
"admin_settings_section_general": "Общие",
|
"admin_settings_section_general": "Общие",
|
||||||
"admin_settings_section_appearance": "Внешний вид",
|
"admin_settings_section_appearance": "Внешний вид",
|
||||||
"admin_settings_section_pricing": "Тарифы и цены",
|
"admin_settings_section_pricing": "Совместимость с legacy-тарифами",
|
||||||
"admin_settings_section_payments": "Платёжные системы",
|
"admin_settings_section_payments": "Платёжные системы",
|
||||||
"admin_settings_section_trial": "Триал",
|
"admin_settings_section_trial": "Триал",
|
||||||
"admin_settings_section_referral": "Реферальная программа",
|
"admin_settings_section_referral": "Реферальная программа",
|
||||||
@@ -951,6 +951,7 @@
|
|||||||
"admin_settings_section_devices": "Устройства",
|
"admin_settings_section_devices": "Устройства",
|
||||||
"admin_settings_section_support": "Поддержка",
|
"admin_settings_section_support": "Поддержка",
|
||||||
"admin_settings_subsection_common": "Общие",
|
"admin_settings_subsection_common": "Общие",
|
||||||
|
"admin_settings_subsection_checkout": "Оформление оплаты",
|
||||||
"admin_settings_subsection_remnawave": "Remnawave",
|
"admin_settings_subsection_remnawave": "Remnawave",
|
||||||
"admin_settings_subsection_telegram_stars": "Telegram Stars",
|
"admin_settings_subsection_telegram_stars": "Telegram Stars",
|
||||||
"admin_settings_subsection_yookassa": "YooKassa",
|
"admin_settings_subsection_yookassa": "YooKassa",
|
||||||
@@ -1241,9 +1242,21 @@
|
|||||||
"admin_ads_col_status": "Статус",
|
"admin_ads_col_status": "Статус",
|
||||||
"admin_no_data": "Нет данных",
|
"admin_no_data": "Нет данных",
|
||||||
"admin_settings_hint": "Изменения в админке имеют приоритет над .env. Кнопка «Сбросить» возвращает значение из переменных окружения.",
|
"admin_settings_hint": "Изменения в админке имеют приоритет над .env. Кнопка «Сбросить» возвращает значение из переменных окружения.",
|
||||||
"admin_settings_legacy_tariffs_warning_title": "Legacy-тарифы",
|
"admin_settings_legacy_tariffs_warning_title": "Совместимость с remnawave-tg-shop legacy",
|
||||||
"admin_settings_legacy_tariffs_warning_body": "Эти настройки не применяются, если тарифы настроены через отдельный раздел «Тарифы».",
|
"admin_settings_legacy_tariffs_warning_body": "Эти поля нужны только для старого режима без JSON-каталога. Если тарифы настроены через отдельный раздел «Тарифы», они не применяются.",
|
||||||
"admin_settings_legacy_tariffs_warning_link": "Открыть тарифы",
|
"admin_settings_legacy_tariffs_warning_link": "Открыть тарифы",
|
||||||
|
"admin_settings_group_platega_common": "Общие настройки",
|
||||||
|
"admin_settings_group_platega_common_hint": "Данные мерчанта, URL API и страницы возврата для всех кнопок Platega.",
|
||||||
|
"admin_settings_group_platega_sbp": "Кнопка SBP/карта",
|
||||||
|
"admin_settings_group_platega_sbp_hint": "Включение, method ID и тексты кнопки для оплаты картой или через SBP.",
|
||||||
|
"admin_settings_group_platega_crypto": "Кнопка Crypto",
|
||||||
|
"admin_settings_group_platega_crypto_hint": "Включение, method ID и тексты кнопки для криптооплаты.",
|
||||||
|
"admin_settings_group_platega_legacy": "Legacy-совместимость",
|
||||||
|
"admin_settings_group_platega_legacy_hint": "Fallback-метод для старых callback и старых установок Platega.",
|
||||||
|
"admin_settings_group_legacy_tariff_periods": "Legacy-подписки на срок",
|
||||||
|
"admin_settings_group_legacy_tariff_periods_hint": "Месячные периоды и цены для режима совместимости со старым remnawave-tg-shop.",
|
||||||
|
"admin_settings_group_legacy_tariff_traffic": "Legacy-пакеты трафика",
|
||||||
|
"admin_settings_group_legacy_tariff_traffic_hint": "Пакеты трафика для старого режима без JSON-каталога тарифов.",
|
||||||
"admin_collapse_all": "Свернуть всё",
|
"admin_collapse_all": "Свернуть всё",
|
||||||
"admin_expand_all": "Развернуть всё",
|
"admin_expand_all": "Развернуть всё",
|
||||||
"admin_settings_params_count": "{count} параметров",
|
"admin_settings_params_count": "{count} параметров",
|
||||||
|
|||||||
Reference in New Issue
Block a user