*/ public static function moduleProvider(): array { $root = realpath(__DIR__ . '/../..'); if ($root === false) { return []; } $modules = []; foreach (glob($root . '/modules/*/module.php') as $manifestFile) { $raw = (static function (string $path): mixed { return require $path; })($manifestFile); if (!is_array($raw)) { continue; } $i18nPath = $raw['i18n_path'] ?? null; if (!is_string($i18nPath) || trim($i18nPath) === '') { continue; } $moduleDir = dirname($manifestFile); $moduleId = basename($moduleDir); $resolvedPath = rtrim($moduleDir, '/') . '/' . ltrim(trim($i18nPath), '/'); $modules[$moduleId] = [$moduleId, $resolvedPath]; } return $modules; } #[DataProvider('moduleProvider')] public function testModuleI18nFilesExistAndAreValidJson(string $moduleId, string $i18nPath): void { $this->assertDirectoryExists($i18nPath, "Module '{$moduleId}' declares i18n_path but directory does not exist."); $deFile = $i18nPath . '/default_de.json'; $enFile = $i18nPath . '/default_en.json'; $this->assertFileExists($deFile, "Module '{$moduleId}' is missing default_de.json"); $this->assertFileExists($enFile, "Module '{$moduleId}' is missing default_en.json"); $deRaw = file_get_contents($deFile); $enRaw = file_get_contents($enFile); $this->assertNotFalse($deRaw); $this->assertNotFalse($enRaw); $this->assertIsArray(json_decode($deRaw, true), "Module '{$moduleId}' default_de.json is not valid JSON"); $this->assertIsArray(json_decode($enRaw, true), "Module '{$moduleId}' default_en.json is not valid JSON"); } #[DataProvider('moduleProvider')] public function testModuleI18nKeyParity(string $moduleId, string $i18nPath): void { $de = $this->loadModuleKeys($i18nPath . '/default_de.json'); $en = $this->loadModuleKeys($i18nPath . '/default_en.json'); $missingInEn = array_values(array_diff($de, $en)); $this->assertSame( [], $missingInEn, "Module '{$moduleId}' default_de.json keys missing from default_en.json:\n" . implode("\n", $missingInEn) ); $missingInDe = array_values(array_diff($en, $de)); $this->assertSame( [], $missingInDe, "Module '{$moduleId}' default_en.json keys missing from default_de.json:\n" . implode("\n", $missingInDe) ); } #[DataProvider('moduleProvider')] public function testModuleI18nValuesAreNonEmpty(string $moduleId, string $i18nPath): void { foreach (['default_de.json', 'default_en.json'] as $file) { $path = $i18nPath . '/' . $file; if (!is_file($path)) { continue; } $data = json_decode(file_get_contents($path), true); $this->assertIsArray($data); $empty = array_keys(array_filter($data, static fn ($v): bool => trim((string) $v) === '')); $this->assertSame( [], $empty, "Module '{$moduleId}' {$file} has empty values:\n" . implode("\n", $empty) ); } } /** * @return list */ private function loadModuleKeys(string $path): array { if (!is_file($path)) { return []; } $data = json_decode(file_get_contents($path), true); if (!is_array($data)) { return []; } $keys = array_keys($data); sort($keys, SORT_STRING); return $keys; } }