160 lines
12 KiB
PHP
160 lines
12 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* Prüft die Mehrsprachigkeit: Sprachpakete als Plugin, Übersetzung der fertigen Seite (Text, Attribute, Knöpfe, JavaScript-Texte,
|
||
* Muster mit {} und {#}), Sammelmodus, Sprachwechsler mit Flaggen und den Wechsel über lang.php.
|
||
*/
|
||
|
||
$pluginDir = sys_get_temp_dir() . '/deskhub-i18n-' . bin2hex(random_bytes(4));
|
||
mkdir($pluginDir, 0755, true);
|
||
putenv('DESKHUB_PLUGINS_DIR=' . $pluginDir);
|
||
putenv('DESKHUB_TESTING=1');
|
||
putenv('DESKHUB_CATALOG_DEFAULTS=' . __DIR__ . '/fixtures/no-catalog.php');
|
||
putenv('DESKHUB_SQLITE_PATH=:memory:');
|
||
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
||
require_once __DIR__ . '/fixtures/paths.php';
|
||
|
||
$failures = [];
|
||
$check = function (bool $ok, string $what) use (&$failures): void {
|
||
if (!$ok) $failures[] = $what;
|
||
};
|
||
$pdo = get_db();
|
||
|
||
// ── Ohne Sprachpaket: nur Deutsch, kein Wechsler ────────────────────────────
|
||
$check(array_keys(i18n_languages()) === ['de'] && i18n_switcher_html() === '' && i18n_current() === 'de', 'ohne Sprachpaket gibt es nur Deutsch und keinen Wechsler');
|
||
|
||
// ── Sprachpaket installieren: eigenes kleines Paket und das mitgelieferte English ──
|
||
$mkZip = function (array $files) {
|
||
$path = sys_get_temp_dir() . '/lang-' . bin2hex(random_bytes(3)) . '.zip';
|
||
$z = new ZipArchive();
|
||
$z->open($path, ZipArchive::CREATE);
|
||
foreach ($files as $name => $content) $z->addFromString($name, $content);
|
||
$z->close();
|
||
return $path;
|
||
};
|
||
$flag = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="2" fill="#009246"/></svg>';
|
||
$strings = [
|
||
'_info' => 'Test',
|
||
'Neues Ticket' => 'New ticket',
|
||
'Ticket erstellen' => 'Create ticket',
|
||
'Betreff' => 'Subject',
|
||
'Kurze Zusammenfassung' => 'Short summary',
|
||
'Willkommen, {}' => 'Welcome, {}',
|
||
'{} · erstellt am {}' => '{1} · created {2}',
|
||
'{#} Treffer' => '{} hits',
|
||
'Wirklich löschen?' => 'Really delete?',
|
||
'Ein <b>Test</b>' => 'A test',
|
||
'Mit & Zeichen' => 'With & signs',
|
||
'Leer' => '',
|
||
];
|
||
$pkg = $mkZip([
|
||
'plugin.json' => json_encode(['slug' => 'testsprache', 'name' => 'Testsprache', 'version' => '1.0.0', 'entry' => 'plugin.php', 'permissions' => [], 'language' => ['code' => 'xx', 'name' => 'Testsprache', 'flag' => 'flag.svg', 'strings' => 'strings.json', 'htmlLang' => 'xx-TS']]),
|
||
'plugin.php' => "<?php\n",
|
||
'flag.svg' => $flag,
|
||
'strings.json' => json_encode($strings, JSON_UNESCAPED_UNICODE),
|
||
]);
|
||
$r = plugin_install_zip($pdo, $pkg);
|
||
$check($r['ok'] && $r['slug'] === 'testsprache', 'Sprachpaket lässt sich installieren: ' . json_encode($r));
|
||
$check(count(i18n_languages(true)) === 1, 'ein nicht aktiviertes Sprachpaket zählt nicht');
|
||
$act = plugin_set_active($pdo, 'testsprache', true, []);
|
||
$check($act['ok'], 'Sprachpaket braucht keine Berechtigungen und lässt sich aktivieren: ' . json_encode($act));
|
||
$langs = i18n_languages(true);
|
||
$check(array_keys($langs) === ['de', 'xx'] && $langs['xx']['name'] === 'Testsprache' && $langs['xx']['htmlLang'] === 'xx-TS' && str_starts_with($langs['xx']['flag'], 'data:image/svg+xml;base64,'), 'Sprachen: Deutsch (eingebaut) und das Paket mit Flagge als Datenadresse');
|
||
$check(i18n_dictionary('xx')['count'] === 10 && !isset(i18n_dictionary('xx')['exact']['Leer']), 'Wörterbuch: leere Werte und _info werden ignoriert');
|
||
|
||
// ── Auswahl der Sprache ─────────────────────────────────────────────────────
|
||
$check(i18n_current() === 'de', 'ohne Cookie gilt die Standardsprache');
|
||
$_COOKIE[I18N_COOKIE] = 'xx';
|
||
$check(i18n_current() === 'xx', 'Cookie wählt die Sprache');
|
||
$_COOKIE[I18N_COOKIE] = 'zz';
|
||
$check(i18n_current() === 'de', 'unbekannter Sprachcode im Cookie wird ignoriert');
|
||
unset($_COOKIE[I18N_COOKIE]);
|
||
set_setting('default_language', 'xx');
|
||
$check(i18n_default() === 'xx' && i18n_current() === 'xx', 'Standardsprache aus den Einstellungen');
|
||
set_setting('default_language', 'qq');
|
||
$check(i18n_default() === 'de', 'unbekannte Standardsprache fällt auf Deutsch zurück');
|
||
set_setting('default_language', 'de');
|
||
|
||
// ── Übersetzung ─────────────────────────────────────────────────────────────
|
||
$t = fn(string $html) => i18n_translate_html($html, 'xx');
|
||
$check($t('<h1>Neues Ticket</h1>') === '<h1>New ticket</h1>', 'Text zwischen Tags');
|
||
$check($t("<p>\n Neues Ticket \n</p>") === "<p>\n New ticket \n</p>", 'Leerraum um den Text bleibt, Leerraum im Text zählt nicht');
|
||
$check($t('<input placeholder="Kurze Zusammenfassung" title="Betreff">') === '<input placeholder="Short summary" title="Subject">', 'Attribute placeholder und title');
|
||
$check($t('<input type="submit" value="Ticket erstellen">') === '<input type="submit" value="Create ticket">', 'Beschriftung von Knöpfen');
|
||
$check($t('<input type="text" value="Ticket erstellen">') === '<input type="text" value="Ticket erstellen">', 'Eingaben (value) bleiben unberührt');
|
||
$check($t('<option value="Betreff">Betreff</option>') === '<option value="Betreff">Subject</option>', 'Optionstext ja, Wert nein');
|
||
$check($t('<textarea>Neues Ticket</textarea>') === '<textarea>Neues Ticket</textarea>', 'Inhalt von Textfeldern bleibt');
|
||
$check($t('<p>Unbekannter Text</p>') === '<p>Unbekannter Text</p>', 'ohne Übersetzung bleibt es deutsch');
|
||
$check($t('<p>Willkommen, Erika</p>') === '<p>Welcome, Erika</p>', 'Muster mit {}');
|
||
$check($t('<p>Technik · erstellt am 24.10.2026</p>') === '<p>Technik · created 24.10.2026</p>', 'Muster mit {1} und {2} (Reihenfolge)');
|
||
$check($t('<p>12 Treffer</p>') === '<p>12 hits</p>' && $t('<p>viele Treffer</p>') === '<p>viele Treffer</p>', 'Muster mit {#} nur für Zahlen');
|
||
$check($t('<p>Mit & Zeichen</p>') === '<p>With & signs</p>', 'Entities im Schlüssel und im Ergebnis');
|
||
$check($t('<p>Wirklich löschen?</p>') === '<p>Really delete?</p>', 'Fragezeichen');
|
||
$check($t('<button onclick="return confirm(\'Wirklich löschen?\')">x</button>') === '<button onclick="return confirm(\'Really delete?\')">x</button>', 'confirm() in Ereignis-Attributen');
|
||
$check($t('<script>if (confirm("Wirklich löschen?")) go(); var a = "Neues Ticket"; var b = "Anderes";</script>') === '<script>if (confirm("Really delete?")) go(); var a = "New ticket"; var b = "Anderes";</script>', 'Textliterale in Skripten (nur genaue Treffer)');
|
||
$check($t('<script src="/x.js"></script>') === '<script src="/x.js"></script>', 'Skript-Dateien bleiben');
|
||
$check($t('<style>.a::after{content:"Neues Ticket"}</style>') === '<style>.a::after{content:"Neues Ticket"}</style>', 'Stile bleiben');
|
||
$check($t('<html lang="de"><body>x</body></html>') === '<html lang="xx-TS"><body>x</body></html>', 'lang-Attribut passt zur Sprache');
|
||
$check($t('<img src="data:image/png;base64,AAAA" alt="Betreff">') === '<img src="data:image/png;base64,AAAA" alt="Subject">', 'Bilder: alt ja, src nein');
|
||
$check($t('<!-- Neues Ticket --><p>Neues Ticket</p>') === '<!-- Neues Ticket --><p>New ticket</p>', 'Kommentare bleiben');
|
||
$check($t('<p>Neues <b>Ticket</b></p>') === '<p>Neues <b>Ticket</b></p>', 'Text mit Markup mittendrin wird nicht zerhackt (nur ganze Textstücke)');
|
||
$check(i18n_translate_html('<p>Neues Ticket</p>', 'de') === '<p>Neues Ticket</p>', 'Deutsch verändert nichts');
|
||
|
||
// ── Sammelmodus ─────────────────────────────────────────────────────────────
|
||
$seen = [];
|
||
i18n_translate_html('<h1>Neues Ticket</h1><p>Völlig unbekannter Satz hier</p><p>42</p><a href="x">https://beispiel.de</a><span title="Ein Tooltip hier">x</span><script>if (confirm("Soll das wirklich weg?")) {}</script>', 'xx', $seen);
|
||
$check(isset($seen['Völlig unbekannter Satz hier']) && isset($seen['Ein Tooltip hier']) && isset($seen['Soll das wirklich weg?']) && !isset($seen['Neues Ticket']) && !isset($seen['42']) && !isset($seen['https://beispiel.de']), 'gesammelt wird nur Unübersetztes, keine Zahlen und Adressen: ' . json_encode(array_keys($seen)));
|
||
i18n_remember(array_keys($seen));
|
||
i18n_remember(['Völlig unbekannter Satz hier']);
|
||
$row = $pdo->query("SELECT hits FROM i18n_missing WHERE text = 'Völlig unbekannter Satz hier'")->fetchColumn();
|
||
$check((int) $pdo->query('SELECT COUNT(*) FROM i18n_missing')->fetchColumn() === 3 && (int) $row === 2, 'unübersetzte Texte werden gespeichert und gezählt');
|
||
|
||
// ── Ausgabe-Filter ──────────────────────────────────────────────────────────
|
||
$GLOBALS['__i18n_code'] = 'xx';
|
||
$check(i18n_output_filter('<!doctype html><html lang="de"><body><h1>Neues Ticket</h1></body></html>') === '<!doctype html><html lang="xx-TS"><body><h1>New ticket</h1></body></html>', 'Ausgabe-Filter übersetzt die Seite');
|
||
$check(i18n_output_filter('{"a":"Neues Ticket"}') === false, 'JSON bleibt unverändert (keine HTML-Tags)');
|
||
|
||
// ── Sprachwechsler ──────────────────────────────────────────────────────────
|
||
$_SERVER['REQUEST_URI'] = '/ticket_new.php?x=1';
|
||
$sw = i18n_switcher_html();
|
||
$check(substr_count($sw, '<img') === 2 && str_contains($sw, '/lang.php?l=xx&back=%2Fticket_new.php%3Fx%3D1') && str_contains($sw, '/lang.php?l=de') && str_contains($sw, 'aria-current'), 'Wechsler: zwei Flaggen, Links mit Rücksprung, aktive Sprache markiert');
|
||
$check(str_contains(i18n_switcher_html('floating'), 'position:fixed') && !str_contains($sw, 'position:fixed'), 'Wechsler: feste Position nur für Anmeldeseiten');
|
||
$check(i18n_safe_back('/staff/tickets.php?a=1') === '/staff/tickets.php?a=1' && i18n_safe_back('//evil.example') === '/' && i18n_safe_back('https://evil.example') === '/' && i18n_safe_back("/a\r\nSet-Cookie: x") === '/' && i18n_safe_back('/\\evil') === '/' && i18n_safe_back('') === '/', 'Rücksprung nur auf Adressen dieser Seite');
|
||
|
||
// ── Das mitgelieferte English-Paket ─────────────────────────────────────────
|
||
$enSrc = plugin_source_dir_or_skip('lang-en', 'i18n');
|
||
$deSrc = plugin_source_dir_or_skip('lang-de', 'i18n');
|
||
foreach ([$enSrc, $deSrc] as $src) {
|
||
$zp = sys_get_temp_dir() . '/lp-' . bin2hex(random_bytes(3)) . '.zip';
|
||
$z = new ZipArchive();
|
||
$z->open($zp, ZipArchive::CREATE);
|
||
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS)) as $f) $z->addFile($f->getPathname(), str_replace('\\', '/', substr($f->getPathname(), strlen($src) + 1)));
|
||
$z->close();
|
||
$ri = plugin_install_zip($pdo, $zp);
|
||
$check($ri['ok'], 'mitgeliefertes Paket installierbar: ' . ($ri['error'] ?? $ri['slug']));
|
||
$slug = (string) ($ri['slug'] ?? '');
|
||
$check(plugin_set_active($pdo, $slug, true, [])['ok'], "Paket {$slug} aktivierbar");
|
||
@unlink($zp);
|
||
}
|
||
$langs = i18n_languages(true);
|
||
$check(isset($langs['en'], $langs['de'], $langs['xx']) && $langs['de']['slug'] === 'lang-de' && $langs['en']['name'] === 'English' && $langs['en']['flag'] !== '', 'English und Deutsch (als Plugin) sind da');
|
||
$en = i18n_dictionary('en');
|
||
$check($en['count'] > 900, 'English hat viele Übersetzungen: ' . $en['count']);
|
||
$check(i18n_translate_html('<h1>Neues Ticket erstellen</h1><label>Betreff</label><p>Willkommen, Erika</p><a>Meine Tickets – Test</a>', 'en') === '<h1>Create new ticket</h1><label>Subject</label><p>Welcome, Erika</p><a>My tickets – Test</a>', 'English übersetzt Kundenportal-Texte');
|
||
$check(i18n_translate_html('<th>Abteilungen</th><button>Speichern</button><span>Abmelden</span>', 'en') === '<th>Departments</th><button>Save</button><span>Sign out</span>', 'English übersetzt Texte des Mitarbeiterbereichs');
|
||
|
||
// ── Aufräumen ───────────────────────────────────────────────────────────────
|
||
@unlink($pkg);
|
||
foreach (scandir($pluginDir) ?: [] as $entry) {
|
||
if ($entry === '.' || $entry === '..') continue;
|
||
is_dir($pluginDir . '/' . $entry) ? plugin_rmdir($pluginDir . '/' . $entry) : @unlink($pluginDir . '/' . $entry);
|
||
}
|
||
@rmdir($pluginDir);
|
||
|
||
if ($failures) {
|
||
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
|
||
exit(1);
|
||
}
|
||
echo 'Mehrsprachigkeit checks passed.' . "\n";
|