refactor(i18n): Batch 1 — eliminate {% if lang %} blocks in content templates

Convert 63 inline lang blocks across 7 content templates to t.key references.
Add 51 new locale keys (scenario_*, markets_*, article_detail_*).

Templates updated:
- content/partials/scenario_summary.html (6 blocks → t.keys)
- content/partials/scenario_returns.html (15 blocks → t.keys)
- content/partials/scenario_operating.html (17 blocks → t.keys)
- content/partials/scenario_cashflow.html (7 blocks → t.keys, tformat)
- content/partials/scenario_capex.html (9 blocks → t.keys)
- content/templates/markets.html (6 blocks → t.keys in title/meta/labels)
- content/templates/article_detail.html (2 blocks → t.keys)

Also: fix scenario card CTA links (href="/planner/" → url_for), add url_for
stub and tformat filter to _bake_env, pass lang+t to bake render calls,
update test_planner_cta_link assertion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-20 23:29:30 +01:00
parent 8732f5a5e0
commit 84df11aee7
11 changed files with 185 additions and 66 deletions

View File

@@ -10,6 +10,7 @@ from markupsafe import Markup
from quart import Blueprint, abort, g, render_template, request from quart import Blueprint, abort, g, render_template, request
from ..core import capture_waitlist_email, config, csrf_protect, fetch_all, fetch_one, waitlist_gate from ..core import capture_waitlist_email, config, csrf_protect, fetch_all, fetch_one, waitlist_gate
from ..i18n import get_translations
bp = Blueprint( bp = Blueprint(
"content", "content",
@@ -37,9 +38,20 @@ SECTION_TEMPLATES = {
} }
# Standalone Jinja2 env for baking scenario cards into static HTML. # Standalone Jinja2 env for baking scenario cards into static HTML.
# Does not require a Quart app context. # Does not use a Quart request context, so url_for and t are injected
# explicitly. Baked content is always EN (admin operation).
_TEMPLATE_DIR = Path(__file__).parent / "templates" _TEMPLATE_DIR = Path(__file__).parent / "templates"
_bake_env = Environment(loader=FileSystemLoader(str(_TEMPLATE_DIR)), autoescape=True) _bake_env = Environment(loader=FileSystemLoader(str(_TEMPLATE_DIR)), autoescape=True)
_bake_env.filters["tformat"] = lambda s, **kw: s.format_map(kw)
# Hardcoded EN URL stubs — the bake env has no request context so Quart's
# url_for cannot be used. Only endpoints referenced by scenario card templates
# need to be listed here.
_BAKE_URLS: dict[str, str] = {
"planner.index": "/en/planner/",
"directory.index": "/en/directory/",
}
_bake_env.globals["url_for"] = lambda endpoint, **kw: _BAKE_URLS.get(endpoint, f"/{endpoint}")
def is_reserved_path(url_path: str) -> bool: def is_reserved_path(url_path: str) -> bool:
@@ -79,7 +91,12 @@ async def bake_scenario_cards(html: str) -> str:
state_data = json.loads(scenario["state_json"]) state_data = json.loads(scenario["state_json"])
tmpl = _bake_env.get_template(template_name) tmpl = _bake_env.get_template(template_name)
card_html = tmpl.render(scenario=scenario, d=calc_data, s=state_data) # Baking is always in the EN admin context; t and lang are required
# by scenario card templates for translated labels.
card_html = tmpl.render(
scenario=scenario, d=calc_data, s=state_data,
lang="en", t=get_translations("en"),
)
html = html[:match.start()] + card_html + html[match.end():] html = html[:match.start()] + card_html + html[match.end():]
return html return html

View File

@@ -40,7 +40,7 @@
<header class="mb-8"> <header class="mb-8">
<h1 class="text-3xl mb-2">{{ article.title }}</h1> <h1 class="text-3xl mb-2">{{ article.title }}</h1>
<p class="text-sm text-slate"> <p class="text-sm text-slate">
{% if article.published_at %}{% if lang == 'de' %}Veröffentlicht{% else %}Published{% endif %} {{ article.published_at[:10] }} · {% endif %}{% if lang == 'de' %}Padelnomics Forschung{% else %}Padelnomics Research{% endif %} {% if article.published_at %}{{ t.article_detail_published_label }} {{ article.published_at[:10] }} · {% endif %}{{ t.article_detail_research_label }}
</p> </p>
</header> </header>

View File

@@ -1,11 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{% if lang == 'de' %}Padel-Märkte - {{ config.APP_NAME }}{% else %}Padel Markets - {{ config.APP_NAME }}{% endif %}{% endblock %} {% block title %}{{ t.markets_page_title }} - {{ config.APP_NAME }}{% endblock %}
{% block head %} {% block head %}
<meta name="description" content="{% if lang == 'de' %}Padel-Platz-Kostenanalyse und Marktdaten für Städte weltweit. Echte Finanzszenarien mit lokalen Daten.{% else %}Padel court cost analysis and market data for cities worldwide. Real financial scenarios with local data.{% endif %}"> <meta name="description" content="{{ t.markets_page_description }}">
<meta property="og:title" content="{% if lang == 'de' %}Padel-Märkte - {{ config.APP_NAME }}{% else %}Padel Markets - {{ config.APP_NAME }}{% endif %}"> <meta property="og:title" content="{{ t.markets_page_og_title }} - {{ config.APP_NAME }}">
<meta property="og:description" content="{% if lang == 'de' %}Erkunde Padel-Platz-Kosten, Umsatzprojektionen und Investitionsrenditen nach Stadt.{% else %}Explore padel court costs, revenue projections, and investment returns by city.{% endif %}"> <meta property="og:description" content="{{ t.markets_page_og_description }}">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@@ -19,7 +19,7 @@
<div class="card mb-8"> <div class="card mb-8">
<div style="display: grid; grid-template-columns: 1fr auto auto; gap: 1rem; align-items: end;"> <div style="display: grid; grid-template-columns: 1fr auto auto; gap: 1rem; align-items: end;">
<div> <div>
<label class="form-label" for="market-q">{% if lang == 'de' %}Suche{% else %}Search{% endif %}</label> <label class="form-label" for="market-q">{{ t.markets_search_label }}</label>
<input type="text" id="market-q" name="q" value="{{ current_q }}" placeholder="{{ t.mkt_search_placeholder }}" <input type="text" id="market-q" name="q" value="{{ current_q }}" placeholder="{{ t.mkt_search_placeholder }}"
class="form-input" class="form-input"
hx-get="{{ url_for('content.market_results') }}" hx-get="{{ url_for('content.market_results') }}"
@@ -28,7 +28,7 @@
hx-include="#market-country, #market-region"> hx-include="#market-country, #market-region">
</div> </div>
<div> <div>
<label class="form-label" for="market-country">{% if lang == 'de' %}Land{% else %}Country{% endif %}</label> <label class="form-label" for="market-country">{{ t.markets_country_label }}</label>
<select id="market-country" name="country" class="form-input" <select id="market-country" name="country" class="form-input"
hx-get="{{ url_for('content.market_results') }}" hx-get="{{ url_for('content.market_results') }}"
hx-target="#market-results" hx-target="#market-results"

View File

@@ -1,15 +1,15 @@
<div class="scenario-widget scenario-capex"> <div class="scenario-widget scenario-capex">
<div class="scenario-widget__header"> <div class="scenario-widget__header">
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span> <span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
<span class="scenario-widget__config">{% if lang == 'de' %}Investitionsaufschlüsselung{% else %}Investment Breakdown{% endif %}</span> <span class="scenario-widget__config">{{ t.scenario_investment_breakdown_title }}</span>
</div> </div>
<div class="scenario-widget__body"> <div class="scenario-widget__body">
<div class="scenario-widget__table-wrap"> <div class="scenario-widget__table-wrap">
<table class="scenario-widget__table"> <table class="scenario-widget__table">
<thead> <thead>
<tr> <tr>
<th>{% if lang == 'de' %}Position{% else %}Item{% endif %}</th> <th>{{ t.scenario_table_item_label }}</th>
<th class="text-right">{% if lang == 'de' %}Betrag{% else %}Amount{% endif %}</th> <th class="text-right">{{ t.scenario_table_amount_label }}</th>
<th>Detail</th> <th>Detail</th>
</tr> </tr>
</thead> </thead>
@@ -24,7 +24,7 @@
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<td><strong>{% if lang == 'de' %}Gesamt-CAPEX{% else %}Total CAPEX{% endif %}</strong></td> <td><strong>{{ t.scenario_total_capex_label }}</strong></td>
<td class="text-right mono"><strong>&euro;{{ "{:,.0f}".format(d.capex) }}</strong></td> <td class="text-right mono"><strong>&euro;{{ "{:,.0f}".format(d.capex) }}</strong></td>
<td></td> <td></td>
</tr> </tr>
@@ -33,7 +33,7 @@
</div> </div>
<div class="scenario-widget__metrics" style="margin-top: 1rem;"> <div class="scenario-widget__metrics" style="margin-top: 1rem;">
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Pro Platz{% else %}Per Court{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_per_court_label }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.capexPerCourt) }}</span> <span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.capexPerCourt) }}</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
@@ -47,20 +47,20 @@
</div> </div>
<div class="scenario-widget__metrics"> <div class="scenario-widget__metrics">
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Eigenkapital{% else %}Equity{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_equity_label }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.equity) }}</span> <span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.equity) }}</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Darlehen{% else %}Loan{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_loan_label }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.loanAmount) }}</span> <span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.loanAmount) }}</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Gesamt{% else %}Total{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_total_label }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.capex) }}</span> <span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.capex) }}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="scenario-widget__cta"> <div class="scenario-widget__cta">
<a href="/planner/">{% if lang == 'de' %}Mit eigenen Zahlen testen &rarr;{% else %}Try with your own numbers &rarr;{% endif %}</a> <a href="{{ url_for('planner.index') }}">{{ t.scenario_cta_try_numbers }}</a>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<div class="scenario-widget scenario-cashflow"> <div class="scenario-widget scenario-cashflow">
<div class="scenario-widget__header"> <div class="scenario-widget__header">
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span> <span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
<span class="scenario-widget__config">{% if lang == 'de' %}{{ s.holdYears }}-Jahres-Projektion{% else %}{{ s.holdYears }}-Year Projection{% endif %}</span> <span class="scenario-widget__config">{{ t.scenario_cashflow_config_title | tformat(years=s.holdYears) }}</span>
</div> </div>
<div class="scenario-widget__body"> <div class="scenario-widget__body">
<div class="scenario-widget__table-wrap"> <div class="scenario-widget__table-wrap">
@@ -10,13 +10,13 @@
<tr> <tr>
<th></th> <th></th>
{% for a in d.annuals %} {% for a in d.annuals %}
<th class="text-right">{% if lang == 'de' %}Jahr{% else %}Year{% endif %} {{ a.year }}</th> <th class="text-right">{{ t.scenario_year_label }} {{ a.year }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{% if lang == 'de' %}Umsatz{% else %}Revenue{% endif %}</td> <td>{{ t.scenario_revenue_label }}</td>
{% for a in d.annuals %} {% for a in d.annuals %}
<td class="text-right mono">&euro;{{ "{:,.0f}".format(a.revenue) }}</td> <td class="text-right mono">&euro;{{ "{:,.0f}".format(a.revenue) }}</td>
{% endfor %} {% endfor %}
@@ -28,19 +28,19 @@
{% endfor %} {% endfor %}
</tr> </tr>
<tr> <tr>
<td>{% if lang == 'de' %}Schuldendienst{% else %}Debt Service{% endif %}</td> <td>{{ t.scenario_debt_service_label }}</td>
{% for a in d.annuals %} {% for a in d.annuals %}
<td class="text-right mono">&euro;{{ "{:,.0f}".format(a.ds) }}</td> <td class="text-right mono">&euro;{{ "{:,.0f}".format(a.ds) }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
<tr> <tr>
<td><strong>{% if lang == 'de' %}Netto-Cashflow{% else %}Net Cash Flow{% endif %}</strong></td> <td><strong>{{ t.scenario_net_cashflow_label }}</strong></td>
{% for a in d.annuals %} {% for a in d.annuals %}
<td class="text-right mono"><strong>&euro;{{ "{:,.0f}".format(a.ncf) }}</strong></td> <td class="text-right mono"><strong>&euro;{{ "{:,.0f}".format(a.ncf) }}</strong></td>
{% endfor %} {% endfor %}
</tr> </tr>
<tr> <tr>
<td>{% if lang == 'de' %}Kumulativer NCF{% else %}Cumulative NCF{% endif %}</td> <td>{{ t.scenario_cumulative_ncf_label }}</td>
{% set cum = namespace(total=-d.capex) %} {% set cum = namespace(total=-d.capex) %}
{% for a in d.annuals %} {% for a in d.annuals %}
{% set cum.total = cum.total + a.ncf %} {% set cum.total = cum.total + a.ncf %}
@@ -60,6 +60,6 @@
</div> </div>
</div> </div>
<div class="scenario-widget__cta"> <div class="scenario-widget__cta">
<a href="/planner/">{% if lang == 'de' %}Mit eigenen Zahlen testen &rarr;{% else %}Try with your own numbers &rarr;{% endif %}</a> <a href="{{ url_for('planner.index') }}">{{ t.scenario_cta_try_numbers }}</a>
</div> </div>
</div> </div>

View File

@@ -1,32 +1,32 @@
<div class="scenario-widget scenario-operating"> <div class="scenario-widget scenario-operating">
<div class="scenario-widget__header"> <div class="scenario-widget__header">
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span> <span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
<span class="scenario-widget__config">{% if lang == 'de' %}Umsatz &amp; Betriebskosten{% else %}Revenue &amp; Operating Costs{% endif %}</span> <span class="scenario-widget__config">{{ t.scenario_revenue_opex_title }}</span>
</div> </div>
<div class="scenario-widget__body"> <div class="scenario-widget__body">
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Umsatzmodell{% else %}Revenue Model{% endif %}</h4> <h4 class="scenario-widget__section-title">{{ t.scenario_revenue_model_title }}</h4>
<div class="scenario-widget__metrics"> <div class="scenario-widget__metrics">
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Gewichteter Satz{% else %}Weighted Rate{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_weighted_rate_label }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:.0f}".format(d.weightedRate) }}/hr</span> <span class="scenario-widget__metric-value">&euro;{{ "{:.0f}".format(d.weightedRate) }}/hr</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Auslastungsziel{% else %}Utilization Target{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_utilization_target_label }}</span>
<span class="scenario-widget__metric-value">{{ s.utilTarget }}%</span> <span class="scenario-widget__metric-value">{{ s.utilTarget }}%</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Gebuchte Std./Monat{% else %}Booked Hours/mo{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_booked_hours_label }}</span>
<span class="scenario-widget__metric-value">{{ "{:,.0f}".format(d.bookedHoursMonth) }}</span> <span class="scenario-widget__metric-value">{{ "{:,.0f}".format(d.bookedHoursMonth) }}</span>
</div> </div>
</div> </div>
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Monatliche OPEX{% else %}Monthly OPEX{% endif %}</h4> <h4 class="scenario-widget__section-title">{{ t.scenario_monthly_opex_title }}</h4>
<div class="scenario-widget__table-wrap"> <div class="scenario-widget__table-wrap">
<table class="scenario-widget__table"> <table class="scenario-widget__table">
<thead> <thead>
<tr> <tr>
<th>{% if lang == 'de' %}Position{% else %}Item{% endif %}</th> <th>{{ t.scenario_table_item_label }}</th>
<th class="text-right">{% if lang == 'de' %}Monatlich{% else %}Monthly{% endif %}</th> <th class="text-right">{{ t.scenario_table_monthly_label }}</th>
<th>Detail</th> <th>Detail</th>
</tr> </tr>
</thead> </thead>
@@ -41,7 +41,7 @@
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<td><strong>{% if lang == 'de' %}Monatliche OPEX gesamt{% else %}Total Monthly OPEX{% endif %}</strong></td> <td><strong>{{ t.scenario_total_monthly_opex_label }}</strong></td>
<td class="text-right mono"><strong>&euro;{{ "{:,.0f}".format(d.opex) }}</strong></td> <td class="text-right mono"><strong>&euro;{{ "{:,.0f}".format(d.opex) }}</strong></td>
<td></td> <td></td>
</tr> </tr>
@@ -49,24 +49,24 @@
</table> </table>
</div> </div>
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Monatliche Übersicht{% else %}Monthly Summary{% endif %}</h4> <h4 class="scenario-widget__section-title">{{ t.scenario_monthly_summary_title }}</h4>
<div class="scenario-widget__table-wrap"> <div class="scenario-widget__table-wrap">
<table class="scenario-widget__table"> <table class="scenario-widget__table">
<tbody> <tbody>
<tr> <tr>
<td>{% if lang == 'de' %}Bruttoumsatz{% else %}Gross Revenue{% endif %}</td> <td>{{ t.scenario_gross_revenue_label }}</td>
<td class="text-right mono">&euro;{{ "{:,.0f}".format(d.grossRevMonth) }}</td> <td class="text-right mono">&euro;{{ "{:,.0f}".format(d.grossRevMonth) }}</td>
</tr> </tr>
<tr> <tr>
<td>{% if lang == 'de' %}Buchungsgebühren{% else %}Booking Fees{% endif %}</td> <td>{{ t.scenario_booking_fees_label }}</td>
<td class="text-right mono">-&euro;{{ "{:,.0f}".format(d.feeDeduction) }}</td> <td class="text-right mono">-&euro;{{ "{:,.0f}".format(d.feeDeduction) }}</td>
</tr> </tr>
<tr> <tr>
<td>{% if lang == 'de' %}Nettoumsatz{% else %}Net Revenue{% endif %}</td> <td>{{ t.scenario_net_revenue_label }}</td>
<td class="text-right mono">&euro;{{ "{:,.0f}".format(d.netRevMonth) }}</td> <td class="text-right mono">&euro;{{ "{:,.0f}".format(d.netRevMonth) }}</td>
</tr> </tr>
<tr> <tr>
<td>{% if lang == 'de' %}Betriebskosten{% else %}Operating Costs{% endif %}</td> <td>{{ t.scenario_operating_costs_label }}</td>
<td class="text-right mono">-&euro;{{ "{:,.0f}".format(d.opex) }}</td> <td class="text-right mono">-&euro;{{ "{:,.0f}".format(d.opex) }}</td>
</tr> </tr>
<tr> <tr>
@@ -74,11 +74,11 @@
<td class="text-right mono"><strong>&euro;{{ "{:,.0f}".format(d.ebitdaMonth) }}</strong></td> <td class="text-right mono"><strong>&euro;{{ "{:,.0f}".format(d.ebitdaMonth) }}</strong></td>
</tr> </tr>
<tr> <tr>
<td>{% if lang == 'de' %}Schuldendienst{% else %}Debt Service{% endif %}</td> <td>{{ t.scenario_debt_service_label }}</td>
<td class="text-right mono">-&euro;{{ "{:,.0f}".format(d.monthlyPayment) }}</td> <td class="text-right mono">-&euro;{{ "{:,.0f}".format(d.monthlyPayment) }}</td>
</tr> </tr>
<tr> <tr>
<td><strong>{% if lang == 'de' %}Netto-Cashflow{% else %}Net Cash Flow{% endif %}</strong></td> <td><strong>{{ t.scenario_net_cashflow_label }}</strong></td>
<td class="text-right mono"><strong>&euro;{{ "{:,.0f}".format(d.netCFMonth) }}</strong></td> <td class="text-right mono"><strong>&euro;{{ "{:,.0f}".format(d.netCFMonth) }}</strong></td>
</tr> </tr>
</tbody> </tbody>
@@ -86,6 +86,6 @@
</div> </div>
</div> </div>
<div class="scenario-widget__cta"> <div class="scenario-widget__cta">
<a href="/planner/">{% if lang == 'de' %}Mit eigenen Zahlen testen &rarr;{% else %}Try with your own numbers &rarr;{% endif %}</a> <a href="{{ url_for('planner.index') }}">{{ t.scenario_cta_try_numbers }}</a>
</div> </div>
</div> </div>

