array( 'post', 'page' ), 'enable_schema' => 0, 'minify' => 0, 'acf_include' => 1, 'cron_enabled' => 0, 'cron_frequency' => 'daily', 'ping_enabled' => 1, 'ping_urls' => "# Beispiele (Platzhalter anpassen)\nhttps://webhook.site/your-uuid\n", 'ping_events' => array( 'publish', 'update' ), 'ping_secret' => '', 'ping_timeout' => 5, 'ping_retry' => 1, 'openai_prompt' => 'Analysiere den folgenden WordPress Beitrag. Antworte ausschließlich als JSON mit overall_score, issues[], suggested_headings[], faq[].', 'score_preset' => 'standard', 'score_weight_content' => 35, 'score_weight_eeat' => 30, 'score_weight_technical' => 20, 'score_weight_linking' => 15, 'seo_plugin' => 'auto', 'remove_category_base' => 0, 'gtm_id' => '', 'ga4_id' => '', 'gsc_verify' => '', 'meta_pixel' => '', 'gsc_client_id' => '', 'gsc_client_secret' => '', 'content_language' => 'auto', 'backend_language' => 'auto', ); } /** * NUR die erlaubten Codes, OHNE __()-Aufruf. Bewusst getrennt von * get_backend_language_choices() (die die übersetzten Labels fürs Dropdown * liefert): get_opts()/sanitize_settings() validieren hiermit, und dürfen * dabei NICHT __() aufrufen — sonst Endlosschleife, weil unser eigener * 'gettext_{domain}'-Filter (siehe filter_backend_gettext()) bei jedem * __()-Aufruf für unsere Domain wiederum get_opts() aufruft. */ private static function backend_language_codes() { return array( 'auto', 'de_DE', 'en_US' ); } /** * Erlaubte Werte für backend_language: 'auto' (WordPress-Profil-/Site-Sprache * des jeweiligen Nutzers entscheidet, wie bisher) oder eine feste Locale, die * die Plugin-Oberfläche (Dashboard, Einstellungen, Meta-Box im Editor, ...) * für ALLE Nutzer unabhängig von deren eigener WP-Spracheinstellung erzwingt. * Getrennt von content_language, das nur die KI-Ausgaben (Meta-Description, * Themen-Analyse) betrifft, nicht die Plugin-Oberfläche selbst. * NUR für die UI (Dropdown in tab-settings.php) — NICHT für Validierung * verwenden, siehe Hinweis bei backend_language_codes(). */ public static function get_backend_language_choices() { return array( 'auto' => __( 'Wie WordPress (Profil-/Website-Sprache)', 'ai-json-extractor-auto-embed-lite' ), 'de_DE' => __( 'Deutsch', 'ai-json-extractor-auto-embed-lite' ), 'en_US' => __( 'Englisch', 'ai-json-extractor-auto-embed-lite' ), ); } /** * Hook für den WP-Core-Filter 'gettext_{domain}' (nur für UNSERE Text-Domain * registriert, siehe boot() in der Haupt-Plugin-Datei). Läuft bei JEDEM * __()/_e()/esc_html__()-Aufruf im Plugin. Wenn der Nutzer in den * Einstellungen explizit Deutsch oder Englisch statt "Automatisch" gewählt * hat, überschreiben wir hier das Ergebnis — unabhängig davon, welche * Sprache WP-Core selbst anhand von determine_locale() (Profil-/ * Website-Sprache des jeweiligen Nutzers) ermittelt und geladen hat. */ public function filter_backend_gettext( $translation, $text, $domain ) { $o = $this->get_opts(); $lang = $o['backend_language'] ?? 'auto'; if ( $lang === 'de_DE' ) { // Quelltext ist ohnehin Deutsch — Original zurückgeben, unabhängig // davon was WP-Core (ggf. für eine andere Locale) aufgelöst hat. return $text; } if ( $lang === 'en_US' ) { $mo = $this->get_own_backend_mo( 'en_US' ); if ( $mo ) { $translated = $mo->translate( $text ); if ( $translated !== $text ) { return $translated; } } } return $translation; } /** * Pendant zu filter_backend_gettext() für Plural-Strings (_n()/_n_noop()). * WP-Core ruft für Plural-Übersetzungen NICHT den 'gettext_{domain}'-Filter * auf, sondern separat 'ngettext_{domain}' — mit eigener Signatur * ($single, $plural, $number statt nur $text). Ohne diesen eigenen Hook * bleiben _n()-Aufrufe von der hier erzwungenen Backend-Sprache unberührt. */ public function filter_backend_ngettext( $translation, $single, $plural, $number, $domain ) { $o = $this->get_opts(); $lang = $o['backend_language'] ?? 'auto'; if ( $lang === 'de_DE' ) { // Quelltext ist ohnehin Deutsch — unveränderte Originalform zurückgeben. return $number == 1 ? $single : $plural; } if ( $lang === 'en_US' ) { $mo = $this->get_own_backend_mo( 'en_US' ); if ( $mo ) { $fallback = $number == 1 ? $single : $plural; $translated = $mo->translate_plural( $single, $plural, $number ); if ( $translated !== $fallback ) { return $translated; } } } return $translation; } /** * Lädt die plugin-eigene .mo-Datei direkt über die WP-eigene MO-Klasse — * bewusst UNABHÄNGIG von load_plugin_textdomain()/WP_Translation_Controller, * deren "just in time"-Ladeverhalten (seit WP 6.7) sich nicht per Filter auf * eine feste Locale zwingen lässt. Wird pro Locale einmal pro Request geladen * und gecacht. */ private function get_own_backend_mo( $locale ) { static $cache = array(); if ( array_key_exists( $locale, $cache ) ) { return $cache[ $locale ]; } if ( ! class_exists( 'MO' ) ) { require_once ABSPATH . WPINC . '/pomo/mo.php'; } $file = dirname( AIJE_PLUGIN_FILE ) . '/languages/ai-json-extractor-auto-embed-lite-' . $locale . '.mo'; $mo = new MO(); if ( is_readable( $file ) && $mo->import_from_file( $file ) ) { $cache[ $locale ] = $mo; } else { $cache[ $locale ] = false; } return $cache[ $locale ]; } /** * NUR die erlaubten Codes, OHNE __()-Aufruf — siehe Hinweis bei * backend_language_codes() oben; derselbe Grund gilt hier. */ private static function content_language_codes() { return array( 'auto', 'de', 'en', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'ru', 'tr' ); } /** * Erlaubte Werte für content_language: 'auto' (Sprache pro Artikel automatisch * per KI erkennen) oder ein fester Sprachname, der bei jedem KI-Aufruf * (Meta-Description-Generator + Themen-Analyse) erzwungen wird — sorgt für * einheitliche Ausgabesprache unabhängig vom Artikelinhalt. * NUR für die UI — NICHT für Validierung verwenden (s.o.). */ public static function get_language_choices() { return array( 'auto' => __( 'Automatisch erkennen (aus Artikelinhalt)', 'ai-json-extractor-auto-embed-lite' ), 'de' => __( 'Deutsch', 'ai-json-extractor-auto-embed-lite' ), 'en' => __( 'Englisch', 'ai-json-extractor-auto-embed-lite' ), 'fr' => __( 'Französisch', 'ai-json-extractor-auto-embed-lite' ), 'es' => __( 'Spanisch', 'ai-json-extractor-auto-embed-lite' ), 'it' => __( 'Italienisch', 'ai-json-extractor-auto-embed-lite' ), 'nl' => __( 'Niederländisch', 'ai-json-extractor-auto-embed-lite' ), 'pl' => __( 'Polnisch', 'ai-json-extractor-auto-embed-lite' ), 'pt' => __( 'Portugiesisch', 'ai-json-extractor-auto-embed-lite' ), 'ru' => __( 'Russisch', 'ai-json-extractor-auto-embed-lite' ), 'tr' => __( 'Türkisch', 'ai-json-extractor-auto-embed-lite' ), ); } private function get_opts() { $o = get_option( self::OPT_KEY, array() ); if ( ! is_array( $o ) ) { $o = array(); } $defaults = self::defaults(); foreach ( $defaults as $k => $v ) { if ( ! isset( $o[ $k ] ) ) { $o[ $k ] = $v; } } $o['enabled_post_types'] = array_values( array_filter( array_map( 'sanitize_key', (array) $o['enabled_post_types'] ) ) ); $o['enable_schema'] = ! empty( $o['enable_schema'] ) ? 1 : 0; $o['minify'] = ! empty( $o['minify'] ) ? 1 : 0; $o['acf_include'] = ! empty( $o['acf_include'] ) ? 1 : 0; $o['cron_enabled'] = ! empty( $o['cron_enabled'] ) ? 1 : 0; $o['cron_frequency'] = in_array( $o['cron_frequency'], array( 'daily', 'weekly' ), true ) ? $o['cron_frequency'] : 'daily'; $o['ping_enabled'] = ! empty( $o['ping_enabled'] ) ? 1 : 0; $o['ping_urls'] = is_string( $o['ping_urls'] ) ? $o['ping_urls'] : ''; $o['ping_secret'] = is_string( $o['ping_secret'] ) ? $o['ping_secret'] : ''; $o['ping_timeout'] = max( 2, (int) $o['ping_timeout'] ); $o['ping_retry'] = ! empty( $o['ping_retry'] ) ? 1 : 0; if ( ! isset( $o['openai_api_key'] ) ) { $o['openai_api_key'] = ''; } $valid_presets = array( 'ymyl', 'standard', 'ecommerce', 'custom' ); $o['score_preset'] = in_array( $o['score_preset'] ?? 'standard', $valid_presets, true ) ? $o['score_preset'] : 'standard'; $o['score_weight_content'] = max( 5, (int) ( $o['score_weight_content'] ?? 35 ) ); $o['score_weight_eeat'] = max( 5, (int) ( $o['score_weight_eeat'] ?? 30 ) ); $o['score_weight_technical'] = max( 5, (int) ( $o['score_weight_technical'] ?? 20 ) ); $o['score_weight_linking'] = max( 5, (int) ( $o['score_weight_linking'] ?? 15 ) ); $valid_seo = array( 'auto', 'yoast', 'rankmath', 'aioseo', 'own' ); $o['seo_plugin'] = in_array( $o['seo_plugin'] ?? 'auto', $valid_seo, true ) ? $o['seo_plugin'] : 'auto'; $valid_languages = self::content_language_codes(); $o['content_language'] = in_array( $o['content_language'] ?? 'auto', $valid_languages, true ) ? $o['content_language'] : 'auto'; $valid_backend_languages = self::backend_language_codes(); $o['backend_language'] = in_array( $o['backend_language'] ?? 'auto', $valid_backend_languages, true ) ? $o['backend_language'] : 'auto'; return $o; } public function get_opts_public() { return $this->get_opts(); } public static function get_score_weights( $o = array() ) { $presets = array( 'ymyl' => array( 'content' => 30, 'eeat' => 40, 'technical' => 20, 'linking' => 10 ), 'standard' => array( 'content' => 35, 'eeat' => 30, 'technical' => 20, 'linking' => 15 ), 'ecommerce' => array( 'content' => 30, 'eeat' => 20, 'technical' => 35, 'linking' => 15 ), ); $preset = isset( $o['score_preset'] ) ? $o['score_preset'] : 'standard'; if ( $preset !== 'custom' ) { return $presets[ $preset ] ?? $presets['standard']; } $w = array( 'content' => max( 5, (int) ( $o['score_weight_content'] ?? 35 ) ), 'eeat' => max( 5, (int) ( $o['score_weight_eeat'] ?? 30 ) ), 'technical' => max( 5, (int) ( $o['score_weight_technical'] ?? 20 ) ), 'linking' => max( 5, (int) ( $o['score_weight_linking'] ?? 15 ) ), ); $sum = array_sum( $w ); if ( $sum !== 100 ) { foreach ( $w as $k => $v ) { $w[ $k ] = (int) round( $v * 100 / $sum ); } $w['content'] += ( 100 - array_sum( $w ) ); } return $w; } public function get_active_seo_plugin() { $o = $this->get_opts(); $choice = $o['seo_plugin'] ?? 'auto'; if ( $choice !== 'auto' ) { return $choice; } if ( defined( 'WPSEO_VERSION' ) ) { return 'yoast'; } if ( defined( 'RANK_MATH_VERSION' ) ) { return 'rankmath'; } if ( defined( 'AIOSEO_VERSION' ) ) { return 'aioseo'; } return 'own'; } public function register_settings() { register_setting( self::OPT_KEY, self::OPT_KEY, array( $this, 'sanitize_settings' ) ); register_setting( 'aije_ai_settings', 'aije_openai_api_key' ); } public function sanitize_settings( $in ) { $d = self::defaults(); $old_opts = get_option( self::OPT_KEY, array() ); if ( ! is_array( $old_opts ) ) { $old_opts = array(); } // WICHTIG: Jeder Tab (Einstellungen, Tracking, ...) ist ein EIGENES //