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(); } }