Multi-Language MVP: 4 Locales (DE/EN/UK/RU) mit SSR + UI + Tests #79

Merged
greggy merged 27 commits from feature/multilanguage-mvp into main 2026-06-05 23:49:39 +02:00
14 changed files with 1799 additions and 850 deletions
Showing only changes of commit 4b1c779846 - Show all commits

View File

@@ -4,8 +4,14 @@ declare(strict_types=1);
namespace App\Controllers;
use App\Core\I18n;
use App\Core\Locale;
use App\Core\View;
/**
* Base controller — injects i18n globals (locale, t() helper, locale switcher)
* into every render() call so views can use `$t('key')` and `$locale` directly.
*/
abstract class Controller
{
protected View $view;
@@ -15,9 +21,28 @@ abstract class Controller
$this->view = new View();
}
/**
* Render a view inside a layout.
*
* @param array<string,mixed> $data
*/
protected function render(string $view, array $data = [], string $layout = 'main'): void
{
foreach ($data as $key => $value) {
$locale = LocaleController::current();
$i18n = static fn (string $key, array $params = []): string => I18n::t($key, $params, $locale);
$globals = [
'locale' => $locale,
't' => $i18n,
'locale_switcher' => static function (string $currentPath) use ($locale): string {
$switcher = new LocaleSwitcher($locale, $currentPath);
return $switcher->render();
},
];
$merged = array_merge($globals, $data);
foreach ($merged as $key => $value) {
$this->view->assign($key, $value);
}
$this->view->render($view, $layout);

View File

@@ -4,15 +4,24 @@ declare(strict_types=1);
namespace App\Controllers;
use App\Core\I18n;
use App\Core\Locale;
class DatenschutzController extends Controller
{
public function index(): void
{
$locale = LocaleController::current();
$this->render('datenschutz/index', [
'pageTitle' => 'Datenschutzerklärung Haus Schleusingen',
'pageDescription' => 'Datenschutzerklärung der Website haus-schleusingen.de',
'pageTitle' => I18n::t('legal.privacy_h1', [], $locale) . ' ' . I18n::t('site.title', [], $locale),
'pageDescription' => I18n::t('legal.privacy_h1', [], $locale) . ' ' . I18n::t('site.title', [], $locale),
'robots' => 'noindex',
'canonical' => 'https://haus-schleusingen.de/datenschutz',
'canonical' => I18n::t('site.canonical_base', [], $locale) . '/datenschutz',
'ogLocale' => Locale::toOgLocale($locale),
'ogUrl' => I18n::t('site.canonical_base', [], $locale) . '/datenschutz',
'ogTitle' => I18n::t('legal.privacy_h1', [], $locale),
'ogDescription' => I18n::t('legal.privacy_h1', [], $locale),
]);
}
}

View File

@@ -4,184 +4,183 @@ declare(strict_types=1);
namespace App\Controllers;
use App\Core\I18n;
use App\Core\Locale;
class HomeController extends Controller
{
/** Map of interest option translation key → internal identifier. */
private const INTEREST_KEYS = [
'visit' => 'form.interest.visit',
'info' => 'form.interest.info',
'apply' => 'form.interest.apply',
];
public function index(): void
{
session_start();
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
// --- Helper functions ---
$normalizeContactValue = function (string $value): string {
return trim($value);
};
$locale = LocaleController::current();
$escapeContactValue = function (string $value): string {
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
};
$escapeContactValue = static fn(string $value): string
=> htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
$containsHeaderInjection = function (string $value): bool {
$containsHeaderInjection = static function (string $value): bool {
return (bool) preg_match('/[\r\n]/', $value);
};
// --- Form processing ---
$formErrors = [];
$formSuccess = false;
if (!empty($_SESSION['form_success'])) {
$formSuccess = true;
unset($_SESSION['form_success']);
}
if (!empty($_SESSION['form_errors'])) {
$formErrors = $_SESSION['form_errors'];
unset($_SESSION['form_errors']);
}
if (!empty($_SESSION['form_data'])) {
$formData = $_SESSION['form_data'];
unset($_SESSION['form_data']);
} else {
$formData = ['fname' => '', 'lname' => '', 'email' => '', 'phone' => '', 'interest' => 'Besichtigung anfragen', 'message' => ''];
// ── Pull flashed state ────────────────────────────────────────
$formSuccess = !empty($_SESSION['form_success']);
$formErrors = $_SESSION['form_errors'] ?? [];
$formData = $_SESSION['form_data'] ?? null;
unset($_SESSION['form_success'], $_SESSION['form_errors'], $_SESSION['form_data']);
if ($formSuccess) {
$formData = ['fname' => '', 'lname' => '', 'email' => '', 'phone' => '', 'interest' => 'visit', 'message' => ''];
} elseif (!is_array($formData)) {
$formData = ['fname' => '', 'lname' => '', 'email' => '', 'phone' => '', 'interest' => 'visit', 'message' => ''];
}
// CSRF-Token generieren (nach Session-Start)
// ── CSRF token ────────────────────────────────────────────────
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF-Token validieren
$csrfToken = $_POST['csrf_token'] ?? '';
// ── Form processing ───────────────────────────────────────────
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
$csrfToken = (string) ($_POST['csrf_token'] ?? '');
if (!hash_equals($_SESSION['csrf_token'] ?? '', $csrfToken)) {
header('Location: /#form-result');
$_SESSION['form_errors'] = ['Sicherheitsüberprüfung fehlgeschlagen. Bitte versuchen Sie es erneut.'];
$_SESSION['form_errors'] = ['form.error.csrf'];
header('Location: /#kontakt');
exit;
}
$formData['fname'] = $normalizeContactValue((string) ($_POST['fname'] ?? ''));
$formData['lname'] = $normalizeContactValue((string) ($_POST['lname'] ?? ''));
$formData['email'] = $normalizeContactValue((string) ($_POST['email'] ?? ''));
$formData['phone'] = $normalizeContactValue((string) ($_POST['phone'] ?? ''));
$formData['interest'] = $normalizeContactValue((string) ($_POST['interest'] ?? ''));
$formData['message'] = $normalizeContactValue((string) ($_POST['message'] ?? ''));
$formData['fname'] = trim((string) ($_POST['fname'] ?? ''));
$formData['lname'] = trim((string) ($_POST['lname'] ?? ''));
$formData['email'] = trim((string) ($_POST['email'] ?? ''));
$formData['phone'] = trim((string) ($_POST['phone'] ?? ''));
$formData['interest'] = trim((string) ($_POST['interest'] ?? 'visit'));
$formData['message'] = trim((string) ($_POST['message'] ?? ''));
$honeypot = $normalizeContactValue((string) ($_POST['website'] ?? ''));
// Honeypot: bots succeed silently.
$honeypot = trim((string) ($_POST['website'] ?? ''));
if ($honeypot !== '') {
header('Location: /#form-result');
$_SESSION['form_success'] = true;
exit;
} else {
if ($formData['fname'] === '') {
$formErrors[] = 'Bitte geben Sie Ihren Vornamen an.';
}
if ($formData['lname'] === '') {
$formErrors[] = 'Bitte geben Sie Ihren Nachnamen an.';
}
if ($formData['email'] === '' || !filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
$formErrors[] = 'Bitte geben Sie eine gültige E-Mail-Adresse an.';
}
if ($formData['message'] === '') {
$formErrors[] = 'Bitte geben Sie eine Nachricht ein.';
}
if ($containsHeaderInjection($formData['email']) || $containsHeaderInjection($formData['fname'] . ' ' . $formData['lname'])) {
$formErrors[] = 'Ungültige Zeichen in den Eingabefeldern.';
}
$formTime = isset($_POST['form_time']) ? (int) $_POST['form_time'] : 0;
if ($formTime > 0 && (time() - $formTime) < 3) {
$formErrors[] = 'Das Formular wurde zu schnell abgeschickt. Bitte versuchen Sie es erneut.';
}
$lastSubmit = $_SESSION['last_contact_submit'] ?? 0;
if ($lastSubmit && (time() - $lastSubmit) < 60) {
$formErrors[] = 'Bitte warten Sie einen Moment vor der nächsten Anfrage.';
}
if (empty($formErrors)) {
$to = 'mki@kies-media.de';
$subject = 'Kontaktanfrage: ' . $formData['interest'];
$body = "Von: {$formData['fname']} {$formData['lname']}\n"
. "E-Mail: {$formData['email']}\n";
if ($formData['phone'] !== '') {
$body .= "Telefon: {$formData['phone']}\n";
}
$body .= "Anliegen: {$formData['interest']}\n\n"
. $formData['message'];
$headers = "From: {$formData['email']}\r\n";
$headers .= "Reply-To: {$formData['email']}\r\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
$headers .= "X-Mailer: PHP/" . phpversion();
$mailSent = mail($to, $subject, $body, $headers);
if ($mailSent) {
$_SESSION['last_contact_submit'] = time();
header('Location: /#form-result');
$_SESSION['form_success'] = true;
exit;
} else {
$formErrors[] = 'Leider konnte die E-Mail nicht gesendet werden. Bitte versuchen Sie es später erneut oder schreiben Sie uns direkt an mki@kies-media.de.';
}
}
}
if (!empty($formErrors)) {
header('Location: /#form-result');
$_SESSION['form_errors'] = $formErrors;
$_SESSION['form_data'] = $formData;
header('Location: /#kontakt');
exit;
}
if ($formData['fname'] === '') {
$formErrors[] = 'form.error.fname_required';
}
if ($formData['lname'] === '') {
$formErrors[] = 'form.error.lname_required';
}
if ($formData['email'] === '' || !filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
$formErrors[] = 'form.error.email_invalid';
}
if ($formData['message'] === '') {
$formErrors[] = 'form.error.message_required';
}
if ($containsHeaderInjection($formData['email']) || $containsHeaderInjection($formData['fname'] . ' ' . $formData['lname'])) {
$formErrors[] = 'form.error.header_injection';
}
$formTime = isset($_POST['form_time']) ? (int) $_POST['form_time'] : 0;
if ($formTime > 0 && (time() - $formTime) < 3) {
$formErrors[] = 'form.error.too_fast';
}
$lastSubmit = $_SESSION['last_contact_submit'] ?? 0;
if ($lastSubmit && (time() - $lastSubmit) < 60) {
$formErrors[] = 'form.error.rate_limit';
}
if (empty($formErrors)) {
$interestKey = self::INTEREST_KEYS[$formData['interest']] ?? 'form.interest.visit';
$interestLabel = I18n::t($interestKey, [], $locale);
$to = 'mki@kies-media.de';
$subject = 'Kontaktanfrage: ' . $interestLabel;
$body = sprintf(
"Von: %s %s\nE-Mail: %s\n%sAnliegen: %s\n\n%s",
$formData['fname'],
$formData['lname'],
$formData['email'],
$formData['phone'] !== '' ? "Telefon: {$formData['phone']}\n" : '',
$interestLabel,
$formData['message']
);
$headers = "From: {$formData['email']}\r\n";
$headers .= "Reply-To: {$formData['email']}\r\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
$headers .= "X-Mailer: PHP/" . phpversion();
if (mail($to, $subject, $body, $headers)) {
$_SESSION['last_contact_submit'] = time();
$_SESSION['form_success'] = true;
header('Location: /#kontakt');
exit;
}
$formErrors[] = 'form.error.send_failed';
}
$_SESSION['form_errors'] = $formErrors;
$_SESSION['form_data'] = $formData;
header('Location: /#kontakt');
exit;
}
// ── Structured data (JSON-LD) — localized ────────────────────
$structuredData = json_encode([
'@context' => 'https://schema.org',
'@type' => 'RealEstateListing',
'name' => I18n::t('structured.listing_name', [], $locale),
'description'=> I18n::t('structured.listing_description', [], $locale),
'url' => I18n::t('site.canonical_base', [], $locale) . '/',
'image' => I18n::t('site.canonical_base', [], $locale) . '/bilder/Außenansicht-2.png',
'datePosted' => '2026-05-14',
'address' => [
'@type' => 'PostalAddress',
'streetAddress' => I18n::t('address.street', [], $locale),
'addressLocality' => I18n::t('address.city', [], $locale),
'postalCode' => '98553',
'addressCountry' => 'DE',
],
'offers' => [
'@type' => 'Offer',
'price' => '1300',
'priceCurrency' => 'EUR',
'priceSpecification' => [
'@type' => 'UnitPriceSpecification',
'price' => '1300',
'priceCurrency' => 'EUR',
'unitCode' => 'MON',
'description' => I18n::t('structured.price_description', [], $locale),
],
],
'floorSize' => [
'@type' => 'QuantitativeValue',
'value' => '227',
'unitCode' => 'MTK',
],
'numberOfRooms' => [
'@type' => 'QuantitativeValue',
'value' => '6',
],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$this->render('home/index', [
'formSuccess' => $formSuccess,
'formErrors' => $formErrors,
'formData' => $formData,
'interestKeys' => self::INTEREST_KEYS,
'escapeContactValue' => $escapeContactValue,
'pageTitle' => 'Einfamilienhaus mieten Schleusingen | 227 m², 6 Zimmer | 1.300 € Kaltmiete',
'pageDescription' => 'Einfamilienhaus zur Langzeitmiete in Schleusingen: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €. Bahnhofstraße 10, 98553 Schleusingen. Ab sofort verfügbar.',
'canonical' => 'https://haus-schleusingen.de/',
'openGraph' => [
'ogTitle' => 'Einfamilienhaus zur Miete in Schleusingen 227 m², 6 Zimmer',
'ogDescription' => 'Großzügiges Einfamilienhaus zur Langzeitmiete: 227 m², 6 Zimmer, 3 Etagen + Dachterrasse. Kaltmiete 1.300 €. Ab sofort verfügbar in Schleusingen.',
'ogImage' => 'https://haus-schleusingen.de/bilder/Außenansicht-2.png',
'ogUrl' => 'https://haus-schleusingen.de/',
],
'structuredData' => json_encode([
'@context' => 'https://schema.org',
'@type' => 'RealEstateListing',
'name' => 'Einfamilienhaus zur Miete in Schleusingen',
'description' => 'Großzügiges Einfamilienhaus zur Langzeitmiete: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €.',
'url' => 'https://haus-schleusingen.de/',
'image' => 'https://haus-schleusingen.de/bilder/Außenansicht-2.png',
'datePosted' => '2026-05-14',
'address' => [
'@type' => 'PostalAddress',
'streetAddress' => 'Bahnhofstraße 10',
'addressLocality' => 'Schleusingen',
'postalCode' => '98553',
'addressCountry' => 'DE',
],
'offers' => [
'@type' => 'Offer',
'price' => '1300',
'priceCurrency' => 'EUR',
'priceSpecification' => [
'@type' => 'UnitPriceSpecification',
'price' => '1300',
'priceCurrency' => 'EUR',
'unitCode' => 'MON',
'description' => 'Kaltmiete pro Monat',
],
],
'floorSize' => [
'@type' => 'QuantitativeValue',
'value' => '227',
'unitCode' => 'MTK',
],
'numberOfRooms' => [
'@type' => 'QuantitativeValue',
'value' => '6',
],
]),
'structuredData' => $structuredData,
]);
}
}

View File

@@ -4,15 +4,24 @@ declare(strict_types=1);
namespace App\Controllers;
use App\Core\I18n;
use App\Core\Locale;
class ImpressumController extends Controller
{
public function index(): void
{
$locale = LocaleController::current();
$this->render('impressum/index', [
'pageTitle' => 'Impressum Haus Schleusingen',
'pageDescription' => 'Impressum der Website haus-schleusingen.de',
'pageTitle' => I18n::t('legal.imprint_h1', [], $locale) . ' ' . I18n::t('site.title', [], $locale),
'pageDescription' => I18n::t('legal.imprint_h1', [], $locale) . ' ' . I18n::t('site.title', [], $locale),
'robots' => 'noindex',
'canonical' => 'https://haus-schleusingen.de/impressum',
'canonical' => I18n::t('site.canonical_base', [], $locale) . '/impressum',
'ogLocale' => Locale::toOgLocale($locale),
'ogUrl' => I18n::t('site.canonical_base', [], $locale) . '/impressum',
'ogTitle' => I18n::t('legal.imprint_h1', [], $locale),
'ogDescription' => I18n::t('legal.imprint_h1', [], $locale),
]);
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\I18n;
use App\Core\Locale;
/**
* Renders the language switcher widget. Pure HTML generation — no
* side effects, no header writing.
*
* Output is semantic HTML (a <ul> of links) with `aria-current` for the
* active locale, `hreflang` for SEO, and `lang` for screen readers.
* The basic list-of-locale-codes is the MVP. Sub-Issue D (responsive
* SVG flag UI) refines the presentation.
*/
final class LocaleSwitcher
{
public function __construct(
private readonly string $currentLocale,
private readonly string $currentPath,
) {
}
public function render(): string
{
$path = $this->sanitisePath($this->currentPath);
$ariaLabel = htmlspecialchars(
I18n::t('locale.switcher.aria', [], $this->currentLocale),
ENT_QUOTES,
'UTF-8',
);
$html = '<ul class="locale-switcher" role="list" aria-label="' . $ariaLabel . '">';
foreach (Locale::SUPPORTED as $code) {
$isCurrent = $code === $this->currentLocale;
$name = htmlspecialchars(I18n::t('locale.' . $code, [], $this->currentLocale), ENT_QUOTES, 'UTF-8');
$codeAttr = htmlspecialchars($code, ENT_QUOTES, 'UTF-8');
$html .= '<li' . ($isCurrent ? ' class="is-current" aria-current="true"' : '') . '>';
if ($isCurrent) {
$html .= '<span lang="' . $codeAttr . '">' . $codeAttr . '<span class="visually-hidden"> (' . $name . ')</span></span>';
} else {
$url = '/locale?set=' . rawurlencode($code) . '&amp;return=' . rawurlencode($path);
$html .= '<a href="' . $url . '" hreflang="' . $codeAttr . '" lang="' . $codeAttr . '" rel="alternate">'
. $codeAttr
. '<span class="visually-hidden"> (' . $name . ')</span>'
. '</a>';
}
$html .= '</li>';
}
$html .= '</ul>';
return $html;
}
/**
* Make sure the path is safe to embed as a query string value and
* a redirect target. Drops query/fragment, keeps only the path.
*/
private function sanitisePath(string $path): string
{
$path = parse_url($path, PHP_URL_PATH) ?: '/';
if ($path === '' || $path[0] !== '/') {
return '/';
}
return $path;
}
}

227
app/Locales/de.php Normal file
View File

@@ -0,0 +1,227 @@
<?php
declare(strict_types=1);
/**
* German (DE) translations — source of truth.
*
* Keys are dot-separated, e.g. 'nav.cta' => '...'.
* Use {placeholders} for runtime interpolation: t('greeting', ['name' => 'Anna']).
*
* @see \App\Core\I18n::t()
*/
return [
// ─── Site meta ───────────────────────────────────────────────────────
'site.name' => 'Haus Schleusingen',
'site.title' => 'Einfamilienhaus mieten Schleusingen | 227 m², 6 Zimmer | 1.300 € Kaltmiete',
'site.description' => 'Einfamilienhaus zur Langzeitmiete in Schleusingen: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €. Bahnhofstraße 10, 98553 Schleusingen. Ab sofort verfügbar.',
'site.og_locale' => 'de_DE',
'site.canonical_base' => 'https://haus-schleusingen.de',
// ─── Navigation ──────────────────────────────────────────────────────
'nav.skip' => 'Zum Inhalt springen',
'nav.main' => 'Hauptnavigation',
'nav.toggle' => 'Navigation öffnen',
'nav.gallery' => 'Galerie',
'nav.layout' => 'Grundriss',
'nav.rent' => 'Miete',
'nav.location' => 'Lage',
'nav.cta' => 'Jetzt anfragen',
// ─── Hero ────────────────────────────────────────────────────────────
'hero.tag' => 'Zur Langzeitmiete · Ab sofort verfügbar',
'hero.h1.line1' => 'Großzügiges',
'hero.h1.line2' => 'Einfamilienhaus',
'hero.h1.line3' => 'in Schleusingen',
'hero.address' => 'Schleusinger Bahnhofstraße 10',
'hero.area' => '227 m² Wohnfläche',
'hero.rooms' => '6 Zimmer',
'hero.floors' => '3 Etagen + Dachterrasse',
'hero.discover' => 'Entdecken',
// ─── Facts strip ─────────────────────────────────────────────────────
'facts.area' => 'm² Wohnfläche',
'facts.rooms' => 'Zimmer',
'facts.floors' => 'Etagen',
'facts.rent' => '€ Kaltmiete',
// ─── Intro ───────────────────────────────────────────────────────────
'intro.eyebrow' => 'Das Objekt',
'intro.h2' => 'Wohnen mit Charakter und viel Raum',
'intro.p1' => 'Vermietet wird ein vollständiges Einfamilienhaus in ruhiger Lage von Schleusingen. Das Haus verbindet historischen Charme mit modernem Wohnkomfort auf drei großzügigen Etagen.',
'intro.p2' => 'Garage für zwei Fahrzeuge, großzügige Dachterrasse mit 35,8 m², vollausgestattete Küche, Vollbad sowie Abstell- und Nutzräume machen das Haus zu einem außergewöhnlichen Mietobjekt.',
'intro.stats.area' => 'Nutzfläche',
'intro.stats.terrace' => 'Dachterrasse',
'intro.stats.garage' => 'Garage',
'intro.badge' => 'Wohnzimmer · 42,6 m²',
// ─── Gallery ─────────────────────────────────────────────────────────
'gallery.aria' => 'Fotogalerie',
'gallery.eyebrow' => 'Fotogalerie',
'gallery.h2' => 'Einblicke ins Haus',
'gallery.zoom' => ' Großansicht öffnen',
'gallery.exterior' => 'Außenansicht',
'gallery.living' => 'Wohnzimmer',
'gallery.living_area' => 'Wohnzimmer · 42,6 m²',
'gallery.kitchen' => 'Küche · 18,4 m²',
'gallery.bedroom' => 'Schlafzimmer · 18 m²',
'gallery.bath' => 'Badezimmer · 9,8 m²',
'gallery.kid1' => 'Kinderzimmer 1 · 21,7 m²',
'gallery.kid2' => 'Kinderzimmer 2 · 15,7 m²',
'gallery.kid_detail' => 'Kinderzimmer Detail',
'gallery.guest' => 'Gästezimmer · 11,5 m²',
'gallery.area1' => 'Wohnbereich',
'gallery.area2' => 'Wohnbereich Detail',
'gallery.area3' => 'Hausansicht',
'gallery.alt.living' => 'Wohnzimmer mit 42,6 m² Wohnfläche',
'gallery.alt.kitchen' => 'Küche mit 18,4 m²',
'gallery.alt.bedroom' => 'Schlafzimmer mit 18 m²',
'gallery.alt.bath' => 'Badezimmer mit 9,8 m²',
'gallery.alt.kid1' => 'Kinderzimmer 1 mit 21,7 m²',
'gallery.alt.kid2' => 'Kinderzimmer 2 mit 15,7 m²',
'gallery.alt.kid_detail' => 'Detailansicht Kinderzimmer',
'gallery.alt.guest' => 'Gästezimmer mit 11,5 m²',
'gallery.alt.bath2' => 'Zweites Badezimmer im Haus',
'gallery.alt.bath3' => 'Drittes Badezimmer im Haus',
'gallery.alt.exterior' => 'Außenansicht des Einfamilienhauses',
// ─── Floor plans (Grundriss) ────────────────────────────────────────
'floors.eyebrow' => 'Raumaufteilung',
'floors.h2' => 'Großzügig auf allen Etagen',
'floors.eg.title' => 'Erdgeschoss',
'floors.og1.title' => '1. Obergeschoss',
'floors.og2.title' => '2. Obergeschoss',
'floors.attic.title' => 'Dachboden',
'floors.eg.area' => '99,5 m²',
'floors.og1.area' => '120,4 m²',
'floors.og2.area' => '68 m²',
'floors.attic.area' => '94 m² Nutzfläche',
'floors.room.hall' => 'Flur',
'floors.room.wc' => 'WC',
'floors.room.garage' => 'Garage / Partykeller',
'floors.room.storage1' => 'Abstellraum 1',
'floors.room.storage2' => 'Abstellraum 2',
'floors.room.heating' => 'Heizungskeller',
'floors.room.living' => 'Wohnzimmer',
'floors.room.guest' => 'Gästezimmer',
'floors.room.bath' => 'Badezimmer',
'floors.room.kitchen' => 'Küche',
'floors.room.bedroom' => 'Schlafzimmer',
'floors.room.kid1' => 'Kinderzimmer 1',
'floors.room.kid2' => 'Kinderzimmer 2',
'floors.room.play' => 'Spielzimmer',
'floors.room.dressing' => 'Ankleidezimmer',
'floors.room.terrace' => 'Dachterrasse',
'floors.room.terrace_note' => '(25% von 35,8 m²)',
'floors.room.attic_low' => 'Dachboden unten (ungeheizt)',
'floors.room.attic_mid' => 'Dachboden Mitte (ungeheizt)',
'floors.room.attic_high' => 'Dachboden oben (ungeheizt)',
'floors.alt.eg' => 'Grundriss Erdgeschoss',
'floors.alt.og1' => 'Grundriss 1. Obergeschoss',
'floors.alt.og2' => 'Grundriss 2. Obergeschoss',
'floors.alt.attic' => 'Grundriss Dachboden',
// ─── Rent (Miete) ────────────────────────────────────────────────────
'rent.eyebrow' => 'Mietkonditionen',
'rent.aria' => 'Mietkonditionen',
'rent.h2' => 'Transparente Preisgestaltung',
'rent.cold' => 'Kaltmiete',
'rent.warm' => 'Gesamtmiete warm',
'rent.deposit' => 'Kaution',
'rent.per_month' => 'pro Monat',
'rent.warm_includes' => 'inkl. 300 € Nebenkosten',
'rent.deposit_months' => '2 Nettokaltmieten',
'rent.note.available' => 'Verfügbarkeit',
'rent.note.available_val'=> 'Ab sofort · unbefristete Laufzeit',
'rent.note.costs' => 'Nebenkosten',
'rent.note.costs_val' => 'Vorauszahlung 300 €/Monat, jährliche Abrechnung',
'rent.note.energy' => 'Energieausweis',
'rent.note.energy_val' => 'Wird bei Mietbeginn übergeben · Erdgasheizung',
'rent.note.pets' => 'Haustiere',
'rent.note.pets_val' => 'Auf Anfrage',
// ─── Location (Lage) ─────────────────────────────────────────────────
'loc.eyebrow' => 'Standort',
'loc.h2' => 'Zentral und ruhig zugleich',
'loc.shopping' => 'Einkaufen & Versorgung',
'loc.shopping_desc' => 'Supermärkte, Ärzte, Apotheken und Schulen sind fußläufig erreichbar',
'loc.transport' => 'Öffentlicher Nahverkehr',
'loc.transport_desc' => 'Zentrale Bushaltestelle ca. 200 m entfernt — direkte Verbindungen in die Region',
'loc.center' => 'Innenstadt Schleusingen',
'loc.center_desc' => 'Wochenmarkt und Stadtmitte nur ca. 500 m entfernt',
'loc.address' => 'Genaue Adresse',
'loc.address_val' => 'Schleusinger Bahnhofstraße 10<br />98553 Schleusingen, Thüringen',
'loc.map_title' => 'Standort Bahnhofstraße 10, Schleusingen',
// ─── Contact ─────────────────────────────────────────────────────────
'contact.eyebrow' => 'Kontakt',
'contact.aria' => 'Kontaktformular',
'contact.h2' => 'Interesse?',
'contact.h2_em' => 'Schreiben Sie uns.',
'contact.intro' => 'Wir freuen uns über Ihre Anfrage und melden uns innerhalb von 24 Stunden. Besichtigungstermine sind nach Absprache möglich. Bitte geben Sie bei Ihrer Anfrage ein paar Terminvorschläge an.',
'contact.success' => 'Vielen Dank für Ihre Anfrage!',
'contact.success_sub' => 'Wir haben Ihre Nachricht erhalten und melden uns innerhalb von 24 Stunden bei Ihnen.',
'contact.fname' => 'Vorname',
'contact.lname' => 'Nachname',
'contact.email' => 'E-Mail',
'contact.phone' => 'Telefon',
'contact.interest' => 'Anliegen',
'contact.interest_visit' => 'Besichtigung anfragen',
'contact.interest_info' => 'Allgemeine Informationen',
'contact.interest_apply' => 'Mietbewerbung einreichen',
'contact.message' => 'Nachricht',
'contact.submit' => 'Anfrage absenden',
'contact.hp_label' => 'Website',
'contact.direct' => 'Oder schreiben Sie uns direkt:',
// ─── Footer ──────────────────────────────────────────────────────────
'footer.address' => 'Bahnhofstraße 10 · Schleusingen',
'footer.imprint' => 'Impressum',
'footer.privacy' => 'Datenschutz',
// ─── Lightbox ────────────────────────────────────────────────────────
'lightbox.aria' => 'Bildansicht',
'lightbox.close' => 'Bildansicht schließen',
// ─── Legal pages ─────────────────────────────────────────────────────
'legal.back' => '← Zurück zum Objekt',
'legal.german_only' => 'Diese Seite ist nur auf Deutsch verfügbar.',
'legal.imprint_eyebrow' => 'Pflichtangaben',
'legal.imprint_h1' => 'Impressum',
'legal.privacy_eyebrow' => 'Datenschutz',
'legal.privacy_h1' => 'Datenschutzerklärung',
// ─── Locale switcher (UI) ────────────────────────────────────────────
'locale.switcher.aria' => 'Sprache wählen',
'locale.de' => 'Deutsch',
'locale.en' => 'English',
'locale.uk' => 'Українська',
'locale.ru' => 'Русский',
'locale.current' => 'Aktuelle Sprache: {lang}',
// ─── Form errors (keys are referenced by the controller) ────────────
'form.error.csrf' => 'Sicherheitsüberprüfung fehlgeschlagen. Bitte versuchen Sie es erneut.',
'form.error.fname_required' => 'Bitte geben Sie Ihren Vornamen an.',
'form.error.lname_required' => 'Bitte geben Sie Ihren Nachnamen an.',
'form.error.email_invalid' => 'Bitte geben Sie eine gültige E-Mail-Adresse an.',
'form.error.message_required' => 'Bitte geben Sie eine Nachricht ein.',
'form.error.header_injection' => 'Ungültige Zeichen in den Eingabefeldern.',
'form.error.too_fast' => 'Das Formular wurde zu schnell abgeschickt. Bitte versuchen Sie es erneut.',
'form.error.rate_limit' => 'Bitte warten Sie einen Moment vor der nächsten Anfrage.',
'form.error.send_failed' => 'Leider konnte die E-Mail nicht gesendet werden. Bitte versuchen Sie es später erneut oder schreiben Sie uns direkt an mki@kies-media.de.',
// ─── Form interest options ──────────────────────────────────────────
'form.interest.visit' => 'Besichtigung anfragen',
'form.interest.info' => 'Allgemeine Informationen',
'form.interest.apply' => 'Mietbewerbung einreichen',
// ─── Misc ───────────────────────────────────────────────────────────
'nav.back_home' => '← Zurück zur Startseite',
// ─── Address & structured data (JSON-LD, search engines) ───────────
'address.street' => 'Bahnhofstraße 10',
'address.city' => 'Schleusingen',
'structured.listing_name' => 'Einfamilienhaus zur Miete in Schleusingen',
'structured.listing_description' => 'Großzügiges Einfamilienhaus zur Langzeitmiete: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €.',
'structured.price_description' => 'Kaltmiete pro Monat',
];

205
app/Locales/en.php Normal file
View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
/**
* English (EN) translations.
* @see de.php for key reference.
*/
return [
'site.name' => 'House Schleusingen',
'site.title' => 'Detached house for rent in Schleusingen | 227 m², 6 rooms | €1,300 cold rent',
'site.description' => 'Detached house for long-term rental in Schleusingen: 227 m² living space, 6 rooms, 3 floors with rooftop terrace. Cold rent €1,300. Bahnhofstraße 10, 98553 Schleusingen. Available immediately.',
'site.og_locale' => 'en_US',
'site.canonical_base' => 'https://haus-schleusingen.de/en',
'nav.skip' => 'Skip to content',
'nav.main' => 'Main navigation',
'nav.toggle' => 'Open navigation',
'nav.gallery' => 'Gallery',
'nav.layout' => 'Floor plan',
'nav.rent' => 'Rent',
'nav.location' => 'Location',
'nav.cta' => 'Inquire now',
'hero.tag' => 'Long-term rental · Available immediately',
'hero.h1.line1' => 'Spacious',
'hero.h1.line2' => 'Detached house',
'hero.h1.line3' => 'in Schleusingen',
'hero.address' => 'Schleusinger Bahnhofstraße 10',
'hero.area' => '227 m² living space',
'hero.rooms' => '6 rooms',
'hero.floors' => '3 floors + rooftop terrace',
'hero.discover' => 'Discover',
'facts.area' => 'm² living space',
'facts.rooms' => 'rooms',
'facts.floors' => 'floors',
'facts.rent' => '€ cold rent',
'intro.eyebrow' => 'The property',
'intro.h2' => 'Living with character and plenty of space',
'intro.p1' => 'A complete detached house in a quiet part of Schleusingen is being rented out. The house combines historic charm with modern living comfort across three spacious floors.',
'intro.p2' => 'A two-car garage, a generous 35.8 m² rooftop terrace, a fully equipped kitchen, a full bathroom plus storage and utility rooms make this an exceptional rental property.',
'intro.stats.area' => 'Usable area',
'intro.stats.terrace' => 'Rooftop terrace',
'intro.stats.garage' => 'Garage',
'intro.badge' => 'Living room · 42.6 m²',
'gallery.aria' => 'Photo gallery',
'gallery.eyebrow' => 'Photo gallery',
'gallery.h2' => 'A look inside',
'gallery.zoom' => ' open full view',
'gallery.exterior' => 'Exterior view',
'gallery.living' => 'Living room',
'gallery.living_area' => 'Living room · 42.6 m²',
'gallery.kitchen' => 'Kitchen · 18.4 m²',
'gallery.bedroom' => 'Bedroom · 18 m²',
'gallery.bath' => 'Bathroom · 9.8 m²',
'gallery.kid1' => 'Child\'s room 1 · 21.7 m²',
'gallery.kid2' => 'Child\'s room 2 · 15.7 m²',
'gallery.kid_detail' => 'Child\'s room detail',
'gallery.guest' => 'Guest room · 11.5 m²',
'gallery.area1' => 'Living area',
'gallery.area2' => 'Living area detail',
'gallery.area3' => 'House view',
'gallery.alt.living' => 'Living room with 42.6 m² of floor space',
'gallery.alt.kitchen' => 'Kitchen with 18.4 m²',
'gallery.alt.bedroom' => 'Bedroom with 18 m²',
'gallery.alt.bath' => 'Bathroom with 9.8 m²',
'gallery.alt.kid1' => 'Child\'s room 1 with 21.7 m²',
'gallery.alt.kid2' => 'Child\'s room 2 with 15.7 m²',
'gallery.alt.kid_detail' => 'Child\'s room detail view',
'gallery.alt.guest' => 'Guest room with 11.5 m²',
'gallery.alt.bath2' => 'Second bathroom in the house',
'gallery.alt.bath3' => 'Third bathroom in the house',
'gallery.alt.exterior' => 'Exterior view of the detached house',
'floors.eyebrow' => 'Layout',
'floors.h2' => 'Spacious on every floor',
'floors.eg.title' => 'Ground floor',
'floors.og1.title' => '1st upper floor',
'floors.og2.title' => '2nd upper floor',
'floors.attic.title' => 'Attic',
'floors.eg.area' => '99.5 m²',
'floors.og1.area' => '120.4 m²',
'floors.og2.area' => '68 m²',
'floors.attic.area' => '94 m² usable area',
'floors.room.hall' => 'Hallway',
'floors.room.wc' => 'WC',
'floors.room.garage' => 'Garage / party room',
'floors.room.storage1' => 'Storage room 1',
'floors.room.storage2' => 'Storage room 2',
'floors.room.heating' => 'Heating cellar',
'floors.room.living' => 'Living room',
'floors.room.guest' => 'Guest room',
'floors.room.bath' => 'Bathroom',
'floors.room.kitchen' => 'Kitchen',
'floors.room.bedroom' => 'Bedroom',
'floors.room.kid1' => 'Child\'s room 1',
'floors.room.kid2' => 'Child\'s room 2',
'floors.room.play' => 'Playroom',
'floors.room.dressing' => 'Dressing room',
'floors.room.terrace' => 'Rooftop terrace',
'floors.room.terrace_note' => '(25% of 35.8 m²)',
'floors.room.attic_low' => 'Attic lower (unheated)',
'floors.room.attic_mid' => 'Attic middle (unheated)',
'floors.room.attic_high' => 'Attic upper (unheated)',
'floors.alt.eg' => 'Floor plan ground floor',
'floors.alt.og1' => 'Floor plan 1st upper floor',
'floors.alt.og2' => 'Floor plan 2nd upper floor',
'floors.alt.attic' => 'Floor plan attic',
'rent.eyebrow' => 'Rental terms',
'rent.aria' => 'Rental terms',
'rent.h2' => 'Transparent pricing',
'rent.cold' => 'Cold rent',
'rent.warm' => 'Total rent (incl. utilities)',
'rent.deposit' => 'Deposit',
'rent.per_month' => 'per month',
'rent.warm_includes' => 'incl. €300 utilities',
'rent.deposit_months' => '2 net cold rents',
'rent.note.available' => 'Availability',
'rent.note.available_val'=> 'Immediately · indefinite term',
'rent.note.costs' => 'Utilities',
'rent.note.costs_val' => 'Advance payment €300/month, annual settlement',
'rent.note.energy' => 'Energy certificate',
'rent.note.energy_val' => 'Handed over at start of tenancy · natural gas heating',
'rent.note.pets' => 'Pets',
'rent.note.pets_val' => 'On request',
'loc.eyebrow' => 'Location',
'loc.h2' => 'Central and quiet at the same time',
'loc.shopping' => 'Shopping & supplies',
'loc.shopping_desc' => 'Supermarkets, doctors, pharmacies and schools within walking distance',
'loc.transport' => 'Public transport',
'loc.transport_desc' => 'Central bus stop about 200 m away — direct connections in the region',
'loc.center' => 'Schleusingen town centre',
'loc.center_desc' => 'Weekly market and town centre just about 500 m away',
'loc.address' => 'Exact address',
'loc.address_val' => 'Schleusinger Bahnhofstraße 10<br />98553 Schleusingen, Thuringia',
'loc.map_title' => 'Location Bahnhofstraße 10, Schleusingen',
'contact.eyebrow' => 'Contact',
'contact.aria' => 'Contact form',
'contact.h2' => 'Interested?',
'contact.h2_em' => 'Get in touch.',
'contact.intro' => 'We look forward to your inquiry and will get back to you within 24 hours. Viewing appointments can be arranged by agreement. Please include a few date suggestions in your request.',
'contact.success' => 'Thank you for your inquiry!',
'contact.success_sub' => 'We have received your message and will get back to you within 24 hours.',
'contact.fname' => 'First name',
'contact.lname' => 'Last name',
'contact.email' => 'Email',
'contact.phone' => 'Phone',
'contact.interest' => 'Subject',
'contact.interest_visit' => 'Request a viewing',
'contact.interest_info' => 'General information',
'contact.interest_apply' => 'Submit rental application',
'contact.message' => 'Message',
'contact.submit' => 'Send inquiry',
'contact.hp_label' => 'Website',
'contact.direct' => 'Or write to us directly:',
'footer.address' => 'Bahnhofstraße 10 · Schleusingen',
'footer.imprint' => 'Imprint',
'footer.privacy' => 'Privacy policy',
'lightbox.aria' => 'Image view',
'lightbox.close' => 'Close image view',
'legal.back' => '← Back to the property',
'legal.german_only' => 'This page is available in German only.',
'legal.imprint_eyebrow' => 'Mandatory information',
'legal.imprint_h1' => 'Imprint',
'legal.privacy_eyebrow' => 'Privacy',
'legal.privacy_h1' => 'Privacy policy',
'locale.switcher.aria' => 'Choose language',
'locale.de' => 'Deutsch',
'locale.en' => 'English',
'locale.uk' => 'Українська',
'locale.ru' => 'Русский',
'locale.current' => 'Current language: {lang}',
'form.error.csrf' => 'Security check failed. Please try again.',
'form.error.fname_required' => 'Please enter your first name.',
'form.error.lname_required' => 'Please enter your last name.',
'form.error.email_invalid' => 'Please enter a valid email address.',
'form.error.message_required' => 'Please enter a message.',
'form.error.header_injection' => 'Invalid characters in the input fields.',
'form.error.too_fast' => 'The form was submitted too quickly. Please try again.',
'form.error.rate_limit' => 'Please wait a moment before sending another request.',
'form.error.send_failed' => 'Unfortunately the email could not be sent. Please try again later or write to us directly at mki@kies-media.de.',
'form.interest.visit' => 'Request a viewing',
'form.interest.info' => 'General information',
'form.interest.apply' => 'Submit rental application',
'nav.back_home' => '← Back to home',
'address.street' => 'Bahnhofstraße 10',
'address.city' => 'Schleusingen',
'structured.listing_name' => 'Family house for rent in Schleusingen',
'structured.listing_description' => 'Spacious family house for long-term rent: 227 m² of living space, 6 rooms, 3 floors with rooftop terrace. Cold rent €1,300.',
'structured.price_description' => 'Cold rent per month',
];

205
app/Locales/ru.php Normal file
View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
/**
* Russian (RU) translations.
* @see de.php for key reference.
*/
return [
'site.name' => 'Дом Шлайзинген',
'site.title' => 'Аренда частного дома в Шлайзингене | 227 м², 6 комнат | 1 300 €',
'site.description' => 'Частный дом для долгосрочной аренды в Шлайзингене: 227 м² жилой площади, 6 комнат, 3 этажа с террасой на крыше. Аренда 1 300 €. Bahnhofstraße 10, 98553 Schleusingen. Свободен сразу.',
'site.og_locale' => 'ru_RU',
'site.canonical_base' => 'https://haus-schleusingen.de/ru',
'nav.skip' => 'Перейти к содержимому',
'nav.main' => 'Главная навигация',
'nav.toggle' => 'Открыть навигацию',
'nav.gallery' => 'Галерея',
'nav.layout' => 'Планировка',
'nav.rent' => 'Аренда',
'nav.location' => 'Расположение',
'nav.cta' => 'Связаться',
'hero.tag' => 'Долгосрочная аренда · Свободен сразу',
'hero.h1.line1' => 'Просторный',
'hero.h1.line2' => 'Частный дом',
'hero.h1.line3' => 'в Шлайзингене',
'hero.address' => 'Schleusinger Bahnhofstraße 10',
'hero.area' => '227 м² жилой площади',
'hero.rooms' => '6 комнат',
'hero.floors' => '3 этажа + терраса на крыше',
'hero.discover' => 'Узнать больше',
'facts.area' => 'м² жилой площади',
'facts.rooms' => 'комнат',
'facts.floors' => 'этажей',
'facts.rent' => '€ аренда',
'intro.eyebrow' => 'Объект',
'intro.h2' => 'Жильё с характером и большим пространством',
'intro.p1' => 'Сдаётся в аренду полноценный частный дом в тихом районе Шлайзингена. Дом сочетает историческое очарование с современным комфортом на трёх просторных этажах.',
'intro.p2' => 'Гараж на два автомобиля, просторная терраса на крыше 35,8 м², полностью оборудованная кухня, полноценная ванная комната, а также кладовые и подсобные помещения делают этот дом исключительным арендным объектом.',
'intro.stats.area' => 'Полезная площадь',
'intro.stats.terrace' => 'Терраса на крыше',
'intro.stats.garage' => 'Гараж',
'intro.badge' => 'Гостиная · 42,6 м²',
'gallery.aria' => 'Фотогалерея',
'gallery.eyebrow' => 'Фотогалерея',
'gallery.h2' => 'Взгляд на дом',
'gallery.zoom' => ' открыть полный вид',
'gallery.exterior' => 'Внешний вид',
'gallery.living' => 'Гостиная',
'gallery.living_area' => 'Гостиная · 42,6 м²',
'gallery.kitchen' => 'Кухня · 18,4 м²',
'gallery.bedroom' => 'Спальня · 18 м²',
'gallery.bath' => 'Ванная комната · 9,8 м²',
'gallery.kid1' => 'Детская 1 · 21,7 м²',
'gallery.kid2' => 'Детская 2 · 15,7 м²',
'gallery.kid_detail' => 'Деталь детской',
'gallery.guest' => 'Гостевая комната · 11,5 м²',
'gallery.area1' => 'Жилая зона',
'gallery.area2' => 'Деталь жилой зоны',
'gallery.area3' => 'Вид на дом',
'gallery.alt.living' => 'Гостиная площадью 42,6 м²',
'gallery.alt.kitchen' => 'Кухня 18,4 м²',
'gallery.alt.bedroom' => 'Спальня 18 м²',
'gallery.alt.bath' => 'Ванная комната 9,8 м²',
'gallery.alt.kid1' => 'Детская комната 1 — 21,7 м²',
'gallery.alt.kid2' => 'Детская комната 2 — 15,7 м²',
'gallery.alt.kid_detail' => 'Детальный вид детской комнаты',
'gallery.alt.guest' => 'Гостевая комната 11,5 м²',
'gallery.alt.bath2' => 'Вторая ванная комната в доме',
'gallery.alt.bath3' => 'Третья ванная комната в доме',
'gallery.alt.exterior' => 'Внешний вид частного дома',
'floors.eyebrow' => 'Планировка',
'floors.h2' => 'Просторно на каждом этаже',
'floors.eg.title' => 'Первый этаж',
'floors.og1.title' => 'Второй этаж',
'floors.og2.title' => 'Третий этаж',
'floors.attic.title' => 'Чердак',
'floors.eg.area' => '99,5 м²',
'floors.og1.area' => '120,4 м²',
'floors.og2.area' => '68 м²',
'floors.attic.area' => '94 м² полезной площади',
'floors.room.hall' => 'Прихожая',
'floors.room.wc' => 'Туалет',
'floors.room.garage' => 'Гараж / комната для вечеринок',
'floors.room.storage1' => 'Кладовая 1',
'floors.room.storage2' => 'Кладовая 2',
'floors.room.heating' => 'Котельная',
'floors.room.living' => 'Гостиная',
'floors.room.guest' => 'Гостевая комната',
'floors.room.bath' => 'Ванная комната',
'floors.room.kitchen' => 'Кухня',
'floors.room.bedroom' => 'Спальня',
'floors.room.kid1' => 'Детская комната 1',
'floors.room.kid2' => 'Детская комната 2',
'floors.room.play' => 'Игровая комната',
'floors.room.dressing' => 'Гардеробная',
'floors.room.terrace' => 'Терраса на крыше',
'floors.room.terrace_note' => '(25% от 35,8 м²)',
'floors.room.attic_low' => 'Чердак нижний (неотапливаемый)',
'floors.room.attic_mid' => 'Чердак средний (неотапливаемый)',
'floors.room.attic_high' => 'Чердак верхний (неотапливаемый)',
'floors.alt.eg' => 'План первого этажа',
'floors.alt.og1' => 'План второго этажа',
'floors.alt.og2' => 'План третьего этажа',
'floors.alt.attic' => 'План чердака',
'rent.eyebrow' => 'Условия аренды',
'rent.aria' => 'Условия аренды',
'rent.h2' => 'Прозрачное ценообразование',
'rent.cold' => 'Базовая аренда',
'rent.warm' => 'Общая аренда',
'rent.deposit' => 'Залог',
'rent.per_month' => 'в месяц',
'rent.warm_includes' => 'вкл. 300 € доп. расходов',
'rent.deposit_months' => '2 базовые аренды',
'rent.note.available' => 'Доступность',
'rent.note.available_val'=> 'Сразу · бессрочно',
'rent.note.costs' => 'Доп. расходы',
'rent.note.costs_val' => 'Аванс 300 €/месяц, годовой расчёт',
'rent.note.energy' => 'Энергетический паспорт',
'rent.note.energy_val' => 'Передаётся при начале аренды · газовое отопление',
'rent.note.pets' => 'Домашние животные',
'rent.note.pets_val' => 'По запросу',
'loc.eyebrow' => 'Расположение',
'loc.h2' => 'Центрально и тихо одновременно',
'loc.shopping' => 'Магазины и услуги',
'loc.shopping_desc' => 'Супермаркеты, врачи, аптеки и школы в пешей доступности',
'loc.transport' => 'Общественный транспорт',
'loc.transport_desc' => 'Центральная автобусная остановка примерно в 200 м — прямые сообщения в регионе',
'loc.center' => 'Центр Шлайзингена',
'loc.center_desc' => 'Еженедельный рынок и центр города всего в 500 м',
'loc.address' => 'Точный адрес',
'loc.address_val' => 'Schleusinger Bahnhofstraße 10<br />98553 Schleusingen, Тюрингия',
'loc.map_title' => 'Расположение Bahnhofstraße 10, Шлайзинген',
'contact.eyebrow' => 'Контакт',
'contact.aria' => 'Контактная форма',
'contact.h2' => 'Интересно?',
'contact.h2_em' => 'Напишите нам.',
'contact.intro' => 'Мы рады вашему запросу и ответим в течение 24 часов. Осмотры возможны по договорённости. Пожалуйста, укажите в запросе несколько возможных дат.',
'contact.success' => 'Спасибо за ваш запрос!',
'contact.success_sub' => 'Мы получили ваше сообщение и свяжемся с вами в течение 24 часов.',
'contact.fname' => 'Имя',
'contact.lname' => 'Фамилия',
'contact.email' => 'Электронная почта',
'contact.phone' => 'Телефон',
'contact.interest' => 'Тема',
'contact.interest_visit' => 'Запрос на осмотр',
'contact.interest_info' => 'Общая информация',
'contact.interest_apply' => 'Подать заявку на аренду',
'contact.message' => 'Сообщение',
'contact.submit' => 'Отправить запрос',
'contact.hp_label' => 'Веб-сайт',
'contact.direct' => 'Или напишите нам напрямую:',
'footer.address' => 'Bahnhofstraße 10 · Шлайзинген',
'footer.imprint' => 'Импрессум',
'footer.privacy' => 'Политика конфиденциальности',
'lightbox.aria' => 'Просмотр изображения',
'lightbox.close' => 'Закрыть просмотр изображения',
'legal.back' => '← Назад к объекту',
'legal.german_only' => 'Эта страница доступна только на немецком.',
'legal.imprint_eyebrow' => 'Обязательная информация',
'legal.imprint_h1' => 'Импрессум',
'legal.privacy_eyebrow' => 'Конфиденциальность',
'legal.privacy_h1' => 'Политика конфиденциальности',
'locale.switcher.aria' => 'Выбрать язык',
'locale.de' => 'Deutsch',
'locale.en' => 'English',
'locale.uk' => 'Українська',
'locale.ru' => 'Русский',
'locale.current' => 'Текущий язык: {lang}',
'form.error.csrf' => 'Ошибка проверки безопасности. Пожалуйста, попробуйте ещё раз.',
'form.error.fname_required' => 'Пожалуйста, укажите ваше имя.',
'form.error.lname_required' => 'Пожалуйста, укажите вашу фамилию.',
'form.error.email_invalid' => 'Пожалуйста, введите корректный адрес электронной почты.',
'form.error.message_required' => 'Пожалуйста, введите сообщение.',
'form.error.header_injection' => 'Недопустимые символы в полях ввода.',
'form.error.too_fast' => 'Форма была отправлена слишком быстро. Пожалуйста, попробуйте ещё раз.',
'form.error.rate_limit' => 'Пожалуйста, подождите немного перед следующим запросом.',
'form.error.send_failed' => 'К сожалению, не удалось отправить письмо. Попробуйте позже или напишите нам напрямую на mki@kies-media.de.',
'form.interest.visit' => 'Запрос на осмотр',
'form.interest.info' => 'Общая информация',
'form.interest.apply' => 'Подать заявку на аренду',
'nav.back_home' => '← На главную',
'address.street' => 'Bahnhofstraße 10',
'address.city' => 'Шлайзинген',
'structured.listing_name' => 'Дом в аренду в Шлайзингене',
'structured.listing_description' => 'Просторный дом для долгосрочной аренды: 227 м² жилой площади, 6 комнат, 3 этажа с террасой на крыше. Базовая аренда 1 300 €.',
'structured.price_description' => 'Базовая аренда в месяц',
];

205
app/Locales/uk.php Normal file
View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
/**
* Ukrainian (UK) translations.
* @see de.php for key reference.
*/
return [
'site.name' => 'Будинок Шлайзінген',
'site.title' => 'Оренда приватного будинку в Шлайзінгені | 227 м², 6 кімнат | 1 300 €',
'site.description' => 'Приватний будинок для довгострокової оренди в Шлайзінгені: 227 м² житлової площі, 6 кімнат, 3 поверхи з терасою на даху. Оренда 1 300 €. Bahnhofstraße 10, 98553 Schleusingen. Доступний негайно.',
'site.og_locale' => 'uk_UA',
'site.canonical_base' => 'https://haus-schleusingen.de/uk',
'nav.skip' => 'Перейти до вмісту',
'nav.main' => 'Головна навігація',
'nav.toggle' => 'Відкрити навігацію',
'nav.gallery' => 'Галерея',
'nav.layout' => 'Планування',
'nav.rent' => 'Оренда',
'nav.location' => 'Розташування',
'nav.cta' => 'Зв\'язатися',
'hero.tag' => 'Довгострокова оренда · Доступно негайно',
'hero.h1.line1' => 'Просторий',
'hero.h1.line2' => 'Приватний будинок',
'hero.h1.line3' => 'у Шлайзінгені',
'hero.address' => 'Schleusinger Bahnhofstraße 10',
'hero.area' => '227 м² житлової площі',
'hero.rooms' => '6 кімнат',
'hero.floors' => '3 поверхи + тераса на даху',
'hero.discover' => 'Дізнатися більше',
'facts.area' => 'м² житлової площі',
'facts.rooms' => 'кімнат',
'facts.floors' => 'поверхи',
'facts.rent' => '€ оренда',
'intro.eyebrow' => 'Об\'єкт',
'intro.h2' => 'Житло з характером і великим простором',
'intro.p1' => 'Здається в оренду повноцінний приватний будинок у тихому районі Шлайзінгена. Будинок поєднує історичний шарм із сучасним комфортом на трьох просторих поверхах.',
'intro.p2' => 'Гараж на два автомобілі, простора тераса на даху 35,8 м², повністю обладнана кухня, повноцінна ванна кімната, а також комори та підсобні приміщення роблять цей будинок винятковим орендним об\'єктом.',
'intro.stats.area' => 'Корисна площа',
'intro.stats.terrace' => 'Тераса на даху',
'intro.stats.garage' => 'Гараж',
'intro.badge' => 'Вітальня · 42,6 м²',
'gallery.aria' => 'Фотогалерея',
'gallery.eyebrow' => 'Фотогалерея',
'gallery.h2' => 'Погляд на будинок',
'gallery.zoom' => ' відкрити повний перегляд',
'gallery.exterior' => 'Зовнішній вигляд',
'gallery.living' => 'Вітальня',
'gallery.living_area' => 'Вітальня · 42,6 м²',
'gallery.kitchen' => 'Кухня · 18,4 м²',
'gallery.bedroom' => 'Спальня · 18 м²',
'gallery.bath' => 'Ванна кімната · 9,8 м²',
'gallery.kid1' => 'Дитяча кімната 1 · 21,7 м²',
'gallery.kid2' => 'Дитяча кімната 2 · 15,7 м²',
'gallery.kid_detail' => 'Деталь дитячої кімнати',
'gallery.guest' => 'Гостьова кімната · 11,5 м²',
'gallery.area1' => 'Житлова зона',
'gallery.area2' => 'Деталь житлової зони',
'gallery.area3' => 'Вид на будинок',
'gallery.alt.living' => 'Вітальня з площею 42,6 м²',
'gallery.alt.kitchen' => 'Кухня 18,4 м²',
'gallery.alt.bedroom' => 'Спальня 18 м²',
'gallery.alt.bath' => 'Ванна кімната 9,8 м²',
'gallery.alt.kid1' => 'Дитяча кімната 1 — 21,7 м²',
'gallery.alt.kid2' => 'Дитяча кімната 2 — 15,7 м²',
'gallery.alt.kid_detail' => 'Детальний вигляд дитячої кімнати',
'gallery.alt.guest' => 'Гостьова кімната 11,5 м²',
'gallery.alt.bath2' => 'Друга ванна кімната в будинку',
'gallery.alt.bath3' => 'Третя ванна кімната в будинку',
'gallery.alt.exterior' => 'Зовнішній вигляд приватного будинку',
'floors.eyebrow' => 'Планування',
'floors.h2' => 'Просторий на кожному поверсі',
'floors.eg.title' => 'Перший поверх',
'floors.og1.title' => 'Другий поверх',
'floors.og2.title' => 'Третій поверх',
'floors.attic.title' => 'Горище',
'floors.eg.area' => '99,5 м²',
'floors.og1.area' => '120,4 м²',
'floors.og2.area' => '68 м²',
'floors.attic.area' => '94 м² корисної площі',
'floors.room.hall' => 'Коридор',
'floors.room.wc' => 'Туалет',
'floors.room.garage' => 'Гараж / кімната для вечірок',
'floors.room.storage1' => 'Комора 1',
'floors.room.storage2' => 'Комора 2',
'floors.room.heating' => 'Котельня',
'floors.room.living' => 'Вітальня',
'floors.room.guest' => 'Гостьова кімната',
'floors.room.bath' => 'Ванна кімната',
'floors.room.kitchen' => 'Кухня',
'floors.room.bedroom' => 'Спальня',
'floors.room.kid1' => 'Дитяча кімната 1',
'floors.room.kid2' => 'Дитяча кімната 2',
'floors.room.play' => 'Ігрова кімната',
'floors.room.dressing' => 'Гардеробна',
'floors.room.terrace' => 'Тераса на даху',
'floors.room.terrace_note' => '(25% від 35,8 м²)',
'floors.room.attic_low' => 'Горище нижнє (неопалюване)',
'floors.room.attic_mid' => 'Горище середнє (неопалюване)',
'floors.room.attic_high' => 'Горище верхнє (неопалюване)',
'floors.alt.eg' => 'План першого поверху',
'floors.alt.og1' => 'План другого поверху',
'floors.alt.og2' => 'План третього поверху',
'floors.alt.attic' => 'План горища',
'rent.eyebrow' => 'Умови оренди',
'rent.aria' => 'Умови оренди',
'rent.h2' => 'Прозоре ціноутворення',
'rent.cold' => 'Базова оренда',
'rent.warm' => 'Загальна оренда',
'rent.deposit' => 'Застава',
'rent.per_month' => 'на місяць',
'rent.warm_includes' => 'вкл. 300 € додаткових витрат',
'rent.deposit_months' => '2 базові оренди',
'rent.note.available' => 'Доступність',
'rent.note.available_val'=> 'Негайно · безстроково',
'rent.note.costs' => 'Додаткові витрати',
'rent.note.costs_val' => 'Аванс 300 €/місяць, щорічний розрахунок',
'rent.note.energy' => 'Енергетичний паспорт',
'rent.note.energy_val' => 'Передається на початку оренди · газове опалення',
'rent.note.pets' => 'Домашні тварини',
'rent.note.pets_val' => 'За запитом',
'loc.eyebrow' => 'Розташування',
'loc.h2' => 'Центрально й тихо одночасно',
'loc.shopping' => 'Магазини та послуги',
'loc.shopping_desc' => 'Супермаркети, лікарі, аптеки та школи в пішій доступності',
'loc.transport' => 'Громадський транспорт',
'loc.transport_desc' => 'Центральна автобусна зупинка приблизно за 200 м — прямі сполучення в регіоні',
'loc.center' => 'Центр Шлайзінгена',
'loc.center_desc' => 'Щотижневий ринок і центр міста лише за 500 м',
'loc.address' => 'Точна адреса',
'loc.address_val' => 'Schleusinger Bahnhofstraße 10<br />98553 Schleusingen, Тюрингія',
'loc.map_title' => 'Розташування Bahnhofstraße 10, Шлайзінген',
'contact.eyebrow' => 'Контакт',
'contact.aria' => 'Контактна форма',
'contact.h2' => 'Цікавить?',
'contact.h2_em' => 'Напишіть нам.',
'contact.intro' => 'Ми раді вашому запиту й дамо відповідь протягом 24 годин. Огляди можливі за домовленістю. Будь ласка, вкажіть у запиті кілька можливих дат.',
'contact.success' => 'Дякуємо за ваш запит!',
'contact.success_sub' => 'Ми отримали ваше повідомлення й зв\'яжемося з вами протягом 24 годин.',
'contact.fname' => 'Ім\'я',
'contact.lname' => 'Прізвище',
'contact.email' => 'Електронна пошта',
'contact.phone' => 'Телефон',
'contact.interest' => 'Тема',
'contact.interest_visit' => 'Запит на огляд',
'contact.interest_info' => 'Загальна інформація',
'contact.interest_apply' => 'Подати заявку на оренду',
'contact.message' => 'Повідомлення',
'contact.submit' => 'Надіслати запит',
'contact.hp_label' => 'Вебсайт',
'contact.direct' => 'Або напишіть нам напряму:',
'footer.address' => 'Bahnhofstraße 10 · Шлайзінген',
'footer.imprint' => 'Імпресум',
'footer.privacy' => 'Політика конфіденційності',
'lightbox.aria' => 'Перегляд зображення',
'lightbox.close' => 'Закрити перегляд зображення',
'legal.back' => '← Повернутися до об\'єкта',
'legal.german_only' => 'Ця сторінка доступна лише німецькою.',
'legal.imprint_eyebrow' => 'Обов\'язкова інформація',
'legal.imprint_h1' => 'Імпресум',
'legal.privacy_eyebrow' => 'Конфіденційність',
'legal.privacy_h1' => 'Політика конфіденційності',
'locale.switcher.aria' => 'Обрати мову',
'locale.de' => 'Deutsch',
'locale.en' => 'English',
'locale.uk' => 'Українська',
'locale.ru' => 'Русский',
'locale.current' => 'Поточна мова: {lang}',
'form.error.csrf' => 'Помилка перевірки безпеки. Будь ласка, спробуйте ще раз.',
'form.error.fname_required' => 'Будь ласка, введіть своє ім\'я.',
'form.error.lname_required' => 'Будь ласка, введіть своє прізвище.',
'form.error.email_invalid' => 'Будь ласка, введіть дійсну адресу електронної пошти.',
'form.error.message_required' => 'Будь ласка, введіть повідомлення.',
'form.error.header_injection' => 'Неприпустимі символи в полях введення.',
'form.error.too_fast' => 'Форму було надіслано занадто швидко. Будь ласка, спробуйте ще раз.',
'form.error.rate_limit' => 'Будь ласка, зачекайте деякий час перед наступним запитом.',
'form.error.send_failed' => 'На жаль, лист не вдалося надіслати. Спробуйте пізніше або напишіть нам напряму на mki@kies-media.de.',
'form.interest.visit' => 'Запит на огляд',
'form.interest.info' => 'Загальна інформація',
'form.interest.apply' => 'Подати заявку на оренду',
'nav.back_home' => '← На головну',
'address.street' => 'Bahnhofstraße 10',
'address.city' => 'Шляйзінген',
'structured.listing_name' => 'Будинок для оренди в Шляйзінгені',
'structured.listing_description' => 'Просторий будинок для довгострокової оренди: 227 м² житлової площі, 6 кімнат, 3 поверхи з терасою на даху. Базова оренда 1 300 €.',
'structured.price_description' => 'Базова оренда на місяць',
];

View File

@@ -1,123 +1,116 @@
<nav id="navbar" class="scrolled">
<div class="nav-logo">Bahnhofstraße 10</div>
<ul class="nav-links">
<li><a href="/#galerie">Galerie</a></li>
<li><a href="/#grundriss">Grundriss</a></li>
<li><a href="/#miete">Miete</a></li>
<li><a href="/#lage">Lage</a></li>
</ul>
<a href="/#kontakt" class="nav-cta" style="text-decoration:none;">Jetzt anfragen</a>
</nav>
<?php
<main class="legal-page">
<div class="section-eyebrow">Datenschutz</div>
<h1>Datenschutzerklärung</h1>
declare(strict_types=1);
<h2>1. Verantwortliche Stelle</h2>
<address>
Martin Kiesewetter<br />
Am Schaftalsgraben 4<br />
98529 Suhl<br />
Telefon: 0176 45853923<br />
E-Mail: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a>
</address>
<p>
Diese Datenschutzerklärung informiert Sie über die Art, den Umfang und den Zweck der Erhebung und Verwendung personenbezogener Daten auf dieser Website.
</p>
/**
* Datenschutz — page body only (nav/footer/lightbox live in layouts/main.php).
* Legal body stays in German by design (DSGVO compliance).
*
* @var string $locale
* @var callable(string,array,string=):string $t
*/
?>
<main class="legal-page">
<div class="section-eyebrow"><?= htmlspecialchars($t('legal.privacy_eyebrow'), ENT_QUOTES) ?></div>
<h1><?= htmlspecialchars($t('legal.privacy_h1'), ENT_QUOTES) ?></h1>
<hr class="legal-divider" />
<h2>1. Verantwortliche Stelle</h2>
<address>
Martin Kiesewetter<br />
Am Schaftalsgraben 4<br />
98529 Suhl<br />
Telefon: 0176 45853923<br />
E-Mail: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a>
</address>
<p>
Diese Datenschutzerklärung informiert Sie über die Art, den Umfang und den Zweck der Erhebung und Verwendung personenbezogener Daten auf dieser Website.
</p>
<h2>2. Erhebung und Speicherung personenbezogener Daten</h2>
<hr class="legal-divider" />
<h3>a) Beim Besuch der Website</h3>
<p>
Beim Aufrufen dieser Website werden durch den Hosting-Anbieter automatisch Informationen allgemeiner Natur erfasst. Diese Informationen (Server-Logfiles) beinhalten etwa die Art des Webbrowsers, das verwendete Betriebssystem, den Domainnamen Ihres Internet-Service-Providers, Ihre IP-Adresse und Ähnliches. Sie werden insbesondere zu einem sicheren und reibungslosen Betrieb der Website benötigt.
</p>
<ul>
<li>IP-Adresse</li>
<li>Datum und Uhrzeit der Anfrage</li>
<li>Zeitzonenunterschied zur Greenwich Mean Time (GMT)</li>
<li>Inhalt der Anforderung (konkrete Seite)</li>
<li>Zugriffsstatus/HTTP-Statuscode</li>
<li>Jeweils übertragene Datenmenge</li>
<li>Website, von der die Anforderung kommt (Referrer-URL)</li>
<li>Verwendeter Browser</li>
<li>Verwendetes Betriebssystem</li>
</ul>
<p>
Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden nicht mit anderen Datenquellen zusammengeführt und nach 30 Tagen automatisch gelöscht.
</p>
<h2>2. Erhebung und Speicherung personenbezogener Daten</h2>
<h3>b) Kontakt per E-Mail</h3>
<p>
Auf dieser Website wird die Kontaktaufnahme über einen E-Mail-Link (mailto:) angeboten. Wenn Sie uns per E-Mail kontaktieren, werden Ihre Angaben (E-Mail-Adresse, ggf. Name und Nachricht) zwecks Bearbeitung Ihrer Anfrage gespeichert. Die Daten werden ausschließlich zur Beantwortung Ihrer Anfrage verwendet und nach Abschluss der Kommunikation gelöscht.
</p>
<p>
Rechtsgrundlage ist Art. 6 Abs. 1 lit. a DSGVO (Ihre Einwilligung) oder Art. 6 Abs. 1 lit. b DSGVO (zur Erfüllung eines Vertrags bzw. vorvertraglicher Maßnahmen).
</p>
<h3>a) Beim Besuch der Website</h3>
<p>
Beim Aufrufen dieser Website werden durch den Hosting-Anbieter automatisch Informationen allgemeiner Natur erfasst. Diese Informationen (Server-Logfiles) beinhalten etwa die Art des Webbrowsers, das verwendete Betriebssystem, den Domainnamen Ihres Internet-Service-Providers, Ihre IP-Adresse und Ähnliches. Sie werden insbesondere zu einem sicheren und reibungslosen Betrieb der Website benötigt.
</p>
<ul>
<li>IP-Adresse</li>
<li>Datum und Uhrzeit der Anfrage</li>
<li>Zeitzonenunterschied zur Greenwich Mean Time (GMT)</li>
<li>Inhalt der Anforderung (konkrete Seite)</li>
<li>Zugriffsstatus/HTTP-Statuscode</li>
<li>Jeweils übertragene Datenmenge</li>
<li>Website, von der die Anforderung kommt (Referrer-URL)</li>
<li>Verwendeter Browser</li>
<li>Verwendetes Betriebssystem</li>
</ul>
<p>
Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden nicht mit anderen Datenquellen zusammengeführt und nach 30 Tagen automatisch gelöscht.
</p>
<hr class="legal-divider" />
<h3>b) Kontakt per E-Mail</h3>
<p>
Auf dieser Website wird die Kontaktaufnahme über einen E-Mail-Link (mailto:) angeboten. Wenn Sie uns per E-Mail kontaktieren, werden Ihre Angaben (E-Mail-Adresse, ggf. Name und Nachricht) zwecks Bearbeitung Ihrer Anfrage gespeichert. Die Daten werden ausschließlich zur Beantwortung Ihrer Anfrage verwendet und nach Abschluss der Kommunikation gelöscht.
</p>
<p>
Rechtsgrundlage ist Art. 6 Abs. 1 lit. a DSGVO (Ihre Einwilligung) oder Art. 6 Abs. 1 lit. b DSGVO (zur Erfüllung eines Vertrags bzw. vorvertraglicher Maßnahmen).
</p>
<h2>3. Cookies</h2>
<p>
Diese Website verwendet <strong>keine Cookies</strong>. Es werden keine Tracking-Cookies, Werbe-Cookies oder sonstige Cookies gesetzt.
</p>
<hr class="legal-divider" />
<h2>4. Tracking &amp; Analyse</h2>
<p>
Diese Website setzt <strong>keine Tracking- oder Analyse-Tools</strong> ein. Es werden keine Besucherstatistiken erstellt, kein Google Analytics, kein Facebook Pixel und keine ähnlichen Dienste verwendet.
</p>
<h2>3. Cookies</h2>
<p>
Diese Website verwendet <strong>keine Cookies</strong>. Es werden keine Tracking-Cookies, Werbe-Cookies oder sonstige Cookies gesetzt.
</p>
<h2>5. Social-Media-Plugins</h2>
<p>
Diese Website verwendet <strong>keine Social-Media-Plugins</strong> (Facebook, Twitter, Instagram etc.).
</p>
<h2>4. Tracking &amp; Analyse</h2>
<p>
Diese Website setzt <strong>keine Tracking- oder Analyse-Tools</strong> ein. Es werden keine Besucherstatistiken erstellt, kein Google Analytics, kein Facebook Pixel und keine ähnlichen Dienste verwendet.
</p>
<hr class="legal-divider" />
<h2>5. Social-Media-Plugins</h2>
<p>
Diese Website verwendet <strong>keine Social-Media-Plugins</strong> (Facebook, Twitter, Instagram etc.).
</p>
<h2>6. Google Maps</h2>
<p>
Auf dieser Website wird ein Google Maps-Embed (Kartenansicht) eingebunden. Beim Laden der Karte werden Daten an Google übertragen, darunter möglicherweise Ihre IP-Adresse. Google Maps wird ausschließlich genutzt, um Ihnen die Lage des Mietobjekts anzuzeigen.
</p>
<p>
Weitere Informationen zum Datenschutz bei Google finden Sie unter: <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">https://policies.google.com/privacy</a>
</p>
<p>
Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigte Interessen an der Darstellung des Objektstandorts).
</p>
<hr class="legal-divider" />
<hr class="legal-divider" />
<h2>6. Google Maps</h2>
<p>
Auf dieser Website wird ein Google Maps-Embed (Kartenansicht) eingebunden. Beim Laden der Karte werden Daten an Google übertragen, darunter möglicherweise Ihre IP-Adresse. Google Maps wird ausschließlich genutzt, um Ihnen die Lage des Mietobjekts anzuzeigen.
</p>
<p>
Weitere Informationen zum Datenschutz bei Google finden Sie unter: <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">https://policies.google.com/privacy</a>
</p>
<p>
Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigte Interessen an der Darstellung des Objektstandorts).
</p>
<h2>7. SSL-Verschlüsselung</h2>
<p>
Diese Seite nutzt aus Sicherheitsgründen eine SSL-Verschlüsselung. Eine verschlüsselte Verbindung erkennen Sie daran, dass die Adresszeile des Browsers von „http://" auf „https://" wechselt und an dem Schloss-Symbol in Ihrer Browserzeile.
</p>
<hr class="legal-divider" />
<hr class="legal-divider" />
<h2>7. SSL-Verschlüsselung</h2>
<p>
Diese Seite nutzt aus Sicherheitsgründen eine SSL-Verschlüsselung. Eine verschlüsselte Verbindung erkennen Sie daran, dass die Adresszeile des Browsers von „http://" auf „https://" wechselt und an dem Schloss-Symbol in Ihrer Browserzeile.
</p>
<h2>8. Ihre Rechte</h2>
<p>Sie haben gegenüber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:</p>
<ul>
<li><strong>Recht auf Auskunft</strong> (Art. 15 DSGVO)</li>
<li><strong>Recht auf Berichtigung</strong> (Art. 16 DSGVO)</li>
<li><strong>Recht auf Löschung</strong> (Art. 17 DSGVO)</li>
<li><strong>Recht auf Einschränkung der Verarbeitung</strong> (Art. 18 DSGVO)</li>
<li><strong>Recht auf Datenübertragbarkeit</strong> (Art. 20 DSGVO)</li>
<li><strong>Widerspruchsrecht</strong> (Art. 21 DSGVO)</li>
<li><strong>Recht auf Widerruf der Einwilligung</strong> (Art. 7 Abs. 3 DSGVO)</li>
<li><strong>Beschwerderecht bei einer Aufsichtsbehörde</strong> (Art. 77 DSGVO)</li>
</ul>
<p>
Zur Ausübung Ihrer Rechte wenden Sie sich bitte an: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a>
</p>
<hr class="legal-divider" />
<a href="/" class="legal-back"> Zurück zum Objekt</a>
</main>
<h2>8. Ihre Rechte</h2>
<p>Sie haben gegenüber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:</p>
<ul>
<li><strong>Recht auf Auskunft</strong> (Art. 15 DSGVO)</li>
<li><strong>Recht auf Berichtigung</strong> (Art. 16 DSGVO)</li>
<li><strong>Recht auf Löschung</strong> (Art. 17 DSGVO)</li>
<li><strong>Recht auf Einschränkung der Verarbeitung</strong> (Art. 18 DSGVO)</li>
<li><strong>Recht auf Datenübertragbarkeit</strong> (Art. 20 DSGVO)</li>
<li><strong>Widerspruchsrecht</strong> (Art. 21 DSGVO)</li>
<li><strong>Recht auf Widerruf der Einwilligung</strong> (Art. 7 Abs. 3 DSGVO)</li>
<li><strong>Beschwerderecht bei einer Aufsichtsbehörde</strong> (Art. 77 DSGVO)</li>
</ul>
<p>
Zur Ausübung Ihrer Rechte wenden Sie sich bitte an: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a>
</p>
<footer>
<div class="footer-logo">Bahnhofstraße 10 · Schleusingen</div>
<div class="footer-links">
<a href="/impressum">Impressum</a>
<a href="/datenschutz">Datenschutz</a>
</div>
</footer>
<a href="/" class="legal-back"><?= htmlspecialchars($t('legal.back'), ENT_QUOTES) ?></a>
</main>

View File

@@ -1,505 +1,331 @@
<a href="#main-content" class="skip-link">Zum Inhalt springen</a>
<nav id="navbar" role="navigation" aria-label="Hauptnavigation">
<div class="nav-logo">Bahnhofstraße 10</div>
<button class="nav-hamburger" aria-label="Navigation öffnen" aria-expanded="false">
<span></span>
</button>
<ul class="nav-links">
<li><a href="#galerie">Galerie</a></li>
<li><a href="#grundriss">Grundriss</a></li>
<li><a href="#miete">Miete</a></li>
<li><a href="#lage">Lage</a></li>
</ul>
<button
class="nav-cta"
onclick="document.getElementById('kontakt').scrollIntoView({behavior:'smooth'})"
>
Jetzt anfragen
</button>
</nav>
<div class="nav-mobile-overlay" aria-hidden="true"></div>
<section class="hero" id="hero">
<div
class="hero-bg"
id="heroBg"
style="background-image: url(/bilder/Außenansicht-2.webp)"
></div>
<div class="hero-overlay"></div>
<div class="hero-content" id="heroContent">
<div class="hero-tag">Zur Langzeitmiete · Ab sofort verfügbar</div>
<h1>
Großzügiges
<br />
<em>Einfamilienhaus</em>
<br />
in Schleusingen
</h1>
<div class="hero-meta">
<span><strong>Schleusinger Bahnhofstraße 10</strong></span>
<span>227 Wohnfläche</span>
<span>6 Zimmer</span>
<span>3 Etagen + Dachterrasse</span>
</div>
</div>
<div class="hero-scroll">
<span>Entdecken</span>
<div class="scroll-line"></div>
</div>
</section>
<main id="main-content">
<div class="facts-strip">
<div class="fact">
<div class="fact-val">227</div>
<div class="fact-label"> Wohnfläche</div>
</div>
<div class="fact">
<div class="fact-val">6</div>
<div class="fact-label">Zimmer</div>
</div>
<div class="fact">
<div class="fact-val">3</div>
<div class="fact-label">Etagen</div>
</div>
<div class="fact">
<div class="fact-val">1.300</div>
<div class="fact-label"> Kaltmiete</div>
</div>
</div>
<section class="intro" id="intro">
<div class="intro-text" data-animate>
<div class="section-eyebrow">Das Objekt</div>
<h2>Wohnen mit Charakter und viel Raum</h2>
<p>
Vermietet wird ein vollständiges Einfamilienhaus in ruhiger Lage von Schleusingen. Das
Haus verbindet historischen Charme mit modernem Wohnkomfort auf drei großzügigen Etagen.
</p>
<p>
Garage für zwei Fahrzeuge, großzügige Dachterrasse mit 35,8 , vollausgestattete Küche,
Vollbad sowie Abstell- und Nutzräume machen das Haus zu einem außergewöhnlichen
Mietobjekt.
</p>
<div class="intro-stats">
<div>
<div class="istat-val">154,9 </div>
<div class="istat-label">Nutzfläche</div>
</div>
<div>
<div class="istat-val">35,8 </div>
<div class="istat-label">Dachterrasse</div>
</div>
<div>
<div class="istat-val">2 Stpl.</div>
<div class="istat-label">Garage</div>
</div>
</div>
</div>
<div class="intro-img" data-animate>
<picture>
<source srcset="/bilder/wohnzimmer2.webp" type="image/webp">
<img src="/bilder/wohnzimmer2.png" alt="Wohnzimmer" loading="lazy" />
</picture>
<div class="intro-img-badge">Wohnzimmer · 42,6 </div>
</div>
</section>
<section id="galerie" class="gallery-section" aria-label="Fotogalerie">
<div class="gallery-header">
<div>
<div class="section-eyebrow">Fotogalerie</div>
<h2>Einblicke ins Haus</h2>
</div>
</div>
<div class="masonry-grid">
<div class="grid-sizer"></div>
<div class="grid-item" data-img="/bilder/Außenansicht-2.webp" role="button" tabindex="0" aria-label="Außenansicht Großansicht öffnen">
<picture>
<source srcset="/bilder/Außenansicht-2-small.webp" type="image/webp">
<img src="/bilder/Außenansicht-2-small.png" alt="Außenansicht des Einfamilienhauses" loading="lazy" />
</picture>
<span class="grid-item-label">Außenansicht</span>
</div>
<div class="grid-item" data-img="/bilder/wohnzimmer2.webp" role="button" tabindex="0" aria-label="Wohnzimmer Großansicht öffnen">
<picture>
<source srcset="/bilder/wohnzimmer2-small.webp" type="image/webp">
<img src="/bilder/wohnzimmer2-small.png" alt="Wohnzimmer mit 42,6 m² Wohnfläche" loading="lazy" />
</picture>
<span class="grid-item-label">Wohnzimmer · 42,6 </span>
</div>
<div class="grid-item" data-img="/bilder/Küche 1.webp" role="button" tabindex="0" aria-label="Küche Großansicht öffnen">
<picture>
<source srcset="/bilder/Küche 1-small.webp" type="image/webp">
<img src="/bilder/Küche 1.jpg" alt="Küche mit 18,4 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Küche · 18,4 </span>
</div>
<div class="grid-item" data-img="/bilder/schlafzimmer.webp" role="button" tabindex="0" aria-label="Schlafzimmer Großansicht öffnen">
<picture>
<source srcset="/bilder/schlafzimmer-small.webp" type="image/webp">
<img src="/bilder/schlafzimmer-small.png" alt="Schlafzimmer mit 18 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Schlafzimmer · 18 </span>
</div>
<div class="grid-item" data-img="/bilder/Bad.webp" role="button" tabindex="0" aria-label="Badezimmer Großansicht öffnen">
<picture>
<source srcset="/bilder/Bad-small.webp" type="image/webp">
<img src="/bilder/Bad.jpg" alt="Badezimmer mit 9,8 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Badezimmer · 9,8 </span>
</div>
<div class="grid-item" data-img="/bilder/Kinderzimmer.webp" role="button" tabindex="0" aria-label="Kinderzimmer 1 Großansicht öffnen">
<picture>
<source srcset="/bilder/Kinderzimmer-small.webp" type="image/webp">
<img src="/bilder/Kinderzimmer-small.png" alt="Kinderzimmer 1 mit 21,7 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Kinderzimmer 1 · 21,7 </span>
</div>
<div class="grid-item" data-img="/bilder/Kinderzimmer 2.webp" role="button" tabindex="0" aria-label="Kinderzimmer 2 Großansicht öffnen">
<picture>
<source srcset="/bilder/Kinderzimmer 2-small.webp" type="image/webp">
<img src="/bilder/Kinderzimmer 2-small.png" alt="Kinderzimmer 2 mit 15,7 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Kinderzimmer 2 · 15,7 </span>
</div>
<div class="grid-item" data-img="/bilder/kinderzimmer 2 2.webp" role="button" tabindex="0" aria-label="Kinderzimmer Detail Großansicht öffnen">
<picture>
<source srcset="/bilder/kinderzimmer 2 2-small.webp" type="image/webp">
<img src="/bilder/kinderzimmer 2 2-small.png" alt="Detailansicht Kinderzimmer" loading="lazy" />
</picture>
<span class="grid-item-label">Kinderzimmer Detail</span>
</div>
<div class="grid-item" data-img="/bilder/Kinderzimmer 3.webp" role="button" tabindex="0" aria-label="Gästezimmer Großansicht öffnen">
<picture>
<source srcset="/bilder/Kinderzimmer 3-small.webp" type="image/webp">
<img src="/bilder/Kinderzimmer 3-small.png" alt="Gästezimmer mit 11,5 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Gästezimmer · 11,5 </span>
</div>
<div class="grid-item" data-img="/bilder/Bad-2.webp" role="button" tabindex="0" aria-label="Zweites Bad Großansicht öffnen">
<picture>
<source srcset="/bilder/Bad-2-small.webp" type="image/webp">
<img src="/bilder/Bad-2-small.jpg" alt="Zweites Badezimmer im Haus" loading="lazy" />
</picture>
<span class="grid-item-label">Wohnbereich</span>
</div>
<div class="grid-item" data-img="/bilder/Bad-3.webp" role="button" tabindex="0" aria-label="Drittes Bad Großansicht öffnen">
<picture>
<source srcset="/bilder/Bad-3-small.webp" type="image/webp">
<img src="/bilder/Bad-3-small.jpg" alt="Drittes Badezimmer im Haus" loading="lazy" />
</picture>
<span class="grid-item-label">Wohnbereich Detail</span>
</div>
<div class="grid-item" data-img="/bilder/Bad-4.webp" role="button" tabindex="0" aria-label="Wohnbereich Detail Großansicht öffnen">
<picture>
<source srcset="/bilder/Bad-4-small.webp" type="image/webp">
<img src="/bilder/Bad-4-small.jpg" alt="Wohnbereich Detail 3" loading="lazy" />
</picture>
<span class="grid-item-label">Hausansicht</span>
</div>
</div>
</section>
<section class="floors-section" id="grundriss">
<div class="section-eyebrow">Raumaufteilung</div>
<h2>Großzügig auf allen Etagen</h2>
<div class="floor-accordion">
<div class="floor-item">
<div class="floor-header" role="button" tabindex="0" aria-expanded="false" aria-controls="floor-body-0" id="floor-title-0">
<span class="floor-title">Erdgeschoss</span>
<div class="floor-size">
<span>99,5 </span>
<div class="floor-icon">+</div>
</div>
</div>
<div class="floor-body" id="floor-body-0" role="region" aria-labelledby="floor-title-0">
<div class="floor-rooms-grid">
<div class="room-chip">Flur<span class="room-chip-area">20,1 </span></div>
<div class="room-chip">WC<span class="room-chip-area">0,8 </span></div>
<div class="room-chip">Garage / Partykeller<span class="room-chip-area">42,6 </span></div>
<div class="room-chip">Abstellraum 1<span class="room-chip-area">9,9 </span></div>
<div class="room-chip">Abstellraum 2<span class="room-chip-area">7,8 </span></div>
<div class="room-chip">Heizungskeller<span class="room-chip-area">18,3 </span></div>
</div>
<div class="floor-plan floor-plan-multi">
<picture>
<source srcset="/bilder/grundrisse/EG-small.webp" type="image/webp">
<img src="/bilder/grundrisse/EG-small.jpg" alt="Grundriss Erdgeschoss" loading="lazy" data-img="/bilder/grundrisse/EG.webp" />
</picture>
<picture>
<source srcset="/bilder/grundrisse/EG 3D-small.webp" type="image/webp">
<img src="/bilder/grundrisse/EG 3D-small.jpg" alt="Grundriss Erdgeschoss" loading="lazy" data-img="/bilder/grundrisse/EG 3D.webp" />
</picture>
</div>
</div>
</div>
<div class="floor-item">
<div class="floor-header" role="button" tabindex="0" aria-expanded="false" aria-controls="floor-body-1" id="floor-title-1">
<span class="floor-title">1. Obergeschoss</span>
<div class="floor-size">
<span>120,4 </span>
<div class="floor-icon">+</div>
</div>
</div>
<div class="floor-body" id="floor-body-1" role="region" aria-labelledby="floor-title-1">
<div class="floor-rooms-grid">
<div class="room-chip">Flur<span class="room-chip-area">20,1 </span></div>
<div class="room-chip">Wohnzimmer<span class="room-chip-area">42,6 </span></div>
<div class="room-chip">Gästezimmer<span class="room-chip-area">11,5 </span></div>
<div class="room-chip">Badezimmer<span class="room-chip-area">9,8 </span></div>
<div class="room-chip">Küche<span class="room-chip-area">18,4 </span></div>
<div class="room-chip">Schlafzimmer<span class="room-chip-area">18,0 </span></div>
</div>
<div class="floor-plan floor-plan-multi">
<picture>
<source srcset="/bilder/grundrisse/OG 1 2-small.webp" type="image/webp">
<img src="/bilder/grundrisse/OG 1 2-small.jpg" alt="Grundriss 1. Obergeschoss" loading="lazy" data-img="/bilder/grundrisse/OG 1 2.webp" />
</picture>
<picture>
<source srcset="/bilder/grundrisse/OG 1 3D-small.webp" type="image/webp">
<img src="/bilder/grundrisse/OG 1 3D-small.jpg" alt="Grundriss 1. Obergeschoss" loading="lazy" data-img="/bilder/grundrisse/OG 1 3D.webp" />
</picture>
</div>
</div>
</div>
<div class="floor-item">
<div class="floor-header" role="button" tabindex="0" aria-expanded="false" aria-controls="floor-body-2" id="floor-title-2">
<span class="floor-title">2. Obergeschoss</span>
<div class="floor-size">
<span>68 </span>
<div class="floor-icon">+</div>
</div>
</div>
<div class="floor-body" id="floor-body-2" role="region" aria-labelledby="floor-title-2">
<div class="floor-rooms-grid">
<div class="room-chip">Flur<span class="room-chip-area">13,9 </span></div>
<div class="room-chip">Kinderzimmer 1<span class="room-chip-area">21,7 </span></div>
<div class="room-chip">Kinderzimmer 2<span class="room-chip-area">15,7 </span></div>
<div class="room-chip">Spielzimmer<span class="room-chip-area">6,3 </span></div>
<div class="room-chip">Ankleidezimmer<span class="room-chip-area">1,4 </span></div>
<div class="room-chip">Dachterrasse<span class="room-chip-area">9,0 </span> <small>(25% von 35,8 )</small></div>
</div>
<div class="floor-plan floor-plan-multi">
<picture>
<source srcset="/bilder/grundrisse/OG 2 grundriss-small.webp" type="image/webp">
<img src="/bilder/grundrisse/OG 2 grundriss-small.jpg" alt="Grundriss 2. Obergeschoss" loading="lazy" data-img="/bilder/grundrisse/OG 2 grundriss.webp" />
</picture>
<picture>
<source srcset="/bilder/grundrisse/OG 2 3D-small.webp" type="image/webp">
<img src="/bilder/grundrisse/OG 2 3D-small.jpg" alt="Grundriss 2. Obergeschoss" loading="lazy" data-img="/bilder/grundrisse/OG 2 3D.webp" />
</picture>
</div>
</div>
</div>
<div class="floor-item">
<div class="floor-header" role="button" tabindex="0" aria-expanded="false" aria-controls="floor-body-3" id="floor-title-3">
<span class="floor-title">Dachboden</span>
<div class="floor-size">
<span>94 Nutzfläche</span>
<div class="floor-icon">+</div>
</div>
</div>
<div class="floor-body" id="floor-body-3" role="region" aria-labelledby="floor-title-3">
<div class="floor-rooms-grid">
<div class="room-chip">Dachboden unten (ungeheizt)<span class="room-chip-area">52 </span></div>
<div class="room-chip">Dachboden Mitte (ungeheizt)<span class="room-chip-area">31 </span></div>
<div class="room-chip">Dachboden oben (ungeheizt)<span class="room-chip-area">11 </span></div>
</div>
<div class="floor-plan floor-plan-multi">
<picture>
<source srcset="/bilder/grundrisse/Dachboden unten 2-small.webp" type="image/webp">
<img src="/bilder/grundrisse/Dachboden unten 2-small.jpg" alt="Grundriss Dachboden" loading="lazy" data-img="/bilder/grundrisse/Dachboden unten 2.webp" />
</picture>
<picture>
<source srcset="/bilder/grundrisse/Dachboden unten-small.webp" type="image/webp">
<img src="/bilder/grundrisse/Dachboden unten-small.jpg" alt="Grundriss Dachboden" loading="lazy" data-img="/bilder/grundrisse/Dachboden unten.webp" />
</picture>
</div>
</div>
</div>
</div>
</section>
<section class="pricing-section" id="miete" aria-label="Mietkonditionen">
<div class="pricing-inner">
<div class="section-eyebrow">Mietkonditionen</div>
<h2>Transparente Preisgestaltung</h2>
<div class="price-cards">
<div class="price-card">
<div class="pc-label">Kaltmiete</div>
<div class="pc-val">1.300 </div>
<div class="pc-sub">pro Monat</div>
</div>
<div class="price-card highlight">
<div class="pc-label">Gesamtmiete warm</div>
<div class="pc-val">1.600 </div>
<div class="pc-sub">inkl. 300 Nebenkosten</div>
</div>
<div class="price-card">
<div class="pc-label">Kaution</div>
<div class="pc-val">2.600 </div>
<div class="pc-sub">2 Nettokaltmieten</div>
</div>
</div>
<div class="price-note">
<div class="pn-item">
<strong>Verfügbarkeit</strong>
Ab sofort · unbefristete Laufzeit
</div>
<div class="pn-item">
<strong>Nebenkosten</strong>
Vorauszahlung 300 /Monat, jährliche Abrechnung
</div>
<div class="pn-item">
<strong>Energieausweis</strong>
Wird bei Mietbeginn übergeben · Erdgasheizung
</div>
<div class="pn-item">
<strong>Haustiere</strong>
Auf Anfrage
</div>
</div>
</div>
</section>
<section class="lage-section" id="lage">
<div class="section-eyebrow">Standort</div>
<h2>Zentral und ruhig zugleich</h2>
<div class="lage-grid">
<div class="lage-item">
<div class="lage-icon">🛒</div>
<div>
<div class="lage-title">Einkaufen & Versorgung</div>
<div class="lage-desc">Supermärkte, Ärzte, Apotheken und Schulen sind fußläufig erreichbar</div>
</div>
</div>
<div class="lage-item">
<div class="lage-icon">🚌</div>
<div>
<div class="lage-title">Öffentlicher Nahverkehr</div>
<div class="lage-desc">Zentrale Bushaltestelle ca. 200 m entfernt direkte Verbindungen in die Region</div>
</div>
</div>
<div class="lage-item">
<div class="lage-icon">🏛</div>
<div>
<div class="lage-title">Innenstadt Schleusingen</div>
<div class="lage-desc">Wochenmarkt und Stadtmitte nur ca. 500 m entfernt</div>
</div>
</div>
<div class="lage-item">
<div class="lage-icon">📍</div>
<div>
<div class="lage-title">Genaue Adresse</div>
<div class="lage-desc">Schleusinger Bahnhofstraße 10<br />98553 Schleusingen, Thüringen</div>
</div>
</div>
</div>
<div class="lage-map-wrapper">
<iframe
src="https://maps.google.com/maps?q=50.5090045,10.7473859&t=&z=16&ie=UTF8&iwloc=&output=embed"
width="100%" height="450" style="border: 0" allowfullscreen="" loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
title="Standort Bahnhofstraße 10, Schleusingen"
></iframe>
</div>
</section>
<section class="contact-section" id="kontakt" aria-label="Kontaktformular">
<div class="contact-inner">
<div class="section-eyebrow">Kontakt</div>
<h2>Interesse?<br /><em>Schreiben Sie uns.</em></h2>
<p>
Wir freuen uns über Ihre Anfrage und melden uns innerhalb von 24 Stunden.
Besichtigungstermine sind nach Absprache möglich. Bitte geben Sie bei Ihrer Anfrage ein
paar Terminvorschläge an.
</p>
<div class="contact-form">
<?php if ($formSuccess): ?>
<div id="form-result" class="form-success" style="display: block">
<p>Vielen Dank für Ihre Anfrage!</p>
<br />
<small>Wir haben Ihre Nachricht erhalten und melden uns innerhalb von 24 Stunden bei Ihnen.</small>
</div>
<?php else: ?>
<?php if (!empty($formErrors)): ?>
<div id="form-errors" class="form-errors">
<ul>
<?php foreach ($formErrors as $error): ?>
<li><?= $escapeContactValue($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form id="contactForm" method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>" />
<div class="form-row">
<div class="form-field">
<label for="fname">Vorname</label>
<input type="text" id="fname" name="fname" placeholder="Max" required value="<?= $escapeContactValue($formData['fname']) ?>" />
</div>
<div class="form-field">
<label for="lname">Nachname</label>
<input type="text" id="lname" name="lname" placeholder="Mustermann" required value="<?= $escapeContactValue($formData['lname']) ?>" />
</div>
</div>
<div class="form-row">
<div class="form-field">
<label for="email">E-Mail</label>
<input type="email" id="email" name="email" placeholder="max@beispiel.de" required value="<?= $escapeContactValue($formData['email']) ?>" />
</div>
<div class="form-field">
<label for="phone">Telefon</label>
<input type="tel" id="phone" name="phone" placeholder="+49 ..." value="<?= $escapeContactValue($formData['phone']) ?>" />
</div>
</div>
<div class="form-row">
<div class="form-field full">
<label for="interest">Anliegen</label>
<select id="interest" name="interest">
<?php
$interestOptions = ['Besichtigung anfragen', 'Allgemeine Informationen', 'Mietbewerbung einreichen'];
foreach ($interestOptions as $opt):
$selected = ($formData['interest'] === $opt) ? ' selected' : '';
declare(strict_types=1);
/**
* Home page — page body only (nav/footer/lightbox live in layouts/main.php).
*
* @var string $locale
* @var array<string,mixed> $formData
* @var list<string> $formErrors Translation keys, resolved via t()
* @var bool $formSuccess
* @var array<string,string> $interestKeys ['visit' => 'form.interest.visit', ...]
* @var callable(string):string $escapeContactValue
* @var callable(string,array,string=):string $t
*/
$gridItems = [
['img' => 'bilder/Außenansicht-2.png', 'key' => 'gallery.exterior', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-2'],
['img' => 'bilder/Wohnzimmer-1.png', 'key' => 'gallery.living', 'alt' => 'gallery.alt.living', 'class' => 'span-2 row-1'],
['img' => 'bilder/Küche-1.png', 'key' => 'gallery.kitchen', 'alt' => 'gallery.alt.kitchen', 'class' => ''],
['img' => 'bilder/Schlafzimmer-1.png','key' => 'gallery.bedroom', 'alt' => 'gallery.alt.bedroom', 'class' => ''],
['img' => 'bilder/Badezimmer-1.png', 'key' => 'gallery.bath', 'alt' => 'gallery.alt.bath', 'class' => ''],
['img' => 'bilder/Kinderzimmer-1-1.png', 'key' => 'gallery.kid1', 'alt' => 'gallery.alt.kid1', 'class' => ''],
['img' => 'bilder/Kinderzimmer-2.png','key' => 'gallery.kid2', 'alt' => 'gallery.alt.kid2', 'class' => ''],
['img' => 'bilder/Kinderzimmer-Detail.png','key' => 'gallery.kid_detail', 'alt' => 'gallery.alt.kid_detail', 'class' => 'span-2 row-1'],
['img' => 'bilder/Gästezimmer.png', 'key' => 'gallery.guest', 'alt' => 'gallery.alt.guest', 'class' => ''],
['img' => 'bilder/Wohnbereich.png', 'key' => 'gallery.area1', 'alt' => 'gallery.alt.living', 'class' => ''],
['img' => 'bilder/Wohnbereich-Detail.png', 'key' => 'gallery.area2', 'alt' => 'gallery.alt.living', 'class' => ''],
['img' => 'bilder/Außenansicht-1.png','key' => 'gallery.area3', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-1'],
];
?>
<option<?= $selected ?>><?= $escapeContactValue($opt) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-row">
<div class="form-field full">
<label for="message">Nachricht</label>
<textarea id="message" name="message" rows="4" placeholder="Ihre Nachricht ..." required><?= $escapeContactValue($formData['message']) ?></textarea>
</div>
</div>
<div class="hp-field" aria-hidden="true">
<label for="website">Website</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off" />
</div>
<input type="hidden" name="form_time" value="<?= time() ?>" />
<button type="submit" class="btn-submit">Anfrage absenden</button>
</form>
<?php endif; ?>
</div>
<div class="contact-details">
<p>Oder schreiben Sie uns direkt: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a></p>
</div>
</div>
</section>
<header class="hero" id="hero">
<img src="/bilder/hero-bg.jpg" alt="" class="hero-bg" id="heroBg" loading="eager" decoding="async" fetchpriority="high">
<div class="hero-content" id="heroContent">
<span class="hero-tag"><?= htmlspecialchars($t('hero.tag'), ENT_QUOTES) ?></span>
<h1 class="hero-h1">
<span class="hero-line"><?= htmlspecialchars($t('hero.h1.line1'), ENT_QUOTES) ?></span>
<span class="hero-line accent"><?= htmlspecialchars($t('hero.h1.line2'), ENT_QUOTES) ?></span>
<span class="hero-line"><?= htmlspecialchars($t('hero.h1.line3'), ENT_QUOTES) ?></span>
</h1>
<ul class="hero-meta" aria-label="<?= htmlspecialchars($t('hero.address'), ENT_QUOTES) ?>">
<li class="hero-meta-item"><?= htmlspecialchars($t('hero.address'), ENT_QUOTES) ?></li>
<li class="hero-meta-item"><?= htmlspecialchars($t('hero.area'), ENT_QUOTES) ?></li>
<li class="hero-meta-item"><?= htmlspecialchars($t('hero.rooms'), ENT_QUOTES) ?></li>
<li class="hero-meta-item"><?= htmlspecialchars($t('hero.floors'), ENT_QUOTES) ?></li>
</ul>
<a class="hero-cta" href="#galerie"><?= htmlspecialchars($t('hero.discover'), ENT_QUOTES) ?> ↓</a>
</div>
</header>
</main>
<footer role="contentinfo">
<div class="footer-logo">Bahnhofstraße 10 · Schleusingen</div>
<div class="footer-links">
<a href="/impressum">Impressum</a>
<a href="/datenschutz">Datenschutz</a>
</div>
</footer>
<section class="facts-strip" aria-label="<?= htmlspecialchars($t('intro.stats.area'), ENT_QUOTES) ?>">
<div class="fact"><span class="fact-value">227</span><span class="fact-unit"><?= htmlspecialchars($t('facts.area'), ENT_QUOTES) ?></span></div>
<div class="fact"><span class="fact-value">6</span><span class="fact-unit"><?= htmlspecialchars($t('facts.rooms'), ENT_QUOTES) ?></span></div>
<div class="fact"><span class="fact-value">3</span><span class="fact-unit"><?= htmlspecialchars($t('facts.floors'), ENT_QUOTES) ?></span></div>
<div class="fact"><span class="fact-value">1.300</span><span class="fact-unit"><?= htmlspecialchars($t('facts.rent'), ENT_QUOTES) ?></span></div>
</section>
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Bildansicht">
<button class="lightbox-close" id="lightboxClose" aria-label="Bildansicht schließen">&times;</button>
<img src="" id="lightboxImg" alt="" />
<section class="intro" id="intro">
<div class="intro-grid">
<div class="intro-text">
<span class="section-eyebrow"><?= htmlspecialchars($t('intro.eyebrow'), ENT_QUOTES) ?></span>
<h2><?= htmlspecialchars($t('intro.h2'), ENT_QUOTES) ?></h2>
<p><?= htmlspecialchars($t('intro.p1'), ENT_QUOTES) ?></p>
<p><?= htmlspecialchars($t('intro.p2'), ENT_QUOTES) ?></p>
</div>
<aside class="intro-stats">
<div class="stat">
<span class="stat-label"><?= htmlspecialchars($t('intro.stats.area'), ENT_QUOTES) ?></span>
<span class="stat-value">196,5 m²</span>
</div>
<div class="stat">
<span class="stat-label"><?= htmlspecialchars($t('intro.stats.terrace'), ENT_QUOTES) ?></span>
<span class="stat-value">35,8 m²</span>
</div>
<div class="stat">
<span class="stat-label"><?= htmlspecialchars($t('intro.stats.garage'), ENT_QUOTES) ?></span>
<span class="stat-value">2 PKW</span>
</div>
<span class="intro-badge"><?= htmlspecialchars($t('intro.badge'), ENT_QUOTES) ?></span>
</aside>
</div>
</section>
<section class="gallery-section" id="galerie" aria-label="<?= htmlspecialchars($t('gallery.aria'), ENT_QUOTES) ?>">
<div class="section-head">
<span class="section-eyebrow"><?= htmlspecialchars($t('gallery.eyebrow'), ENT_QUOTES) ?></span>
<h2><?= htmlspecialchars($t('gallery.h2'), ENT_QUOTES) ?></h2>
</div>
<div class="gallery-grid">
<?php foreach ($gridItems as $item): ?>
<button type="button" class="grid-item <?= htmlspecialchars($item['class'], ENT_QUOTES) ?>"
data-img="<?= htmlspecialchars($item['img'], ENT_QUOTES) ?>"
aria-label="<?= htmlspecialchars($t($item['key']) . $t('gallery.zoom'), ENT_QUOTES) ?>">
<img src="/<?= htmlspecialchars($item['img'], ENT_QUOTES) ?>" alt="<?= htmlspecialchars($t($item['alt']), ENT_QUOTES) ?>" loading="lazy" decoding="async">
<span class="grid-caption"><?= htmlspecialchars($t($item['key']), ENT_QUOTES) ?></span>
</button>
<?php endforeach; ?>
</div>
</section>
<section class="floors-section" id="grundriss" aria-label="<?= htmlspecialchars($t('floors.eyebrow'), ENT_QUOTES) ?>">
<div class="section-head">
<span class="section-eyebrow"><?= htmlspecialchars($t('floors.eyebrow'), ENT_QUOTES) ?></span>
<h2><?= htmlspecialchars($t('floors.h2'), ENT_QUOTES) ?></h2>
</div>
<?php
$floors = [
['id' => 'eg', 'titleKey' => 'floors.eg.title', 'areaKey' => 'floors.eg.area', 'altKey' => 'floors.alt.eg',
'rooms' => [
['key' => 'floors.room.hall', 'size' => '21,0'],
['key' => 'floors.room.wc', 'size' => '1,7'],
['key' => 'floors.room.garage', 'size' => '23,4'],
['key' => 'floors.room.storage1', 'size' => '5,5'],
['key' => 'floors.room.heating', 'size' => '11,2'],
['key' => 'floors.room.storage2', 'size' => '6,4'],
]],
['id' => 'og1', 'titleKey' => 'floors.og1.title', 'areaKey' => 'floors.og1.area', 'altKey' => 'floors.alt.og1',
'rooms' => [
['key' => 'floors.room.living', 'size' => '42,6'],
['key' => 'floors.room.kitchen', 'size' => '18,4'],
['key' => 'floors.room.guest', 'size' => '11,5'],
['key' => 'floors.room.bath', 'size' => '9,8'],
['key' => 'floors.room.storage1','size' => '3,4'],
['key' => 'floors.room.heating', 'size' => '8,0'],
]],
['id' => 'og2', 'titleKey' => 'floors.og2.title', 'areaKey' => 'floors.og2.area', 'altKey' => 'floors.alt.og2',
'rooms' => [
['key' => 'floors.room.bedroom', 'size' => '18,0'],
['key' => 'floors.room.kid1', 'size' => '21,7'],
['key' => 'floors.room.kid2', 'size' => '15,7'],
['key' => 'floors.room.bath', 'size' => '6,4'],
]],
['id' => 'attic','titleKey' => 'floors.attic.title', 'areaKey' => 'floors.attic.area', 'altKey' => 'floors.alt.attic',
'rooms' => [
['key' => 'floors.room.attic_low', 'size' => ''],
['key' => 'floors.room.attic_mid', 'size' => ''],
['key' => 'floors.room.attic_high', 'size' => ''],
]],
];
?>
<div class="floors-accordion">
<?php foreach ($floors as $floor): ?>
<details class="floor-item" id="floor-<?= htmlspecialchars($floor['id'], ENT_QUOTES) ?>">
<summary class="floor-header">
<span class="floor-title"><?= htmlspecialchars($t($floor['titleKey']), ENT_QUOTES) ?></span>
<span class="floor-area"><?= htmlspecialchars($t($floor['areaKey']), ENT_QUOTES) ?></span>
</summary>
<div class="floor-body">
<img src="/bilder/grundriss-<?= htmlspecialchars($floor['id'], ENT_QUOTES) ?>.png"
alt="<?= htmlspecialchars($t($floor['altKey']), ENT_QUOTES) ?>"
loading="lazy" decoding="async"
class="floor-plan-img">
<ul class="room-list">
<?php foreach ($floor['rooms'] as $room): ?>
<li>
<span class="room-name"><?= htmlspecialchars($t($room['key']), ENT_QUOTES) ?></span>
<?php if ($room['size'] !== ''): ?>
<span class="room-size"><?= htmlspecialchars($room['size'], ENT_QUOTES) ?> m²</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
</details>
<?php endforeach; ?>
</div>
</section>
<section class="pricing-section" id="miete" aria-label="<?= htmlspecialchars($t('rent.aria'), ENT_QUOTES) ?>">
<div class="section-head">
<span class="section-eyebrow"><?= htmlspecialchars($t('rent.eyebrow'), ENT_QUOTES) ?></span>
<h2><?= htmlspecialchars($t('rent.h2'), ENT_QUOTES) ?></h2>
</div>
<div class="pricing-grid">
<div class="price-card">
<span class="price-label"><?= htmlspecialchars($t('rent.cold'), ENT_QUOTES) ?></span>
<span class="price-value">1.300 €</span>
<span class="price-unit"><?= htmlspecialchars($t('rent.per_month'), ENT_QUOTES) ?></span>
</div>
<div class="price-card">
<span class="price-label"><?= htmlspecialchars($t('rent.warm'), ENT_QUOTES) ?></span>
<span class="price-value">1.600 €</span>
<span class="price-unit"><?= htmlspecialchars($t('rent.warm_includes'), ENT_QUOTES) ?></span>
</div>
<div class="price-card">
<span class="price-label"><?= htmlspecialchars($t('rent.deposit'), ENT_QUOTES) ?></span>
<span class="price-value">2.600 €</span>
<span class="price-unit"><?= htmlspecialchars($t('rent.deposit_months'), ENT_QUOTES) ?></span>
</div>
</div>
<dl class="rent-notes">
<dt><?= htmlspecialchars($t('rent.note.available'), ENT_QUOTES) ?></dt>
<dd><?= htmlspecialchars($t('rent.note.available_val'), ENT_QUOTES) ?></dd>
<dt><?= htmlspecialchars($t('rent.note.costs'), ENT_QUOTES) ?></dt>
<dd><?= htmlspecialchars($t('rent.note.costs_val'), ENT_QUOTES) ?></dd>
<dt><?= htmlspecialchars($t('rent.note.energy'), ENT_QUOTES) ?></dt>
<dd><?= htmlspecialchars($t('rent.note.energy_val'), ENT_QUOTES) ?></dd>
<dt><?= htmlspecialchars($t('rent.note.pets'), ENT_QUOTES) ?></dt>
<dd><?= htmlspecialchars($t('rent.note.pets_val'), ENT_QUOTES) ?></dd>
</dl>
</section>
<section class="lage-section" id="lage" aria-label="<?= htmlspecialchars($t('loc.eyebrow'), ENT_QUOTES) ?>">
<div class="section-head">
<span class="section-eyebrow"><?= htmlspecialchars($t('loc.eyebrow'), ENT_QUOTES) ?></span>
<h2><?= htmlspecialchars($t('loc.h2'), ENT_QUOTES) ?></h2>
</div>
<div class="lage-grid">
<ul class="lage-features">
<li>
<span class="lage-feature-title"><?= htmlspecialchars($t('loc.shopping'), ENT_QUOTES) ?></span>
<span class="lage-feature-desc"><?= htmlspecialchars($t('loc.shopping_desc'), ENT_QUOTES) ?></span>
</li>
<li>
<span class="lage-feature-title"><?= htmlspecialchars($t('loc.transport'), ENT_QUOTES) ?></span>
<span class="lage-feature-desc"><?= htmlspecialchars($t('loc.transport_desc'), ENT_QUOTES) ?></span>
</li>
<li>
<span class="lage-feature-title"><?= htmlspecialchars($t('loc.center'), ENT_QUOTES) ?></span>
<span class="lage-feature-desc"><?= htmlspecialchars($t('loc.center_desc'), ENT_QUOTES) ?></span>
</li>
</ul>
<div class="lage-map">
<iframe
title="<?= htmlspecialchars($t('loc.map_title'), ENT_QUOTES) ?>"
src="https://www.openstreetmap.org/export/embed.html?bbox=10.7535%2C50.5095%2C10.7705%2C50.5185&amp;layer=mapnik&amp;marker=50.5140%2C10.7620"
loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
<p class="lage-address">
<strong><?= htmlspecialchars($t('loc.address'), ENT_QUOTES) ?>:</strong><br>
<?= /* address HTML is XSS-safe — composed of trusted translations */ $t('loc.address_val') ?>
</p>
</div>
</div>
</section>
<section class="contact-section" id="kontakt" aria-label="<?= htmlspecialchars($t('contact.aria'), ENT_QUOTES) ?>">
<div class="section-head">
<span class="section-eyebrow"><?= htmlspecialchars($t('contact.eyebrow'), ENT_QUOTES) ?></span>
<h2><?= htmlspecialchars($t('contact.h2'), ENT_QUOTES) ?> <em><?= htmlspecialchars($t('contact.h2_em'), ENT_QUOTES) ?></em></h2>
<p class="contact-intro"><?= htmlspecialchars($t('contact.intro'), ENT_QUOTES) ?></p>
</div>
<div id="form-result" class="form-result" role="status" aria-live="polite">
<?php if ($formSuccess): ?>
<div class="form-success">
<strong><?= htmlspecialchars($t('contact.success'), ENT_QUOTES) ?></strong>
<p><?= htmlspecialchars($t('contact.success_sub'), ENT_QUOTES) ?></p>
</div>
<?php elseif (!empty($formErrors)): ?>
<div class="form-errors" role="alert">
<ul>
<?php foreach ($formErrors as $errKey): ?>
<li><?= htmlspecialchars($t($errKey), ENT_QUOTES) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</div>
<form class="contact-form" method="post" action="/#kontakt" novalidate>
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '', ENT_QUOTES) ?>">
<input type="hidden" name="form_time" value="<?= htmlspecialchars((string) time(), ENT_QUOTES) ?>">
<div class="form-hp" aria-hidden="true">
<label for="website-hp"><?= htmlspecialchars($t('contact.hp_label'), ENT_QUOTES) ?></label>
<input type="text" id="website-hp" name="website" tabindex="-1" autocomplete="off">
</div>
<div class="form-row">
<div class="form-field">
<label for="fname"><?= htmlspecialchars($t('contact.fname'), ENT_QUOTES) ?></label>
<input type="text" id="fname" name="fname" required maxlength="80" autocomplete="given-name"
value="<?= $escapeContactValue($formData['fname'] ?? '') ?>">
</div>
<div class="form-field">
<label for="lname"><?= htmlspecialchars($t('contact.lname'), ENT_QUOTES) ?></label>
<input type="text" id="lname" name="lname" required maxlength="80" autocomplete="family-name"
value="<?= $escapeContactValue($formData['lname'] ?? '') ?>">
</div>
</div>
<div class="form-row">
<div class="form-field">
<label for="email"><?= htmlspecialchars($t('contact.email'), ENT_QUOTES) ?></label>
<input type="email" id="email" name="email" required maxlength="120" autocomplete="email"
value="<?= $escapeContactValue($formData['email'] ?? '') ?>">
</div>
<div class="form-field">
<label for="phone"><?= htmlspecialchars($t('contact.phone'), ENT_QUOTES) ?></label>
<input type="tel" id="phone" name="phone" maxlength="40" autocomplete="tel"
value="<?= $escapeContactValue($formData['phone'] ?? '') ?>">
</div>
</div>
<div class="form-field">
<label for="interest"><?= htmlspecialchars($t('contact.interest'), ENT_QUOTES) ?></label>
<select id="interest" name="interest" required>
<?php
$currentInterest = $formData['interest'] ?? 'visit';
$interestLabels = [
'visit' => 'contact.interest_visit',
'info' => 'contact.interest_info',
'apply' => 'contact.interest_apply',
];
foreach ($interestLabels as $value => $labelKey): ?>
<option value="<?= htmlspecialchars($value, ENT_QUOTES) ?>"
<?= $currentInterest === $value ? 'selected' : '' ?>>
<?= htmlspecialchars($t($labelKey), ENT_QUOTES) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-field">
<label for="message"><?= htmlspecialchars($t('contact.message'), ENT_QUOTES) ?></label>
<textarea id="message" name="message" required rows="6" maxlength="2000"
placeholder="<?= htmlspecialchars($t('contact.message'), ENT_QUOTES) ?>"><?= $escapeContactValue($formData['message'] ?? '') ?></textarea>
</div>
<button type="submit" class="form-submit"><?= htmlspecialchars($t('contact.submit'), ENT_QUOTES) ?></button>
<p class="contact-direct"><?= htmlspecialchars($t('contact.direct'), ENT_QUOTES) ?>
<a href="mailto:mki@kies-media.de">mki@kies-media.de</a>
</p>
</form>
</section>

View File

@@ -1,85 +1,78 @@
<nav id="navbar" class="scrolled">
<div class="nav-logo">Bahnhofstraße 10</div>
<ul class="nav-links">
<li><a href="/#galerie">Galerie</a></li>
<li><a href="/#grundriss">Grundriss</a></li>
<li><a href="/#miete">Miete</a></li>
<li><a href="/#lage">Lage</a></li>
</ul>
<a href="/#kontakt" class="nav-cta" style="text-decoration:none;">Jetzt anfragen</a>
</nav>
<?php
<main class="legal-page">
<div class="section-eyebrow">Pflichtangaben</div>
<h1>Impressum</h1>
declare(strict_types=1);
<h2>Angaben gemäß § 5 TMG</h2>
<address>
Martin Kiesewetter<br />
Am Schaftalsgraben 4<br />
98529 Suhl<br />
Deutschland
</address>
/**
* Impressum — page body only (nav/footer/lightbox live in layouts/main.php).
* Legal body stays in German by design (§ 5 TMG requires German).
*
* @var string $locale
* @var callable(string,array,string=):string $t
*/
?>
<main class="legal-page">
<div class="section-eyebrow"><?= htmlspecialchars($t('legal.imprint_eyebrow'), ENT_QUOTES) ?></div>
<h1><?= htmlspecialchars($t('legal.imprint_h1'), ENT_QUOTES) ?></h1>
<h3>Kontakt</h3>
<ul>
<li>Telefon: 0176 45853923</li>
<li>E-Mail: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a></li>
</ul>
<h2>Angaben gemäß § 5 TMG</h2>
<address>
Martin Kiesewetter<br />
Am Schaftalsgraben 4<br />
98529 Suhl<br />
Deutschland
</address>
<hr class="legal-divider" />
<h3>Kontakt</h3>
<ul>
<li>Telefon: 0176 45853923</li>
<li>E-Mail: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a></li>
</ul>
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
<p>
Martin Kiesewetter<br />
Am Schaftalsgraben 4<br />
98529 Suhl
</p>
<hr class="legal-divider" />
<hr class="legal-divider" />
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
<p>
Martin Kiesewetter<br />
Am Schaftalsgraben 4<br />
98529 Suhl
</p>
<h2>Streitschlichtung</h2>
<p>
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
<a href="https://ec.europa.eu/consumers/odr/" target="_blank" rel="noopener">https://ec.europa.eu/consumers/odr/</a>
</p>
<p>
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.
</p>
<hr class="legal-divider" />
<hr class="legal-divider" />
<h2>Streitschlichtung</h2>
<p>
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
<a href="https://ec.europa.eu/consumers/odr/" target="_blank" rel="noopener">https://ec.europa.eu/consumers/odr/</a>
</p>
<p>
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.
</p>
<h2>Haftung für Inhalte</h2>
<p>
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
</p>
<p>
Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.
</p>
<hr class="legal-divider" />
<h2>Haftung für Links</h2>
<p>
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.
</p>
<p>
Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.
</p>
<h2>Haftung für Inhalte</h2>
<p>
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
</p>
<p>
Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.
</p>
<h2>Urheberrecht</h2>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet.
</p>
<p>
Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
</p>
<h2>Haftung für Links</h2>
<p>
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.
</p>
<p>
Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.
</p>
<a href="/" class="legal-back"> Zurück zum Objekt</a>
</main>
<h2>Urheberrecht</h2>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet.
</p>
<p>
Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
</p>
<footer>
<div class="footer-logo">Bahnhofstraße 10 · Schleusingen</div>
<div class="footer-links">
<a href="/impressum">Impressum</a>
<a href="/datenschutz">Datenschutz</a>
</div>
</footer>
<a href="/" class="legal-back"><?= htmlspecialchars($t('legal.back'), ENT_QUOTES) ?></a>
</main>

View File

@@ -1,21 +1,64 @@
<?php
declare(strict_types=1);
/**
* @var string $content Page body (rendered by the controller)
* @var string $locale Current locale code (e.g. 'de')
* @var callable $t Translation helper
* @var callable $locale_switcher Returns the locale switcher HTML
* @var string|null $pageTitle Optional page title override
* @var string|null $pageDescription Optional meta description override
* @var string|null $canonical Optional canonical URL override
* @var string|null $robots Optional robots meta override
* @var array<string,string>|null $openGraph OG meta map: ogTitle, ogDescription, ogImage, ogUrl
* @var string|null $structuredData JSON-LD blob
* @var string|null $extraCss Optional inline CSS
*/
use App\Core\I18n;
use App\Core\Locale;
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$ogLocale = Locale::toOgLocale($locale);
$title = $pageTitle ?? I18n::t('site.title', [], $locale);
$description = $pageDescription ?? I18n::t('site.description', [], $locale);
$canonicalBase = I18n::t('site.canonical_base', [], $locale);
$canonical = $canonical ?? $canonicalBase . ($currentPath === '/' ? '/' : $currentPath);
$siteName = I18n::t('site.name', [], $locale);
$ogTitle = $openGraph['ogTitle'] ?? $title;
$ogDescription = $openGraph['ogDescription'] ?? $description;
$ogImage = $openGraph['ogImage'] ?? 'https://haus-schleusingen.de/bilder/Aussenansicht-2.webp';
$ogUrl = $openGraph['ogUrl'] ?? $canonical;
$hreflangs = Locale::hreflangAlternates($currentPath === '/' ? '/' : $currentPath, $canonicalBase);
$homeUrl = $canonicalBase . '/';
$isHome = $currentPath === '/' || $currentPath === '';
$isImpr = $currentPath === '/impressum';
$isPriv = $currentPath === '/datenschutz';
$navItems = [
['href' => '/#galerie', 'label' => 'nav.gallery', 'active' => false],
['href' => '/#grundriss','label' => 'nav.layout', 'active' => false],
['href' => '/#miete', 'label' => 'nav.rent', 'active' => false],
['href' => '/#lage', 'label' => 'nav.location', 'active' => false],
];
?>
<!doctype html>
<html lang="de">
<html lang="<?= htmlspecialchars($locale, ENT_QUOTES) ?>">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<?php if (!isset($pageTitle)) $pageTitle = 'Einfamilienhaus mieten Schleusingen | 227 m², 6 Zimmer | 1.300 € Kaltmiete'; ?>
<title><?= htmlspecialchars($pageTitle) ?></title>
<?php if (isset($pageDescription)): ?>
<meta name="description" content="<?= htmlspecialchars($pageDescription) ?>" />
<?php else: ?>
<meta name="description" content="Einfamilienhaus zur Langzeitmiete in Schleusingen: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €. Bahnhofstraße 10, 98553 Schleusingen. Ab sofort verfügbar." />
<?php endif; ?>
<title><?= htmlspecialchars($title) ?></title>
<meta name="description" content="<?= htmlspecialchars($description) ?>" />
<?php if (isset($robots)): ?>
<meta name="robots" content="<?= htmlspecialchars($robots) ?>" />
<?php endif; ?>
<?php if (isset($canonical)): ?>
<link rel="canonical" href="<?= htmlspecialchars($canonical) ?>" />
<?php endif; ?>
<?php foreach ($hreflangs as $alt): ?>
<link rel="alternate" hreflang="<?= htmlspecialchars($alt['hreflang']) ?>" href="<?= htmlspecialchars($alt['href']) ?>" />
<?php endforeach; ?>
<link rel="icon" type="image/png" sizes="32x32" href="/bilder/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/bilder/favicon/favicon-16x16.png">
@@ -23,15 +66,13 @@
<link rel="apple-touch-icon" sizes="180x180" href="/bilder/favicon/apple-touch-icon.png">
<link rel="manifest" href="/bilder/favicon/site.webmanifest">
<?php if (isset($openGraph)): extract($openGraph); ?>
<meta property="og:type" content="website" />
<meta property="og:title" content="<?= htmlspecialchars($ogTitle ?? '') ?>" />
<meta property="og:description" content="<?= htmlspecialchars($ogDescription ?? '') ?>" />
<meta property="og:image" content="<?= htmlspecialchars($ogImage ?? '') ?>" />
<meta property="og:url" content="<?= htmlspecialchars($ogUrl ?? '') ?>" />
<meta property="og:locale" content="de_DE" />
<meta property="og:site_name" content="Haus Schleusingen" />
<?php endif; ?>
<meta property="og:type" content="<?= htmlspecialchars($ogType ?? 'website', ENT_QUOTES) ?>">
<meta property="og:url" content="<?= htmlspecialchars($ogUrl ?? $canonical ?? ($t('site.canonical_base') . '/'), ENT_QUOTES) ?>">
<meta property="og:title" content="<?= htmlspecialchars($ogTitle ?? $pageTitle ?? $t('site.title'), ENT_QUOTES) ?>">
<meta property="og:description" content="<?= htmlspecialchars($ogDescription ?? $pageDescription ?? $t('site.description'), ENT_QUOTES) ?>">
<meta property="og:locale" content="<?= htmlspecialchars($ogLocale ?? Locale::toOgLocale($locale), ENT_QUOTES) ?>">
<meta property="og:site_name" content="<?= htmlspecialchars($ogSiteName ?? $t('site.name'), ENT_QUOTES) ?>">
<meta property="og:image" content="<?= htmlspecialchars($ogImage ?? ($t('site.canonical_base') . '/bilder/hero-bg.jpg'), ENT_QUOTES) ?>">
<?php if (isset($structuredData)): ?>
<script type="application/ld+json"><?= $structuredData ?></script>
@@ -44,7 +85,58 @@
<?php endif; ?>
</head>
<body>
<?= $content ?>
<a class="skip-link" href="#main"><?= htmlspecialchars($t('nav.skip'), ENT_QUOTES) ?></a>
<nav id="navbar" class="scrolled" aria-label="<?= htmlspecialchars($t('nav.main'), ENT_QUOTES) ?>">
<div class="nav-logo">
<a href="<?= htmlspecialchars($homeUrl) ?>">
<span class="logo-icon" aria-hidden="true">🏠</span>
<span class="logo-text"><?= htmlspecialchars($siteName) ?></span>
</a>
</div>
<ul class="nav-links" role="list">
<?php foreach ($navItems as $item): ?>
<li>
<a href="<?= htmlspecialchars($item['href']) ?>"><?= htmlspecialchars($t($item['label']), ENT_QUOTES) ?></a>
</li>
<?php endforeach; ?>
</ul>
<?= $locale_switcher($currentPath) ?>
<a class="nav-cta" href="/#kontakt"><?= htmlspecialchars($t('nav.cta'), ENT_QUOTES) ?></a>
<button class="nav-hamburger" type="button" aria-expanded="false" aria-controls="navMobile" aria-label="<?= htmlspecialchars($t('nav.toggle'), ENT_QUOTES) ?>">
<span></span>
</button>
<div id="navMobile" class="nav-mobile-overlay" hidden></div>
</nav>
<main id="main" tabindex="-1">
<?= $content ?>
</main>
<footer>
<div class="footer-logo">
<span class="logo-icon" aria-hidden="true">🏠</span>
<span><?= htmlspecialchars($t('footer.address'), ENT_QUOTES) ?></span>
</div>
<div class="footer-links">
<a href="/impressum"<?= $isImpr ? ' aria-current="page"' : '' ?>><?= htmlspecialchars($t('footer.imprint'), ENT_QUOTES) ?></a>
<a href="/datenschutz"<?= $isPriv ? ' aria-current="page"' : '' ?>><?= htmlspecialchars($t('footer.privacy'), ENT_QUOTES) ?></a>
</div>
<div class="footer-bottom">© <?= date('Y') ?> <?= htmlspecialchars($siteName) ?></div>
</footer>
<div id="lightbox" class="lightbox" role="dialog" aria-modal="true" aria-label="<?= htmlspecialchars($t('lightbox.aria'), ENT_QUOTES) ?>">
<button id="lightboxClose" class="lightbox-close" type="button" aria-label="<?= htmlspecialchars($t('lightbox.close'), ENT_QUOTES) ?>">&times;</button>
<div class="lightbox-content">
<img id="lightboxImg" class="lightbox-img" alt="" />
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<script src="/js/haus-schleusingen.js"></script>
</body>

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Tests\Core;
use App\Core\I18n;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/**
* Guards against the most common i18n regression: a developer adds a key
* to one Locale file and forgets the other three. CI must fail loudly.
*/
final class LocaleConsistencyTest extends TestCase
{
/** @return array<string, array{string}> */
public static function localeProvider(): array
{
return [
'de' => ['de'],
'en' => ['en'],
'uk' => ['uk'],
'ru' => ['ru'],
];
}
#[Test]
public function allFourLocaleFilesLoadAndAreArrays(): void
{
foreach (self::localeProvider() as [$locale]) {
I18n::flushCache();
$data = require __DIR__ . '/../../app/Locales/' . $locale . '.php';
self::assertIsArray($data, "Locale file {$locale}.php must return an array");
self::assertNotEmpty($data, "Locale file {$locale}.php must not be empty");
}
}
#[Test]
public function everyLocaleHasExactlyTheSameKeySet(): void
{
$keysByLocale = [];
foreach (self::localeProvider() as [$locale]) {
$keysByLocale[$locale] = array_keys(require __DIR__ . '/../../app/Locales/' . $locale . '.php');
sort($keysByLocale[$locale]);
}
$reference = $keysByLocale['de'];
foreach (['en', 'uk', 'ru'] as $locale) {
$missing = array_diff($reference, $keysByLocale[$locale]);
$extra = array_diff($keysByLocale[$locale], $reference);
self::assertSame(
[],
$missing,
"Locale '{$locale}' is missing keys: " . implode(', ', $missing)
);
self::assertSame(
[],
$extra,
"Locale '{$locale}' has extra keys not in DE: " . implode(', ', $extra)
);
}
}
#[Test]
public function noTranslationValueIsEmpty(): void
{
foreach (self::localeProvider() as [$locale]) {
$data = require __DIR__ . '/../../app/Locales/' . $locale . '.php';
foreach ($data as $key => $value) {
self::assertIsString($value, "{$locale}.{$key} must be a string");
self::assertNotSame('', trim($value), "{$locale}.{$key} must not be empty");
}
}
}
#[Test]
#[DataProvider('localeProvider')]
public function everyTranslationIsValidUtf8(string $locale): void
{
$data = require __DIR__ . '/../../app/Locales/' . $locale . '.php';
foreach ($data as $key => $value) {
self::assertTrue(
mb_check_encoding($value, 'UTF-8'),
"{$locale}.{$key} contains invalid UTF-8"
);
}
}
}