diff --git a/.gitea/workflows/deploy-test.yml b/.gitea/workflows/deploy-test.yml index 365abc0..29585e2 100755 --- a/.gitea/workflows/deploy-test.yml +++ b/.gitea/workflows/deploy-test.yml @@ -1,131 +1,134 @@ -name: Deploy Feature Branch to Test +name: Deploy Feature Branch to Test (haus.test.kies-media.de) on: push: branches: - "feature/**" + workflow_dispatch: + inputs: + ref: + description: "Branch or tag to deploy (default: HEAD)" + required: false + default: "" jobs: - lint-php: - name: PHP Syntax Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install PHP - run: apt-get update -qq && apt-get install -y -qq php-cli > /dev/null 2>&1 - - - name: PHP Lint - run: | - errors=0 - while IFS= read -r file; do - if ! php -l "$file" > /dev/null 2>&1; then - echo "❌ Syntax error in $file" - php -l "$file" - errors=1 - fi - done < <(find . -name "*.php" -not -path "./vendor/*") - if [ "$errors" -eq 1 ]; then - echo "::error::PHP lint check failed" - exit 1 - fi - echo "✅ All PHP files pass syntax check" - - lint-css: - name: CSS Lint (stylelint) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Node.js & stylelint - run: | - apt-get update -qq && apt-get install -y -qq npm nodejs > /dev/null 2>&1 - npm install -g stylelint stylelint-config-standard stylelint-prettier > /dev/null 2>&1 - - - name: CSS Lint - run: | - npx stylelint "**/*.css" --config .stylelintrc.json --allow-empty-input - echo "✅ All CSS files pass lint" - - lint-html: - name: HTML Lint (htmlhint) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Node.js & htmlhint - run: | - apt-get update -qq && apt-get install -y -qq npm nodejs > /dev/null 2>&1 - npm install -g htmlhint > /dev/null 2>&1 - - - name: HTML Lint - run: | - npx htmlhint "**/*.html" --config .htmlhintrc - echo "✅ All HTML files pass lint" - deploy: name: Deploy to Test Environment runs-on: ubuntu-latest - needs: [lint-php, lint-css, lint-html] - container: - volumes: - - /var/www/test/html:/deploy + concurrency: + group: deploy-test + cancel-in-progress: false steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 1 - - name: Show branch info + - name: Setup SSH + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} run: | - echo "=== Deploying branch: ${{ gitea.ref_name }} ===" - echo "=== Commit: ${{ gitea.sha }} ===" - echo "=== By: ${{ gitea.actor }} ===" - echo "=== All lint checks passed ✅ ===" - date + if [ -z "$DEPLOY_SSH_KEY" ]; then + echo "⚠️ DEPLOY_SSH_KEY secret not set — skipping deploy step" + echo "skip_deploy=1" >> $GITHUB_ENV + exit 0 + fi + mkdir -p ~/.ssh + echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -H 188.245.242.194 >> ~/.ssh/known_hosts 2>/dev/null + echo "✅ SSH key configured" - - name: Deploy to test environment + - name: Verify SSH connectivity + if: env.skip_deploy != '1' run: | - echo "Syncing files to test environment..." - apt-get update -qq && apt-get install -y -qq rsync > /dev/null 2>&1 || true - - rsync -av --delete \ + ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_ed25519 \ + haustest@188.245.242.194 "echo 'SSH-OK as:' \$(whoami) 'on' \$(hostname)" + + - name: Backup current test deployment + if: env.skip_deploy != '1' + run: | + ssh -i ~/.ssh/id_ed25519 haustest@188.245.242.194 \ + "cd /home/haustest/htdocs && \ + tar czf /home/haustest/backup-pre-deploy-\$(date +%Y%m%d-%H%M%S).tar.gz \ + haus.test.kies-media.de && \ + ls -lh /home/haustest/backup-pre-deploy-*.tar.gz | tail -1" + + - name: Rsync to test environment + if: env.skip_deploy != '1' + run: | + rsync -avz --delete \ --exclude='.git' \ + --exclude='node_modules' \ + --exclude='tests' \ + --exclude='docs' \ --exclude='.gitea' \ - --exclude='.husky' \ - --exclude='.prettierrc' \ - --exclude='.prettierignore' \ - --exclude='.stylelintrc.json' \ - --exclude='.htmlhintrc' \ - --exclude='.gitignore' \ - --exclude='.dockerignore' \ --exclude='.continue' \ + --exclude='.husky' \ --exclude='Dockerfile' \ + --exclude='.dockerignore' \ --exclude='nginx.conf' \ --exclude='eslint.config.js' \ --exclude='package.json' \ --exclude='package-lock.json' \ - --exclude='docs/' \ + --exclude='phpunit.xml' \ + --exclude='scripts' \ --exclude='AGENTS.md' \ --exclude='README.md' \ - --exclude='scripts/' \ - ./ /deploy/ - + --exclude='CLAUDE.md' \ + --exclude='*.md' \ + --exclude='.htmlhintrc' \ + --exclude='.prettierrc' \ + --exclude='.prettierignore' \ + --exclude='.stylelintrc.json' \ + --exclude='.editorconfig' \ + --exclude='.well-known' \ + -e "ssh -i ~/.ssh/id_ed25519" \ + ./ haustest@188.245.242.194:/home/haustest/htdocs/haus.test.kies-media.de/ - echo "✅ Deployment complete!" - - - name: Set permissions + - name: Smoke test + if: env.skip_deploy != '1' run: | - chown -R 33:33 /deploy/ 2>/dev/null || true - chmod -R 755 /deploy/ 2>/dev/null || true - echo "✅ Permissions set" + sleep 2 + echo "--- HTTP status codes ---" + for path in "/" "/impressum" "/datenschutz"; do + code=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Cache-Control: no-cache" \ + "https://haus.test.kies-media.de${path}") + echo " $path → HTTP $code" + if [ "$code" != "200" ]; then + echo "❌ Smoke test failed for $path" + exit 1 + fi + done + echo "" + echo "--- Locale switcher present? ---" + if curl -sL "https://haus.test.kies-media.de/" | grep -q "class=\"locale-switcher\""; then + echo " ✅ Locale switcher rendered" + else + echo " ❌ Locale switcher MISSING" + exit 1 + fi + echo "" + echo "--- All 4 locales serving? ---" + for loc in de en uk ru; do + lang=$(curl -sL -H "Cache-Control: no-cache" \ + -b "locale=$loc" \ + "https://haus.test.kies-media.de/" \ + | grep -oE '> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Target:** https://haus.test.kies-media.de" >> $GITHUB_STEP_SUMMARY + echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Server:** haustest@188.245.242.194" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Review URL:** https://haus.test.kies-media.de" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 2166845..ad003fa 100755 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ *.ps1 *.py +!/tests/E2E/*.py /node_modules/ package-lock.json .continue/ .playwright-mcp/ vendor/ .phpunit.cache/ +build/ +.phpunit.coverage.cache/ diff --git a/app/Controllers/Controller.php b/app/Controllers/Controller.php index 309b848..8e7571e 100644 --- a/app/Controllers/Controller.php +++ b/app/Controllers/Controller.php @@ -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,31 @@ abstract class Controller $this->view = new View(); } - protected function render(string $view, array $data = [], string $layout = 'main'): void + /** + * Render a view inside a layout. + * + * @param array $data + * @param string|null $forceLocale If set, overrides the locale resolved from + * cookie/Accept-Language for this render. Used by legal pages (Impressum, + * Datenschutz) that must be served in German only by German law. + */ + protected function render(string $view, array $data = [], string $layout = 'main', ?string $forceLocale = null): void { - foreach ($data as $key => $value) { + $locale = $forceLocale ?? 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); diff --git a/app/Controllers/DatenschutzController.php b/app/Controllers/DatenschutzController.php index 9bb59b6..efc3994 100644 --- a/app/Controllers/DatenschutzController.php +++ b/app/Controllers/DatenschutzController.php @@ -4,15 +4,25 @@ declare(strict_types=1); namespace App\Controllers; +use App\Core\I18n; +use App\Core\Locale; + class DatenschutzController extends Controller { public function index(): void { + // Legal pages (Datenschutzerklärung) must be served in German only by GDPR / German law. + // Force German locale for render() so + German meta are emitted + // regardless of cookie/Accept-Language. $this->render('datenschutz/index', [ - 'pageTitle' => 'Datenschutzerklärung – Haus Schleusingen', - 'pageDescription' => 'Datenschutzerklärung der Website haus-schleusingen.de', + 'pageTitle' => I18n::t('legal.privacy_h1', [], 'de') . ' – ' . I18n::t('site.title', [], 'de'), + 'pageDescription' => I18n::t('legal.privacy_h1', [], 'de') . ' – ' . I18n::t('site.title', [], 'de'), 'robots' => 'noindex', - 'canonical' => 'https://haus-schleusingen.de/datenschutz', - ]); + 'canonical' => I18n::t('site.canonical_base', [], 'de') . '/datenschutz', + 'ogLocale' => Locale::toOgLocale('de'), + 'ogUrl' => I18n::t('site.canonical_base', [], 'de') . '/datenschutz', + 'ogTitle' => I18n::t('legal.privacy_h1', [], 'de'), + 'ogDescription' => I18n::t('legal.privacy_h1', [], 'de'), + ], 'main', 'de'); } } diff --git a/app/Controllers/HomeController.php b/app/Controllers/HomeController.php index f6e483b..507c114 100644 --- a/app/Controllers/HomeController.php +++ b/app/Controllers/HomeController.php @@ -4,184 +4,212 @@ 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']); + // ── Pull flashed state ──────────────────────────────────────── + $formSuccess = !empty($_SESSION['form_success']); + $formErrors = $_SESSION['form_errors'] ?? []; + $formFieldErrors = $_SESSION['form_field_errors'] ?? []; + $formData = $_SESSION['form_data'] ?? null; + unset( + $_SESSION['form_success'], + $_SESSION['form_errors'], + $_SESSION['form_field_errors'], + $_SESSION['form_data'], + ); + + if ($formSuccess) { + $formData = self::emptyFormData(); + $formFieldErrors = []; + } elseif (!is_array($formData)) { + $formData = self::emptyFormData(); + $formFieldErrors = is_array($formFieldErrors) ? $formFieldErrors : []; } else { - $formData = ['fname' => '', 'lname' => '', 'email' => '', 'phone' => '', 'interest' => 'Besichtigung anfragen', 'message' => '']; + $formFieldErrors = is_array($formFieldErrors) ? $formFieldErrors : []; } - // 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; } + + // Per-field errors enable aria-invalid + aria-describedby. + $formFieldErrors = []; + if ($formData['fname'] === '') { + $formFieldErrors['fname'][] = 'form.error.fname_required'; + } + if ($formData['lname'] === '') { + $formFieldErrors['lname'][] = 'form.error.lname_required'; + } + if ($formData['email'] === '' || !filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) { + $formFieldErrors['email'][] = 'form.error.email_invalid'; + } + if ($formData['message'] === '') { + $formFieldErrors['message'][] = '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) && empty($formFieldErrors)) { + $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_field_errors'] = $formFieldErrors; + $_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, + 'formFieldErrors' => $formFieldErrors, '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, ]); } + + /** + * @return array{fname: string, lname: string, email: string, phone: string, interest: string, message: string} + */ + private static function emptyFormData(): array + { + return [ + 'fname' => '', + 'lname' => '', + 'email' => '', + 'phone' => '', + 'interest' => 'visit', + 'message' => '', + ]; + } } diff --git a/app/Controllers/ImpressumController.php b/app/Controllers/ImpressumController.php index a7bef04..e856c16 100644 --- a/app/Controllers/ImpressumController.php +++ b/app/Controllers/ImpressumController.php @@ -4,15 +4,25 @@ declare(strict_types=1); namespace App\Controllers; +use App\Core\I18n; +use App\Core\Locale; + class ImpressumController extends Controller { public function index(): void { + // Legal pages (Impressum) must be served in German only by German law (TMG §5). + // Force German locale for render() so + German meta are emitted + // regardless of cookie/Accept-Language. $this->render('impressum/index', [ - 'pageTitle' => 'Impressum – Haus Schleusingen', - 'pageDescription' => 'Impressum der Website haus-schleusingen.de', + 'pageTitle' => I18n::t('legal.imprint_h1', [], 'de') . ' – ' . I18n::t('site.title', [], 'de'), + 'pageDescription' => I18n::t('legal.imprint_h1', [], 'de') . ' – ' . I18n::t('site.title', [], 'de'), 'robots' => 'noindex', - 'canonical' => 'https://haus-schleusingen.de/impressum', - ]); + 'canonical' => I18n::t('site.canonical_base', [], 'de') . '/impressum', + 'ogLocale' => Locale::toOgLocale('de'), + 'ogUrl' => I18n::t('site.canonical_base', [], 'de') . '/impressum', + 'ogTitle' => I18n::t('legal.imprint_h1', [], 'de'), + 'ogDescription' => I18n::t('legal.imprint_h1', [], 'de'), + ], 'main', 'de'); } } diff --git a/app/Controllers/LocaleController.php b/app/Controllers/LocaleController.php new file mode 100644 index 0000000..44a0137 --- /dev/null +++ b/app/Controllers/LocaleController.php @@ -0,0 +1,145 @@ + $resp['cookie_expires'], + 'path' => '/', + 'secure' => $resp['cookie_secure'], + 'httponly' => false, // read by JS-free client (we keep it readable for SSR) + 'samesite' => 'Lax', + ]; + setcookie(self::COOKIE_NAME, $resp['cookie_value'], $params); + } + + header('Location: ' . $resp['redirect'], true, $resp['status']); + exit; + } + + /** + * Pure response builder — testable without headers/exit. + * + * @return array{ + * status: int, + * redirect: string, + * set_cookie: bool, + * cookie_value: string, + * cookie_expires: int, + * cookie_secure: bool + * } + */ + public static function buildResponse( + ?string $locale, + ?string $explicitReturn, + ?string $referer, + bool $isHttps, + ): array { + $valid = is_string($locale) && Locale::isSupported($locale); + + $redirect = self::safeRedirect($explicitReturn, $referer); + + if (!$valid) { + return [ + 'status' => 302, + 'redirect' => $redirect, + 'set_cookie' => false, + 'cookie_value' => '', + 'cookie_expires' => 0, + 'cookie_secure' => $isHttps, + ]; + } + + return [ + 'status' => 302, + 'redirect' => $redirect, + 'set_cookie' => true, + 'cookie_value' => $locale, + 'cookie_expires' => time() + 60 * 60 * 24 * 365, // 1 year + 'cookie_secure' => $isHttps, + ]; + } + + /** + * Compute the current locale from $_GET, $_COOKIE and Accept-Language. + * Convenience for the front controller / View layer. + */ + public static function current(): string + { + return Locale::resolve( + isset($_GET['lang']) && is_string($_GET['lang']) ? $_GET['lang'] : null, + $_COOKIE[self::COOKIE_NAME] ?? null, + $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null, + ); + } + + /** + * Sanitize the return URL — same-origin path-only. Anything with a + * scheme, host, or `//` prefix is rejected and replaced with `/`. + */ + private static function safeRedirect(?string $explicit, ?string $referer): string + { + $candidate = $explicit ?: $referer; + if (!is_string($candidate) || $candidate === '') { + return '/'; + } + + // Reject absolute URLs and protocol-relative BEFORE backslash fixup, + // so a backslash in the input doesn't smuggle a `//` past us. + if (preg_match('#^(https?:)?//#i', $candidate)) { + return '/'; + } + + // Normalize backslashes (some browsers treat \ as /) + $candidate = str_replace('\\', '/', $candidate); + + // After normalization, re-check for `//` (could be a backslash trick). + if (preg_match('#^//#', $candidate)) { + return '/'; + } + + if ($candidate[0] !== '/') { + return '/'; + } + + return $candidate; + } +} diff --git a/app/Controllers/LocaleSwitcher.php b/app/Controllers/LocaleSwitcher.php new file mode 100644 index 0000000..4bfa525 --- /dev/null +++ b/app/Controllers/LocaleSwitcher.php @@ -0,0 +1,154 @@ +-based dropdown shown at every viewport. The + * trigger is a flag-sized button (24×16 SVG + tiny caret) that + * opens to a menu of all 4 supported locales. + * + * Each menu option gets: + * - an inline 24×16 SVG flag, + * - `hreflang` and `lang` for SEO and screen readers, + * - `aria-current="true"` on the active option. + * + * The active option is rendered as a (not a link) so it + * cannot be reactivated. The trigger and every menu option are + * ≥44px touch targets via CSS. + */ +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', + ); + + $currentName = htmlspecialchars( + I18n::t('locale.' . $this->currentLocale, [], $this->currentLocale), + ENT_QUOTES, + 'UTF-8', + ); + $currentCode = htmlspecialchars($this->currentLocale, ENT_QUOTES, 'UTF-8'); + $currentFlag = self::flagImg($this->currentLocale); + + $html = '
'; + $html .= ''; + $html .= ''; + $html .= $currentFlag; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= '
    '; + 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'); + $flag = self::flagImg($code); + + $html .= '
  • '; + if ($isCurrent) { + $html .= '' + . $flag + . '' . $name . '' + . ''; + } else { + $url = '/locale?set=' . rawurlencode($code) . '&return=' . rawurlencode($path); + $html .= '' + . $flag + . '' . $name . '' + . ''; + } + $html .= '
  • '; + } + $html .= '
