Files
landingpage-haus-schleusingen/app/Controllers/LocaleSwitcher.php
Hermes 7dd8023222 -m
2026-06-04 17:23:49 +00:00

184 lines
7.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\I18n;
use App\Core\Locale;
/**
* Renders the language switcher widget. Pure HTML generation — no
* side effects, no header writing.
*
* Each option gets:
* - an inline 24×16 SVG flag (sub-Issue D),
* - `hreflang` and `lang` for SEO and screen readers,
* - `aria-current="true"` on the active option.
*
* Active option is rendered as a <span> (not a link) so it cannot
* be reactivated. All options 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',
);
$html = '<div class="locale-switcher-wrapper">';
$html .= '<ul class="locale-switcher" role="list" aria-label="' . $ariaLabel . '">';
foreach (Locale::SUPPORTED as $code) {
$isCurrent = $code === $this->currentLocale;
$name = htmlspecialchars(
I18n::t('locale.' . $code, [], $this->currentLocale),
ENT_QUOTES,
'UTF-8',
);
$codeAttr = htmlspecialchars($code, ENT_QUOTES, 'UTF-8');
$flag = self::flagSvg($code);
$classes = 'locale-switcher__option';
if ($isCurrent) {
$classes .= ' is-current';
}
$html .= '<li class="locale-switcher__item">';
if ($isCurrent) {
$html .= '<span class="' . $classes . '" aria-current="true" lang="' . $codeAttr . '">'
. $flag
. '<span class="locale-switcher__label">' . $name . '</span>'
. '</span>';
} else {
$url = '/locale?set=' . rawurlencode($code) . '&amp;return=' . rawurlencode($path);
$html .= '<a class="' . $classes . '"'
. ' href="' . $url . '"'
. ' hreflang="' . $codeAttr . '"'
. ' lang="' . $codeAttr . '"'
. ' rel="alternate"'
. '>'
. $flag
. '<span class="locale-switcher__label">' . $name . '</span>'
. '</a>';
}
$html .= '</li>';
}
$html .= '</ul>';
// Mobile dropdown — compact single-trigger switcher for narrow viewports
$currentName = htmlspecialchars(
I18n::t('locale.' . $this->currentLocale, [], $this->currentLocale),
ENT_QUOTES,
'UTF-8',
);
$currentCode = htmlspecialchars($this->currentLocale, ENT_QUOTES, 'UTF-8');
$currentFlag = self::flagSvg($this->currentLocale);
$html .= '<details class="locale-switcher-mobile">';
$html .= '<summary class="locale-switcher-mobile__trigger" aria-label="' . $ariaLabel . '">';
$html .= '<span class="locale-switcher-mobile__current" lang="' . $currentCode . '">';
$html .= $currentFlag;
$html .= '<span class="locale-switcher-mobile__current-code">' . strtoupper($currentCode) . '</span>';
$html .= '</span>';
$html .= '<span class="locale-switcher-mobile__caret" aria-hidden="true">▾</span>';
$html .= '</summary>';
$html .= '<ul class="locale-switcher-mobile__menu" role="list">';
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::flagSvg($code);
$html .= '<li>';
if ($isCurrent) {
$html .= '<span class="locale-switcher-mobile__option is-current" aria-current="true" lang="' . $codeAttr . '">'
. $flag . '<span>' . $name . '</span></span>';
} else {
$url = '/locale?set=' . rawurlencode($code) . '&amp;return=' . rawurlencode($path);
$html .= '<a class="locale-switcher-mobile__option"'
. ' href="' . $url . '"'
. ' hreflang="' . $codeAttr . '"'
. ' lang="' . $codeAttr . '"'
. ' rel="alternate"'
. '>'
. $flag . '<span>' . $name . '</span></a>';
}
$html .= '</li>';
}
$html .= '</ul>';
$html .= '</details>';
$html .= '</div>';
return $html;
}
/**
* Inline 24×16 SVG for the four supported locales.
*
* - DE: black/red/gold horizontal stripes (Germany)
* - EN: simplified Union Jack (en-GB per ADR-002)
* - UK: blue/yellow horizontal stripes (Ukraine)
* - RU: white/blue/red horizontal stripes (Russia)
*
* Decorative: marked `aria-hidden="true"` + `focusable="false"`.
* The full accessible name is conveyed by the visible label and
* the <a>'s hreflang/lang.
*/
public static function flagSvg(string $locale): string
{
$svg = match ($locale) {
'de' => '<svg class="flag" viewBox="0 0 24 16" aria-hidden="true" focusable="false">'
. '<rect width="24" height="5.33" fill="#000"/>'
. '<rect y="5.33" width="24" height="5.34" fill="#DD0000"/>'
. '<rect y="10.67" width="24" height="5.33" fill="#FFCC00"/>'
. '</svg>',
'en' => '<svg class="flag" viewBox="0 0 24 16" aria-hidden="true" focusable="false">'
. '<rect width="24" height="16" fill="#012169"/>'
. '<path d="M0,0 L24,16 M24,0 L0,16" stroke="#fff" stroke-width="2.4"/>'
. '<path d="M0,0 L24,16 M24,0 L0,16" stroke="#C8102E" stroke-width="1.2"/>'
. '<path d="M12,0 V16 M0,8 H24" stroke="#fff" stroke-width="3.2"/>'
. '<path d="M12,0 V16 M0,8 H24" stroke="#C8102E" stroke-width="1.6"/>'
. '</svg>',
'uk' => '<svg class="flag" viewBox="0 0 24 16" aria-hidden="true" focusable="false">'
. '<rect width="24" height="8" fill="#005BBB"/>'
. '<rect y="8" width="24" height="8" fill="#FFD500"/>'
. '</svg>',
'ru' => '<svg class="flag" viewBox="0 0 24 16" aria-hidden="true" focusable="false">'
. '<rect width="24" height="5.33" fill="#fff"/>'
. '<rect y="5.33" width="24" height="5.34" fill="#0039A6"/>'
. '<rect y="10.67" width="24" height="5.33" fill="#D52B1E"/>'
. '</svg>',
default => '<svg class="flag" viewBox="0 0 24 16" aria-hidden="true" focusable="false">'
. '<rect width="24" height="16" fill="#888"/>'
. '</svg>',
};
return $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;
}
}