View File

@@ -1,10 +1,10 @@
<div class="scenario-widget scenario-returns"> <div class="scenario-widget scenario-returns">
<div class="scenario-widget__header"> <div class="scenario-widget__header">
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span> <span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
<span class="scenario-widget__config">{% if lang == 'de' %}Renditen &amp; Finanzierung{% else %}Returns &amp; Financing{% endif %}</span> <span class="scenario-widget__config">{{ t.scenario_returns_config_title }}</span>
</div> </div>
<div class="scenario-widget__body"> <div class="scenario-widget__body">
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Renditekennzahlen{% else %}Return Metrics{% endif %}</h4> <h4 class="scenario-widget__section-title">{{ t.scenario_return_metrics_title }}</h4>
<div class="scenario-widget__metrics"> <div class="scenario-widget__metrics">
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">IRR ({{ s.holdYears }}yr)</span> <span class="scenario-widget__metric-label">IRR ({{ s.holdYears }}yr)</span>
@@ -15,8 +15,8 @@
<span class="scenario-widget__metric-value">{{ "{:.2f}".format(d.moic) }}x</span> <span class="scenario-widget__metric-value">{{ "{:.2f}".format(d.moic) }}x</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Amortisation{% else %}Payback{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_payback_label }}</span>
<span class="scenario-widget__metric-value">{% if d.paybackIdx >= 0 %}{{ d.paybackIdx + 1 }} {% if lang == 'de' %}Monate{% else %}months{% endif %}{% else %}N/A{% endif %}</span> <span class="scenario-widget__metric-value">{% if d.paybackIdx >= 0 %}{{ d.paybackIdx + 1 }} {{ t.scenario_months_unit }}{% else %}N/A{% endif %}</span>
</div> </div>
</div> </div>
<div class="scenario-widget__metrics"> <div class="scenario-widget__metrics">
@@ -25,48 +25,48 @@
<span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.cashOnCash * 100) }}%</span> <span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.cashOnCash * 100) }}%</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Rendite auf Kosten{% else %}Yield on Cost{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_yield_on_cost_label }}</span>
<span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.yieldOnCost * 100) }}%</span> <span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.yieldOnCost * 100) }}%</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}EBITDA-Marge{% else %}EBITDA Margin{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_ebitda_margin_label }}</span>
<span class="scenario-widget__metric-value">{{ "{:.0f}".format(d.ebitdaMargin * 100) }}%</span> <span class="scenario-widget__metric-value">{{ "{:.0f}".format(d.ebitdaMargin * 100) }}%</span>
</div> </div>
</div> </div>
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Exit-Analyse{% else %}Exit Analysis{% endif %}</h4> <h4 class="scenario-widget__section-title">{{ t.scenario_exit_analysis_title }}</h4>
<div class="scenario-widget__metrics"> <div class="scenario-widget__metrics">
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Exit-Wert ({{ s.exitMultiple }}x EBITDA){% else %}Exit Value ({{ s.exitMultiple }}x EBITDA){% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_exit_value_label | tformat(multiple=s.exitMultiple) }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.exitValue) }}</span> <span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.exitValue) }}</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Restschuld{% else %}Remaining Loan{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_remaining_loan_label }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.remainingLoan) }}</span> <span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.remainingLoan) }}</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Netto-Exit{% else %}Net Exit{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_net_exit_label }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.netExit) }}</span> <span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.netExit) }}</span>
</div> </div>
</div> </div>
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Finanzierung{% else %}Financing{% endif %}</h4> <h4 class="scenario-widget__section-title">{{ t.scenario_financing_title }}</h4>
<div class="scenario-widget__metrics"> <div class="scenario-widget__metrics">
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Darlehensbetrag{% else %}Loan Amount{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_loan_amount_label }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.loanAmount) }}</span> <span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.loanAmount) }}</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Zinssatz / Laufzeit{% else %}Rate / Term{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_rate_term_label }}</span>
<span class="scenario-widget__metric-value">{{ s.interestRate }}% / {{ s.loanTerm }}yr</span> <span class="scenario-widget__metric-value">{{ s.interestRate }}% / {{ s.loanTerm }}yr</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Monatliche Rate{% else %}Monthly Payment{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_monthly_payment_label }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.monthlyPayment) }}</span> <span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.monthlyPayment) }}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="scenario-widget__cta"> <div class="scenario-widget__cta">
<a href="/planner/">{% if lang == 'de' %}Mit eigenen Zahlen testen &rarr;{% else %}Try with your own numbers &rarr;{% endif %}</a> <a href="{{ url_for('planner.index') }}">{{ t.scenario_cta_try_numbers }}</a>
</div> </div>
</div> </div>