'; + $html .= '
'; + + return $html; + } + + /** + * Country flag for the given locale. Renders a 24×18 + * pointing at the official flag-icons SVG asset shipped under + * public/img/flags/. 4:3 aspect (de/gb/ua/ru), crisp at any DPI, + * no external CDN dependency. + * + * Decorative: `alt=""` (the visible locale-switcher label and + * the 's `hreflang`/`lang` carry the accessible name). + */ + public static function flagImg(string $locale): string + { + $src = self::flagSource($locale); + // 32×24 = ~4:3, large enough that the flag is the visual + // anchor of the option. No loading="lazy" — these are 4 + // small SVGs that must be ready the moment the
+ // opens (lazy would cause a flash of empty boxes). + return ''; + } + + /** + * Map our locale codes to flag-icons file names. Locale "en" + * is en-GB per ADR-002, so the asset is "gb.svg". Anything we + * do not know falls back to a transparent 1×1 gif so the layout + * stays intact and the alt text (from the surrounding ) is + * the only signal. + */ + private static function flagSource(string $locale): string + { + $file = match ($locale) { + 'de' => 'de', + 'en' => 'gb', + 'uk' => 'ua', + 'ru' => 'ru', + default => null, + }; + if ($file === null) { + return 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAACAkQBADs='; + } + return '/img/flags/' . $file . '.svg'; + } + + /** + * 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; + } +} diff --git a/app/Core/I18n.php b/app/Core/I18n.php new file mode 100644 index 0000000..7a151f2 --- /dev/null +++ b/app/Core/I18n.php @@ -0,0 +1,132 @@ + 'text'` arrays from `app/Locales/{locale}.php` once per + * request per locale, caches in static memory. Supports {placeholder} + * interpolation. + * + * Fallback chain: current locale → 'de' → key string itself (with optional + * missing-key indicator in dev). + * + * Stateless on the instance — `t()` is a static method so views can call + * it without a container. + */ +final class I18n +{ + /** @var array> locale => [key => text] */ + private static array $cache = []; + + /** @var string|null Path to the Locales directory (overridable for tests) */ + private static ?string $localesPath = null; + + /** + * Translate a key in the current locale, with {placeholder} interpolation. + * + * @param string $key Dotted key, e.g. 'nav.gallery' + * @param array $params Placeholders: ['name' => 'Martin'] + * @param string|null $locale Override locale (defaults to current) + */ + public static function t(string $key, array $params = [], ?string $locale = null): string + { + $locale ??= Locale::DEFAULT; + + // Unsupported locale = likely a developer bug; surface the key + // rather than silently falling back to DE. + if (!Locale::isSupported($locale)) { + return self::interpolate($key, $params); + } + + $text = self::lookup($key, $locale); + if ($text === null && $locale !== Locale::DEFAULT) { + $text = self::lookup($key, Locale::DEFAULT); + } + $text ??= $key; + return self::interpolate($text, $params); + } + + /** + * Plural-aware translation. MVP: no ICU — we just append `{n}` to the + * key so the caller provides singular and plural variants. + * + * @param array $params + */ + public static function tn(string $keySingular, string $keyPlural, int $n, array $params = [], ?string $locale = null): string + { + $key = $n === 1 ? $keySingular : $keyPlural; + $params = array_merge($params, ['n' => (string) $n]); + return self::t($key, $params, $locale); + } + + /** + * Check whether a key exists in the given locale (or the default). + * Useful for tests and conditional UI logic. + */ + public static function has(string $key, ?string $locale = null): bool + { + $locale ??= Locale::DEFAULT; + return self::lookup($key, $locale) !== null + || ($locale !== Locale::DEFAULT && self::lookup($key, Locale::DEFAULT) !== null); + } + + /** + * Reset the in-memory cache. Test-only utility. + */ + public static function flushCache(): void + { + self::$cache = []; + } + + /** + * Override the Locales directory. Test-only utility. + */ + public static function setLocalesPath(string $path): void + { + self::$localesPath = $path; + } + + /** + * Look up a key in a specific locale's array. + */ + private static function interpolate(string $text, array $params): string + { + if ($params === []) { + return $text; + } + $search = array_map(static fn (string $k): string => '{' . $k . '}', array_keys($params)); + $replace = array_values($params); + return str_replace($search, $replace, $text); + } + + private static function lookup(string $key, string $locale): ?string + { + if (!Locale::isSupported($locale)) { + return null; + } + + if (!isset(self::$cache[$locale])) { + $file = self::localesPath() . '/' . $locale . '.php'; + if (!is_file($file)) { + self::$cache[$locale] = []; + return null; + } + $data = require $file; + self::$cache[$locale] = is_array($data) ? $data : []; + } + + return self::$cache[$locale][$key] ?? null; + } + + private static function localesPath(): string + { + if (self::$localesPath !== null) { + return self::$localesPath; + } + return dirname(__DIR__) . '/Locales'; + } +} diff --git a/app/Core/Locale.php b/app/Core/Locale.php new file mode 100644 index 0000000..8ecb1d9 --- /dev/null +++ b/app/Core/Locale.php @@ -0,0 +1,162 @@ + ISO 639-1 codes, de is the source of truth */ + public const SUPPORTED = ['de', 'en', 'uk', 'ru']; + + /** + * Resolve a locale from request signals. + * + * Priority: explicit query/cookie > Accept-Language header > default. + * + * @param string|null $queryParam Value of ?lang= (raw, unvalidated) + * @param string|null $cookieValue Value of the 'locale' cookie (raw) + * @param string|null $acceptLanguage Raw Accept-Language header + */ + public static function resolve( + ?string $queryParam = null, + ?string $cookieValue = null, + ?string $acceptLanguage = null, + ): string { + // 1. Query param wins (one-shot, used by LocaleController to set cookie) + if (is_string($queryParam) && self::isSupported($queryParam)) { + return $queryParam; + } + + // 2. Cookie next + if (is_string($cookieValue) && self::isSupported($cookieValue)) { + return $cookieValue; + } + + // 3. Accept-Language header + if (is_string($acceptLanguage) && $acceptLanguage !== '') { + $parsed = self::parseAcceptLanguage($acceptLanguage); + foreach ($parsed as $tag) { + if (self::isSupported($tag)) { + return $tag; + } + } + } + + // 4. Fallback + return self::DEFAULT; + } + + /** + * Normalize an Accept-Language header into a list of ISO 639-1 codes + * sorted by q-value (highest first), with q=0 entries dropped. + * + * Handles wildcards ("*") and BCP-47 subtags ("en-US" → "en", + * "uk-UA" → "uk"). Entries with the same q-value keep header order + * (stable). + * + * @return list + */ + public static function parseAcceptLanguage(string $header): array + { + $header = trim($header); + if ($header === '') { + return []; + } + + $entries = []; + foreach (explode(',', $header) as $i => $part) { + $parts = explode(';', trim($part)); + $tag = trim($parts[0]); + $q = 1.0; + + for ($j = 1; $j < count($parts); $j++) { + if (preg_match('/^q\s*=\s*([0-9.]+)$/i', trim($parts[$j]), $m)) { + $q = (float) $m[1]; + } + } + + if ($q <= 0.0) { + continue; + } + + // Strip BCP-47 region: "en-US" → "en", "uk-UA" → "uk" + $primary = strtolower(explode('-', $tag)[0]); + if ($primary === '*' || $primary === '') { + continue; + } + + // Sort key: -q (descending) and original position (ascending) + $entries[] = [ + 'tag' => $primary, + 'q' => $q, + 'pos' => $i, + ]; + } + + usort($entries, static function (array $a, array $b): int { + if ($a['q'] !== $b['q']) { + return $b['q'] <=> $a['q']; + } + return $a['pos'] <=> $b['pos']; + }); + + return array_values(array_map(static fn (array $e): string => $e['tag'], $entries)); + } + + /** + * Check whether a code is in {@see self::SUPPORTED}. + */ + public static function isSupported(string $code): bool + { + return in_array($code, self::SUPPORTED, true); + } + + /** + * Map ISO 639-1 → BCP-47 og:locale format. + * Used by View layout for . + */ + public static function toOgLocale(string $code): string + { + return match ($code) { + 'de' => 'de_DE', + 'en' => 'en_GB', // UK English by user requirement + 'uk' => 'uk_UA', + 'ru' => 'ru_RU', + default => 'de_DE', + }; + } + + /** + * Build the full hreflang alternate list for the current page, given its + * canonical path. Returns an array of ['locale' => 'hreflang', 'href' => url]. + * + * @return list + */ + public static function hreflangAlternates(string $canonicalPath, string $baseUrl = 'https://haus-schleusingen.de'): array + { + $out = []; + foreach (self::SUPPORTED as $code) { + $hreflang = $code === 'en' ? 'en-GB' : ($code === 'uk' ? 'uk' : $code); + $out[] = [ + 'locale' => $code, + 'hreflang' => $hreflang, + 'href' => $baseUrl . $canonicalPath . '?lang=' . $code, + ]; + } + return $out; + } +} diff --git a/app/Locales/de.php b/app/Locales/de.php new file mode 100644 index 0000000..f1a68fa --- /dev/null +++ b/app/Locales/de.php @@ -0,0 +1,229 @@ + '...'. + * 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
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', + 'footer.aria' => 'Fußbereich', + 'a11y.main' => 'Hauptinhalt', + + // ─── 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', +]; diff --git a/app/Locales/en.php b/app/Locales/en.php new file mode 100644 index 0000000..bf8f954 --- /dev/null +++ b/app/Locales/en.php @@ -0,0 +1,207 @@ + '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
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', + 'footer.aria' => 'Footer', + 'a11y.main' => 'Main content', + + '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', +]; diff --git a/app/Locales/ru.php b/app/Locales/ru.php new file mode 100644 index 0000000..faab040 --- /dev/null +++ b/app/Locales/ru.php @@ -0,0 +1,207 @@ + 'Дом Шлайзинген', + '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
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' => 'Политика конфиденциальности', + 'footer.aria' => 'Подвал сайта', + 'a11y.main' => 'Основное содержимое', + + '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' => 'Базовая аренда в месяц', +]; diff --git a/app/Locales/uk.php b/app/Locales/uk.php new file mode 100644 index 0000000..8deb259 --- /dev/null +++ b/app/Locales/uk.php @@ -0,0 +1,207 @@ + 'Будинок Шлайзінген', + '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
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' => 'Політика конфіденційності', + 'footer.aria' => 'Нижній колонтитул', + 'a11y.main' => 'Головний вміст', + + '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' => 'Базова оренда на місяць', +]; diff --git a/app/controllers/Controller.php b/app/controllers/Controller.php deleted file mode 100644 index 309b848..0000000 --- a/app/controllers/Controller.php +++ /dev/null @@ -1,25 +0,0 @@ -view = new View(); - } - - protected function render(string $view, array $data = [], string $layout = 'main'): void - { - foreach ($data as $key => $value) { - $this->view->assign($key, $value); - } - $this->view->render($view, $layout); - } -} diff --git a/app/controllers/DatenschutzController.php b/app/controllers/DatenschutzController.php deleted file mode 100644 index 9bb59b6..0000000 --- a/app/controllers/DatenschutzController.php +++ /dev/null @@ -1,18 +0,0 @@ -render('datenschutz/index', [ - 'pageTitle' => 'Datenschutzerklärung – Haus Schleusingen', - 'pageDescription' => 'Datenschutzerklärung der Website haus-schleusingen.de', - 'robots' => 'noindex', - 'canonical' => 'https://haus-schleusingen.de/datenschutz', - ]); - } -} diff --git a/app/controllers/HomeController.php b/app/controllers/HomeController.php deleted file mode 100644 index f6e483b..0000000 --- a/app/controllers/HomeController.php +++ /dev/null @@ -1,187 +0,0 @@ - '', 'lname' => '', 'email' => '', 'phone' => '', 'interest' => 'Besichtigung anfragen', 'message' => '']; - } - - // CSRF-Token generieren (nach Session-Start) - if (empty($_SESSION['csrf_token'])) { - $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); - } - - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - // CSRF-Token validieren - $csrfToken = $_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.']; - 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'] ?? '')); - - $honeypot = $normalizeContactValue((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; - exit; - } - } - - $this->render('home/index', [ - 'formSuccess' => $formSuccess, - 'formErrors' => $formErrors, - 'formData' => $formData, - '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', - ], - ]), - ]); - } -} diff --git a/app/controllers/ImpressumController.php b/app/controllers/ImpressumController.php deleted file mode 100644 index a7bef04..0000000 --- a/app/controllers/ImpressumController.php +++ /dev/null @@ -1,18 +0,0 @@ -render('impressum/index', [ - 'pageTitle' => 'Impressum – Haus Schleusingen', - 'pageDescription' => 'Impressum der Website haus-schleusingen.de', - 'robots' => 'noindex', - 'canonical' => 'https://haus-schleusingen.de/impressum', - ]); - } -} diff --git a/app/views/datenschutz/index.php b/app/views/datenschutz/index.php index 7bb1594..e7d6856 100644 --- a/app/views/datenschutz/index.php +++ b/app/views/datenschutz/index.php @@ -1,123 +1,116 @@ -
+ -
Datenschutz
-

