Revert "Revert "Add FAQ management and user menu support""

This commit is contained in:
Egor
2025-10-07 06:02:50 +03:00
committed by GitHub
parent f7dd3be27c
commit 7e340bc13a
12 changed files with 1960 additions and 1 deletions

View File

@@ -45,6 +45,7 @@ from app.handlers.admin import (
pricing as admin_pricing,
privacy_policy as admin_privacy_policy,
public_offer as admin_public_offer,
faq as admin_faq,
)
from app.handlers.stars_payments import register_stars_handlers
@@ -153,6 +154,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
admin_pricing.register_handlers(dp)
admin_privacy_policy.register_handlers(dp)
admin_public_offer.register_handlers(dp)
admin_faq.register_handlers(dp)
common.register_handlers(dp)
register_stars_handlers(dp)
logger.info("⭐ Зарегистрированы обработчики Telegram Stars платежей")

150
app/database/crud/faq.py Normal file
View File

@@ -0,0 +1,150 @@
import logging
from datetime import datetime
from typing import Iterable, Optional
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import FaqPage, FaqSetting
logger = logging.getLogger(__name__)
async def get_faq_setting(db: AsyncSession, language: str) -> Optional[FaqSetting]:
result = await db.execute(
select(FaqSetting).where(FaqSetting.language == language)
)
return result.scalar_one_or_none()
async def set_faq_enabled(db: AsyncSession, language: str, enabled: bool) -> FaqSetting:
setting = await get_faq_setting(db, language)
if setting:
setting.is_enabled = bool(enabled)
setting.updated_at = datetime.utcnow()
else:
setting = FaqSetting(
language=language,
is_enabled=bool(enabled),
)
db.add(setting)
await db.commit()
await db.refresh(setting)
logger.info(
"✅ Статус FAQ для языка %s обновлен: %s",
language,
"enabled" if setting.is_enabled else "disabled",
)
return setting
async def upsert_faq_setting(db: AsyncSession, language: str, enabled: bool) -> FaqSetting:
return await set_faq_enabled(db, language, enabled)
async def get_faq_pages(
db: AsyncSession,
language: str,
*,
include_inactive: bool = False,
) -> list[FaqPage]:
query = select(FaqPage).where(FaqPage.language == language)
if not include_inactive:
query = query.where(FaqPage.is_active.is_(True))
query = query.order_by(FaqPage.display_order.asc(), FaqPage.id.asc())
result = await db.execute(query)
pages = list(result.scalars().all())
return pages
async def get_faq_page_by_id(db: AsyncSession, page_id: int) -> Optional[FaqPage]:
result = await db.execute(select(FaqPage).where(FaqPage.id == page_id))
return result.scalar_one_or_none()
async def create_faq_page(
db: AsyncSession,
*,
language: str,
title: str,
content: str,
display_order: Optional[int] = None,
is_active: bool = True,
) -> FaqPage:
if display_order is None:
result = await db.execute(
select(func.max(FaqPage.display_order)).where(FaqPage.language == language)
)
max_order = result.scalar() or 0
display_order = max_order + 1
page = FaqPage(
language=language,
title=title,
content=content,
display_order=display_order,
is_active=is_active,
)
db.add(page)
await db.commit()
await db.refresh(page)
logger.info("✅ Создана страница FAQ %s для языка %s", page.id, language)
return page
async def update_faq_page(
db: AsyncSession,
page: FaqPage,
*,
title: Optional[str] = None,
content: Optional[str] = None,
display_order: Optional[int] = None,
is_active: Optional[bool] = None,
) -> FaqPage:
if title is not None:
page.title = title
if content is not None:
page.content = content
if display_order is not None:
page.display_order = display_order
if is_active is not None:
page.is_active = bool(is_active)
page.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(page)
logger.info("✅ Страница FAQ %s обновлена", page.id)
return page
async def delete_faq_page(db: AsyncSession, page_id: int) -> None:
await db.execute(delete(FaqPage).where(FaqPage.id == page_id))
await db.commit()
logger.info("🗑️ Страница FAQ %s удалена", page_id)
async def bulk_update_order(
db: AsyncSession,
pages: Iterable[tuple[int, int]],
) -> None:
for page_id, order in pages:
await db.execute(
update(FaqPage)
.where(FaqPage.id == page_id)
.values(display_order=order, updated_at=datetime.utcnow())
)
await db.commit()

View File