View File

@@ -6,11 +6,11 @@
<div class="scenario-widget__body"> <div class="scenario-widget__body">
<div class="scenario-widget__metrics"> <div class="scenario-widget__metrics">
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Gesamt-CAPEX{% else %}Total CAPEX{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_total_capex_label }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.capex) }}</span> <span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.capex) }}</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Monatliches EBITDA{% else %}Monthly EBITDA{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_monthly_ebitda_label }}</span>
<span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.ebitdaMonth) }}</span> <span class="scenario-widget__metric-value">&euro;{{ "{:,.0f}".format(d.ebitdaMonth) }}</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
@@ -20,20 +20,20 @@
</div> </div>
<div class="scenario-widget__metrics"> <div class="scenario-widget__metrics">
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Amortisation{% else %}Payback{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_payback_label }}</span>
<span class="scenario-widget__metric-value">{% if d.paybackIdx >= 0 %}{{ d.paybackIdx + 1 }} {% if lang == 'de' %}Monate{% else %}months{% endif %}{% else %}N/A{% endif %}</span> <span class="scenario-widget__metric-value">{% if d.paybackIdx >= 0 %}{{ d.paybackIdx + 1 }} {{ t.scenario_months_unit }}{% else %}N/A{% endif %}</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">Cash-on-Cash</span> <span class="scenario-widget__metric-label">Cash-on-Cash</span>
<span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.cashOnCash * 100) }}%</span> <span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.cashOnCash * 100) }}%</span>
</div> </div>
<div class="scenario-widget__metric"> <div class="scenario-widget__metric">
<span class="scenario-widget__metric-label">{% if lang == 'de' %}EBITDA-Marge{% else %}EBITDA Margin{% endif %}</span> <span class="scenario-widget__metric-label">{{ t.scenario_ebitda_margin_label }}</span>
<span class="scenario-widget__metric-value">{{ "{:.0f}".format(d.ebitdaMargin * 100) }}%</span> <span class="scenario-widget__metric-value">{{ "{:.0f}".format(d.ebitdaMargin * 100) }}%</span>
</div> </div>
</div> </div>
</div> </div>
<div class="scenario-widget__cta"> <div class="scenario-widget__cta">
<a href="/planner/">{% if lang == 'de' %}Mit eigenen Zahlen testen &rarr;{% else %}Try with your own numbers &rarr;{% endif %}</a> <a href="{{ url_for('planner.index') }}">{{ t.scenario_cta_try_numbers }}</a>
</div> </div>
</div> </div>