Datenschutzerklärung

+declare(strict_types=1); -

1. Verantwortliche Stelle

-
- Martin Kiesewetter
- Am Schaftalsgraben 4
- 98529 Suhl
- Telefon: 0176 – 45853923
- E-Mail: mki@kies-media.de -
-

- Diese Datenschutzerklärung informiert Sie über die Art, den Umfang und den Zweck der Erhebung und Verwendung personenbezogener Daten auf dieser Website. -

+/** + * 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 + */ +?> +
+
+

- +

1. Verantwortliche Stelle

+
+ Martin Kiesewetter
+ Am Schaftalsgraben 4
+ 98529 Suhl
+ Telefon: 0176 – 45853923
+ E-Mail: mki@kies-media.de +
+

+ Diese Datenschutzerklärung informiert Sie über die Art, den Umfang und den Zweck der Erhebung und Verwendung personenbezogener Daten auf dieser Website. +

-

2. Erhebung und Speicherung personenbezogener Daten

+ -

a) Beim Besuch der Website

-

- 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. -

-
    -
  • IP-Adresse
  • -
  • Datum und Uhrzeit der Anfrage
  • -
  • Zeitzonenunterschied zur Greenwich Mean Time (GMT)
  • -
  • Inhalt der Anforderung (konkrete Seite)
  • -
  • Zugriffsstatus/HTTP-Statuscode
  • -
  • Jeweils übertragene Datenmenge
  • -
  • Website, von der die Anforderung kommt (Referrer-URL)
  • -
  • Verwendeter Browser
  • -
  • Verwendetes Betriebssystem
  • -
