open($path, ZipArchive::CREATE); foreach ($files as $name => $content) $z->addFromString($name, $content); $z->close(); return $path; }; $flag = ''; $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 Test' => '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' => " $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('

Neues Ticket

') === '

New ticket

', 'Text zwischen Tags'); $check($t("

\n Neues Ticket \n

") === "

\n New ticket \n

", 'Leerraum um den Text bleibt, Leerraum im Text zählt nicht'); $check($t('') === '', 'Attribute placeholder und title'); $check($t('') === '', 'Beschriftung von Knöpfen'); $check($t('') === '', 'Eingaben (value) bleiben unberührt'); $check($t('') === '', 'Optionstext ja, Wert nein'); $check($t('') === '', 'Inhalt von Textfeldern bleibt'); $check($t('

Unbekannter Text

') === '

Unbekannter Text

', 'ohne Übersetzung bleibt es deutsch'); $check($t('

Willkommen, Erika

') === '

Welcome, Erika

', 'Muster mit {}'); $check($t('

Technik · erstellt am 24.10.2026

') === '

Technik · created 24.10.2026

', 'Muster mit {1} und {2} (Reihenfolge)'); $check($t('

12 Treffer

') === '

12 hits

' && $t('

viele Treffer

') === '

viele Treffer

', 'Muster mit {#} nur für Zahlen'); $check($t('

Mit & Zeichen

') === '

With & signs

', 'Entities im Schlüssel und im Ergebnis'); $check($t('

Wirklich löschen?

') === '

Really delete?

', 'Fragezeichen'); $check($t('') === '', 'confirm() in Ereignis-Attributen'); $check($t('') === '', 'Textliterale in Skripten (nur genaue Treffer)'); $check($t('') === '', 'Skript-Dateien bleiben'); $check($t('') === '', 'Stile bleiben'); $check($t('x') === 'x', 'lang-Attribut passt zur Sprache'); $check($t('Betreff') === 'Subject', 'Bilder: alt ja, src nein'); $check($t('

Neues Ticket

') === '

New ticket

', 'Kommentare bleiben'); $check($t('

Neues Ticket

') === '

Neues Ticket

', 'Text mit Markup mittendrin wird nicht zerhackt (nur ganze Textstücke)'); $check(i18n_translate_html('

Neues Ticket

', 'de') === '

Neues Ticket

', 'Deutsch verändert nichts'); // ── Sammelmodus ───────────────────────────────────────────────────────────── $seen = []; i18n_translate_html('

Neues Ticket

Völlig unbekannter Satz hier

42

https://beispiel.dex', '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('

Neues Ticket

') === '

New ticket

', '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, '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('

Neues Ticket erstellen

Willkommen, Erika

Meine Tickets – Test', 'en') === '

Create new ticket

Welcome, Erika

My tickets – Test', 'English übersetzt Kundenportal-Texte'); $check(i18n_translate_html('AbteilungenAbmelden', 'en') === 'DepartmentsSign out', '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";