tmpDir = sys_get_temp_dir() . '/i18n-test-' . bin2hex(random_bytes(4)); mkdir($this->tmpDir); I18n::setLocalesPath($this->tmpDir); I18n::flushCache(); } protected function tearDown(): void { I18n::flushCache(); I18n::setLocalesPath(dirname(__DIR__, 2) . '/app/Locales'); if (is_dir($this->tmpDir)) { foreach (glob($this->tmpDir . '/*') as $f) { @unlink($f); } @rmdir($this->tmpDir); } } private function writeLocale(string $code, array $data): void { $content = 'tmpDir . '/' . $code . '.php', $content); } // ────────────────────────────────────────────── // t(): basic lookup // ────────────────────────────────────────────── public function testReturnsKeyWhenNoLocalesExist(): void { $this->assertSame('missing.key', I18n::t('missing.key')); } public function testReturnsKeyWhenLocaleFileMissing(): void { // Only DE file exists $this->writeLocale('de', ['hello' => 'Hallo']); $this->assertSame('missing.key', I18n::t('missing.key', [], 'en')); } public function testReturnsKeyWhenKeyMissingInAllLocales(): void { $this->writeLocale('de', ['hello' => 'Hallo']); $this->writeLocale('en', ['other' => 'Other']); $this->assertSame('greeting', I18n::t('greeting', [], 'en')); } public function testFallsBackToDeWhenKeyMissingInCurrentLocale(): void { $this->writeLocale('de', ['nav.home' => 'Start']); $this->writeLocale('en', ['other.key' => 'Other']); $this->assertSame('Start', I18n::t('nav.home', [], 'en')); } public function testReturnsValueInCurrentLocale(): void { $this->writeLocale('de', ['greeting' => 'Hallo']); $this->writeLocale('en', ['greeting' => 'Hello']); $this->assertSame('Hallo', I18n::t('greeting', [], 'de')); $this->assertSame('Hello', I18n::t('greeting', [], 'en')); } // ────────────────────────────────────────────── // t(): placeholders // ────────────────────────────────────────────── public function testInterpolatesPlaceholders(): void { $this->writeLocale('de', ['welcome' => 'Willkommen, {name}!']); $this->assertSame( 'Willkommen, Martin!', I18n::t('welcome', ['name' => 'Martin'], 'de') ); } public function testInterpolatesMultiplePlaceholders(): void { $this->writeLocale('de', ['mail' => '{greeting}, deine Bestellung #{order} ist da.']); $this->assertSame( 'Hi, deine Bestellung #42 ist da.', I18n::t('mail', ['greeting' => 'Hi', 'order' => '42'], 'de') ); } public function testLeavesUnreplacedPlaceholdersAlone(): void { $this->writeLocale('de', ['x' => 'Hallo {name}']); $this->assertSame('Hallo {name}', I18n::t('x', [], 'de')); } // ────────────────────────────────────────────── // t(): default locale behavior // ────────────────────────────────────────────── public function testDefaultsToDeLocaleWhenNoneSpecified(): void { $this->writeLocale('de', ['greeting' => 'Hallo']); $this->assertSame('Hallo', I18n::t('greeting')); } public function testRejectsUnsupportedLocaleAndReturnsKey(): void { $this->writeLocale('de', ['greeting' => 'Hallo']); $this->assertSame('greeting', I18n::t('greeting', [], 'fr')); } // ────────────────────────────────────────────── // tn(): plural variants (MVP: {n} interpolation) // ────────────────────────────────────────────── public function testTnPicksSingularForOne(): void { $this->writeLocale('de', [ 'room.singular' => '1 Zimmer', 'room.plural' => '{n} Zimmer', ]); $this->assertSame('1 Zimmer', I18n::tn('room.singular', 'room.plural', 1, [], 'de')); } public function testTnPicksPluralForOtherNumbers(): void { $this->writeLocale('de', [ 'room.singular' => '1 Zimmer', 'room.plural' => '{n} Zimmer', ]); $this->assertSame('6 Zimmer', I18n::tn('room.singular', 'room.plural', 6, [], 'de')); $this->assertSame('0 Zimmer', I18n::tn('room.singular', 'room.plural', 0, [], 'de')); } // ────────────────────────────────────────────── // has() // ────────────────────────────────────────────── public function testHasReturnsTrueForExistingKey(): void { $this->writeLocale('de', ['greeting' => 'Hallo']); $this->writeLocale('en', ['greeting' => 'Hello']); $this->assertTrue(I18n::has('greeting', 'en')); $this->assertTrue(I18n::has('greeting', 'de')); } public function testHasReturnsTrueForFallbackKey(): void { $this->writeLocale('de', ['only_de' => 'Nur DE']); $this->assertTrue(I18n::has('only_de', 'en')); } public function testHasReturnsFalseForMissingKey(): void { $this->writeLocale('de', ['x' => 'X']); $this->assertFalse(I18n::has('nope', 'de')); } // ────────────────────────────────────────────── // Caching // ────────────────────────────────────────────── public function testCacheSurvivesAcrossCalls(): void { $this->writeLocale('de', ['k' => 'v1']); $this->assertSame('v1', I18n::t('k', [], 'de')); // Mutate the file — cached value should still be returned $this->writeLocale('de', ['k' => 'v2']); $this->assertSame('v1', I18n::t('k', [], 'de')); // Flush — now we see the new value I18n::flushCache(); $this->assertSame('v2', I18n::t('k', [], 'de')); } public function testFlushCacheClearsAllLocales(): void { $this->writeLocale('de', ['k' => 'de-v']); $this->writeLocale('en', ['k' => 'en-v']); I18n::t('k', [], 'en'); I18n::flushCache(); $this->writeLocale('en', ['k' => 'en-v2']); $this->assertSame('en-v2', I18n::t('k', [], 'en')); } }