Files
usbcheck.it/tools/i18n_collect_keys.php
2025-11-28 00:26:28 +01:00

442 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/*
* tools/i18n_collect_keys.php
*
* Sammelt alle verwendeten Übersetzungs-Keys aus:
* - /public/landingpage/{*}/{*}
* - /partials/landing/{*}/{*}
* - /partials/structure/{*}/{*}
* - (optional) /partials/partials/{*}/{*}, falls vorhanden
*
* und trägt fehlende Keys in public/assets/i18n/de.json ein.
*
* Aufruf:
* - CLI: php tools/i18n_collect_keys.php
* - HTTP: https://.../tools/i18n_collect_keys.php
*/
declare(strict_types=1);
$baseDir = dirname(__DIR__);
$deJson = $baseDir . '/public/assets/i18n/de.json';
// -------------------------------
// 1) Bestehende de.json laden
// -------------------------------
if (!is_file($deJson)) {
die("de.json nicht gefunden unter: $deJson\n");
}
$raw = file_get_contents($deJson);
if ($raw === false) {
die("Konnte de.json nicht lesen: $deJson\n");
}
$data = json_decode($raw, true);
if (!is_array($data)) {
die("Ungültiges JSON in de.json\n");
}
if (!isset($data['meta']) || !is_array($data['meta'])) {
$data['meta'] = [
'code' => 'de',
'label' => 'Deutsch',
'flag' => '🇩🇪',
];
}
// -------------------------------
// 2) Verzeichnisse zum Scannen
// -------------------------------
$scanDirs = [
$baseDir . '/public/landingpage',
$baseDir . '/partials/landing',
$baseDir . '/partials/structure',
$baseDir . '/partials/partials', // nur, falls vorhanden
];
$allowedExtensions = ['php', 'html', 'htm'];
// -------------------------------
// 3) Helper-Funktionen
// -------------------------------
function dotKeyExists(array $data, string $key): bool
{
$segments = explode('.', $key);
$node = $data;
foreach ($segments as $seg) {
if (!is_array($node) || !array_key_exists($seg, $node)) {
return false;
}
$node = $node[$seg];
}
return true;
}
function addDotKey(array &$data, string $key, string $default = ''): void
{
if (dotKeyExists($data, $key)) {
return;
}
$segments = explode('.', $key);
$node =& $data;
$last = array_pop($segments);
foreach ($segments as $seg) {
if (!isset($node[$seg]) || !is_array($node[$seg])) {
$node[$seg] = [];
}
$node =& $node[$seg];
}
if (!array_key_exists($last, $node)) {
$node[$last] = $default;
}
}
function simpleKeyExistsRecursive(array $data, string $key): bool
{
foreach ($data as $k => $v) {
if ($k === $key) {
return true;
}
if (is_array($v) && simpleKeyExistsRecursive($v, $key)) {
return true;
}
}
return false;
}
/**
* Fügt einen einfachen Key unter $data['auto'] hinzu,
* sofern er nicht irgendwo in der Struktur bereits existiert.
*
* WICHTIG: $defaultValue ist explizit nullable, um
* deprecation unter PHP 8.4+ zu vermeiden.
*/
function addSimpleKey(array &$data, string $key, ?string $defaultValue = null): void
{
if (simpleKeyExistsRecursive($data, $key)) {
return;
}
if (!isset($data['auto']) || !is_array($data['auto'])) {
$data['auto'] = [];
}
if ($defaultValue === null || $defaultValue === '') {
$defaultValue = $key;
}
if (!array_key_exists($key, $data['auto'])) {
$data['auto'][$key] = $defaultValue;
}
}
/**
* Key einfügen, mit optionalem Default-Text.
* Dot-Notation → verschachtelt,
* Simple-Key → unter "auto".
*/
function addKeyToData(array &$data, string $key, ?string $defaultValue = null): void
{
$key = trim($key);
if ($key === '') {
return;
}
// Dynamische Keys mit Variablen wie pages.$pageKey.meta.title ignorieren
if (strpos($key, '$') !== false) {
return;
}
$default = ($defaultValue !== null && $defaultValue !== '')
? $defaultValue
: $key;
if (strpos($key, '.') !== false) {
addDotKey($data, $key, $default);
} else {
addSimpleKey($data, $key, $default);
}
}
/**
* Extrahiert den „aktuellen Inhalt“ eines data-i18n-Elements
* grob über Regex:
* ... data-i18n="key"> Inhalt </...
* Ist nur ein Best-Effort; bei komplexeren Strukturen fällt es auf null zurück.
*/
function extractInlineTextForDataI18n(string $content, int $matchOffset, int $matchLength): ?string
{
$start = $matchOffset + $matchLength;
$gtPos = strpos($content, '>', $start);
if ($gtPos === false) {
return null;
}
$closePos = strpos($content, '<', $gtPos + 1);
if ($closePos === false) {
return null;
}
$inner = substr($content, $gtPos + 1, $closePos - $gtPos - 1);
$inner = trim($inner);
if ($inner === '') {
return null;
}
// Ganz grob Tags entfernen
$inner = strip_tags($inner);
$inner = trim($inner);
return $inner !== '' ? $inner : null;
}
/**
* Scannt eine Datei nach i18n-Keys und möglichen Default-Texten.
*
* Rückgabe:
* [ 'key1' => 'Default-Text oder null', 'key2' => null, ... ]
*/
function collectKeysFromFile(string $file): array
{
$content = file_get_contents($file);
if ($content === false || $content === '') {
return [];
}
$keysWithDefaults = [];
// --- data-i18n="key" / 'key' ---
if (preg_match_all('/data-i18n\s*=\s*(["\'])(.+?)\1/i', $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
foreach ($matches as $m) {
$fullMatch = $m[0][0];
$fullOffset = $m[0][1];
$key = trim($m[2][0]);
// Versuche, den Inline-Text zu extrahieren: >Text<
$inlineText = extractInlineTextForDataI18n($content, $fullOffset, strlen($fullMatch));
if (!array_key_exists($key, $keysWithDefaults)) {
$keysWithDefaults[$key] = $inlineText;
} elseif ($keysWithDefaults[$key] === null && $inlineText !== null) {
// Falls wir bisher keinen Default hatten, aber jetzt einen finden:
$keysWithDefaults[$key] = $inlineText;
}
}
}
// --- i18n_get('path.to.key', 'Default') ---
if (preg_match_all(
'/\bi18n_get\s*\(\s*(["\'])([^"\']+)\1\s*(,\s*(["\'])(.*?)\4)?/i',
$content,
$m2,
PREG_SET_ORDER
)) {
foreach ($m2 as $match) {
$key = trim($match[2]);
// Dynamische Keys mit Variablen wie pages.$pageKey.* hier sofort ignorieren
if (strpos($key, '$') !== false) {
continue;
}
$default = isset($match[5]) ? trim($match[5]) : null;
if ($default !== null && $default !== '') {
// expliziter Default im PHP-Code → höchste Priorität
$keysWithDefaults[$key] = $default;
} else {
if (!array_key_exists($key, $keysWithDefaults)) {
$keysWithDefaults[$key] = null;
}
}
}
}
// --- i18n_get_fmt("path.to.key", "Default", ...) ---
if (preg_match_all(
'/\bi18n_get_fmt\s*\(\s*(["\'])([^"\']+)\1\s*(,\s*(["\'])(.*?)\4)?/i',
$content,
$m3,
PREG_SET_ORDER
)) {
foreach ($m3 as $match) {
$key = trim($match[2]);
// Auch hier dynamische Keys mit $ ignorieren
if (strpos($key, '$') !== false) {
continue;
}
$default = isset($match[5]) ? trim($match[5]) : null;
if ($default !== null && $default !== '') {
$keysWithDefaults[$key] = $default;
} else {
if (!array_key_exists($key, $keysWithDefaults)) {
$keysWithDefaults[$key] = null;
}
}
}
}
return $keysWithDefaults;
}
// -------------------------------
// 4) Dateien durchlaufen & Keys sammeln
// -------------------------------
$foundKeys = []; // key => defaultText|null
foreach ($scanDirs as $dir) {
if (!is_dir($dir)) {
continue;
}
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
);
foreach ($it as $fileInfo) {
/** @var SplFileInfo $fileInfo */
if (!$fileInfo->isFile()) {
continue;
}
$ext = strtolower($fileInfo->getExtension());
if (!in_array($ext, $allowedExtensions, true)) {
continue;
}
$filePath = $fileInfo->getPathname();
$keysInFile = collectKeysFromFile($filePath);
foreach ($keysInFile as $key => $defaultText) {
if (!array_key_exists($key, $foundKeys)) {
$foundKeys[$key] = $defaultText;
} else {
// Wenn wir bisher keinen Default hatten, aber jetzt einen haben → übernehmen
if (($foundKeys[$key] === null || $foundKeys[$key] === '')
&& $defaultText !== null
&& $defaultText !== ''
) {
$foundKeys[$key] = $defaultText;
}
}
}
}
}
// -------------------------------
// 5) Gefundene Keys in de.json eintragen
// -------------------------------
$addedCount = 0;
$skippedCount = 0;
$newKeys = [];
foreach ($foundKeys as $key => $defaultText) {
$before = json_encode($data);
addKeyToData($data, $key, $defaultText);
$after = json_encode($data);
if ($before === $after) {
$skippedCount++;
} else {
$addedCount++;
$newKeys[] = $key;
}
}
// -------------------------------
// 5b) Metadaten für alle Landingpages ergänzen
// /public/landingpage/{slug}/ → pages.{slug}.meta.{title,description}
// -------------------------------
$landingRoot = $baseDir . '/public/landingpage';
if (is_dir($landingRoot)) {
foreach (new DirectoryIterator($landingRoot) as $entry) {
if ($entry->isDot() || !$entry->isDir()) {
continue;
}
$slug = $entry->getFilename();
// sehr simple Heuristik: Landingpage gilt als "aktiv", wenn irgendeine .php drin liegt
$hasPhp = false;
foreach (new DirectoryIterator($entry->getPathname()) as $file) {
if ($file->isFile() && strtolower($file->getExtension()) === 'php') {
$hasPhp = true;
break;
}
}
if (!$hasPhp) {
continue;
}
$titleKey = "pages.$slug.meta.title";
$descKey = "pages.$slug.meta.description";
if (!dotKeyExists($data, $titleKey)) {
$defaultTitle = '{{primary_domain}} ' . ucfirst($slug);
addDotKey($data, $titleKey, $defaultTitle);
$newKeys[] = $titleKey;
$addedCount++;
}
if (!dotKeyExists($data, $descKey)) {
$defaultDesc = 'Beschreibung für ' . ucfirst($slug) . ' auf {{primary_domain}}';
addDotKey($data, $descKey, $defaultDesc);
$newKeys[] = $descKey;
$addedCount++;
}
}
}
// -------------------------------
// 6) de.json zurückschreiben
// -------------------------------
$newJson = json_encode(
$data,
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
if ($newJson === false) {
die("Fehler beim JSON-Encode von de.json\n");
}
if (file_put_contents($deJson, $newJson) === false) {
die("Konnte de.json nicht schreiben: $deJson\n");
}
// -------------------------------
// 7) Ausgabe
// -------------------------------
$isCli = (php_sapi_name() === 'cli');
$output = "i18n-Collect abgeschlossen.\n";
$output .= "Gefundene Keys gesamt: " . count($foundKeys) . "\n";
$output .= "Neu hinzugefügt: " . $addedCount . "\n";
$output .= "Übersprungen (bereits vorhanden): $skippedCount\n";
$output .= "Datei aktualisiert: $deJson\n";
if (!empty($newKeys)) {
$output .= "\nNeu angelegte Keys:\n";
foreach ($newKeys as $k) {
$output .= " - " . $k . "\n";
}
}
if ($isCli) {
echo $output;
} else {
if (!headers_sent()) {
header('Content-Type: text/plain; charset=utf-8');
}
echo $output;
}