Considering a padel center in {{ country_name_en }}? Model your investment with real market data →
diff --git a/web/src/padelnomics/content/templates/partials/market_results.html b/web/src/padelnomics/content/templates/partials/market_results.html
index eb65871..c515775 100644
--- a/web/src/padelnomics/content/templates/partials/market_results.html
+++ b/web/src/padelnomics/content/templates/partials/market_results.html
@@ -8,7 +8,7 @@
{% endif %}
{% if article.country %}
-
{{ article.country }}
+
{{ article.country | country_name(lang) }}
{% endif %}
{% if article.region %}
{{ article.region }}
diff --git a/web/src/padelnomics/core.py b/web/src/padelnomics/core.py
index 5260ec0..ccf41e5 100644
--- a/web/src/padelnomics/core.py
+++ b/web/src/padelnomics/core.py
@@ -4,13 +4,14 @@ Core infrastructure: database, config, email, and shared utilities.
import hashlib
import hmac
+import logging
import os
import random
import re
import secrets
import unicodedata
from contextvars import ContextVar
-from datetime import datetime, timedelta
+from datetime import UTC, datetime, timedelta
from functools import wraps
from pathlib import Path
@@ -88,6 +89,43 @@ class Config:
config = Config()
+
+def setup_logging() -> None:
+ """Configure root logger. Call once from each entry point (app, worker, scripts)."""
+ level_name = os.environ.get("LOG_LEVEL", "DEBUG" if config.DEBUG else "INFO")
+ level = getattr(logging, level_name.upper(), logging.INFO)
+ logging.basicConfig(
+ level=level,
+ format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+ )
+ logging.getLogger("hypercorn").setLevel(logging.WARNING)
+ logging.getLogger("hypercorn.error").setLevel(logging.WARNING)
+ logging.getLogger("hypercorn.access").setLevel(logging.WARNING)
+ logging.getLogger("asyncio").setLevel(logging.WARNING)
+ logging.getLogger("aiosqlite").setLevel(logging.WARNING)
+
+
+logger = logging.getLogger(__name__)
+
+# =============================================================================
+# Datetime helpers
+# =============================================================================
+
+
+def utcnow() -> datetime:
+ """Timezone-aware UTC now (replaces deprecated datetime.utcnow())."""
+ return datetime.now(UTC)
+
+
+def utcnow_iso() -> str:
+ """UTC now as naive ISO string for SQLite TEXT columns.
+
+ Produces YYYY-MM-DD HH:MM:SS (space separator, no +00:00 suffix) to match
+ SQLite's native datetime('now') format so lexicographic SQL comparisons work.
+ """
+ return datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
+
# =============================================================================
# Database
# =============================================================================
@@ -364,7 +402,7 @@ async def send_email(
resend_id = None
if not config.RESEND_API_KEY:
- print(f"[EMAIL] Would send to {to}: {subject}")
+ logger.info("Would send to %s: %s", to, subject)
resend_id = "dev"
else:
resend.api_key = config.RESEND_API_KEY
@@ -380,7 +418,7 @@ async def send_email(
)
resend_id = result.get("id") if isinstance(result, dict) else getattr(result, "id", None)
except Exception as e:
- print(f"[EMAIL] Error sending to {to}: {e}")
+ logger.error("Error sending to %s: %s", to, e)
return None
# Log to email_log (best-effort, never fail the send)
@@ -391,7 +429,7 @@ async def send_email(
(resend_id, sender, to, subject, email_type),
)
except Exception as e:
- print(f"[EMAIL] Failed to log email: {e}")
+ logger.error("Failed to log email: %s", e)
return resend_id
@@ -528,17 +566,18 @@ async def check_rate_limit(key: str, limit: int = None, window: int = None) -> t
"""
limit = limit or config.RATE_LIMIT_REQUESTS
window = window or config.RATE_LIMIT_WINDOW
- now = datetime.utcnow()
+ now = utcnow()
window_start = now - timedelta(seconds=window)
# Clean old entries and count recent
await execute(
- "DELETE FROM rate_limits WHERE key = ? AND timestamp < ?", (key, window_start.isoformat())
+ "DELETE FROM rate_limits WHERE key = ? AND timestamp < ?",
+ (key, window_start.strftime("%Y-%m-%d %H:%M:%S")),
)
result = await fetch_one(
"SELECT COUNT(*) as count FROM rate_limits WHERE key = ? AND timestamp > ?",
- (key, window_start.isoformat()),
+ (key, window_start.strftime("%Y-%m-%d %H:%M:%S")),
)
count = result["count"] if result else 0
@@ -552,7 +591,10 @@ async def check_rate_limit(key: str, limit: int = None, window: int = None) -> t
return False, info
# Record this request
- await execute("INSERT INTO rate_limits (key, timestamp) VALUES (?, ?)", (key, now.isoformat()))
+ await execute(
+ "INSERT INTO rate_limits (key, timestamp) VALUES (?, ?)",
+ (key, now.strftime("%Y-%m-%d %H:%M:%S")),
+ )
return True, info
@@ -628,7 +670,7 @@ async def soft_delete(table: str, id: int) -> bool:
"""Mark record as deleted."""
result = await execute(
f"UPDATE {table} SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL",
- (datetime.utcnow().isoformat(), id),
+ (utcnow_iso(), id),
)
return result > 0
@@ -647,7 +689,7 @@ async def hard_delete(table: str, id: int) -> bool:
async def purge_deleted(table: str, days: int = 30) -> int:
"""Purge records deleted more than X days ago."""
- cutoff = (datetime.utcnow() - timedelta(days=days)).isoformat()
+ cutoff = (utcnow() - timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S")
return await execute(
f"DELETE FROM {table} WHERE deleted_at IS NOT NULL AND deleted_at < ?", (cutoff,)
)
diff --git a/web/src/padelnomics/credits.py b/web/src/padelnomics/credits.py
index ac58f37..fa8c855 100644
--- a/web/src/padelnomics/credits.py
+++ b/web/src/padelnomics/credits.py
@@ -5,9 +5,7 @@ All balance mutations go through this module to keep credit_ledger (source of tr
and suppliers.credit_balance (denormalized cache) in sync within a single transaction.
"""
-from datetime import datetime
-
-from .core import execute, fetch_all, fetch_one, transaction
+from .core import execute, fetch_all, fetch_one, transaction, utcnow_iso
# Credit cost per heat tier
HEAT_CREDIT_COSTS = {"hot": 35, "warm": 20, "cool": 8}
@@ -44,7 +42,7 @@ async def add_credits(
note: str = None,
) -> int:
"""Add credits to a supplier. Returns new balance."""
- now = datetime.utcnow().isoformat()
+ now = utcnow_iso()
async with transaction() as db:
row = await db.execute_fetchall(
"SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,)
@@ -73,7 +71,7 @@ async def spend_credits(
note: str = None,
) -> int:
"""Spend credits from a supplier. Returns new balance. Raises InsufficientCredits."""
- now = datetime.utcnow().isoformat()
+ now = utcnow_iso()
async with transaction() as db:
row = await db.execute_fetchall(
"SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,)
@@ -116,7 +114,7 @@ async def unlock_lead(supplier_id: int, lead_id: int) -> dict:
raise ValueError("Lead not found")
cost = lead["credit_cost"] or compute_credit_cost(lead)
- now = datetime.utcnow().isoformat()
+ now = utcnow_iso()
async with transaction() as db:
# Check balance
@@ -180,7 +178,7 @@ async def monthly_credit_refill(supplier_id: int) -> int:
if not row or not row["monthly_credits"]:
return 0
- now = datetime.utcnow().isoformat()
+ now = utcnow_iso()
new_balance = await add_credits(
supplier_id,
row["monthly_credits"],
@@ -201,6 +199,6 @@ async def get_ledger(supplier_id: int, limit: int = 50) -> list[dict]:
FROM credit_ledger cl
LEFT JOIN lead_forwards lf ON cl.reference_id = lf.id AND cl.event_type = 'lead_unlock'
WHERE cl.supplier_id = ?
- ORDER BY cl.created_at DESC LIMIT ?""",
+ ORDER BY cl.created_at DESC, cl.id DESC LIMIT ?""",
(supplier_id, limit),
)
diff --git a/web/src/padelnomics/dashboard/routes.py b/web/src/padelnomics/dashboard/routes.py
index 89fd001..6afb375 100644
--- a/web/src/padelnomics/dashboard/routes.py
+++ b/web/src/padelnomics/dashboard/routes.py
@@ -1,13 +1,12 @@
"""
Dashboard domain: user dashboard and settings.
"""
-from datetime import datetime
from pathlib import Path
from quart import Blueprint, flash, g, redirect, render_template, request, url_for
from ..auth.routes import login_required, update_user
-from ..core import csrf_protect, fetch_one, soft_delete
+from ..core import csrf_protect, fetch_one, soft_delete, utcnow_iso
from ..i18n import get_translations
bp = Blueprint(
@@ -57,7 +56,7 @@ async def settings():
await update_user(
g.user["id"],
name=form.get("name", "").strip() or None,
- updated_at=datetime.utcnow().isoformat(),
+ updated_at=utcnow_iso(),
)
t = get_translations(g.get("lang") or "en")
await flash(t["dash_settings_saved"], "success")
diff --git a/web/src/padelnomics/directory/routes.py b/web/src/padelnomics/directory/routes.py
index f0d3f59..a7f46cd 100644
--- a/web/src/padelnomics/directory/routes.py
+++ b/web/src/padelnomics/directory/routes.py
@@ -2,13 +2,12 @@
Supplier directory: public, searchable listing of padel court suppliers.
"""
-from datetime import UTC, datetime
from pathlib import Path
from quart import Blueprint, g, make_response, redirect, render_template, request, url_for
-from ..core import csrf_protect, execute, fetch_all, fetch_one
-from ..i18n import get_translations
+from ..core import csrf_protect, execute, fetch_all, fetch_one, utcnow_iso
+from ..i18n import COUNTRY_LABELS, get_translations
bp = Blueprint(
"directory",
@@ -17,41 +16,6 @@ bp = Blueprint(
template_folder=str(Path(__file__).parent / "templates"),
)
-COUNTRY_LABELS = {
- "DE": "Germany",
- "ES": "Spain",
- "IT": "Italy",
- "FR": "France",
- "PT": "Portugal",
- "GB": "United Kingdom",
- "NL": "Netherlands",
- "BE": "Belgium",
- "SE": "Sweden",
- "DK": "Denmark",
- "FI": "Finland",
- "NO": "Norway",
- "AT": "Austria",
- "SI": "Slovenia",
- "IS": "Iceland",
- "CH": "Switzerland",
- "EE": "Estonia",
- "US": "United States",
- "CA": "Canada",
- "MX": "Mexico",
- "BR": "Brazil",
- "AR": "Argentina",
- "AE": "UAE",
- "SA": "Saudi Arabia",
- "TR": "Turkey",
- "CN": "China",
- "IN": "India",
- "SG": "Singapore",
- "ID": "Indonesia",
- "TH": "Thailand",
- "AU": "Australia",
- "ZA": "South Africa",
- "EG": "Egypt",
-}
CATEGORY_LABELS = {
"manufacturer": "Manufacturer",
@@ -89,7 +53,7 @@ async def _build_directory_query(q, country, category, region, page, per_page=24
lang = g.get("lang", "en")
cat_labels, country_labels, region_labels = get_directory_labels(lang)
- now = datetime.now(UTC).isoformat()
+ now = utcnow_iso()
params: list = []
wheres: list[str] = []
diff --git a/web/src/padelnomics/i18n.py b/web/src/padelnomics/i18n.py
index 59b2545..f6c60f9 100644
--- a/web/src/padelnomics/i18n.py
+++ b/web/src/padelnomics/i18n.py
@@ -13,6 +13,44 @@ from pathlib import Path
SUPPORTED_LANGS = {"en", "de"}
LANG_BLUEPRINTS = {"public", "planner", "directory", "content", "leads", "suppliers"}
+# 2-letter ISO country code → English name.
+# Used by the directory, article templates, and get_country_name().
+COUNTRY_LABELS: dict[str, str] = {
+ "DE": "Germany",
+ "ES": "Spain",
+ "IT": "Italy",
+ "FR": "France",
+ "PT": "Portugal",
+ "GB": "United Kingdom",
+ "NL": "Netherlands",
+ "BE": "Belgium",
+ "SE": "Sweden",
+ "DK": "Denmark",
+ "FI": "Finland",
+ "NO": "Norway",
+ "AT": "Austria",
+ "SI": "Slovenia",
+ "IS": "Iceland",
+ "CH": "Switzerland",
+ "EE": "Estonia",
+ "US": "United States",
+ "CA": "Canada",
+ "MX": "Mexico",
+ "BR": "Brazil",
+ "AR": "Argentina",
+ "AE": "UAE",
+ "SA": "Saudi Arabia",
+ "TR": "Turkey",
+ "CN": "China",
+ "IN": "India",
+ "SG": "Singapore",
+ "ID": "Indonesia",
+ "TH": "Thailand",
+ "AU": "Australia",
+ "ZA": "South Africa",
+ "EG": "Egypt",
+}
+
_LOCALES_DIR = Path(__file__).parent / "locales"
@@ -138,3 +176,30 @@ def get_calc_item_names(lang: str) -> dict[str, str]:
"""
assert lang in _CALC_ITEM_NAMES, f"Unknown lang: {lang!r}"
return _CALC_ITEM_NAMES[lang]
+
+
+# Reverse map: English country name → 2-letter code (e.g. "Germany" → "DE").
+# Built once at load time from COUNTRY_LABELS.
+_COUNTRY_CODE_BY_EN_NAME: dict[str, str] = {v: k for k, v in COUNTRY_LABELS.items()}
+
+
+def get_country_name(country_str: str, lang: str) -> str:
+ """Return the localised name for a country stored as a 2-letter code or English name.
+
+ Handles both formats stored in the DB:
+ - 2-letter ISO code: "CH" → "Schweiz" (de) / "Switzerland" (en)
+ - English name: "Switzerland" → "Schweiz" (de)
+
+ Falls back to the original string if not found in translations.
+ Used as a Jinja filter: {{ article.country | country_name(lang) }}
+ """
+ if not country_str:
+ return country_str
+ effective_lang = lang if lang in _TRANSLATIONS else "en"
+ # Accept both 2-letter code ("CH") and English name ("Switzerland")
+ upper = country_str.upper()
+ code = upper if upper in COUNTRY_LABELS else _COUNTRY_CODE_BY_EN_NAME.get(country_str, "")
+ if not code:
+ return country_str
+ key = f"dir_country_{code}"
+ return _TRANSLATIONS[effective_lang].get(key, country_str)
diff --git a/web/src/padelnomics/leads/routes.py b/web/src/padelnomics/leads/routes.py
index ba73d8c..265fc74 100644
--- a/web/src/padelnomics/leads/routes.py
+++ b/web/src/padelnomics/leads/routes.py
@@ -4,7 +4,6 @@ Leads domain: capture interest in court suppliers and financing.
import json
import secrets
-from datetime import datetime
from pathlib import Path
from quart import Blueprint, flash, g, jsonify, redirect, render_template, request, session, url_for
@@ -27,6 +26,7 @@ from ..core import (
is_disposable_email,
is_plausible_phone,
send_email,
+ utcnow_iso,
)
from ..i18n import get_translations
@@ -102,7 +102,7 @@ async def suppliers():
form.get("court_count", 0),
form.get("budget", 0),
form.get("message", ""),
- datetime.utcnow().isoformat(),
+ utcnow_iso(),
),
)
# Notify admin
@@ -147,7 +147,7 @@ async def financing():
form.get("court_count", 0),
form.get("budget", 0),
form.get("message", ""),
- datetime.utcnow().isoformat(),
+ utcnow_iso(),
),
)
await send_email(
@@ -375,7 +375,7 @@ async def quote_request():
status,
credit_cost,
secrets.token_urlsafe(16),
- datetime.utcnow().isoformat(),
+ utcnow_iso(),
),
)
@@ -520,7 +520,7 @@ async def verify_quote():
from ..credits import compute_credit_cost
credit_cost = compute_credit_cost(dict(lead))
- now = datetime.utcnow().isoformat()
+ now = utcnow_iso()
await execute(
"UPDATE lead_requests SET status = 'new', verified_at = ?, credit_cost = ? WHERE id = ?",
(now, credit_cost, lead["id"]),
diff --git a/web/src/padelnomics/locales/de.json b/web/src/padelnomics/locales/de.json
index ee73447..25c5c84 100644
--- a/web/src/padelnomics/locales/de.json
+++ b/web/src/padelnomics/locales/de.json
@@ -1,6 +1,6 @@
{
"nav_planner": "Finanzplaner",
- "nav_quotes": "Angebot erhalten",
+ "nav_quotes": "Angebote anfragen",
"nav_directory": "Anbieterverzeichnis",
"nav_markets": "Märkte",
"nav_suppliers": "Für Anbieter",
@@ -14,7 +14,7 @@
"nav_section_plan": "Planen & Entdecken",
"nav_section_suppliers": "Anbieter",
"nav_section_account": "Konto",
- "footer_tagline": "Plane, finanziere und baue dein Padel-Business.",
+ "footer_tagline": "Plane, finanziere und baue Dein Padel-Business.",
"footer_product": "Produkt",
"footer_legal": "Rechtliches",
"footer_company": "Unternehmen",
@@ -52,29 +52,29 @@
"auth_signup_have_account": "Bereits ein Konto?",
"auth_signup_signin_link": "Anmelden",
"auth_magic_title": "E-Mail prüfen",
- "auth_magic_sent_to": "Wir haben dir einen Anmeldelink geschickt an:",
- "auth_magic_instructions": "Klick auf den Link in der E-Mail, um dich anzumelden. Der Link läuft in {minutes} Minuten ab.",
+ "auth_magic_sent_to": "Wir haben Dir einen Anmeldelink geschickt an:",
+ "auth_magic_instructions": "Klick auf den Link in der E-Mail, um Dich anzumelden. Der Link läuft in {minutes} Minuten ab.",
"auth_magic_no_email": "Keine E-Mail erhalten?",
"auth_magic_check_spam": "Schau in deinen Spam-Ordner",
"auth_magic_correct_email": "Stelle sicher, dass die E-Mail-Adresse korrekt ist",
- "auth_magic_wait": "Warte eine Minute und versuche es erneut",
+ "auth_magic_wait": "Warte einen Moment und versuche es erneut",
"auth_magic_resend_btn": "Link erneut senden",
- "auth_waitlist_title": "Sei Erster beim Start deines Padel-Business",
+ "auth_waitlist_title": "Als Erster mit Deinem Padel-Business durchstarten",
"auth_waitlist_sub": "Wir bereiten die ultimative Planungsplattform für Padel-Unternehmer vor. Trag dich in die Warteliste ein für Frühzugang, exklusive Boni und priorisierten Support.",
"auth_waitlist_hint": "Du gehörst zu den Ersten, die Zugang erhalten, wenn wir launchen.",
"auth_waitlist_btn": "In Warteliste eintragen",
"auth_waitlist_confirmed_title": "Du stehst auf der Warteliste!",
"auth_waitlist_confirmed_sent_to": "Wir haben dir eine Bestätigung geschickt an:",
- "auth_waitlist_confirmed_sub": "Du gehörst zu den Ersten, die es wissen, wenn wir launchen. Wir schicken dir Frühzugang, exklusive Launch-Boni und prioriertes Onboarding.",
+ "auth_waitlist_confirmed_sub": "Du gehörst zu den Ersten, die es erfahren, wenn wir launchen. Wir schicken Dir Frühzugang, exklusive Launch-Boni und bevorzugtes Onboarding.",
"auth_waitlist_confirmed_next": "Was passiert als Nächstes?",
"auth_waitlist_confirmed_step1": "Du erhältst in Kürze eine Bestätigungs-E-Mail",
- "auth_waitlist_confirmed_step2": "Wir benachrichtigen dich, sobald wir launchen",
+ "auth_waitlist_confirmed_step2": "Wir benachrichtigen Dich, sobald wir launchen",
"auth_waitlist_confirmed_step3": "Du erhältst exklusiven Frühzugang vor dem öffentlichen Launch",
"auth_waitlist_confirmed_back": "Zurück zur Startseite",
"auth_flash_invalid_email": "Bitte gib eine gültige E-Mail-Adresse ein.",
"auth_flash_disposable_email": "Bitte verwende eine dauerhafte E-Mail-Adresse.",
"auth_flash_login_sent": "Schau in deine E-Mails für den Anmeldelink!",
- "auth_flash_account_exists": "Konto existiert bereits. Bitte melde dich an.",
+ "auth_flash_account_exists": "Konto bereits vorhanden. Bitte melde Dich an.",
"auth_flash_signup_sent": "Schau in deine E-Mails, um die Registrierung abzuschließen!",
"auth_flash_invalid_token": "Ungültiger oder abgelaufener Link.",
"auth_flash_invalid_token_detail": "Ungültiger oder abgelaufener Link. Bitte fordere einen neuen an.",
@@ -84,22 +84,22 @@
"flash_feedback_success": "Vielen Dank für dein Feedback!",
"flash_feedback_empty": "Bitte gib eine Nachricht ein.",
"flash_feedback_rate_limit": "Zu viele Anfragen. Bitte versuch es später erneut.",
- "flash_suppliers_success": "Danke! Wir verbinden dich mit verifizierten Hoflieferanten.",
- "flash_financing_success": "Danke! Wir verbinden dich mit Finanzierungspartnern.",
+ "flash_suppliers_success": "Danke! Wir vermitteln Dich an verifizierte Platz-Anbieter.",
+ "flash_financing_success": "Danke! Wir vermitteln Dich an Finanzierungspartner.",
"flash_verify_invalid": "Ungültiger Verifizierungslink.",
"flash_verify_expired": "Dieser Link ist abgelaufen oder wurde bereits verwendet. Bitte stelle eine neue Anfrage.",
"flash_verify_invalid_lead": "Dieses Angebot wurde bereits verifiziert oder existiert nicht.",
- "landing_hero_badge": "Padel-Kostenrechner & Finanzplaner",
+ "landing_hero_badge": "Padel-Finanzrechner & Businessplan-Tool",
"landing_hero_h1_1": "Plan Dein Padel-",
"landing_hero_h1_2": "Business in Minuten,",
"landing_hero_h1_3": "nicht Monaten",
- "landing_hero_btn_primary": "Jetzt planen →",
+ "landing_hero_btn_primary": "Jetzt Dein Padel-Business planen →",
"landing_hero_btn_secondary": "Anbieter durchsuchen",
"landing_hero_bullet_1": "Keine Registrierung erforderlich",
"landing_hero_bullet_2": "60+ Variablen",
"landing_hero_bullet_3": "Unbegrenzte Szenarien",
"landing_roi_title": "Schnelle Renditeschätzung",
- "landing_roi_subtitle": "Schieberegler bewegen und Projektion sehen",
+ "landing_roi_subtitle": "Schieberegler bewegen und Projektion in Echtzeit sehen",
"landing_roi_courts": "Plätze",
"landing_roi_rate": "Durchschn. Stundensatz",
"landing_roi_util": "Ziel-Auslastung",
@@ -108,7 +108,7 @@
"landing_roi_payback": "Amortisationszeit",
"landing_roi_annual_roi": "Jährlicher ROI",
"landing_roi_note": "Annahmen: Indoorhalle Mietmodell, 8 €/m² Miete, Personalkosten, 5 % Zinsen, 10-jähriges Darlehen. Amortisation und ROI basieren auf der Gesamtinvestition.",
- "landing_roi_cta": "Jetzt planen →",
+ "landing_roi_cta": "Jetzt Dein Padel-Business planen →",
"landing_journey_title": "Deine Reise",
"landing_journey_01": "Analysieren",
"landing_journey_01_badge": "Demnächst",
@@ -118,7 +118,7 @@
"landing_journey_04": "Bauen",
"landing_journey_05": "Wachsen",
"landing_journey_05_badge": "Demnächst",
- "landing_features_title": "Für ernsthafte Padel-Unternehmer entwickelt",
+ "landing_features_title": "Für ernsthafte Padel-Unternehmer gebaut",
"landing_feature_1_h3": "60+ Variablen",
"landing_feature_2_h3": "6 Analyse-Tabs",
"landing_feature_3_h3": "Indoor & Outdoor",
@@ -137,9 +137,9 @@
"landing_faq_q4": "Ist das Anbieterverzeichnis kostenlos?",
"landing_faq_q5": "Wie genau sind die Finanzprojektionen?",
"landing_seo_title": "Padel-Platz-Investitionsplanung",
- "landing_final_cta_h2": "Jetzt mit der Planung beginnen",
- "landing_final_cta_btn": "Jetzt planen →",
- "features_h1": "Alles, was du für dein Padel-Business brauchst",
+ "landing_final_cta_h2": "Jetzt mit der Planung loslegen",
+ "landing_final_cta_btn": "Jetzt Dein Padel-Business planen →",
+ "features_h1": "Alles, was Du für Dein Padel-Business brauchst",
"features_subtitle": "Professionelles Finanzmodell — vollständig kostenlos.",
"features_card_1_h2": "60+ Variablen",
"features_card_2_h2": "6 Analyse-Tabs",
@@ -154,19 +154,19 @@
"features_cta_open": "Planer öffnen",
"features_cta_signup": "Kostenloses Konto erstellen",
"about_why_h3": "Warum kostenlos?",
- "about_next_h3": "Was kommt als nächstes",
+ "about_next_h3": "Was als Nächstes kommt",
"about_cta_open": "Planer öffnen",
"about_cta_signup": "Kostenloses Konto erstellen",
"suppliers_hero_cta": "Pläne & Preise ansehen",
"suppliers_stat_plans_label": "Erstellte Geschäftspläne",
"suppliers_stat_avg_value": "Durchschn. Projektwert",
"suppliers_stat_leads_label": "Leads diesen Monat",
- "suppliers_problem_h2": "Das Problem bei der Kundengewinnung heute",
- "suppliers_problem_sub": "Die meisten Kanäle verschwenden Zeit und Budget, bevor du mit einem echten Käufer sprichst.",
+ "suppliers_problem_h2": "Das Problem bei der Neukundengewinnung heute",
+ "suppliers_problem_sub": "Die meisten Kanäle verschwenden Zeit und Budget, bevor Du mit einem einzigen echten Käufer sprichst.",
"suppliers_problem_1_h3": "Messen",
"suppliers_problem_2_h3": "Google Ads",
"suppliers_problem_3_h3": "Kaltakquise",
- "suppliers_transition": "Was wäre, wenn jeder Lead mit einem vollständigen Projektbrief und einem Finanzmodell käme?",
+ "suppliers_transition": "Was wäre, wenn jeder Lead mit einem vollständigen Projektbriefing und einem Finanzmodell käme?",
"suppliers_how_h2": "So funktioniert es",
"suppliers_how_sub": "Drei Schritte zu qualifizierten Leads.",
"suppliers_step_1_h3": "Eintrag beanspruchen",
@@ -205,8 +205,8 @@
"suppliers_boosts_sub": "Mit jedem bezahlten Plan verfügbar. Verwalte sie über Dein Dashboard.",
"suppliers_comparison_h2": "Der direkte Vergleich",
"suppliers_faq_h2": "FAQ für Anbieter",
- "suppliers_final_cta_h2": "Dein nächster Kunde erstellt gerade einen Geschäftsplan",
- "suppliers_final_cta_desc": "Er hat die Rentabilität berechnet. Er kennt sein Budget. Er sucht einen Anbieter wie dich.",
+ "suppliers_final_cta_h2": "Dein nächster Kunde erstellt gerade einen Businessplan",
+ "suppliers_final_cta_desc": "Er hat die Rentabilität berechnet. Er kennt sein Budget. Er sucht einen Anbieter wie Dich.",
"suppliers_final_cta_btn": "Pläne & Preise ansehen",
"planner_page_h2": "100 % kostenlos. Kein Haken.",
"planner_card_1_h3": "Finanzplaner",
@@ -220,7 +220,7 @@
"planner_card_2_signup_btn": "Registrieren und loslegen",
"planner_quote_cta_label": "Nächster Schritt",
"planner_quote_cta_title": "Angebote von verifizierten Anbietern einholen",
- "planner_quote_cta_desc": "Teile Deine Projektspezifikationen und wir verbinden dich mit passenden Anbietern.",
+ "planner_quote_cta_desc": "Teile Deine Projektspezifikationen und wir vermitteln Dich an passende Anbieter.",
"planner_quote_cta_check_1": "Passende Anbieter",
"planner_quote_cta_check_2": "Direktkontakt, kein Vermittler",
"planner_quote_cta_check_3": "Keine Verpflichtung",
@@ -245,12 +245,12 @@
"export_back": "← Zurück zum Planer",
"export_success_title": "Zahlung eingegangen",
"export_success_subtitle": "Dein Geschäftsplan-PDF wird generiert. Dies dauert üblicherweise weniger als eine Minute.",
- "export_success_status": "Dein PDF wird erstellt. Aktualisiere diese Seite gleich, oder überprüfe Deine E-Mail — wir senden Dir einen Download-Link, wenn es fertig ist.",
+ "export_success_status": "Dein PDF wird erstellt. Aktualisiere diese Seite gleich, oder schau in Deine E-Mails — wir senden Dir einen Download-Link, wenn es fertig ist.",
"export_success_refresh": "Status aktualisieren",
"export_success_all": "Alle Exporte anzeigen",
"export_success_planner": "Zurück zum Planer",
"export_gen_title": "Geschäftsplan wird generiert",
- "export_gen_subtitle": "Dies dauert üblicherweise weniger als eine Minute. Diese Seite wird automatisch aktualisiert.",
+ "export_gen_subtitle": "Dies dauert üblicherweise weniger als eine Minute. Diese Seite aktualisiert sich automatisch.",
"export_gen_refresh": "Jetzt aktualisieren",
"export_gen_all": "Alle Exporte anzeigen",
"export_waitlist_title": "Geschäftsplan-PDF-Export demnächst verfügbar",
@@ -264,9 +264,9 @@
"scenario_created": "Erstellt",
"dir_heading": "Padelplatz-Hersteller, Platzbauer & Anbieter",
"dir_page_title": "Padel-Platz Anbieterverzeichnis",
- "dir_page_meta_desc": "Über {count}+ Anbieter aus {countries} Ländern. Hersteller, Baufirmen, Kunstrasenproduzenten, Beleuchtung und Software. Den richtigen Partner für dein Projekt finden.",
- "dir_page_og_desc": "Über {count}+ Anbieter aus {countries} Ländern. Hersteller, Baufirmen, Kunstrasenproduzenten, Beleuchtung und Software.",
- "dir_subheading": "Über {n} Anbieter aus {c} Ländern. Hersteller, Platzbauer und schlüsselfertige Lösungen für Deinen Padelplatz.",
+ "dir_page_meta_desc": "{count}+ Anbieter aus {countries} Ländern. Hersteller, Baufirmen, Kunstrasenproduzenten, Beleuchtung und Software. Den richtigen Partner für Dein Projekt finden.",
+ "dir_page_og_desc": "{count}+ Anbieter aus {countries} Ländern. Hersteller, Baufirmen, Kunstrasenproduzenten, Beleuchtung und Software.",
+ "dir_subheading": "{n}+ Anbieter aus {c} Ländern. Hersteller, Platzbauer und schlüsselfertige Lösungen für Deine Padel-Anlage.",
"dir_stat_suppliers": "Anbieter",
"dir_stat_countries": "Länder",
"dir_stat_categories": "Kategorien",
@@ -276,7 +276,7 @@
"dir_search_btn": "Suchen",
"dir_filter_clear": "Alle löschen",
"dir_cta_heading": "Bist Du ein Padelplatz-Anbieter?",
- "dir_cta_subheading": "Eintrag erstellen und Kontakt zu planenden Unternehmern aufnehmen.",
+ "dir_cta_subheading": "Eintrag erstellen und Kontakt zu planenden Unternehmern herstellen.",
"dir_cta_btn": "Eintrag erstellen",
"dir_card_verified": "Verifiziert",
"dir_card_featured": "Featured",
@@ -352,16 +352,16 @@
"sp_about": "Über uns",
"sp_services": "Angebotene Leistungen",
"sp_service_area": "Servicegebiet",
- "sp_enquiry_heading": "Anfrage senden",
+ "sp_enquiry_heading": "Anfrage stellen",
"sp_enquiry_name": "Dein Name",
"sp_enquiry_email": "E-Mail",
"sp_enquiry_message": "Nachricht",
- "sp_enquiry_submit": "Anfrage senden",
+ "sp_enquiry_submit": "Anfrage absenden",
"sp_contact": "Kontakt",
"sp_years": "Jahre aktiv",
"sp_projects": "Projekte",
"sp_trust": "Verifizierter Eintrag — Identität und Inhaberschaft bestätigt",
- "sp_cta_basic_h3": "Auf der Suche nach direkter Angebotsabstimmung?",
+ "sp_cta_basic_h3": "Direkte Angebote und Lead-Zugang gesucht?",
"sp_cta_claim_h3": "Ist das Dein Unternehmen?",
"sp_cta_claim_btn": "Eintrag beanspruchen →",
"sp_locked_hint": "Eintrag noch nicht verifiziert",
@@ -369,18 +369,18 @@
"sp_locked_popover_link": "Angebotsassistent nutzen →",
"sp_locked_popover_dismiss": "Schließen",
"sp_enquiry_placeholder": "Erzähl {name} von Deinem Projekt…",
- "sp_cta_basic_desc": "Upgrade auf Growth, um in unserer Anbieter-Vermittlung zu erscheinen und qualifizierte Projekt-Leads zu erhalten.",
+ "sp_cta_basic_desc": "Wechsle zu Growth, um in unserer Anbieter-Vermittlung zu erscheinen und qualifizierte Projekt-Leads zu erhalten.",
"sp_cta_basic_btn": "Auf Growth upgraden →",
- "sp_locked_popover_desc": "Dieser Anbieter hat seinen Eintrag noch nicht verifiziert. Nutze unseren Angebotsassistenten und wir vermitteln dich mit verifizierten Anbietern in Deiner Region.",
+ "sp_locked_popover_desc": "Dieser Anbieter hat seinen Eintrag noch nicht verifiziert. Nutze unseren Angebotsassistenten — wir vermitteln Dich mit verifizierten Anbietern in Deiner Region.",
"sp_cta_claim_desc": "Beanspruche und verifiziere diesen Eintrag, um Projektanfragen von Padel-Entwicklern zu erhalten.",
"enquiry_success_title": "Anfrage gesendet!",
"enquiry_error_title": "Bitte korrigiere Folgendes:",
- "enquiry_forwarded_msg": "Deine Nachricht wurde an {name} weitergeleitet. Der Anbieter meldet sich direkt bei dir.",
- "enquiry_received_msg": "Deine Nachricht wurde empfangen. Das Team meldet sich in Kürze bei dir.",
+ "enquiry_forwarded_msg": "Deine Nachricht wurde an {name} weitergeleitet. Der Anbieter meldet sich direkt bei Dir.",
+ "enquiry_received_msg": "Deine Nachricht wurde empfangen. Das Team meldet sich in Kürze bei Dir.",
"q_btn_next": "Weiter →",
"q_btn_back": "← Zurück",
"q_btn_submit": "Absenden & Angebote erhalten →",
- "q_page_title": "Angebote von Bauunternehmen erhalten",
+ "q_page_title": "Angebote von Bauunternehmen einholen",
"q_step_counter": "Schritt {step} von {total}",
"q1_heading": "Dein Projekt",
"q1_subheading": "Welche Art von Padel-Anlage planst Du?",
@@ -450,7 +450,7 @@
"q6_decision_partners": "Mit Partnern",
"q6_decision_committee": "Ausschuss / Vorstand",
"q7_heading": "Über Dich",
- "q7_subheading": "Das hilft uns, dich mit den richtigen Anbietern zusammenzubringen.",
+ "q7_subheading": "Das hilft uns, Dich mit den richtigen Anbietern zusammenzubringen.",
"q7_role_label": "Du bist…",
"q7_role_entrepreneur": "Unternehmer / Investor",
"q7_role_tennis": "Tennis- / Sportclub",
@@ -477,7 +477,7 @@
"q8_additional_label": "Noch etwas?",
"q8_additional_placeholder": "Besondere Anforderungen, Fragen oder Hintergrundinformationen…",
"q9_heading": "Kontaktdaten",
- "q9_subheading": "Wie sollen passende Anbieter dich erreichen?",
+ "q9_subheading": "Wie sollen passende Anbieter Dich erreichen?",
"q9_privacy_msg": "Deine Kontaktdaten werden nur mit geprüften Anbietern geteilt, die zu Deinen Projektspezifikationen passen.",
"q9_name_label": "Vollständiger Name",
"q9_email_label": "E-Mail",
@@ -492,7 +492,7 @@
"q9_error_email": "E-Mail ist erforderlich",
"q9_error_phone": "Telefonnummer ist erforderlich",
"qs_title": "Erfolgreich vermittelt!",
- "qs_next_h2": "Was als nächstes passiert",
+ "qs_next_h2": "Was als Nächstes passiert",
"qs_step_1": "Anbieter prüfen Deinen Projektbrief und bereiten Angebote vor",
"qs_step_1_time": "Jetzt",
"qs_step_2": "Passende Anbieter kontaktieren Dich mit maßgeschneiderten Angeboten",
@@ -509,7 +509,7 @@
"qs_matched_court_suffix": "-Platz-",
"qs_matched_facility_fmt": "{type}-",
"qs_matched_project": "Projekt",
- "qs_matched_post": "mit verifizierten Anbietern abgestimmt, die sich mit maßgeschneiderten Angeboten bei Dir melden.",
+ "qs_matched_post": "mit verifizierten Anbietern abgeglichen, die sich mit maßgeschneiderten Angeboten bei Dir melden.",
"qv_heading": "E-Mail prüfen",
"qv_link_expiry": "Der Link läuft in 60 Minuten ab.",
"qv_spam": "Spam-Ordner überprüfen",
@@ -517,7 +517,7 @@
"qv_wrong_email": "Falsche E-Mail?",
"qv_wrong_email_link": "Neue Anfrage stellen",
"qv_sent_msg": "Wir haben einen Verifizierungslink an folgende Adresse gesendet:",
- "qv_instructions": "Klick auf den Link in der E-Mail, um Deine Adresse zu bestätigen und Deine Angebotsanfrage zu aktivieren. Dadurch wird auch Dein {app_name}-Konto erstellt und du wirst automatisch angemeldet.",
+ "qv_instructions": "Klick auf den Link in der E-Mail, um Deine Adresse zu bestätigen und Deine Angebotsanfrage zu aktivieren. Dadurch wird auch Dein {app_name}-Konto erstellt und Du wirst automatisch angemeldet.",
"qv_no_email": "E-Mail nicht erhalten?",
"qv_check_email_pre": "Stell sicher, dass ",
"qv_check_email_post": " korrekt ist",
@@ -529,20 +529,20 @@
"sup_signup_of_steps": "von 4",
"sup_success_h2": "Alles bereit!",
"sup_success_text": "Dein Anbieter-Konto wird aktiviert. Du erhältst in Kürze qualifizierte Leads, die Deinen Leistungen entsprechen.",
- "sup_success_next_h3": "Was als nächstes passiert:",
+ "sup_success_next_h3": "Was als Nächstes passiert:",
"sup_success_btn": "Zum Lead-Feed",
"sup_success_page_title": "Willkommen!",
"sup_success_li1": "Dein Eintrag wird in wenigen Minuten aktualisiert",
"sup_success_li2": "Lead-Credits wurden deinem Konto hinzugefügt",
"sup_success_li3": "Prüfe deine E-Mail auf einen Anmelde-Link",
"sup_success_li4": "Durchsuche und entsperre Leads in deinem Feed",
- "sup_waitlist_h1": "Auf die Warteliste für die Anbieter-Plattform",
+ "sup_waitlist_h1": "Auf die Anbieter-Plattform-Warteliste eintragen",
"sup_waitlist_email_label": "E-Mail",
"sup_waitlist_submit": "Zur Warteliste",
"sup_waitlist_signin_text": "Bereits ein Konto?",
"sup_waitlist_signin_link": "Anmelden",
"sup_waitlist_page_title": "Anbieter-Warteliste",
- "sup_waitlist_intro": "Wir bauen die ultimative Plattform, um verifizierte Padel-Anbieter mit Unternehmern zu verbinden. Sei Erster in der Schlange für den {plan_name}-Tier-Zugang.",
+ "sup_waitlist_intro": "Wir bauen die ultimative Plattform, um verifizierte Padel-Anbieter mit Unternehmern zu verbinden. Trag Dich als Erster für den {plan_name}-Tier-Zugang ein.",
"sup_waitlist_plan_h3": "{name} Plan-Highlights",
"sup_waitlist_hint": "Frühzeitiger Zugang, exklusiver Launch-Preis und bevorzugtes Onboarding.",
"sup_waitlist_conf_page_title": "Du stehst auf der Anbieter-Warteliste",
@@ -550,7 +550,7 @@
"sup_waitlist_conf_msg": "Wir haben eine Bestätigung gesendet an:",
"sup_waitlist_conf_first_pre": "Du gehörst zu den ersten Anbietern mit Zugang zum ",
"sup_waitlist_conf_first_post": "-Tier bei unserem Launch.",
- "sup_waitlist_conf_early_h3": "Was du als Frühmitglied erhältst:",
+ "sup_waitlist_conf_early_h3": "Was Du als frühes Mitglied erhältst:",
"sup_waitlist_conf_li1": "Erster Zugang zu qualifizierten Leads von Padel-Unternehmern",
"sup_waitlist_conf_li2": "Exklusiver Launch-Preis (für 12 Monate festgeschrieben)",
"sup_waitlist_conf_li3": "Vorrangiges Onboarding und Support bei der Eintragsoptimierung",
@@ -577,7 +577,7 @@
"sup_step3_free_desc": "Nur Plan-Credits",
"sup_step3_next": "Weiter: Deine Daten",
"sup_step4_title": "Kontodaten",
- "sup_step4_sub": "Erzähl uns von deinem Unternehmen und wie wir dich erreichen können.",
+ "sup_step4_sub": "Erzähl uns von Deinem Unternehmen und wie wir Dich erreichen können.",
"sup_step4_contact_name": "Ansprechpartner",
"sup_step4_email": "E-Mail",
"sup_step4_phone": "Telefon",
@@ -710,8 +710,8 @@
"sl_hold_years": "Haltedauer",
"sl_exit_multiple": "Exit-EBITDA-Multiplikator",
"sl_annual_rev_growth": "Jährliches Umsatzwachstum",
- "wiz_summary_label": "Aktuelle Werte",
- "tip_permits_compliance": "Baugenehmigungen, Lärmgutachten, Nutzungsänderungen, Brandschutz und behördliche Auflagen. Wird automatisch angepasst, wenn du ein Land wählst — kann manuell überschrieben werden.",
+ "wiz_summary_label": "Aktuelle Zusammenfassung",
+ "tip_permits_compliance": "Baugenehmigungen, Lärmgutachten, Nutzungsänderungen, Brandschutz und behördliche Auflagen. Wird automatisch angepasst, wenn Du ein Land wählst — kann manuell überschrieben werden.",
"tip_dbl_courts": "Standard-Padelplatz für 4 Spieler. Häufigstes Format mit der höchsten Freizeitnachfrage.",
"tip_sgl_courts": "Schmaler Platz für 2 Spieler. Beliebt für Coaching, Training und Wettkampf.",
"tip_sqm_dbl_hall": "Gesamte Hallenfläche pro Doppelplatz. Enthält Spielfeld (200 m²), Sicherheitszonen, Laufwege und Mindestabstände. Standard: 300–350 m².",
@@ -723,7 +723,7 @@
"tip_rate_single": "Stundensatz für Einzelplätze. Meist niedriger als Doppelplätze, da sich weniger Spieler die Kosten teilen.",
"tip_peak_pct": "Anteil der gebuchten Stunden zum Spitzentarif. Höherer Wert bedeutet mehr Umsatz, aber schwieriger zu füllende Nebenstunden.",
"tip_booking_fee": "Provision von Buchungsplattformen wie Playtomic oder Matchi. Typisch: 5–15 % des Platzumsatzes.",
- "tip_util_target": "Anteil der verfügbaren Platzstunden, der tatsächlich gebucht wird. 35–45 % sind realistisch für neue Anlagen, 50 %+ ist stark.",
+ "tip_util_target": "Anteil der verfügbaren Platzstunden, der tatsächlich gebucht wird. 35–45 % sind realistisch für neue Anlagen, ab 50 % ist stark.",
"tip_hours_per_day": "Gesamte Betriebsstunden pro Tag. Typische Padel-Anlagen öffnen 7–23 Uhr (16 h). Manche auch 6–24 Uhr.",
"tip_days_indoor": "Durchschnittliche Betriebstage pro Monat für Indoor-Anlagen. ~29 berücksichtigt Feiertage und Wartungsschließungen.",
"tip_days_outdoor": "Durchschnittliche bespielbaren Tage pro Monat im Freien. Reduziert durch Regen, Extremhitze oder Kälte.",
@@ -750,7 +750,7 @@
"tip_outdoor_site_work": "Geländeausgleich, Entwässerungsinstallation, Versorgungsanschlüsse und Erschließung für Außenplätze.",
"tip_outdoor_lighting": "Flutlichtinstallation pro Platz. LED empfohlen für Energieeffizienz. Wettkampfnormen einhalten, falls relevant.",
"tip_outdoor_fencing": "Einzäunung der Außenplatzanlage. Enthält Windschutz, Sicherheitstore und Ballrückhaltevorrichtungen.",
- "tip_working_capital": "Kassenreserve für Betriebsverluste in der Anlaufphase und bei saisonalen Schwankungen. Kritischer Puffer — zu geringes Betriebskapital ist ein häufiger Startup-Fehler.",
+ "tip_working_capital": "Kassenreserve für Betriebsverluste in der Anlaufphase und bei saisonalen Schwankungen. Kritischer Puffer — zu geringes Betriebskapital ist ein häufiger Fehler in der Startphase.",
"tip_contingency": "Prozentualer Puffer auf den Gesamt-CAPEX für unvorhergesehene Kosten. 10–15 % sind beim Bau Standard, 15–20 % bei komplexen Projekten.",
"tip_budget_target": "Gesamtbudget festlegen, um den geplanten CAPEX zu vergleichen. 0 lassen, um den Budgetindikator auszublenden.",
"tip_rent_sqm": "Monatliche Miete pro m² für Hallenfläche. Abhängig von Lage, Gebäudequalität und Mietkonditionen.",
@@ -764,11 +764,11 @@
"tip_cleaning": "Monatliche professionelle Reinigung von Plätzen, Umkleiden, Gemeinschaftsflächen und Empfang.",
"tip_marketing": "Monatliche Ausgaben für Marketing, Buchungsplattform-Abonnements, Website, Social Media und Kundengewinnung.",
"tip_staff": "Monatliche Personalkosten: Gehälter, Sozialabgaben und Leistungen. Viele Anlagen fahren schlank mit automatisierten Buchungs- und Zugangssystemen.",
- "tip_loan_pct": "Anteil des Gesamt-CAPEX, der fremdfinanziert wird. Banken bieten typisch 70–85 %. Höher mit Bürgüschaft oder Fördermitteln.",
+ "tip_loan_pct": "Anteil des Gesamt-CAPEX, der fremdfinanziert wird. Banken bieten typisch 70–85 %. Höher mit Bürgschaft oder Fördermitteln.",
"tip_interest_rate": "Jährlicher Zinssatz des Darlehens. Abhängig von Bonität, Sicherheiten, Marktlage und Bankbeziehung.",
"tip_loan_term": "Kreditlaufzeit in Jahren. Längere Laufzeit bedeutet niedrigere Monatsraten, aber mehr Gesamtzinsen.",
"tip_construction_months": "Monate Bau/Einrichtung vor der Eröffnung. Kosten laufen bereits auf (Zinsen, Miete), aber noch kein Umsatz.",
- "tip_hold_years": "Investitionshaltedauer bis zum Exit/Verkauf. Typisch für PE/Investoren: 5–7 Jahre. Betreiber-Eigentümer können unbegrenzt halten.",
+ "tip_hold_years": "Investitionshaltedauer bis zum Exit/Verkauf. Typisch für PE/Investoren: 5–7 Jahre. Eigentümer-Betreiber können unbegrenzt halten.",
"tip_exit_multiple": "EBITDA-Multiplikator zur Unternehmensbewertung beim Exit. Spiegelt Marktnachfrage, Markenstärke und Wachstumspotenzial wider. Kleines Business: 4–6×, starke Marke: 6–8×.",
"tip_annual_rev_growth": "Erwartetes jährliches Umsatzwachstum nach der ersten 12-monatigen Anlaufphase. Getrieben durch Preiserhöhungen und steigende Auslastung.",
"tip_result_irr": "Interner Zinsfuß über den Haltezeitraum — der annualisierte Diskontsatz, bei dem der Barwert aller Cashflows null ergibt. Ziel: über 20 %. N/A wenn Cashflows nie positiv werden.",
@@ -783,7 +783,7 @@
"tip_result_yield_on_cost": "Stabilisiertes EBITDA ÷ Gesamtinvestition (CAPEX). Ungehebelte Rendite — nützlich zum Vergleich mit anderen Anlageklassen oder Bauprojekten.",
"btn_save": "Speichern",
"btn_my_scenarios": "Meine Szenarien",
- "btn_reset": "Zurücksetzen",
+ "btn_reset": "Auf Standardwerte zurücksetzen",
"btn_reset_confirm": "Sicher? Zurücksetzen",
"btn_back": "← Zurück",
"btn_next": "Weiter →",
@@ -911,12 +911,12 @@
"sup_prob_transition": "Was wäre, wenn jeder Lead mit einem vollständigen Projektbriefing und einem Finanzmodell käme?",
"sup_how_h2": "So funktioniert es",
"sup_how_sub": "Drei Schritte zu qualifizierten Leads.",
- "sup_how_step1_h3": "Dein Inserat beanspruchen",
- "sup_how_step1_p": "Dein Unternehmen ist bereits in unserem Verzeichnis. Wähle einen Plan, um dein Inserat aufzuwerten und Zugang zum Lead-Feed zu erhalten.",
+ "sup_how_step1_h3": "Deinen Eintrag beanspruchen",
+ "sup_how_step1_p": "Dein Unternehmen ist bereits in unserem Verzeichnis. Wähle einen Plan, um Deinen Eintrag aufzuwerten und Zugang zum Lead-Feed zu erhalten.",
"sup_how_step2_h3": "Vorqualifizierte Leads durchsuchen",
- "sup_how_step2_p": "Jeder Lead enthält Projektspezifikationen, Budget, Zeitplan und ein selbst erstelltes Finanzmodell. Setze Credits nur für Leads ein, die zu deinen Leistungen passen.",
+ "sup_how_step2_p": "Jeder Lead enthält Projektspezifikationen, Budget, Zeitplan und ein selbst erstelltes Finanzmodell. Setze Credits nur für Leads ein, die zu Deinen Leistungen passen.",
"sup_how_step3_h3": "Projekte schneller gewinnen",
- "sup_how_step3_p": "Kontaktiere den Unternehmer direkt. Du kennst bereits sein Budget, Zeitplan und Finanzierungsstatus – kein Discovery-Call nötig.",
+ "sup_how_step3_p": "Kontaktiere den Unternehmer direkt. Du kennst bereits sein Budget, seinen Zeitplan und seinen Finanzierungsstatus — kein Discovery-Call nötig.",
"sup_credits_h3": "Wie Credits funktionieren",
"sup_credits_sub": "Jeder Lead kostet Credits, je nachdem wie kaufbereit er ist. Growth-Pläne beinhalten 30 Credits/Monat, Pro 100.",
"sup_credits_hot": "Heißer Lead",
@@ -986,7 +986,7 @@
"sup_boost_sticky": "Sticky Top",
"sup_boost_color": "Eigene Kartenfarbe",
"sup_cmp_h2": "So schlagen wir den Vergleich",
- "sup_cmp_sub": "Deine Interessenten wägen diese Alternativen bereits ab. Hier der ehrliche Vergleich.",
+ "sup_cmp_sub": "Deine Interessenten wägen diese Alternativen bereits ab — hier der ehrliche Vergleich.",
"sup_cmp_th_us": "Padelnomics Growth",
"sup_cmp_th_tradeshow": "Messepräsenz",
"sup_cmp_th_ads": "Google Ads",
@@ -1012,7 +1012,7 @@
"sup_cmp_t4": "Nie",
"sup_cmp_m1": "Nach Kategorie gefiltert",
"sup_cmp_footnote": "*Google-Ads-Schätzung basierend auf €20–80 CPC für Padel-Baukeywords bei 5–10 Klicks/Tag.",
- "sup_proof_h2": "Vertrauen von Führungsunternehmen der Padel-Branche",
+ "sup_proof_h2": "Vertrauen von führenden Unternehmen der Padel-Branche",
"sup_proof_stat1": "erstellte Businesspläne",
"sup_proof_stat2": "Anbieter",
"sup_proof_stat3": "Länder",
@@ -1023,7 +1023,7 @@
"sup_faq_h2": "Anbieter-FAQ",
"sup_faq_q1": "Wie werde ich gelistet?",
"sup_faq_a1_pre": "Finde dein Unternehmen in unserem",
- "sup_faq_a1_post": "und klicke auf „Ist das dein Unternehmen?“ Wir überprüfen deine Identität und geben dir Zugang, um einen Plan auszuwählen und dein Profil zu aktualisieren.",
+ "sup_faq_a1_post": "und klicke auf „Ist das Dein Unternehmen?“ Wir prüfen Deine Identität und geben Dir Zugang, um einen Plan auszuwählen und Dein Profil zu aktualisieren.",
"sup_faq_dir_link": "Verzeichnis",
"sup_faq_q2": "Wie viel kostet es?",
"sup_faq_a2": "Wir bieten drei Pläne an: Basic (€39/Monat) für einen verifizierten Verzeichniseintrag mit Kontaktformular; Growth (€199/Monat, 30 Credits) mit vollem Lead-Zugang und Prioritätsplatzierung; und Pro (€499/Monat, 100 Credits) für maximale Sichtbarkeit und Lead-Volumen. Jährliche Abrechnung spart bis zu 26 % – Basic bei €349/Jahr, Growth bei €1.799/Jahr, Pro bei €4.499/Jahr. Optionale Boost-Add-Ons sind zusätzlich erhältlich.",
@@ -1046,7 +1046,7 @@
"sup_faq_a10_pre": "Schreib uns eine E-Mail an",
"sup_faq_a10_post": "mit deinen Unternehmensdetails und wir fügen dich innerhalb von 48 Stunden dem Verzeichnis hinzu.",
"sup_cta_h2": "Dein nächster Kunde erstellt gerade einen Businessplan",
- "sup_cta_p": "Er hat den ROI modelliert. Er kennt sein Budget. Er sucht einen Anbieter wie dich.",
+ "sup_cta_p": "Er hat den ROI berechnet. Er kennt sein Budget. Er sucht einen Anbieter wie Dich.",
"scenario_cta_try_numbers": "Mit eigenen Zahlen testen →",
"scenario_payback_label": "Amortisation",
"scenario_months_unit": "Monate",
@@ -1155,10 +1155,10 @@
"about_meta_desc": "Padelnomics ist eine kostenlose Finanzplanungsplattform für Padel-Unternehmer. Modelliere deine Investition, finde Anbieter und plane dein Padel-Business mit professionellen Tools.",
"about_og_desc": "Entwickelt für Padel-Unternehmer, die professionelle Finanztools ohne Beratungskosten benötigen. Kostenloser Planer, 60+ Variablen, Anbieterverzeichnis und mehr.",
"about_h1": "Über Padelnomics",
- "about_body_p1": "Padel ist eine der am schnellsten wachsenden Sportarten weltweit, doch für die meisten Unternehmer ist die Eröffnung einer Paddelhalle immer noch ein Sprung ins Ungewisse. Die Finanzen sind komplex: Der CAPEX variiert stark je nach Anlagentyp, der Standort bestimmt die Auslastung, und der Unterschied zwischen 60 % und 75 % Belegung kann über Erfolg oder Misserfolg einer Investition entscheiden.",
- "about_body_p2": "Wir haben Padelnomics gebaut, weil wir kein ausreichend gutes Finanzplanungstool gefunden haben. Vorhandene Rechner sind entweder zu simpel (5 Eingaben, ein Ergebnis) oder hinter teuren Beratungsmandaten verborgen. Wir wollten etwas mit der Tiefe eines professionellen Finanzmodells, aber der Zugänglichkeit einer Web-App.",
+ "about_body_p1": "Padel ist eine der am schnellsten wachsenden Sportarten weltweit, doch für die meisten Unternehmer ist die Eröffnung einer Padel-Anlage noch immer ein Sprung ins Ungewisse. Die Finanzen sind komplex: Der CAPEX variiert stark je nach Anlagentyp, der Standort bestimmt die Auslastung, und der Unterschied zwischen 60 % und 75 % Belegung kann über Erfolg oder Misserfolg einer Investition entscheiden.",
+ "about_body_p2": "Wir haben Padelnomics gebaut, weil wir kein ausreichend gutes Finanzplanungstool gefunden haben. Vorhandene Rechner sind entweder zu simpel (5 Eingaben, ein Ergebnis) oder hinter teuren Beratungsmandaten versteckt. Wir wollten etwas mit der Tiefe eines professionellen Finanzmodells, aber der Zugänglichkeit einer Web-App.",
"about_body_p3": "Das Ergebnis ist ein kostenloser Finanzplaner mit 60+ anpassbaren Variablen, 6 Analyse-Tabs, Sensitivitätsanalyse und den professionellen Kennzahlen, die Banken und Investoren sehen müssen. Jede Annahme ist transparent und anpassbar. Keine Blackboxen.",
- "about_why_p": "Der Planer ist kostenlos, weil wir glauben, dass bessere Planung zu besseren Padelanlagen führt — und das ist gut für die gesamte Branche. Wir verdienen Geld, indem wir Unternehmer mit Platz-Anbietern und Finanzierungspartnern verbinden, wenn sie bereit sind, von der Planung zum Bau überzugehen.",
+ "about_why_p": "Der Planer ist kostenlos, weil wir glauben, dass bessere Planung zu besseren Padelanlagen führt — und das ist gut für die gesamte Branche. Wir verdienen Geld, indem wir Unternehmer mit Platz-Anbietern und Finanzierungspartnern verbinden, wenn sie bereit sind, von der Planung in den Bau zu wechseln.",
"about_next_p": "Padelnomics baut die Infrastruktur für Padel-Unternehmertum auf. Nach der Planung kommen Finanzierung, Bau und Betrieb. Wir arbeiten an Marktintelligenz auf Basis realer Buchungsdaten, einem Anbietermarktplatz für Platzausstattung und Analyse-Tools für Betreiber.",
"features_title_prefix": "Funktionen - Padel-Kostenrechner & Finanzplaner",
"features_meta_desc": "60+ anpassbare Variablen, 6 Analyse-Tabs, Sensitivitätsanalyse und professionelle Finanzprojektionen für deine Padelplatz-Investition.",
@@ -1175,11 +1175,11 @@
"landing_page_title": "Padelnomics - Padel-Kostenrechner & Finanzplaner",
"landing_meta_desc": "Modelliere deine Padelplatz-Investition mit 60+ Variablen, Sensitivitätsanalyse und professionellen Projektionen. Innen-/Außenanlage, Miet- oder Eigentumsmodell.",
"landing_og_desc": "Der professionellste Padel-Finanzplaner. 60+ Variablen, 6 Analyse-Tabs, Diagramme, Sensitivitätsanalyse und Anbieter-Vermittlung.",
- "landing_hero_desc": "Modelliere deine Padelplatz-Investition mit 60+ Variablen, Sensitivitätsanalyse und professionellen Projektionen. Dann wirst du mit verifizierten Anbietern zusammengebracht.",
+ "landing_hero_desc": "Modelliere Deine Padelplatz-Investition mit 60+ Variablen, Sensitivitätsanalyse und professionellen Projektionen. Danach wirst Du mit verifizierten Anbietern zusammengebracht.",
"landing_journey_01_desc": "Marktbedarfsanalyse, Standortbewertung und Identifikation von Nachfragepotenzialen.",
"landing_journey_02_desc": "Modelliere deine Investition mit 60+ Variablen, Diagrammen und Sensitivitätsanalyse.",
"landing_journey_03_desc": "Kontakte zu Banken und Investoren herstellen. Dein Finanzplan wird zum Businesscase.",
- "landing_journey_04_desc": "Über {total_suppliers}+ Platz-Anbieter aus {total_countries} Ländern durchsuchen. Passend zu deinen Anforderungen vermittelt.",
+ "landing_journey_04_desc": "{total_suppliers}+ Platz-Anbieter aus {total_countries} Ländern durchsuchen. Passend zu Deinen Anforderungen vermittelt.",
"landing_journey_05_desc": "Launch-Playbook, Performance-Benchmarks und Wachstumsanalysen für deinen Betrieb.",
"landing_feature_1_body": "Jede Annahme ist anpassbar: Platzbaukosten, Miete, Preisgestaltung, Auslastung, Finanzierungskonditionen, Exit-Szenarien. Nichts ist fest vorgegeben.",
"landing_feature_2_body": "Annahmen, Investition (CAPEX), Betriebsmodell, Cashflow, Renditen & Exit sowie Kennzahlen — jeder Tab mit interaktiven Diagrammen.",
@@ -1196,9 +1196,9 @@
"landing_faq_a3": "Wenn du über den Planer Angebote anforderst, teilen wir deine Projektdetails (Anlagentyp, Platzzahl, Glas, Beleuchtung, Land, Budget, Zeitplan) mit passenden Anbietern aus unserem Verzeichnis. Diese kontaktieren dich direkt mit ihren Angeboten.",
"landing_faq_a4": "Das Durchsuchen des Verzeichnisses ist für alle kostenlos. Anbieter erhalten standardmäßig einen Basiseintrag. Kostenpflichtige Pläne (Basic ab 39 €/Monat, Growth ab 199 €/Monat, Pro ab 499 €/Monat) schalten Anfrageformulare, vollständige Beschreibungen, Logos, verifizierte Badges und Prioritätsplatzierung frei.",
"landing_faq_a5": "Das Modell verwendet reale Standardwerte auf Basis globaler Marktdaten. Jede Annahme ist anpassbar, sodass du deine lokalen Gegebenheiten abbilden kannst. Die Sensitivitätsanalyse zeigt, wie sich die Ergebnisse in verschiedenen Szenarien verändern, und hilft dir, die Bandbreite möglicher Ergebnisse zu verstehen.",
- "landing_seo_p1": "Padel ist eine der am schnellsten wachsenden Racketsportarten weltweit — die Nachfrage nach Plätzen übersteigt das Angebot von Deutschland, Spanien und Schweden bis in die USA und den Nahen Osten. Eine Paddelhalle zu eröffnen kann eine attraktive Investition sein, aber die Zahlen müssen stimmen. Eine typische Indoorhalle mit 6–8 Plätzen erfordert zwischen 300.000 € (Anmietung eines Bestandsgebäudes) und 2–3 Mio. € (Neubau), mit Amortisationszeiten von 3–5 Jahren für gut gelegene Anlagen.",
- "landing_seo_p2": "Die entscheidenden Faktoren für den Erfolg sind Standort (treibt die Auslastung), Baukosten (CAPEX), Miet- oder Grundstückskosten sowie die Preisstrategie. Unser Finanzplaner ermöglicht es dir, alle diese Variablen interaktiv zu modellieren und die Auswirkungen auf IRR, MOIC, Cashflow und Schuldendienstdeckungsgrad in Echtzeit zu sehen. Ob du als Unternehmer deine erste Anlage prüfst, als Immobilienentwickler Padel in ein Mixed-Use-Projekt integrierst oder als Investor eine bestehende Paddelhalle bewertest — Padelnomics gibt dir die finanzielle Klarheit für fundierte Entscheidungen.",
- "landing_final_cta_sub": "Modelliere deine Investition und lass dich mit verifizierten Platz-Anbietern aus {total_countries} Ländern zusammenbringen.",
+ "landing_seo_p1": "Padel ist eine der am schnellsten wachsenden Racketsportarten weltweit — die Nachfrage nach Plätzen übersteigt das Angebot in Märkten von Deutschland, Spanien und Schweden bis in die USA und den Nahen Osten. Eine Padel-Anlage zu eröffnen kann eine attraktive Investition sein, aber die Zahlen müssen stimmen. Eine typische Indoorhalle mit 6–8 Plätzen erfordert zwischen 300.000 € (Anmietung eines Bestandsgebäudes) und 2–3 Mio. € (Neubau), mit Amortisationszeiten von 3–5 Jahren für gut gelegene Anlagen.",
+ "landing_seo_p2": "Die entscheidenden Faktoren für den Erfolg sind Standort (treibt die Auslastung), Baukosten (CAPEX), Miet- oder Grundstückskosten sowie die Preisstrategie. Unser Finanzplaner ermöglicht es Dir, alle diese Variablen interaktiv zu modellieren und die Auswirkungen auf IRR, MOIC, Cashflow und Schuldendienstdeckungsgrad in Echtzeit zu sehen. Ob Du als Unternehmer Deine erste Anlage prüfst, als Immobilienentwickler Padel in ein Mixed-Use-Projekt integrierst oder als Investor eine bestehende Padel-Anlage bewertest — Padelnomics gibt Dir die finanzielle Klarheit für fundierte Entscheidungen.",
+ "landing_final_cta_sub": "Modelliere Deine Investition und lass Dich mit verifizierten Platz-Anbietern aus {total_countries} Ländern zusammenbringen.",
"landing_jsonld_org_desc": "Professionelle Planungsplattform für Padelplatz-Investitionen. Finanzplaner, Anbieterverzeichnis und Marktinformationen für Padel-Unternehmer.",
"plan_basic_f1": "Verifiziert-Badge",
"plan_basic_f2": "Firmenlogo",
@@ -1259,7 +1259,7 @@
"billing_pricing_og_title": "Kostenloser Padel-Finanzplaner",
"billing_pricing_og_desc": "Plane deine Padel-Investition mit 60+ Variablen, Sensitivitätsanalyse und professionellen Projektionen. Keine Anmeldung erforderlich. Vollständig kostenlos.",
"billing_pricing_h1": "100% kostenlos. Kein Haken.",
- "billing_pricing_subtitle": "Der ausgefeilteste Padel-Finanzplaner auf dem Markt — vollständig kostenlos. Plane deine Investition mit 60+ Variablen, Sensitivitätsanalyse und professionellen Projektionen.",
+ "billing_pricing_subtitle": "Der ausgefeilteste Padel-Finanzplaner am Markt — vollständig kostenlos. Plane Deine Investition mit 60+ Variablen, Sensitivitätsanalyse und professionellen Projektionen.",
"billing_planner_card": "Finanzplaner",
"billing_planner_free": "Kostenlos",
"billing_planner_forever": "— für immer",
@@ -1281,7 +1281,7 @@
"billing_signup": "Jetzt registrieren",
"billing_success_title": "Willkommen",
"billing_success_h1": "Willkommen bei Padelnomics!",
- "billing_success_body": "Dein Konto ist bereit. Starte jetzt mit dem Planen deiner Padel-Investition.",
+ "billing_success_body": "Dein Konto ist bereit. Fang jetzt an, Deine Padel-Investition mit unserem Finanzplaner zu planen.",
"billing_success_btn": "Planer öffnen",
"billing_no_subscription": "Kein aktives Abonnement gefunden.",
"sd_page_title": "Anbieter-Dashboard",
@@ -1303,11 +1303,11 @@
"sd_ov_credits_balance": "Credits-Guthaben",
"sd_ov_directory_rank": "Verzeichnis-Rang",
"sd_ov_basic_plan_label": "Basic-Tarif",
- "sd_ov_basic_plan_desc": "Du hast einen verifizierten Eintrag mit Kontaktformular. Wechsle zu Growth, um auf qualifizierte Projekt-Leads zuzugreifen.",
+ "sd_ov_basic_plan_desc": "Du hast einen verifizierten Eintrag mit Kontaktformular. Wechsle zu Growth, um Zugang zu qualifizierten Projekt-Leads zu erhalten.",
"sd_ov_upgrade_growth": "Auf Growth upgraden",
"sd_ov_recent_activity": "Letzte Aktivitäten",
"sd_ov_credits": "Credits",
- "sd_ov_no_activity": "Noch keine Aktivitäten. Schalte deinen ersten Lead frei.",
+ "sd_ov_no_activity": "Noch keine Aktivitäten. Schalte Deinen ersten Lead frei.",
"sd_bst_current_plan": "Aktueller Tarif",
"sd_bst_credits_month": "Credits/Monat",
"sd_bst_per_mo": "/Monat",
@@ -1462,16 +1462,15 @@
"sd_boost_verified_name": "Verifiziert-Badge",
"sd_boost_verified_desc": "Verifiziertes Häkchen-Badge",
"sd_boost_card_color_name": "Individuelle Kartenfarbe",
- "sd_boost_card_color_desc": "Hebe dich mit einer individuellen Randfarbe in deinem Verzeichniseintrag ab",
+ "sd_boost_card_color_desc": "Heb Dich mit einer individuellen Rahmenfarbe in Deinem Verzeichniseintrag ab",
"sd_billing_yearly": "jährlich abgerechnet zu €{price}/Jahr",
"sd_billing_monthly": "monatlich abgerechnet",
- "sd_flash_signin": "Bitte melde dich an, um fortzufahren.",
+ "sd_flash_signin": "Bitte melde Dich an, um fortzufahren.",
"sd_flash_active_plan": "Du benötigst einen aktiven Anbieter-Tarif, um auf diese Seite zuzugreifen.",
"sd_flash_lead_access": "Lead-Zugang erfordert einen Growth- oder Pro-Tarif.",
"sd_flash_valid_email": "Bitte gib eine gültige E-Mail-Adresse ein.",
"sd_flash_claim_error": "Dieser Eintrag wurde bereits beansprucht oder existiert nicht.",
"sd_flash_listing_saved": "Eintrag erfolgreich gespeichert.",
-
"bp_indoor": "Indoor",
"bp_outdoor": "Outdoor",
"bp_own": "Kauf",
@@ -1480,49 +1479,41 @@
"bp_payback_not_reached": "Nicht in 60 Monaten erreicht",
"bp_months": "{n} Monate",
"bp_years": "{n} Jahre",
- "bp_exec_paragraph": "Dieser Businessplan modelliert eine
{facility_type}-Padel-Anlage mit
{courts} Pl\u00e4tzen ({sqm} m\u00b2). Die Gesamtinvestition betr\u00e4gt {total_capex}, finanziert mit {equity} Eigenkapital und {loan} Fremdkapital. Die prognostizierte IRR betr\u00e4gt {irr} bei einer Amortisationszeit von {payback}.",
-
+ "bp_exec_paragraph": "Dieser Businessplan modelliert eine
{facility_type}-Padel-Anlage mit
{courts} Plätzen ({sqm} m²). Die Gesamtinvestition beträgt {total_capex}, finanziert mit {equity} Eigenkapital und {loan} Fremdkapital. Die prognostizierte IRR beträgt {irr} bei einer Amortisationszeit von {payback}.",
"bp_lbl_scenario": "Szenario",
- "bp_lbl_generated_by": "Erstellt von Padelnomics \u2014 padelnomics.io",
-
+ "bp_lbl_generated_by": "Erstellt von Padelnomics — padelnomics.io",
"bp_lbl_total_investment": "Gesamtinvestition",
"bp_lbl_equity_required": "Eigenkapitalbedarf",
"bp_lbl_year3_ebitda": "EBITDA Jahr 3",
"bp_lbl_irr": "IRR",
"bp_lbl_payback_period": "Amortisationszeit",
"bp_lbl_year1_revenue": "Umsatz Jahr 1",
-
"bp_lbl_item": "Position",
"bp_lbl_amount": "Betrag",
"bp_lbl_notes": "Hinweise",
"bp_lbl_total_capex": "Gesamt-CAPEX",
- "bp_lbl_capex_stats": "CAPEX je Platz: {per_court} \u2022 CAPEX je m\u00b2: {per_sqm}",
-
+ "bp_lbl_capex_stats": "CAPEX je Platz: {per_court} • CAPEX je m²: {per_sqm}",
"bp_lbl_equity": "Eigenkapital",
"bp_lbl_loan": "Darlehen",
"bp_lbl_interest_rate": "Zinssatz",
"bp_lbl_loan_term": "Darlehenslaufzeit",
"bp_lbl_monthly_payment": "Monatliche Rate",
- "bp_lbl_annual_debt_service": "J\u00e4hrlicher Schuldendienst",
+ "bp_lbl_annual_debt_service": "Jährlicher Schuldendienst",
"bp_lbl_ltv": "Beleihungsauslauf",
-
"bp_lbl_monthly": "Monatlich",
"bp_lbl_total_monthly_opex": "Monatlicher OPEX gesamt",
"bp_lbl_annual_opex": "Jahres-OPEX",
-
"bp_lbl_weighted_hourly_rate": "Gewichteter Stundensatz",
"bp_lbl_target_utilization": "Zielauslastung",
"bp_lbl_gross_monthly_revenue": "Monatlicher Bruttoumsatz",
"bp_lbl_net_monthly_revenue": "Monatlicher Nettoumsatz",
"bp_lbl_monthly_ebitda": "Monatliches EBITDA",
"bp_lbl_monthly_net_cf": "Monatlicher Netto-Cashflow",
-
"bp_lbl_year": "Jahr",
"bp_lbl_revenue": "Umsatz",
"bp_lbl_ebitda": "EBITDA",
"bp_lbl_debt_service": "Schuldendienst",
"bp_lbl_net_cf": "Netto-CF",
-
"bp_lbl_moic": "MOIC",
"bp_lbl_cash_on_cash": "Cash-on-Cash (J3)",
"bp_lbl_payback": "Amortisation",
@@ -1530,116 +1521,148 @@
"bp_lbl_ebitda_margin": "EBITDA-Marge",
"bp_lbl_dscr_y3": "DSCR (J3)",
"bp_lbl_yield_on_cost": "Rendite auf Kosten",
-
"bp_lbl_month": "Monat",
"bp_lbl_opex": "OPEX",
"bp_lbl_debt": "Schulden",
"bp_lbl_cumulative": "Kumulativ",
-
- "bp_lbl_disclaimer": "
Haftungsausschluss: Dieser Businessplan wurde auf Basis benutzerdefinierter Annahmen mit dem Padelnomics-Finanzmodell erstellt. Alle Prognosen sind Sch\u00e4tzungen und stellen keine Finanzberatung dar. Die tats\u00e4chlichen Ergebnisse k\u00f6nnen je nach Marktbedingungen, Umsetzung und anderen Faktoren erheblich abweichen. Konsultiere Finanzberater, bevor du Investitionsentscheidungen triffst. \u00a9 Padelnomics \u2014 padelnomics.io",
-
+ "bp_lbl_disclaimer": "
Haftungsausschluss: Dieser Businessplan wurde auf Basis benutzerdefinierter Annahmen mit dem Padelnomics-Finanzmodell erstellt. Alle Prognosen sind Schätzungen und stellen keine Finanzberatung dar. Die tatsächlichen Ergebnisse können je nach Marktbedingungen, Umsetzung und anderen Faktoren erheblich abweichen. Konsultiere Finanzberater, bevor du Investitionsentscheidungen triffst. © Padelnomics — padelnomics.io",
"email_magic_link_heading": "Bei {app_name} anmelden",
- "email_magic_link_body": "Hier ist dein Anmeldelink. Er l\u00e4uft in {expiry_minutes} Minuten ab.",
- "email_magic_link_btn": "Anmelden \u2192",
+ "email_magic_link_body": "Hier ist dein Anmeldelink. Er läuft in {expiry_minutes} Minuten ab.",
+ "email_magic_link_btn": "Anmelden →",
"email_magic_link_fallback": "Wenn der Button nicht funktioniert, kopiere diese URL in deinen Browser:",
- "email_magic_link_ignore": "Wenn du das nicht angefordert hast, kannst du diese E-Mail ignorieren.",
- "email_magic_link_subject": "Dein Anmeldelink f\u00fcr {app_name}",
- "email_magic_link_preheader": "Dieser Link l\u00e4uft in {expiry_minutes} Minuten ab",
-
- "email_quote_verify_heading": "Best\u00e4tige deine E-Mail f\u00fcr Angebote",
+ "email_magic_link_ignore": "Falls Du das nicht angefordert hast, kannst Du diese E-Mail ignorieren.",
+ "email_magic_link_subject": "Dein Anmeldelink für {app_name}",
+ "email_magic_link_preheader": "Dieser Link läuft in {expiry_minutes} Minuten ab",
+ "email_quote_verify_heading": "Bestätige deine E-Mail für Angebote",
"email_quote_verify_greeting": "Hallo {first_name},",
- "email_quote_verify_body": "Danke f\u00fcr deine Angebotsanfrage. Best\u00e4tige deine E-Mail, um deine Anfrage zu aktivieren und dein {app_name}-Konto zu erstellen.",
+ "email_quote_verify_body": "Danke für deine Angebotsanfrage. Bestätige deine E-Mail, um deine Anfrage zu aktivieren und dein {app_name}-Konto zu erstellen.",
"email_quote_verify_project_label": "Dein Projekt:",
- "email_quote_verify_urgency": "Verifizierte Anfragen werden von unserem Anbieternetzwerk bevorzugt behandelt.",
- "email_quote_verify_btn": "Best\u00e4tigen & Aktivieren \u2192",
- "email_quote_verify_expires": "Dieser Link l\u00e4uft in 60 Minuten ab.",
+ "email_quote_verify_urgency": "Verifizierte Anfragen werden von unserem Anbieternetzwerk bevorzugt bearbeitet.",
+ "email_quote_verify_btn": "Bestätigen & Aktivieren →",
+ "email_quote_verify_expires": "Dieser Link läuft in 60 Minuten ab.",
"email_quote_verify_fallback": "Wenn der Button nicht funktioniert, kopiere diese URL in deinen Browser:",
- "email_quote_verify_ignore": "Wenn du das nicht angefordert hast, kannst du diese E-Mail ignorieren.",
- "email_quote_verify_subject": "Best\u00e4tige deine E-Mail \u2014 Anbieter sind bereit f\u00fcr Angebote",
+ "email_quote_verify_ignore": "Falls Du das nicht angefordert hast, kannst Du diese E-Mail ignorieren.",
+ "email_quote_verify_subject": "Bestätige deine E-Mail — Anbieter sind bereit für Angebote",
"email_quote_verify_preheader": "Ein Klick, um deine Angebotsanfrage zu aktivieren",
"email_quote_verify_preheader_courts": "Ein Klick, um dein {court_count}-Court-Projekt zu aktivieren",
-
"email_welcome_heading": "Willkommen bei {app_name}",
"email_welcome_greeting": "Hallo {first_name},",
- "email_welcome_body": "Du hast jetzt Zugang zum Finanzplaner, Marktdaten und dem Anbieterverzeichnis \u2014 alles, was du f\u00fcr die Planung deines Padel-Gesch\u00e4fts brauchst.",
+ "email_welcome_body": "Du hast jetzt Zugang zum Finanzplaner, Marktdaten und dem Anbieterverzeichnis — alles, was du für die Planung deines Padel-Geschäfts brauchst.",
"email_welcome_quickstart_heading": "Schnellstart:",
- "email_welcome_link_planner": "Finanzplaner \u2014 modelliere deine Investition",
- "email_welcome_link_markets": "Marktdaten \u2014 erkunde die Padel-Nachfrage nach Stadt",
- "email_welcome_link_quotes": "Angebote einholen \u2014 verbinde dich mit verifizierten Anbietern",
- "email_welcome_btn": "Jetzt planen \u2192",
- "email_welcome_subject": "Du bist dabei \u2014 so f\u00e4ngst du an",
+ "email_welcome_link_planner": "Finanzplaner — modelliere deine Investition",
+ "email_welcome_link_markets": "Marktdaten — erkunde die Padel-Nachfrage nach Stadt",
+ "email_welcome_link_quotes": "Angebote einholen — verbinde dich mit verifizierten Anbietern",
+ "email_welcome_btn": "Jetzt planen →",
+ "email_welcome_subject": "Du bist dabei — so fängst Du an",
"email_welcome_preheader": "Dein Padel-Planungstoolkit ist bereit",
-
"email_waitlist_supplier_heading": "Du stehst auf der Anbieter-Warteliste",
- "email_waitlist_supplier_body": "Danke f\u00fcr dein Interesse am
{plan_name}-Plan. Wir bauen eine Plattform, die dich mit qualifizierten Leads von Padel-Unternehmern verbindet, die aktiv Projekte planen.",
- "email_waitlist_supplier_perks_intro": "Als fr\u00fches Wartelisten-Mitglied erh\u00e4ltst du:",
- "email_waitlist_supplier_perk_1": "Fr\u00fchen Zugang vor dem \u00f6ffentlichen Launch",
+ "email_waitlist_supplier_body": "Danke für dein Interesse am
{plan_name}-Plan. Wir bauen eine Plattform, die dich mit qualifizierten Leads von Padel-Unternehmern verbindet, die aktiv Projekte planen.",
+ "email_waitlist_supplier_perks_intro": "Als frühes Wartelisten-Mitglied erhältst du:",
+ "email_waitlist_supplier_perk_1": "Frühen Zugang vor dem öffentlichen Launch",
"email_waitlist_supplier_perk_2": "Exklusive Launch-Preise (gesichert)",
- "email_waitlist_supplier_perk_3": "Pers\u00f6nliches Onboarding-Gespr\u00e4ch",
+ "email_waitlist_supplier_perk_3": "Persönliches Onboarding-Gespräch",
"email_waitlist_supplier_meanwhile": "In der Zwischenzeit erkunde unsere kostenlosen Ressourcen:",
- "email_waitlist_supplier_link_planner": "Finanzplanungstool \u2014 plane deine Padel-Anlage",
- "email_waitlist_supplier_link_directory": "Anbieterverzeichnis \u2014 verifizierte Anbieter durchsuchen",
- "email_waitlist_supplier_subject": "Du bist dabei \u2014 {plan_name} fr\u00fcher Zugang kommt",
+ "email_waitlist_supplier_link_planner": "Finanzplanungstool — plane deine Padel-Anlage",
+ "email_waitlist_supplier_link_directory": "Anbieterverzeichnis — verifizierte Anbieter durchsuchen",
+ "email_waitlist_supplier_subject": "Du bist dabei — {plan_name} früher Zugang kommt",
"email_waitlist_supplier_preheader": "Exklusive Launch-Preise + bevorzugtes Onboarding",
"email_waitlist_general_heading": "Du stehst auf der Warteliste",
- "email_waitlist_general_body": "Danke f\u00fcr deine Anmeldung. Wir bauen die Planungsplattform f\u00fcr Padel-Unternehmer \u2014 Finanzmodellierung, Marktdaten und Anbietervernetzung an einem Ort.",
- "email_waitlist_general_perks_intro": "Als fr\u00fches Wartelisten-Mitglied erh\u00e4ltst du:",
- "email_waitlist_general_perk_1": "Fr\u00fchen Zugang vor dem \u00f6ffentlichen Launch",
+ "email_waitlist_general_body": "Danke für deine Anmeldung. Wir bauen die Planungsplattform für Padel-Unternehmer — Finanzmodellierung, Marktdaten und Anbietervernetzung an einem Ort.",
+ "email_waitlist_general_perks_intro": "Als frühes Wartelisten-Mitglied erhältst du:",
+ "email_waitlist_general_perk_1": "Frühen Zugang vor dem öffentlichen Launch",
"email_waitlist_general_perk_2": "Exklusive Launch-Preise",
- "email_waitlist_general_perk_3": "Priorit\u00e4ts-Onboarding und Support",
- "email_waitlist_general_outro": "Wir melden uns bald.",
- "email_waitlist_general_subject": "Du stehst auf der Liste \u2014 wir benachrichtigen dich zum Launch",
- "email_waitlist_general_preheader": "Fr\u00fcher Zugang + exklusive Launch-Preise",
-
+ "email_waitlist_general_perk_3": "Prioritäts-Onboarding und Support",
+ "email_waitlist_general_outro": "Wir melden uns in Kürze.",
+ "email_waitlist_general_subject": "Du stehst auf der Liste — wir benachrichtigen dich zum Launch",
+ "email_waitlist_general_preheader": "Früher Zugang + exklusive Launch-Preise",
"email_lead_forward_heading": "Neues Projekt-Lead",
- "email_lead_forward_urgency": "Dieses Lead wurde gerade freigeschaltet. Anbieter, die innerhalb von 24 Stunden antworten, gewinnen 3x h\u00e4ufiger das Projekt.",
+ "email_lead_forward_urgency": "Dieses Lead wurde gerade freigeschaltet. Anbieter, die innerhalb von 24 Stunden antworten, gewinnen das Projekt 3× häufiger.",
"email_lead_forward_section_brief": "Projektbeschreibung",
"email_lead_forward_section_contact": "Kontakt",
"email_lead_forward_lbl_facility": "Anlage",
- "email_lead_forward_lbl_courts": "Pl\u00e4tze",
+ "email_lead_forward_lbl_courts": "Plätze",
"email_lead_forward_lbl_location": "Standort",
"email_lead_forward_lbl_timeline": "Zeitplan",
"email_lead_forward_lbl_phase": "Phase",
"email_lead_forward_lbl_services": "Leistungen",
- "email_lead_forward_lbl_additional": "Zus\u00e4tzlich",
+ "email_lead_forward_lbl_additional": "Zusätzlich",
"email_lead_forward_lbl_name": "Name",
"email_lead_forward_lbl_email": "E-Mail",
"email_lead_forward_lbl_phone": "Telefon",
"email_lead_forward_lbl_company": "Unternehmen",
"email_lead_forward_lbl_role": "Rolle",
- "email_lead_forward_btn": "Im Lead-Feed ansehen \u2192",
+ "email_lead_forward_btn": "Im Lead-Feed ansehen →",
"email_lead_forward_reply_direct": "oder
direkt an {contact_email} antworten",
"email_lead_forward_preheader_suffix": "Kontaktdaten enthalten",
-
- "email_lead_matched_heading": "Ein Anbieter m\u00f6chte dein Projekt besprechen",
+ "email_lead_matched_heading": "Ein Anbieter möchte dein Projekt besprechen",
"email_lead_matched_greeting": "Hallo {first_name},",
- "email_lead_matched_body": "Gute Nachrichten \u2014 ein verifizierter Anbieter wurde mit deinem Padel-Projekt abgeglichen. Er hat deine Projektbeschreibung und Kontaktdaten.",
- "email_lead_matched_context": "Du hast eine Angebotsanfrage f\u00fcr eine {facility_type}-Anlage mit {court_count} Pl\u00e4tzen in {country} eingereicht.",
- "email_lead_matched_next_heading": "Was passiert als N\u00e4chstes",
- "email_lead_matched_next_body": "Der Anbieter hat deine Projektbeschreibung und Kontaktdaten erhalten. Die meisten Anbieter melden sich innerhalb von 24\u201348 Stunden per E-Mail oder Telefon.",
- "email_lead_matched_tip": "Tipp: Schnelles Reagieren auf Anbieter-Kontaktaufnahmen erh\u00f6ht deine Chance auf wettbewerbsf\u00e4hige Angebote.",
- "email_lead_matched_btn": "Zum Dashboard \u2192",
- "email_lead_matched_note": "Du erh\u00e4ltst diese Benachrichtigung jedes Mal, wenn ein neuer Anbieter deine Projektdetails freischaltet.",
- "email_lead_matched_subject": "{first_name}, ein Anbieter m\u00f6chte dein Projekt besprechen",
- "email_lead_matched_preheader": "Der Anbieter wird sich direkt bei dir melden \u2014 das erwartet dich",
-
+ "email_lead_matched_body": "Gute Neuigkeit — ein verifizierter Anbieter wurde mit Deinem Padel-Projekt abgeglichen. Er hat Dein Projektbriefing und Deine Kontaktdaten.",
+ "email_lead_matched_context": "Du hast eine Angebotsanfrage für eine {facility_type}-Anlage mit {court_count} Plätzen in {country} eingereicht.",
+ "email_lead_matched_next_heading": "Was passiert als Nächstes",
+ "email_lead_matched_next_body": "Der Anbieter hat Dein Projektbriefing und Deine Kontaktdaten erhalten. Die meisten Anbieter melden sich innerhalb von 24–48 Stunden per E-Mail oder Telefon.",
+ "email_lead_matched_tip": "Tipp: Wer schnell auf Anbieter-Kontaktaufnahmen reagiert, erhöht seine Chancen auf wettbewerbsfähige Angebote.",
+ "email_lead_matched_btn": "Zum Dashboard →",
+ "email_lead_matched_note": "Du erhältst diese Benachrichtigung jedes Mal, wenn ein neuer Anbieter Deine Projektdetails freischaltet.",
+ "email_lead_matched_subject": "{first_name}, ein Anbieter möchte dein Projekt besprechen",
+ "email_lead_matched_preheader": "Der Anbieter meldet sich direkt bei Dir — das erwartet Dich",
"email_enquiry_heading": "Neue Anfrage von {contact_name}",
- "email_enquiry_body": "Du hast eine neue Anfrage \u00fcber deinen
{supplier_name}-Verzeichniseintrag.",
+ "email_enquiry_body": "Du hast eine neue Anfrage über deinen
{supplier_name}-Verzeichniseintrag.",
"email_enquiry_lbl_from": "Von",
"email_enquiry_lbl_message": "Nachricht",
- "email_enquiry_respond_fast": "Antworte innerhalb von 24 Stunden f\u00fcr den besten Eindruck.",
+ "email_enquiry_respond_fast": "Antworte innerhalb von 24 Stunden für den besten ersten Eindruck.",
"email_enquiry_reply": "Antworte direkt an
{contact_email}.",
- "email_enquiry_subject": "Neue Anfrage von {contact_name} \u00fcber deinen Verzeichniseintrag",
- "email_enquiry_preheader": "Antworte, um mit diesem potenziellen Kunden in Kontakt zu treten",
-
+ "email_enquiry_subject": "Neue Anfrage von {contact_name} über deinen Verzeichniseintrag",
+ "email_enquiry_preheader": "Antworte, um mit diesem potenziellen Kunden in Kontakt zu kommen",
"email_business_plan_heading": "Dein Businessplan ist fertig",
"email_business_plan_body": "Dein Padel-Businessplan wurde als PDF erstellt und steht zum Download bereit.",
- "email_business_plan_includes": "Dein Plan enth\u00e4lt Investitions\u00fcbersicht, Umsatzprognosen und Break-Even-Analyse.",
- "email_business_plan_btn": "PDF herunterladen \u2192",
- "email_business_plan_quote_cta": "Bereit f\u00fcr den n\u00e4chsten Schritt?
Angebote von Anbietern einholen \u2192",
+ "email_business_plan_includes": "Dein Plan enthält Investitionsübersicht, Umsatzprognosen und Break-Even-Analyse.",
+ "email_business_plan_btn": "PDF herunterladen →",
+ "email_business_plan_quote_cta": "Bereit für den nächsten Schritt?
Angebote von Anbietern einholen →",
"email_business_plan_subject": "Dein Businessplan-PDF steht zum Download bereit",
- "email_business_plan_preheader": "Professioneller Padel-Finanzplan \u2014 jetzt herunterladen",
-
- "email_footer_tagline": "Die Planungsplattform f\u00fcr Padel-Unternehmer",
- "email_footer_copyright": "\u00a9 {year} {app_name}. Du erh\u00e4ltst diese E-Mail, weil du ein Konto hast oder eine Anfrage gestellt hast."
+ "email_business_plan_preheader": "Professioneller Padel-Finanzplan — jetzt herunterladen",
+ "email_footer_tagline": "Die Planungsplattform für Padel-Unternehmer",
+ "email_footer_copyright": "© {year} {app_name}. Du erhältst diese E-Mail, weil du ein Konto hast oder eine Anfrage gestellt hast.",
+ "footer_market_score": "Market Score",
+ "mscore_page_title": "Der padelnomics Market Score — So messen wir Marktpotenzial",
+ "mscore_meta_desc": "Der padelnomics Market Score bewertet Städte von 0 bis 100 nach ihrem Potenzial für Padel-Investitionen. Erfahre, wie Demografie, Wirtschaftskraft, Nachfragesignale und Datenabdeckung einfließen.",
+ "mscore_og_desc": "Ein datengestützter Komposit-Score (0–100), der die Attraktivität einer Stadt für Padelanlagen-Investitionen misst. Was steckt dahinter — und was bedeutet er für Deine Planung?",
+ "mscore_h1": "Der padelnomics Market Score",
+ "mscore_subtitle": "Ein datengestütztes Maß für die Attraktivität einer Stadt als Padel-Investitionsstandort.",
+ "mscore_what_h2": "Was der Score misst",
+ "mscore_what_intro": "Der Market Score ist ein Komposit-Index von 0 bis 100, der das Potenzial einer Stadt als Standort für Padelanlagen-Investitionen bewertet. Vier Datenkategorien fließen in eine einzige Kennzahl ein — damit Du schnell einschätzen kannst, welche Märkte sich genauer anzuschauen lohnen.",
+ "mscore_cat_demo_h3": "Demografie",
+ "mscore_cat_demo_p": "Bevölkerungsgröße als Indikator für den adressierbaren Markt. Größere Städte tragen in der Regel mehr Anlagen und höhere Auslastung.",
+ "mscore_cat_econ_h3": "Wirtschaftskraft",
+ "mscore_cat_econ_p": "Regionale Kaufkraft und Einkommensindikatoren. In Märkten mit höherem verfügbarem Einkommen ist die Nachfrage nach Freizeitsportarten wie Padel tendenziell stärker.",
+ "mscore_cat_demand_h3": "Nachfrageindikatoren",
+ "mscore_cat_demand_p": "Signale aus dem laufenden Betrieb bestehender Anlagen — Auslastungsraten, Buchungsdaten, Anzahl aktiver Standorte. Wo sich reale Nachfrage bereits messen lässt, ist das der stärkste Indikator.",
+ "mscore_cat_data_h3": "Datenqualität",
+ "mscore_cat_data_p": "Wie umfassend die Datenlage für eine Stadt ist. Ein Score auf Basis unvollständiger Daten ist weniger belastbar — wir machen das transparent, damit Du weißt, wo eigene Recherche sinnvoll ist.",
+ "mscore_read_h2": "Wie Du den Score liest",
+ "mscore_band_high_label": "70–100: Starker Markt",
+ "mscore_band_high_p": "Große Bevölkerung, hohe Wirtschaftskraft und nachgewiesene Nachfrage durch bestehende Anlagen. Diese Städte haben validierte Padel-Märkte mit belastbaren Benchmarks für die Finanzplanung.",
+ "mscore_band_mid_label": "45–69: Solides Mittelfeld",
+ "mscore_band_mid_p": "Gute Grundlagen mit Wachstumspotenzial. Genug Daten für fundierte Planung, aber weniger Wettbewerb als in den Top-Städten. Häufig der Sweet Spot für Neueinsteiger.",
+ "mscore_band_low_label": "Unter 45: Früher Markt",
+ "mscore_band_low_p": "Weniger validierte Daten oder kleinere Bevölkerung. Das heißt nicht, dass die Stadt unattraktiv ist — es kann weniger Wettbewerb und bessere Konditionen für Früheinsteiger bedeuten. Rechne mit mehr eigener Recherche vor Ort.",
+ "mscore_read_note": "Ein niedriger Score bedeutet nicht automatisch eine schlechte Investition. Er kann auf begrenzte Datenlage oder einen noch jungen Markt hinweisen — weniger Wettbewerb und günstigere Einstiegsbedingungen sind möglich.",
+ "mscore_sources_h2": "Datenquellen",
+ "mscore_sources_p": "Der Market Score basiert auf Daten europäischer Statistikämter (Bevölkerung und Wirtschaftsindikatoren), Buchungsplattformen für Padelanlagen (Standortanzahl, Preise, Auslastung) und geografischen Datenbanken (Standortdaten). Die Daten werden monatlich aktualisiert.",
+ "mscore_limits_h2": "Einschränkungen",
+ "mscore_limits_p1": "Der Score bildet die verfügbare Datenlage ab, nicht die absolute Marktwahrheit. Städte, in denen weniger Anlagen auf Buchungsplattformen erfasst sind, können bei den Nachfrageindikatoren niedrigere Werte zeigen — selbst wenn die lokale Nachfrage hoch ist.",
+ "mscore_limits_p2": "Der Score berücksichtigt keine lokalen Faktoren wie Immobilienkosten, Genehmigungszeiträume, Wettbewerbsdynamik oder regulatorische Rahmenbedingungen. Diese Aspekte sind entscheidend und erfordern Recherche vor Ort.",
+ "mscore_limits_p3": "Nutze den Market Score als Ausgangspunkt für die Priorisierung, nicht als finale Investitionsentscheidung. Im Finanzplaner kannst Du Dein konkretes Szenario durchrechnen.",
+ "mscore_cta_markets": "Stadtbewertungen ansehen",
+ "mscore_cta_planner": "Investment modellieren",
+ "mscore_faq_h2": "Häufig gestellte Fragen",
+ "mscore_faq_q1": "Was ist der padelnomics Market Score?",
+ "mscore_faq_a1": "Ein Komposit-Index von 0 bis 100, der die Attraktivität einer Stadt für Padelanlagen-Investitionen misst. Er kombiniert Demografie, Wirtschaftskraft, Nachfrageindikatoren und Datenqualität in einer vergleichbaren Kennzahl.",
+ "mscore_faq_q2": "Wie oft wird der Score aktualisiert?",
+ "mscore_faq_a2": "Monatlich. Neue Daten aus Statistikämtern, Buchungsplattformen und Standortdatenbanken werden regelmäßig extrahiert und verarbeitet. Der Score spiegelt immer die aktuellsten verfügbaren Daten wider.",
+ "mscore_faq_q3": "Warum hat meine Stadt einen niedrigen Score?",
+ "mscore_faq_a3": "Meist wegen begrenzter Datenabdeckung oder geringerer Bevölkerung. Ein niedriger Score bedeutet nicht, dass die Stadt unattraktiv ist — sondern dass uns weniger Daten zur Quantifizierung der Chance vorliegen. Eigene Recherche kann die Lücken schließen.",
+ "mscore_faq_q4": "Kann ich Scores länderübergreifend vergleichen?",
+ "mscore_faq_a4": "Ja. Die Methodik ist für alle Märkte einheitlich, sodass ein Score von 72 in Deutschland direkt vergleichbar ist mit einem 72 in Spanien oder Großbritannien.",
+ "mscore_faq_q5": "Garantiert ein hoher Score eine gute Investition?",
+ "mscore_faq_a5": "Nein. Der Score misst die Marktattraktivität auf Makroebene. Deine konkrete Investition hängt von Anlagentyp, Baukosten, Mietkonditionen und Dutzenden weiterer Faktoren ab. Im Finanzplaner kannst Du Dein Szenario mit echten Zahlen durchrechnen."
}
diff --git a/web/src/padelnomics/locales/en.json b/web/src/padelnomics/locales/en.json
index 70927f4..9b5a0ab 100644
--- a/web/src/padelnomics/locales/en.json
+++ b/web/src/padelnomics/locales/en.json
@@ -1641,5 +1641,49 @@
"email_business_plan_preheader": "Professional padel facility financial plan \u2014 download now",
"email_footer_tagline": "The padel business planning platform",
- "email_footer_copyright": "\u00a9 {year} {app_name}. You received this email because you have an account or submitted a request."
+ "email_footer_copyright": "\u00a9 {year} {app_name}. You received this email because you have an account or submitted a request.",
+
+ "footer_market_score": "Market Score",
+ "mscore_page_title": "The padelnomics Market Score \u2014 How We Measure Market Potential",
+ "mscore_meta_desc": "The padelnomics Market Score rates cities from 0 to 100 on their potential for padel investment. Learn how demographics, economic strength, demand signals, and data coverage feed into the score.",
+ "mscore_og_desc": "A data-driven composite score (0\u2013100) that measures how attractive a city is for padel court investment. See what goes into it and what it means for your planning.",
+ "mscore_h1": "The padelnomics Market Score",
+ "mscore_subtitle": "A data-driven measure of how attractive a city is for padel investment.",
+ "mscore_what_h2": "What It Measures",
+ "mscore_what_intro": "The Market Score is a composite index from 0 to 100 that evaluates a city\u2019s potential as a location for padel court investment. It combines four categories of data into a single number designed to help you prioritize markets worth investigating further.",
+ "mscore_cat_demo_h3": "Demographics",
+ "mscore_cat_demo_p": "Population size as a proxy for the addressable market. Larger cities generally support more venues and higher utilization.",
+ "mscore_cat_econ_h3": "Economic Strength",
+ "mscore_cat_econ_p": "Regional purchasing power and income indicators. Markets where people have higher disposable income tend to sustain stronger demand for leisure sports like padel.",
+ "mscore_cat_demand_h3": "Demand Evidence",
+ "mscore_cat_demand_p": "Signals from existing venue activity \u2014 occupancy rates, booking data, and the number of operating venues. Where real demand is already measurable, it\u2019s the strongest indicator.",
+ "mscore_cat_data_h3": "Data Completeness",
+ "mscore_cat_data_p": "How much data we have for that city. A score influenced by incomplete data is less reliable \u2014 we surface this explicitly so you know when to dig deeper on your own.",
+ "mscore_read_h2": "How To Read the Score",
+ "mscore_band_high_label": "70\u2013100: Strong market",
+ "mscore_band_high_p": "Large population, economic power, and proven demand from existing venues. These cities have validated padel markets with reliable benchmarks for financial planning.",
+ "mscore_band_mid_label": "45\u201369: Solid mid-tier",
+ "mscore_band_mid_p": "Good fundamentals with room for growth. Enough data to plan with confidence, but less competition than top-tier cities. Often the sweet spot for new entrants.",
+ "mscore_band_low_label": "Below 45: Early-stage market",
+ "mscore_band_low_p": "Less validated data or smaller populations. This does not mean a city is a bad investment \u2014 it may mean less competition and first-mover advantage. Expect to do more local research.",
+ "mscore_read_note": "A lower score does not mean a city is a bad investment. It may indicate less available data or a market still developing \u2014 which can mean less competition and better terms for early entrants.",
+ "mscore_sources_h2": "Data Sources",
+ "mscore_sources_p": "The Market Score draws on data from European statistical offices (population and economic indicators), court booking platforms (venue counts, pricing, occupancy), and geographic databases (venue locations). Data is refreshed monthly as new extractions run.",
+ "mscore_limits_h2": "Limitations",
+ "mscore_limits_p1": "The score reflects available data, not absolute market truth. Cities where fewer venues are tracked on booking platforms may score lower on demand evidence \u2014 even if local demand is strong.",
+ "mscore_limits_p2": "The score does not account for local factors like real estate costs, permitting timelines, competitive dynamics, or regulatory environment. These matter enormously and require on-the-ground research.",
+ "mscore_limits_p3": "Use the Market Score as a starting point for prioritization, not a final investment decision. The financial planner is where you model your specific scenario.",
+ "mscore_cta_markets": "Browse city scores",
+ "mscore_cta_planner": "Model your investment",
+ "mscore_faq_h2": "Frequently Asked Questions",
+ "mscore_faq_q1": "What is the padelnomics Market Score?",
+ "mscore_faq_a1": "A composite index from 0 to 100 that measures how attractive a city is for padel court investment. It combines demographics, economic strength, demand evidence, and data completeness into a single comparable number.",
+ "mscore_faq_q2": "How often is the score updated?",
+ "mscore_faq_a2": "Monthly. New data from statistical offices, booking platforms, and venue databases is extracted and processed on a regular cycle. Scores reflect the most recent available data.",
+ "mscore_faq_q3": "Why is my city\u2019s score low?",
+ "mscore_faq_a3": "Usually because of limited data coverage or smaller population. A low score doesn\u2019t mean the city is unattractive \u2014 it means we have less data to quantify the opportunity. Local research can fill the gaps.",
+ "mscore_faq_q4": "Can I compare scores across countries?",
+ "mscore_faq_a4": "Yes. The methodology is consistent across all markets we track, so a score of 72 in Germany is directly comparable to a 72 in Spain or the UK.",
+ "mscore_faq_q5": "Does a high score guarantee a good investment?",
+ "mscore_faq_a5": "No. The score measures market attractiveness at a macro level. Your specific investment depends on venue type, build costs, lease terms, and dozens of other factors. Use the financial planner to model your scenario with real numbers."
}
diff --git a/web/src/padelnomics/migrations/migrate.py b/web/src/padelnomics/migrations/migrate.py
index 08f514c..8705e5d 100644
--- a/web/src/padelnomics/migrations/migrate.py
+++ b/web/src/padelnomics/migrations/migrate.py
@@ -34,12 +34,15 @@ Design decisions
"""
import importlib
+import logging
import os
import re
import sqlite3
import sys
from pathlib import Path
+logger = logging.getLogger(__name__)
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from dotenv import load_dotenv
@@ -89,7 +92,7 @@ def migrate(db_path=None):
if pending:
for name in pending:
- print(f" Applying {name}...")
+ logger.info("Applying %s...", name)
mod = importlib.import_module(
f"padelnomics.migrations.versions.{name}"
)
@@ -98,9 +101,9 @@ def migrate(db_path=None):
"INSERT INTO _migrations (name) VALUES (?)", (name,)
)
conn.commit()
- print(f"✓ Applied {len(pending)} migration(s): {db_path}")
+ logger.info("Applied %s migration(s): %s", len(pending), db_path)
else:
- print(f"✓ All migrations already applied: {db_path}")
+ logger.info("All migrations already applied: %s", db_path)
# Show tables (excluding internal sqlite/fts tables)
cursor = conn.execute(
@@ -109,10 +112,11 @@ def migrate(db_path=None):
" ORDER BY name"
)
tables = [row[0] for row in cursor.fetchall()]
- print(f" Tables: {', '.join(tables)}")
+ logger.info("Tables: %s", ", ".join(tables))
conn.close()
if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
migrate()
diff --git a/web/src/padelnomics/migrations/versions/0020_articles_unique_url_language.py b/web/src/padelnomics/migrations/versions/0020_articles_unique_url_language.py
new file mode 100644
index 0000000..0cda8d6
--- /dev/null
+++ b/web/src/padelnomics/migrations/versions/0020_articles_unique_url_language.py
@@ -0,0 +1,81 @@
+"""Change articles unique constraint from url_path alone to (url_path, language).
+
+Previously url_path was declared UNIQUE, which prevented multiple languages
+from sharing the same url_path (e.g. /markets/germany/berlin for both de and en).
+"""
+
+
+def up(conn) -> None:
+ # ── 1. Drop FTS triggers + virtual table ──────────────────────────────────
+ conn.execute("DROP TRIGGER IF EXISTS articles_ai")
+ conn.execute("DROP TRIGGER IF EXISTS articles_ad")
+ conn.execute("DROP TRIGGER IF EXISTS articles_au")
+ conn.execute("DROP TABLE IF EXISTS articles_fts")
+
+ # ── 2. Recreate articles with UNIQUE(url_path, language) ──────────────────
+ conn.execute("""
+ CREATE TABLE articles_new (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ url_path TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ title TEXT NOT NULL,
+ meta_description TEXT,
+ country TEXT,
+ region TEXT,
+ og_image_url TEXT,
+ status TEXT NOT NULL DEFAULT 'draft',
+ published_at TEXT,
+ template_slug TEXT,
+ language TEXT NOT NULL DEFAULT 'en',
+ date_modified TEXT,
+ seo_head TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ updated_at TEXT,
+ UNIQUE(url_path, language)
+ )
+ """)
+ conn.execute("""
+ INSERT INTO articles_new
+ (id, url_path, slug, title, meta_description, country, region,
+ og_image_url, status, published_at, template_slug, language,
+ date_modified, seo_head, created_at, updated_at)
+ SELECT id, url_path, slug, title, meta_description, country, region,
+ og_image_url, status, published_at, template_slug, language,
+ date_modified, seo_head, created_at, updated_at
+ FROM articles
+ """)
+ conn.execute("DROP TABLE articles")
+ conn.execute("ALTER TABLE articles_new RENAME TO articles")
+
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_articles_url_path ON articles(url_path)")
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_articles_url_lang ON articles(url_path, language)")
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_articles_slug ON articles(slug)")
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_articles_status ON articles(status, published_at)")
+
+ # ── 3. Recreate FTS + triggers ─────────────────────────────────────────────
+ conn.execute("""
+ CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5(
+ title, meta_description, country, region,
+ content='articles', content_rowid='id'
+ )
+ """)
+ conn.execute("""
+ CREATE TRIGGER IF NOT EXISTS articles_ai AFTER INSERT ON articles BEGIN
+ INSERT INTO articles_fts(rowid, title, meta_description, country, region)
+ VALUES (new.id, new.title, new.meta_description, new.country, new.region);
+ END
+ """)
+ conn.execute("""
+ CREATE TRIGGER IF NOT EXISTS articles_ad AFTER DELETE ON articles BEGIN
+ INSERT INTO articles_fts(articles_fts, rowid, title, meta_description, country, region)
+ VALUES ('delete', old.id, old.title, old.meta_description, old.country, old.region);
+ END
+ """)
+ conn.execute("""
+ CREATE TRIGGER IF NOT EXISTS articles_au AFTER UPDATE ON articles BEGIN
+ INSERT INTO articles_fts(articles_fts, rowid, title, meta_description, country, region)
+ VALUES ('delete', old.id, old.title, old.meta_description, old.country, old.region);
+ INSERT INTO articles_fts(rowid, title, meta_description, country, region)
+ VALUES (new.id, new.title, new.meta_description, new.country, new.region);
+ END
+ """)
diff --git a/web/src/padelnomics/planner/routes.py b/web/src/padelnomics/planner/routes.py
index b5f6247..b888dad 100644
--- a/web/src/padelnomics/planner/routes.py
+++ b/web/src/padelnomics/planner/routes.py
@@ -3,10 +3,12 @@ Planner domain: padel court financial planner + scenario management.
"""
import json
+import logging
import math
-from datetime import datetime
from pathlib import Path
+logger = logging.getLogger(__name__)
+
from quart import Blueprint, Response, g, jsonify, render_template, request
from ..auth.routes import login_required
@@ -18,6 +20,7 @@ from ..core import (
fetch_all,
fetch_one,
get_paddle_price,
+ utcnow_iso,
)
from ..i18n import get_translations
from .calculator import COUNTRY_CURRENCY, CURRENCY_DEFAULT, calc, validate_state
@@ -502,7 +505,7 @@ async def save_scenario():
location = form.get("location", "")
scenario_id = form.get("scenario_id")
- now = datetime.utcnow().isoformat()
+ now = utcnow_iso()
is_first_save = not scenario_id and (await count_scenarios(g.user["id"])) == 0
@@ -533,7 +536,7 @@ async def save_scenario():
}
)
except Exception as e:
- print(f"[NURTURE] Failed to add {g.user['email']} to audience: {e}")
+ logger.warning("Failed to add %s to nurture audience: %s", g.user["email"], e)
lang = g.get("lang", "en")
t = get_translations(lang)
@@ -563,7 +566,7 @@ async def get_scenario(scenario_id: int):
@login_required
@csrf_protect
async def delete_scenario(scenario_id: int):
- now = datetime.utcnow().isoformat()
+ now = utcnow_iso()
await execute(
"UPDATE scenarios SET deleted_at = ? WHERE id = ? AND user_id = ? AND deleted_at IS NULL",
(now, scenario_id, g.user["id"]),
diff --git a/web/src/padelnomics/planner/templates/export.html b/web/src/padelnomics/planner/templates/export.html
index a2446dd..9d9d9af 100644
--- a/web/src/padelnomics/planner/templates/export.html
+++ b/web/src/padelnomics/planner/templates/export.html
@@ -71,6 +71,7 @@
+
@@ -106,9 +107,16 @@
{% block scripts %}
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/web/src/padelnomics/scripts/refresh_from_daas.py b/web/src/padelnomics/scripts/refresh_from_daas.py
index 04e9b93..0fcafa6 100644
--- a/web/src/padelnomics/scripts/refresh_from_daas.py
+++ b/web/src/padelnomics/scripts/refresh_from_daas.py
@@ -34,12 +34,15 @@ Fields mapped (DuckDB → data_json camelCase key):
import argparse
import json
+import logging
import os
import sqlite3
from pathlib import Path
from dotenv import load_dotenv
+logger = logging.getLogger(__name__)
+
load_dotenv()
DATABASE_PATH = os.getenv("DATABASE_PATH", "data/app.db")
@@ -67,13 +70,13 @@ def _load_analytics(city_slugs: list[str]) -> dict[str, dict]:
"""
path = Path(DUCKDB_PATH)
if not path.exists():
- print(f" [analytics] DuckDB not found at {path} — skipping analytics refresh.")
+ logger.warning("DuckDB not found at %s — skipping analytics refresh.", path)
return {}
try:
import duckdb
except ImportError:
- print(" [analytics] duckdb not installed — skipping analytics refresh.")
+ logger.warning("duckdb not installed — skipping analytics refresh.")
return {}
result: dict[str, dict] = {}
@@ -98,7 +101,7 @@ def _load_analytics(city_slugs: list[str]) -> dict[str, dict]:
result[slug] = overrides
except Exception as exc:
- print(f" [analytics] DuckDB query failed: {exc}")
+ logger.error("DuckDB query failed: %s", exc)
return result
@@ -124,13 +127,13 @@ def refresh(dry_run: bool = False) -> int:
city_slug_to_ids.setdefault(slug, []).append(row["id"])
if not city_slug_to_ids:
- print("No template_data rows with city_slug found.")
+ logger.info("No template_data rows with city_slug found.")
conn.close()
return 0
analytics = _load_analytics(list(city_slug_to_ids.keys()))
if not analytics:
- print("No analytics data found — nothing to update.")
+ logger.info("No analytics data found — nothing to update.")
conn.close()
return 0
@@ -154,13 +157,13 @@ def refresh(dry_run: bool = False) -> int:
data.update(overrides)
if dry_run:
- print(f" [dry-run] id={row_id} city_slug={slug}: {changed}")
+ logger.info("[dry-run] id=%s city_slug=%s: %s", row_id, slug, changed)
else:
conn.execute(
"UPDATE template_data SET data_json = ?, updated_at = datetime('now') WHERE id = ?",
(json.dumps(data), row_id),
)
- print(f" Updated id={row_id} city_slug={slug}: {list(changed.keys())}")
+ logger.info("Updated id=%s city_slug=%s: %s", row_id, slug, list(changed.keys()))
updated += 1
if not dry_run:
@@ -184,7 +187,7 @@ def _trigger_generation() -> None:
headers={"X-Admin-Key": admin_key},
)
with urllib.request.urlopen(req, timeout=120) as resp:
- print(f" Generation triggered: HTTP {resp.status}")
+ logger.info("Generation triggered: HTTP %s", resp.status)
def main() -> None:
@@ -195,14 +198,17 @@ def main() -> None:
help="Trigger article re-generation after updating")
args = parser.parse_args()
- print(f"{'[DRY RUN] ' if args.dry_run else ''}Refreshing template_data from DuckDB…")
+ prefix = "[DRY RUN] " if args.dry_run else ""
+ logger.info("%sRefreshing template_data from DuckDB...", prefix)
count = refresh(dry_run=args.dry_run)
- print(f"{'Would update' if args.dry_run else 'Updated'} {count} rows.")
+ action = "Would update" if args.dry_run else "Updated"
+ logger.info("%s %s rows.", action, count)
if args.generate and count > 0 and not args.dry_run:
- print("Triggering article generation…")
+ logger.info("Triggering article generation...")
_trigger_generation()
if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
main()
diff --git a/web/src/padelnomics/scripts/seed_content.py b/web/src/padelnomics/scripts/seed_content.py
index 23ee2f7..27aa33a 100644
--- a/web/src/padelnomics/scripts/seed_content.py
+++ b/web/src/padelnomics/scripts/seed_content.py
@@ -15,14 +15,17 @@ Usage:
import asyncio
import json
+import logging
import os
import sqlite3
import sys
-from datetime import date, timedelta
+from datetime import UTC, date, datetime, timedelta
from pathlib import Path
from dotenv import load_dotenv
+logger = logging.getLogger(__name__)
+
load_dotenv()
DATABASE_PATH = os.getenv("DATABASE_PATH", "data/app.db")
@@ -1363,7 +1366,7 @@ def seed_templates(conn: sqlite3.Connection) -> dict[str, int]:
).fetchone()
if existing:
- print(f" Template '{tmpl['slug']}' already exists (id={existing[0]}), skipping.")
+ logger.info(" Template '%s' already exists (id=%s), skipping.", tmpl["slug"], existing[0])
template_ids[tmpl["slug"]] = existing[0]
else:
cur = conn.execute(
@@ -1383,14 +1386,14 @@ def seed_templates(conn: sqlite3.Connection) -> dict[str, int]:
),
)
template_ids[tmpl["slug"]] = cur.lastrowid
- print(f" Created template '{tmpl['slug']}' (id={cur.lastrowid})")
+ logger.info(" Created template '%s' (id=%s)", tmpl["slug"], cur.lastrowid)
return template_ids
def seed_data_rows(conn: sqlite3.Connection, template_ids: dict[str, int]) -> int:
"""Insert template_data rows for all cities × languages. Returns count inserted."""
- now = __import__("datetime").datetime.utcnow().isoformat()
+ now = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
inserted = 0
en_id = template_ids.get("city-padel-cost-en")
@@ -1411,7 +1414,7 @@ def seed_data_rows(conn: sqlite3.Connection, template_ids: dict[str, int]) -> in
).fetchone()
if existing:
- print(f" Data row '{city_slug}' ({lang}) already exists, skipping.")
+ logger.info(" Data row '%s' (%s) already exists, skipping.", city_slug, lang)
else:
conn.execute(
"""INSERT INTO template_data (template_id, data_json, created_at)
@@ -1419,7 +1422,7 @@ def seed_data_rows(conn: sqlite3.Connection, template_ids: dict[str, int]) -> in
(tmpl_id, data_json, now),
)
inserted += 1
- print(f" Inserted data row '{city_slug}' ({lang})")
+ logger.info(" Inserted data row '%s' (%s)", city_slug, lang)
return inserted
@@ -1432,7 +1435,7 @@ async def generate_articles(template_ids: dict[str, int]) -> None:
from padelnomics.admin.routes import _generate_from_template # noqa: PLC0415
from padelnomics.core import close_db, fetch_one, init_db
- print("\nInitialising database connection...")
+ logger.info("Initialising database connection...")
await init_db(DATABASE_PATH)
start_date = date.today() - timedelta(days=30) # backdate so all are immediately live
@@ -1441,9 +1444,9 @@ async def generate_articles(template_ids: dict[str, int]) -> None:
template = await fetch_one("SELECT * FROM article_templates WHERE id = ?", (tmpl_id,))
assert template is not None, f"Template '{slug}' not found in DB"
- print(f"\nGenerating articles for template '{slug}'...")
+ logger.info("Generating articles for template '%s'...", slug)
count = await _generate_from_template(template, start_date, articles_per_day=3)
- print(f" Generated {count} articles.")
+ logger.info(" Generated %s articles.", count)
await close_db()
@@ -1463,28 +1466,29 @@ def main() -> None:
conn.execute("PRAGMA foreign_keys=ON")
conn.row_factory = sqlite3.Row
- print("Seeding article templates...")
+ logger.info("Seeding article templates...")
template_ids = seed_templates(conn)
- print("\nSeeding city data rows...")
+ logger.info("Seeding city data rows...")
inserted = seed_data_rows(conn, template_ids)
conn.commit()
conn.close()
- print(f"\nDone. {inserted} data rows inserted.")
- print("Templates and data rows are visible in admin → Templates.")
+ logger.info("Done. %s data rows inserted.", inserted)
+ logger.info("Templates and data rows are visible in admin -> Templates.")
if "--generate" in sys.argv:
- print("\nRunning article generation pipeline...")
+ logger.info("Running article generation pipeline...")
asyncio.run(generate_articles(template_ids))
- print("\nGeneration complete. Check admin → Articles.")
+ logger.info("Generation complete. Check admin -> Articles.")
else:
- print(
- "\nTo generate articles, either:\n"
+ logger.info(
+ "To generate articles, either:\n"
" 1. Run: uv run python -m padelnomics.scripts.seed_content --generate\n"
- " 2. Or visit admin → Templates → (template) → Generate"
+ " 2. Or visit admin -> Templates -> (template) -> Generate"
)
if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
main()
diff --git a/web/src/padelnomics/scripts/seed_dev_data.py b/web/src/padelnomics/scripts/seed_dev_data.py
index 96dfe37..ecd085c 100644
--- a/web/src/padelnomics/scripts/seed_dev_data.py
+++ b/web/src/padelnomics/scripts/seed_dev_data.py
@@ -7,14 +7,17 @@ Usage:
uv run python -m padelnomics.scripts.seed_dev_data
"""
+import logging
import os
import sqlite3
import sys
-from datetime import datetime, timedelta
+from datetime import UTC, datetime, timedelta
from pathlib import Path
from dotenv import load_dotenv
+logger = logging.getLogger(__name__)
+
load_dotenv()
DATABASE_PATH = os.getenv("DATABASE_PATH", "data/app.db")
@@ -284,7 +287,7 @@ LEADS = [
def main():
db_path = DATABASE_PATH
if not Path(db_path).exists():
- print(f"ERROR: Database not found at {db_path}. Run migrations first.")
+ logger.error("Database not found at %s. Run migrations first.", db_path)
sys.exit(1)
conn = sqlite3.connect(db_path)
@@ -292,37 +295,37 @@ def main():
conn.execute("PRAGMA foreign_keys=ON")
conn.row_factory = sqlite3.Row
- now = datetime.utcnow()
+ now = datetime.now(UTC)
# 1. Create dev user
- print("Creating dev user (dev@localhost)...")
+ logger.info("Creating dev user (dev@localhost)...")
existing = conn.execute("SELECT id FROM users WHERE email = 'dev@localhost'").fetchone()
if existing:
dev_user_id = existing["id"]
- print(f" Already exists (id={dev_user_id})")
+ logger.info(" Already exists (id=%s)", dev_user_id)
else:
cursor = conn.execute(
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
- ("dev@localhost", "Dev User", now.isoformat()),
+ ("dev@localhost", "Dev User", now.strftime("%Y-%m-%d %H:%M:%S")),
)
dev_user_id = cursor.lastrowid
- print(f" Created (id={dev_user_id})")
+ logger.info(" Created (id=%s)", dev_user_id)
# Grant admin role to dev user
conn.execute(
"INSERT OR IGNORE INTO user_roles (user_id, role) VALUES (?, 'admin')",
(dev_user_id,),
)
- print(" Admin role granted")
+ logger.info(" Admin role granted")
# 2. Seed suppliers
- print(f"\nSeeding {len(SUPPLIERS)} suppliers...")
+ logger.info("Seeding %s suppliers...", len(SUPPLIERS))
supplier_ids = {}
for s in SUPPLIERS:
existing = conn.execute("SELECT id FROM suppliers WHERE slug = ?", (s["slug"],)).fetchone()
if existing:
supplier_ids[s["slug"]] = existing["id"]
- print(f" {s['name']} already exists (id={existing['id']})")
+ logger.info(" %s already exists (id=%s)", s["name"], existing["id"])
continue
cursor = conn.execute(
@@ -336,20 +339,20 @@ def main():
s["website"], s["description"], s["category"], s["tier"],
s["credit_balance"], s["monthly_credits"], s["contact_name"],
s["contact_email"], s["years_in_business"], s["project_count"],
- s["service_area"], now.isoformat(),
+ s["service_area"], now.strftime("%Y-%m-%d %H:%M:%S"),
),
)
supplier_ids[s["slug"]] = cursor.lastrowid
- print(f" {s['name']} -> id={cursor.lastrowid}")
+ logger.info(" %s -> id=%s", s["name"], cursor.lastrowid)
# 3. Claim paid suppliers — each gets its own owner user + subscription
- print("\nClaiming paid suppliers with owner accounts...")
+ logger.info("Claiming paid suppliers with owner accounts...")
claimed_suppliers = [
("padeltech-gmbh", "supplier_pro", "hans@padeltech.example.com", "Hans Weber"),
("courtbuild-spain", "supplier_growth", "maria@courtbuild.example.com", "Maria Garcia"),
("desert-padel-fze", "supplier_pro", "ahmed@desertpadel.example.com", "Ahmed Al-Rashid"),
]
- period_end = (now + timedelta(days=30)).isoformat()
+ period_end = (now + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S")
for slug, plan, email, name in claimed_suppliers:
sid = supplier_ids.get(slug)
if not sid:
@@ -364,14 +367,14 @@ def main():
else:
cursor = conn.execute(
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
- (email, name, now.isoformat()),
+ (email, name, now.strftime("%Y-%m-%d %H:%M:%S")),
)
owner_id = cursor.lastrowid
# Claim the supplier
conn.execute(
"UPDATE suppliers SET claimed_by = ?, claimed_at = ? WHERE id = ? AND claimed_by IS NULL",
- (owner_id, now.isoformat(), sid),
+ (owner_id, now.strftime("%Y-%m-%d %H:%M:%S"), sid),
)
# Create billing customer record
@@ -382,7 +385,7 @@ def main():
conn.execute(
"""INSERT INTO billing_customers (user_id, provider_customer_id, created_at)
VALUES (?, ?, ?)""",
- (owner_id, f"ctm_dev_{slug}", now.isoformat()),
+ (owner_id, f"ctm_dev_{slug}", now.strftime("%Y-%m-%d %H:%M:%S")),
)
# Create active subscription
@@ -396,12 +399,12 @@ def main():
current_period_end, created_at)
VALUES (?, ?, 'active', ?, ?, ?)""",
(owner_id, plan, f"sub_dev_{slug}",
- period_end, now.isoformat()),
+ period_end, now.strftime("%Y-%m-%d %H:%M:%S")),
)
- print(f" {slug} -> owner {email} ({plan})")
+ logger.info(" %s -> owner %s (%s)", slug, email, plan)
# 4. Seed leads
- print(f"\nSeeding {len(LEADS)} leads...")
+ logger.info("Seeding %s leads...", len(LEADS))
lead_ids = []
for i, lead in enumerate(LEADS):
from padelnomics.credits import HEAT_CREDIT_COSTS
@@ -426,10 +429,10 @@ def main():
),
)
lead_ids.append(cursor.lastrowid)
- print(f" Lead #{cursor.lastrowid}: {lead['contact_name']} ({lead['heat_score']}, {lead['country']})")
+ logger.info(" Lead #%s: %s (%s, %s)", cursor.lastrowid, lead["contact_name"], lead["heat_score"], lead["country"])
# 5. Add credit ledger entries for claimed suppliers
- print("\nAdding credit ledger entries...")
+ logger.info("Adding credit ledger entries...")
for slug in ("padeltech-gmbh", "courtbuild-spain", "desert-padel-fze"):
sid = supplier_ids.get(slug)
if not sid:
@@ -448,10 +451,10 @@ def main():
VALUES (?, ?, ?, 'admin_adjustment', 'Welcome bonus', ?)""",
(sid, 10, monthly + 10, (now - timedelta(days=25)).isoformat()),
)
- print(f" {slug}: 2 ledger entries")
+ logger.info(" %s: 2 ledger entries", slug)
# 6. Add lead forwards for testing
- print("\nAdding lead forwards...")
+ logger.info("Adding lead forwards...")
padeltech_id = supplier_ids.get("padeltech-gmbh")
if padeltech_id and len(lead_ids) >= 2:
for lead_id in lead_ids[:2]:
@@ -476,15 +479,16 @@ def main():
(padeltech_id, 80, lead_id, f"Unlocked lead #{lead_id}",
(now - timedelta(hours=6)).isoformat()),
)
- print(f" PadelTech unlocked lead #{lead_id}")
+ logger.info(" PadelTech unlocked lead #%s", lead_id)
conn.commit()
conn.close()
- print(f"\nDone! Seed data written to {db_path}")
- print(" Login: /auth/dev-login?email=dev@localhost")
- print(" Admin: set ADMIN_EMAILS=dev@localhost in .env, then dev-login grants admin role")
+ logger.info("Done! Seed data written to %s", db_path)
+ logger.info(" Login: /auth/dev-login?email=dev@localhost")
+ logger.info(" Admin: set ADMIN_EMAILS=dev@localhost in .env, then dev-login grants admin role")
if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
main()
diff --git a/web/src/padelnomics/scripts/setup_paddle.py b/web/src/padelnomics/scripts/setup_paddle.py
index 240f436..849d457 100644
--- a/web/src/padelnomics/scripts/setup_paddle.py
+++ b/web/src/padelnomics/scripts/setup_paddle.py
@@ -6,6 +6,7 @@ Commands:
uv run python -m padelnomics.scripts.setup_paddle --sync # re-populate DB from existing Paddle products
"""
+import logging
import os
import re
import sqlite3
@@ -13,6 +14,8 @@ import sys
from pathlib import Path
from dotenv import load_dotenv
+
+logger = logging.getLogger(__name__)
from paddle_billing import Client as PaddleClient
from paddle_billing import Environment, Options
from paddle_billing.Entities.Events.EventTypeName import EventTypeName
@@ -33,7 +36,8 @@ DATABASE_PATH = os.getenv("DATABASE_PATH", "data/app.db")
BASE_URL = os.getenv("BASE_URL", "http://localhost:5000")
if not PADDLE_API_KEY:
- print("ERROR: Set PADDLE_API_KEY in .env first")
+ logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
+ logger.error("Set PADDLE_API_KEY in .env first")
sys.exit(1)
@@ -202,7 +206,7 @@ _PRODUCT_BY_NAME = {p["name"]: p for p in PRODUCTS}
def _open_db():
db_path = DATABASE_PATH
if not Path(db_path).exists():
- print(f"ERROR: Database not found at {db_path}. Run migrations first.")
+ logger.error("Database not found at %s. Run migrations first.", db_path)
sys.exit(1)
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
@@ -221,7 +225,7 @@ def _write_product(conn, key, product_id, price_id, name, price_cents, billing_t
def sync(paddle, conn):
"""Fetch existing products from Paddle and re-populate paddle_products table."""
- print(f"Syncing products from Paddle ({PADDLE_ENVIRONMENT})...\n")
+ logger.info("Syncing products from Paddle (%s)...", PADDLE_ENVIRONMENT)
products = paddle.products.list(ListProducts(includes=[Includes.Prices]))
@@ -231,7 +235,7 @@ def sync(paddle, conn):
if not spec:
continue
if not product.prices or len(product.prices) == 0:
- print(f" SKIP {spec['key']}: no prices on {product.id}")
+ logger.warning(" SKIP %s: no prices on %s", spec["key"], product.id)
continue
# Use the first active price
@@ -241,26 +245,26 @@ def sync(paddle, conn):
spec["name"], spec["price"], spec["billing_type"],
)
matched += 1
- print(f" {spec['key']}: {product.id} / {price.id}")
+ logger.info(" %s: %s / %s", spec["key"], product.id, price.id)
conn.commit()
if matched == 0:
- print("\nNo matching products found in Paddle. Run without --sync first.")
+ logger.warning("No matching products found in Paddle. Run without --sync first.")
else:
- print(f"\n✓ {matched}/{len(PRODUCTS)} products synced to DB")
+ logger.info("%s/%s products synced to DB", matched, len(PRODUCTS))
def create(paddle, conn):
"""Create new products and prices in Paddle, write to DB, set up webhook."""
- print(f"Creating products in {PADDLE_ENVIRONMENT}...\n")
+ logger.info("Creating products in %s...", PADDLE_ENVIRONMENT)
for spec in PRODUCTS:
product = paddle.products.create(CreateProduct(
name=spec["name"],
tax_category=TaxCategory.Standard,
))
- print(f" Product: {spec['name']} -> {product.id}")
+ logger.info(" Product: %s -> %s", spec["name"], product.id)
price_kwargs = {
"description": spec["name"],
@@ -276,7 +280,7 @@ def create(paddle, conn):
price_kwargs["billing_cycle"] = Duration(interval=Interval.Month, frequency=1)
price = paddle.prices.create(CreatePrice(**price_kwargs))
- print(f" Price: {spec['key']} = {price.id}")
+ logger.info(" Price: %s = %s", spec["key"], price.id)
_write_product(
conn, spec["key"], product.id, price.id,
@@ -284,7 +288,7 @@ def create(paddle, conn):
)
conn.commit()
- print("\n✓ All products written to DB")
+ logger.info("All products written to DB")
# -- Notification destination (webhook) -----------------------------------
@@ -298,8 +302,8 @@ def create(paddle, conn):
EventTypeName.TransactionCompleted,
]
- print("\nCreating webhook notification destination...")
- print(f" URL: {webhook_url}")
+ logger.info("Creating webhook notification destination...")
+ logger.info(" URL: %s", webhook_url)
notification_setting = paddle.notification_settings.create(
CreateNotificationSetting(
@@ -313,8 +317,8 @@ def create(paddle, conn):
)
webhook_secret = notification_setting.endpoint_secret_key
- print(f" ID: {notification_setting.id}")
- print(f" Secret: {webhook_secret}")
+ logger.info(" ID: %s", notification_setting.id)
+ logger.info(" Secret: %s", webhook_secret)
env_path = Path(".env")
env_vars = {
@@ -331,13 +335,13 @@ def create(paddle, conn):
else:
env_text = env_text.rstrip("\n") + f"\n{replacement}\n"
env_path.write_text(env_text)
- print("\n✓ PADDLE_WEBHOOK_SECRET and PADDLE_NOTIFICATION_SETTING_ID written to .env")
+ logger.info("PADDLE_WEBHOOK_SECRET and PADDLE_NOTIFICATION_SETTING_ID written to .env")
else:
- print("\n Add to .env:")
+ logger.info("Add to .env:")
for key, value in env_vars.items():
- print(f" {key}={value}")
+ logger.info(" %s=%s", key, value)
- print("\nDone. dev_run.sh will start ngrok and update the webhook URL automatically.")
+ logger.info("Done. dev_run.sh will start ngrok and update the webhook URL automatically.")
def main():
@@ -355,4 +359,5 @@ def main():
if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
main()
diff --git a/web/src/padelnomics/seo/_bing.py b/web/src/padelnomics/seo/_bing.py
index 5a76446..df6192a 100644
--- a/web/src/padelnomics/seo/_bing.py
+++ b/web/src/padelnomics/seo/_bing.py
@@ -3,12 +3,12 @@
Uses an API key for auth. Fetches query stats and page stats.
"""
-from datetime import datetime, timedelta
+from datetime import UTC, datetime, timedelta
from urllib.parse import urlparse
import httpx
-from ..core import config, execute
+from ..core import config, execute, utcnow, utcnow_iso
_TIMEOUT_SECONDS = 30
@@ -27,7 +27,7 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
if not config.BING_WEBMASTER_API_KEY or not config.BING_SITE_URL:
return 0 # Bing not configured — skip silently
- started_at = datetime.utcnow()
+ started_at = utcnow()
try:
rows_synced = 0
@@ -48,14 +48,14 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
if not isinstance(entries, list):
entries = []
- cutoff = datetime.utcnow() - timedelta(days=days_back)
+ cutoff = utcnow() - timedelta(days=days_back)
for entry in entries:
# Bing date format: "/Date(1708905600000)/" (ms since epoch)
date_str = entry.get("Date", "")
if "/Date(" in date_str:
ms = int(date_str.split("(")[1].split(")")[0])
- entry_date = datetime.utcfromtimestamp(ms / 1000)
+ entry_date = datetime.fromtimestamp(ms / 1000, tz=UTC)
else:
continue
@@ -99,7 +99,7 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
date_str = entry.get("Date", "")
if "/Date(" in date_str:
ms = int(date_str.split("(")[1].split(")")[0])
- entry_date = datetime.utcfromtimestamp(ms / 1000)
+ entry_date = datetime.fromtimestamp(ms / 1000, tz=UTC)
else:
continue
@@ -122,21 +122,21 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
)
rows_synced += 1
- duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
+ duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, started_at, completed_at, duration_ms)
VALUES ('bing', 'success', ?, ?, ?, ?)""",
- (rows_synced, started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
+ (rows_synced, started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
)
return rows_synced
except Exception as exc:
- duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
+ duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
VALUES ('bing', 'failed', 0, ?, ?, ?, ?)""",
- (str(exc), started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
+ (str(exc), started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
)
raise
diff --git a/web/src/padelnomics/seo/_gsc.py b/web/src/padelnomics/seo/_gsc.py
index 83fa70e..dbdce33 100644
--- a/web/src/padelnomics/seo/_gsc.py
+++ b/web/src/padelnomics/seo/_gsc.py
@@ -5,11 +5,11 @@ is synchronous, so sync runs in asyncio.to_thread().
"""
import asyncio
-from datetime import datetime, timedelta
+from datetime import timedelta
from pathlib import Path
from urllib.parse import urlparse
-from ..core import config, execute
+from ..core import config, execute, utcnow, utcnow_iso
# GSC returns max 25K rows per request
_ROWS_PER_PAGE = 25_000
@@ -95,11 +95,11 @@ async def sync_gsc(days_back: int = 3, max_pages: int = 10) -> int:
if not config.GSC_SERVICE_ACCOUNT_PATH or not config.GSC_SITE_URL:
return 0 # GSC not configured — skip silently
- started_at = datetime.utcnow()
+ started_at = utcnow()
# GSC has ~2 day delay; fetch from days_back ago to 2 days ago
- end_date = (datetime.utcnow() - timedelta(days=2)).strftime("%Y-%m-%d")
- start_date = (datetime.utcnow() - timedelta(days=days_back + 2)).strftime("%Y-%m-%d")
+ end_date = (utcnow() - timedelta(days=2)).strftime("%Y-%m-%d")
+ start_date = (utcnow() - timedelta(days=days_back + 2)).strftime("%Y-%m-%d")
try:
rows = await asyncio.to_thread(
@@ -122,21 +122,21 @@ async def sync_gsc(days_back: int = 3, max_pages: int = 10) -> int:
)
rows_synced += 1
- duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
+ duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, started_at, completed_at, duration_ms)
VALUES ('gsc', 'success', ?, ?, ?, ?)""",
- (rows_synced, started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
+ (rows_synced, started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
)
return rows_synced
except Exception as exc:
- duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
+ duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
VALUES ('gsc', 'failed', 0, ?, ?, ?, ?)""",
- (str(exc), started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
+ (str(exc), started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
)
raise
diff --git a/web/src/padelnomics/seo/_queries.py b/web/src/padelnomics/seo/_queries.py
index 94434c0..c12820b 100644
--- a/web/src/padelnomics/seo/_queries.py
+++ b/web/src/padelnomics/seo/_queries.py
@@ -4,14 +4,14 @@ All heavy lifting happens in SQL. Functions accept filter parameters
and return plain dicts/lists.
"""
-from datetime import datetime, timedelta
+from datetime import timedelta
-from ..core import execute, fetch_all, fetch_one
+from ..core import execute, fetch_all, fetch_one, utcnow
def _date_cutoff(date_range_days: int) -> str:
"""Return ISO date string for N days ago."""
- return (datetime.utcnow() - timedelta(days=date_range_days)).strftime("%Y-%m-%d")
+ return (utcnow() - timedelta(days=date_range_days)).strftime("%Y-%m-%d")
async def get_search_performance(
diff --git a/web/src/padelnomics/seo/_umami.py b/web/src/padelnomics/seo/_umami.py
index c35f357..9a3172a 100644
--- a/web/src/padelnomics/seo/_umami.py
+++ b/web/src/padelnomics/seo/_umami.py
@@ -4,11 +4,11 @@ Uses bearer token auth. Self-hosted instance, no rate limits.
Config already exists: UMAMI_API_URL, UMAMI_API_TOKEN, UMAMI_WEBSITE_ID.
"""
-from datetime import datetime, timedelta
+from datetime import timedelta
import httpx
-from ..core import config, execute
+from ..core import config, execute, utcnow, utcnow_iso
_TIMEOUT_SECONDS = 15
@@ -21,7 +21,7 @@ async def sync_umami(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS
if not config.UMAMI_API_TOKEN or not config.UMAMI_API_URL:
return 0 # Umami not configured — skip silently
- started_at = datetime.utcnow()
+ started_at = utcnow()
try:
rows_synced = 0
@@ -34,7 +34,7 @@ async def sync_umami(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS
# (Umami's metrics endpoint returns totals for the period,
# so we query one day at a time for daily granularity)
for day_offset in range(days_back):
- day = datetime.utcnow() - timedelta(days=day_offset + 1)
+ day = utcnow() - timedelta(days=day_offset + 1)
metric_date = day.strftime("%Y-%m-%d")
start_ms = int(day.replace(hour=0, minute=0, second=0).timestamp() * 1000)
end_ms = int(day.replace(hour=23, minute=59, second=59).timestamp() * 1000)
@@ -96,21 +96,21 @@ async def sync_umami(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS
(metric_date, page_count, visitors, br, avg_time),
)
- duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
+ duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, started_at, completed_at, duration_ms)
VALUES ('umami', 'success', ?, ?, ?, ?)""",
- (rows_synced, started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
+ (rows_synced, started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
)
return rows_synced
except Exception as exc:
- duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
+ duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
VALUES ('umami', 'failed', 0, ?, ?, ?, ?)""",
- (str(exc), started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
+ (str(exc), started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
)
raise
diff --git a/web/src/padelnomics/sitemap.py b/web/src/padelnomics/sitemap.py
index 8a279cd..02e5010 100644
--- a/web/src/padelnomics/sitemap.py
+++ b/web/src/padelnomics/sitemap.py
@@ -23,6 +23,7 @@ STATIC_PATHS = [
"/imprint",
"/suppliers",
"/markets",
+ "/market-score",
"/planner/",
"/directory/",
]
diff --git a/web/src/padelnomics/static/css/input.css b/web/src/padelnomics/static/css/input.css
index ab1c5b1..539d63e 100644
--- a/web/src/padelnomics/static/css/input.css
+++ b/web/src/padelnomics/static/css/input.css
@@ -549,4 +549,23 @@
.article-body a {
@apply text-electric underline hover:text-electric-hover;
}
+ .article-body details {
+ @apply border border-light-gray rounded-lg mb-3 overflow-hidden;
+ }
+ .article-body details summary {
+ @apply px-4 py-3 font-semibold text-navy cursor-pointer select-none;
+ list-style: none;
+ }
+ .article-body details summary::-webkit-details-marker { display: none; }
+ .article-body details summary::after {
+ content: '+';
+ @apply float-right text-slate font-normal;
+ }
+ .article-body details[open] summary::after {
+ content: '−';
+ }
+ .article-body details > p,
+ .article-body details > div {
+ @apply px-4 pb-4 text-slate-dark;
+ }
}
diff --git a/web/src/padelnomics/suppliers/routes.py b/web/src/padelnomics/suppliers/routes.py
index f7d2977..7846887 100644
--- a/web/src/padelnomics/suppliers/routes.py
+++ b/web/src/padelnomics/suppliers/routes.py
@@ -13,9 +13,9 @@ from ..core import (
config,
csrf_protect,
execute,
+ feature_gate,
fetch_all,
fetch_one,
- feature_gate,
get_paddle_price,
is_flag_enabled,
)
diff --git a/web/src/padelnomics/templates/base.html b/web/src/padelnomics/templates/base.html
index a17ce5a..f2a066e 100644
--- a/web/src/padelnomics/templates/base.html
+++ b/web/src/padelnomics/templates/base.html
@@ -171,6 +171,7 @@