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('
\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('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('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('Willkommen, Erika
Meine Tickets – Test', 'en') === 'Welcome, Erika
My tickets – Test', 'English übersetzt Kundenportal-Texte'); $check(i18n_translate_html('