-

- 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. -

+

2. Erhebung und Speicherung personenbezogener Daten

-

b) Kontakt per E-Mail

-

- 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. -

-

- 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). -

+

a) Beim Besuch der Website

+

+ 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. +

+
    +
  • IP-Adresse
  • +
  • Datum und Uhrzeit der Anfrage
  • +
  • Zeitzonenunterschied zur Greenwich Mean Time (GMT)
  • +
  • Inhalt der Anforderung (konkrete Seite)
  • +
  • Zugriffsstatus/HTTP-Statuscode
  • +
  • Jeweils übertragene Datenmenge
  • +
  • Website, von der die Anforderung kommt (Referrer-URL)
  • +
  • Verwendeter Browser
  • +
  • Verwendetes Betriebssystem
  • +
+

+ 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. +

- +

b) Kontakt per E-Mail

+

+ 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. +

+

+ 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). +

-

3. Cookies

-

- Diese Website verwendet keine Cookies. Es werden keine Tracking-Cookies, Werbe-Cookies oder sonstige Cookies gesetzt. -

+ -

4. Tracking & Analyse

-

- Diese Website setzt keine Tracking- oder Analyse-Tools ein. Es werden keine Besucherstatistiken erstellt, kein Google Analytics, kein Facebook Pixel und keine ähnlichen Dienste verwendet. -

