is_string( $v ) ? self::translate( $v, 'business_name' ) : $v ); } foreach ( [ 'vizyt_send_booking_created', 'vizyt_send_booking_cancelled', 'vizyt_send_booking_reminder', 'vizyt_send_booking_rescheduled' ] as $hook ) { add_action( $hook, [ self::class, 'switch_for_booking' ], 1 ); add_action( $hook, [ self::class, 'restore_locale' ], 999 ); } } /** * switch_to_locale() in admin / AJAX requests reloads plugin translations * in the user's locale, not the switched one — load ours explicitly. */ public static function reload_textdomain( string $locale ): void { unload_textdomain( 'vizyt' ); // not reloadable: no JIT reload in the user's locale $mo = WP_LANG_DIR . '/plugins/vizyt-' . $locale . '.mo'; // the language pack from translate.wordpress.org if ( is_readable( $mo ) ) { load_textdomain( 'vizyt', $mo, $locale ); return; } // No file for this locale: keep the source strings — and stop the // just-in-time loader (which uses the user's locale in admin). $GLOBALS['l10n_unloaded']['vizyt'] = true; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- WordPress core's own $l10n_unloaded list, not a new global. } /** * 'polylang', 'wpml' or '' (none). */ public static function provider(): string { if ( function_exists( 'pll_register_string' ) && function_exists( 'pll_translate_string' ) ) { return 'polylang'; } if ( defined( 'ICL_SITEPRESS_VERSION' ) && has_filter( 'wpml_translate_single_string' ) ) { return 'wpml'; } return ''; } /** * Current language slug ('' without a multilingual plugin). */ public static function current_lang(): string { if ( '' !== self::$forced_lang ) { return self::$forced_lang; } switch ( self::provider() ) { case 'polylang': return (string) pll_current_language( 'slug' ); case 'wpml': return (string) apply_filters( 'wpml_current_language', null ); default: return ''; } } /** * Default language slug ('' without a multilingual plugin). */ public static function default_lang(): string { switch ( self::provider() ) { case 'polylang': return (string) pll_default_language( 'slug' ); case 'wpml': return (string) apply_filters( 'wpml_default_language', null ); default: return ''; } } /** * Configured language slugs. * * @return string[] */ public static function languages(): array { switch ( self::provider() ) { case 'polylang': return (array) pll_languages_list( [ 'fields' => 'slug' ] ); case 'wpml': return array_keys( (array) apply_filters( 'wpml_active_languages', null, [ 'skip_missing' => 0 ] ) ); default: return []; } } /** * Configured languages with the names set in Polylang / WPML: * [ slug => name ], in the plugin's order. * * @return array */ public static function language_names(): array { switch ( self::provider() ) { case 'polylang': $slugs = (array) pll_languages_list( [ 'fields' => 'slug' ] ); $names = (array) pll_languages_list( [ 'fields' => 'name' ] ); return count( $slugs ) === count( $names ) ? array_combine( $slugs, $names ) : array_combine( $slugs, $slugs ); case 'wpml': $out = []; foreach ( (array) apply_filters( 'wpml_active_languages', null, [ 'skip_missing' => 0 ] ) as $slug => $lang ) { $out[ $slug ] = (string) ( $lang['translated_name'] ?? $lang['native_name'] ?? $slug ); } return $out; default: return []; } } /** * Is $lang a configured language? Unknown values are ignored everywhere. */ public static function is_language( string $lang ): bool { return '' !== $lang && in_array( $lang, self::languages(), true ); } /** * WordPress locale for a language slug (fr → fr_FR). */ public static function locale_for( string $lang ): string { if ( ! self::is_language( $lang ) ) { return ''; } if ( 'polylang' === self::provider() && function_exists( 'PLL' ) && isset( PLL()->model ) ) { $language = PLL()->model->get_language( $lang ); return $language ? (string) $language->locale : ''; } if ( 'wpml' === self::provider() ) { $all = (array) apply_filters( 'wpml_active_languages', null, [ 'skip_missing' => 0 ] ); return (string) ( $all[ $lang ]['default_locale'] ?? '' ); } return ''; } /** * Register one string. */ public static function register( string $name, string $value, bool $multiline = false ): void { if ( '' === trim( $value ) ) { return; } switch ( self::provider() ) { case 'polylang': pll_register_string( $name, $value, self::GROUP, $multiline ); break; case 'wpml': do_action( 'wpml_register_single_string', self::GROUP, $name, $value ); break; } } /** * Translate one string into $lang (current language when null). */ public static function translate( string $value, string $name, ?string $lang = null ): string { if ( '' === $value ) { return $value; } $lang = ( null !== $lang && self::is_language( $lang ) ) ? $lang : self::current_lang(); switch ( self::provider() ) { case 'polylang': return $lang ? (string) pll_translate_string( $value, $lang ) : $value; case 'wpml': return (string) apply_filters( 'wpml_translate_single_string', $value, self::GROUP, $name, $lang ?: null ); default: return $value; } } /** * Register every service, category and staff text, plus whatever * add-ons add through `vizyt_translatable_strings` ([ name => [ value, multiline ] ]). */ public static function register_all(): void { global $wpdb; $strings = []; foreach ( (array) $wpdb->get_results( "SELECT id, name, description FROM {$wpdb->prefix}vizyt_services" ) as $row ) { $strings[ 'service_' . $row->id . '_name' ] = [ (string) $row->name, false ]; $strings[ 'service_' . $row->id . '_description' ] = [ (string) $row->description, true ]; } foreach ( (array) $wpdb->get_results( "SELECT id, name, description FROM {$wpdb->prefix}vizyt_categories" ) as $row ) { $strings[ 'category_' . $row->id . '_name' ] = [ (string) $row->name, false ]; $strings[ 'category_' . $row->id . '_description' ] = [ (string) $row->description, true ]; } foreach ( (array) $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}vizyt_staff" ) as $row ) { $strings[ 'staff_' . $row->id . '_name' ] = [ (string) $row->name, false ]; $strings[ 'staff_' . $row->id . '_job_title' ] = [ (string) ( $row->job_title ?? '' ), false ]; $strings[ 'staff_' . $row->id . '_description' ] = [ (string) $row->description, true ]; } $strings['business_name'] = [ (string) get_option( 'vizyt_business_name', '' ), false ]; $strings = (array) apply_filters( 'vizyt_translatable_strings', $strings ); foreach ( $strings as $name => $pair ) { [ $value, $multiline ] = array_pad( (array) $pair, 2, false ); self::register( (string) $name, (string) $value, (bool) $multiline ); } } /** * Translate a public service row (object from Service::getPublicList()). */ public static function translate_service( object $row, ?string $lang = null ): object { $row = clone $row; $row->name = self::translate( (string) $row->name, 'service_' . $row->id . '_name', $lang ); $row->description = self::translate( (string) ( $row->description ?? '' ), 'service_' . $row->id . '_description', $lang ); if ( isset( $row->category_name, $row->category_id ) && $row->category_id ) { $row->category_name = self::translate( (string) $row->category_name, 'category_' . $row->category_id . '_name', $lang ); } return $row; } /** * Translate a list of public service rows. */ public static function translate_services( array $rows, ?string $lang = null ): array { return array_map( fn ( $row ) => self::translate_service( $row, $lang ), $rows ); } /* ------------------------------------------------------------------ * Bookings and emails * ------------------------------------------------------------------ */ /** * Save the widget's language on the booking (column `lang`). */ public static function store_booking_lang( int $booking_id, array $params ): void { global $wpdb; $lang = sanitize_key( (string) ( $params['lang'] ?? '' ) ); if ( ! self::is_language( $lang ) ) { $lang = self::current_lang(); } if ( $lang ) { $wpdb->update( $wpdb->prefix . 'vizyt_bookings', [ 'lang' => $lang ], [ 'id' => $booking_id ], [ '%s' ], [ '%d' ] ); } } public static function booking_lang( int $booking_id ): string { global $wpdb; $lang = (string) $wpdb->get_var( $wpdb->prepare( "SELECT lang FROM {$wpdb->prefix}vizyt_bookings WHERE id = %d", $booking_id ) ); return self::is_language( $lang ) ? $lang : ''; } private static bool $switched = false; /** * Language forced while sending a booking's emails (cron has none). */ private static string $forced_lang = ''; /** * Before an email cron callback: switch the locale (and the multilingual * plugin's language) to the booking's language. */ public static function switch_for_booking( int $booking_id ): void { $lang = self::booking_lang( $booking_id ); $locale = self::locale_for( $lang ); if ( ! $locale ) { return; } self::$forced_lang = $lang; if ( 'wpml' === self::provider() ) { do_action( 'wpml_switch_language', $lang ); } self::$switched = switch_to_locale( $locale ); } public static function restore_locale(): void { self::$forced_lang = ''; if ( self::$switched ) { restore_previous_locale(); self::$switched = false; } if ( 'wpml' === self::provider() ) { do_action( 'wpml_switch_language', null ); } } /** * Translate service/staff names in email data. */ public static function translate_email_data( array $data, int $booking_id ): array { $lang = self::booking_lang( $booking_id ) ?: null; if ( ! empty( $data['service_id'] ) ) { $data['service_name'] = self::translate( (string) $data['service_name'], 'service_' . (int) $data['service_id'] . '_name', $lang ); } if ( ! empty( $data['staff_id'] ) ) { $data['staff_name'] = self::translate( (string) $data['staff_name'], 'staff_' . (int) $data['staff_id'] . '_name', $lang ); } return $data; } }