View File

@@ -939,5 +939,56 @@
"sup_faq_a10_pre": "Schreib uns eine E-Mail an", "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_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_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 modelliert. 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",
"scenario_ebitda_margin_label": "EBITDA-Marge",
"scenario_debt_service_label": "Schuldendienst",
"scenario_net_cashflow_label": "Netto-Cashflow",
"scenario_table_item_label": "Position",
"scenario_table_monthly_label": "Monatlich",
"scenario_revenue_label": "Umsatz",
"scenario_total_label": "Gesamt",
"scenario_total_capex_label": "Gesamt-CAPEX",
"scenario_monthly_ebitda_label": "Monatliches EBITDA",
"scenario_returns_config_title": "Renditen &amp; Finanzierung",
"scenario_return_metrics_title": "Renditekennzahlen",
"scenario_yield_on_cost_label": "Rendite auf Kosten",
"scenario_exit_analysis_title": "Exit-Analyse",
"scenario_exit_value_label": "Exit-Wert ({multiple}x EBITDA)",
"scenario_remaining_loan_label": "Restschuld",
"scenario_net_exit_label": "Netto-Exit",
"scenario_financing_title": "Finanzierung",
"scenario_loan_amount_label": "Darlehensbetrag",
"scenario_rate_term_label": "Zinssatz / Laufzeit",
"scenario_monthly_payment_label": "Monatliche Rate",
"scenario_revenue_opex_title": "Umsatz &amp; Betriebskosten",
"scenario_revenue_model_title": "Umsatzmodell",
"scenario_weighted_rate_label": "Gewichteter Satz",
"scenario_utilization_target_label": "Auslastungsziel",
"scenario_booked_hours_label": "Gebuchte Std./Monat",
"scenario_monthly_opex_title": "Monatliche OPEX",
"scenario_table_amount_label": "Betrag",
"scenario_total_monthly_opex_label": "Monatliche OPEX gesamt",
"scenario_monthly_summary_title": "Monatliche Übersicht",
"scenario_gross_revenue_label": "Bruttoumsatz",
"scenario_booking_fees_label": "Buchungsgebühren",
"scenario_net_revenue_label": "Nettoumsatz",
"scenario_operating_costs_label": "Betriebskosten",
"scenario_cashflow_config_title": "{years}-Jahres-Projektion",
"scenario_year_label": "Jahr",
"scenario_cumulative_ncf_label": "Kumulativer NCF",
"scenario_investment_breakdown_title": "Investitionsaufschlüsselung",
"scenario_per_court_label": "Pro Platz",
"scenario_equity_label": "Eigenkapital",
"scenario_loan_label": "Darlehen",
"markets_page_title": "Padel-Märkte",
"markets_page_description": "Padel-Platz-Kostenanalyse und Marktdaten für Städte weltweit. Echte Finanzszenarien mit lokalen Daten.",
"markets_page_og_title": "Padel-Märkte",
"markets_page_og_description": "Erkunde Padel-Platz-Kosten, Umsatzprojektionen und Investitionsrenditen nach Stadt.",
"markets_search_label": "Suche",
"markets_country_label": "Land",
"article_detail_published_label": "Veröffentlicht",
"article_detail_research_label": "Padelnomics Forschung"
} }