@@ -782,9 +782,32 @@ class PublicOffer(Base):
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class FaqSetting(Base):
__tablename__ = "faq_settings"
id = Column(Integer, primary_key=True, index=True)
language = Column(String(10), nullable=False, unique=True)
is_enabled = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class FaqPage(Base):
__tablename__ = "faq_pages"
id = Column(Integer, primary_key=True, index=True)
language = Column(String(10), nullable=False, index=True)
title = Column(String(255), nullable=False)
content = Column(Text, nullable=False)
display_order = Column(Integer, default=0, nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class SystemSetting(Base):
__tablename__ = "system_settings"
id = Column(Integer, primary_key=True, index=True)
key = Column(String(255), unique=True, nullable=False)
value = Column(Text, nullable=True)

View File

@@ -2606,6 +2606,120 @@ async def create_public_offers_table() -> bool:
return False
async def create_faq_settings_table() -> bool:
table_exists = await check_table_exists("faq_settings")
if table_exists:
logger.info(" Таблица faq_settings уже существует")
return True
try:
async with engine.begin() as conn:
db_type = await get_database_type()
if db_type == "sqlite":
create_sql = """
CREATE TABLE faq_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
language VARCHAR(10) NOT NULL UNIQUE,
is_enabled BOOLEAN NOT NULL DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
"""
elif db_type == "postgresql":
create_sql = """
CREATE TABLE faq_settings (
id SERIAL PRIMARY KEY,
language VARCHAR(10) NOT NULL UNIQUE,
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
"""
else:
create_sql = """
CREATE TABLE faq_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
language VARCHAR(10) NOT NULL UNIQUE,
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
"""
await conn.execute(text(create_sql))
logger.info("✅ Таблица faq_settings создана")
return True
except Exception as error:
logger.error(f"❌ Ошибка создания таблицы faq_settings: {error}")
return False
async def create_faq_pages_table() -> bool:
table_exists = await check_table_exists("faq_pages")
if table_exists:
logger.info(" Таблица faq_pages уже существует")
return True
try:
async with engine.begin() as conn:
db_type = await get_database_type()
if db_type == "sqlite":
create_sql = """
CREATE TABLE faq_pages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
language VARCHAR(10) NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
display_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_faq_pages_language ON faq_pages(language);
"""
elif db_type == "postgresql":
create_sql = """
CREATE TABLE faq_pages (
id SERIAL PRIMARY KEY,
language VARCHAR(10) NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
display_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_faq_pages_language ON faq_pages(language);
CREATE INDEX idx_faq_pages_order ON faq_pages(language, display_order);
"""
else:
create_sql = """
CREATE TABLE faq_pages (
id INT AUTO_INCREMENT PRIMARY KEY,
language VARCHAR(10) NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
display_order INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE INDEX idx_faq_pages_language ON faq_pages(language);
CREATE INDEX idx_faq_pages_order ON faq_pages(language, display_order);
"""
await conn.execute(text(create_sql))
logger.info("✅ Таблица faq_pages создана")
return True
except Exception as error:
logger.error(f"❌ Ошибка создания таблицы faq_pages: {error}")
return False
async def ensure_default_web_api_token() -> bool:
default_token = (settings.WEB_API_DEFAULT_TOKEN or "").strip()
if not default_token:
@@ -2702,6 +2816,20 @@ async def run_universal_migration():
else:
logger.warning("⚠️ Проблемы с таблицей public_offers")
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ FAQ_SETTINGS ===")
faq_settings_ready = await create_faq_settings_table()
if faq_settings_ready:
logger.info("✅ Таблица faq_settings готова")
else:
logger.warning("⚠️ Проблемы с таблицей faq_settings")
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ FAQ_PAGES ===")
faq_pages_ready = await create_faq_pages_table()
if faq_pages_ready:
logger.info("✅ Таблица faq_pages готова")
else:
logger.warning("⚠️ Проблемы с таблицей faq_pages")
logger.info("=== ПРОВЕРКА БАЗОВЫХ ТОКЕНОВ ВЕБ-API ===")
default_token_ready = await ensure_default_web_api_token()
if default_token_ready:

1065
app/handlers/admin/faq.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
import html
import logging
from aiogram import Dispatcher, types, F
from aiogram.filters import StateFilter
@@ -27,6 +28,7 @@ from app.utils.promo_offer import (
)
from app.services.privacy_policy_service import PrivacyPolicyService
from app.services.public_offer_service import PublicOfferService
from app.services.faq_service import FaqService
logger = logging.getLogger(__name__)
@@ -114,6 +116,7 @@ async def show_info_menu(
privacy_enabled = await PrivacyPolicyService.is_policy_enabled(db, db_user.language)
public_offer_enabled = await PublicOfferService.is_offer_enabled(db, db_user.language)
faq_enabled = await FaqService.is_enabled(db, db_user.language)
await edit_or_answer_photo(
callback=callback,
@@ -122,12 +125,180 @@ async def show_info_menu(
language=db_user.language,
show_privacy_policy=privacy_enabled,
show_public_offer=public_offer_enabled,
show_faq=faq_enabled,
),
parse_mode="HTML",
)
await callback.answer()
async def show_faq_pages(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
):
texts = get_texts(db_user.language)
pages = await FaqService.get_pages(db, db_user.language)
if not pages:
await callback.answer(
texts.t("FAQ_NOT_AVAILABLE", "FAQ временно недоступен."),
show_alert=True,
)
return
header = texts.t("FAQ_HEADER", "❓ <b>FAQ</b>")
prompt = texts.t("FAQ_PAGES_PROMPT", "Выберите вопрос:" )
caption = f"{header}\n\n{prompt}" if prompt else header
buttons: list[list[types.InlineKeyboardButton]] = []
for index, page in enumerate(pages, start=1):
raw_title = (page.title or "").strip()
if not raw_title:
raw_title = texts.t("FAQ_PAGE_UNTITLED", "Без названия")
if len(raw_title) > 60:
raw_title = f"{raw_title[:57]}..."
buttons.append([
types.InlineKeyboardButton(
text=f"{index}. {raw_title}",
callback_data=f"menu_faq_page:{page.id}:1",
)
])
buttons.append([
types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_info")
])
await callback.message.edit_text(
caption,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=buttons),
)
await callback.answer()
async def show_faq_page(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
):
texts = get_texts(db_user.language)
raw_data = callback.data or ""
parts = raw_data.split(":")
page_id = None
requested_page = 1
if len(parts) >= 2:
try:
page_id = int(parts[1])
except ValueError:
page_id = None
if len(parts) >= 3:
try:
requested_page = int(parts[2])
except ValueError:
requested_page = 1
if not page_id:
await callback.answer()
return
page = await FaqService.get_page(db, page_id, db_user.language)
if not page or not page.is_active:
await callback.answer(
texts.t("FAQ_PAGE_NOT_AVAILABLE", "Эта страница FAQ недоступна."),
show_alert=True,
)
return
content_pages = FaqService.split_content_into_pages(page.content)
if not content_pages:
await callback.answer(
texts.t("FAQ_PAGE_EMPTY", "Текст для этой страницы ещё не добавлен."),
show_alert=True,
)
return
total_pages = len(content_pages)
current_page = max(1, min(requested_page, total_pages))
header = texts.t("FAQ_HEADER", "❓ <b>FAQ</b>")
title_template = texts.t("FAQ_PAGE_TITLE", "<b>{title}</b>")
page_title = (page.title or "").strip()
if not page_title:
page_title = texts.t("FAQ_PAGE_UNTITLED", "Без названия")
title_block = title_template.format(title=html.escape(page_title))
body = content_pages[current_page - 1]
footer_template = texts.t(
"FAQ_PAGE_FOOTER",
"Страница {current} из {total}",
)
footer = ""
if total_pages > 1 and footer_template:
try:
footer = footer_template.format(current=current_page, total=total_pages)
except Exception:
footer = f"{current_page}/{total_pages}"
parts_to_join = [header, title_block]
if body:
parts_to_join.append(body)
if footer:
parts_to_join.append(f"<code>{footer}</code>")
message_text = "\n\n".join(segment for segment in parts_to_join if segment)
keyboard_rows: list[list[types.InlineKeyboardButton]] = []
if total_pages > 1:
nav_row: list[types.InlineKeyboardButton] = []
if current_page > 1:
nav_row.append(
types.InlineKeyboardButton(
text=texts.t("PAGINATION_PREV", "⬅️"),
callback_data=f"menu_faq_page:{page.id}:{current_page - 1}",
)
)
nav_row.append(
types.InlineKeyboardButton(
text=f"{current_page}/{total_pages}",
callback_data="noop",
)
)
if current_page < total_pages:
nav_row.append(
types.InlineKeyboardButton(
text=texts.t("PAGINATION_NEXT", "➡️"),
callback_data=f"menu_faq_page:{page.id}:{current_page + 1}",
)
)
keyboard_rows.append(nav_row)
keyboard_rows.append([
types.InlineKeyboardButton(
text=texts.t("FAQ_BACK_TO_LIST", "⬅️ К списку FAQ"),
callback_data="menu_faq",
)
])
keyboard_rows.append([
types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_info")
])
await callback.message.edit_text(
message_text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows),
)
await callback.answer()
async def show_privacy_policy(
callback: types.CallbackQuery,
db_user: User,
@@ -611,6 +782,16 @@ def register_handlers(dp: Dispatcher):
F.data == "menu_info",
)
dp.callback_query.register(
show_faq_pages,
F.data == "menu_faq",
)
dp.callback_query.register(
show_faq_page,
F.data.startswith("menu_faq_page:"),
)
dp.callback_query.register(
show_privacy_policy,
F.data == "menu_privacy_policy",

View File

@@ -190,6 +190,12 @@ def get_admin_settings_submenu_keyboard(language: str = "ru") -> InlineKeyboardM
callback_data="admin_public_offer",
)
],
[
InlineKeyboardButton(
text=_t(texts, "ADMIN_SETTINGS_FAQ", "❓ FAQ"),
callback_data="admin_faq",
)
],
[
InlineKeyboardButton(text=texts.BACK, callback_data="admin_panel")
]

