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

512 lines
14 KiB
PHP
Raw Permalink 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/{*}.php
* - (optional) /partials/partials/{*}/{*}, falls vorhanden
*
* und trägt fehlende Keys in public/assets/i18n/de.json ein.
*
* WICHTIG (angepasst):
* - partials/structure/{name}.php wird jetzt unter
* partials.structure.{name}.{key}
* abgelegt (nicht mehr im Root).
* - meta-Keys werden in jedem Block an erste Stelle gesetzt.
*
* 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 irgendwo in der Struktur ein
* (früher unter $data['auto'], jetzt nur noch für Reste).
*
* WICHTIG: $defaultValue ist explizit nullable → keine Deprecation.
*/
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 → (Rest) unter "auto".
*
* Dynamische Keys mit $ (z.B. pages.$pageKey.meta.title) werden ignoriert.
*/
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, ... ]
*
* Dynamische Keys mit $ (pages.$pageKey...) werden bereits hier
* ignoriert.
*/
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]);
if (strpos($key, '$') !== false) {
continue;
}
// 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]);
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]);
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();
$relPath = str_replace($baseDir . DIRECTORY_SEPARATOR, '', $filePath);
$keysInFile = collectKeysFromFile($filePath);
foreach ($keysInFile as $key => $defaultText) {
$origKey = $key;
// Nur für einfache Keys (ohne Punkt) Kontext-basiert umschreiben
if (strpos($key, '.') === false) {
// 4a) partials/landing/{slug}/{section}.php
if (preg_match('~^partials/landing/([^/]+)/([^/]+)\.(php|html?|phtml)$~', $relPath, $m)) {
$slug = $m[1]; // z.B. landing, fakecheck, login, dashboard
$section = $m[2]; // z.B. hero, how, problem, features, security, faq, main
$key = "pages.$slug.sections.$section.$key";
// 4b) public/landingpage/{slug}/*.php → Fallback: section "main"
} elseif (preg_match('~^public/landingpage/([^/]+)/.+\.(php|html?|phtml)$~', $relPath, $m)) {
$slug = $m[1]; // z.B. landing, fakecheck, login, dashboard
$key = "pages.$slug.sections.main.$key";
// 4c) partials/structure/{name}.php → z.B. header.*, footer.*, layout_*
} elseif (preg_match('~^partials/structure/([^/]+)\.(php|html?|phtml)$~', $relPath, $m)) {
$section = $m[1]; // header, footer, layout_start, layout_end, app_config, ...
// NEU: unter partials.structure.{section}.{key}
$key = "partials.structure.$section.$key";
}
}
// Gefilterten Key übernehmen
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();
// einfache 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++;
}
}
}
// -------------------------------
// 5c) meta-Keys in jedem Block nach vorne ziehen
// -------------------------------
function reorderMetaFirstRecursive(array &$node): void
{
// Zuerst rekursiv in die Tiefe gehen
foreach ($node as $k => &$v) {
if (is_array($v)) {
reorderMetaFirstRecursive($v);
}
}
unset($v);
// Wenn es in diesem Block einen 'meta'-Key gibt, diesen an erste Stelle setzen
if (array_key_exists('meta', $node) && count($node) > 1) {
$metaValue = $node['meta'];
unset($node['meta']);
// Rest in aktueller Reihenfolge behalten
$rest = $node;
// Neu zusammensetzen: meta zuerst
$node = ['meta' => $metaValue] + $rest;
}
}
reorderMetaFirstRecursive($data);
// -------------------------------
// 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;
}