View File

@@ -939,5 +939,56 @@
"sup_faq_a10_pre": "Email us at", "sup_faq_a10_pre": "Email us at",
"sup_faq_a10_post": "with your company details and well add you to the directory within 48 hours.", "sup_faq_a10_post": "with your company details and well add you to the directory within 48 hours.",
"sup_cta_h2": "Your Next Client Is Already Building a Business Plan", "sup_cta_h2": "Your Next Client Is Already Building a Business Plan",
"sup_cta_p": "Theyve modeled the ROI. They know their budget. Theyre looking for a supplier like you." "sup_cta_p": "Theyve modeled the ROI. They know their budget. Theyre looking for a supplier like you.",
"scenario_cta_try_numbers": "Try with your own numbers →",
"scenario_payback_label": "Payback",
"scenario_months_unit": "months",
"scenario_ebitda_margin_label": "EBITDA Margin",
"scenario_debt_service_label": "Debt Service",
"scenario_net_cashflow_label": "Net Cash Flow",
"scenario_table_item_label": "Item",
"scenario_table_monthly_label": "Monthly",
"scenario_revenue_label": "Revenue",
"scenario_total_label": "Total",
"scenario_total_capex_label": "Total CAPEX",
"scenario_monthly_ebitda_label": "Monthly EBITDA",
"scenario_returns_config_title": "Returns &amp; Financing",
"scenario_return_metrics_title": "Return Metrics",
"scenario_yield_on_cost_label": "Yield on Cost",
"scenario_exit_analysis_title": "Exit Analysis",
"scenario_exit_value_label": "Exit Value ({multiple}x EBITDA)",
"scenario_remaining_loan_label": "Remaining Loan",
"scenario_net_exit_label": "Net Exit",
"scenario_financing_title": "Financing",
"scenario_loan_amount_label": "Loan Amount",
"scenario_rate_term_label": "Rate / Term",
"scenario_monthly_payment_label": "Monthly Payment",
"scenario_revenue_opex_title": "Revenue &amp; Operating Costs",
"scenario_revenue_model_title": "Revenue Model",
"scenario_weighted_rate_label": "Weighted Rate",
"scenario_utilization_target_label": "Utilization Target",
"scenario_booked_hours_label": "Booked Hours/mo",
"scenario_monthly_opex_title": "Monthly OPEX",
"scenario_table_amount_label": "Amount",
"scenario_total_monthly_opex_label": "Total Monthly OPEX",
"scenario_monthly_summary_title": "Monthly Summary",
"scenario_gross_revenue_label": "Gross Revenue",
"scenario_booking_fees_label": "Booking Fees",
"scenario_net_revenue_label": "Net Revenue",
"scenario_operating_costs_label": "Operating Costs",
"scenario_cashflow_config_title": "{years}-Year Projection",
"scenario_year_label": "Year",
"scenario_cumulative_ncf_label": "Cumulative NCF",
"scenario_investment_breakdown_title": "Investment Breakdown",
"scenario_per_court_label": "Per Court",
"scenario_equity_label": "Equity",
"scenario_loan_label": "Loan",
"markets_page_title": "Padel Markets",
"markets_page_description": "Padel court cost analysis and market data for cities worldwide. Real financial scenarios with local data.",
"markets_page_og_title": "Padel Markets",
"markets_page_og_description": "Explore padel court costs, revenue projections, and investment returns by city.",
"markets_search_label": "Search",
"markets_country_label": "Country",
"article_detail_published_label": "Published",
"article_detail_research_label": "Padelnomics Research"
} }

View File

@@ -386,7 +386,7 @@ class TestBakeScenarioCards:
await _create_published_scenario(slug="cta-test") await _create_published_scenario(slug="cta-test")
html = "[scenario:cta-test]" html = "[scenario:cta-test]"
result = await bake_scenario_cards(html) result = await bake_scenario_cards(html)
assert "/planner/" in result assert "planner" in result
assert "Try with your own numbers" in result assert "Try with your own numbers" in result
async def test_invalid_section_ignored(self, db): async def test_invalid_section_ignored(self, db):