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 = '
';
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 .= '- ';
if ($isCurrent) {
$html .= '' . $codeAttr . ' (' . $name . ')';
} else {
$url = '/locale?set=' . rawurlencode($code) . '&return=' . rawurlencode($path);
$html .= ''
. $codeAttr
. ' (' . $name . ')'
. '';
}
$html .= '
';
}
$html .= '
';
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;
}
}