Files
breadcrumb-the-shire/tests/I18n/TranslationKeysTest.php

237 lines
7.4 KiB
PHP
Raw Normal View History

2026-02-11 19:28:12 +01:00
<?php
namespace MintyPHP\Tests\I18n;
use PHPUnit\Framework\TestCase;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
class TranslationKeysTest extends TestCase
{
public function testAllTranslationKeysExistInDefaultLocales(): void
{
$root = realpath(__DIR__ . '/../..');
$this->assertNotFalse($root, 'Project root not found.');
// Core scan paths
$corePaths = [
2026-02-11 19:28:12 +01:00
$root . '/pages',
$root . '/templates',
$root . '/lib',
$root . '/web/js',
];
$extensions = ['php', 'phtml', 'js'];
// Collect core keys
$coreKeys = $this->collectKeysFromPaths($corePaths, $extensions);
// Load core translations
$coreTranslations = $this->loadTranslationMap($root . '/i18n', ['default_de.json', 'default_en.json']);
// Also load all module translations so core keys provided by modules are available
$moduleTranslations = $this->loadAllModuleTranslations($root . '/modules');
$mergedTranslations = [];
foreach ($coreTranslations as $name => $map) {
$mergedTranslations[$name] = $map;
}
foreach ($moduleTranslations as $name => $maps) {
foreach ($maps as $locale => $map) {
$mergedTranslations[$locale] = array_merge($mergedTranslations[$locale] ?? [], $map);
}
}
// Validate core keys exist in merged (core + module) translations
$missing = [];
foreach ($coreKeys as $key) {
foreach ($mergedTranslations 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));
}
public function testAllModuleTranslationKeysExist(): void
{
$root = realpath(__DIR__ . '/../..');
$this->assertNotFalse($root, 'Project root not found.');
$modulesDir = $root . '/modules';
if (!is_dir($modulesDir)) {
$this->markTestSkipped('No modules directory found.');
}
$extensions = ['php', 'phtml', 'js'];
// Load core translations as fallback
$coreTranslations = $this->loadTranslationMap($root . '/i18n', ['default_de.json', 'default_en.json']);
$allMissing = [];
foreach (glob($modulesDir . '/*/module.php') as $manifestFile) {
$moduleDir = dirname($manifestFile);
$moduleId = basename($moduleDir);
// Scan module directories for keys
$modulePaths = [
$moduleDir . '/pages',
$moduleDir . '/templates',
$moduleDir . '/lib',
$moduleDir . '/web',
];
$moduleKeys = $this->collectKeysFromPaths($modulePaths, $extensions);
if ($moduleKeys === []) {
continue;
}
// Load module translations and merge with core
$moduleI18nDir = $moduleDir . '/i18n';
$moduleTranslations = is_dir($moduleI18nDir)
? $this->loadTranslationMap($moduleI18nDir, ['default_de.json', 'default_en.json'])
: [];
$merged = [];
foreach ($coreTranslations as $locale => $map) {
$merged[$locale] = array_merge($map, $moduleTranslations[$locale] ?? []);
}
foreach ($moduleKeys as $key) {
foreach ($merged as $locale => $map) {
if (!array_key_exists($key, $map)) {
$allMissing["modules/{$moduleId}{$locale}"][] = $key;
}
}
}
}
$messages = [];
foreach ($allMissing as $context => $keys) {
$messages[] = $context . ' missing: ' . implode(', ', $keys);
}
$this->assertSame([], $allMissing, implode(' | ', $messages));
}
/**
* @param list<string> $paths
* @param list<string> $extensions
* @return list<string>
*/
private function collectKeysFromPaths(array $paths, array $extensions): array
{
2026-02-11 19:28:12 +01:00
$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;
}
}
}
$keys = array_keys($usedKeys);
sort($keys);
2026-02-11 19:28:12 +01:00
return $keys;
}
/**
* @param list<string> $files
* @return array<string, array<string, string>>
*/
private function loadTranslationMap(string $dir, array $files): array
{
2026-02-11 19:28:12 +01:00
$translations = [];
foreach ($files as $name) {
$path = $dir . '/' . $name;
if (!is_file($path)) {
continue;
}
2026-02-11 19:28:12 +01:00
$raw = file_get_contents($path);
if ($raw === false) {
continue;
}
2026-02-11 19:28:12 +01:00
$json = json_decode($raw, true);
if (is_array($json)) {
$translations[$name] = $json;
}
2026-02-11 19:28:12 +01:00
}
return $translations;
}
/**
* @return array<string, array<string, array<string, string>>>
*/
private function loadAllModuleTranslations(string $modulesDir): array
{
$result = [];
if (!is_dir($modulesDir)) {
return $result;
2026-02-11 19:28:12 +01:00
}
foreach (glob($modulesDir . '/*/i18n') as $i18nDir) {
$moduleId = basename(dirname($i18nDir));
foreach (['default_de.json', 'default_en.json'] as $file) {
$path = $i18nDir . '/' . $file;
if (!is_file($path)) {
continue;
}
$raw = file_get_contents($path);
if ($raw === false) {
continue;
}
$json = json_decode($raw, true);
if (is_array($json)) {
$result[$moduleId][$file] = $json;
}
}
2026-02-11 19:28:12 +01:00
}
return $result;
2026-02-11 19:28:12 +01:00
}
/**
* @return list<string>
*/
2026-02-11 19:28:12 +01:00
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;
}
}