From 420a2f063b68b787f51c0e091b41e1c1bdf5ee36 Mon Sep 17 00:00:00 2001 From: Deeman Date: Sat, 21 Feb 2026 01:24:22 +0100 Subject: [PATCH] feat(i18n): translate all auth-gated pages (Iteration 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Phase 0: Fix language detection for dashboard/billing routes — use _detect_lang() fallback in inject_globals() so g.lang is always set; use g.get("lang") or "en" in route handlers before template render - Phase 1: Dashboard templates (~29 keys, dash_* prefix) index.html, settings.html, flash messages in routes.py - Phase 2: Billing templates (~31 keys, billing_* prefix) pricing.html, success.html, flash message in routes.py - Phase 3: Supplier dashboard (~171 keys, sd_* prefix) dashboard shell + overview + boosts, lead feed tab + lead cards, listing tab; BOOST_OPTIONS now use name_key/desc_key; _compute_order() accepts t dict for translated billing labels; all flash messages replaced with get_translations(g.lang) - Phase 4: Business plan PDF (~64 keys, bp_* prefix) _fmt_months() accepts t dict; get_plan_sections() translates venue_type/own_type/courts_desc/loan_term; adds sections["labels"] sub-dict with all template-level strings; plan.html uses s.labels.* and s.lang for the html[lang] attribute - Update test_i18n_parity.py allowlist for new identical-value keys (financial abbreviations, brand names, terms same in both languages) Locale files: 1469 → 1533 keys (en.json and de.json) All 1018 tests pass. Co-Authored-By: Claude Opus 4.6 --- padelnomics/src/padelnomics/app.py | 3 +- padelnomics/src/padelnomics/billing/routes.py | 4 +- .../billing/templates/pricing.html | 48 +-- .../billing/templates/success.html | 8 +- padelnomics/src/padelnomics/businessplan.py | 109 ++++-- .../src/padelnomics/dashboard/routes.py | 7 +- .../dashboard/templates/index.html | 28 +- .../dashboard/templates/settings.html | 26 +- padelnomics/src/padelnomics/locales/de.json | 314 +++++++++++++++++- padelnomics/src/padelnomics/locales/en.json | 314 +++++++++++++++++- .../src/padelnomics/suppliers/routes.py | 33 +- .../templates/suppliers/dashboard.html | 18 +- .../templates/suppliers/lead_feed.html | 16 +- .../suppliers/partials/dashboard_boosts.html | 42 +-- .../suppliers/partials/dashboard_leads.html | 50 +-- .../suppliers/partials/dashboard_listing.html | 46 +-- .../partials/dashboard_listing_preview.html | 2 +- .../partials/dashboard_overview.html | 28 +- .../suppliers/partials/lead_card.html | 20 +- .../suppliers/partials/lead_card_error.html | 6 +- .../partials/lead_card_unlocked.html | 74 ++--- .../templates/businessplan/plan.html | 91 +++-- padelnomics/tests/test_i18n_parity.py | 16 + 23 files changed, 1007 insertions(+), 296 deletions(-) diff --git a/padelnomics/src/padelnomics/app.py b/padelnomics/src/padelnomics/app.py index ec69728..4cd4508 100644 --- a/padelnomics/src/padelnomics/app.py +++ b/padelnomics/src/padelnomics/app.py @@ -196,7 +196,8 @@ def create_app() -> Quart: @app.context_processor def inject_globals(): from datetime import datetime - lang = g.get("lang", "en") + lang = g.get("lang") or _detect_lang() + g.lang = lang # ensure g.lang is always set (e.g. for dashboard/billing routes) effective_lang = lang if lang in SUPPORTED_LANGS else "en" return { "config": config, diff --git a/padelnomics/src/padelnomics/billing/routes.py b/padelnomics/src/padelnomics/billing/routes.py index d089406..8a45453 100644 --- a/padelnomics/src/padelnomics/billing/routes.py +++ b/padelnomics/src/padelnomics/billing/routes.py @@ -14,6 +14,7 @@ from quart import Blueprint, flash, g, jsonify, redirect, render_template, reque from ..auth.routes import login_required from ..core import config, execute, fetch_one, get_paddle_price +from ..i18n import get_translations def _paddle_client() -> PaddleClient: @@ -177,7 +178,8 @@ async def manage(): """Redirect to Paddle customer portal.""" sub = await get_subscription(g.user["id"]) if not sub or not sub.get("provider_subscription_id"): - await flash("No active subscription found.", "error") + t = get_translations(g.get("lang") or "en") + await flash(t["billing_no_subscription"], "error") return redirect(url_for("dashboard.settings")) paddle = _paddle_client() diff --git a/padelnomics/src/padelnomics/billing/templates/pricing.html b/padelnomics/src/padelnomics/billing/templates/pricing.html index 7f9a5f1..85c7ce5 100644 --- a/padelnomics/src/padelnomics/billing/templates/pricing.html +++ b/padelnomics/src/padelnomics/billing/templates/pricing.html @@ -1,51 +1,51 @@ {% extends "base.html" %} -{% block title %}Free Padel Court Financial Planner - {{ config.APP_NAME }}{% endblock %} +{% block title %}{{ t.billing_pricing_title }} - {{ config.APP_NAME }}{% endblock %} {% block head %} - - - + + + {% endblock %} {% block content %}
-

100% Free. No Catch.

-

The most sophisticated padel court financial planner available — completely free. Plan your investment with 60+ variables, sensitivity analysis, and professional-grade projections.

+

{{ t.billing_pricing_h1 }}

+

{{ t.billing_pricing_subtitle }}

-

Financial Planner

-

Free — forever

+

{{ t.billing_planner_card }}

+

{{ t.billing_planner_free }} {{ t.billing_planner_forever }}

    -
  • 60+ adjustable variables
  • -
  • 6 analysis tabs (CAPEX, Operating, Cash Flow, Returns, Metrics)
  • -
  • Sensitivity analysis (utilization + pricing)
  • -
  • Save unlimited scenarios
  • -
  • Interactive charts
  • -
  • Indoor/outdoor & rent/buy models
  • +
  • {{ t.billing_feature_1 }}
  • +
  • {{ t.billing_feature_2 }}
  • +
  • {{ t.billing_feature_3 }}
  • +
  • {{ t.billing_feature_4 }}
  • +
  • {{ t.billing_feature_5 }}
  • +
  • {{ t.billing_feature_6 }}
{% if user %} - Open Planner + {{ t.billing_open_planner }} {% else %} - Create Free Account + {{ t.billing_create_account }} {% endif %}
-

Need Help Building?

-

We connect you with verified partners

+

{{ t.billing_help_card }}

+

{{ t.billing_help_subtitle }}

    -
  • Court supplier quotes
  • -
  • Financing & bank connections
  • -
  • Construction planning
  • -
  • Equipment sourcing
  • +
  • {{ t.billing_help_feature_1 }}
  • +
  • {{ t.billing_help_feature_2 }}
  • +
  • {{ t.billing_help_feature_3 }}
  • +
  • {{ t.billing_help_feature_4 }}
{% if user %} - Get Supplier Quotes + {{ t.billing_get_quotes }} {% else %} - Sign Up to Get Started + {{ t.billing_signup }} {% endif %}
diff --git a/padelnomics/src/padelnomics/billing/templates/success.html b/padelnomics/src/padelnomics/billing/templates/success.html index 82dcd56..e718d55 100644 --- a/padelnomics/src/padelnomics/billing/templates/success.html +++ b/padelnomics/src/padelnomics/billing/templates/success.html @@ -1,15 +1,15 @@ {% extends "base.html" %} -{% block title %}Welcome - {{ config.APP_NAME }}{% endblock %} +{% block title %}{{ t.billing_success_title }} - {{ config.APP_NAME }}{% endblock %} {% block content %}
-

Welcome to Padelnomics!

+

{{ t.billing_success_h1 }}

-

Your account is ready. Start planning your padel court investment with our financial planner.

+

{{ t.billing_success_body }}

- Open Planner + {{ t.billing_success_btn }}
{% endblock %} diff --git a/padelnomics/src/padelnomics/businessplan.py b/padelnomics/src/padelnomics/businessplan.py index 77762e4..1bf9f61 100644 --- a/padelnomics/src/padelnomics/businessplan.py +++ b/padelnomics/src/padelnomics/businessplan.py @@ -28,15 +28,15 @@ def _fmt_pct(n) -> str: return f"{n * 100:.1f}%" -def _fmt_months(idx: int) -> str: +def _fmt_months(idx: int, t: dict) -> str: """Format payback month index as readable string.""" if idx < 0: - return "Not reached in 60 months" + return t["bp_payback_not_reached"] months = idx + 1 if months <= 12: - return f"{months} months" + return t["bp_months"].format(n=months) years = months / 12 - return f"{years:.1f} years" + return t["bp_years"].format(n=f"{years:.1f}") def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict: @@ -44,13 +44,22 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict: s = state t = get_translations(language) - venue_type = "Indoor" if s["venue"] == "indoor" else "Outdoor" - own_type = "Own" if s["own"] == "buy" else "Rent" + venue_type = t["bp_indoor"] if s["venue"] == "indoor" else t["bp_outdoor"] + own_type = t["bp_own"] if s["own"] == "buy" else t["bp_rent"] + + payback_str = _fmt_months(d["paybackIdx"], t) + irr_str = _fmt_pct(d["irr"]) + total_capex_str = _fmt_eur(d["capex"]) + equity_str = _fmt_eur(d["equity"]) + loan_str = _fmt_eur(d["loanAmount"]) + per_court_str = _fmt_eur(d["capexPerCourt"]) + per_sqm_str = _fmt_eur(d["capexPerSqm"]) sections = { + "lang": language, "title": t["bp_title"], "subtitle": f"{venue_type} ({own_type}) \u2014 {s.get('country', 'DE')}", - "courts": f"{s['dblCourts']} double + {s['sglCourts']} single ({d['totalCourts']} total)", + "courts": t["bp_courts_desc"].format(dbl=s["dblCourts"], sgl=s["sglCourts"], total=d["totalCourts"]), # Executive Summary "executive_summary": { @@ -58,22 +67,22 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict: "facility_type": f"{venue_type} ({own_type})", "courts": d["totalCourts"], "sqm": d["sqm"], - "total_capex": _fmt_eur(d["capex"]), - "equity": _fmt_eur(d["equity"]), - "loan": _fmt_eur(d["loanAmount"]), + "total_capex": total_capex_str, + "equity": equity_str, + "loan": loan_str, "y1_revenue": _fmt_eur(d["annuals"][0]["revenue"]) if d["annuals"] else "-", "y3_ebitda": _fmt_eur(d["stabEbitda"]), - "irr": _fmt_pct(d["irr"]), - "payback": _fmt_months(d["paybackIdx"]), + "irr": irr_str, + "payback": payback_str, }, # Investment Plan (CAPEX) "investment": { "heading": t["bp_investment"], "items": d["capexItems"], - "total": _fmt_eur(d["capex"]), - "per_court": _fmt_eur(d["capexPerCourt"]), - "per_sqm": _fmt_eur(d["capexPerSqm"]), + "total": total_capex_str, + "per_court": per_court_str, + "per_sqm": per_sqm_str, }, # Operating Costs @@ -114,10 +123,10 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict: "financing": { "heading": t["bp_financing"], "loan_pct": _fmt_pct(s["loanPct"] / 100), - "equity": _fmt_eur(d["equity"]), - "loan": _fmt_eur(d["loanAmount"]), + "equity": equity_str, + "loan": loan_str, "interest_rate": f"{s['interestRate']}%", - "term": f"{s['loanTerm']} years", + "term": t["bp_years"].format(n=s["loanTerm"]), "monthly_payment": _fmt_eur(d["monthlyPayment"]), "annual_debt_service": _fmt_eur(d["annualDebtService"]), "ltv": _fmt_pct(d["ltv"]), @@ -126,10 +135,10 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict: # Key Metrics "metrics": { "heading": t["bp_metrics"], - "irr": _fmt_pct(d["irr"]), + "irr": irr_str, "moic": f"{d['moic']:.2f}x", "cash_on_cash": _fmt_pct(d["cashOnCash"]), - "payback": _fmt_months(d["paybackIdx"]), + "payback": payback_str, "break_even_util": _fmt_pct(d["breakEvenUtil"]), "ebitda_margin": _fmt_pct(d["ebitdaMargin"]), "dscr_y3": f"{d['dscr'][2]['dscr']:.2f}x" if len(d["dscr"]) >= 3 else "-", @@ -152,6 +161,66 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict: for m in d["months"][:12] ], }, + + # Template labels + "labels": { + "scenario": t["bp_lbl_scenario"], + "generated_by": t["bp_lbl_generated_by"], + "exec_paragraph": t["bp_exec_paragraph"].format( + facility_type=f"{venue_type} ({own_type})", + courts=d["totalCourts"], + sqm=d["sqm"], + total_capex=total_capex_str, + equity=equity_str, + loan=loan_str, + irr=irr_str, + payback=payback_str, + ), + "total_investment": t["bp_lbl_total_investment"], + "equity_required": t["bp_lbl_equity_required"], + "year3_ebitda": t["bp_lbl_year3_ebitda"], + "irr": t["bp_lbl_irr"], + "payback_period": t["bp_lbl_payback_period"], + "year1_revenue": t["bp_lbl_year1_revenue"], + "item": t["bp_lbl_item"], + "amount": t["bp_lbl_amount"], + "notes": t["bp_lbl_notes"], + "total_capex": t["bp_lbl_total_capex"], + "capex_stats": t["bp_lbl_capex_stats"].format(per_court=per_court_str, per_sqm=per_sqm_str), + "equity": t["bp_lbl_equity"], + "loan": t["bp_lbl_loan"], + "interest_rate": t["bp_lbl_interest_rate"], + "loan_term": t["bp_lbl_loan_term"], + "monthly_payment": t["bp_lbl_monthly_payment"], + "annual_debt_service": t["bp_lbl_annual_debt_service"], + "ltv": t["bp_lbl_ltv"], + "monthly": t["bp_lbl_monthly"], + "total_monthly_opex": t["bp_lbl_total_monthly_opex"], + "annual_opex": t["bp_lbl_annual_opex"], + "weighted_hourly_rate": t["bp_lbl_weighted_hourly_rate"], + "target_utilization": t["bp_lbl_target_utilization"], + "gross_monthly_revenue": t["bp_lbl_gross_monthly_revenue"], + "net_monthly_revenue": t["bp_lbl_net_monthly_revenue"], + "monthly_ebitda": t["bp_lbl_monthly_ebitda"], + "monthly_net_cf": t["bp_lbl_monthly_net_cf"], + "year": t["bp_lbl_year"], + "revenue": t["bp_lbl_revenue"], + "ebitda": t["bp_lbl_ebitda"], + "debt_service": t["bp_lbl_debt_service"], + "net_cf": t["bp_lbl_net_cf"], + "moic": t["bp_lbl_moic"], + "cash_on_cash": t["bp_lbl_cash_on_cash"], + "payback": t["bp_lbl_payback"], + "break_even_util": t["bp_lbl_break_even_util"], + "ebitda_margin": t["bp_lbl_ebitda_margin"], + "dscr_y3": t["bp_lbl_dscr_y3"], + "yield_on_cost": t["bp_lbl_yield_on_cost"], + "month": t["bp_lbl_month"], + "opex": t["bp_lbl_opex"], + "debt": t["bp_lbl_debt"], + "cumulative": t["bp_lbl_cumulative"], + "disclaimer": t["bp_lbl_disclaimer"], + }, } return sections diff --git a/padelnomics/src/padelnomics/dashboard/routes.py b/padelnomics/src/padelnomics/dashboard/routes.py index bba5ec6..89fd001 100644 --- a/padelnomics/src/padelnomics/dashboard/routes.py +++ b/padelnomics/src/padelnomics/dashboard/routes.py @@ -8,6 +8,7 @@ from quart import Blueprint, flash, g, redirect, render_template, request, url_f from ..auth.routes import login_required, update_user from ..core import csrf_protect, fetch_one, soft_delete +from ..i18n import get_translations bp = Blueprint( "dashboard", @@ -58,7 +59,8 @@ async def settings(): name=form.get("name", "").strip() or None, updated_at=datetime.utcnow().isoformat(), ) - await flash("Settings saved!", "success") + t = get_translations(g.get("lang") or "en") + await flash(t["dash_settings_saved"], "success") return redirect(url_for("dashboard.settings")) return await render_template("settings.html") @@ -71,5 +73,6 @@ async def delete_account(): from quart import session await soft_delete("users", g.user["id"]) session.clear() - await flash("Your account has been deleted.", "info") + t = get_translations(g.lang) + await flash(t["dash_account_deleted"], "info") return redirect(url_for("public.landing")) diff --git a/padelnomics/src/padelnomics/dashboard/templates/index.html b/padelnomics/src/padelnomics/dashboard/templates/index.html index 2c9daec..87fd425 100644 --- a/padelnomics/src/padelnomics/dashboard/templates/index.html +++ b/padelnomics/src/padelnomics/dashboard/templates/index.html @@ -1,34 +1,34 @@ {% extends "base.html" %} -{% block title %}Dashboard - {{ config.APP_NAME }}{% endblock %} +{% block title %}{{ t.dash_page_title }} - {{ config.APP_NAME }}{% endblock %} {% block content %}
-

Dashboard

-

Welcome back{% if user.name %}, {{ user.name }}{% endif %}!

+

{{ t.dash_h1 }}

+

{{ t.dash_welcome }}{% if user.name %}, {{ user.name }}{% endif %}!

-

Saved Scenarios

+

{{ t.dash_saved_scenarios }}

{{ stats.scenarios }}

-

No limits

+

{{ t.dash_no_limits }}

-

Lead Requests

+

{{ t.dash_lead_requests }}

{{ stats.leads }}

-

Supplier & financing inquiries

+

{{ t.dash_lead_requests_sub }}

-

Plan

-

Free

-

Full access to all features

+

{{ t.dash_plan }}

+

{{ t.dash_plan_free }}

+

{{ t.dash_plan_free_sub }}

-

Quick Actions

+

{{ t.dash_quick_actions }}

{% endblock %} diff --git a/padelnomics/src/padelnomics/dashboard/templates/settings.html b/padelnomics/src/padelnomics/dashboard/templates/settings.html index 98af2ba..6833eb5 100644 --- a/padelnomics/src/padelnomics/dashboard/templates/settings.html +++ b/padelnomics/src/padelnomics/dashboard/templates/settings.html @@ -1,43 +1,43 @@ {% extends "base.html" %} -{% block title %}Settings - {{ config.APP_NAME }}{% endblock %} +{% block title %}{{ t.dash_settings_title }} - {{ config.APP_NAME }}{% endblock %} {% block content %}
-

Settings

+

{{ t.dash_settings_h1 }}

-

Profile

+

{{ t.dash_profile }}

- + -

Email cannot be changed

+

{{ t.dash_email_hint }}

- - + +
- +
-

Danger Zone

+

{{ t.dash_danger_zone }}

-

Once you delete your account, there is no going back.

+

{{ t.dash_delete_warning }}

- Delete Account -

This will delete all your scenarios and data permanently.

+ {{ t.dash_delete_account }} +

{{ t.dash_delete_confirm }}

- +
diff --git a/padelnomics/src/padelnomics/locales/de.json b/padelnomics/src/padelnomics/locales/de.json index 2c39fe3..3969074 100644 --- a/padelnomics/src/padelnomics/locales/de.json +++ b/padelnomics/src/padelnomics/locales/de.json @@ -1220,5 +1220,317 @@ "bp_annuals": "5-Jahres-Projektion", "bp_financing": "Finanzierungsstruktur", "bp_metrics": "Kennzahlen", - "bp_cashflow_12m": "12-Monats-Liquiditätsplan" + "bp_cashflow_12m": "12-Monats-Liquiditätsplan", + "dash_page_title": "Dashboard", + "dash_h1": "Dashboard", + "dash_welcome": "Willkommen zurück", + "dash_saved_scenarios": "Gespeicherte Szenarien", + "dash_no_limits": "Kein Limit", + "dash_lead_requests": "Lead-Anfragen", + "dash_lead_requests_sub": "Anbieter- & Finanzierungsanfragen", + "dash_plan": "Tarif", + "dash_plan_free": "Kostenlos", + "dash_plan_free_sub": "Voller Zugang zu allen Funktionen", + "dash_quick_actions": "Schnellzugriff", + "dash_open_planner": "Planer öffnen", + "dash_get_quotes": "Anbieter-Angebote anfragen", + "dash_settings": "Einstellungen", + "dash_settings_title": "Einstellungen", + "dash_settings_h1": "Einstellungen", + "dash_profile": "Profil", + "dash_email_label": "E-Mail", + "dash_email_hint": "E-Mail-Adresse kann nicht geändert werden", + "dash_name_label": "Name", + "dash_name_placeholder": "Dein Name", + "dash_save_changes": "Änderungen speichern", + "dash_danger_zone": "Gefahrenzone", + "dash_delete_warning": "Das Löschen deines Kontos kann nicht rückgängig gemacht werden.", + "dash_delete_account": "Konto löschen", + "dash_delete_confirm": "Alle deine Szenarien und Daten werden dauerhaft gelöscht.", + "dash_delete_btn": "Ja, Konto löschen", + "dash_settings_saved": "Einstellungen gespeichert!", + "dash_account_deleted": "Dein Konto wurde gelöscht.", + "billing_pricing_title": "Kostenloser Padel-Finanzplaner", + "billing_pricing_meta_desc": "Der ausgefeilteste Padel-Business-Plan-Rechner — vollständig kostenlos. 60+ Variablen, Sensitivitätsanalyse, Cashflow-Projektionen und Anbieterverbindungen.", + "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_planner_card": "Finanzplaner", + "billing_planner_free": "Kostenlos", + "billing_planner_forever": "— für immer", + "billing_feature_1": "60+ anpassbare Variablen", + "billing_feature_2": "6 Analyse-Tabs (CAPEX, Betrieb, Cashflow, Rendite, Kennzahlen)", + "billing_feature_3": "Sensitivitätsanalyse (Auslastung + Preisgestaltung)", + "billing_feature_4": "Unbegrenzte Szenarien speichern", + "billing_feature_5": "Interaktive Diagramme", + "billing_feature_6": "Innen/Außen & Kauf/Miete-Modelle", + "billing_open_planner": "Planer öffnen", + "billing_create_account": "Kostenloses Konto erstellen", + "billing_help_card": "Hilfe beim Aufbau?", + "billing_help_subtitle": "Wir verbinden dich mit verifizierten Partnern", + "billing_help_feature_1": "Anbieter-Angebote für Plätze", + "billing_help_feature_2": "Finanzierungs- & Bankverbindungen", + "billing_help_feature_3": "Bauplanung", + "billing_help_feature_4": "Ausstattungsbeschaffung", + "billing_get_quotes": "Anbieter-Angebote anfragen", + "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_btn": "Planer öffnen", + "billing_no_subscription": "Kein aktives Abonnement gefunden.", + "sd_page_title": "Anbieter-Dashboard", + "sd_nav_overview": "Übersicht", + "sd_nav_leads": "Lead-Feed", + "sd_nav_listing": "Mein Eintrag", + "sd_nav_boosts": "Boosts & Extras", + "sd_basic_plan_label": "Basic-Tarif", + "sd_basic_plan_desc": "Verzeichniseintrag + Kontaktformular.", + "sd_upgrade_growth": "Auf Growth upgraden für Lead-Zugang", + "sd_credits": "Credits", + "sd_loading": "Wird geladen…", + "sd_ov_new_leads_text": "neue Leads passen zu deinem Profil.", + "sd_ov_view_lead_feed": "Lead-Feed ansehen", + "sd_ov_profile_views": "Profilaufrufe", + "sd_ov_via_umami": "via Umami", + "sd_ov_enquiries": "Erhaltene Anfragen", + "sd_ov_leads_unlocked": "Freigeschaltete Leads", + "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_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_bst_current_plan": "Aktueller Tarif", + "sd_bst_credits_month": "Credits/Monat", + "sd_bst_per_mo": "/Monat", + "sd_bst_active_boosts": "Aktive Boosts", + "sd_bst_expires": "Läuft ab", + "sd_bst_active_subscription": "Aktives Abonnement", + "sd_bst_active": "Aktiv", + "sd_bst_no_active_boosts": "Keine aktiven Boosts", + "sd_bst_available_boosts": "Verfügbare Boosts", + "sd_bst_activate": "Aktivieren", + "sd_bst_not_configured": "Nicht konfiguriert", + "sd_bst_buy_credits": "Credit-Pakete kaufen", + "sd_bst_credits": "Credits", + "sd_bst_buy": "Kaufen", + "sd_bst_summary": "Übersicht", + "sd_bst_plan_suffix": "Tarif", + "sd_bst_subscription": "Abonnement", + "sd_bst_credits_balance": "Credits-Guthaben", + "sd_leads_h2": "Lead-Feed", + "sd_leads_credits": "Credits", + "sd_leads_buy_more": "Mehr kaufen", + "sd_leads_search_placeholder": "Leads nach Land, Typ, Details suchen…", + "sd_leads_filter_all": "Alle", + "sd_leads_filter_hot": "Hot", + "sd_leads_filter_warm": "Warm", + "sd_leads_filter_cool": "Cool", + "sd_leads_filter_countries": "Alle Länder", + "sd_leads_filter_any": "Beliebig", + "sd_leads_filter_asap": "So schnell wie möglich", + "sd_leads_filter_3_6mo": "3–6 Monate", + "sd_leads_filter_6_12mo": "6–12 Monate", + "sd_leads_region_badge": "Deine Region", + "sd_leads_facility": "Anlage", + "sd_leads_courts": "Plätze", + "sd_leads_country": "Land", + "sd_leads_timeline": "Zeitplan", + "sd_leads_budget": "Budget", + "sd_leads_be_first": "Noch keine anderen Anbieter — sei der Erste!", + "sd_leads_already_unlocked": "Anbieter haben bereits freigeschaltet", + "sd_leads_credits_to_unlock": "Credits zum Freischalten", + "sd_leads_unlock": "Freischalten", + "sd_leads_no_match": "Keine Leads entsprechen deinen Filtern", + "sd_leads_no_match_hint": "Passe deine Filter an oder schau später wieder vorbei.", + "sd_card_facility": "Anlage", + "sd_card_courts": "Plätze", + "sd_card_country": "Land", + "sd_card_timeline": "Zeitplan", + "sd_card_budget": "Budget", + "sd_card_context": "Kontext", + "sd_card_services": "Leistungen:", + "sd_card_credits": "Credits", + "sd_card_unlock_btn": "Lead freischalten", + "sd_card_unlocks": "Anbieter haben freigeschaltet", + "sd_timeline_asap": "So schnell wie möglich", + "sd_timeline_3_6mo": "3–6 Monate", + "sd_timeline_6_12mo": "6–12 Monate", + "sd_timeline_12plus": "12+ Monate", + "sd_timeline_exploring": "In der Recherche", + "sd_phase_permit_granted": "Genehmigung erteilt", + "sd_phase_lease_signed": "Mietvertrag unterzeichnet", + "sd_phase_permit_pending": "Genehmigung ausstehend", + "sd_phase_converting": "Bestand wird umgebaut", + "sd_phase_permit_not_filed": "Genehmigung nicht beantragt", + "sd_phase_location_found": "Standort gefunden", + "sd_phase_searching": "Standortsuche", + "sd_financing_self_funded": "Eigenfinanzierung", + "sd_financing_loan_approved": "Kredit genehmigt", + "sd_financing_seeking": "Finanzierung gesucht", + "sd_financing_not_started": "Noch nicht begonnen", + "sd_decision_solo": "Alleinentscheider", + "sd_decision_partners": "Mit Partnern", + "sd_decision_board": "Vorstand/Ausschuss", + "sd_decision_investor": "Investorengeführt", + "sd_contact_received_quotes": "Hat Angebote erhalten", + "sd_contact_contacted": "Hat Anbieter kontaktiert", + "sd_contact_none": "Kein vorheriger Kontakt", + "sd_stakeholder_owner": "Eigentümer/Betreiber", + "sd_stakeholder_investor": "Investor", + "sd_stakeholder_developer": "Projektentwickler", + "sd_stakeholder_club": "Verein/Verband", + "sd_stakeholder_other": "Sonstiges", + "sd_unlocked_badge": "Freigeschaltet", + "sd_unlocked_section_project": "Projekt", + "sd_unlocked_section_location": "Standort & Zeitplan", + "sd_unlocked_section_readiness": "Bereitschaft", + "sd_unlocked_section_notes": "Notizen", + "sd_unlocked_section_contact": "Kontakt", + "sd_unlocked_label_facility": "Anlage", + "sd_unlocked_label_courts": "Plätze", + "sd_unlocked_label_glass": "Glas", + "sd_unlocked_label_lighting": "Beleuchtung", + "sd_unlocked_label_budget": "Budget", + "sd_unlocked_label_services": "Leistungen", + "sd_unlocked_label_location": "Standort", + "sd_unlocked_label_timeline": "Zeitplan", + "sd_unlocked_label_phase": "Phase", + "sd_unlocked_label_financing": "Finanzierung", + "sd_unlocked_label_wants_financing": "Möchte Finanzierungshilfe", + "sd_unlocked_label_decision": "Entscheidungsprozess", + "sd_unlocked_label_prior_contact": "Vorheriger Anbieterkontakt", + "sd_unlocked_yes": "Ja", + "sd_unlocked_no": "Nein", + "sd_unlocked_label_name": "Name", + "sd_unlocked_label_email": "E-Mail", + "sd_unlocked_label_phone": "Telefon", + "sd_unlocked_label_company": "Unternehmen", + "sd_unlocked_label_role": "Rolle", + "sd_unlocked_view_plan": "Plan ansehen", + "sd_unlocked_credits_used": "Credits verwendet", + "sd_unlocked_remaining": "verbleibend", + "sd_unlocked_credits": "Credits", + "sd_unlocked_buy_more": "Mehr kaufen", + "sd_error_not_enough": "Nicht genug Credits", + "sd_error_credit_msg": "Du hast {balance} Credits, dieser Lead kostet {required}.", + "sd_error_buy": "Credits kaufen", + "sd_lf_page_title": "Lead-Feed", + "sd_lf_h1": "Lead-Feed", + "sd_lf_subtitle": "Qualifizierte Padel-Projekt-Leads durchsuchen und freischalten.", + "sd_lf_credits_available": "Credits verfügbar", + "sd_lf_all_countries": "Alle Länder", + "sd_lf_all_heat": "Alle", + "sd_lf_no_match": "Keine Leads entsprechen deinen Filtern", + "sd_lf_no_match_hint": "Passe deine Länder- oder Intensitätsfilter an oder schau später wieder vorbei.", + "sd_lst_saved": "Eintrag erfolgreich gespeichert.", + "sd_lst_preview_title": "Vorschau deines Verzeichniseintrags", + "sd_lst_edit_title": "Unternehmensdaten bearbeiten", + "sd_lst_company_name": "Unternehmensname", + "sd_lst_tagline": "Kurzaussage", + "sd_lst_tagline_placeholder": "Ein Satz für die Suchergebnisse", + "sd_lst_short_desc": "Kurzbeschreibung", + "sd_lst_full_desc": "Vollständige Beschreibung", + "sd_lst_website": "Website", + "sd_lst_logo": "Logo", + "sd_lst_cover_photo": "Titelbild", + "sd_lst_cover_hint": "— 16:9, min. 640px breit. Wird in den Suchergebnissen angezeigt.", + "sd_lst_contact_name": "Kontaktname", + "sd_lst_contact_email": "Kontakt-E-Mail", + "sd_lst_contact_phone": "Kontakttelefon", + "sd_lst_years_in_business": "Jahre im Geschäft", + "sd_lst_project_count": "Projektanzahl", + "sd_lst_service_categories": "Servicekategorien", + "sd_lst_service_area": "Servicegebiet (Länder)", + "sd_lst_contact_role": "Kontaktrolle / Titel", + "sd_lst_services_offered": "Angebotene Leistungen", + "sd_lst_social_links": "Social-Media-Links", + "sd_lst_save": "Änderungen speichern", + "sd_lst_verified": "Verifiziert", + "sd_boost_logo_name": "Logo", + "sd_boost_logo_desc": "Unternehmenslogo anzeigen", + "sd_boost_highlight_name": "Hervorhebung", + "sd_boost_highlight_desc": "Blauer Rahmen für die Karte", + "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_billing_yearly": "jährlich abgerechnet zu €{price}/Jahr", + "sd_billing_monthly": "monatlich abgerechnet", + "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", + "bp_rent": "Miete", + "bp_courts_desc": "{dbl} Doppel + {sgl} Einzel ({total} gesamt)", + "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_lbl_scenario": "Szenario", + "bp_lbl_generated_by": "Erstellt von Padelnomics \u2014 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_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_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", + "bp_lbl_break_even_util": "Break-Even-Auslastung", + "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" } diff --git a/padelnomics/src/padelnomics/locales/en.json b/padelnomics/src/padelnomics/locales/en.json index 23bbf66..af079d7 100644 --- a/padelnomics/src/padelnomics/locales/en.json +++ b/padelnomics/src/padelnomics/locales/en.json @@ -1220,5 +1220,317 @@ "bp_annuals": "5-Year Projection", "bp_financing": "Financing Structure", "bp_metrics": "Key Metrics", - "bp_cashflow_12m": "12-Month Cash Flow" + "bp_cashflow_12m": "12-Month Cash Flow", + "dash_page_title": "Dashboard", + "dash_h1": "Dashboard", + "dash_welcome": "Welcome back", + "dash_saved_scenarios": "Saved Scenarios", + "dash_no_limits": "No limits", + "dash_lead_requests": "Lead Requests", + "dash_lead_requests_sub": "Supplier & financing inquiries", + "dash_plan": "Plan", + "dash_plan_free": "Free", + "dash_plan_free_sub": "Full access to all features", + "dash_quick_actions": "Quick Actions", + "dash_open_planner": "Open Planner", + "dash_get_quotes": "Get Supplier Quotes", + "dash_settings": "Settings", + "dash_settings_title": "Settings", + "dash_settings_h1": "Settings", + "dash_profile": "Profile", + "dash_email_label": "Email", + "dash_email_hint": "Email cannot be changed", + "dash_name_label": "Name", + "dash_name_placeholder": "Your name", + "dash_save_changes": "Save Changes", + "dash_danger_zone": "Danger Zone", + "dash_delete_warning": "Once you delete your account, there is no going back.", + "dash_delete_account": "Delete Account", + "dash_delete_confirm": "This will delete all your scenarios and data permanently.", + "dash_delete_btn": "Yes, Delete My Account", + "dash_settings_saved": "Settings saved!", + "dash_account_deleted": "Your account has been deleted.", + "billing_pricing_title": "Free Padel Court Financial Planner", + "billing_pricing_meta_desc": "The most sophisticated padel court business plan calculator — completely free. 60+ variables, sensitivity analysis, cash flow projections, and supplier connections.", + "billing_pricing_og_title": "Free Padel Court Financial Planner", + "billing_pricing_og_desc": "Plan your padel court investment with 60+ variables, sensitivity analysis, and professional-grade projections. No signup required. Completely free.", + "billing_pricing_h1": "100% Free. No Catch.", + "billing_pricing_subtitle": "The most sophisticated padel court financial planner available — completely free. Plan your investment with 60+ variables, sensitivity analysis, and professional-grade projections.", + "billing_planner_card": "Financial Planner", + "billing_planner_free": "Free", + "billing_planner_forever": "— forever", + "billing_feature_1": "60+ adjustable variables", + "billing_feature_2": "6 analysis tabs (CAPEX, Operating, Cash Flow, Returns, Metrics)", + "billing_feature_3": "Sensitivity analysis (utilization + pricing)", + "billing_feature_4": "Save unlimited scenarios", + "billing_feature_5": "Interactive charts", + "billing_feature_6": "Indoor/outdoor & rent/buy models", + "billing_open_planner": "Open Planner", + "billing_create_account": "Create Free Account", + "billing_help_card": "Need Help Building?", + "billing_help_subtitle": "We connect you with verified partners", + "billing_help_feature_1": "Court supplier quotes", + "billing_help_feature_2": "Financing & bank connections", + "billing_help_feature_3": "Construction planning", + "billing_help_feature_4": "Equipment sourcing", + "billing_get_quotes": "Get Supplier Quotes", + "billing_signup": "Sign Up to Get Started", + "billing_success_title": "Welcome", + "billing_success_h1": "Welcome to Padelnomics!", + "billing_success_body": "Your account is ready. Start planning your padel court investment with our financial planner.", + "billing_success_btn": "Open Planner", + "billing_no_subscription": "No active subscription found.", + "sd_page_title": "Supplier Dashboard", + "sd_nav_overview": "Overview", + "sd_nav_leads": "Lead Feed", + "sd_nav_listing": "My Listing", + "sd_nav_boosts": "Boost & Upsells", + "sd_basic_plan_label": "Basic plan", + "sd_basic_plan_desc": "directory listing + enquiry form.", + "sd_upgrade_growth": "Upgrade to Growth for lead access", + "sd_credits": "credits", + "sd_loading": "Loading...", + "sd_ov_new_leads_text": "new lead(s) matching your profile.", + "sd_ov_view_lead_feed": "View Lead Feed", + "sd_ov_profile_views": "Profile Views", + "sd_ov_via_umami": "via Umami", + "sd_ov_enquiries": "Enquiries Received", + "sd_ov_leads_unlocked": "Leads Unlocked", + "sd_ov_credits_balance": "Credits Balance", + "sd_ov_directory_rank": "Directory Rank", + "sd_ov_basic_plan_label": "Basic plan", + "sd_ov_basic_plan_desc": "You have a verified listing with an enquiry form. Upgrade to Growth to access qualified project leads.", + "sd_ov_upgrade_growth": "Upgrade to Growth", + "sd_ov_recent_activity": "Recent Activity", + "sd_ov_credits": "credits", + "sd_ov_no_activity": "No activity yet. Unlock your first lead to get started.", + "sd_bst_current_plan": "Current Plan", + "sd_bst_credits_month": "credits/month", + "sd_bst_per_mo": "/mo", + "sd_bst_active_boosts": "Active Boosts", + "sd_bst_expires": "Expires", + "sd_bst_active_subscription": "Active subscription", + "sd_bst_active": "Active", + "sd_bst_no_active_boosts": "No active boosts", + "sd_bst_available_boosts": "Available Boosts", + "sd_bst_activate": "Activate", + "sd_bst_not_configured": "Not configured", + "sd_bst_buy_credits": "Buy Credit Packs", + "sd_bst_credits": "credits", + "sd_bst_buy": "Buy", + "sd_bst_summary": "Summary", + "sd_bst_plan_suffix": "plan", + "sd_bst_subscription": "subscription", + "sd_bst_credits_balance": "Credits Balance", + "sd_leads_h2": "Lead Feed", + "sd_leads_credits": "credits", + "sd_leads_buy_more": "Buy More", + "sd_leads_search_placeholder": "Search leads by country, type, details...", + "sd_leads_filter_all": "All", + "sd_leads_filter_hot": "Hot", + "sd_leads_filter_warm": "Warm", + "sd_leads_filter_cool": "Cool", + "sd_leads_filter_countries": "All countries", + "sd_leads_filter_any": "Any", + "sd_leads_filter_asap": "ASAP", + "sd_leads_filter_3_6mo": "3-6mo", + "sd_leads_filter_6_12mo": "6-12mo", + "sd_leads_region_badge": "Your region", + "sd_leads_facility": "Facility", + "sd_leads_courts": "Courts", + "sd_leads_country": "Country", + "sd_leads_timeline": "Timeline", + "sd_leads_budget": "Budget", + "sd_leads_be_first": "No other suppliers yet — be first!", + "sd_leads_already_unlocked": "supplier(s) already unlocked", + "sd_leads_credits_to_unlock": "credits to unlock", + "sd_leads_unlock": "Unlock", + "sd_leads_no_match": "No leads match your filters", + "sd_leads_no_match_hint": "Try adjusting your filters, or check back later for new leads.", + "sd_card_facility": "Facility", + "sd_card_courts": "Courts", + "sd_card_country": "Country", + "sd_card_timeline": "Timeline", + "sd_card_budget": "Budget", + "sd_card_context": "Context", + "sd_card_services": "Services:", + "sd_card_credits": "credits", + "sd_card_unlock_btn": "Unlock Lead", + "sd_card_unlocks": "supplier(s) unlocked", + "sd_timeline_asap": "ASAP", + "sd_timeline_3_6mo": "3-6 months", + "sd_timeline_6_12mo": "6-12 months", + "sd_timeline_12plus": "12+ months", + "sd_timeline_exploring": "Exploring", + "sd_phase_permit_granted": "Permit granted", + "sd_phase_lease_signed": "Lease signed", + "sd_phase_permit_pending": "Permit pending", + "sd_phase_converting": "Converting existing", + "sd_phase_permit_not_filed": "Permit not filed", + "sd_phase_location_found": "Location found", + "sd_phase_searching": "Searching", + "sd_financing_self_funded": "Self-funded", + "sd_financing_loan_approved": "Loan approved", + "sd_financing_seeking": "Seeking financing", + "sd_financing_not_started": "Not started", + "sd_decision_solo": "Solo decision-maker", + "sd_decision_partners": "With partners", + "sd_decision_board": "Board/committee", + "sd_decision_investor": "Investor-led", + "sd_contact_received_quotes": "Has received quotes", + "sd_contact_contacted": "Has contacted suppliers", + "sd_contact_none": "No prior contact", + "sd_stakeholder_owner": "Owner/Operator", + "sd_stakeholder_investor": "Investor", + "sd_stakeholder_developer": "Property Developer", + "sd_stakeholder_club": "Club/Association", + "sd_stakeholder_other": "Other", + "sd_unlocked_badge": "Unlocked", + "sd_unlocked_section_project": "Project", + "sd_unlocked_section_location": "Location & Timeline", + "sd_unlocked_section_readiness": "Readiness", + "sd_unlocked_section_notes": "Notes", + "sd_unlocked_section_contact": "Contact", + "sd_unlocked_label_facility": "Facility", + "sd_unlocked_label_courts": "Courts", + "sd_unlocked_label_glass": "Glass", + "sd_unlocked_label_lighting": "Lighting", + "sd_unlocked_label_budget": "Budget", + "sd_unlocked_label_services": "Services", + "sd_unlocked_label_location": "Location", + "sd_unlocked_label_timeline": "Timeline", + "sd_unlocked_label_phase": "Phase", + "sd_unlocked_label_financing": "Financing", + "sd_unlocked_label_wants_financing": "Wants financing help", + "sd_unlocked_label_decision": "Decision process", + "sd_unlocked_label_prior_contact": "Prior supplier contact", + "sd_unlocked_yes": "Yes", + "sd_unlocked_no": "No", + "sd_unlocked_label_name": "Name", + "sd_unlocked_label_email": "Email", + "sd_unlocked_label_phone": "Phone", + "sd_unlocked_label_company": "Company", + "sd_unlocked_label_role": "Role", + "sd_unlocked_view_plan": "View their plan", + "sd_unlocked_credits_used": "credits used", + "sd_unlocked_remaining": "remaining", + "sd_unlocked_credits": "credits", + "sd_unlocked_buy_more": "Buy More", + "sd_error_not_enough": "Not enough credits", + "sd_error_credit_msg": "You have {balance} credits, this lead costs {required}.", + "sd_error_buy": "Buy Credits", + "sd_lf_page_title": "Lead Feed", + "sd_lf_h1": "Lead Feed", + "sd_lf_subtitle": "Browse and unlock qualified padel project leads.", + "sd_lf_credits_available": "credits available", + "sd_lf_all_countries": "All countries", + "sd_lf_all_heat": "All heat", + "sd_lf_no_match": "No leads match your filters", + "sd_lf_no_match_hint": "Try adjusting your country or heat filters, or check back later for new leads.", + "sd_lst_saved": "Listing saved successfully.", + "sd_lst_preview_title": "Your Directory Card Preview", + "sd_lst_edit_title": "Edit Company Info", + "sd_lst_company_name": "Company Name", + "sd_lst_tagline": "Tagline", + "sd_lst_tagline_placeholder": "One-liner for search results", + "sd_lst_short_desc": "Short Description", + "sd_lst_full_desc": "Full Description", + "sd_lst_website": "Website", + "sd_lst_logo": "Logo", + "sd_lst_cover_photo": "Cover Photo", + "sd_lst_cover_hint": "— 16:9, min 640px wide. Shown in directory search results.", + "sd_lst_contact_name": "Contact Name", + "sd_lst_contact_email": "Contact Email", + "sd_lst_contact_phone": "Contact Phone", + "sd_lst_years_in_business": "Years in Business", + "sd_lst_project_count": "Project Count", + "sd_lst_service_categories": "Service Categories", + "sd_lst_service_area": "Service Area (Countries)", + "sd_lst_contact_role": "Contact Role / Title", + "sd_lst_services_offered": "Services Offered", + "sd_lst_social_links": "Social Links", + "sd_lst_save": "Save Changes", + "sd_lst_verified": "Verified", + "sd_boost_logo_name": "Logo", + "sd_boost_logo_desc": "Display your company logo", + "sd_boost_highlight_name": "Highlight", + "sd_boost_highlight_desc": "Blue highlighted card border", + "sd_boost_verified_name": "Verified Badge", + "sd_boost_verified_desc": "Verified checkmark badge", + "sd_boost_card_color_name": "Custom Card Color", + "sd_boost_card_color_desc": "Stand out with a custom border color on your directory listing", + "sd_billing_yearly": "billed annually at €{price}/yr", + "sd_billing_monthly": "billed monthly", + "sd_flash_signin": "Please sign in to continue.", + "sd_flash_active_plan": "You need an active supplier plan to access this page.", + "sd_flash_lead_access": "Lead access requires a Growth or Pro plan.", + "sd_flash_valid_email": "Please enter a valid email address.", + "sd_flash_claim_error": "This listing has already been claimed or does not exist.", + "sd_flash_listing_saved": "Listing saved successfully.", + + "bp_indoor": "Indoor", + "bp_outdoor": "Outdoor", + "bp_own": "Own", + "bp_rent": "Rent", + "bp_courts_desc": "{dbl} double + {sgl} single ({total} total)", + "bp_payback_not_reached": "Not reached in 60 months", + "bp_months": "{n} months", + "bp_years": "{n} years", + "bp_exec_paragraph": "This business plan models a {facility_type} padel facility with {courts} courts ({sqm} m\u00b2). Total investment is {total_capex}, financed with {equity} equity and {loan} debt. The projected IRR is {irr} with a payback period of {payback}.", + + "bp_lbl_scenario": "Scenario", + "bp_lbl_generated_by": "Generated by Padelnomics \u2014 padelnomics.io", + + "bp_lbl_total_investment": "Total Investment", + "bp_lbl_equity_required": "Equity Required", + "bp_lbl_year3_ebitda": "Year 3 EBITDA", + "bp_lbl_irr": "IRR", + "bp_lbl_payback_period": "Payback Period", + "bp_lbl_year1_revenue": "Year 1 Revenue", + + "bp_lbl_item": "Item", + "bp_lbl_amount": "Amount", + "bp_lbl_notes": "Notes", + "bp_lbl_total_capex": "Total CAPEX", + "bp_lbl_capex_stats": "CAPEX per court: {per_court} \u2022 CAPEX per m\u00b2: {per_sqm}", + + "bp_lbl_equity": "Equity", + "bp_lbl_loan": "Loan", + "bp_lbl_interest_rate": "Interest Rate", + "bp_lbl_loan_term": "Loan Term", + "bp_lbl_monthly_payment": "Monthly Payment", + "bp_lbl_annual_debt_service": "Annual Debt Service", + "bp_lbl_ltv": "Loan-to-Value", + + "bp_lbl_monthly": "Monthly", + "bp_lbl_total_monthly_opex": "Total Monthly OPEX", + "bp_lbl_annual_opex": "Annual OPEX", + + "bp_lbl_weighted_hourly_rate": "Weighted Hourly Rate", + "bp_lbl_target_utilization": "Target Utilization", + "bp_lbl_gross_monthly_revenue": "Gross Monthly Revenue", + "bp_lbl_net_monthly_revenue": "Net Monthly Revenue", + "bp_lbl_monthly_ebitda": "Monthly EBITDA", + "bp_lbl_monthly_net_cf": "Monthly Net Cash Flow", + + "bp_lbl_year": "Year", + "bp_lbl_revenue": "Revenue", + "bp_lbl_ebitda": "EBITDA", + "bp_lbl_debt_service": "Debt Service", + "bp_lbl_net_cf": "Net CF", + + "bp_lbl_moic": "MOIC", + "bp_lbl_cash_on_cash": "Cash-on-Cash (Y3)", + "bp_lbl_payback": "Payback", + "bp_lbl_break_even_util": "Break-Even Util.", + "bp_lbl_ebitda_margin": "EBITDA Margin", + "bp_lbl_dscr_y3": "DSCR (Y3)", + "bp_lbl_yield_on_cost": "Yield on Cost", + + "bp_lbl_month": "Month", + "bp_lbl_opex": "OPEX", + "bp_lbl_debt": "Debt", + "bp_lbl_cumulative": "Cumulative", + + "bp_lbl_disclaimer": "Disclaimer: This business plan is generated from user-provided assumptions using the Padelnomics financial model. All projections are estimates and do not constitute financial advice. Actual results may vary significantly based on market conditions, execution, and other factors. Consult with financial advisors before making investment decisions. \u00a9 Padelnomics \u2014 padelnomics.io" } diff --git a/padelnomics/src/padelnomics/suppliers/routes.py b/padelnomics/src/padelnomics/suppliers/routes.py index b7fde82..496f0dc 100644 --- a/padelnomics/src/padelnomics/suppliers/routes.py +++ b/padelnomics/src/padelnomics/suppliers/routes.py @@ -18,6 +18,7 @@ from ..core import ( get_paddle_price, waitlist_gate, ) +from ..i18n import get_translations bp = Blueprint( "suppliers", @@ -85,10 +86,10 @@ PLAN_FEATURES = { } BOOST_OPTIONS = [ - {"key": "boost_logo", "type": "logo", "name": "Logo", "price": 29, "desc": "Display your company logo"}, - {"key": "boost_highlight", "type": "highlight", "name": "Highlight", "price": 39, "desc": "Blue highlighted card border"}, - {"key": "boost_verified", "type": "verified", "name": "Verified Badge", "price": 49, "desc": "Verified checkmark badge"}, - {"key": "boost_card_color", "type": "card_color", "name": "Custom Card Color", "price": 19, "desc": "Stand out with a custom border color on your directory listing"}, + {"key": "boost_logo", "type": "logo", "name_key": "sd_boost_logo_name", "price": 29, "desc_key": "sd_boost_logo_desc"}, + {"key": "boost_highlight", "type": "highlight", "name_key": "sd_boost_highlight_name", "price": 39, "desc_key": "sd_boost_highlight_desc"}, + {"key": "boost_verified", "type": "verified", "name_key": "sd_boost_verified_name", "price": 49, "desc_key": "sd_boost_verified_desc"}, + {"key": "boost_card_color", "type": "card_color", "name_key": "sd_boost_card_color_name", "price": 19, "desc_key": "sd_boost_card_color_desc"}, ] CREDIT_PACK_OPTIONS = [ @@ -136,15 +137,16 @@ def _supplier_required(f): @wraps(f) async def decorated(*args, **kwargs): + t = get_translations(g.get("lang") or "en") if not g.get("user"): - await flash("Please sign in to continue.", "warning") + await flash(t["sd_flash_signin"], "warning") return redirect(url_for("auth.login", next=request.path)) supplier = await fetch_one( "SELECT * FROM suppliers WHERE claimed_by = ? AND tier IN ('basic', 'growth', 'pro')", (g.user["id"],), ) if not supplier: - await flash("You need an active supplier plan to access this page.", "warning") + await flash(t["sd_flash_active_plan"], "warning") return redirect(url_for("suppliers.signup")) g.supplier = supplier return await f(*args, **kwargs) @@ -158,15 +160,16 @@ def _lead_tier_required(f): @wraps(f) async def decorated(*args, **kwargs): + t = get_translations(g.get("lang") or "en") if not g.get("user"): - await flash("Please sign in to continue.", "warning") + await flash(t["sd_flash_signin"], "warning") return redirect(url_for("auth.login", next=request.path)) supplier = await fetch_one( "SELECT * FROM suppliers WHERE claimed_by = ? AND tier IN ('growth', 'pro')", (g.user["id"],), ) if not supplier: - await flash("Lead access requires a Growth or Pro plan.", "warning") + await flash(t["sd_flash_lead_access"], "warning") return redirect(url_for("suppliers.dashboard")) g.supplier = supplier return await f(*args, **kwargs) @@ -217,7 +220,8 @@ async def signup_waitlist(): plan = form.get("plan", "supplier_growth") if not email or "@" not in email: - await flash("Please enter a valid email address.", "error") + t = get_translations(g.get("lang") or "en") + await flash(t["sd_flash_valid_email"], "error") return redirect(url_for("suppliers.signup", plan=plan)) # Capture to DB with intent="supplier", but email confirmation uses plan name @@ -259,7 +263,7 @@ async def signup_step(step: int): included_boosts = plan_info.get("includes", []) # Compute order summary for step 4 - order = _compute_order(accumulated, included_boosts) + order = _compute_order(accumulated, included_boosts, get_translations(g.lang)) return await render_template( f"suppliers/partials/signup_step_{next_step}.html", @@ -273,7 +277,7 @@ async def signup_step(step: int): ) -def _compute_order(data: dict, included_boosts: list) -> dict: +def _compute_order(data: dict, included_boosts: list, t: dict) -> dict: """Compute order summary from accumulated wizard state.""" plan = data.get("plan", "supplier_growth") plan_info = PLAN_FEATURES.get(plan, PLAN_FEATURES["supplier_growth"]) @@ -282,11 +286,11 @@ def _compute_order(data: dict, included_boosts: list) -> dict: if period == "yearly": plan_price = plan_info["yearly_price"] plan_price_display = plan_info["yearly_monthly_equivalent"] - billing_label = f"billed annually at €{plan_price}/yr" + billing_label = t["sd_billing_yearly"].format(price=plan_price) else: plan_price = plan_info["monthly_price"] plan_price_display = plan_info["monthly_price"] - billing_label = "billed monthly" + billing_label = t["sd_billing_monthly"] one_time = 0 selected_boosts = data.get("boosts", []) @@ -425,7 +429,8 @@ async def claim(slug: str): "SELECT * FROM suppliers WHERE slug = ? AND claimed_by IS NULL", (slug,) ) if not supplier: - await flash("This listing has already been claimed or does not exist.", "warning") + t = get_translations(g.get("lang") or "en") + await flash(t["sd_flash_claim_error"], "warning") return redirect(url_for("directory.index")) return redirect(url_for("suppliers.signup", claim=slug)) diff --git a/padelnomics/src/padelnomics/suppliers/templates/suppliers/dashboard.html b/padelnomics/src/padelnomics/suppliers/templates/suppliers/dashboard.html index b14a447..798b93a 100644 --- a/padelnomics/src/padelnomics/suppliers/templates/suppliers/dashboard.html +++ b/padelnomics/src/padelnomics/suppliers/templates/suppliers/dashboard.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% block title %}Supplier Dashboard - {{ config.APP_NAME }}{% endblock %} +{% block title %}{{ t.sd_page_title }} - {{ config.APP_NAME }}{% endblock %} {% block paddle %}{% include "_paddle.html" %}{% endblock %} {% block head %} @@ -67,7 +67,7 @@ hx-push-url="{{ url_for('suppliers.dashboard', tab='overview') }}" class="{% if active_tab == 'overview' %}active{% endif %}"> - Overview + {{ t.sd_nav_overview }} {% if supplier.tier in ('growth', 'pro') %} - Lead Feed + {{ t.sd_nav_leads }} {% endif %} - My Listing + {{ t.sd_nav_listing }} {% if supplier.tier in ('growth', 'pro') %} - Boost & Upsells + {{ t.sd_nav_boosts }} {% endif %} {% if supplier.tier == 'basic' %}
- Basic plan — directory listing + enquiry form. + {{ t.sd_basic_plan_label }} — {{ t.sd_basic_plan_desc }} - Upgrade to Growth for lead access → + {{ t.sd_upgrade_growth }} →
{% endif %} @@ -129,7 +129,7 @@ hx-get="{% if active_tab == 'leads' and supplier.tier in ('growth', 'pro') %}{{ url_for('suppliers.dashboard_leads') }}{% elif active_tab == 'listing' %}{{ url_for('suppliers.dashboard_listing') }}{% elif active_tab == 'boosts' and supplier.tier in ('growth', 'pro') %}{{ url_for('suppliers.dashboard_boosts') }}{% else %}{{ url_for('suppliers.dashboard_overview') }}{% endif %}" hx-trigger="load" hx-swap="innerHTML"> -
Loading...
+
{{ t.sd_loading }}
{% endblock %} diff --git a/padelnomics/src/padelnomics/suppliers/templates/suppliers/lead_feed.html b/padelnomics/src/padelnomics/suppliers/templates/suppliers/lead_feed.html index 2a5e18c..b6a7bed 100644 --- a/padelnomics/src/padelnomics/suppliers/templates/suppliers/lead_feed.html +++ b/padelnomics/src/padelnomics/suppliers/templates/suppliers/lead_feed.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% block title %}Lead Feed - {{ config.APP_NAME }}{% endblock %} +{% block title %}{{ t.sd_lf_page_title }} - {{ config.APP_NAME }}{% endblock %} {% block head %}
-

Lead Feed

+

{{ t.sd_leads_h2 }}

- {{ supplier.credit_balance }} credits + {{ supplier.credit_balance }} {{ t.sd_leads_credits }} Buy More + hx-push-url="{{ url_for('suppliers.dashboard', tab='boosts') }}">{{ t.sd_leads_buy_more }}
- All + hx-get="{{ url_for('suppliers.dashboard_leads', country=current_country, timeline=current_timeline) }}">{{ t.sd_leads_filter_all }} Hot + hx-get="{{ url_for('suppliers.dashboard_leads', heat='hot', country=current_country, timeline=current_timeline) }}">{{ t.sd_leads_filter_hot }} Warm + hx-get="{{ url_for('suppliers.dashboard_leads', heat='warm', country=current_country, timeline=current_timeline) }}">{{ t.sd_leads_filter_warm }} Cool + hx-get="{{ url_for('suppliers.dashboard_leads', heat='cool', country=current_country, timeline=current_timeline) }}">{{ t.sd_leads_filter_cool }}
@@ -70,7 +70,7 @@ hx-target="#dashboard-content" hx-include="[name='heat'],[name='timeline']" name="country"> - + {% for c in countries %} {% endfor %} @@ -82,13 +82,13 @@ Any + hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country) }}">{{ t.sd_leads_filter_any }} ASAP + hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country, timeline='asap') }}">{{ t.sd_leads_filter_asap }} 3-6mo + hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country, timeline='3_6_months') }}">{{ t.sd_leads_filter_3_6mo }} 6-12mo + hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country, timeline='6_12_months') }}">{{ t.sd_leads_filter_6_12mo }} {% if leads %} @@ -103,27 +103,27 @@
{{ (lead.heat_score or 'cool') | upper }} {% if lead.country in service_area %} - Your region + {{ t.sd_leads_region_badge }} {% endif %}
-
Facility
{{ lead.facility_type or '-' }}
-
Courts
{{ lead.court_count or '-' }}
-
Country
{{ lead.country or '-' }}
-
Timeline
{{ lead.timeline or '-' }}
-
Budget
{% if lead.budget_estimate %}€{{ lead.budget_estimate }}{% else %}-{% endif %}
+
{{ t.sd_leads_facility }}
{{ lead.facility_type or '-' }}
+
{{ t.sd_leads_courts }}
{{ lead.court_count or '-' }}
+
{{ t.sd_leads_country }}
{{ lead.country or '-' }}
+
{{ t.sd_leads_timeline }}
{{ lead.timeline or '-' }}
+
{{ t.sd_leads_budget }}
{% if lead.budget_estimate %}€{{ lead.budget_estimate }}{% else %}-{% endif %}
{# Bidder count messaging #} {% if lead.bidder_count == 0 %} -
No other suppliers yet — be first!
+
{{ t.sd_leads_be_first }}
{% else %} -
{{ lead.bidder_count }} supplier{{ 's' if lead.bidder_count != 1 }} already unlocked
+
{{ lead.bidder_count }} {{ t.sd_leads_already_unlocked }}
{% endif %}
-
{{ lead.credit_cost or '?' }} credits to unlock
+
{{ lead.credit_cost or '?' }} {{ t.sd_leads_credits_to_unlock }}
- +
@@ -133,8 +133,8 @@ {% else %}
-

No leads match your filters

-

Try adjusting your filters, or check back later for new leads.

+

{{ t.sd_leads_no_match }}

+

{{ t.sd_leads_no_match_hint }}

{% endif %} diff --git a/padelnomics/src/padelnomics/suppliers/templates/suppliers/partials/dashboard_listing.html b/padelnomics/src/padelnomics/suppliers/templates/suppliers/partials/dashboard_listing.html index 3339b08..9d7be97 100644 --- a/padelnomics/src/padelnomics/suppliers/templates/suppliers/partials/dashboard_listing.html +++ b/padelnomics/src/padelnomics/suppliers/templates/suppliers/partials/dashboard_listing.html @@ -43,12 +43,12 @@ {% if saved is defined and saved %} -
Listing saved successfully.
+
{{ t.sd_lst_saved }}
{% endif %}
-

Your Directory Card Preview

+

{{ t.sd_lst_preview_title }}

{% include "suppliers/partials/dashboard_listing_preview.html" %}
@@ -56,53 +56,53 @@
-

Edit Company Info

+

{{ t.sd_lst_edit_title }}

- +
- - {{ t.sd_lst_tagline }} +
- +
- +
- +
- +
-