feat(i18n): translation files DE/EN/UK/RU + layout integration (closes #74)

This commit is contained in:
Hermes
2026-06-04 09:31:34 +00:00
parent ce21242308
commit 4b1c779846
14 changed files with 1799 additions and 850 deletions

View File

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