refactor(i18n): Phase 4 — eliminate ad-hoc features_de and is_en patterns

Replace parallel features/features_de lists in PLAN_FEATURES with
feature_keys (translation key references). Update waitlist.html and
signup_step_1.html to iterate over plan.feature_keys and render
{{ t[key] }} instead of raw strings.

Replace is_en ternaries in businessplan.py with t = get_translations(language)
and t["bp_*"] lookups for all 9 section headings.

Adds 25 new keys (1197 → 1222): plan_basic_f1-7, plan_growth_f1-4,
plan_pro_f1-5, bp_title, bp_exec_summary, bp_investment, bp_operations,
bp_revenue, bp_annuals, bp_financing, bp_metrics, bp_cashflow_12m.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-21 00:34:00 +01:00
parent 7c440a209a
commit 8174b7f0c0
6 changed files with 86 additions and 58 deletions

View File

@@ -8,6 +8,7 @@ import json
from pathlib import Path from pathlib import Path
from .core import fetch_one from .core import fetch_one
from .i18n import get_translations
from .planner.calculator import calc, validate_state from .planner.calculator import calc, validate_state
TEMPLATE_DIR = Path(__file__).parent / "templates" / "businessplan" TEMPLATE_DIR = Path(__file__).parent / "templates" / "businessplan"
@@ -41,19 +42,19 @@ def _fmt_months(idx: int) -> str:
def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict: def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
"""Extract and format all business plan sections from planner data.""" """Extract and format all business plan sections from planner data."""
s = state s = state
is_en = language == "en" t = get_translations(language)
venue_type = "Indoor" if s["venue"] == "indoor" else "Outdoor" venue_type = "Indoor" if s["venue"] == "indoor" else "Outdoor"
own_type = "Own" if s["own"] == "buy" else "Rent" own_type = "Own" if s["own"] == "buy" else "Rent"
sections = { sections = {
"title": "Padel Business Plan" if is_en else "Padel Businessplan", "title": t["bp_title"],
"subtitle": f"{venue_type} ({own_type}) \u2014 {s.get('country', 'DE')}", "subtitle": f"{venue_type} ({own_type}) \u2014 {s.get('country', 'DE')}",
"courts": f"{s['dblCourts']} double + {s['sglCourts']} single ({d['totalCourts']} total)", "courts": f"{s['dblCourts']} double + {s['sglCourts']} single ({d['totalCourts']} total)",
# Executive Summary # Executive Summary
"executive_summary": { "executive_summary": {
"heading": "Executive Summary" if is_en else "Zusammenfassung", "heading": t["bp_exec_summary"],
"facility_type": f"{venue_type} ({own_type})", "facility_type": f"{venue_type} ({own_type})",
"courts": d["totalCourts"], "courts": d["totalCourts"],
"sqm": d["sqm"], "sqm": d["sqm"],
@@ -68,7 +69,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
# Investment Plan (CAPEX) # Investment Plan (CAPEX)
"investment": { "investment": {
"heading": "Investment Plan" if is_en else "Investitionsplan", "heading": t["bp_investment"],
"items": d["capexItems"], "items": d["capexItems"],
"total": _fmt_eur(d["capex"]), "total": _fmt_eur(d["capex"]),
"per_court": _fmt_eur(d["capexPerCourt"]), "per_court": _fmt_eur(d["capexPerCourt"]),
@@ -77,7 +78,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
# Operating Costs # Operating Costs
"operations": { "operations": {
"heading": "Operating Costs" if is_en else "Betriebskosten", "heading": t["bp_operations"],
"items": d["opexItems"], "items": d["opexItems"],
"monthly_total": _fmt_eur(d["opex"]), "monthly_total": _fmt_eur(d["opex"]),
"annual_total": _fmt_eur(d["annualOpex"]), "annual_total": _fmt_eur(d["annualOpex"]),
@@ -85,7 +86,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
# Revenue Model # Revenue Model
"revenue": { "revenue": {
"heading": "Revenue & Profitability" if is_en else "Umsatz & Rentabilit\u00e4t", "heading": t["bp_revenue"],
"weighted_rate": _fmt_eur(d["weightedRate"]), "weighted_rate": _fmt_eur(d["weightedRate"]),
"utilization": _fmt_pct(s["utilTarget"] / 100), "utilization": _fmt_pct(s["utilTarget"] / 100),
"gross_monthly": _fmt_eur(d["grossRevMonth"]), "gross_monthly": _fmt_eur(d["grossRevMonth"]),
@@ -96,7 +97,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
# 5-Year P&L # 5-Year P&L
"annuals": { "annuals": {
"heading": "5-Year Projection" if is_en else "5-Jahres-Projektion", "heading": t["bp_annuals"],
"years": [ "years": [
{ {
"year": a["year"], "year": a["year"],
@@ -111,7 +112,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
# Financing # Financing
"financing": { "financing": {
"heading": "Financing Structure" if is_en else "Finanzierungsstruktur", "heading": t["bp_financing"],
"loan_pct": _fmt_pct(s["loanPct"] / 100), "loan_pct": _fmt_pct(s["loanPct"] / 100),
"equity": _fmt_eur(d["equity"]), "equity": _fmt_eur(d["equity"]),
"loan": _fmt_eur(d["loanAmount"]), "loan": _fmt_eur(d["loanAmount"]),
@@ -124,7 +125,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
# Key Metrics # Key Metrics
"metrics": { "metrics": {
"heading": "Key Metrics" if is_en else "Kennzahlen", "heading": t["bp_metrics"],
"irr": _fmt_pct(d["irr"]), "irr": _fmt_pct(d["irr"]),
"moic": f"{d['moic']:.2f}x", "moic": f"{d['moic']:.2f}x",
"cash_on_cash": _fmt_pct(d["cashOnCash"]), "cash_on_cash": _fmt_pct(d["cashOnCash"]),
@@ -137,7 +138,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
# 12-Month Cash Flow # 12-Month Cash Flow
"cashflow_12m": { "cashflow_12m": {
"heading": "12-Month Cash Flow" if is_en else "12-Monats-Liquidit\u00e4tsplan", "heading": t["bp_cashflow_12m"],
"months": [ "months": [
{ {
"month": m["m"], "month": m["m"],

View File

@@ -1195,5 +1195,30 @@
"landing_seo_p1": "Padel ist der am schnellsten wachsende Sport in Europa — die Nachfrage nach Plätzen übersteigt das Angebot in Deutschland, Österreich, der Schweiz und darüber hinaus bei weitem. Eine Paddelhalle zu eröffnen kann eine attraktive Investition sein, aber die Zahlen müssen stimmen. Eine typische Innenhalle mit 68 Plätzen erfordert zwischen 300.000 € (Anmietung eines Bestandsgebäudes) und 23 Mio. € (Neubau), mit Amortisationszeiten von 35 Jahren für gut gelegene Anlagen.", "landing_seo_p1": "Padel ist der am schnellsten wachsende Sport in Europa — die Nachfrage nach Plätzen übersteigt das Angebot in Deutschland, Österreich, der Schweiz und darüber hinaus bei weitem. Eine Paddelhalle zu eröffnen kann eine attraktive Investition sein, aber die Zahlen müssen stimmen. Eine typische Innenhalle mit 68 Plätzen erfordert zwischen 300.000 € (Anmietung eines Bestandsgebäudes) und 23 Mio. € (Neubau), mit Amortisationszeiten von 35 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_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_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." "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",
"plan_basic_f3": "Vollständige Beschreibung & Slogan",
"plan_basic_f4": "Website & Kontaktdaten sichtbar",
"plan_basic_f5": "Checkliste angebotener Leistungen",
"plan_basic_f6": "Social-Links (LinkedIn, Instagram, YouTube)",
"plan_basic_f7": "Kontaktformular auf der Listing-Seite",
"plan_growth_f1": "Alles aus Basic",
"plan_growth_f2": "30 Lead-Credits/Monat",
"plan_growth_f3": "Zugang zum Lead-Feed",
"plan_growth_f4": "Priorität gegenüber Basic-Einträgen",
"plan_pro_f1": "Alles aus Growth",
"plan_pro_f2": "100 Lead-Credits/Monat",
"plan_pro_f3": "Firmenlogo angezeigt",
"plan_pro_f4": "Hervorgehobener Kartenrahmen",
"plan_pro_f5": "Bevorzugte Platzierung",
"bp_title": "Padel Businessplan",
"bp_exec_summary": "Zusammenfassung",
"bp_investment": "Investitionsplan",
"bp_operations": "Betriebskosten",
"bp_revenue": "Umsatz & Rentabilität",
"bp_annuals": "5-Jahres-Projektion",
"bp_financing": "Finanzierungsstruktur",
"bp_metrics": "Kennzahlen",
"bp_cashflow_12m": "12-Monats-Liquiditätsplan"
} }

View File

@@ -1195,5 +1195,30 @@
"landing_seo_p1": "Padel is the fastest-growing sport in Europe, with demand for courts far outstripping supply in Germany, the UK, Scandinavia, and beyond. Opening a padel hall can be a lucrative investment, but the numbers need to work. A typical indoor padel venue with 6-8 courts requires between €300K (renting an existing building) and €2-3M (building new), with payback periods of 3-5 years for well-located venues.", "landing_seo_p1": "Padel is the fastest-growing sport in Europe, with demand for courts far outstripping supply in Germany, the UK, Scandinavia, and beyond. Opening a padel hall can be a lucrative investment, but the numbers need to work. A typical indoor padel venue with 6-8 courts requires between €300K (renting an existing building) and €2-3M (building new), with payback periods of 3-5 years for well-located venues.",
"landing_seo_p2": "The key variables that determine success are location (driving utilization), construction costs (CAPEX), rent or land costs, and pricing strategy. Our financial planner lets you model all of these variables interactively, seeing the impact on your IRR, MOIC, cash flow, and debt service coverage ratio in real time. Whether you're an entrepreneur exploring your first venue, a real estate developer adding padel to a mixed-use project, or an investor evaluating a padel hall acquisition, Padelnomics gives you the financial clarity to make informed decisions.", "landing_seo_p2": "The key variables that determine success are location (driving utilization), construction costs (CAPEX), rent or land costs, and pricing strategy. Our financial planner lets you model all of these variables interactively, seeing the impact on your IRR, MOIC, cash flow, and debt service coverage ratio in real time. Whether you're an entrepreneur exploring your first venue, a real estate developer adding padel to a mixed-use project, or an investor evaluating a padel hall acquisition, Padelnomics gives you the financial clarity to make informed decisions.",
"landing_final_cta_sub": "Model your investment, then get matched with verified court suppliers across {total_countries} countries.", "landing_final_cta_sub": "Model your investment, then get matched with verified court suppliers across {total_countries} countries.",
"landing_jsonld_org_desc": "Professional padel court investment planning platform. Financial planner, supplier directory, and market intelligence for padel entrepreneurs." "landing_jsonld_org_desc": "Professional padel court investment planning platform. Financial planner, supplier directory, and market intelligence for padel entrepreneurs.",
"plan_basic_f1": "Verified badge",
"plan_basic_f2": "Company logo",
"plan_basic_f3": "Full description & tagline",
"plan_basic_f4": "Website & contact details shown",
"plan_basic_f5": "Services offered checklist",
"plan_basic_f6": "Social links (LinkedIn, Instagram, YouTube)",
"plan_basic_f7": "Enquiry form on listing page",
"plan_growth_f1": "Everything in Basic",
"plan_growth_f2": "30 lead credits/month",
"plan_growth_f3": "Lead feed access",
"plan_growth_f4": "Priority over Basic listings",
"plan_pro_f1": "Everything in Growth",
"plan_pro_f2": "100 lead credits/month",
"plan_pro_f3": "Company logo displayed",
"plan_pro_f4": "Highlighted card border",
"plan_pro_f5": "Priority placement",
"bp_title": "Padel Business Plan",
"bp_exec_summary": "Executive Summary",
"bp_investment": "Investment Plan",
"bp_operations": "Operating Costs",
"bp_revenue": "Revenue & Profitability",
"bp_annuals": "5-Year Projection",
"bp_financing": "Financing Structure",
"bp_metrics": "Key Metrics",
"bp_cashflow_12m": "12-Month Cash Flow"
} }

View File

@@ -40,23 +40,14 @@ PLAN_FEATURES = {
"monthly_credits": 0, "monthly_credits": 0,
"paddle_key_monthly": "supplier_basic_monthly", "paddle_key_monthly": "supplier_basic_monthly",
"paddle_key_yearly": "supplier_basic_yearly", "paddle_key_yearly": "supplier_basic_yearly",
"features": [ "feature_keys": [
"Verified badge", "plan_basic_f1",
"Company logo", "plan_basic_f2",
"Full description & tagline", "plan_basic_f3",
"Website & contact details shown", "plan_basic_f4",
"Services offered checklist", "plan_basic_f5",
"Social links (LinkedIn, Instagram, YouTube)", "plan_basic_f6",
"Enquiry form on listing page", "plan_basic_f7",
],
"features_de": [
"Verifiziert-Badge",
"Firmenlogo",
"Vollständige Beschreibung & Slogan",
"Website & Kontaktdaten sichtbar",
"Checkliste angebotener Leistungen",
"Social-Links (LinkedIn, Instagram, YouTube)",
"Kontaktformular auf der Listing-Seite",
], ],
}, },
"supplier_growth": { "supplier_growth": {
@@ -67,17 +58,11 @@ PLAN_FEATURES = {
"monthly_credits": 30, "monthly_credits": 30,
"paddle_key_monthly": "supplier_growth", "paddle_key_monthly": "supplier_growth",
"paddle_key_yearly": "supplier_growth_yearly", "paddle_key_yearly": "supplier_growth_yearly",
"features": [ "feature_keys": [
"Everything in Basic", "plan_growth_f1",
"30 lead credits/month", "plan_growth_f2",
"Lead feed access", "plan_growth_f3",
"Priority over Basic listings", "plan_growth_f4",
],
"features_de": [
"Alles aus Basic",
"30 Lead-Credits/Monat",
"Zugang zum Lead-Feed",
"Priorität gegenüber Basic-Einträgen",
], ],
}, },
"supplier_pro": { "supplier_pro": {
@@ -89,19 +74,12 @@ PLAN_FEATURES = {
"paddle_key_monthly": "supplier_pro", "paddle_key_monthly": "supplier_pro",
"paddle_key_yearly": "supplier_pro_yearly", "paddle_key_yearly": "supplier_pro_yearly",
"includes": ["logo", "highlight", "verified"], "includes": ["logo", "highlight", "verified"],
"features": [ "feature_keys": [
"Everything in Growth", "plan_pro_f1",
"100 lead credits/month", "plan_pro_f2",
"Company logo displayed", "plan_pro_f3",
"Highlighted card border", "plan_pro_f4",
"Priority placement", "plan_pro_f5",
],
"features_de": [
"Alles aus Growth",
"100 Lead-Credits/Monat",
"Firmenlogo angezeigt",
"Hervorgehobener Kartenrahmen",
"Bevorzugte Platzierung",
], ],
}, },
} }

View File

@@ -72,8 +72,8 @@
<div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">{{ t.sup_step1_billed_monthly }}</div> <div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">{{ t.sup_step1_billed_monthly }}</div>
</div> </div>
<ul> <ul>
{% for f in (plan.features_de if lang == 'de' else plan.features) %} {% for key in plan.feature_keys %}
<li>{{ f }}</li> <li>{{ t[key] }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</label> </label>

View File

@@ -13,13 +13,12 @@
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-6"> <div class="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-6">
<h3 class="font-semibold text-navy text-sm mb-2">{{ t.sup_waitlist_plan_h3 | tformat(name=plan_info.name) }}</h3> <h3 class="font-semibold text-navy text-sm mb-2">{{ t.sup_waitlist_plan_h3 | tformat(name=plan_info.name) }}</h3>
<ul class="text-sm text-slate-dark space-y-1"> <ul class="text-sm text-slate-dark space-y-1">
{% set feature_list = plan_info.features_de if lang == 'de' else plan_info.features %} {% for key in plan_info.feature_keys[:4] %}
{% for feature in feature_list[:4] %}
<li class="flex items-start gap-2"> <li class="flex items-start gap-2">
<svg class="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path> <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg> </svg>
<span>{{ feature }}</span> <span>{{ t[key] }}</span>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>