assertNotFalse($root, 'Project root not found.'); $paths = [ $root . '/pages', $root . '/templates', $root . '/lib', $root . '/web/js', ]; $extensions = ['php', 'phtml', 'js']; $usedKeys = []; foreach ($paths as $path) { if (!is_dir($path)) { continue; } $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); foreach ($iterator as $file) { if (!$file->isFile()) { continue; } $ext = strtolower((string) $file->getExtension()); if (!in_array($ext, $extensions, true)) { continue; } $content = file_get_contents($file->getPathname()); if ($content === false || $content === '') { continue; } foreach ($this->extractTranslationKeys($content) as $key) { $usedKeys[$key] = true; } } } $usedKeys = array_keys($usedKeys); sort($usedKeys); $locales = [ 'default_de.json' => $root . '/i18n/default_de.json', 'default_en.json' => $root . '/i18n/default_en.json', ]; $translations = []; foreach ($locales as $name => $path) { $this->assertFileExists($path, 'Missing locale file: ' . $name); $raw = file_get_contents($path); $this->assertNotFalse($raw, 'Failed to read locale file: ' . $name); $json = json_decode($raw, true); $this->assertIsArray($json, 'Invalid JSON in locale file: ' . $name); $translations[$name] = $json; } $missing = []; foreach ($usedKeys as $key) { foreach ($translations as $name => $map) { if (!array_key_exists($key, $map)) { $missing[$name][] = $key; } } } $messages = []; foreach ($missing as $name => $keys) { $messages[] = $name . ' missing: ' . implode(', ', $keys); } $this->assertSame([], $missing, implode(' | ', $messages)); } private function extractTranslationKeys(string $content): array { $keys = []; // Match t('...') with escaped quotes inside, and the same for t("..."). $patterns = [ "/\\bt\\s*\\(\\s*'((?:\\\\\\\\.|[^'\\\\\\\\])*)'\\s*\\)/", '/\\bt\\s*\\(\\s*"((?:\\\\\\\\.|[^"\\\\\\\\])*)"\\s*\\)/', ]; foreach ($patterns as $pattern) { if (!preg_match_all($pattern, $content, $matches)) { continue; } foreach ($matches[1] as $match) { $keys[] = stripcslashes($match); } } return $keys; } }