+

3. Cookies

+

+ Diese Website verwendet keine Cookies. Es werden keine Tracking-Cookies, Werbe-Cookies oder sonstige Cookies gesetzt. +

-

5. Social-Media-Plugins

-

- Diese Website verwendet keine Social-Media-Plugins (Facebook, Twitter, Instagram etc.). -

+

4. Tracking & Analyse

+

+ Diese Website setzt keine Tracking- oder Analyse-Tools ein. Es werden keine Besucherstatistiken erstellt, kein Google Analytics, kein Facebook Pixel und keine ähnlichen Dienste verwendet. +

- +

5. Social-Media-Plugins

+

+ Diese Website verwendet keine Social-Media-Plugins (Facebook, Twitter, Instagram etc.). +

-

6. Google Maps

-

- 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. -

-

- Weitere Informationen zum Datenschutz bei Google finden Sie unter: https://policies.google.com/privacy -

-

- Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigte Interessen an der Darstellung des Objektstandorts). -

+ - +

6. Google Maps

+

+ 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. +

+

+ Weitere Informationen zum Datenschutz bei Google finden Sie unter: https://policies.google.com/privacy +

+

+ Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigte Interessen an der Darstellung des Objektstandorts). +

-

7. SSL-Verschlüsselung

-

- 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. -

+ - +

