> */ private $presets = array( 'af' => array( 'english_label' => 'Afrikaans', 'native_label' => 'Afrikaans', 'flag' => 'za', 'rtl' => 0, 'wp_locale' => 'af' ), 'sq' => array( 'english_label' => 'Albanian', 'native_label' => 'Shqip', 'flag' => 'al', 'rtl' => 0, 'wp_locale' => 'sq' ), 'am' => array( 'english_label' => 'Amharic', 'native_label' => 'Amharic', 'flag' => 'et', 'rtl' => 0, 'wp_locale' => 'am' ), 'ar' => array( 'english_label' => 'Arabic', 'native_label' => 'العربية', 'flag' => 'sa', 'rtl' => 1, 'wp_locale' => 'ar' ), 'hy' => array( 'english_label' => 'Armenian', 'native_label' => 'Հայերեն', 'flag' => 'am', 'rtl' => 0, 'wp_locale' => 'hy' ), 'az' => array( 'english_label' => 'Azerbaijani', 'native_label' => 'Azərbaycan dili', 'flag' => 'az', 'rtl' => 0, 'wp_locale' => 'az' ), 'eu' => array( 'english_label' => 'Basque', 'native_label' => 'Euskara', 'flag' => 'es', 'rtl' => 0, 'wp_locale' => 'eu' ), 'be' => array( 'english_label' => 'Belarusian', 'native_label' => 'Беларуская', 'flag' => 'by', 'rtl' => 0, 'wp_locale' => 'bel' ), 'bn' => array( 'english_label' => 'Bengali', 'native_label' => 'বাংলা', 'flag' => 'bd', 'rtl' => 0, 'wp_locale' => 'bn_BD' ), 'bs' => array( 'english_label' => 'Bosnian', 'native_label' => 'Bosanski', 'flag' => 'ba', 'rtl' => 0, 'wp_locale' => 'bs_BA' ), 'bg' => array( 'english_label' => 'Bulgarian', 'native_label' => 'Български', 'flag' => 'bg', 'rtl' => 0, 'wp_locale' => 'bg_BG' ), 'ca' => array( 'english_label' => 'Catalan', 'native_label' => 'Català', 'flag' => 'es', 'rtl' => 0, 'wp_locale' => 'ca' ), 'zh' => array( 'english_label' => 'Chinese (Simplified)', 'native_label' => '简体中文', 'flag' => 'cn', 'rtl' => 0, 'wp_locale' => 'zh_CN' ), 'zh-tw' => array( 'english_label' => 'Chinese (Traditional)', 'native_label' => '繁體中文', 'flag' => 'tw', 'rtl' => 0, 'wp_locale' => 'zh_TW' ), 'hr' => array( 'english_label' => 'Croatian', 'native_label' => 'Hrvatski', 'flag' => 'hr', 'rtl' => 0, 'wp_locale' => 'hr' ), 'cs' => array( 'english_label' => 'Czech', 'native_label' => 'Čeština', 'flag' => 'cz', 'rtl' => 0, 'wp_locale' => 'cs_CZ' ), 'da' => array( 'english_label' => 'Danish', 'native_label' => 'Dansk', 'flag' => 'dk', 'rtl' => 0, 'wp_locale' => 'da_DK' ), 'nl' => array( 'english_label' => 'Dutch', 'native_label' => 'Nederlands', 'flag' => 'nl', 'rtl' => 0, 'wp_locale' => 'nl_NL' ), 'et' => array( 'english_label' => 'Estonian', 'native_label' => 'Eesti', 'flag' => 'ee', 'rtl' => 0, 'wp_locale' => 'et' ), 'fa' => array( 'english_label' => 'Persian', 'native_label' => 'فارسی', 'flag' => 'ir', 'rtl' => 1, 'wp_locale' => 'fa_IR' ), 'fi' => array( 'english_label' => 'Finnish', 'native_label' => 'Suomi', 'flag' => 'fi', 'rtl' => 0, 'wp_locale' => 'fi' ), 'fr' => array( 'english_label' => 'French', 'native_label' => 'Français', 'flag' => 'fr', 'rtl' => 0, 'wp_locale' => 'fr_FR' ), 'ka' => array( 'english_label' => 'Georgian', 'native_label' => 'ქართული', 'flag' => 'ge', 'rtl' => 0, 'wp_locale' => 'ka_GE' ), 'de' => array( 'english_label' => 'German', 'native_label' => 'Deutsch', 'flag' => 'de', 'rtl' => 0, 'wp_locale' => 'de_DE' ), 'el' => array( 'english_label' => 'Greek', 'native_label' => 'Ελληνικά', 'flag' => 'gr', 'rtl' => 0, 'wp_locale' => 'el' ), 'en' => array( 'english_label' => 'English', 'native_label' => 'English', 'flag' => 'us', 'rtl' => 0, 'wp_locale' => 'en_US' ), 'gu' => array( 'english_label' => 'Gujarati', 'native_label' => 'ગુજરાતી', 'flag' => 'in', 'rtl' => 0, 'wp_locale' => 'gu' ), 'he' => array( 'english_label' => 'Hebrew', 'native_label' => 'עברית', 'flag' => 'il', 'rtl' => 1, 'wp_locale' => 'he_IL' ), 'hi' => array( 'english_label' => 'Hindi', 'native_label' => 'हिन्दी', 'flag' => 'in', 'rtl' => 0, 'wp_locale' => 'hi_IN' ), 'hu' => array( 'english_label' => 'Hungarian', 'native_label' => 'Magyar', 'flag' => 'hu', 'rtl' => 0, 'wp_locale' => 'hu_HU' ), 'is' => array( 'english_label' => 'Icelandic', 'native_label' => 'Íslenska', 'flag' => 'is', 'rtl' => 0, 'wp_locale' => 'is_IS' ), 'id' => array( 'english_label' => 'Indonesian', 'native_label' => 'Bahasa Indonesia', 'flag' => 'id', 'rtl' => 0, 'wp_locale' => 'id_ID' ), 'ga' => array( 'english_label' => 'Irish', 'native_label' => 'Gaeilge', 'flag' => 'ie', 'rtl' => 0, 'wp_locale' => 'ga' ), 'it' => array( 'english_label' => 'Italian', 'native_label' => 'Italiano', 'flag' => 'it', 'rtl' => 0, 'wp_locale' => 'it_IT' ), 'ja' => array( 'english_label' => 'Japanese', 'native_label' => '日本語', 'flag' => 'jp', 'rtl' => 0, 'wp_locale' => 'ja' ), 'ko' => array( 'english_label' => 'Korean', 'native_label' => '한국어', 'flag' => 'kr', 'rtl' => 0, 'wp_locale' => 'ko_KR' ), 'lv' => array( 'english_label' => 'Latvian', 'native_label' => 'Latviešu', 'flag' => 'lv', 'rtl' => 0, 'wp_locale' => 'lv' ), 'lt' => array( 'english_label' => 'Lithuanian', 'native_label' => 'Lietuvių', 'flag' => 'lt', 'rtl' => 0, 'wp_locale' => 'lt_LT' ), 'ms' => array( 'english_label' => 'Malay', 'native_label' => 'Bahasa Melayu', 'flag' => 'my', 'rtl' => 0, 'wp_locale' => 'ms_MY' ), 'mr' => array( 'english_label' => 'Marathi', 'native_label' => 'मराठी', 'flag' => 'in', 'rtl' => 0, 'wp_locale' => 'mr' ), 'no' => array( 'english_label' => 'Norwegian', 'native_label' => 'Norsk', 'flag' => 'no', 'rtl' => 0, 'wp_locale' => 'nb_NO' ), 'ps' => array( 'english_label' => 'Pashto', 'native_label' => 'پښتو', 'flag' => 'af', 'rtl' => 1, 'wp_locale' => 'ps' ), 'pl' => array( 'english_label' => 'Polish', 'native_label' => 'Polski', 'flag' => 'pl', 'rtl' => 0, 'wp_locale' => 'pl_PL' ), 'pt' => array( 'english_label' => 'Portuguese', 'native_label' => 'Português', 'flag' => 'pt', 'rtl' => 0, 'wp_locale' => 'pt_PT' ), 'pt-br' => array( 'english_label' => 'Portuguese (Brazil)', 'native_label' => 'Português do Brasil', 'flag' => 'br', 'rtl' => 0, 'wp_locale' => 'pt_BR' ), 'ro' => array( 'english_label' => 'Romanian', 'native_label' => 'Română', 'flag' => 'ro', 'rtl' => 0, 'wp_locale' => 'ro_RO' ), 'ru' => array( 'english_label' => 'Russian', 'native_label' => 'Русский', 'flag' => 'ru', 'rtl' => 0, 'wp_locale' => 'ru_RU' ), 'sr' => array( 'english_label' => 'Serbian', 'native_label' => 'Српски', 'flag' => 'rs', 'rtl' => 0, 'wp_locale' => 'sr_RS' ), 'sk' => array( 'english_label' => 'Slovak', 'native_label' => 'Slovenčina', 'flag' => 'sk', 'rtl' => 0, 'wp_locale' => 'sk_SK' ), 'sl' => array( 'english_label' => 'Slovenian', 'native_label' => 'Slovenščina', 'flag' => 'si', 'rtl' => 0, 'wp_locale' => 'sl_SI' ), 'es' => array( 'english_label' => 'Spanish', 'native_label' => 'Español', 'flag' => 'es', 'rtl' => 0, 'wp_locale' => 'es_ES' ), 'sw' => array( 'english_label' => 'Swahili', 'native_label' => 'Kiswahili', 'flag' => 'ke', 'rtl' => 0, 'wp_locale' => 'sw' ), 'sv' => array( 'english_label' => 'Swedish', 'native_label' => 'Svenska', 'flag' => 'se', 'rtl' => 0, 'wp_locale' => 'sv_SE' ), 'ta' => array( 'english_label' => 'Tamil', 'native_label' => 'தமிழ்', 'flag' => 'in', 'rtl' => 0, 'wp_locale' => 'ta_IN' ), 'te' => array( 'english_label' => 'Telugu', 'native_label' => 'తెలుగు', 'flag' => 'in', 'rtl' => 0, 'wp_locale' => 'te' ), 'th' => array( 'english_label' => 'Thai', 'native_label' => 'ไทย', 'flag' => 'th', 'rtl' => 0, 'wp_locale' => 'th' ), 'tr' => array( 'english_label' => 'Turkish', 'native_label' => 'Türkçe', 'flag' => 'tr', 'rtl' => 0, 'wp_locale' => 'tr_TR' ), 'uk' => array( 'english_label' => 'Ukrainian', 'native_label' => 'Українська', 'flag' => 'ua', 'rtl' => 0, 'wp_locale' => 'uk' ), 'ur' => array( 'english_label' => 'Urdu', 'native_label' => 'اردو', 'flag' => 'pk', 'rtl' => 1, 'wp_locale' => 'ur' ), 'uz' => array( 'english_label' => 'Uzbek', 'native_label' => 'Oʻzbekcha', 'flag' => 'uz', 'rtl' => 0, 'wp_locale' => 'uz_UZ' ), 'vi' => array( 'english_label' => 'Vietnamese', 'native_label' => 'Tiếng Việt', 'flag' => 'vn', 'rtl' => 0, 'wp_locale' => 'vi' ), 'cy' => array( 'english_label' => 'Welsh', 'native_label' => 'Cymraeg', 'flag' => 'gb', 'rtl' => 0, 'wp_locale' => 'cy' ), ); /** * Map short language codes to WordPress locale identifiers. * * @var array */ private $wp_locale_map = array( 'en' => 'en_US', 'de' => 'de_DE', 'es' => 'es_ES', 'fr' => 'fr_FR', 'ar' => 'ar', ); /** * Minimal frontend UI fallback dictionary for common commerce strings. * * Used only when translated text still equals source text. * * @var array> */ private $ui_fallback_dictionary = array( 'de' => array( 'Add to cart' => 'In den Warenkorb', 'Read more' => 'Mehr erfahren', 'Select options' => 'Optionen auswählen', 'View cart' => 'Warenkorb ansehen', 'Proceed to checkout' => 'Zur Kasse', 'Checkout' => 'Kasse', 'Place order' => 'Bestellung aufgeben', 'Buy now' => 'Jetzt kaufen', 'Related products' => 'Ähnliche Produkte', 'You may also like' => 'Das könnte Ihnen auch gefallen', 'Out of stock' => 'Nicht vorrätig', 'In stock' => 'Auf Lager', 'People watching this product now!' => 'Personen sehen sich dieses Produkt gerade an!', ), 'es' => array( 'Add to cart' => 'Añadir al carrito', 'Read more' => 'Leer más', 'Select options' => 'Seleccionar opciones', 'View cart' => 'Ver carrito', 'Proceed to checkout' => 'Finalizar compra', 'Checkout' => 'Pagar', 'Place order' => 'Realizar pedido', 'Buy now' => 'Comprar ahora', 'Related products' => 'Productos relacionados', 'You may also like' => 'También te puede gustar', 'Out of stock' => 'Agotado', 'In stock' => 'En stock', 'People watching this product now!' => 'Personas están viendo este producto ahora!', ), 'fr' => array( 'Add to cart' => 'Ajouter au panier', 'Read more' => 'En savoir plus', 'Select options' => 'Choisir les options', 'View cart' => 'Voir le panier', 'Proceed to checkout' => 'Passer à la caisse', 'Checkout' => 'Paiement', 'Place order' => 'Passer la commande', 'Buy now' => 'Acheter maintenant', 'Related products' => 'Produits similaires', 'You may also like' => 'Vous aimerez aussi', 'Out of stock' => 'Rupture de stock', 'In stock' => 'En stock', 'People watching this product now!' => 'Des personnes regardent ce produit en ce moment !', ), 'ar' => array( 'Add to cart' => 'أضف إلى السلة', 'Read more' => 'اقرأ المزيد', 'Select options' => 'اختر الخيارات', 'View cart' => 'عرض السلة', 'Proceed to checkout' => 'متابعة الدفع', 'Checkout' => 'الدفع', 'Place order' => 'إتمام الطلب', 'Buy now' => 'اشتر الآن', 'Related products' => 'منتجات ذات صلة', 'You may also like' => 'قد يعجبك أيضًا', 'Out of stock' => 'غير متوفر', 'In stock' => 'متوفر', 'People watching this product now!' => 'أشخاص يشاهدون هذا المنتج الآن!', ), ); /** * Register hooks. * * @return void */ public function init() { $this->ensure_options(); add_filter( 'determine_locale', array( $this, 'filter_determine_locale' ), 20 ); add_filter( 'gettext', array( $this, 'filter_gettext_fallback' ), 20, 3 ); add_filter( 'the_content', array( $this, 'filter_rendered_html_fallback' ), 20 ); add_filter( 'elementor/frontend/the_content', array( $this, 'filter_rendered_html_fallback' ), 20 ); add_filter( 'woocommerce_short_description', array( $this, 'filter_rendered_html_fallback' ), 20 ); add_filter( 'language_attributes', array( $this, 'filter_language_attributes' ) ); add_filter( 'body_class', array( $this, 'body_classes' ) ); } /** * Ensure options exist even after restoring a production database. * * @return void */ private function ensure_options() { $this->ensure_language_integrity(); if ( false === get_option( 'sitelang_settings', false ) ) { add_option( 'sitelang_settings', $this->get_default_settings(), '', false ); return; } $settings = $this->get_settings(); $merged = array_merge( $this->get_default_settings(), $settings ); if ( false === get_option( 'sitelang_manual_switcher_placement_migrated', false ) ) { $legacy_hook = ! empty( $merged['hook_name'] ) && 'woodmart_after_header' === $merged['hook_name']; if ( ! empty( $merged['hook_enabled'] ) && $legacy_hook ) { $merged['hook_enabled'] = 0; $merged['hook_name'] = ''; } add_option( 'sitelang_manual_switcher_placement_migrated', 1, '', false ); } if ( $merged !== $settings ) { update_option( 'sitelang_settings', $merged, false ); } } /** * Get enabled language rows. * * @return array> */ public function get_enabled() { $languages = get_option( self::LANGUAGES_OPTION, array() ); return is_array( $languages ) ? $languages : array(); } /** * Get enabled language codes. * * @return array */ public function get_enabled_codes() { return array_keys( $this->get_enabled() ); } /** * Get language data by code. * * @param string $code Language code. * @return array|null */ public function get_language( $code ) { $code = $this->sanitize_code( $code ); $languages = $this->get_enabled(); return isset( $languages[ $code ] ) ? $languages[ $code ] : null; } /** * Is language enabled. * * @param string $code Language code. * @return bool */ public function is_enabled( $code ) { return null !== $this->get_language( $code ); } /** * Get default language code. * * @return string */ public function get_default_code() { $settings = $this->get_settings(); return ! empty( $settings['default_language'] ) ? $this->sanitize_code( $settings['default_language'] ) : 'en'; } /** * Get default language label. * * @return string */ public function get_default_label() { $settings = $this->get_settings(); $presets = $this->get_presets(); $default = $presets[ $this->get_default_code() ]['english_label'] ?? 'English'; return ! empty( $settings['default_label'] ) ? sanitize_text_field( $settings['default_label'] ) : $default; } /** * Get default language native label. * * @return string */ public function get_default_native_label() { $settings = $this->get_settings(); $presets = $this->get_presets(); $default = $presets[ $this->get_default_code() ]['native_label'] ?? $this->get_default_label(); return ! empty( $settings['default_native'] ) ? sanitize_text_field( $settings['default_native'] ) : $default; } /** * Get all languages including default. * * @return array> */ public function get_all() { $settings = $this->get_settings(); $default_code = $this->get_default_code(); $all = array( $default_code => array( 'code' => $default_code, 'english_label' => $this->get_default_label(), 'native_label' => $this->get_default_native_label(), 'flag' => ! empty( $settings['default_flag'] ) ? sanitize_text_field( $settings['default_flag'] ) : ( $this->presets[ $default_code ]['flag'] ?? 'us' ), 'rtl' => isset( $settings['default_rtl'] ) ? (int) $settings['default_rtl'] : (int) ( $this->presets[ $default_code ]['rtl'] ?? 0 ), 'wp_locale' => $this->presets[ $default_code ]['wp_locale'] ?? '', 'is_default' => 1, ), ); foreach ( $this->get_enabled() as $code => $language ) { $language['code'] = $code; $language['is_default'] = 0; $all[ $code ] = $language; } return $all; } /** * Save enabled languages. * * @param array> $rows Posted rows. * @param bool $allow_empty Allow every added language to be disabled. * @return true|\WP_Error */ public function save_languages( array $rows, $allow_empty = false ) { $languages = $this->normalize_languages( $rows ); $current = $this->get_enabled(); if ( empty( $languages ) && ! empty( $current ) && ! $allow_empty ) { return new \WP_Error( 'sitelang_languages_empty_not_confirmed', __( 'SiteLang prevented all languages from being removed. Confirm the remove-all option if this is intentional.', 'sitelang' ) ); } $this->persist_languages( $languages ); update_option( 'sitelang_rewrite_flush_needed', 1, false ); update_option( 'sitelang_hard_rewrite_flush_needed', 1, false ); return true; } /** * Validate language rows and normalize them by code. * * Accepts both form rows and the associative format used by settings exports. * * @param array $rows Language rows. * @return array> */ public function normalize_languages( array $rows ) { $languages = array(); foreach ( $rows as $row_key => $row ) { if ( ! is_array( $row ) ) { continue; } $raw_code = isset( $row['code'] ) ? $row['code'] : ( is_string( $row_key ) ? $row_key : '' ); $code = $this->sanitize_code( $raw_code ); if ( '' === $code || $code === $this->get_default_code() ) { continue; } $preset = isset( $this->presets[ $code ] ) ? $this->presets[ $code ] : array(); $languages[ $code ] = array( 'code' => $code, 'english_label' => isset( $row['english_label'] ) && '' !== trim( (string) $row['english_label'] ) ? sanitize_text_field( $row['english_label'] ) : ( $preset['english_label'] ?? strtoupper( $code ) ), 'native_label' => isset( $row['native_label'] ) && '' !== trim( (string) $row['native_label'] ) ? sanitize_text_field( $row['native_label'] ) : ( $preset['native_label'] ?? strtoupper( $code ) ), 'flag' => isset( $row['flag'] ) && '' !== trim( (string) $row['flag'] ) ? sanitize_key( $row['flag'] ) : ( $preset['flag'] ?? '' ), 'rtl' => ! empty( $row['rtl'] ) ? 1 : 0, 'wp_locale' => isset( $row['wp_locale'] ) && '' !== trim( (string) $row['wp_locale'] ) ? sanitize_text_field( $row['wp_locale'] ) : ( $preset['wp_locale'] ?? '' ), ); } ksort( $languages ); return $languages; } /** * Restore an unexpectedly missing/corrupt registry from a last-known-good * copy or, on upgrade, from authoritative translation relationships. * * @return void */ private function ensure_language_integrity() { $raw = get_option( self::LANGUAGES_OPTION, false ); $backup = get_option( self::LANGUAGES_BACKUP_OPTION, false ); $stored_checksum = (string) get_option( self::LANGUAGES_CHECKSUM_OPTION, '' ); $languages = is_array( $raw ) ? $this->normalize_languages( $raw ) : array(); $backup = is_array( $backup ) ? $this->normalize_languages( $backup ) : array(); if ( ! empty( $languages ) ) { $checksum = $this->languages_checksum( $languages ); // Initialize integrity metadata for installs upgraded from an older release. if ( '' === $stored_checksum ) { $this->persist_languages( $languages ); return; } // An out-of-band or interrupted write changed only the primary option. // Restore the last complete registry when its checksum is still valid. if ( $checksum !== $stored_checksum && ! empty( $backup ) && $this->languages_checksum( $backup ) === $stored_checksum ) { $this->persist_languages( $backup ); $this->record_language_recovery( 'checksum' ); return; } if ( $checksum === $stored_checksum && ( $languages !== $raw || $backup !== $languages ) ) { $this->persist_languages( $languages ); } return; } if ( ! empty( $backup ) ) { $this->persist_languages( $backup ); $this->record_language_recovery( 'backup' ); return; } // Existing installs did not have integrity metadata. Recover their language // codes once from mappings instead of leaving valid translations unreachable. if ( '' === $stored_checksum ) { $discovered = $this->discover_mapped_languages(); $this->persist_languages( $discovered ); if ( ! empty( $discovered ) ) { $this->record_language_recovery( 'mappings' ); } return; } // A checksum for an empty registry records an intentional remove-all save. $this->persist_languages( array() ); } /** * Persist the primary registry and its recovery metadata together. * * @param array> $languages Languages. * @return void */ private function persist_languages( array $languages ) { update_option( self::LANGUAGES_OPTION, $languages, false ); update_option( self::LANGUAGES_BACKUP_OPTION, $languages, false ); update_option( self::LANGUAGES_CHECKSUM_OPTION, $this->languages_checksum( $languages ), false ); } /** * Build a stable checksum for the normalized registry. * * @param array> $languages Languages. * @return string */ private function languages_checksum( array $languages ) { return hash( 'sha256', (string) wp_json_encode( $languages ) ); } /** * Discover language codes already used by mapped translations. * * @return array> */ private function discover_mapped_languages() { global $wpdb; $codes = array(); $table = $wpdb->prefix . 'sitelang_translations'; if ( $table === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) ) { $mapped_codes = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT language_code FROM %i WHERE language_code <> ''", $table ) ); $codes = is_array( $mapped_codes ) ? $mapped_codes : array(); } if ( empty( $codes ) ) { $meta_codes = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value <> ''", '_sitelang_language' ) ); $codes = is_array( $meta_codes ) ? $meta_codes : array(); } $rows = array(); foreach ( $codes as $code ) { $code = $this->sanitize_code( $code ); if ( '' === $code || $code === $this->get_default_code() ) { continue; } $rows[ $code ] = array( 'code' => $code ); } return $this->normalize_languages( $rows ); } /** * Record a recovery for an administrator-facing notice and diagnostics. * * @param string $source Recovery source. * @return void */ private function record_language_recovery( $source ) { update_option( 'sitelang_rewrite_flush_needed', 1, false ); update_option( 'sitelang_hard_rewrite_flush_needed', 1, false ); update_option( self::LANGUAGES_RECOVERY_OPTION, array( 'time' => time(), 'source' => sanitize_key( $source ), ), false ); } /** * Get settings. * * @return array */ public function get_settings() { $settings = get_option( 'sitelang_settings', array() ); return is_array( $settings ) ? $settings : array(); } /** * Default plugin settings. * * @return array */ public function get_default_settings() { return array( 'default_language' => 'en', 'default_label' => 'English', 'default_native' => 'English', 'default_flag' => 'us', 'switcher_mode' => 'dropdown', 'switcher_preset' => 'minimal', 'switcher_show_flag' => 1, 'switcher_show_name' => 0, 'switcher_radius' => 6, 'switcher_bg' => '#ffffff', 'switcher_text' => '#1f2937', 'switcher_accent' => '#b68b2d', 'switcher_custom_hover' => 0, 'switcher_hover_bg' => '#eaf3f5', 'switcher_hover_text' => '#045f78', 'switcher_placement' => 'header-floating', 'hook_enabled' => 0, 'hook_name' => '', 'mobile_switcher_enabled' => 0, 'mobile_switcher_mode' => 'dropdown', 'mobile_switcher_target' => '', 'homepages' => array(), 'auto_publish_system_pages' => 0, 'merchant_exclude_translated_display_ads' => 1, ); } /** * Save settings. * * @param array $settings Settings to merge. * @return void */ public function save_settings( array $settings ) { $current = $this->get_settings(); update_option( 'sitelang_settings', array_merge( $current, $settings ), false ); } /** Save the source language and its display metadata. */ public function save_default_language( $code ) { $code = $this->sanitize_code( $code ); $presets = $this->get_presets(); if ( '' === $code || ! isset( $presets[ $code ] ) ) { return new \WP_Error( 'sitelang_invalid_default_language', __( 'Choose a valid original language.', 'sitelang' ) ); } $preset = $presets[ $code ]; $this->save_settings( array( 'default_language' => $code, 'default_label' => $preset['english_label'], 'default_native' => $preset['native_label'], 'default_flag' => $preset['flag'], 'default_rtl' => ! empty( $preset['rtl'] ) ? 1 : 0, ) ); return true; } /** * Get language presets. * * @return array> */ public function get_presets() { return $this->presets; } /** * Set current language. * * @param string $code Language code. * @return void */ public function set_current( $code ) { $this->current_language = $this->sanitize_code( $code ); } /** * Get current language. * * @return string */ public function get_current() { return $this->current_language ? $this->current_language : $this->get_default_code(); } /** * Is current language RTL. * * @param string|null $code Optional language code. * @return bool */ public function is_rtl( $code = null ) { $code = $code ? $this->sanitize_code( $code ) : $this->get_current(); if ( $code === $this->get_default_code() ) { $all = $this->get_all(); return ! empty( $all[ $code ]['rtl'] ); } $lang = $this->get_language( $code ); return $lang ? ! empty( $lang['rtl'] ) : false; } /** * Sanitize language code. * * @param string $code Language code. * @return string */ public function sanitize_code( $code ) { return strtolower( preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $code ) ); } /** * Add lang and dir attributes. * * @param string $output Language attributes. * @return string */ public function filter_language_attributes( $output ) { if ( $this->should_bypass_frontend_language_filters() ) { return $output; } $current = $this->get_current(); $output = preg_replace( '/\s?lang=("|\')[^"\']*("|\')/', '', $output ); $output = preg_replace( '/\s?dir=("|\')[^"\']*("|\')/', '', $output ); $output .= ' lang="' . esc_attr( str_replace( '_', '-', $current ) ) . '"'; $output .= ' dir="' . ( $this->is_rtl( $current ) ? 'rtl' : 'ltr' ) . '"'; return trim( $output ); } /** * Add frontend body classes. * * @param array $classes Body classes. * @return array */ public function body_classes( $classes ) { if ( $this->should_bypass_frontend_language_filters() ) { return $classes; } $current = $this->get_current(); $classes[] = 'sitelang-language'; $classes[] = 'lang-' . sanitize_html_class( $current ); if ( $this->is_rtl( $current ) ) { $classes[] = 'rtl-language'; } return $classes; } /** * Force frontend locale from current SiteLang language. * * @param string $locale Existing locale. * @return string */ public function filter_determine_locale( $locale ) { if ( $this->should_bypass_frontend_language_filters() ) { return $locale; } $language = $this->resolve_frontend_language(); if ( ! $language || $language === $this->get_default_code() ) { return $locale; } $mapped = $this->map_language_to_wp_locale( $language ); return $mapped ? $mapped : $locale; } /** * Translate common frontend commerce strings when no textdomain translation is available. * * @param string $translated Already translated text. * @param string $text Original text. * @param string $domain Text domain. * @return string */ public function filter_gettext_fallback( $translated, $text, $domain ) { if ( $this->should_bypass_frontend_language_filters() || '' === $text ) { return $translated; } $language = $this->resolve_frontend_language(); if ( ! $language || $language === $this->get_default_code() ) { return $translated; } if ( $translated !== $text ) { return $translated; } $dictionary = isset( $this->ui_fallback_dictionary[ $language ] ) ? $this->ui_fallback_dictionary[ $language ] : array(); if ( isset( $dictionary[ $text ] ) ) { return $dictionary[ $text ]; } return $translated; } /** * Fallback replacement for hardcoded frontend widget strings. * * Some builder/theme widgets print raw HTML strings that never hit gettext. * * @param string $html Rendered HTML. * @return string */ public function filter_rendered_html_fallback( $html ) { if ( $this->should_bypass_frontend_language_filters() || ! is_string( $html ) || '' === $html ) { return $html; } $language = $this->resolve_frontend_language(); if ( ! $language || $language === $this->get_default_code() ) { return $html; } $dictionary = isset( $this->ui_fallback_dictionary[ $language ] ) ? $this->ui_fallback_dictionary[ $language ] : array(); if ( empty( $dictionary ) ) { return $html; } return strtr( $html, $dictionary ); } /** * Get active frontend language from current state or request path. * * @return string */ private function resolve_frontend_language() { $current = $this->get_current(); if ( $current && $current !== $this->get_default_code() && $this->is_enabled( $current ) ) { return $this->sanitize_code( $current ); } if ( $this->is_frontend_woocommerce_ajax() ) { $referrer_language = $this->get_frontend_referrer_language(); if ( null !== $referrer_language ) { return $referrer_language; } } $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; if ( '' === $request_uri ) { return $this->get_default_code(); } $path = wp_parse_url( $request_uri, PHP_URL_PATH ); if ( ! is_string( $path ) || '' === $path ) { return $this->get_default_code(); } $parts = array_values( array_filter( explode( '/', trim( $path, '/' ) ) ) ); if ( empty( $parts ) ) { return $this->get_default_code(); } $candidate = $this->sanitize_code( $parts[0] ); return $this->is_enabled( $candidate ) ? $candidate : $this->get_default_code(); } /** * Avoid language rewrites/translations during admin, auth, AJAX, REST, and cron requests. * * @return bool */ private function should_bypass_frontend_language_filters() { if ( $this->is_frontend_woocommerce_ajax() ) { return false; } if ( is_admin() || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) { return true; } if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) { return true; } if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return true; } global $pagenow; if ( isset( $pagenow ) && in_array( $pagenow, array( 'wp-login.php', 'wp-register.php' ), true ) ) { return true; } $script = isset( $_SERVER['SCRIPT_NAME'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_NAME'] ) ) : ''; $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; return false !== strpos( $script, 'wp-login.php' ) || false !== strpos( $uri, '/wp-login.php' ) || false !== strpos( $uri, '/wp-admin/' ); } /** * Whether the current AJAX request belongs to the customer-facing store. * * @return bool */ private function is_frontend_woocommerce_ajax() { if ( defined( 'WC_AJAX' ) && WC_AJAX ) { return true; } if ( ! empty( $_REQUEST['wc-ajax'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only request context. return true; } if ( ! function_exists( 'wp_doing_ajax' ) || ! wp_doing_ajax() ) { return false; } return null !== $this->get_frontend_referrer_language(); } /** * Resolve an internal frontend referrer to its explicit language context. * * @return string|null */ private function get_frontend_referrer_language() { $referrer = wp_get_referer(); if ( ! $referrer && ! empty( $_SERVER['HTTP_REFERER'] ) ) { $referrer = esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ); } if ( ! $referrer ) { return null; } $home_parts = wp_parse_url( home_url( '/' ) ); $referrer_parts = wp_parse_url( $referrer ); if ( empty( $home_parts['host'] ) || empty( $referrer_parts['host'] ) || strtolower( $home_parts['host'] ) !== strtolower( $referrer_parts['host'] ) ) { return null; } $path = isset( $referrer_parts['path'] ) ? trim( $referrer_parts['path'], '/' ) : ''; $home_path = isset( $home_parts['path'] ) ? trim( $home_parts['path'], '/' ) : ''; if ( '' !== $home_path && ( $path === $home_path || 0 === strpos( $path, $home_path . '/' ) ) ) { $path = ltrim( substr( $path, strlen( $home_path ) ), '/' ); } $first_segment = strtok( $path, '/' ); if ( in_array( $first_segment, array( 'wp-admin', 'wp-login.php', 'wp-json' ), true ) ) { return null; } $candidate = $this->sanitize_code( $first_segment ? $first_segment : '' ); return $candidate && $this->is_enabled( $candidate ) ? $candidate : $this->get_default_code(); } /** * Map SiteLang language code to WordPress locale. * * @param string $language_code Language code. * @return string */ private function map_language_to_wp_locale( $language_code ) { $language_code = $this->sanitize_code( $language_code ); if ( isset( $this->wp_locale_map[ $language_code ] ) ) { return $this->wp_locale_map[ $language_code ]; } $language = $this->get_language( $language_code ); if ( is_array( $language ) && ! empty( $language['wp_locale'] ) ) { return sanitize_text_field( $language['wp_locale'] ); } return ''; } }