value]
*/
private function renderHomeIn(string $locale, array $formErrors = [], array $formFieldErrors = [], bool $formSuccess = false): array
{
// Sanity: the locale must be one of the supported ones.
self::assertTrue(Locale::isSupported($locale), "Unsupported locale for render: $locale");
$t = static function (string $key) use ($locale): string {
return I18n::t($key, [], $locale);
};
$formData = [
'fname' => '',
'lname' => '',
'email' => '',
'phone' => '',
'interest' => 'visit',
'message' => '',
];
$interestKeys = [
'visit' => 'form.interest.visit',
'info' => 'form.interest.info',
'apply' => 'form.interest.apply',
];
$escapeContactValue = static fn(string $value): string
=> htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
// Empty honeypot/csrf/session.
$_SESSION = [
'csrf_token' => 'csrf-test-token',
'form_start_time' => time(),
];
$viewPath = dirname(__DIR__, 2) . '/app/views/home/index.php';
self::assertFileExists($viewPath);
ob_start();
include $viewPath;
$body = (string) ob_get_clean();
// Layout prefix: open ... via a tiny shim.
$layout = sprintf(
'
%s%s',
htmlspecialchars($locale, ENT_QUOTES),
htmlspecialchars($t('hero.h1.line1') . ' ' . $t('hero.h1.line2'), ENT_QUOTES),
$body,
);
return [$layout, $locale];
}
/**
* Each locale's home render must contain the locale-specific
* translation keys (and they must be the locale's expected text).
*/
#[Test]
#[DataProvider('supportedLocaleProvider')]
public function homeRendersLocaleSpecificHeroCopy(string $locale): void
{
[$html] = $this->renderHomeIn($locale);
// Per-locale hero h1 line 1 — strong, locale-specific token.
$expectedLine1 = I18n::t('hero.h1.line1', [], $locale);
self::assertNotSame('hero.h1.line1', $expectedLine1, "Translation missing for hero.h1.line1 in $locale");
self::assertStringContainsString(
$expectedLine1,
$html,
"Expected hero line 1 ($expectedLine1) not rendered in $locale"
);
// Hero tag
$expectedTag = I18n::t('hero.tag', [], $locale);
self::assertStringContainsString($expectedTag, $html, "Hero tag not rendered in $locale");
}
/**
* `` must match the active locale so screen readers,
* search engines, and the browser's auto-translate UI behave.
*/
#[Test]
#[DataProvider('supportedLocaleProvider')]
public function htmlRootLangAttributeMatchesActiveLocale(string $locale): void
{
[$html] = $this->renderHomeIn($locale);
self::assertMatchesRegularExpression(
'~ to $locale"
);
}
/**
* Regression guard: a hardcoded DE string that's not a proper noun
* (e.g. "Einfamilienhaus") must NOT appear in the EN/UK/RU render.
*/
#[Test]
#[DataProvider('nonGermanLocaleProvider')]
public function nonGermanRenderDoesNotLeakGermanCopy(string $locale): void
{
[$html] = $this->renderHomeIn($locale);
$germanOnly = [
'Einfamilienhaus', // hero.h1.line2 DE
'Großzügiges', // hero.h1.line1 DE (with ß)
'Entdecken', // hero.discover DE
'Galerie', // nav.gallery DE
];
foreach ($germanOnly as $needle) {
self::assertStringNotContainsString(
$needle,
$html,
"German copy \"$needle\" leaked into $locale render"
);
}
}
/**
* Switcher widget: with the active locale, that locale's flag/link
* must carry `aria-current="true"` per a11y contract.
*/
#[Test]
#[DataProvider('supportedLocaleProvider')]
public function localeSwitcherMarksActiveLocaleWithAriaCurrent(string $locale): void
{
$switcherHtml = (new LocaleSwitcher($locale, '/'))->render();
self::assertStringContainsString('aria-current="true"', $switcherHtml, "Active locale $locale should have aria-current");
// The active locale's link must point at itself (relative path stays on the page).
self::assertMatchesRegularExpression(
'~aria-current="true"[^>]*>.*?'
. preg_quote(I18n::t('locale.' . $locale, [], $locale), '~')
. '~s',
$switcherHtml,
"Active locale $locale not properly labelled in switcher"
);
}
/**
* All four locales should produce roughly the same DOM skeleton
* (same section ids, same form structure) — translation is
* content swap, not structural drift.
*/
#[Test]
public function homeDomSkeletonIsStableAcrossLocales(): void
{
$skeletons = [];
foreach (Locale::SUPPORTED as $locale) {
[$html] = $this->renderHomeIn($locale);
// Pull out section/landmark ids + a couple of structural tags.
preg_match_all(
'~<(?:section|main|header|footer|nav|aside|form)\b[^>]*(?:\bid="([^"]+)")?~',
$html,
$matches
);
$skeletons[$locale] = $matches[0];
}
// All four skeletons must have the same number of structural tags.
$counts = array_map('count', $skeletons);
$unique = array_unique($counts);
self::assertCount(1, $unique, 'DOM skeleton size differs across locales: ' . json_encode($counts));
}
public static function supportedLocaleProvider(): array
{
$out = [];
foreach (Locale::SUPPORTED as $locale) {
$out[$locale] = [$locale];
}
return $out;
}
public static function nonGermanLocaleProvider(): array
{
$out = [];
foreach (Locale::SUPPORTED as $locale) {
if ($locale === Locale::DEFAULT) {
continue;
}
$out[$locale] = [$locale];
}
return $out;
}
protected function setUp(): void
{
// Make sure the I18n cache is fresh per test.
I18n::flushCache();
parent::setUp();
}
}