7. SSL-Verschlüsselung

+

+ 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. +

-

8. Ihre Rechte

-

Sie haben gegenüber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:

-
    -
  • Recht auf Auskunft (Art. 15 DSGVO)
  • -
  • Recht auf Berichtigung (Art. 16 DSGVO)
  • -
  • Recht auf Löschung (Art. 17 DSGVO)
  • -
  • Recht auf Einschränkung der Verarbeitung (Art. 18 DSGVO)
  • -
  • Recht auf Datenübertragbarkeit (Art. 20 DSGVO)
  • -
  • Widerspruchsrecht (Art. 21 DSGVO)
  • -
  • Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
  • -
  • Beschwerderecht bei einer Aufsichtsbehörde (Art. 77 DSGVO)
  • -
-

- Zur Ausübung Ihrer Rechte wenden Sie sich bitte an: mki@kies-media.de -

+ - ← Zurück zum Objekt -
+

8. Ihre Rechte

+

Sie haben gegenüber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:

+
    +
  • Recht auf Auskunft (Art. 15 DSGVO)
  • +
  • Recht auf Berichtigung (Art. 16 DSGVO)
  • +
  • Recht auf Löschung (Art. 17 DSGVO)
  • +
  • Recht auf Einschränkung der Verarbeitung (Art. 18 DSGVO)
  • +
  • Recht auf Datenübertragbarkeit (Art. 20 DSGVO)
  • +
  • Widerspruchsrecht (Art. 21 DSGVO)
  • +
  • Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
  • +
  • Beschwerderecht bei einer Aufsichtsbehörde (Art. 77 DSGVO)
  • +
