import json import sys from pathlib import Path from types import SimpleNamespace REPO_ROOT = Path(__file__).resolve().parents[2] BACKEND_ROOT = REPO_ROOT / "backend" sys.path.insert(0, str(BACKEND_ROOT)) if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") from bot.services.email_templates import ( # noqa: E402 render_account_merged, render_login_code, render_payment_success, render_subscription_expiring, render_subscription_lifecycle_notification, render_support_admin_reply_user, render_support_new_ticket_admin, render_support_ticket_closed_user, render_support_user_reply_admin, render_user_notification, ) LANGUAGE = "ru" class PreviewI18n: def __init__(self, path: Path, default: str = "ru"): self.default_lang = default self.locales_data = {} for item in path.glob("*.json"): try: data = json.loads(item.read_text(encoding="utf-8")) except json.JSONDecodeError: continue if isinstance(data, dict): self.locales_data[item.stem] = { str(key): str(value) for key, value in data.items() if isinstance(value, str) } def gettext(self, lang_code: str | None, key: str, **kwargs) -> str: requested = str(lang_code or "").strip().lower().replace("_", "-") requested_base = requested.split("-", 1)[0] if requested in self.locales_data: messages = self.locales_data[requested] elif requested_base in self.locales_data: messages = self.locales_data[requested_base] else: messages = self.locales_data.get(self.default_lang) or self.locales_data.get("en", {}) template = messages.get(key) if template is None and self.default_lang in self.locales_data: template = self.locales_data[self.default_lang].get(key) if template is None: template = key try: return template.format(**kwargs) if kwargs else template except Exception: return template def settings(): return SimpleNamespace( DEFAULT_LANGUAGE=LANGUAGE, EMAIL_CODE_TTL_SECONDS=600, WEBAPP_LOGO_URL="", WEBAPP_PRIMARY_COLOR="#00fe7a", WEBAPP_TITLE="remnawave-minishop", ) I18N = PreviewI18n(REPO_ROOT / "locales", default=LANGUAGE) SETTINGS = settings() SAMPLE = { "amount": 390, "code": "483921", "currency": "RUB", "dashboard_url": "https://mini.example.com/app", "end_date": "21.06.2026, 18:00", "magic_url": "https://mini.example.com/app/auth/magic/preview", "premium_traffic": 25, "regular_traffic": 100, "ticket_url": "https://mini.example.com/app/support/42", } def t(key: str, **kwargs) -> str: return I18N.gettext(LANGUAGE, key, **kwargs) def preview(item_id: str, category: str, title: str, content): return { "id": item_id, "category": category, "title": title, "subject": content.subject, "html": content.html, } def payment_preview( item_id: str, title: str, sale_mode: str, *, months: int = 0, traffic_gb: float | None = None, ): return preview( item_id, "Платежи", title, render_payment_success( SETTINGS, language_code=LANGUAGE, sale_mode=sale_mode, months=months, traffic_gb=traffic_gb, amount=SAMPLE["amount"], currency=SAMPLE["currency"], end_date_text=SAMPLE["end_date"], dashboard_url=SAMPLE["dashboard_url"], provider_label="YooKassa", i18n=I18N, ), ) def user_notification_preview( item_id: str, title: str, subject_key: str, message_text: str, *, cta_label_key: str = "email_user_notification_cta", ): subject = t(subject_key) return preview( item_id, "Уведомления", title, render_user_notification( SETTINGS, language_code=LANGUAGE, subject=subject, heading=subject, intro=t("email_user_notification_intro"), message_text=message_text, dashboard_url=SAMPLE["dashboard_url"], cta_label=t(cta_label_key), i18n=I18N, ), ) def expiring_preview(item_id: str, title: str, days_left: int): return preview( item_id, "Подписка", title, render_subscription_expiring( SETTINGS, language_code=LANGUAGE, days_left=days_left, end_date_text=SAMPLE["end_date"], dashboard_url=SAMPLE["dashboard_url"], i18n=I18N, ), ) def lifecycle_preview( item_id: str, title: str, notification_key: str, message_text: str, *, mirrored_from_telegram: bool = False, days_left: int | None = None, hours_before: int | None = None, ): return preview( item_id, "Подписка", title, render_subscription_lifecycle_notification( SETTINGS, language_code=LANGUAGE, notification_key=notification_key, message_text=message_text, end_date_text=SAMPLE["end_date"], dashboard_url=SAMPLE["dashboard_url"], mirrored_from_telegram=mirrored_from_telegram, days_left=days_left, hours_before=hours_before, i18n=I18N, ), ) def support_snapshot_rows(): return [ ("email_support_row_tariff", "Premium"), ("email_support_row_remaining", "3 д. 4 ч."), ] EMAIL_PREVIEWS = [ preview( "login-code", "Доступ", "Код для входа", render_login_code( SETTINGS, code=SAMPLE["code"], language_code=LANGUAGE, magic_link=SAMPLE["magic_url"], purpose="login", i18n=I18N, ), ), preview( "set-password-code", "Доступ", "Код для создания пароля", render_login_code( SETTINGS, code=SAMPLE["code"], language_code=LANGUAGE, purpose="set_password", i18n=I18N, ), ), preview( "account-merged", "Аккаунт", "Аккаунты объединены", render_account_merged( SETTINGS, language_code=LANGUAGE, primary_user_id=100200300, removed_user_id=-42, final_end_date_text=SAMPLE["end_date"], i18n=I18N, ), ), payment_preview( "payment-subscription", "Оплата подписки", "subscription", months=1, ), payment_preview( "payment-traffic", "Покупка трафика", "traffic", traffic_gb=SAMPLE["regular_traffic"], ), payment_preview( "payment-premium-traffic", "Покупка premium-трафика", "premium_topup", traffic_gb=SAMPLE["premium_traffic"], ), payment_preview("payment-hwid", "Покупка HWID-устройств", "hwid_device", months=2), payment_preview("payment-tariff-upgrade", "Платное повышение тарифа", "tariff_upgrade"), user_notification_preview( "payment-failed", "Неуспешная оплата", "email_payment_failed_subject", "Платеж не был завершен. Можно попробовать еще раз из личного кабинета.", ), user_notification_preview( "payment-method-bound", "Способ оплаты привязан", "email_payment_method_bound_subject", "Автопродление подключено, следующий платеж пройдет автоматически.", ), user_notification_preview( "referral-bonus", "Реферальный бонус", "email_referral_bonus_subject", "Друг активировал подписку, и бонусные дни уже добавлены к вашему аккаунту.", ), user_notification_preview( "trial-traffic-depleted", "Трафик пробного периода закончился", "email_trial_traffic_depleted_subject", "Пробный трафик израсходован. Оформите подписку, чтобы продолжить пользоваться сервисом.", ), user_notification_preview( "regular-traffic-almost", "Обычный трафик почти закончился", "email_traffic_warning_regular_almost_subject", "Использовано больше 85% трафика тарифа. Можно докупить пакет заранее.", cta_label_key="email_traffic_warning_regular_cta", ), user_notification_preview( "regular-traffic-depleted", "Обычный трафик закончился", "email_traffic_warning_regular_depleted_subject", "Трафик тарифа израсходован. Докупите пакет, чтобы восстановить доступ.", cta_label_key="email_traffic_warning_regular_cta", ), user_notification_preview( "premium-traffic-almost", "Premium-трафик почти закончился", "email_traffic_warning_premium_almost_subject", "Premium-трафика осталось мало. Можно докупить пакет до полного расхода.", cta_label_key="email_traffic_warning_premium_cta", ), user_notification_preview( "premium-traffic-depleted", "Premium-трафик закончился", "email_traffic_warning_premium_depleted_subject", ( "Premium-трафик израсходован. Докупите пакет, " "чтобы продолжить использовать premium-маршруты." ), cta_label_key="email_traffic_warning_premium_cta", ), expiring_preview( "subscription-expiring-today", "Подписка заканчивается сегодня", 0, ), expiring_preview( "subscription-expiring-tomorrow", "Подписка заканчивается завтра", 1, ), expiring_preview( "subscription-expiring-days", "Подписка скоро закончится", 3, ), lifecycle_preview( "lifecycle-before-days", "Lifecycle: осталось несколько дней", "before_days", "Подписка скоро закончится. Продлите ее заранее, чтобы доступ не прерывался.", days_left=3, ), lifecycle_preview( "lifecycle-before-hours", "Lifecycle: осталось несколько часов", "before_hours", "До окончания подписки осталось несколько часов.", hours_before=6, ), lifecycle_preview( "lifecycle-expired", "Lifecycle: подписка закончилась", "expired", "Подписка закончилась. Продлите доступ в личном кабинете.", ), lifecycle_preview( "lifecycle-expired-after", "Lifecycle: подписка закончилась вчера", "expired_24h_after", "Вчера подписка была отключена. Вы можете восстановить доступ продлением.", ), lifecycle_preview( "lifecycle-autorenew", "Lifecycle: автопродление завтра", "before_2d_autorenew", "Завтра будет выполнено автопродление подписки.", ), lifecycle_preview( "lifecycle-mirrored", "Lifecycle: копия Telegram-уведомления", "before_days", "Это письмо дублирует важное уведомление, отправленное в Telegram.", mirrored_from_telegram=True, days_left=2, ), preview( "support-new-ticket-admin", "Поддержка", "Новый тикет для администратора", render_support_new_ticket_admin( SETTINGS, I18N, LANGUAGE, ticket_id=42, user_display="alex@example.com", subject="Не работает подключение", body_preview="Пользователь не может подключиться после продления.", snapshot_rows=support_snapshot_rows(), ticket_url="https://mini.example.com/app/admin/support/42", ), ), preview( "support-user-reply-admin", "Поддержка", "Ответ пользователя для администратора", render_support_user_reply_admin( SETTINGS, I18N, LANGUAGE, ticket_id=42, user_display="alex@example.com", subject="Не работает подключение", body_preview="Проблема повторилась на телефоне и ноутбуке.", snapshot_rows=support_snapshot_rows(), ticket_url="https://mini.example.com/app/admin/support/42", ), ), preview( "support-admin-reply-user", "Поддержка", "Ответ поддержки пользователю", render_support_admin_reply_user( SETTINGS, I18N, LANGUAGE, ticket_id=42, subject="Не работает подключение", body_preview="Мы обновили конфигурацию. Попробуйте подключиться еще раз.", ticket_url=SAMPLE["ticket_url"], ), ), preview( "support-ticket-closed-user", "Поддержка", "Тикет закрыт", render_support_ticket_closed_user( SETTINGS, I18N, LANGUAGE, ticket_id=42, subject="Не работает подключение", ticket_url=SAMPLE["ticket_url"], ), ), ] print(json.dumps(EMAIL_PREVIEWS, ensure_ascii=False))