*/ private $presets = array( 'minimal', 'soft', 'compact', 'floating', 'underline', 'segmented', 'borderless', 'glass', 'dark', 'flag-chip', 'capsule', 'solid', 'outline', 'muted', 'floating-popup', 'compact-flag-dropdown', 'language-card', 'labeled-language-button' ); /** * Languages service. * * @var Languages */ private $languages; /** * Mapping repository. * * @var Mapping_Repository */ private $mapping; /** * Router. * * @var Router */ private $router; /** * Constructor. * * @param Languages $languages Languages service. * @param Mapping_Repository $mapping Mapping repository. * @param Router $router Router. */ public function __construct( Languages $languages, Mapping_Repository $mapping, Router $router ) { $this->languages = $languages; $this->mapping = $mapping; $this->router = $router; } /** * Register hooks. * * @return void */ public function init() { add_shortcode( 'sitelang_switcher', array( $this, 'shortcode' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'register_assets' ) ); add_action( 'init', array( $this, 'register_block' ) ); add_action( 'wp', array( $this, 'maybe_add_theme_hook' ) ); add_action( 'wp_footer', array( $this, 'render_mobile_nav_switcher' ), 8 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_shortcode_cleanup' ) ); } /** * Register frontend assets. * * @return void */ public function register_assets() { wp_register_style( 'sitelang-frontend', SITELANG_URL . 'assets/css/frontend.css', array(), (string) filemtime( SITELANG_PATH . 'assets/css/frontend.css' ) ); wp_register_script( 'sitelang-frontend', SITELANG_URL . 'assets/js/frontend.js', array(), (string) filemtime( SITELANG_PATH . 'assets/js/frontend.js' ), true ); } /** * Register Gutenberg block. * * @return void */ public function register_block() { wp_register_script( 'sitelang-switcher-block', SITELANG_URL . 'assets/js/block.js', array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-server-side-render' ), SITELANG_VERSION, true ); register_block_type( 'sitelang/switcher', array( 'editor_script' => 'sitelang-switcher-block', 'render_callback' => array( $this, 'render_block' ), 'attributes' => array( 'mode' => array( 'type' => 'string', 'default' => 'dropdown', ), 'style' => array( 'type' => 'string', 'default' => 'minimal', ), ), ) ); } /** * Attach switcher to configured theme hook. * * @return void */ public function maybe_add_theme_hook() { $settings = $this->languages->get_settings(); if ( empty( $settings['hook_enabled'] ) || empty( $settings['hook_name'] ) ) { return; } $hook = sanitize_key( $settings['hook_name'] ); add_action( $hook, function () { $settings = $this->languages->get_settings(); $placement = ! empty( $settings['switcher_placement'] ) ? sanitize_html_class( $settings['switcher_placement'] ) : 'header-floating'; echo '
'; echo $this->render( array( 'placement' => $placement ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '
'; }, 30 ); } /** * Render an opt-in switcher mount for theme mobile navigation drawers. * * The frontend script moves this element into Woodmart's mobile navigation * or into a custom selector supplied by the administrator. It remains hidden * when no compatible mobile navigation exists. * * @return void */ public function render_mobile_nav_switcher() { if ( is_admin() || wp_doing_ajax() ) { return; } $settings = $this->languages->get_settings(); if ( empty( $settings['mobile_switcher_enabled'] ) ) { return; } $mode = isset( $settings['mobile_switcher_mode'] ) ? sanitize_key( $settings['mobile_switcher_mode'] ) : 'dropdown'; if ( ! in_array( $mode, array( 'dropdown', 'pills' ), true ) ) { $mode = isset( $settings['switcher_mode'] ) ? sanitize_key( $settings['switcher_mode'] ) : 'dropdown'; } $selector = isset( $settings['mobile_switcher_target'] ) ? sanitize_text_field( $settings['mobile_switcher_target'] ) : ''; $selector = $selector ? $selector : '.mobile-nav[role="navigation"], .mobile-nav'; echo ''; } /** * Shortcode callback. * * @param array $atts Shortcode attributes. * @return string */ public function shortcode( $atts ) { if ( is_admin() ) { return ''; } $atts = shortcode_atts( array( 'mode' => '', 'style' => '', 'show_name' => '', 'show_flag' => '', ), $atts, 'sitelang_switcher' ); return $this->render( $atts ); } /** * Load cleanup for themes that print unprocessed header shortcodes in wp-admin. * * Some builders reuse frontend header content inside admin screens without * running do_shortcode(). The shortcode should never be visible there. * * @return void */ public function enqueue_admin_shortcode_cleanup( $hook = '' ) { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; $site_lang_screen = in_array( $page, array( 'sitelang-wc', 'sitelang-wc-setup', 'sitelang-license' ), true ) || ( $screen && in_array( (string) $screen->post_type, array( 'page', 'post', 'product', 'html_block', 'cms_block' ), true ) ); if ( ! $site_lang_screen ) { return; } wp_enqueue_script( 'sitelang-admin-shortcode-cleanup', SITELANG_URL . 'assets/js/admin-shortcode-cleanup.js', array(), SITELANG_VERSION, true ); } /** * Block render callback. * * @param array $attributes Block attributes. * @return string */ public function render_block( $attributes ) { return $this->render( $attributes ); } /** * Render switcher. * * @param array $args Render args. * @return string */ public function render( array $args = array() ) { if ( is_admin() && ! wp_doing_ajax() ) { return ''; } $languages = (array) apply_filters( 'sitelang_frontend_languages', $this->languages->get_all() ); if ( count( $languages ) < 2 ) { return ''; } wp_enqueue_style( 'sitelang-frontend' ); wp_enqueue_script( 'sitelang-frontend' ); $settings = $this->languages->get_settings(); $mode = ! empty( $args['mode'] ) ? sanitize_key( $args['mode'] ) : ( $settings['switcher_mode'] ?? 'dropdown' ); $preset = ! empty( $args['style'] ) ? sanitize_key( $args['style'] ) : ( $settings['switcher_preset'] ?? 'minimal' ); $show_name = '' !== ( $args['show_name'] ?? '' ) ? $this->to_bool( $args['show_name'] ) : ! empty( $settings['switcher_show_name'] ); $show_flag = '' !== ( $args['show_flag'] ?? '' ) ? $this->to_bool( $args['show_flag'] ) : ! empty( $settings['switcher_show_flag'] ); $current = $this->languages->get_current(); if ( ! isset( $languages[ $current ] ) ) { $current = $this->languages->get_default_code(); } $style = $this->inline_style( $settings, $args ); if ( ! in_array( $preset, $this->presets, true ) ) { $preset = 'minimal'; } if ( 'language-card' === $preset ) { return $this->render_language_card( $languages, $current, $style, $preset ); } if ( 'pills' === $mode && ! in_array( $preset, array( 'floating-popup', 'compact-flag-dropdown', 'labeled-language-button' ), true ) ) { return $this->render_pills( $languages, $current, $show_name, $show_flag, $style, $preset ); } return $this->render_dropdown( $languages, $current, $show_name, $show_flag, $style, $preset ); } /** * Render dropdown mode. * * @param array> $languages Languages. * @param string $current Current code. * @param bool $show_name Show name. * @param bool $show_flag Show flag. * @param string $style Inline custom properties. * @return string */ private function render_dropdown( array $languages, $current, $show_name, $show_flag, $style, $preset ) { $current_language = isset( $languages[ $current ] ) ? $languages[ $current ] : reset( $languages ); $language_prefix = self::get_language_prefix( $current_language ); $flag_shape = in_array( $preset, array( 'flag-chip', 'language-card', 'labeled-language-button' ), true ) ? 'circular' : 'rectangular'; $trigger_flag_size = 'flag-chip' === $preset ? 'chip' : 'trigger'; $is_disclosure = in_array( $preset, array( 'floating-popup', 'compact-flag-dropdown', 'labeled-language-button' ), true ); $show_flag = in_array( $preset, array( 'floating-popup', 'compact-flag-dropdown', 'labeled-language-button' ), true ) || $show_flag; self::$instance++; $menu_id = 'sitelang-language-menu-' . self::$instance; ob_start(); ?> > $languages Languages. * @param string $current Current code. * @param string $style Inline custom properties. * @param string $preset Preset key. * @return string */ private function render_language_card( array $languages, $current, $style, $preset ) { ob_start(); ?> > $languages Languages. * @param string $current Current code. * @param bool $show_name Show name. * @param bool $show_flag Show flag. * @param string $style Inline custom properties. * @return string */ private function render_pills( array $languages, $current, $show_name, $show_flag, $style, $preset ) { $flag_shape = in_array( $preset, array( 'flag-chip', 'language-card', 'labeled-language-button' ), true ) ? 'circular' : 'rectangular'; $flag_size = 'flag-chip' === $preset ? 'chip' : 'trigger'; ob_start(); ?> $language Language row. * @param bool $show_name Show name. * @param bool $show_flag Show flag. * @return string */ private function language_label_html( array $language, $show_name, $show_flag, $flag_shape = 'rectangular', $flag_size = 'menu' ) { $code = ! empty( $language['code'] ) ? strtoupper( $language['code'] ) : ''; $label = $show_name && ! empty( $language['native_label'] ) ? $language['native_label'] : $code; $html = ''; if ( $show_flag ) { $html .= Flag_Assets::render( $language, $flag_shape, 'switcher', $flag_size ); } $html .= '' . esc_html( $label ) . ''; return $html; } /** * Build dropdown option label HTML. * * @param array $language Language row. * @param bool $show_flag Show flag. * @return string */ private function language_menu_label_html( array $language, $show_flag, $flag_shape = 'rectangular' ) { $label = ! empty( $language['native_label'] ) ? $language['native_label'] : ( $language['code'] ?? '' ); $html = ''; if ( $show_flag ) { $html .= Flag_Assets::render( $language, $flag_shape, 'switcher', 'menu' ); } $html .= '' . esc_html( $label ) . ''; return $html; } /** * Return the word "Language" in the active language's native language. * * @param array $language Current language row. * @return string */ public static function get_language_prefix( array $language ) { $words = array( 'af' => 'Taal', 'sq' => 'Gjuha', 'am' => 'ቋንቋ', 'ar' => 'اللغة', 'hy' => 'Լեզու', 'az' => 'Dil', 'eu' => 'Hizkuntza', 'be' => 'Мова', 'bn' => 'ভাষা', 'bs' => 'Jezik', 'bg' => 'Език', 'ca' => 'Idioma', 'zh' => '语言', 'zh-tw' => '語言', 'hr' => 'Jezik', 'cs' => 'Jazyk', 'da' => 'Sprog', 'nl' => 'Taal', 'et' => 'Keel', 'fa' => 'زبان', 'fi' => 'Kieli', 'fr' => 'Langue', 'ka' => 'ენა', 'de' => 'Sprache', 'el' => 'Γλώσσα', 'en' => 'Language', 'gu' => 'ભાષા', 'he' => 'שפה', 'hi' => 'भाषा', 'hu' => 'Nyelv', 'is' => 'Tungumál', 'id' => 'Bahasa', 'ga' => 'Teanga', 'it' => 'Lingua', 'ja' => '言語', 'ko' => '언어', 'lv' => 'Valoda', 'lt' => 'Kalba', 'ms' => 'Bahasa', 'mr' => 'भाषा', 'no' => 'Språk', 'ps' => 'ژبه', 'pl' => 'Język', 'pt' => 'Idioma', 'pt-br' => 'Idioma', 'ro' => 'Limbă', 'ru' => 'Язык', 'sr' => 'Језик', 'sk' => 'Jazyk', 'sl' => 'Jezik', 'es' => 'Idioma', 'sw' => 'Lugha', 'sv' => 'Språk', 'ta' => 'மொழி', 'te' => 'భాష', 'th' => 'ภาษา', 'tr' => 'Dil', 'uk' => 'Мова', 'ur' => 'زبان', 'uz' => 'Til', 'vi' => 'Ngôn ngữ', 'cy' => 'Iaith', ); $code = isset( $language['code'] ) ? strtolower( str_replace( '_', '-', sanitize_text_field( (string) $language['code'] ) ) ) : ''; return $words[ $code ] ?? __( 'Language', 'sitelang' ); } /** * Parse shortcode-style booleans safely. * * @param mixed $value Value. * @return bool */ private function to_bool( $value ) { return in_array( strtolower( (string) $value ), array( '1', 'true', 'yes', 'on' ), true ); } /** * Build custom property string. * * @param array $settings Settings. * @return string */ private function inline_style( array $settings, array $args = array() ) { $radius = isset( $settings['switcher_radius'] ) ? min( 32, absint( $settings['switcher_radius'] ) ) : 6; $bg = ! empty( $settings['switcher_bg'] ) ? sanitize_hex_color( $settings['switcher_bg'] ) : '#ffffff'; $text = ! empty( $settings['switcher_text'] ) ? sanitize_hex_color( $settings['switcher_text'] ) : '#1f2937'; $accent = ! empty( $settings['switcher_accent'] ) ? sanitize_hex_color( $settings['switcher_accent'] ) : '#b68b2d'; $hover_bg = ! empty( $settings['switcher_hover_bg'] ) ? sanitize_hex_color( $settings['switcher_hover_bg'] ) : '#eaf3f5'; $hover_text = ! empty( $settings['switcher_hover_text'] ) ? sanitize_hex_color( $settings['switcher_hover_text'] ) : '#045f78'; return sprintf( '--sitelang-bg:%1$s;--sitelang-text:%2$s;--sitelang-accent:%3$s;--sitelang-radius:%4$dpx;--sitelang-hover-bg:%5$s;--sitelang-hover-text:%6$s;', $bg ? $bg : '#ffffff', $text ? $text : '#1f2937', $accent ? $accent : '#b68b2d', $radius, $hover_bg ? $hover_bg : '#eaf3f5', $hover_text ? $hover_text : '#045f78' ); } }