+

+ Zur Ausübung Ihrer Rechte wenden Sie sich bitte an: mki@kies-media.de +

- + + diff --git a/app/views/home/index.php b/app/views/home/index.php index 5812219..9bee39d 100644 --- a/app/views/home/index.php +++ b/app/views/home/index.php @@ -1,505 +1,355 @@ - - - - -
-
-
-
-
Zur Langzeitmiete · Ab sofort verfügbar
-

- Großzügiges -
- Einfamilienhaus -
- in Schleusingen -

-
- Schleusinger Bahnhofstraße 10 - 227 m² Wohnfläche - 6 Zimmer - 3 Etagen + Dachterrasse -
-
-
- Entdecken -
-
-
- -
-
-
-
227
-
m² Wohnfläche
-
-
-
6
-
Zimmer
-
-
-
3
-
Etagen
-
-
-
1.300
-
€ Kaltmiete
-
-
- -
-
-
Das Objekt
-

Wohnen mit Charakter und viel Raum

-

- 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. -

-

- 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. -

-
-
-
154,9 m²
-
Nutzfläche
-
-
-
35,8 m²
-
Dachterrasse
-
-
-
2 Stpl.
-
Garage
-
-
-
-
- - - Wohnzimmer - -
Wohnzimmer · 42,6 m²
-
-
- - - -
-
Raumaufteilung
-

