rules = $rules; $this->site_locale = sanitize_locale_name( get_locale() ); } public function register() { add_filter( 'glovario_current_locale', array( $this, 'filter_locale' ), 5, 2 ); add_filter( 'glovario_relevant_locales', array( $this, 'relevant_locales' ) ); } /** * Locale resolution modes available on this site. * * @return string[] */ public function modes() { $modes = (array) apply_filters( 'glovario_locale_modes', array( 'auto', 'wordpress' ) ); return array_values( array_unique( array_map( 'sanitize_key', $modes ) ) ); } /** * The language settings: mode, default_locale, second_locale ('' when the site * uses one language), and any extra keys such as global_switcher. * * @return array */ public function settings() { $site_locale = $this->site_locale ? $this->site_locale : 'en_US'; $saved = get_option( self::OPTION, array() ); $saved = is_array( $saved ) ? $saved : array(); $settings = wp_parse_args( $saved, array( 'mode' => 'auto', 'default_locale' => $site_locale, 'second_locale' => '', ) ); $settings['mode'] = in_array( $settings['mode'], $this->modes(), true ) ? $settings['mode'] : 'auto'; $default = $this->valid_locale( $settings['default_locale'] ); $default = $default ? $default : $site_locale; $second = $this->valid_locale( $settings['second_locale'] ); $settings['default_locale'] = $default; $settings['second_locale'] = $second === $default ? '' : $second; return $settings; } public function default_locale() { return $this->settings()['default_locale']; } /** * The second language, or an empty string when the site uses only the default. * * @return string */ public function second_locale() { return $this->settings()['second_locale']; } /** * The default language followed by the second language, when there is one. * * @return string[] */ public function languages() { $settings = $this->settings(); return '' === $settings['second_locale'] ? array( $settings['default_locale'] ) : array( $settings['default_locale'], $settings['second_locale'] ); } /** * Whether a locale is the default or the second language. * * @param string $locale Locale code. * @return bool */ public function is_supported_locale( $locale ) { $locale = sanitize_locale_name( (string) $locale ); $settings = $this->settings(); $free = $locale === $settings['default_locale'] || ( '' !== $settings['second_locale'] && $locale === $settings['second_locale'] ); /** * Filters whether a locale is one of the site's configured languages. * * @param bool $supported Whether the locale is the default or the second language. * @param string $locale Locale code. */ return (bool) apply_filters( 'glovario_is_supported_locale', $free, $locale ); } /** * Configured languages for display, previews, and diagnostics only; language * handling itself relies on default_locale() and second_locale(). * * @return string[] */ public function configured_locales() { /** * Filters the languages shown in Locale Settings, previews, and diagnostics. * * @param string[] $locales Default language and, when set, the second language. */ $locales = apply_filters( 'glovario_configured_locales', $this->languages() ); return $this->normalize_locales( is_array( $locales ) ? $locales : $this->languages() ); } /** * Validate and store the language settings. * * Other stored keys are kept unchanged. * * @param string $mode Resolution mode. * @param string $default_locale Default language. * @param string $second_locale Second language, or an empty string for none. * @param array $extra Additional settings stored by extensions. * @return array|\WP_Error */ public function save_settings( $mode, $default_locale, $second_locale = '', $extra = array() ) { $mode = sanitize_key( $mode ); $default = $this->valid_locale( $default_locale ); $second = $this->valid_locale( $second_locale ); if ( ! in_array( $mode, $this->modes(), true ) ) { return new \WP_Error( 'invalid_locale_mode', __( 'Choose a valid locale resolution mode.', 'glovario' ) ); } if ( '' === $default ) { return new \WP_Error( 'invalid_default_locale', __( 'Choose a valid default language.', 'glovario' ) ); } if ( '' !== trim( (string) $second_locale ) && '' === $second ) { return new \WP_Error( 'invalid_second_locale', __( 'Choose a valid second language, or leave it empty.', 'glovario' ) ); } if ( $second === $default ) { return new \WP_Error( 'same_locales', __( 'The second language must be different from the default language.', 'glovario' ) ); } $stored = get_option( self::OPTION, array() ); $settings = array_merge( is_array( $stored ) ? $stored : array(), is_array( $extra ) ? $extra : array(), array( 'mode' => $mode, 'default_locale' => $default, 'second_locale' => $second, ) ); update_option( self::OPTION, $settings, true ); // Read on every front-end request. return $this->settings(); } /** * A syntactically valid locale code, or an empty string. * * @param mixed $locale Candidate. * @return string */ private function valid_locale( $locale ) { $locale = sanitize_locale_name( trim( (string) $locale ) ); return preg_match( '/^[A-Za-z]{2,3}(?:_[A-Za-z0-9]{2,12}){0,3}$/', $locale ) ? $locale : ''; } public function filter_locale( $locale, $context = array() ) { $locale = sanitize_locale_name( (string) $locale ); $context = is_array( $context ) ? $context : array(); if ( ! empty( $context['_glovario_locale_explicit'] ) ) { return sanitize_locale_name( (string) ( $context['locale'] ?? $locale ) ); } $preview = $this->preview_request_locale(); if ( $preview ) { return $preview; } $settings = $this->settings(); if ( ! in_array( $settings['mode'], array( 'auto', 'wordpress' ), true ) ) { /** * Resolves the locale for an extension-provided mode. * * @param string $locale Locale WordPress determined for the request. * @param string $mode Configured resolution mode. * @param array $context Resolution context. */ $resolved = sanitize_locale_name( (string) apply_filters( 'glovario_locale_for_mode', $locale, $settings['mode'], $context ) ); return $resolved ? $resolved : $settings['default_locale']; } if ( 'auto' === $settings['mode'] ) { $integration = $this->integration(); if ( $integration['locale'] ) { return $integration['locale']; } } return $locale ? $locale : $settings['default_locale']; } public function integration() { $providers = $this->detected_providers(); $integration = $providers ? reset( $providers ) : array( 'provider' => 'wordpress', 'label' => __( 'WordPress request locale', 'glovario' ), 'locale' => '', ); /** * Filters the multilingual integration result. * * @param array $integration Provider id, label, and WordPress locale. */ $integration = apply_filters( 'glovario_multilingual_integration', $integration ); return array( 'provider' => sanitize_key( $integration['provider'] ?? 'wordpress' ), 'label' => sanitize_text_field( $integration['label'] ?? __( 'WordPress request locale', 'glovario' ) ), 'locale' => $this->normalize_provider_locale( $integration['locale'] ?? '' ), ); } public function detected_providers() { if ( is_array( $this->detected_providers ) ) { return $this->detected_providers; } $providers = array(); if ( function_exists( 'pll_current_language' ) ) { $raw_locale = $this->provider_value( 'polylang', static function () { return pll_current_language( 'locale' ); } ); $providers[] = array( 'provider' => 'polylang', 'label' => 'Polylang', 'raw_locale' => $raw_locale, 'locale' => $raw_locale, 'languages' => $this->provider_value( 'polylang', static function () { return function_exists( 'pll_languages_list' ) ? pll_languages_list( array( 'fields' => 'locale' ) ) : array(); }, array() ), ); } $translatepress = class_exists( 'TRP_Translate_Press' ) || function_exists( 'trp_get_current_language' ) || ! empty( $GLOBALS['TRP_LANGUAGE'] ); if ( $translatepress ) { $raw_locale = $this->provider_value( 'translatepress', static function () { return function_exists( 'trp_get_current_language' ) ? trp_get_current_language() : ( $GLOBALS['TRP_LANGUAGE'] ?? '' ); } ); $providers[] = array( 'provider' => 'translatepress', 'label' => 'TranslatePress', 'raw_locale' => $raw_locale, 'locale' => $raw_locale, 'languages' => $this->provider_value( 'translatepress', static function () { if ( ! class_exists( 'TRP_Translate_Press' ) || ! method_exists( 'TRP_Translate_Press', 'get_trp_instance' ) ) { return array(); } $settings = \TRP_Translate_Press::get_trp_instance()->get_component( 'settings' )->get_settings(); return $settings['translation-languages'] ?? array(); }, array() ), ); } $weglot = $this->weglot_state(); if ( $weglot['available'] ) { $providers[] = array( 'provider' => 'weglot', 'label' => 'Weglot', 'raw_locale' => $weglot['locale'], 'locale' => $weglot['locale'], 'destinations' => $weglot['destinations'], 'languages' => $weglot['destinations'], ); } /** * Filters the complete ordered list of detected multilingual providers. * The first entry is the native provider Glovario follows automatically. * * @param array $providers Provider identifiers, labels, and locale details. */ $providers = apply_filters( 'glovario_detected_multilingual_providers', $providers ); $clean = array(); foreach ( (array) $providers as $provider ) { if ( ! is_array( $provider ) ) { continue; } $id = sanitize_key( $provider['provider'] ?? '' ); if ( ! $id || isset( $clean[ $id ] ) ) { continue; } $raw_locale = sanitize_locale_name( (string) ( $provider['raw_locale'] ?? ( $provider['locale'] ?? '' ) ) ); $clean[ $id ] = array( 'provider' => $id, 'label' => sanitize_text_field( $provider['label'] ?? $id ), 'raw_locale' => $raw_locale, 'locale' => $this->normalize_provider_locale( $provider['locale'] ?? $raw_locale ), 'destinations' => $this->normalize_locales( $provider['destinations'] ?? array() ), 'languages' => array_values( array_unique( array_filter( array_map( array( $this, 'normalize_provider_locale' ), $this->normalize_locales( $provider['languages'] ?? array() ) ) ) ) ), ); } $this->detected_providers = array_values( $clean ); return $this->detected_providers; } /** * Languages the active multilingual plugin offers that are not configured * here; visitors in those languages see each variable's default value. * * @return string[] */ public function unconfigured_provider_locales() { $integration = $this->integration(); foreach ( $this->detected_providers() as $provider ) { if ( $provider['provider'] === $integration['provider'] ) { return array_values( array_filter( $provider['languages'], function ( $locale ) { return ! $this->is_supported_locale( $locale ); } ) ); } } return array(); } public function diagnostics() { $settings = $this->settings(); $providers = $this->detected_providers(); $integration = $this->integration(); $status = $this->status(); $raw_locale = ''; foreach ( $providers as $provider ) { if ( $provider['provider'] === $integration['provider'] ) { $raw_locale = $provider['raw_locale']; break; } } return array( 'mode' => $settings['mode'], 'providers' => $providers, 'integration' => $integration, 'provider_raw_locale' => $raw_locale, 'resolved_locale' => $status['locale'], 'wordpress_locale' => sanitize_locale_name( determine_locale() ), 'site_locale' => $this->site_locale, 'configured_locales' => $this->configured_locales(), 'default_locale' => $settings['default_locale'], 'missing_packs' => $this->missing_wordpress_language_packs(), ); } public function status() { $settings = $this->settings(); $integration = $this->integration(); $source = 'wordpress' === $settings['mode'] ? __( 'WordPress request locale', 'glovario' ) : $integration['label']; $status = array( 'mode' => $settings['mode'], 'source' => $source, 'integration' => $integration, 'locale' => $this->filter_locale( determine_locale(), array() ), ); /** * Filters the locale detection summary shown in Locale Settings. * * @param array $status Mode, source label, integration, and resolved locale. * @param array $settings Locale settings. */ return apply_filters( 'glovario_locale_status', $status, $settings ); } public function missing_wordpress_language_packs() { $installed = array_merge( array( 'en_US' ), get_available_languages() ); return array_values( array_diff( $this->configured_locales(), $installed ) ); } public function preview_locales() { return $this->normalize_locales( array_merge( $this->configured_locales(), array( determine_locale(), $this->site_locale ), get_available_languages(), $this->rules->used_locales() ) ); } public function relevant_locales( $locales ) { return array_values( array_unique( array_merge( (array) $locales, $this->configured_locales() ) ) ); } public function mode() { return $this->settings()['mode']; } public function has_multilingual_integration() { $detected = ! empty( $this->detected_providers() ); return (bool) apply_filters( 'glovario_has_multilingual_integration', $detected ); } private function provider_value( $provider, $callback, $fallback = '' ) { try { return call_user_func( $callback ); } catch ( \Throwable $exception ) { do_action( 'glovario_multilingual_integration_error', $provider, $exception ); return $fallback; } } private function weglot_state() { if ( is_array( $this->weglot_state ) ) { return $this->weglot_state; } $state = array( 'available' => false, 'locale' => '', 'destinations' => array(), ); if ( function_exists( 'weglot_get_current_language' ) && function_exists( 'weglot_get_destination_languages' ) ) { try { $state['destinations'] = $this->weglot_destination_codes( weglot_get_destination_languages() ); $state['available'] = ! empty( $state['destinations'] ); if ( $state['available'] ) { $state['locale'] = weglot_get_current_language(); } } catch ( \Throwable $exception ) { do_action( 'glovario_multilingual_integration_error', 'weglot', $exception ); } } /** * Filters Weglot's detected state. This is useful for compatibility * layers and for hosts that load Weglot's helpers unusually late. * * @param array $state Availability, current locale, and destination codes. */ $state = apply_filters( 'glovario_weglot_integration_state', $state ); $state = is_array( $state ) ? $state : array(); $this->weglot_state = array( 'available' => ! empty( $state['available'] ), 'locale' => sanitize_locale_name( (string) ( $state['locale'] ?? '' ) ), 'destinations' => $this->normalize_locales( $state['destinations'] ?? array() ), ); return $this->weglot_state; } private function weglot_destination_codes( $destinations ) { $codes = array(); foreach ( (array) $destinations as $destination ) { if ( is_string( $destination ) ) { $code = $destination; } elseif ( is_array( $destination ) ) { if ( ( array_key_exists( 'public', $destination ) && ! $destination['public'] ) || ( array_key_exists( 'enabled', $destination ) && ! $destination['enabled'] ) ) { continue; } $code = $destination['language_to'] ?? ( $destination['language'] ?? ( $destination['code'] ?? '' ) ); } else { continue; } $code = sanitize_locale_name( (string) $code ); if ( $code ) { $codes[] = $code; } } return array_values( array_unique( $codes ) ); } public function preview_request_locale() { if ( ! current_user_can( 'manage_options' ) || empty( $_GET[ self::PREVIEW_QUERY ] ) || empty( $_GET['_glovario_locale_nonce'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Presence check only; the nonce is verified next. return ''; } $nonce = sanitize_text_field( wp_unslash( $_GET['_glovario_locale_nonce'] ) ); if ( ! wp_verify_nonce( $nonce, 'glovario_preview_locale' ) ) { return ''; } $locale = sanitize_locale_name( wp_unslash( $_GET[ self::PREVIEW_QUERY ] ) ); return in_array( $locale, $this->preview_locales(), true ) ? $locale : ''; } public function normalize_locales( $locales ) { if ( is_string( $locales ) ) { $locales = explode( ',', $locales ); } $normalized = array(); foreach ( (array) $locales as $locale ) { $locale = sanitize_locale_name( trim( (string) $locale ) ); if ( preg_match( '/^[A-Za-z]{2,3}(?:_[A-Za-z0-9]{2,12}){0,3}$/', $locale ) ) { $normalized[ $locale ] = true; } } return array_keys( $normalized ); } private function normalize_provider_locale( $locale ) { $locale = sanitize_locale_name( (string) $locale ); if ( '' === $locale ) { return ''; } $known = $this->preview_locales_without_integration(); if ( in_array( $locale, $known, true ) || false !== strpos( $locale, '_' ) ) { return $locale; } $prefix = strtolower( $locale ) . '_'; foreach ( $known as $candidate ) { if ( 0 === strpos( strtolower( $candidate ), $prefix ) ) { return $candidate; } } return $locale; } private function preview_locales_without_integration() { return $this->normalize_locales( array_merge( $this->configured_locales(), array( determine_locale(), $this->site_locale ), get_available_languages(), $this->rules->used_locales() ) ); } public function is_rest_request() { return defined( 'REST_REQUEST' ) && REST_REQUEST; } }