tmpDir = sys_get_temp_dir().'/translate_test_'.uniqid(); $this->translationsDir = $this->tmpDir.'/translations'; mkdir($this->translationsDir, 0o755, true); } protected function tearDown(): void { $files = glob($this->translationsDir.'/*') ?: []; foreach ($files as $f) { unlink($f); } rmdir($this->translationsDir); rmdir($this->tmpDir); } private function createTester(HttpClientInterface $httpClient): CommandTester { $command = new TranslateCommand($httpClient, $this->tmpDir); $app = new Application(); $app->addCommand($command); return new CommandTester($app->find('app:translate')); } private function mockHttpClient(array $translateResponses = [], bool $apiAvailable = true): HttpClientInterface { $httpClient = $this->createMock(HttpClientInterface::class); $callIndex = 0; $httpClient->method('request')->willReturnCallback( function (string $method, string $url) use (&$callIndex, $translateResponses, $apiAvailable) { $response = $this->createMock(ResponseInterface::class); if ('GET' === $method && str_contains($url, '/languages')) { if (!$apiAvailable) { throw new \RuntimeException('Connection refused'); } $response->method('getStatusCode')->willReturn(200); return $response; } if ('POST' === $method && str_contains($url, '/translate')) { $translated = $translateResponses[$callIndex] ?? 'Translated text'; ++$callIndex; $response->method('toArray')->willReturn(['translatedText' => $translated]); return $response; } return $response; } ); return $httpClient; } public function testApiUnreachable(): void { $httpClient = $this->mockHttpClient([], false); $tester = $this->createTester($httpClient); $tester->execute(['--url' => 'http://fake:5000']); self::assertSame(1, $tester->getStatusCode()); self::assertStringContainsString('not reachable', $tester->getDisplay()); } public function testNoFrenchFiles(): void { $httpClient = $this->mockHttpClient(); $tester = $this->createTester($httpClient); $tester->execute(['--url' => 'http://fake:5000']); self::assertSame(0, $tester->getStatusCode()); self::assertStringContainsString('No French translation files', $tester->getDisplay()); } public function testEmptyFrenchFile(): void { file_put_contents($this->translationsDir.'/messages.fr.yaml', ''); $httpClient = $this->mockHttpClient(); $tester = $this->createTester($httpClient); $tester->execute(['--url' => 'http://fake:5000']); self::assertSame(0, $tester->getStatusCode()); self::assertStringContainsString('Skipped', $tester->getDisplay()); } public function testTranslatesNewKeys(): void { file_put_contents($this->translationsDir.'/messages.fr.yaml', Yaml::dump([ 'hello' => 'Bonjour', 'goodbye' => 'Au revoir', ])); // 2 keys × 4 languages = 8 translations $responses = array_fill(0, 8, null); $responses[0] = 'Hello'; $responses[1] = 'Goodbye'; $responses[2] = 'Hola'; $responses[3] = 'Adios'; $responses[4] = 'Hallo'; $responses[5] = 'Auf Wiedersehen'; $responses[6] = 'Ciao'; $responses[7] = 'Arrivederci'; $httpClient = $this->mockHttpClient($responses); $tester = $this->createTester($httpClient); $tester->execute(['--url' => 'http://fake:5000']); self::assertSame(0, $tester->getStatusCode()); self::assertStringContainsString('Translated 2 new keys', $tester->getDisplay()); // Check EN file $enData = Yaml::parseFile($this->translationsDir.'/messages.en.yaml'); self::assertSame('Hello', $enData['hello']); self::assertSame('Goodbye', $enData['goodbye']); // Check ES file $esData = Yaml::parseFile($this->translationsDir.'/messages.es.yaml'); self::assertSame('Hola', $esData['hello']); // Check DE file self::assertFileExists($this->translationsDir.'/messages.de.yaml'); // Check IT file self::assertFileExists($this->translationsDir.'/messages.it.yaml'); } public function testSkipsExistingKeys(): void { file_put_contents($this->translationsDir.'/messages.fr.yaml', Yaml::dump([ 'hello' => 'Bonjour', 'new_key' => 'Nouveau', ])); file_put_contents($this->translationsDir.'/messages.en.yaml', Yaml::dump([ 'hello' => 'Hello (manual)', ])); // Only new_key needs translating for EN, then 2 keys for ES/DE/IT = 7 $responses = []; $responses[0] = 'New'; // new_key → EN $responses[1] = 'Hola'; // hello → ES $responses[2] = 'Nuevo'; // new_key → ES $responses[3] = 'Hallo'; // hello → DE $responses[4] = 'Neu'; // new_key → DE $responses[5] = 'Ciao'; // hello → IT $responses[6] = 'Nuovo'; // new_key → IT $httpClient = $this->mockHttpClient($responses); $tester = $this->createTester($httpClient); $tester->execute(['--url' => 'http://fake:5000']); self::assertSame(0, $tester->getStatusCode()); $enData = Yaml::parseFile($this->translationsDir.'/messages.en.yaml'); self::assertSame('Hello (manual)', $enData['hello']); self::assertSame('New', $enData['new_key']); } public function testRemovesObsoleteKeys(): void { file_put_contents($this->translationsDir.'/messages.fr.yaml', Yaml::dump([ 'hello' => 'Bonjour', ])); file_put_contents($this->translationsDir.'/messages.en.yaml', Yaml::dump([ 'hello' => 'Hello', 'removed_key' => 'This was removed from FR', ])); $httpClient = $this->mockHttpClient(); $tester = $this->createTester($httpClient); $tester->execute(['--url' => 'http://fake:5000']); self::assertSame(0, $tester->getStatusCode()); $enData = Yaml::parseFile($this->translationsDir.'/messages.en.yaml'); self::assertArrayHasKey('hello', $enData); self::assertArrayNotHasKey('removed_key', $enData); self::assertStringContainsString('Removed 1 obsolete', $tester->getDisplay()); } public function testAlreadyUpToDate(): void { file_put_contents($this->translationsDir.'/messages.fr.yaml', Yaml::dump([ 'hello' => 'Bonjour', ])); foreach (['en', 'es', 'de', 'it'] as $lang) { file_put_contents($this->translationsDir.'/messages.'.$lang.'.yaml', Yaml::dump([ 'hello' => 'Translated', ])); } $httpClient = $this->mockHttpClient(); $tester = $this->createTester($httpClient); $tester->execute(['--url' => 'http://fake:5000']); self::assertSame(0, $tester->getStatusCode()); self::assertStringContainsString('Already up to date', $tester->getDisplay()); } public function testPreservesPlaceholders(): void { file_put_contents($this->translationsDir.'/messages.fr.yaml', Yaml::dump([ 'retry' => 'Reessayez dans %minutes% minutes.', ])); // LibreTranslate might mangle the placeholder $responses = [ 'Try again in % minutes % minutes.', // EN - mangled 'Inténtelo de nuevo en %minutes% minutos.', // ES - kept 'Versuchen Sie es in % minuten % Minuten erneut.', // DE - mangled 'Riprova tra %minutes% minuti.', // IT - kept ]; $httpClient = $this->mockHttpClient($responses); $tester = $this->createTester($httpClient); $tester->execute(['--url' => 'http://fake:5000']); self::assertSame(0, $tester->getStatusCode()); $esData = Yaml::parseFile($this->translationsDir.'/messages.es.yaml'); self::assertStringContainsString('%minutes%', $esData['retry']); } public function testTranslationApiFailureFallbacksToOriginal(): void { file_put_contents($this->translationsDir.'/messages.fr.yaml', Yaml::dump([ 'error' => 'Une erreur est survenue.', ])); $httpClient = $this->createMock(HttpClientInterface::class); $callCount = 0; $httpClient->method('request')->willReturnCallback( function (string $method, string $url) use (&$callCount) { $response = $this->createMock(ResponseInterface::class); if ('GET' === $method && str_contains($url, '/languages')) { $response->method('getStatusCode')->willReturn(200); return $response; } if ('POST' === $method && str_contains($url, '/translate')) { ++$callCount; throw new \RuntimeException('API error'); } return $response; } ); $tester = $this->createTester($httpClient); $tester->execute(['--url' => 'http://fake:5000']); self::assertSame(0, $tester->getStatusCode()); self::assertStringContainsString('Failed to translate', $tester->getDisplay()); // Fallback: original FR text used $enData = Yaml::parseFile($this->translationsDir.'/messages.en.yaml'); self::assertSame('Une erreur est survenue.', $enData['error']); } public function testMultipleDomainFiles(): void { file_put_contents($this->translationsDir.'/messages.fr.yaml', Yaml::dump([ 'hello' => 'Bonjour', ])); file_put_contents($this->translationsDir.'/security.fr.yaml', Yaml::dump([ 'login' => 'Connexion', ])); $httpClient = $this->mockHttpClient(array_fill(0, 8, 'Translated')); $tester = $this->createTester($httpClient); $tester->execute(['--url' => 'http://fake:5000']); self::assertSame(0, $tester->getStatusCode()); self::assertFileExists($this->translationsDir.'/messages.en.yaml'); self::assertFileExists($this->translationsDir.'/security.en.yaml'); self::assertFileExists($this->translationsDir.'/messages.it.yaml'); self::assertFileExists($this->translationsDir.'/security.it.yaml'); } public function testKeysOrderMatchesFrench(): void { file_put_contents($this->translationsDir.'/messages.fr.yaml', Yaml::dump([ 'aaa' => 'Premier', 'zzz' => 'Dernier', 'mmm' => 'Milieu', ])); $responses = ['First', 'Last', 'Middle']; // Repeat for 4 languages $allResponses = array_merge($responses, $responses, $responses, $responses); $httpClient = $this->mockHttpClient($allResponses); $tester = $this->createTester($httpClient); $tester->execute(['--url' => 'http://fake:5000']); $enData = Yaml::parseFile($this->translationsDir.'/messages.en.yaml'); $keys = array_keys($enData); self::assertSame(['aaa', 'zzz', 'mmm'], $keys); } }