Großzügig auf allen Etagen

-
-
- -
-
-
Flur20,1 m²
-
WC0,8 m²
-
Garage / Partykeller42,6 m²
-
Abstellraum 19,9 m²
-
Abstellraum 27,8 m²
-
Heizungskeller18,3 m²
-
-
- - - Grundriss Erdgeschoss - - - - Grundriss Erdgeschoss - -
-
-
-
- -
-
-
Flur20,1 m²
-
Wohnzimmer42,6 m²
-
Gästezimmer11,5 m²
-
Badezimmer9,8 m²
-
Küche18,4 m²
-
Schlafzimmer18,0 m²
-
-
- - - Grundriss 1. Obergeschoss - - - - Grundriss 1. Obergeschoss - -
-
-
-
- -
-
-
Flur13,9 m²
-
Kinderzimmer 121,7 m²
-
Kinderzimmer 215,7 m²
-
Spielzimmer6,3 m²
-
Ankleidezimmer1,4 m²
-
Dachterrasse9,0 m² (25% von 35,8 m²)
-
-
- - - Grundriss 2. Obergeschoss - - - - Grundriss 2. Obergeschoss - -
-
-
-
- -
-
-
Dachboden unten (ungeheizt)52 m²
-
Dachboden Mitte (ungeheizt)31 m²
-
Dachboden oben (ungeheizt)11 m²
-
-
- - - Grundriss Dachboden - - - - Grundriss Dachboden - -
-
-
-
-
- -
-
-
Mietkonditionen
-

Transparente Preisgestaltung

-
-
-
Kaltmiete
-
1.300 €
-
pro Monat
-
-
-
Gesamtmiete warm
-
1.600 €
-
inkl. 300 € Nebenkosten
-
-
-
Kaution
-
2.600 €
-
2 Nettokaltmieten
-
-
-
-
- Verfügbarkeit - Ab sofort · unbefristete Laufzeit -
-
- Nebenkosten - Vorauszahlung 300 €/Monat, jährliche Abrechnung -
-
- Energieausweis - Wird bei Mietbeginn übergeben · Erdgasheizung -
-
- Haustiere - Auf Anfrage -
-
-
-
- -
-
Standort
-

Zentral und ruhig zugleich

-
-
-
🛒
-
-
Einkaufen & Versorgung
-
Supermärkte, Ärzte, Apotheken und Schulen sind fußläufig erreichbar
-
-
-
-
🚌
-
-
Öffentlicher Nahverkehr
-
Zentrale Bushaltestelle ca. 200 m entfernt — direkte Verbindungen in die Region
-
-
-
-
🏛
-
-
Innenstadt Schleusingen
-
Wochenmarkt und Stadtmitte nur ca. 500 m entfernt
-
-
-
-
📍
-
-
Genaue Adresse
-
Schleusinger Bahnhofstraße 10
98553 Schleusingen, Thüringen
-
-
-
-
- -
-
- -
-
-
Kontakt
-

Interesse?
Schreiben Sie uns.

-

- 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. -

-
- -
-

Vielen Dank für Ihre Anfrage!

-
- Wir haben Ihre Nachricht erhalten und melden uns innerhalb von 24 Stunden bei Ihnen. -
- - -
-
    - -
  • - -
-
- -
- -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- - - -
- -
-
-

Oder schreiben Sie uns direkt: mki@kies-media.de

-
-
-
+
+ + +
+ +

+ + + +

+
    +
  • +
  • +
  • +
  • +
+
+
-
- +
+
227
+
6
+
3
+
1.300
+
-