Multi-Language MVP: 4 Locales (DE/EN/UK/RU) mit SSR + UI + Tests #79

Merged
greggy merged 27 commits from feature/multilanguage-mvp into main 2026-06-05 23:49:39 +02:00
3 changed files with 34 additions and 34 deletions
Showing only changes of commit acaea97415 - Show all commits

View File

@@ -110,7 +110,11 @@ final class LocaleSwitcher
public static function flagImg(string $locale): string public static function flagImg(string $locale): string
{ {
$src = self::flagSource($locale); $src = self::flagSource($locale);
return '<img class="flag" src="' . $src . '" alt="" width="24" height="18" loading="lazy" decoding="async">'; // 32×24 = ~4:3, large enough that the flag is the visual
// anchor of the option. No loading="lazy" — these are 4
// small SVGs that must be ready the moment the <details>
// opens (lazy would cause a flash of empty boxes).
return '<img class="flag" src="' . $src . '" alt="" width="32" height="24">';
} }
/** /**

View File

@@ -1476,7 +1476,14 @@ footer {
} }
} }
/* LOCALE SWITCHER — single <details> dropdown, flag-sized trigger */ /* LOCALE SWITCHER — single <details> dropdown, flag-sized trigger.
Design goals (after Martin-feedback round 3):
- The flag is the visual anchor of every row, not a tiny icon
drowning in padding.
- Trigger is a 44×44 touch target with the flag centred, no
artificial 1px outline (real flag-icons need no border).
- Menu rows are 40px+ tall with comfortable flag+label spacing.
- No FOUC on open: SVGs load eager (4 small files). */
.locale-switcher { .locale-switcher {
position: relative; position: relative;
display: inline-block; display: inline-block;
@@ -1485,15 +1492,14 @@ footer {
.locale-switcher__trigger { .locale-switcher__trigger {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.25rem; gap: 4px;
padding: 14px 8px; padding: 6px;
min-height: 44px;
border: none; border: none;
border-radius: 4px; border-radius: 6px;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
list-style: none; list-style: none;
min-height: 44px;
min-width: 44px;
color: var(--dark); color: var(--dark);
transition: transition:
background 0.2s, background 0.2s,
@@ -1516,11 +1522,10 @@ footer {
} }
.locale-switcher__caret { .locale-switcher__caret {
font-size: 0.65rem; font-size: 0.7rem;
line-height: 1; line-height: 1;
color: inherit; color: inherit;
transition: transform 0.2s ease; transition: transform 0.2s ease;
margin-left: 2px;
} }
.locale-switcher[open] .locale-switcher__caret { .locale-switcher[open] .locale-switcher__caret {
@@ -1533,25 +1538,27 @@ footer {
right: 0; right: 0;
min-width: 180px; min-width: 180px;
margin: 0; margin: 0;
padding: 0.4rem; padding: 6px;
list-style: none; list-style: none;
background: var(--white); background: var(--white);
border: 1px solid var(--warm); border: 1px solid var(--warm);
border-radius: 8px; border-radius: 10px;
box-shadow: 0 8px 24px rgb(0 0 0 / 14%); box-shadow:
0 1px 2px rgb(0 0 0 / 6%),
0 8px 24px rgb(0 0 0 / 14%);
z-index: 60; z-index: 60;
} }
.locale-switcher__option { .locale-switcher__option {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.6rem; gap: 10px;
padding: 0.6rem 0.7rem; padding: 8px 10px;
border-radius: 4px; border-radius: 6px;
text-decoration: none; text-decoration: none;
color: var(--dark); color: var(--dark);
font-size: 0.85rem; font-size: 0.9rem;
min-height: 44px; font-weight: 500;
} }
.locale-switcher__option.is-current { .locale-switcher__option.is-current {
@@ -1566,24 +1573,13 @@ footer {
outline: none; outline: none;
} }
/* Flag is the visual anchor: 32×24, no border, no rounded corners
(flags look better as crisp rectangles than as pills). */
.locale-switcher .flag { .locale-switcher .flag {
width: 24px; width: 32px;
height: 18px; height: 24px;
flex: 0 0 24px; flex: 0 0 32px;
border-radius: 2px;
box-shadow: 0 0 0 1px rgb(0 0 0 / 15%);
display: block; display: block;
object-fit: cover;
}
.locale-switcher__option .flag {
width: 24px;
height: 18px;
flex: 0 0 24px;
border-radius: 2px;
box-shadow: 0 0 0 1px rgb(0 0 0 / 15%);
display: block;
object-fit: cover;
} }
.locale-switcher__label { .locale-switcher__label {

View File

@@ -111,7 +111,7 @@ final class LocaleSwitcherTest extends TestCase
$img = LocaleSwitcher::flagImg($code); $img = LocaleSwitcher::flagImg($code);
self::assertStringStartsWith('<img', $img); self::assertStringStartsWith('<img', $img);
self::assertStringContainsString('class="flag"', $img); self::assertStringContainsString('class="flag"', $img);
self::assertStringContainsString('width="24" height="18"', $img); self::assertStringContainsString('width="32" height="24"', $img);
self::assertStringContainsString('alt=""', $img); self::assertStringContainsString('alt=""', $img);
self::assertStringEndsWith('>', $img); self::assertStringEndsWith('>', $img);
} }