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