Multi-Language MVP: 4 Locales (DE/EN/UK/RU) mit SSR + UI + Tests #79
@@ -25,10 +25,13 @@ abstract class Controller
|
||||
* Render a view inside a layout.
|
||||
*
|
||||
* @param array<string,mixed> $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'): void
|
||||
protected function render(string $view, array $data = [], string $layout = 'main', ?string $forceLocale = null): void
|
||||
{
|
||||
$locale = LocaleController::current();
|
||||
$locale = $forceLocale ?? LocaleController::current();
|
||||
$i18n = static fn (string $key, array $params = []): string => I18n::t($key, $params, $locale);
|
||||
|
||||
$globals = [
|
||||
|
||||
@@ -11,17 +11,18 @@ class DatenschutzController extends Controller
|
||||
{
|
||||
public function index(): void
|
||||
{
|
||||
$locale = LocaleController::current();
|
||||
|
||||
// Legal pages (Datenschutzerklärung) must be served in German only by GDPR / German law.
|
||||
// Force German locale for render() so <html lang="de"> + German meta are emitted
|
||||
// regardless of cookie/Accept-Language.
|
||||
$this->render('datenschutz/index', [
|
||||
'pageTitle' => I18n::t('legal.privacy_h1', [], $locale) . ' – ' . I18n::t('site.title', [], $locale),
|
||||
'pageDescription' => I18n::t('legal.privacy_h1', [], $locale) . ' – ' . I18n::t('site.title', [], $locale),
|
||||
'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' => I18n::t('site.canonical_base', [], $locale) . '/datenschutz',
|
||||
'ogLocale' => Locale::toOgLocale($locale),
|
||||
'ogUrl' => I18n::t('site.canonical_base', [], $locale) . '/datenschutz',
|
||||
'ogTitle' => I18n::t('legal.privacy_h1', [], $locale),
|
||||
'ogDescription' => I18n::t('legal.privacy_h1', [], $locale),
|
||||
]);
|
||||
'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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,17 +11,18 @@ class ImpressumController extends Controller
|
||||
{
|
||||
public function index(): void
|
||||
{
|
||||
$locale = LocaleController::current();
|
||||
|
||||
// Legal pages (Impressum) must be served in German only by German law (TMG §5).
|
||||
// Force German locale for render() so <html lang="de"> + German meta are emitted
|
||||
// regardless of cookie/Accept-Language.
|
||||
$this->render('impressum/index', [
|
||||
'pageTitle' => I18n::t('legal.imprint_h1', [], $locale) . ' – ' . I18n::t('site.title', [], $locale),
|
||||
'pageDescription' => I18n::t('legal.imprint_h1', [], $locale) . ' – ' . I18n::t('site.title', [], $locale),
|
||||
'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' => I18n::t('site.canonical_base', [], $locale) . '/impressum',
|
||||
'ogLocale' => Locale::toOgLocale($locale),
|
||||
'ogUrl' => I18n::t('site.canonical_base', [], $locale) . '/impressum',
|
||||
'ogTitle' => I18n::t('legal.imprint_h1', [], $locale),
|
||||
'ogDescription' => I18n::t('legal.imprint_h1', [], $locale),
|
||||
]);
|
||||
'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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,22 +15,22 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
$gridItems = [
|
||||
['img' => 'bilder/Außenansicht-2.png', 'key' => 'gallery.exterior', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-2'],
|
||||
['img' => 'bilder/Wohnzimmer-1.png', 'key' => 'gallery.living', 'alt' => 'gallery.alt.living', 'class' => 'span-2 row-1'],
|
||||
['img' => 'bilder/Küche-1.png', 'key' => 'gallery.kitchen', 'alt' => 'gallery.alt.kitchen', 'class' => ''],
|
||||
['img' => 'bilder/Schlafzimmer-1.png','key' => 'gallery.bedroom', 'alt' => 'gallery.alt.bedroom', 'class' => ''],
|
||||
['img' => 'bilder/Badezimmer-1.png', 'key' => 'gallery.bath', 'alt' => 'gallery.alt.bath', 'class' => ''],
|
||||
['img' => 'bilder/Kinderzimmer-1-1.png', 'key' => 'gallery.kid1', 'alt' => 'gallery.alt.kid1', 'class' => ''],
|
||||
['img' => 'bilder/Kinderzimmer-2.png','key' => 'gallery.kid2', 'alt' => 'gallery.alt.kid2', 'class' => ''],
|
||||
['img' => 'bilder/Kinderzimmer-Detail.png','key' => 'gallery.kid_detail', 'alt' => 'gallery.alt.kid_detail', 'class' => 'span-2 row-1'],
|
||||
['img' => 'bilder/Gästezimmer.png', 'key' => 'gallery.guest', 'alt' => 'gallery.alt.guest', 'class' => ''],
|
||||
['img' => 'bilder/Wohnbereich.png', 'key' => 'gallery.area1', 'alt' => 'gallery.alt.living', 'class' => ''],
|
||||
['img' => 'bilder/Wohnbereich-Detail.png', 'key' => 'gallery.area2', 'alt' => 'gallery.alt.living', 'class' => ''],
|
||||
['img' => 'bilder/Außenansicht-1.png','key' => 'gallery.area3', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-1'],
|
||||
// NOTE: image filenames reflect the actual files in public/bilder/ on the server.
|
||||
// 3 items were removed (gästezimmer / wohnbereich / wohnbereich-detail)
|
||||
// because no matching files exist in the image inventory.
|
||||
['img' => 'bilder/Außenansicht-2.png', 'key' => 'gallery.exterior', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-2'],
|
||||
['img' => 'bilder/wohnzimmer2.png', 'key' => 'gallery.living', 'alt' => 'gallery.alt.living', 'class' => 'span-2 row-1'],
|
||||
['img' => 'bilder/Küche 1.jpg', 'key' => 'gallery.kitchen', 'alt' => 'gallery.alt.kitchen', 'class' => ''],
|
||||
['img' => 'bilder/schlafzimmer.png', 'key' => 'gallery.bedroom', 'alt' => 'gallery.alt.bedroom', 'class' => ''],
|
||||
['img' => 'bilder/Bad.jpg', 'key' => 'gallery.bath', 'alt' => 'gallery.alt.bath', 'class' => ''],
|
||||
['img' => 'bilder/Kinderzimmer 2.jpg', 'key' => 'gallery.kid1', 'alt' => 'gallery.alt.kid1', 'class' => ''],
|
||||
['img' => 'bilder/Kinderzimmer 3.jpg', 'key' => 'gallery.kid2', 'alt' => 'gallery.alt.kid2', 'class' => ''],
|
||||
['img' => 'bilder/kinderzimmer 2 2.webp', 'key' => 'gallery.kid_detail', 'alt' => 'gallery.alt.kid_detail', 'class' => 'span-2 row-1'],
|
||||
['img' => 'bilder/Außenansicht-2.png', 'key' => 'gallery.area3', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-1'],
|
||||
];
|
||||
?>
|
||||
<header class="hero" id="hero">
|
||||
<img src="/bilder/hero-bg.jpg" alt="" class="hero-bg" id="heroBg" loading="eager" decoding="async" fetchpriority="high">
|
||||
<img src="/bilder/Außenansicht-2.webp" alt="" class="hero-bg" id="heroBg" loading="eager" decoding="async" fetchpriority="high">
|
||||
<div class="hero-content" id="heroContent">
|
||||
<span class="hero-tag"><?= htmlspecialchars($t('hero.tag'), ENT_QUOTES) ?></span>
|
||||
<h1 class="hero-h1">
|
||||
@@ -105,6 +105,13 @@ $gridItems = [
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$floorImageMap = [
|
||||
'eg' => 'bilder/grundrisse/EG.png',
|
||||
'og1' => 'bilder/grundrisse/OG 1 2.png',
|
||||
'og2' => 'bilder/grundrisse/OG 2 grundriss.png',
|
||||
'attic' => 'bilder/grundrisse/Dachboden unten.png',
|
||||
];
|
||||
|
||||
$floors = [
|
||||
['id' => 'eg', 'titleKey' => 'floors.eg.title', 'areaKey' => 'floors.eg.area', 'altKey' => 'floors.alt.eg',
|
||||
'rooms' => [
|
||||
@@ -148,7 +155,7 @@ $gridItems = [
|
||||
<span class="floor-area"><?= htmlspecialchars($t($floor['areaKey']), ENT_QUOTES) ?></span>
|
||||
</summary>
|
||||
<div class="floor-body">
|
||||
<img src="/bilder/grundriss-<?= htmlspecialchars($floor['id'], ENT_QUOTES) ?>.png"
|
||||
<img src="/<?= htmlspecialchars($floorImageMap[$floor['id']] ?? 'bilder/grundrisse/EG.png', ENT_QUOTES) ?>"
|
||||
alt="<?= htmlspecialchars($t($floor['altKey']), ENT_QUOTES) ?>"
|
||||
loading="lazy" decoding="async"
|
||||
class="floor-plan-img">
|
||||
|
||||
@@ -28,7 +28,7 @@ $canonical = $canonical ?? $canonicalBase . ($currentPath === '/' ? '
|
||||
$siteName = I18n::t('site.name', [], $locale);
|
||||
$ogTitle = $openGraph['ogTitle'] ?? $title;
|
||||
$ogDescription = $openGraph['ogDescription'] ?? $description;
|
||||
$ogImage = $openGraph['ogImage'] ?? 'https://haus-schleusingen.de/bilder/Aussenansicht-2.webp';
|
||||
$ogImage = $openGraph['ogImage'] ?? 'https://haus-schleusingen.de/bilder/Außenansicht-2.png';
|
||||
$ogUrl = $openGraph['ogUrl'] ?? $canonical;
|
||||
$hreflangs = Locale::hreflangAlternates($currentPath === '/' ? '/' : $currentPath, $canonicalBase);
|
||||
|
||||
@@ -72,7 +72,7 @@ $navItems = [
|
||||
<meta property="og:description" content="<?= htmlspecialchars($ogDescription ?? $pageDescription ?? $t('site.description'), ENT_QUOTES) ?>">
|
||||
<meta property="og:locale" content="<?= htmlspecialchars($ogLocale ?? Locale::toOgLocale($locale), ENT_QUOTES) ?>">
|
||||
<meta property="og:site_name" content="<?= htmlspecialchars($ogSiteName ?? $t('site.name'), ENT_QUOTES) ?>">
|
||||
<meta property="og:image" content="<?= htmlspecialchars($ogImage ?? ($t('site.canonical_base') . '/bilder/hero-bg.jpg'), ENT_QUOTES) ?>">
|
||||
<meta property="og:image" content="<?= htmlspecialchars($ogImage ?? ($t('site.canonical_base') . '/bilder/Außenansicht-2.png'), ENT_QUOTES) ?>">
|
||||
|
||||
<?php if (isset($structuredData)): ?>
|
||||
<script type="application/ld+json"><?= $structuredData ?></script>
|
||||
|
||||
Reference in New Issue
Block a user