View File

@@ -284,11 +284,20 @@ def get_info_menu_keyboard(
language: str = DEFAULT_LANGUAGE,
show_privacy_policy: bool = False,
show_public_offer: bool = False,
show_faq: bool = False,
) -> InlineKeyboardMarkup:
texts = get_texts(language)
buttons: List[List[InlineKeyboardButton]] = []
if show_faq:
buttons.append([
InlineKeyboardButton(
text=texts.t("MENU_FAQ", "❓ FAQ"),
callback_data="menu_faq",
)
])
if show_privacy_policy:
buttons.append([
InlineKeyboardButton(

269
app/services/faq_service.py Normal file
View File

@@ -0,0 +1,269 @@
import logging
from typing import List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.crud.faq import (
bulk_update_order,
create_faq_page,
delete_faq_page,
get_faq_page_by_id,
get_faq_pages,
get_faq_setting,
set_faq_enabled,
update_faq_page,
)
from app.database.models import FaqPage, FaqSetting
logger = logging.getLogger(__name__)
class FaqService:
MAX_PAGE_LENGTH = 3500
@staticmethod
def _normalize_language(language: str) -> str:
base_language = language or settings.DEFAULT_LANGUAGE or "ru"
return base_language.split("-")[0].lower()
@staticmethod
def normalize_language(language: str) -> str:
return FaqService._normalize_language(language)
@classmethod
async def get_setting(
cls,
db: AsyncSession,
language: str,
*,
fallback: bool = True,
) -> Optional[FaqSetting]:
lang = cls._normalize_language(language)
setting = await get_faq_setting(db, lang)
if setting or not fallback:
return setting
default_lang = cls._normalize_language(settings.DEFAULT_LANGUAGE)
if lang != default_lang:
return await get_faq_setting(db, default_lang)
return setting
@classmethod
async def is_enabled(cls, db: AsyncSession, language: str) -> bool:
pages = await cls.get_pages(db, language)
return bool(pages)
@classmethod
async def set_enabled(
cls,
db: AsyncSession,
language: str,
enabled: bool,
) -> FaqSetting:
lang = cls._normalize_language(language)
return await set_faq_enabled(db, lang, enabled)
@classmethod
async def toggle_enabled(
cls,
db: AsyncSession,
language: str,
) -> FaqSetting:
lang = cls._normalize_language(language)
setting = await get_faq_setting(db, lang)
new_status = True
if setting:
new_status = not setting.is_enabled
return await set_faq_enabled(db, lang, new_status)
@classmethod
async def get_pages(
cls,
db: AsyncSession,
language: str,
*,
include_inactive: bool = False,
fallback: bool = True,
) -> List[FaqPage]:
lang = cls._normalize_language(language)
pages = await get_faq_pages(db, lang, include_inactive=include_inactive)
if pages:
setting = await get_faq_setting(db, lang)
if setting and not setting.is_enabled and not include_inactive:
return []
return pages
if not fallback:
return []
default_lang = cls._normalize_language(settings.DEFAULT_LANGUAGE)
if lang == default_lang:
return []
fallback_pages = await get_faq_pages(
db,
default_lang,
include_inactive=include_inactive,
)
if not fallback_pages:
return []
setting = await get_faq_setting(db, default_lang)
if setting and not setting.is_enabled and not include_inactive:
return []
return fallback_pages
@classmethod
async def get_page(
cls,
db: AsyncSession,
page_id: int,
language: str,
*,
fallback: bool = True,
include_inactive: bool = False,
) -> Optional[FaqPage]:
page = await get_faq_page_by_id(db, page_id)
if not page:
return None
lang = cls._normalize_language(language)
default_lang = cls._normalize_language(settings.DEFAULT_LANGUAGE)
if not include_inactive and not page.is_active:
return None
if page.language == lang:
return page
if fallback and page.language == default_lang:
return page
return None
@classmethod
async def create_page(
cls,
db: AsyncSession,
*,
language: str,
title: str,
content: str,
) -> FaqPage:
lang = cls._normalize_language(language)
page = await create_faq_page(
db,
language=lang,
title=title,
content=content,
is_active=True,
)
setting = await get_faq_setting(db, lang)
if not setting:
await set_faq_enabled(db, lang, True)
return page
@classmethod
async def update_page(
cls,
db: AsyncSession,
page: FaqPage,
*,
title: Optional[str] = None,
content: Optional[str] = None,
display_order: Optional[int] = None,
is_active: Optional[bool] = None,
) -> FaqPage:
return await update_faq_page(
db,
page,
title=title,
content=content,
display_order=display_order,
is_active=is_active,
)
@classmethod
async def delete_page(cls, db: AsyncSession, page_id: int) -> None:
await delete_faq_page(db, page_id)
@classmethod
async def reorder_pages(
cls,
db: AsyncSession,
language: str,
pages: List[FaqPage],
) -> None:
lang = cls._normalize_language(language)
ordered = [page for page in pages if page.language == lang]
payload = [(page.id, index + 1) for index, page in enumerate(ordered)]
await bulk_update_order(db, payload)
@staticmethod
def split_content_into_pages(
content: str,
*,
max_length: Optional[int] = None,
) -> List[str]:
if not content:
return []
normalized = content.replace("\r\n", "\n").strip()
if not normalized:
return []
limit = max_length or FaqService.MAX_PAGE_LENGTH
if len(normalized) <= limit:
return [normalized]
paragraphs = [
paragraph.strip()
for paragraph in normalized.split("\n\n")
if paragraph.strip()
]
pages: List[str] = []
current = ""
def flush_current() -> None:
nonlocal current
if current:
pages.append(current.strip())
current = ""
for paragraph in paragraphs:
candidate = f"{current}\n\n{paragraph}".strip() if current else paragraph
if len(candidate) <= limit:
current = candidate
continue
flush_current()
if len(paragraph) <= limit:
current = paragraph
continue
start = 0
while start < len(paragraph):
chunk = paragraph[start : start + limit]
pages.append(chunk.strip())
start += limit
current = ""
flush_current()
if not pages:
return [normalized[:limit]]
return pages

View File

@@ -89,6 +89,10 @@ class AdminStates(StatesGroup):
editing_rules_page = State()
editing_privacy_policy = State()
editing_public_offer = State()
creating_faq_title = State()
creating_faq_content = State()
editing_faq_title = State()
editing_faq_content = State()
editing_notification_value = State()
confirming_sync = State()

View File

@@ -333,6 +333,16 @@
"MENU_INFO": " Info",
"MENU_INFO_HEADER": " <b>Info</b>",
"MENU_INFO_PROMPT": "Choose a section:",
"MENU_FAQ": "❓ FAQ",
"FAQ_HEADER": "❓ <b>FAQ</b>",
"FAQ_PAGES_PROMPT": "Select a topic:",
"FAQ_NOT_AVAILABLE": "FAQ is temporarily unavailable.",
"FAQ_PAGE_UNTITLED": "Untitled",
"FAQ_PAGE_NOT_AVAILABLE": "This FAQ page is not available.",
"FAQ_PAGE_EMPTY": "Content for this page has not been added yet.",
"FAQ_PAGE_TITLE": "<b>{title}</b>",
"FAQ_PAGE_FOOTER": "Page {current} of {total}",
"FAQ_BACK_TO_LIST": "⬅️ Back to FAQ list",
"MENU_PRIVACY_POLICY": "🛡️ Privacy policy",
"PRIVACY_POLICY_HEADER": "🛡️ <b>Privacy policy</b>",
"PRIVACY_POLICY_NOT_AVAILABLE": "The privacy policy is temporarily unavailable.",
@@ -1059,6 +1069,7 @@
"PUBLIC_OFFER_HEADER": "📄 <b>Public Offer</b>",
"PUBLIC_OFFER_PAGE_INFO": "Page {current} of {total}",
"ADMIN_SETTINGS_PUBLIC_OFFER": "📄 Public offer",
"ADMIN_SETTINGS_FAQ": "❓ FAQ",
"ADMIN_PUBLIC_OFFER_HEADER": "📄 <b>Public offer</b>",
"ADMIN_PUBLIC_OFFER_DESCRIPTION": "The public offer is shown in the “Info” section.",
"ADMIN_PUBLIC_OFFER_LANGUAGE": "Language: <code>{lang}</code>",
@@ -1074,6 +1085,56 @@
"ADMIN_PUBLIC_OFFER_ENABLE_BUTTON": "✅ Enable display",
"ADMIN_PUBLIC_OFFER_DISABLE_BUTTON": "🚫 Disable display",
"ADMIN_PUBLIC_OFFER_HTML_HELP": " HTML help",
"ADMIN_FAQ_HEADER": "❓ <b>FAQ</b>",
"ADMIN_FAQ_DESCRIPTION": "FAQ is shown in the “Info” section.",
"ADMIN_FAQ_LANGUAGE": "Language: <code>{lang}</code>",
"ADMIN_FAQ_PAGE_STATS": "Total pages: {total}",
"ADMIN_FAQ_STATUS_DISABLED": "⚠️ FAQ display is turned off.",
"ADMIN_FAQ_STATUS_ENABLED": "✅ FAQ is enabled. Active pages: {count}.",
"ADMIN_FAQ_STATUS_ENABLED_EMPTY": "⚠️ FAQ is enabled but there are no active pages.",
"ADMIN_FAQ_STATUS_EMPTY": "⚠️ FAQ has not been configured yet.",
"ADMIN_FAQ_PAGES_EMPTY": "No pages have been created yet.",
"ADMIN_FAQ_PAGES_OVERVIEW": "<b>Page list:</b>\n{items}",
"ADMIN_FAQ_ACTION_PROMPT": "Choose an action:",
"ADMIN_FAQ_ADD_PAGE_BUTTON": " Add page",
"ADMIN_FAQ_ENABLE_BUTTON": "✅ Enable display",
"ADMIN_FAQ_DISABLE_BUTTON": "🚫 Disable display",
"ADMIN_FAQ_HTML_HELP": " HTML help",
"ADMIN_FAQ_ENTER_TITLE": "Enter a title for the new FAQ page:",
"ADMIN_FAQ_CANCEL_BUTTON": "⬅️ Cancel",
"ADMIN_FAQ_TITLE_EMPTY": "❌ Title cannot be empty.",
"ADMIN_FAQ_TITLE_TOO_LONG": "❌ Title is too long. Maximum 255 characters.",
"ADMIN_FAQ_ENTER_CONTENT": "Send the content of the FAQ page. HTML is allowed.",
"ADMIN_FAQ_CONTENT_TOO_LONG": "❌ Text is too long. Maximum 6000 characters.",
"ADMIN_FAQ_CONTENT_EMPTY": "❌ Text cannot be empty.",
"ADMIN_FAQ_HTML_ERROR": "❌ HTML error: {error}",
"ADMIN_FAQ_PAGE_CREATED": "✅ FAQ page created.",
"ADMIN_FAQ_BACK_TO_LIST": "⬅️ Back to FAQ settings",
"ADMIN_FAQ_ENABLED_ALERT": "✅ FAQ enabled.",
"ADMIN_FAQ_DISABLED_ALERT": "🚫 FAQ disabled.",
"ADMIN_FAQ_PAGE_NOT_FOUND": "⚠️ Page not found.",
"ADMIN_FAQ_PAGE_HEADER": "📄 <b>FAQ page</b>",
"ADMIN_FAQ_PAGE_STATUS_ACTIVE": "✅ Active",
"ADMIN_FAQ_PAGE_STATUS_INACTIVE": "🚫 Disabled",
"ADMIN_FAQ_PAGE_UPDATED": "Updated: {timestamp}",
"ADMIN_FAQ_PAGE_PREVIEW_EMPTY": "Content has not been provided yet.",
"ADMIN_FAQ_PAGE_PREVIEW": "<b>Preview:</b>\n{content}",
"ADMIN_FAQ_PAGE_TITLE": "<b>Title:</b> {title}",
"ADMIN_FAQ_PAGE_STATUS": "Status: {status}",
"ADMIN_FAQ_EDIT_TITLE_BUTTON": "✏️ Edit title",
"ADMIN_FAQ_EDIT_CONTENT_BUTTON": "📝 Edit text",
"ADMIN_FAQ_PAGE_ENABLE_BUTTON": "✅ Enable page",
"ADMIN_FAQ_PAGE_DISABLE_BUTTON": "🚫 Disable page",
"ADMIN_FAQ_PAGE_MOVE_UP": "⬆️ Up",
"ADMIN_FAQ_PAGE_MOVE_DOWN": "⬇️ Down",
"ADMIN_FAQ_PAGE_DELETE_BUTTON": "🗑️ Delete",
"ADMIN_FAQ_TITLE_UPDATED": "✅ Title updated.",
"ADMIN_FAQ_UNEXPECTED_STATE": "⚠️ State was reset.",
"ADMIN_FAQ_CONTENT_UPDATED": "✅ Page text updated.",
"ADMIN_FAQ_PAGE_ENABLED_ALERT": "✅ Page enabled.",
"ADMIN_FAQ_PAGE_DISABLED_ALERT": "🚫 Page disabled.",
"ADMIN_FAQ_PAGE_DELETED": "🗑️ Page deleted.",
"ADMIN_FAQ_PAGE_REORDERED": "✅ Order updated.",
"ADMIN_PUBLIC_OFFER_CURRENT_PREVIEW": "Current text (preview):",
"ADMIN_PUBLIC_OFFER_EDIT_PROMPT": "Send a new public offer text. HTML markup is allowed.",
"ADMIN_PUBLIC_OFFER_EDIT_HINT": "Use /html_help for the list of allowed tags.",

View File

@@ -246,6 +246,16 @@
"MENU_INFO": " Инфо",
"MENU_INFO_HEADER": " <b>Инфо</b>",
"MENU_INFO_PROMPT": "Выберите раздел:",
"MENU_FAQ": "❓ FAQ",
"FAQ_HEADER": "❓ <b>FAQ</b>",
"FAQ_PAGES_PROMPT": "Выберите вопрос:",
"FAQ_NOT_AVAILABLE": "FAQ временно недоступен.",
"FAQ_PAGE_UNTITLED": "Без названия",
"FAQ_PAGE_NOT_AVAILABLE": "Эта страница FAQ недоступна.",
"FAQ_PAGE_EMPTY": "Текст для этой страницы ещё не добавлен.",
"FAQ_PAGE_TITLE": "<b>{title}</b>",
"FAQ_PAGE_FOOTER": "Страница {current} из {total}",
"FAQ_BACK_TO_LIST": "⬅️ К списку FAQ",
"MENU_LANGUAGE": "🌐 Язык",
"MENU_PROMOCODE": "🎫 Промокод",
"MENU_REFERRALS": "🤝 Партнерка",
@@ -1023,6 +1033,7 @@
"PUBLIC_OFFER_HEADER": "📄 <b>Публичная оферта</b>",
"PUBLIC_OFFER_PAGE_INFO": "Страница {current} из {total}",
"ADMIN_SETTINGS_PUBLIC_OFFER": "📄 Публичная оферта",
"ADMIN_SETTINGS_FAQ": "❓ FAQ",
"ADMIN_PUBLIC_OFFER_HEADER": "📄 <b>Публичная оферта</b>",
"ADMIN_PUBLIC_OFFER_DESCRIPTION": "Публичная оферта отображается в разделе «Инфо».",
"ADMIN_PUBLIC_OFFER_LANGUAGE": "Язык: <code>{lang}</code>",
@@ -1038,6 +1049,56 @@
"ADMIN_PUBLIC_OFFER_ENABLE_BUTTON": "✅ Включить показ",
"ADMIN_PUBLIC_OFFER_DISABLE_BUTTON": "🚫 Отключить показ",
"ADMIN_PUBLIC_OFFER_HTML_HELP": " HTML помощь",
"ADMIN_FAQ_HEADER": "❓ <b>FAQ</b>",
"ADMIN_FAQ_DESCRIPTION": "FAQ отображается в разделе «Инфо».",
"ADMIN_FAQ_LANGUAGE": "Язык: <code>{lang}</code>",
"ADMIN_FAQ_PAGE_STATS": "Всего страниц: {total}",
"ADMIN_FAQ_STATUS_DISABLED": "⚠️ Показ FAQ выключен.",
"ADMIN_FAQ_STATUS_ENABLED": "✅ FAQ включён. Активных страниц: {count}.",
"ADMIN_FAQ_STATUS_ENABLED_EMPTY": "⚠️ FAQ включён, но нет активных страниц.",
"ADMIN_FAQ_STATUS_EMPTY": "⚠️ FAQ ещё не настроен.",
"ADMIN_FAQ_PAGES_EMPTY": "Страницы ещё не созданы.",
"ADMIN_FAQ_PAGES_OVERVIEW": "<b>Список страниц:</b>\n{items}",
"ADMIN_FAQ_ACTION_PROMPT": "Выберите действие:",
"ADMIN_FAQ_ADD_PAGE_BUTTON": " Добавить страницу",
"ADMIN_FAQ_ENABLE_BUTTON": "✅ Включить показ",
"ADMIN_FAQ_DISABLE_BUTTON": "🚫 Отключить показ",
"ADMIN_FAQ_HTML_HELP": " HTML помощь",
"ADMIN_FAQ_ENTER_TITLE": "Введите заголовок для новой страницы FAQ:",
"ADMIN_FAQ_CANCEL_BUTTON": "⬅️ Отмена",
"ADMIN_FAQ_TITLE_EMPTY": "❌ Заголовок не может быть пустым.",
"ADMIN_FAQ_TITLE_TOO_LONG": "❌ Заголовок слишком длинный. Максимум 255 символов.",
"ADMIN_FAQ_ENTER_CONTENT": "Отправьте содержимое страницы FAQ. Допускается HTML.",
"ADMIN_FAQ_CONTENT_TOO_LONG": "❌ Текст слишком длинный. Максимум 6000 символов.",
"ADMIN_FAQ_CONTENT_EMPTY": "❌ Текст не может быть пустым.",
"ADMIN_FAQ_HTML_ERROR": "❌ Ошибка в HTML: {error}",
"ADMIN_FAQ_PAGE_CREATED": "✅ Страница FAQ создана.",
"ADMIN_FAQ_BACK_TO_LIST": "⬅️ К настройкам FAQ",
"ADMIN_FAQ_ENABLED_ALERT": "✅ FAQ включён.",
"ADMIN_FAQ_DISABLED_ALERT": "🚫 FAQ отключён.",
"ADMIN_FAQ_PAGE_NOT_FOUND": "⚠️ Страница не найдена.",
"ADMIN_FAQ_PAGE_HEADER": "📄 <b>Страница FAQ</b>",
"ADMIN_FAQ_PAGE_STATUS_ACTIVE": "✅ Активна",
"ADMIN_FAQ_PAGE_STATUS_INACTIVE": "🚫 Выключена",
"ADMIN_FAQ_PAGE_UPDATED": "Обновлено: {timestamp}",
"ADMIN_FAQ_PAGE_PREVIEW_EMPTY": "Текст ещё не задан.",
"ADMIN_FAQ_PAGE_PREVIEW": "<b>Превью:</b>\n{content}",
"ADMIN_FAQ_PAGE_TITLE": "<b>Заголовок:</b> {title}",
"ADMIN_FAQ_PAGE_STATUS": "Статус: {status}",
"ADMIN_FAQ_EDIT_TITLE_BUTTON": "✏️ Изменить заголовок",
"ADMIN_FAQ_EDIT_CONTENT_BUTTON": "📝 Изменить текст",
"ADMIN_FAQ_PAGE_ENABLE_BUTTON": "✅ Включить страницу",
"ADMIN_FAQ_PAGE_DISABLE_BUTTON": "🚫 Выключить страницу",
"ADMIN_FAQ_PAGE_MOVE_UP": "⬆️ Выше",
"ADMIN_FAQ_PAGE_MOVE_DOWN": "⬇️ Ниже",
"ADMIN_FAQ_PAGE_DELETE_BUTTON": "🗑️ Удалить",
"ADMIN_FAQ_TITLE_UPDATED": "✅ Заголовок обновлён.",
"ADMIN_FAQ_UNEXPECTED_STATE": "⚠️ Состояние сброшено.",
"ADMIN_FAQ_CONTENT_UPDATED": "✅ Текст страницы обновлён.",
"ADMIN_FAQ_PAGE_ENABLED_ALERT": "✅ Страница включена.",
"ADMIN_FAQ_PAGE_DISABLED_ALERT": "🚫 Страница выключена.",
"ADMIN_FAQ_PAGE_DELETED": "🗑️ Страница удалена.",
"ADMIN_FAQ_PAGE_REORDERED": "✅ Порядок обновлён.",
"ADMIN_PUBLIC_OFFER_CURRENT_PREVIEW": "Текущий текст (превью):",
"ADMIN_PUBLIC_OFFER_EDIT_PROMPT": "Отправьте новый текст публичной оферты. Допускается HTML-разметка.",
"ADMIN_PUBLIC_OFFER_EDIT_HINT": "Используйте /html_help для справки по тегам.",