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

@@ -0,0 +1,71 @@
<?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) . '&amp;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;
}
}