From 9a16659c8a58567f0304aa75fd807543542eb6ad Mon Sep 17 00:00:00 2001 From: Yasir Arafat <148990700+Arafat-plugins@users.noreply.github.com> Date: Wed, 6 May 2026 13:14:45 +0600 Subject: [PATCH 1/9] fix: improve WPML listing translation compatibility --- .distignore | 10 +- README.md | 16 +- .../Ajax/Get_Directory_Type_Translations.php | 15 +- .../Hook/Add_Listing_Form_Translation.php | 58 ++- .../Hook/Directory_Builder_Actions.php | 21 +- .../Hook/Directory_Type_Meta_Translation.php | 192 +++++++ app/Controller/Hook/Email_Translation.php | 1 + app/Controller/Hook/Filter_Permalinks.php | 4 +- app/Controller/Hook/Init.php | 3 +- app/Controller/Hook/Query_Filtering.php | 100 ++++ app/Controller/Hook/REST_API.php | 17 +- .../Hook/Search_Form_Field_Translation.php | 479 ++++++++++-------- app/Controller/Hook/Search_Form_Filter.php | 36 +- app/Helper/WPML_Helper.php | 295 ++++++++++- directorist-wpml-integration.php | 2 +- readme.txt | 203 +++++--- wpml-config.xml | 13 +- 17 files changed, 1080 insertions(+), 385 deletions(-) create mode 100644 app/Controller/Hook/Directory_Type_Meta_Translation.php diff --git a/.distignore b/.distignore index a232681..455cb18 100644 --- a/.distignore +++ b/.distignore @@ -1,14 +1,22 @@ .DS_Store +.github +.wordpress-org +__build assets/src +dev-tools .distignore .gitignore composer.json +composer.lock +node_modules package.json package-lock.json pot.js postcss.config.js README.md +webpack.config.js webpack-entry-list.js webpack.common.js webpack.dev.js -webpack.prod.js \ No newline at end of file +webpack.prod.js +yarn.lock diff --git a/README.md b/README.md index df94a17..1e40de9 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,20 @@ Official WPML integration extension for [Directorist](https://directorist.com) t - Syncs Directorist listings, directory types, categories, locations and tags across WPML languages. - Makes Directorist settings, search forms, widgets/blocks and email templates translatable via WPML String Translation. -- Automatically syncs category `_directory_type` meta and directory `_default` flags across translations (as of 2.2.1). -- Tested with WordPress 6.9, PHP 7.4+, and the latest WPML core and addons. +- Automatically syncs category and listing `_directory_type` meta plus directory `_default` flags across translations. +- Tested with WordPress 6.9, PHP 7.4+, Directorist 8.7.1, WPML 4.9.2.1, and WP-CLI. +- Verified with a 138-check WP-CLI compatibility suite across English, French, German, Romanian, and Bengali. ## Changelog (short) -- **2.2.0 (2026‑02‑05)** - - Added automatic syncing of Directorist category directory assignments across WPML languages. - - Added WPML config for copying the default directory type flag across translations. +- **2.2.3 (2026-05-06)** + - Fixed translated listing visibility for all-listing and search-result queries. + - Fixed directory type meta synchronization across WPML translation groups. + - Added WP-CLI compatibility verification across `en`, `fr`, `de`, `ro`, and `bn`. + +- **2.2.1 (2026-02-05)** + - Added automatic syncing of Directorist category directory assignments across WPML languages. + - Added WPML config for copying the default directory type flag across translations. - Improved overall compatibility with WordPress 6.8 and current WPML releases. For full details, see `readme.txt` or the [plugin page](https://github.com/sovware/directorist-wpml-integration). diff --git a/app/Controller/Ajax/Get_Directory_Type_Translations.php b/app/Controller/Ajax/Get_Directory_Type_Translations.php index 484c291..a8336d8 100644 --- a/app/Controller/Ajax/Get_Directory_Type_Translations.php +++ b/app/Controller/Ajax/Get_Directory_Type_Translations.php @@ -57,7 +57,7 @@ public function create_directory_type_translation() { wp_send_json( $response->toArray() ); } - $directory_type_id = ( isset( $_REQUEST['directory_type_id'] ) ) ? sanitize_text_field( $_REQUEST['directory_type_id'] ) : 0; + $directory_type_id = isset( $_REQUEST['directory_type_id'] ) ? absint( $_REQUEST['directory_type_id'] ) : 0; $taranslation_language_code = ( isset( $_REQUEST['taranslation_language_code'] ) ) ? sanitize_text_field( $_REQUEST['taranslation_language_code'] ) : ''; if ( empty( $directory_type_id ) ) { @@ -134,8 +134,7 @@ public function get_wpml_active_languages() { * @return array */ public function get_directory_type_translations() { - $taxonomy = ATBDP_DIRECTORY_TYPE; - $element_type = apply_filters( 'wpml_element_type', $taxonomy ); + $taxonomy = ATBDP_DIRECTORY_TYPE; $directory_types = get_terms([ 'taxonomy' => $taxonomy, @@ -149,13 +148,13 @@ public function get_directory_type_translations() { $directory_type_translations = []; foreach( $directory_types as $directory_type ) { - $translation_id = apply_filters( 'wpml_element_trid', NULL, $directory_type->term_id, $element_type ); - $translation = apply_filters( 'wpml_get_element_translations', NULL, $translation_id, $element_type ); - - $directory_type_translations[ $directory_type->term_id ] = $translation; + $directory_type_translations[ $directory_type->term_id ] = WPML_Helper::get_element_translations( + $directory_type->term_id, + $taxonomy + ); } return $directory_type_translations; } -} \ No newline at end of file +} diff --git a/app/Controller/Hook/Add_Listing_Form_Translation.php b/app/Controller/Hook/Add_Listing_Form_Translation.php index 1ad1fe7..9a39795 100644 --- a/app/Controller/Hook/Add_Listing_Form_Translation.php +++ b/app/Controller/Hook/Add_Listing_Form_Translation.php @@ -11,6 +11,8 @@ namespace Directorist_WPML_Integration\Controller\Hook; +use Directorist_WPML_Integration\Helper\WPML_Helper; + class Add_Listing_Form_Translation { /** @@ -124,6 +126,27 @@ private function safe_slug( $text ) { return sanitize_key( sanitize_title( $text ) ); } + /** + * Get a stable WPML context ID for a directory type. + * + * Using the translation group ID keeps one set of strings shared across all + * language variants of the same directory type. + * + * @param int $directory_id Directory term ID. + * @return int + */ + private function get_directory_context_id( $directory_id ) { + $directory_id = (int) $directory_id; + + if ( $directory_id <= 0 ) { + return 0; + } + + $translation_group_id = WPML_Helper::get_element_trid( $directory_id, ATBDP_DIRECTORY_TYPE ); + + return $translation_group_id > 0 ? $translation_group_id : $directory_id; + } + /** * Register string with WPML * @@ -232,6 +255,11 @@ public function translate_field_data( $field_data ) { return $field_data; } + $directory_context_id = $this->get_directory_context_id( $directory_id ); + if ( empty( $directory_context_id ) ) { + return $field_data; + } + // Get field key $field_key = ! empty( $field_data['field_key'] ) ? $field_data['field_key'] : ''; if ( empty( $field_key ) ) { @@ -242,7 +270,7 @@ public function translate_field_data( $field_data ) { // Translate field label if ( ! empty( $field_data['label'] ) && is_string( $field_data['label'] ) ) { - $string_name = sprintf( 'add_listing_dir_%d_field_%s_label', $directory_id, $field_key_slug ); + $string_name = sprintf( 'add_listing_dir_%d_field_%s_label', $directory_context_id, $field_key_slug ); $this->register_wpml_string( $string_name, $field_data['label'] ); $translated = $this->translate_wpml_string( $field_data['label'], $string_name ); @@ -254,7 +282,7 @@ public function translate_field_data( $field_data ) { // Translate field placeholder if ( ! empty( $field_data['placeholder'] ) && is_string( $field_data['placeholder'] ) ) { - $string_name = sprintf( 'add_listing_dir_%d_field_%s_placeholder', $directory_id, $field_key_slug ); + $string_name = sprintf( 'add_listing_dir_%d_field_%s_placeholder', $directory_context_id, $field_key_slug ); $this->register_wpml_string( $string_name, $field_data['placeholder'] ); $translated = $this->translate_wpml_string( $field_data['placeholder'], $string_name ); @@ -266,7 +294,7 @@ public function translate_field_data( $field_data ) { // Translate field description if ( ! empty( $field_data['description'] ) && is_string( $field_data['description'] ) ) { - $string_name = sprintf( 'add_listing_dir_%d_field_%s_description', $directory_id, $field_key_slug ); + $string_name = sprintf( 'add_listing_dir_%d_field_%s_description', $directory_context_id, $field_key_slug ); $this->register_wpml_string( $string_name, $field_data['description'] ); $translated = $this->translate_wpml_string( $field_data['description'], $string_name ); @@ -310,7 +338,7 @@ public function translate_field_data( $field_data ) { ]; if ( $is_translatable || in_array( $property_key, $known_custom_properties ) ) { - $string_name = sprintf( 'add_listing_dir_%d_field_%s_%s', $directory_id, $field_key_slug, $this->safe_slug( $property_key ) ); + $string_name = sprintf( 'add_listing_dir_%d_field_%s_%s', $directory_context_id, $field_key_slug, $this->safe_slug( $property_key ) ); $this->register_wpml_string( $string_name, $property_value ); $translated = $this->translate_wpml_string( $property_value, $string_name ); @@ -332,7 +360,7 @@ public function translate_field_data( $field_data ) { $string_name = sprintf( 'add_listing_dir_%d_field_%s_option_%s', - $directory_id, + $directory_context_id, $field_key_slug, $option_value_slug ); @@ -350,7 +378,7 @@ public function translate_field_data( $field_data ) { $string_name = sprintf( 'add_listing_dir_%d_field_%s_option_%s', - $directory_id, + $directory_context_id, $field_key_slug, $option_slug ); @@ -402,6 +430,11 @@ public function translate_section_label( $load_section, $args ) { return $load_section; } + $directory_context_id = $this->get_directory_context_id( $directory_id ); + if ( empty( $directory_context_id ) ) { + return $load_section; + } + // Translate section label if ( ! empty( $args['section_data']['label'] ) && is_string( $args['section_data']['label'] ) ) { $original_label = $args['section_data']['label']; @@ -411,7 +444,7 @@ public function translate_section_label( $load_section, $args ) { ? $this->safe_slug( $args['section_data']['key'] ) : $this->safe_slug( $original_label ); - $string_name = sprintf( 'add_listing_dir_%d_section_%s_label', $directory_id, $section_slug ); + $string_name = sprintf( 'add_listing_dir_%d_section_%s_label', $directory_context_id, $section_slug ); // Register and translate $this->register_wpml_string( $string_name, $original_label ); @@ -419,7 +452,7 @@ public function translate_section_label( $load_section, $args ) { // Store translation for output replacement (since $args is passed by value) // Always store even if same, so output replacement can use it - $translation_key = sprintf( '%d_%s', $directory_id, $section_slug ); + $translation_key = sprintf( '%d_%s', $directory_context_id, $section_slug ); self::$section_translations[ $translation_key ] = [ 'original' => $original_label, 'translated' => $translated, @@ -514,6 +547,11 @@ public function translate_section_labels_in_output( $template_output, $args ) { return $template_output; } + $directory_context_id = $this->get_directory_context_id( $directory_id ); + if ( empty( $directory_context_id ) ) { + return $template_output; + } + // Translate all section labels in output // If form_data is empty, we'll still try to translate common section names $sections_to_translate = []; @@ -547,8 +585,8 @@ public function translate_section_labels_in_output( $template_output, $args ) { ? $this->safe_slug( $section['key'] ) : $this->safe_slug( $original_label ); - $string_name = sprintf( 'add_listing_dir_%d_section_%s_label', $directory_id, $section_slug ); - $translation_key = sprintf( '%d_%s', $directory_id, $section_slug ); + $string_name = sprintf( 'add_listing_dir_%d_section_%s_label', $directory_context_id, $section_slug ); + $translation_key = sprintf( '%d_%s', $directory_context_id, $section_slug ); // Check if we already have translation stored from translate_section_label() $translated = null; diff --git a/app/Controller/Hook/Directory_Builder_Actions.php b/app/Controller/Hook/Directory_Builder_Actions.php index 30f7f0e..767b83a 100644 --- a/app/Controller/Hook/Directory_Builder_Actions.php +++ b/app/Controller/Hook/Directory_Builder_Actions.php @@ -33,16 +33,7 @@ public function before_update_directory_type( $directory_type_id = 0 ) { set_transient( 'directorist_wpml_integration:current_language', $current_language ); - $element_type = ATBDP_DIRECTORY_TYPE; - $wpml_element_type = apply_filters( 'wpml_element_type', $element_type ); - - // get the language info of the original post - $get_language_args = [ - 'element_id' => $directory_type_id, - 'element_type' => $wpml_element_type - ]; - - $original_post_language_info = apply_filters( 'wpml_element_language_details', null, $get_language_args ); + $original_post_language_info = WPML_Helper::get_language_info( $directory_type_id, ATBDP_DIRECTORY_TYPE ); if ( empty( $original_post_language_info ) ) { return; @@ -105,16 +96,18 @@ public function after_update_default_directory_type( $directory_type_id = 0 ) { delete_transient( 'directorist_wpml_integration:current_language' ); - $element_type = apply_filters( 'wpml_element_type', ATBDP_DIRECTORY_TYPE ); - $translation_id = apply_filters( 'wpml_element_trid', NULL, $directory_type_id, $element_type ); - $translations = apply_filters( 'wpml_get_element_translations', NULL, $translation_id, $element_type ); + $translations = WPML_Helper::get_element_translations( $directory_type_id, ATBDP_DIRECTORY_TYPE ); if ( empty( $translations ) ) { return; } foreach( $translations as $translation ) { - update_term_meta( $translation->term_id, '_default', true ); + if ( empty( $translation->term_id ) ) { + continue; + } + + update_term_meta( (int) $translation->term_id, '_default', true ); } } } diff --git a/app/Controller/Hook/Directory_Type_Meta_Translation.php b/app/Controller/Hook/Directory_Type_Meta_Translation.php new file mode 100644 index 0000000..8d97f16 --- /dev/null +++ b/app/Controller/Hook/Directory_Type_Meta_Translation.php @@ -0,0 +1,192 @@ +is_wpml_active() ) { + return $value; + } + + $language_code = $this->get_post_language_code( (int) $object_id ); + if ( empty( $language_code ) ) { + return $value; + } + + self::$resolving_post_meta = true; + $raw_value = get_post_meta( $object_id, $meta_key, true ); + self::$resolving_post_meta = false; + + if ( empty( $raw_value ) || ! is_numeric( $raw_value ) ) { + return $raw_value; + } + + $translated_value = apply_filters( + 'wpml_object_id', + (int) $raw_value, + ATBDP_DIRECTORY_TYPE, + true, + $language_code + ); + + return ! empty( $translated_value ) ? (int) $translated_value : (int) $raw_value; + } + + /** + * Translate `_directory_type` when it is loaded from terms. + * + * Directorist categories and locations store directory type relationships in + * term meta. We map those IDs into the term language so add-listing/search + * screens stay in sync even when older translations still hold source IDs. + * + * @param mixed $value Existing metadata value. + * @param int $object_id Term ID. + * @param string $meta_key Meta key. + * @param bool $single Whether a single value is requested. + * + * @return mixed + */ + public function translate_term_directory_type_meta( $value, $object_id, $meta_key, $single ) { + if ( '_directory_type' !== $meta_key || ! $single || self::$resolving_term_meta || ! $this->is_wpml_active() ) { + return $value; + } + + $language_code = $this->get_term_language_code( (int) $object_id ); + if ( empty( $language_code ) ) { + return $value; + } + + self::$resolving_term_meta = true; + $raw_value = get_term_meta( $object_id, $meta_key, true ); + self::$resolving_term_meta = false; + + if ( empty( $raw_value ) || ! is_array( $raw_value ) ) { + return $raw_value; + } + + $translated_ids = []; + + foreach ( wp_parse_id_list( $raw_value ) as $directory_id ) { + $translated_id = apply_filters( + 'wpml_object_id', + $directory_id, + ATBDP_DIRECTORY_TYPE, + true, + $language_code + ); + + if ( ! empty( $translated_id ) ) { + $translated_ids[] = (int) $translated_id; + } + } + + return ! empty( $translated_ids ) + ? array_values( array_unique( $translated_ids ) ) + : wp_parse_id_list( $raw_value ); + } + + /** + * Get a post's WPML language code. + * + * @param int $post_id Post ID. + * @return string + */ + private function get_post_language_code( $post_id ) { + $post_type = get_post_type( $post_id ); + + if ( empty( $post_type ) ) { + return ''; + } + + $language_info = WPML_Helper::get_language_info( $post_id, $post_type ); + + if ( empty( $language_info ) || ! is_object( $language_info ) ) { + return ''; + } + + if ( ! empty( $language_info->language_code ) ) { + return (string) $language_info->language_code; + } + + return ! empty( $language_info->language ) ? (string) $language_info->language : ''; + } + + /** + * Get a term's WPML language code. + * + * @param int $term_id Term ID. + * @return string + */ + private function get_term_language_code( $term_id ) { + $term = get_term( $term_id ); + + if ( empty( $term ) || is_wp_error( $term ) || empty( $term->taxonomy ) ) { + return ''; + } + + $language_info = WPML_Helper::get_language_info( $term_id, $term->taxonomy ); + + if ( empty( $language_info ) || ! is_object( $language_info ) ) { + return ''; + } + + if ( ! empty( $language_info->language_code ) ) { + return (string) $language_info->language_code; + } + + return ! empty( $language_info->language ) ? (string) $language_info->language : ''; + } +} diff --git a/app/Controller/Hook/Email_Translation.php b/app/Controller/Hook/Email_Translation.php index a8ff6f6..47174db 100644 --- a/app/Controller/Hook/Email_Translation.php +++ b/app/Controller/Hook/Email_Translation.php @@ -68,6 +68,7 @@ public function after_send_email() { } do_action( 'wpml_switch_language', $previous_language ); + delete_transient( 'directorist_wpml_integration_before_change_current_language' ); } diff --git a/app/Controller/Hook/Filter_Permalinks.php b/app/Controller/Hook/Filter_Permalinks.php index 2587bf1..5964604 100644 --- a/app/Controller/Hook/Filter_Permalinks.php +++ b/app/Controller/Hook/Filter_Permalinks.php @@ -424,7 +424,7 @@ public function get_current_page_taxonomy_data() { continue; } - $term_page_translations = apply_filters( 'wpml_get_element_translations', null, $term_page_id, 'post_page' ); + $term_page_translations = WPML_Helper::get_element_translations( $term_page_id, 'page' ); if ( empty( $term_page_translations ) ) { continue; @@ -591,7 +591,7 @@ public function wpml_language_url_format_is_pretty() { */ public function is_id_current_page( $page_id = 0, $element_type = 'post_page' ) { - $page_translations = apply_filters( 'wpml_get_element_translations', null, $page_id, $element_type ); + $page_translations = WPML_Helper::get_element_translations( $page_id, $element_type ); if ( empty( $page_translations ) ) { return $page_id === get_the_ID(); diff --git a/app/Controller/Hook/Init.php b/app/Controller/Hook/Init.php index e257d82..6a99583 100644 --- a/app/Controller/Hook/Init.php +++ b/app/Controller/Hook/Init.php @@ -31,6 +31,7 @@ protected function get_hooks() { Filter_Permalinks::class, Directory_Builder_Actions::class, Listings_Actions::class, + Directory_Type_Meta_Translation::class, Category_Directory_Sync::class, Email_Translation::class, @@ -49,4 +50,4 @@ protected function get_hooks() { ]; } -} \ No newline at end of file +} diff --git a/app/Controller/Hook/Query_Filtering.php b/app/Controller/Hook/Query_Filtering.php index f23af71..58ffba5 100644 --- a/app/Controller/Hook/Query_Filtering.php +++ b/app/Controller/Hook/Query_Filtering.php @@ -2,6 +2,8 @@ namespace Directorist_WPML_Integration\Controller\Hook; +use Directorist_WPML_Integration\Helper\WPML_Helper; + class Query_Filtering { /** @@ -16,6 +18,7 @@ public function __construct() { // Ensure suppress_filters is false for Directorist queries // This runs BEFORE the query is created, allowing WPML to filter at SQL level add_filter( 'directorist_all_listings_query_arguments', [ $this, 'ensure_wpml_filtering' ], 10, 1 ); + add_filter( 'atbdp_listing_search_query_argument', [ $this, 'ensure_wpml_filtering' ], 10, 1 ); add_filter( 'directorist_dashboard_query_arguments', [ $this, 'ensure_wpml_filtering' ], 10, 1 ); // Also filter author listings query arguments (like author profile page) @@ -58,6 +61,11 @@ public function filter_listing_queries( $query ) { $query->query_vars['tax_query'] = $this->translate_tax_query_terms( $query->query_vars['tax_query'] ); } + // Accept directory type IDs from any translation in the same WPML group. + if ( ! empty( $query->query_vars['meta_query'] ) && is_array( $query->query_vars['meta_query'] ) ) { + $query->query_vars['meta_query'] = $this->normalize_directory_type_meta_query( $query->query_vars['meta_query'] ); + } + // WPML will automatically filter by current language when suppress_filters is false // This ensures WPML's query filtering hooks are applied } @@ -185,6 +193,10 @@ public function ensure_wpml_filtering( $args ) { if ( ! empty( $args['tax_query'] ) && is_array( $args['tax_query'] ) ) { $args['tax_query'] = $this->translate_tax_query_terms( $args['tax_query'] ); } + + if ( ! empty( $args['meta_query'] ) && is_array( $args['meta_query'] ) ) { + $args['meta_query'] = $this->normalize_directory_type_meta_query( $args['meta_query'] ); + } } else { // For other post types, just ensure suppress_filters is false if it was true if ( isset( $args['suppress_filters'] ) && $args['suppress_filters'] ) { @@ -195,6 +207,94 @@ public function ensure_wpml_filtering( $args ) { return $args; } + /** + * Expand `_directory_type` meta queries to include all translated directory IDs. + * + * Listings translated before our sync hooks ran can still store the source + * language directory ID in post meta. Matching the whole WPML translation + * group keeps those listings visible on the correct archive page. + * + * @param array $meta_query Meta query array. + * @return array + */ + private function normalize_directory_type_meta_query( $meta_query ) { + if ( ! is_array( $meta_query ) || ! has_filter( 'wpml_element_trid' ) || ! has_filter( 'wpml_get_element_translations' ) ) { + return $meta_query; + } + + foreach ( $meta_query as $key => $clause ) { + if ( ! is_array( $clause ) ) { + continue; + } + + if ( isset( $clause['key'] ) && '_directory_type' === $clause['key'] && ! empty( $clause['value'] ) ) { + $directory_ids = $this->get_directory_type_translation_ids( $clause['value'] ); + + if ( empty( $directory_ids ) ) { + continue; + } + + $compare = isset( $clause['compare'] ) ? strtoupper( $clause['compare'] ) : '='; + + if ( in_array( $compare, [ '!=', 'NOT IN' ], true ) ) { + $meta_query[ $key ]['compare'] = count( $directory_ids ) > 1 ? 'NOT IN' : '!='; + } else { + $meta_query[ $key ]['compare'] = count( $directory_ids ) > 1 ? 'IN' : '='; + } + + $meta_query[ $key ]['value'] = count( $directory_ids ) > 1 ? $directory_ids : $directory_ids[0]; + + continue; + } + + $meta_query[ $key ] = $this->normalize_directory_type_meta_query( $clause ); + } + + return $meta_query; + } + + /** + * Collect all translated term IDs for one or more directory types. + * + * @param mixed $directory_ids One ID or a list of IDs. + * @return array + */ + private function get_directory_type_translation_ids( $directory_ids ) { + $directory_ids = wp_parse_id_list( (array) $directory_ids ); + + if ( empty( $directory_ids ) ) { + return []; + } + + $translated_group_ids = []; + + foreach ( $directory_ids as $directory_id ) { + $translated_group_ids[] = (int) $directory_id; + + $translations = WPML_Helper::get_element_translations( $directory_id, ATBDP_DIRECTORY_TYPE ); + if ( empty( $translations ) || ! is_array( $translations ) ) { + continue; + } + + foreach ( $translations as $translation ) { + if ( ! is_object( $translation ) ) { + continue; + } + + if ( ! empty( $translation->term_id ) ) { + $translated_group_ids[] = (int) $translation->term_id; + continue; + } + + if ( ! empty( $translation->element_id ) ) { + $translated_group_ids[] = (int) $translation->element_id; + } + } + } + + return array_values( array_unique( array_filter( wp_parse_id_list( $translated_group_ids ) ) ) ); + } + /** * Filter query results to ensure only current language listings * diff --git a/app/Controller/Hook/REST_API.php b/app/Controller/Hook/REST_API.php index d9c3799..2fb8c00 100644 --- a/app/Controller/Hook/REST_API.php +++ b/app/Controller/Hook/REST_API.php @@ -11,8 +11,19 @@ public function __construct() { // Set content language before rest query public function set_content_language_before_rest_query( $type, $request ) { - $language = ( ! empty( $request['language'] ) ) ? $request['language'] : 'en'; - do_action( 'wpml_switch_language', $language ); + $language = ! empty( $request['language'] ) ? $request['language'] : ''; + + if ( empty( $language ) && ! empty( $request['wpml_lang'] ) ) { + $language = $request['wpml_lang']; + } + + if ( empty( $language ) ) { + $language = apply_filters( 'wpml_default_language', null ); + } + + if ( ! empty( $language ) ) { + do_action( 'wpml_switch_language', $language ); + } } // Set content language in REST response header @@ -23,4 +34,4 @@ public function set_content_language_in_rest_response_header( $response ) { return $response; } -} \ No newline at end of file +} diff --git a/app/Controller/Hook/Search_Form_Field_Translation.php b/app/Controller/Hook/Search_Form_Field_Translation.php index 3b35cae..3eba8fa 100644 --- a/app/Controller/Hook/Search_Form_Field_Translation.php +++ b/app/Controller/Hook/Search_Form_Field_Translation.php @@ -1,13 +1,11 @@ listing_type ) ) { - return (int) $searchform->listing_type; - } + public function translate_search_form_fields_meta( $value, $object_id, $meta_key, $single ) { + if ( 'search_form_fields' !== $meta_key || ! $single || self::$translating_term_meta || ! $this->is_wpml_active() ) { + return $value; } - // Second priority: During AJAX, get from POST data - if ( wp_doing_ajax() && ! empty( $_POST['listing_type'] ) ) { - $listing_type_slug = sanitize_text_field( $_POST['listing_type'] ); - if ( function_exists( 'get_term_by' ) && function_exists( 'ATBDP_TYPE' ) ) { - $term = get_term_by( 'slug', $listing_type_slug, ATBDP_TYPE ); - if ( $term && ! is_wp_error( $term ) ) { - return (int) $term->term_id; - } - } - } + self::$translating_term_meta = true; + $raw_value = get_term_meta( $object_id, $meta_key, true ); + self::$translating_term_meta = false; - // Fallback: Get default directory - if ( function_exists( 'directorist_get_default_directory' ) ) { - return (int) directorist_get_default_directory(); + if ( empty( $raw_value ) || ! is_array( $raw_value ) || empty( $raw_value['fields'] ) || ! is_array( $raw_value['fields'] ) ) { + return $raw_value; } - return 0; + return $this->translate_search_form_fields( $raw_value, (int) $object_id ); } - /** - * Apply translated field data in directorist_template filter - * - * Hook Location: Helper::get_template() line 63 - * Filter: apply_filters( 'directorist_template', $template, $args ) - * - * This filter receives $args which contains $args['data'] = $field_data and $args['searchform']. - * We translate the field_data here (now that we have directory_id from searchform) and replace $args['data']. - * - * @param string $template Template name - * @param array $args Template arguments (contains 'data' => $field_data, 'searchform' => $searchform) - * @return string Template name (unchanged) + * Translate all search form field strings for a directory type. + * + * @param array $search_form_fields Search form term meta. + * @param int $directory_id Directory type ID. + * @return array */ - public function apply_translated_field_data( $template, $args ) { - // Only process search form field templates - if ( strpos( $template, 'search-form/fields/' ) === false && strpos( $template, 'search-form/custom-fields/' ) === false ) { - return $template; - } + private function translate_search_form_fields( $search_form_fields, $directory_id ) { + $directory_context_id = $this->get_directory_context_id( $directory_id ); - if ( ! $this->is_wpml_active() ) { - return $template; + if ( empty( $directory_context_id ) ) { + return $search_form_fields; } - // Check if this is a search form field template with field data - if ( empty( $args['data'] ) || ! is_array( $args['data'] ) ) { - return $template; - } - - // Get directory ID from searchform instance (available in $args) - $directory_id = 0; - if ( ! empty( $args['searchform'] ) && is_object( $args['searchform'] ) ) { - if ( ! empty( $args['searchform']->listing_type ) ) { - $directory_id = (int) $args['searchform']->listing_type; + foreach ( $search_form_fields['fields'] as $field_key => $field_data ) { + if ( ! is_array( $field_data ) ) { + continue; } - } else { - $directory_id = $this->get_directory_id(); - } - - if ( empty( $directory_id ) ) { - return $template; - } - // Translate the field data (now that we have directory_id) - $translated_field_data = $this->translate_single_field( $args['data'], $directory_id ); + $field_slug = $this->get_field_slug( $field_key, $field_data ); - // Replace $args['data'] with translated version - $args['data'] = $translated_field_data; + $search_form_fields['fields'][ $field_key ] = $this->translate_single_field( + $field_data, + $directory_context_id, + $field_slug + ); + } - return $template; + return $search_form_fields; } /** - * Translate a single field's label and other translatable strings - * - * @param array $field_data Field data array - * @param int $directory_id Directory type ID - * @return array Translated field data + * Get a stable WPML context ID for a directory type. + * + * @param int $directory_id Directory type ID. + * @return int */ - private function translate_single_field( $field_data, $directory_id ) { - if ( empty( $field_data ) || ! is_array( $field_data ) ) { - return $field_data; - } + private function get_directory_context_id( $directory_id ) { + $directory_id = (int) $directory_id; - $widget_name = ! empty( $field_data['widget_name'] ) ? $field_data['widget_name'] : ''; - if ( empty( $widget_name ) ) { - return $field_data; + if ( $directory_id <= 0 ) { + return 0; } - $widget_slug = $this->safe_slug( $widget_name ); + $translation_group_id = WPML_Helper::get_element_trid( $directory_id, ATBDP_DIRECTORY_TYPE ); - // Translate common field properties - $field_data = $this->translate_field_property( $field_data, 'label', $directory_id, $widget_slug ); - $field_data = $this->translate_field_property( $field_data, 'placeholder', $directory_id, $widget_slug ); - $field_data = $this->translate_field_property( $field_data, 'description', $directory_id, $widget_slug ); + return $translation_group_id > 0 ? $translation_group_id : $directory_id; + } - // Translate field options (for select, radio, checkbox fields) - if ( ! empty( $field_data['options'] ) && is_array( $field_data['options'] ) ) { - $field_data = $this->translate_field_options( $field_data, $directory_id, $widget_slug ); + /** + * Build a stable field slug for string keys. + * + * @param string|int $field_key Field array key. + * @param array $field_data Field data. + * @return string + */ + private function get_field_slug( $field_key, $field_data ) { + $candidates = []; + + if ( is_string( $field_key ) || is_numeric( $field_key ) ) { + $candidates[] = (string) $field_key; + } + + foreach ( [ 'field_key', 'original_widget_key', 'widget_key', 'widget_name' ] as $candidate_key ) { + if ( ! empty( $field_data[ $candidate_key ] ) && ( is_string( $field_data[ $candidate_key ] ) || is_numeric( $field_data[ $candidate_key ] ) ) ) { + $candidates[] = (string) $field_data[ $candidate_key ]; + } } - // Translate field-specific properties - if ( $widget_name === 'pricing' ) { - $field_data = $this->translate_pricing_field( $field_data, $directory_id, $widget_slug ); - } elseif ( $widget_name === 'radius_search' ) { - $field_data = $this->translate_radius_search_field( $field_data, $directory_id, $widget_slug ); + foreach ( $candidates as $candidate ) { + $slug = $this->safe_slug( $candidate ); + + if ( '' !== $slug ) { + return $slug; + } } - return $field_data; + return 'field_' . substr( md5( wp_json_encode( $field_data ) ), 0, 10 ); } /** - * Translate field options array - * - * @param array $field_data Field data array - * @param int $directory_id Directory type ID - * @param string $widget_slug Widget slug - * @return array Modified field data + * Translate one search-form field definition. + * + * @param array $field_data Field data. + * @param int $directory_context_id Stable directory context ID. + * @param string $field_slug Field identifier. + * @return array */ - private function translate_field_options( $field_data, $directory_id, $widget_slug ) { - if ( empty( $field_data['options'] ) || ! is_array( $field_data['options'] ) ) { - return $field_data; + private function translate_single_field( $field_data, $directory_context_id, $field_slug ) { + foreach ( [ 'label', 'placeholder', 'description' ] as $property ) { + $field_data = $this->translate_field_property( $field_data, $property, $directory_context_id, $field_slug ); } - $translated_options = []; - foreach ( $field_data['options'] as $key => $option ) { - if ( is_array( $option ) ) { - // Handle nested option arrays (e.g., ['value' => 'x', 'label' => 'y']) - if ( ! empty( $option['label'] ) && is_string( $option['label'] ) ) { - // Use option value for stable naming, fallback to index - $option_identifier = ! empty( $option['value'] ) ? $option['value'] : $key; - $string_name = sprintf( 'search_form_dir_%d_field_%s_option_%s', $directory_id, $widget_slug, $this->safe_slug( $option_identifier ) ); - $this->register_wpml_string( $string_name, $option['label'] ); - $translated_label = $this->translate_wpml_string( $option['label'], $string_name ); - - if ( ! empty( $translated_label ) && $translated_label !== $option['label'] ) { - $option['label'] = $translated_label; - } - } - $translated_options[ $key ] = $option; - } elseif ( is_string( $option ) ) { - // Handle simple option arrays (e.g., ['option1', 'option2']) - // Use option value for stable naming - $option_identifier = $option; - $string_name = sprintf( 'search_form_dir_%d_field_%s_option_%s', $directory_id, $widget_slug, $this->safe_slug( $option_identifier ) ); - $this->register_wpml_string( $string_name, $option ); - $translated_option = $this->translate_wpml_string( $option, $string_name ); - - $translated_options[ $key ] = ! empty( $translated_option ) && $translated_option !== $option - ? $translated_option - : $option; - } else { - $translated_options[ $key ] = $option; + if ( ! empty( $field_data['options'] ) && is_array( $field_data['options'] ) ) { + $field_data['options'] = $this->translate_option_collection( + $field_data['options'], + $directory_context_id, + $field_slug + ); + } + + if ( ! empty( $field_data['widget_name'] ) && 'pricing' === $field_data['widget_name'] ) { + $field_data = $this->translate_min_max_placeholder( $field_data, 'price_range_min_placeholder', 'Min', $directory_context_id, $field_slug ); + $field_data = $this->translate_min_max_placeholder( $field_data, 'price_range_max_placeholder', 'Max', $directory_context_id, $field_slug ); + } + + if ( ! empty( $field_data['widget_name'] ) && 'radius_search' === $field_data['widget_name'] ) { + if ( ! empty( $field_data['radius_min_placeholder'] ) ) { + $field_data = $this->translate_field_property( $field_data, 'radius_min_placeholder', $directory_context_id, $field_slug ); + } + + if ( ! empty( $field_data['radius_max_placeholder'] ) ) { + $field_data = $this->translate_field_property( $field_data, 'radius_max_placeholder', $directory_context_id, $field_slug ); } } - $field_data['options'] = $translated_options; return $field_data; } /** - * Translate a single field property (label, placeholder, description) - * - * @param array $field_data Field data array - * @param string $property Property name (label, placeholder, description) - * @param int $directory_id Directory type ID - * @param string $widget_slug Widget slug - * @return array Modified field data + * Translate one field string property. + * + * @param array $field_data Field data. + * @param string $property Property name. + * @param int $directory_context_id Stable directory context ID. + * @param string $field_slug Field identifier. + * @return array */ - private function translate_field_property( $field_data, $property, $directory_id, $widget_slug ) { + private function translate_field_property( $field_data, $property, $directory_context_id, $field_slug ) { if ( empty( $field_data[ $property ] ) || ! is_string( $field_data[ $property ] ) ) { return $field_data; } - $string_name = sprintf( 'search_form_dir_%d_field_%s_%s', $directory_id, $widget_slug, $property ); + $string_name = sprintf( 'search_form_dir_%d_field_%s_%s', $directory_context_id, $field_slug, $property ); $this->register_wpml_string( $string_name, $field_data[ $property ] ); + $translated = $this->translate_wpml_string( $field_data[ $property ], $string_name ); if ( ! empty( $translated ) && $translated !== $field_data[ $property ] ) { @@ -252,53 +221,113 @@ private function translate_field_property( $field_data, $property, $directory_id } /** - * Translate pricing field specific placeholders - * - * @param array $field_data Field data array - * @param int $directory_id Directory type ID - * @param string $widget_slug Widget slug - * @return array Modified field data + * Translate nested option collections. + * + * @param array $options Options array. + * @param int $directory_context_id Stable directory context ID. + * @param string $field_slug Field identifier. + * @param string $path Current option path. + * @return array */ - private function translate_pricing_field( $field_data, $directory_id, $widget_slug ) { - $field_data = $this->translate_min_max_placeholder( $field_data, 'price_range_min_placeholder', 'Min', $directory_id, $widget_slug ); - $field_data = $this->translate_min_max_placeholder( $field_data, 'price_range_max_placeholder', 'Max', $directory_id, $widget_slug ); - return $field_data; - } + private function translate_option_collection( $options, $directory_context_id, $field_slug, $path = 'option' ) { + foreach ( $options as $key => $option ) { + $option_identifier = $this->safe_slug( is_string( $key ) || is_numeric( $key ) ? (string) $key : 'item' ); - /** - * Translate radius search field specific strings - * - * @param array $field_data Field data array - * @param int $directory_id Directory type ID - * @param string $widget_slug Widget slug - * @return array Modified field data - */ - private function translate_radius_search_field( $field_data, $directory_id, $widget_slug ) { - if ( empty( $field_data['radius_min_placeholder'] ) ) { - $field_data = $this->translate_min_max_placeholder( $field_data, 'radius_min_placeholder', 'Min', $directory_id, $widget_slug ); - } - if ( empty( $field_data['radius_max_placeholder'] ) ) { - $field_data = $this->translate_min_max_placeholder( $field_data, 'radius_max_placeholder', 'Max', $directory_id, $widget_slug ); + if ( is_array( $option ) ) { + if ( ! empty( $option['label'] ) && is_string( $option['label'] ) ) { + $identifier = ! empty( $option['value'] ) ? $option['value'] : $option_identifier; + $string_name = sprintf( + 'search_form_dir_%d_field_%s_%s_%s_label', + $directory_context_id, + $field_slug, + $path, + $this->safe_slug( $identifier ) + ); + + $this->register_wpml_string( $string_name, $option['label'] ); + $translated = $this->translate_wpml_string( $option['label'], $string_name ); + + if ( ! empty( $translated ) && $translated !== $option['label'] ) { + $option['label'] = $translated; + } + } + + if ( ! empty( $option['option_label'] ) && is_string( $option['option_label'] ) ) { + $identifier = ! empty( $option['option_value'] ) ? $option['option_value'] : $option_identifier; + $string_name = sprintf( + 'search_form_dir_%d_field_%s_%s_%s_option_label', + $directory_context_id, + $field_slug, + $path, + $this->safe_slug( $identifier ) + ); + + $this->register_wpml_string( $string_name, $option['option_label'] ); + $translated = $this->translate_wpml_string( $option['option_label'], $string_name ); + + if ( ! empty( $translated ) && $translated !== $option['option_label'] ) { + $option['option_label'] = $translated; + } + } + + if ( ! empty( $option['options'] ) && is_array( $option['options'] ) ) { + $option['options'] = $this->translate_option_collection( + $option['options'], + $directory_context_id, + $field_slug, + $path . '_' . $option_identifier + ); + } + + $options[ $key ] = $option; + continue; + } + + if ( ! is_string( $option ) || '' === $option ) { + continue; + } + + $string_name = sprintf( + 'search_form_dir_%d_field_%s_%s_%s', + $directory_context_id, + $field_slug, + $path, + $option_identifier + ); + + $this->register_wpml_string( $string_name, $option ); + $translated = $this->translate_wpml_string( $option, $string_name ); + + if ( ! empty( $translated ) && $translated !== $option ) { + $options[ $key ] = $translated; + } } - return $field_data; + + return $options; } /** - * Translate min/max placeholder with default fallback - * - * @param array $field_data Field data array - * @param string $property Property name - * @param string $default Default value if property is empty - * @param int $directory_id Directory type ID - * @param string $widget_slug Widget slug - * @return array Modified field data + * Translate min/max placeholders with default fallbacks. + * + * @param array $field_data Field data. + * @param string $property Property name. + * @param string $default Default value. + * @param int $directory_context_id Stable directory context ID. + * @param string $field_slug Field identifier. + * @return array */ - private function translate_min_max_placeholder( $field_data, $property, $default, $directory_id, $widget_slug ) { - $value = ! empty( $field_data[ $property ] ) && is_string( $field_data[ $property ] ) - ? $field_data[ $property ] + private function translate_min_max_placeholder( $field_data, $property, $default, $directory_context_id, $field_slug ) { + $value = ! empty( $field_data[ $property ] ) && is_string( $field_data[ $property ] ) + ? $field_data[ $property ] : __( $default, 'directorist-wpml-integration' ); - $string_name = sprintf( 'search_form_dir_%d_field_%s_%s', $directory_id, $widget_slug, str_replace( [ 'price_range_', 'radius_' ], '', $property ) ); + $string_name = sprintf( + 'search_form_dir_%d_field_%s_%s', + $directory_context_id, + $field_slug, + str_replace( [ 'price_range_', 'radius_' ], '', $property ) + ); + $this->register_wpml_string( $string_name, $value ); $translated = $this->translate_wpml_string( $value, $string_name ); @@ -310,34 +339,36 @@ private function translate_min_max_placeholder( $field_data, $property, $default } /** - * Register string with WPML - * - * @param string $string_name String name/context - * @param string $string_value String value + * Register a string with WPML. + * + * @param string $string_name String identifier. + * @param string $string_value String value. * @return void */ private function register_wpml_string( $string_name, $string_value ) { - if ( ! function_exists( 'do_action' ) ) { + if ( ! is_string( $string_value ) || '' === $string_value ) { return; } - - if ( is_string( $string_value ) && ! empty( $string_value ) ) { - do_action( 'wpml_register_single_string', self::WPML_DOMAIN, $string_name, $string_value ); + + if ( is_admin() && ! empty( $_GET['page'] ) ) { + $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); + + if ( false !== strpos( $page, 'wpml-string-translation' ) ) { + return; + } } + + do_action( 'wpml_register_single_string', self::WPML_DOMAIN, $string_name, $string_value ); } /** - * Translate WPML string - * - * @param string $string_value Original string value - * @param string $string_name String name/context - * @return string Translated string + * Translate a registered WPML string. + * + * @param string $string_value Original string. + * @param string $string_name String identifier. + * @return string */ private function translate_wpml_string( $string_value, $string_name ) { - if ( ! function_exists( 'apply_filters' ) ) { - return $string_value; - } - return apply_filters( 'wpml_translate_single_string', $string_value, @@ -347,12 +378,12 @@ private function translate_wpml_string( $string_value, $string_name ) { } /** - * Create safe slug from string - * - * @param string $string Input string - * @return string Safe slug + * Create a safe slug from a string. + * + * @param string $string Input string. + * @return string */ private function safe_slug( $string ) { - return sanitize_key( str_replace( [ ' ', '-', '_' ], '_', strtolower( $string ) ) ); + return sanitize_key( sanitize_title( (string) $string ) ); } } diff --git a/app/Controller/Hook/Search_Form_Filter.php b/app/Controller/Hook/Search_Form_Filter.php index d8eb4cb..3ca76b3 100644 --- a/app/Controller/Hook/Search_Form_Filter.php +++ b/app/Controller/Hook/Search_Form_Filter.php @@ -2,6 +2,8 @@ namespace Directorist_WPML_Integration\Controller\Hook; +use Directorist_WPML_Integration\Helper\WPML_Helper; + class Search_Form_Filter { /** @@ -12,14 +14,7 @@ class Search_Form_Filter { public function __construct() { // Filter search queries to only show current language listings add_filter( 'directorist_all_listings_query_arguments', [ $this, 'filter_search_query' ], 10, 1 ); - - // Filter taxonomy terms in search forms to show only current language - add_filter( 'directorist_search_form_categories', [ $this, 'filter_taxonomy_terms' ], 10, 2 ); - add_filter( 'directorist_search_form_locations', [ $this, 'filter_taxonomy_terms' ], 10, 2 ); - add_filter( 'directorist_search_form_tags', [ $this, 'filter_taxonomy_terms' ], 10, 2 ); - - // Ensure search result queries filter by language - add_action( 'directorist_before_search_query', [ $this, 'ensure_search_language_filter' ], 10, 1 ); + add_filter( 'atbdp_listing_search_query_argument', [ $this, 'filter_search_query' ], 10, 1 ); } /** @@ -77,14 +72,7 @@ public function filter_taxonomy_terms( $terms, $taxonomy ) { } // Check if term is in current language - $term_language = apply_filters( - 'wpml_element_language_details', - null, - [ - 'element_id' => $term_id, - 'element_type' => $taxonomy - ] - ); + $term_language = WPML_Helper::get_language_info( $term_id, $taxonomy ); if ( ! empty( $term_language ) && $term_language->language_code === $current_language ) { $filtered_terms[] = $term; @@ -178,7 +166,21 @@ private function translate_tax_query_terms( $tax_query ) { } } - return array_values( $tax_query ); // Re-index array + $normalized_tax_query = []; + + if ( isset( $tax_query['relation'] ) ) { + $normalized_tax_query['relation'] = $tax_query['relation']; + } + + foreach ( $tax_query as $key => $query ) { + if ( 'relation' === $key || ! is_array( $query ) ) { + continue; + } + + $normalized_tax_query[] = $query; + } + + return $normalized_tax_query; } /** diff --git a/app/Helper/WPML_Helper.php b/app/Helper/WPML_Helper.php index 3af03cf..741284e 100644 --- a/app/Helper/WPML_Helper.php +++ b/app/Helper/WPML_Helper.php @@ -81,12 +81,18 @@ public static function set_post_translation( $original_post_id = 0, $translation * @return void */ public static function assign_language( $post_id = 0, $element_type = '', $language_code = '', $trid = false, $source_language_code = null ) { - $default_language = apply_filters( 'wpml_default_language', null ); - $language_code = ( ! empty( $language_code ) ) ? $language_code : $default_language; - $wpml_element_type = apply_filters( 'wpml_element_type', $element_type ); + $default_language = apply_filters( 'wpml_default_language', null ); + $language_code = ( ! empty( $language_code ) ) ? $language_code : $default_language; + $raw_element_type = self::get_raw_element_type( $element_type ); + $wpml_element_type = self::get_wpml_element_type( $raw_element_type ); + $wpml_element_id = self::get_wpml_element_id( $post_id, $raw_element_type ); + + if ( empty( $wpml_element_id ) || empty( $wpml_element_type ) ) { + return; + } $set_language_args = [ - 'element_id' => $post_id, + 'element_id' => $wpml_element_id, 'element_type' => $wpml_element_type, 'trid' => $trid, 'language_code' => $language_code, @@ -105,9 +111,16 @@ public static function assign_language( $post_id = 0, $element_type = '', $lang * @return stdClass|false Language Info */ public static function get_language_info( $post_id = 0, $element_type = 'post' ) { + $raw_element_type = self::get_raw_element_type( $element_type ); + $wpml_element_id = self::get_wpml_element_id( $post_id, $raw_element_type ); + + if ( empty( $wpml_element_id ) || empty( $raw_element_type ) ) { + return false; + } + $get_language_args = [ - 'element_id' => $post_id, - 'element_type' => $element_type + 'element_id' => $wpml_element_id, + 'element_type' => $raw_element_type, ]; return apply_filters( 'wpml_element_language_details', null, $get_language_args ); @@ -175,14 +188,7 @@ public static function create_duplicate_term( $term_id = 0, $taxonomy = '', $new * @return object $language_info */ public static function get_element_language_info( $element_id, $element_type ) { - $get_language_args = [ - 'element_id' => $element_id, - 'element_type' => $element_type - ]; - - $language_info = apply_filters( 'wpml_element_language_details', null, $get_language_args ); - - return $language_info; + return self::get_language_info( $element_id, $element_type ); } /** @@ -194,13 +200,266 @@ public static function get_element_language_info( $element_id, $element_type ) { * @return object $language_info */ public static function get_element_translations( $element_id, $element_type ) { - $wpml_element_type = apply_filters( 'wpml_element_type', $element_type ); - $translation_id = apply_filters( 'wpml_element_trid', NULL, $element_id, $wpml_element_type ); - $translations = apply_filters( 'wpml_get_element_translations', NULL, $translation_id, $wpml_element_type ); + $raw_element_type = self::get_raw_element_type( $element_type ); + $wpml_element_id = self::get_wpml_element_id( $element_id, $raw_element_type ); + $wpml_element_type = self::get_wpml_element_type( $raw_element_type ); + + if ( empty( $wpml_element_id ) || empty( $wpml_element_type ) ) { + return []; + } + + $translation_id = apply_filters( 'wpml_element_trid', null, $wpml_element_id, $wpml_element_type ); + + if ( empty( $translation_id ) ) { + return []; + } + + $translations = apply_filters( 'wpml_get_element_translations', null, $translation_id, $wpml_element_type ); + + return self::normalize_translations( $translations, $raw_element_type ); + } + + /** + * Get the WPML translation group ID for an element. + * + * @param int $element_id Element ID. + * @param string $element_type Post type or taxonomy key. + * + * @return int + */ + public static function get_element_trid( $element_id, $element_type ) { + $raw_element_type = self::get_raw_element_type( $element_type ); + $wpml_element_id = self::get_wpml_element_id( $element_id, $raw_element_type ); + $wpml_element_type = self::get_wpml_element_type( $raw_element_type ); + + if ( empty( $wpml_element_id ) || empty( $wpml_element_type ) ) { + return 0; + } + + return (int) apply_filters( 'wpml_element_trid', null, $wpml_element_id, $wpml_element_type ); + } + + /** + * Normalize an element type into the raw WPML-friendly key. + * + * @param string $element_type Element type. + * @return string + */ + public static function get_raw_element_type( $element_type ) { + if ( ! is_string( $element_type ) || '' === $element_type ) { + return ''; + } + + if ( 0 === strpos( $element_type, 'post_' ) ) { + return substr( $element_type, 5 ); + } + + if ( 0 === strpos( $element_type, 'tax_' ) ) { + return substr( $element_type, 4 ); + } + + return $element_type; + } + + /** + * Get the fully-qualified WPML element type. + * + * @param string $element_type Element type. + * @return string + */ + public static function get_wpml_element_type( $element_type ) { + $raw_element_type = self::get_raw_element_type( $element_type ); + + if ( '' === $raw_element_type ) { + return ''; + } + + return (string) apply_filters( 'wpml_element_type', $raw_element_type ); + } + + /** + * Convert a WordPress object ID into the ID WPML stores internally. + * + * WPML uses term_taxonomy_id for taxonomy items and post_id for posts. + * + * @param int $element_id Element ID. + * @param string $element_type Element type. + * @return int + */ + public static function get_wpml_element_id( $element_id, $element_type ) { + $element_id = (int) $element_id; + + if ( $element_id <= 0 ) { + return 0; + } + + $taxonomy = self::get_taxonomy_name( $element_type ); + + if ( '' === $taxonomy ) { + return $element_id; + } + + $term_taxonomy_id = self::get_term_taxonomy_id( $element_id, $taxonomy ); + if ( $term_taxonomy_id > 0 ) { + return $term_taxonomy_id; + } + + if ( self::get_term_id_from_term_taxonomy_id( $element_id, $taxonomy ) > 0 ) { + return $element_id; + } + + return $element_id; + } + + /** + * Convert a WPML taxonomy element ID back into a WordPress term ID. + * + * @param int $element_id Element ID. + * @param string $element_type Element type. + * @return int + */ + public static function get_wordpress_element_id( $element_id, $element_type ) { + $element_id = (int) $element_id; + + if ( $element_id <= 0 ) { + return 0; + } + + $taxonomy = self::get_taxonomy_name( $element_type ); + + if ( '' === $taxonomy ) { + return $element_id; + } + + $term_id = self::get_term_id_from_term_taxonomy_id( $element_id, $taxonomy ); + if ( $term_id > 0 ) { + return $term_id; + } + + if ( self::get_term_taxonomy_id( $element_id, $taxonomy ) > 0 ) { + return $element_id; + } + + return $element_id; + } + + /** + * Check whether an element type refers to a taxonomy. + * + * @param string $element_type Element type. + * @return bool + */ + public static function is_taxonomy_element_type( $element_type ) { + return '' !== self::get_taxonomy_name( $element_type ); + } + + /** + * Normalize translation objects so taxonomy translations expose term IDs. + * + * @param mixed $translations Translation data. + * @param string $element_type Element type. + * @return array + */ + public static function normalize_translations( $translations, $element_type ) { + if ( empty( $translations ) || ! is_array( $translations ) ) { + return []; + } + + if ( ! self::is_taxonomy_element_type( $element_type ) ) { + return $translations; + } + + foreach ( $translations as $language_code => $translation ) { + if ( ! is_object( $translation ) ) { + continue; + } + + $wpml_element_id = 0; + + if ( ! empty( $translation->element_id ) ) { + $wpml_element_id = (int) $translation->element_id; + } elseif ( ! empty( $translation->term_taxonomy_id ) ) { + $wpml_element_id = (int) $translation->term_taxonomy_id; + } elseif ( ! empty( $translation->term_id ) ) { + $wpml_element_id = self::get_wpml_element_id( $translation->term_id, $element_type ); + } + + if ( $wpml_element_id <= 0 ) { + continue; + } + + $translations[ $language_code ]->term_id = self::get_wordpress_element_id( $wpml_element_id, $element_type ); + } return $translations; } + /** + * Resolve a raw taxonomy key from an element type. + * + * @param string $element_type Element type. + * @return string + */ + private static function get_taxonomy_name( $element_type ) { + $raw_element_type = self::get_raw_element_type( $element_type ); + + if ( '' === $raw_element_type || ! taxonomy_exists( $raw_element_type ) ) { + return ''; + } + + return $raw_element_type; + } + + /** + * Resolve term_taxonomy_id without using term APIs that WPML filters by language. + * + * @param int $term_id Term ID. + * @param string $taxonomy Taxonomy name. + * @return int + */ + private static function get_term_taxonomy_id( $term_id, $taxonomy ) { + global $wpdb; + + $term_id = (int) $term_id; + + if ( $term_id <= 0 || empty( $taxonomy ) || ! isset( $wpdb->term_taxonomy ) ) { + return 0; + } + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE term_id = %d AND taxonomy = %s LIMIT 1", + $term_id, + $taxonomy + ) + ); + } + + /** + * Resolve term ID from term_taxonomy_id without triggering WPML term filters. + * + * @param int $term_taxonomy_id Term taxonomy ID. + * @param string $taxonomy Taxonomy name. + * @return int + */ + private static function get_term_id_from_term_taxonomy_id( $term_taxonomy_id, $taxonomy ) { + global $wpdb; + + $term_taxonomy_id = (int) $term_taxonomy_id; + + if ( $term_taxonomy_id <= 0 || empty( $taxonomy ) || ! isset( $wpdb->term_taxonomy ) ) { + return 0; + } + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT term_id FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id = %d AND taxonomy = %s LIMIT 1", + $term_taxonomy_id, + $taxonomy + ) + ); + } + /** * Register Directorist setting with WPML * @@ -238,4 +497,4 @@ public static function translate_option( $option_key, $default_value = '' ) { ); } -} \ No newline at end of file +} diff --git a/directorist-wpml-integration.php b/directorist-wpml-integration.php index bda55e6..edbc05f 100644 --- a/directorist-wpml-integration.php +++ b/directorist-wpml-integration.php @@ -4,7 +4,7 @@ * Plugin URI: https://github.com/sovware/directorist-wpml-integration * Description: WPML integration plugin for Directorist. * Requires Plugins: directorist - * Version: 2.2.2 + * Version: 2.2.3 * Requires at least: 6.0 * Requires PHP: 7.4 * Tested up to: 6.9 diff --git a/readme.txt b/readme.txt index 85d7c06..fa0a750 100644 --- a/readme.txt +++ b/readme.txt @@ -1,80 +1,123 @@ -=== Directorist - WPML Integration === -Contributors: wpwax -Tags: directory, directorist, directorist wpml, wpml -Requires at least: 5.7 -Tested up to: 6.9 -Requires PHP: 7.4 -Stable tag: 2.2.1 -License: GPLv3 -License URI: https://www.gnu.org/licenses/gpl-2.0.html - -Directorist-WPML integration plugin uses automatic translation to translate your directory website content instantly and lets you check and edit the translations just before publishing them on your directory site. - -== Description == - -[DOC](https://directorist.com/documentation/directorist/directorist-wpml-translation-guide/directory-type-translation/) | [Contact](https://directorist.com/contact/) | [Other Extensions](https://directorist.com/extensions/) - -Want to make your directory multi-lingual and gain multinational exposure? Using Directorist WPML Integration you can build multilingual directory sites more conveniently by switching your directory website from one language to another. The plugin connects Directorist and WPML together and makes a room for you to bring users from different parts of the World with different languages without even writing a single line of code. - -👉 Join Our FB Community : [Directorist Community](https://www.facebook.com/groups/directorist) -👉 Official Facebook Page : [Like and Follow on Facebook](https://www.facebook.com/directorist) -👉 Official Twitter handle : [Follow on Twitter](https://twitter.com/wpdirectorist) -👉 Official YouTube Channel : [Follow on YouTube](https://www.youtube.com/c/wpWax) -👉 Official Support : [Contact](https://directorist.com/dashboard/) - -WPML provides you with an easy-to-use interface for professional content translation. This makes the whole process of translation more convenient and affordable. Allowing you to create listings in many different languages to captivate international users and improve site traffic. - -== REQUIREMENTS == - -The following plugins must be installed in order to translate. - -1. WPML Multilingual CMS - (Paid) -2. Directorist – WordPress Business Directory Plugin with Classified Ads Listings (Free) -3. Directorist - WPML Integration plugin (Free) - -== Recommended (Not Required): == - -1. WPML Translation Management -2. WPML Media -3. WPML String Translation - -== FEATURES AT A GLANCE == - -* Create or translate directory listings to multiple languages -* Translate directory taxonomies; categories and locations into multiple languages -* Make your all listings/archive page multi-lingual -* Make Directorist dashboard multi-lingual -* Create directory type translation in one click. -* Translate strings in the settings panel. -* Make email templates multi-lingual. - -== Contribute to Directorist - WPML Integration == - -If you want to contribute to the project, you’re most welcome to make it happen. The full source code is available on [GitHub](https://github.com/sovware/directorist-wpml-integration). If you find anything improbable, feel free to shoot a bug report. - -== Installation == - -1. Upload the plugin files to the `/wp-content/plugins/` directory, install the plugin through the WordPress plugins screen directly, or search for `QuickPost` in the Block Library. -2. Activate the plugin through the 'Plugins' screen in WordPress if installed manually or through the WordPress plugins screen. -3. Use the `Add New` button in the Block Editor toolbar when needed. - -== Changelogs == -2.2.1 - Feb 05, 2026 - -* Added: Built‑in synchronization of Directorist category `_directory_type` meta across WPML languages, based on WPML’s recommended workaround (no extra code snippet needed). -* Added: WPML config to copy the `_default` directory type flag across translations, ensuring a proper default directory type per language. -* Improved: Overall WPML compatibility for directory types, categories, search form fields, and settings strings on WordPress 6.8 and the latest WPML versions. -* Fixed: New translatable data is available for translation and displays correctly on the front‑end. -* Fixed: Data saved in posts remains translatable and displays correctly on the front‑end. -* Fixed: Data saved in taxonomies remains translatable and displays correctly on the front‑end. -* Fixed: Front‑end strings are translatable with WPML String Translation and display correctly. -* Fixed: Email sending process so content is translated and sent in the user’s preferred language. - -2.1.4 - Jun 09, 2025 - -* Added: Directorist as a required dependency -* Added: Translation support for Claim Listing Settings - -2.0.0 - Nov 17, 2024 - -* Added: Directorist compatibility +=== Directorist - WPML Integration === +Contributors: wpwax +Tags: directory, directorist, multilingual, wpml +Requires at least: 6.0 +Tested up to: 6.9 +Stable tag: 2.2.3 +Requires PHP: 7.4 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +WPML compatibility extension for Directorist multilingual listings, directories, search forms, settings, and emails. + +== Description == + +Directorist - WPML Integration connects Directorist with WPML so directory owners can run multilingual listing websites with translated listings, directory types, directory pages, search forms, settings strings, and email content. + +Useful links: + +* [Documentation](https://directorist.com/documentation/directorist/directorist-wpml-translation-guide/directory-type-translation/) +* [Support](https://directorist.com/contact/) +* [Directorist extensions](https://directorist.com/extensions/) + +== Requirements == + +The following plugins are required: + +* Directorist - WordPress Business Directory Plugin with Classified Ads Listings +* WPML Multilingual CMS +* Directorist - WPML Integration + +Recommended WPML add-ons: + +* WPML String Translation +* WPML Media Translation + +== Features == + +* Translate Directorist listings across WPML languages. +* Translate Directorist directory types and keep directory type post meta aligned. +* Translate Directorist categories, locations, tags, and directory-related taxonomy data. +* Show listing archives and search results in the current WPML language. +* Translate Directorist add-listing and search-form field labels, placeholders, options, and section labels. +* Translate Directorist settings strings and email templates. +* Resolve Directorist page links to the matching WPML language page. +* Support Directorist REST requests with WPML language parameters. + +== Installation == + +1. Install and activate Directorist. +2. Install and configure WPML Multilingual CMS. +3. Install and activate WPML String Translation if settings, forms, and frontend strings need translation. +4. Install and activate Directorist - WPML Integration. +5. Translate the Directorist pages configured under Directorist settings, including All Listings, Add Listing, and Search Result. +6. Translate directory types and listing content with WPML. + +== Compatibility Testing == + +Version 2.2.3 was tested with: + +* WordPress 6.9.4 +* PHP 8.3.26 +* Directorist 8.7.1 +* WPML Multilingual CMS 4.9.2.1 +* WPML String Translation 3.5.1 +* WPML Media Translation 3.1.0 +* Active WPML languages: English, French, German, Romanian, and Bengali + +The release was verified with WP-CLI using a 138-check compatibility suite covering: + +* Plugin activation and WPML language configuration. +* Directorist listing post type and directory taxonomy WPML settings. +* Listing translations in every active language. +* Directory type translation groups in every active language. +* All Listings, Add Listing, and Search Result page translations and permalinks. +* Default listing queries and explicit directory search queries. +* Directorist REST language switching with `language` and `wpml_lang` parameters. +* Directorist AJAX hook registration. +* Shortcode rendering for all-listing, add-listing, and search-result pages. + +== Frequently Asked Questions == + += Listings show in English but not in another language. What should I check first? = + +Confirm that the listing post type is translatable in WPML, the listing has a translation in the target language, the translated listing has a translated directory type, and the translated All Listings or Search Result page exists. + += Do Directorist pages need translations? = + +Yes. Translate the Directorist pages selected in Directorist settings so WPML can resolve each language to its own All Listings, Add Listing, and Search Result page. + += Does this plugin replace WPML String Translation? = + +No. WPML String Translation is recommended for translating Directorist settings, frontend strings, form labels, and email text. + +== Changelog == + += 2.2.3 = +* Fixed: Directorist listing queries now keep WPML SQL filtering enabled for all-listing, search-result, dashboard, and author listing contexts. +* Fixed: Directory type meta queries now include the full WPML directory translation group, so translated listings remain visible even when older listing meta stores a source-language directory ID. +* Fixed: Directory type taxonomy IDs are converted through stable term taxonomy IDs for WPML API calls. +* Fixed: Directorist REST requests now respect `language` and `wpml_lang` request parameters instead of falling back to English. +* Fixed: Directorist Add Listing and Search Form field translation contexts now use stable directory translation group IDs. +* Added: Synchronization support for translated listing `_directory_type` post meta and directory type term relationships. +* Added: WPML admin-text configuration for Directorist page options required by multilingual page resolution. +* Tested: Verified with WP-CLI across English, French, German, Romanian, and Bengali. + += 2.2.1 = +* Added: Built-in synchronization of Directorist category `_directory_type` meta across WPML languages. +* Added: WPML config to copy the `_default` directory type flag across translations. +* Improved: WPML compatibility for directory types, categories, search form fields, and settings strings. +* Fixed: New translatable data displays correctly on the frontend. +* Fixed: Post, taxonomy, frontend string, and email translation handling. + += 2.1.4 = +* Added: Directorist as a required dependency. +* Added: Translation support for Claim Listing settings. + += 2.0.0 = +* Added: Directorist compatibility. + +== Upgrade Notice == + += 2.2.3 = +This release fixes translated listing visibility, directory type meta synchronization, REST language handling, and multilingual Directorist page/query resolution. diff --git a/wpml-config.xml b/wpml-config.xml index 2e7fb5d..c545698 100644 --- a/wpml-config.xml +++ b/wpml-config.xml @@ -34,13 +34,24 @@ + + + + + + + + + + + @@ -345,4 +356,4 @@ - \ No newline at end of file + From 23546190d51b67eb3ee8db5a8abdbd2eecf52ca8 Mon Sep 17 00:00:00 2001 From: Yasir Arafat <148990700+Arafat-plugins@users.noreply.github.com> Date: Wed, 6 May 2026 15:47:53 +0600 Subject: [PATCH 2/9] fix: avoid metadata warnings during WPML activation --- .../Hook/Directory_Type_Meta_Translation.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/Controller/Hook/Directory_Type_Meta_Translation.php b/app/Controller/Hook/Directory_Type_Meta_Translation.php index 8d97f16..5f3405b 100644 --- a/app/Controller/Hook/Directory_Type_Meta_Translation.php +++ b/app/Controller/Hook/Directory_Type_Meta_Translation.php @@ -71,7 +71,7 @@ public function translate_post_directory_type_meta( $value, $object_id, $meta_ke self::$resolving_post_meta = false; if ( empty( $raw_value ) || ! is_numeric( $raw_value ) ) { - return $raw_value; + return $value; } $translated_value = apply_filters( @@ -114,7 +114,7 @@ public function translate_term_directory_type_meta( $value, $object_id, $meta_ke self::$resolving_term_meta = false; if ( empty( $raw_value ) || ! is_array( $raw_value ) ) { - return $raw_value; + return $value; } $translated_ids = []; @@ -133,9 +133,15 @@ public function translate_term_directory_type_meta( $value, $object_id, $meta_ke } } - return ! empty( $translated_ids ) + $directory_type_ids = ! empty( $translated_ids ) ? array_values( array_unique( $translated_ids ) ) : wp_parse_id_list( $raw_value ); + + if ( empty( $directory_type_ids ) ) { + return $value; + } + + return [ $directory_type_ids ]; } /** From 8da0d4042988d58b1a3fa17332bc346d8ba2e4c0 Mon Sep 17 00:00:00 2001 From: Yasir Arafat <148990700+Arafat-plugins@users.noreply.github.com> Date: Wed, 6 May 2026 15:51:43 +0600 Subject: [PATCH 3/9] fix: use valid WPML Gutenberg key config --- wpml-config.xml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/wpml-config.xml b/wpml-config.xml index c545698..38874aa 100644 --- a/wpml-config.xml +++ b/wpml-config.xml @@ -242,41 +242,41 @@ - search_bar_title - search_bar_sub_title - more_filters_text - reset_filters_text - apply_filters_text + + + + + - header_title + - header_title + - header_title + - header_title + - header_title + - title - text + + - title - text + + From 13618e53fb45118b47a77fca9fedc26c9e85b549 Mon Sep 17 00:00:00 2001 From: Yasir Arafat <148990700+Arafat-plugins@users.noreply.github.com> Date: Thu, 7 May 2026 10:09:40 +0600 Subject: [PATCH 4/9] fix: fallback directory type metadata for WPML --- app/Controller/Hook/Directory_Translation.php | 111 +++++++++++++++++- .../Hook/Directory_Type_Meta_Translation.php | 111 +++++++++++++++++- 2 files changed, 220 insertions(+), 2 deletions(-) diff --git a/app/Controller/Hook/Directory_Translation.php b/app/Controller/Hook/Directory_Translation.php index 757de0b..a17343a 100644 --- a/app/Controller/Hook/Directory_Translation.php +++ b/app/Controller/Hook/Directory_Translation.php @@ -220,6 +220,10 @@ public function translate_directory_names( $directories, $args ) { * @return array|WP_Error Filtered terms with translated names */ public function translate_directory_terms( $terms, $taxonomies, $args, $term_query ) { + if ( $this->is_default_directory_query( $taxonomies, $args ) && ( empty( $terms ) || is_wp_error( $terms ) ) ) { + return $this->get_translated_default_directory_terms( $args ); + } + // Safety checks if ( empty( $terms ) || is_wp_error( $terms ) || ! is_array( $terms ) ) { return $terms; @@ -316,6 +320,112 @@ public function translate_directory_terms( $terms, $taxonomies, $args, $term_que return $terms; } + /** + * Check whether Directorist is looking up the default directory type. + * + * Directorist resolves the default directory with get_terms() using the + * `_default` term meta. Older WPML translations can miss that copied meta, + * which makes Directorist fall back to directory ID 0 in secondary languages. + * + * @param array $taxonomies Queried taxonomies. + * @param array $args get_terms() arguments. + * + * @return bool + */ + private function is_default_directory_query( $taxonomies, $args ) { + if ( ! $this->is_wpml_active() || empty( $taxonomies ) || ! is_array( $taxonomies ) ) { + return false; + } + + $directory_taxonomy = defined( 'ATBDP_DIRECTORY_TYPE' ) ? ATBDP_DIRECTORY_TYPE : 'atbdp_listing_types'; + + if ( ! in_array( $directory_taxonomy, $taxonomies, true ) && ! in_array( 'atbdp_listing_types', $taxonomies, true ) ) { + return false; + } + + $meta_key = isset( $args['meta_key'] ) ? $args['meta_key'] : ''; + $meta_value = isset( $args['meta_value'] ) ? (string) $args['meta_value'] : ''; + + return '_default' === $meta_key && '1' === $meta_value; + } + + /** + * Resolve the current language's default directory from the default language. + * + * @param array $args get_terms() arguments. + * @return array + */ + private function get_translated_default_directory_terms( $args ) { + global $wpdb; + + $current_language = apply_filters( 'wpml_current_language', null ); + $default_language = apply_filters( 'wpml_default_language', null ); + + if ( empty( $current_language ) || empty( $default_language ) || $current_language === $default_language ) { + return []; + } + + $directory_taxonomy = defined( 'ATBDP_DIRECTORY_TYPE' ) ? ATBDP_DIRECTORY_TYPE : 'atbdp_listing_types'; + $element_type = WPML_Helper::get_wpml_element_type( $directory_taxonomy ); + + if ( empty( $element_type ) ) { + return []; + } + + $default_directory_id = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT t.term_id + FROM {$wpdb->terms} t + INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = t.term_id + INNER JOIN {$wpdb->termmeta} tm ON tm.term_id = t.term_id + INNER JOIN {$wpdb->prefix}icl_translations tr + ON tr.element_id = tt.term_taxonomy_id + AND tr.element_type = %s + WHERE tt.taxonomy = %s + AND tm.meta_key = '_default' + AND tm.meta_value = '1' + AND tr.language_code = %s + ORDER BY t.term_id ASC + LIMIT 1", + $element_type, + $directory_taxonomy, + $default_language + ) + ); + + if ( $default_directory_id <= 0 ) { + return []; + } + + $translations = WPML_Helper::get_element_translations( $default_directory_id, $directory_taxonomy ); + + if ( empty( $translations[ $current_language ]->term_id ) ) { + return []; + } + + $translated_term = get_term( (int) $translations[ $current_language ]->term_id, $directory_taxonomy ); + + if ( ! $translated_term || is_wp_error( $translated_term ) ) { + return []; + } + + $fields = isset( $args['fields'] ) ? $args['fields'] : 'all'; + + if ( 'ids' === $fields ) { + return [ (int) $translated_term->term_id ]; + } + + if ( 'slugs' === $fields ) { + return [ $translated_term->slug ]; + } + + if ( 'names' === $fields ) { + return [ $translated_term->name ]; + } + + return [ $translated_term ]; + } + /** * Translate term name for directory types and tags in admin areas * @@ -419,4 +529,3 @@ public function modify_language_switcher_url( $languages_links ) { return $languages_links; } } - diff --git a/app/Controller/Hook/Directory_Type_Meta_Translation.php b/app/Controller/Hook/Directory_Type_Meta_Translation.php index 5f3405b..88ef97d 100644 --- a/app/Controller/Hook/Directory_Type_Meta_Translation.php +++ b/app/Controller/Hook/Directory_Type_Meta_Translation.php @@ -100,10 +100,14 @@ public function translate_post_directory_type_meta( $value, $object_id, $meta_ke * @return mixed */ public function translate_term_directory_type_meta( $value, $object_id, $meta_key, $single ) { - if ( '_directory_type' !== $meta_key || ! $single || self::$resolving_term_meta || ! $this->is_wpml_active() ) { + if ( self::$resolving_term_meta || ! $this->is_wpml_active() ) { return $value; } + if ( '_directory_type' !== $meta_key || ! $single ) { + return $this->fallback_translated_directory_meta( $value, $object_id, $meta_key, $single ); + } + $language_code = $this->get_term_language_code( (int) $object_id ); if ( empty( $language_code ) ) { return $value; @@ -144,6 +148,51 @@ public function translate_term_directory_type_meta( $value, $object_id, $meta_ke return [ $directory_type_ids ]; } + /** + * Use the source directory type's builder meta when a translation has none. + * + * Directory type translations created before WPML copied term meta can miss + * layout/form settings. Directorist then finds listings but cannot render + * their cards/forms because keys such as listings_card_grid_view are empty. + * + * @param mixed $value Existing metadata value. + * @param int $object_id Term ID. + * @param string $meta_key Meta key. + * @param bool $single Whether a single value is requested. + * + * @return mixed + */ + private function fallback_translated_directory_meta( $value, $object_id, $meta_key, $single ) { + $object_id = (int) $object_id; + + if ( $object_id <= 0 || empty( $meta_key ) || ! $this->is_directory_type_term( $object_id ) ) { + return $value; + } + + if ( $this->term_meta_exists( $object_id, $meta_key ) ) { + return $value; + } + + $source_term_id = $this->get_source_directory_type_id( $object_id ); + if ( $source_term_id <= 0 || $source_term_id === $object_id ) { + return $value; + } + + if ( ! $this->term_meta_exists( $source_term_id, $meta_key ) ) { + return $value; + } + + self::$resolving_term_meta = true; + $source_value = get_term_meta( $source_term_id, $meta_key, $single ); + self::$resolving_term_meta = false; + + if ( $single && is_array( $source_value ) ) { + return [ $source_value ]; + } + + return $source_value; + } + /** * Get a post's WPML language code. * @@ -195,4 +244,64 @@ private function get_term_language_code( $term_id ) { return ! empty( $language_info->language ) ? (string) $language_info->language : ''; } + + /** + * Check whether a term is a Directorist directory type. + * + * @param int $term_id Term ID. + * @return bool + */ + private function is_directory_type_term( $term_id ) { + $term = get_term( $term_id ); + + if ( empty( $term ) || is_wp_error( $term ) || empty( $term->taxonomy ) ) { + return false; + } + + $directory_taxonomy = defined( 'ATBDP_DIRECTORY_TYPE' ) ? ATBDP_DIRECTORY_TYPE : 'atbdp_listing_types'; + + return $directory_taxonomy === $term->taxonomy || 'atbdp_listing_types' === $term->taxonomy; + } + + /** + * Check raw term meta existence without triggering metadata filters. + * + * @param int $term_id Term ID. + * @param string $meta_key Meta key. + * @return bool + */ + private function term_meta_exists( $term_id, $meta_key ) { + global $wpdb; + + return (bool) $wpdb->get_var( + $wpdb->prepare( + "SELECT meta_id FROM {$wpdb->termmeta} WHERE term_id = %d AND meta_key = %s LIMIT 1", + (int) $term_id, + $meta_key + ) + ); + } + + /** + * Get the default-language source directory type for a translated term. + * + * @param int $term_id Term ID. + * @return int + */ + private function get_source_directory_type_id( $term_id ) { + $directory_taxonomy = defined( 'ATBDP_DIRECTORY_TYPE' ) ? ATBDP_DIRECTORY_TYPE : 'atbdp_listing_types'; + $default_language = apply_filters( 'wpml_default_language', null ); + + if ( empty( $default_language ) ) { + return 0; + } + + $translations = WPML_Helper::get_element_translations( $term_id, $directory_taxonomy ); + + if ( empty( $translations ) || ! is_array( $translations ) || empty( $translations[ $default_language ]->term_id ) ) { + return 0; + } + + return (int) $translations[ $default_language ]->term_id; + } } From 5fed17dc958bea418033f11d3b34c9332956d478 Mon Sep 17 00:00:00 2001 From: Yasir Arafat <148990700+Arafat-plugins@users.noreply.github.com> Date: Thu, 7 May 2026 10:50:26 +0600 Subject: [PATCH 5/9] fix: stabilize translated Directorist pages with WPML --- app/Controller/Hook/Search_Form_Field_Translation.php | 4 ++-- wpml-config.xml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Controller/Hook/Search_Form_Field_Translation.php b/app/Controller/Hook/Search_Form_Field_Translation.php index 3eba8fa..b1d3ce0 100644 --- a/app/Controller/Hook/Search_Form_Field_Translation.php +++ b/app/Controller/Hook/Search_Form_Field_Translation.php @@ -69,10 +69,10 @@ public function translate_search_form_fields_meta( $value, $object_id, $meta_key self::$translating_term_meta = false; if ( empty( $raw_value ) || ! is_array( $raw_value ) || empty( $raw_value['fields'] ) || ! is_array( $raw_value['fields'] ) ) { - return $raw_value; + return $value; } - return $this->translate_search_form_fields( $raw_value, (int) $object_id ); + return [ $this->translate_search_form_fields( $raw_value, (int) $object_id ) ]; } /** diff --git a/wpml-config.xml b/wpml-config.xml index 38874aa..7a25849 100644 --- a/wpml-config.xml +++ b/wpml-config.xml @@ -45,6 +45,7 @@ + From ab5196b85a2491933ca8cf90c0570643360e9357 Mon Sep 17 00:00:00 2001 From: Yasir Arafat <148990700+Arafat-plugins@users.noreply.github.com> Date: Thu, 7 May 2026 11:45:29 +0600 Subject: [PATCH 6/9] fix: expose directory builder strings to WPML ATE --- .../Hook/Add_Listing_Form_Translation.php | 17 +- .../Hook/Directory_Builder_String_Package.php | 742 ++++++++++++++++++ app/Controller/Hook/Init.php | 1 + wpml-config.xml | 12 + 4 files changed, 768 insertions(+), 4 deletions(-) create mode 100644 app/Controller/Hook/Directory_Builder_String_Package.php diff --git a/app/Controller/Hook/Add_Listing_Form_Translation.php b/app/Controller/Hook/Add_Listing_Form_Translation.php index 9a39795..7fbfe3a 100644 --- a/app/Controller/Hook/Add_Listing_Form_Translation.php +++ b/app/Controller/Hook/Add_Listing_Form_Translation.php @@ -175,9 +175,10 @@ private function register_wpml_string( $name, $value ) { // This happens when WPML processes Gutenberg block configurations during string registration ob_start(); - // Suppress warnings during registration + // Suppress WPML internals during registration. Some WPML versions emit + // PHP 8.x deprecations while sanitizing package/string metadata. $error_level = error_reporting(); - error_reporting( $error_level & ~E_WARNING ); + error_reporting( $error_level & ~E_WARNING & ~E_DEPRECATED ); try { // Use WPML String Translation function @@ -212,8 +213,16 @@ private function translate_wpml_string( $value, $name ) { return $value; } - // Use WPML String Translation filter hook - $translated = apply_filters( 'wpml_translate_single_string', $value, self::WPML_DOMAIN, $name ); + // Use WPML String Translation filter hook. Some WPML versions emit PHP + // 8.x deprecations while resolving untranslated strings. + $error_level = error_reporting(); + error_reporting( $error_level & ~E_WARNING & ~E_DEPRECATED ); + + try { + $translated = apply_filters( 'wpml_translate_single_string', $value, self::WPML_DOMAIN, $name ); + } finally { + error_reporting( $error_level ); + } // If translation is empty or same as original, return original // This ensures we never lose the label even if translation is empty diff --git a/app/Controller/Hook/Directory_Builder_String_Package.php b/app/Controller/Hook/Directory_Builder_String_Package.php new file mode 100644 index 0000000..e8587bc --- /dev/null +++ b/app/Controller/Hook/Directory_Builder_String_Package.php @@ -0,0 +1,742 @@ + [ + 'value' => 'Finish', + 'title' => 'Add Listing Wizard: Finish button', + ], + 'add_listing_wizard_save_next' => [ + 'value' => 'Save & Next', + 'title' => 'Add Listing Wizard: Save and next button', + ], + 'add_listing_wizard_go_to_next' => [ + 'value' => 'Go to Next', + 'title' => 'Add Listing Wizard: Next aria label', + ], + ]; + + /** + * Prevent recursive term meta lookups. + * + * @var bool + */ + private static $resolving_term_meta = false; + + /** + * Track packages registered during a request. + * + * @var array + */ + private static $registered_packages = []; + + /** + * Constructor. + * + * @return void + */ + public function __construct() { + add_filter( 'wpml_active_string_package_kinds', [ $this, 'register_package_kind' ] ); + add_action( 'init', [ $this, 'register_directory_builder_packages' ], 30 ); + add_action( 'wpml_register_string_packages', [ $this, 'register_directory_builder_packages' ] ); + + add_action( 'created_' . $this->get_directory_taxonomy(), [ $this, 'register_term_package_on_save' ], 30, 2 ); + add_action( 'edited_' . $this->get_directory_taxonomy(), [ $this, 'register_term_package_on_save' ], 30, 2 ); + add_action( 'directorist_after_update_directory_type', [ $this, 'refresh_directory_builder_package_on_save' ], 30, 1 ); + + add_filter( 'get_term_metadata', [ $this, 'translate_builder_term_meta' ], 30, 4 ); + add_filter( 'atbdp_add_listing_page_template', [ $this, 'translate_add_listing_template_ui' ], 20, 2 ); + } + + /** + * Register package kind for WPML Translation Dashboard. + * + * @param array $kinds Package kind definitions. + * @return array + */ + public function register_package_kind( $kinds ) { + $kinds[ self::PACKAGE_KIND_SLUG ] = [ + 'title' => self::PACKAGE_KIND, + 'slug' => self::PACKAGE_KIND_SLUG, + 'plural' => 'Directorist Directory Builders', + ]; + + return $kinds; + } + + /** + * Register all default-language directory builder packages. + * + * @return void + */ + public function register_directory_builder_packages() { + if ( ! $this->is_wpml_active() ) { + return; + } + + if ( ! is_admin() && ! wp_doing_ajax() && ! wp_doing_cron() ) { + return; + } + + foreach ( $this->get_source_directory_ids() as $directory_id ) { + $this->register_directory_builder_package( $directory_id ); + } + } + + /** + * Register package after a directory term is created/edited. + * + * @param int $term_id Term ID. + * @param int $tt_id Term taxonomy ID. + * @return void + */ + public function register_term_package_on_save( $term_id, $tt_id = 0 ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter + $this->refresh_directory_builder_package_on_save( $term_id ); + } + + /** + * Refresh package strings after Directorist saves directory builder data. + * + * @param int $directory_id Directory type term ID. + * @return void + */ + public function refresh_directory_builder_package_on_save( $directory_id ) { + if ( ! $this->is_wpml_active() ) { + return; + } + + $source_directory_id = $this->get_source_directory_id( (int) $directory_id ); + if ( $source_directory_id <= 0 ) { + return; + } + + $package = $this->get_package( $source_directory_id ); + if ( ! empty( $package['name'] ) ) { + unset( self::$registered_packages[ $package['name'] ] ); + } + + $this->register_directory_builder_package( $source_directory_id ); + } + + /** + * Register all translatable builder strings for one directory type. + * + * @param int $directory_id Directory type term ID. + * @return void + */ + public function register_directory_builder_package( $directory_id ) { + if ( ! $this->is_wpml_active() ) { + return; + } + + $source_directory_id = $this->get_source_directory_id( (int) $directory_id ); + if ( $source_directory_id <= 0 ) { + return; + } + + $package = $this->get_package( $source_directory_id ); + if ( empty( $package['name'] ) || isset( self::$registered_packages[ $package['name'] ] ) ) { + return; + } + + self::$registered_packages[ $package['name'] ] = true; + + do_action( 'wpml_start_string_package_registration', $package ); + + foreach ( $this->builder_meta_keys as $meta_key ) { + $meta_value = $this->get_raw_term_meta( $source_directory_id, $meta_key, true ); + + if ( is_array( $meta_value ) ) { + foreach ( $this->extract_strings( $meta_value, $meta_key ) as $string ) { + $this->register_package_string( $package, $string ); + } + + continue; + } + + if ( $this->is_translatable_string( $meta_key, $meta_value ) ) { + $this->register_package_string( + $package, + $this->build_string_data( $meta_key, [ $meta_key ], $meta_value ) + ); + } + } + + foreach ( $this->template_ui_strings as $string_name => $string_data ) { + if ( empty( $string_data['value'] ) || ! is_string( $string_data['value'] ) ) { + continue; + } + + $this->register_package_string( + $package, + [ + 'name' => $string_name, + 'title' => $string_data['title'], + 'type' => 'LINE', + 'value' => $string_data['value'], + ] + ); + } + + do_action( 'wpml_delete_unused_package_strings', $package ); + } + + /** + * Translate Directorist builder term meta before Directorist renders it. + * + * @param mixed $value Existing metadata value. + * @param int $object_id Term ID. + * @param string $meta_key Meta key. + * @param bool $single Whether a single value was requested. + * @return mixed + */ + public function translate_builder_term_meta( $value, $object_id, $meta_key, $single ) { + if ( self::$resolving_term_meta || ! $single || ! in_array( $meta_key, $this->builder_meta_keys, true ) || ! $this->is_wpml_active() ) { + return $value; + } + + $object_id = (int) $object_id; + if ( $object_id <= 0 || ! $this->is_directory_type_term( $object_id ) ) { + return $value; + } + + $meta_value = $this->normalize_filtered_meta_value( $value ); + + if ( null === $meta_value ) { + self::$resolving_term_meta = true; + $meta_value = get_term_meta( $object_id, $meta_key, true ); + self::$resolving_term_meta = false; + } + + if ( ! is_array( $meta_value ) && ! $this->is_translatable_string( $meta_key, $meta_value ) ) { + return $value; + } + + $source_directory_id = $this->get_source_directory_id( $object_id ); + if ( $source_directory_id <= 0 ) { + return $value; + } + + if ( is_array( $meta_value ) ) { + $translated_value = $this->translate_meta_value( $meta_value, $source_directory_id, $meta_key ); + + return [ $translated_value ]; + } + + $string = $this->build_string_data( $meta_key, [ $meta_key ], $meta_value ); + + return $this->translate_package_string( $meta_value, $string['name'], $this->get_package( $source_directory_id ) ); + } + + /** + * Translate Add Listing wizard strings that are not stored in term meta. + * + * @param string $template_output Rendered Add Listing template output. + * @param array $args Template arguments. + * @return string + */ + public function translate_add_listing_template_ui( $template_output, $args ) { + if ( empty( $template_output ) || ! is_string( $template_output ) || ! $this->is_wpml_active() ) { + return $template_output; + } + + $directory_id = $this->get_directory_id_from_template_args( $args ); + if ( $directory_id <= 0 ) { + return $template_output; + } + + $source_directory_id = $this->get_source_directory_id( $directory_id ); + if ( $source_directory_id <= 0 ) { + return $template_output; + } + + $package = $this->get_package( $source_directory_id ); + + foreach ( $this->template_ui_strings as $string_name => $string_data ) { + $translated = $this->translate_package_string( $string_data['value'], $string_name, $package ); + + if ( empty( $translated ) || $translated === $string_data['value'] ) { + continue; + } + + $template_output = str_replace( '>' . $string_data['value'] . '<', '>' . $translated . '<', $template_output ); + $template_output = str_replace( '>' . $string_data['value'], '>' . $translated, $template_output ); + $template_output = str_replace( '"' . $string_data['value'] . '"', '"' . esc_attr( $translated ) . '"', $template_output ); + } + + return $template_output; + } + + /** + * Translate all translatable strings in a builder meta array. + * + * @param array $data Builder meta value. + * @param int $source_directory_id Source directory type term ID. + * @param string $meta_key Meta key. + * @param array $path Current nested path. + * @return array + */ + private function translate_meta_value( $data, $source_directory_id, $meta_key, $path = [] ) { + foreach ( $data as $key => $value ) { + $current_path = array_merge( $path, [ (string) $key ] ); + + if ( is_array( $value ) ) { + $data[ $key ] = $this->translate_meta_value( $value, $source_directory_id, $meta_key, $current_path ); + continue; + } + + if ( ! $this->is_translatable_string( $key, $value ) ) { + continue; + } + + $string = $this->build_string_data( $meta_key, $current_path, $value ); + $data[ $key ] = $this->translate_package_string( $value, $string['name'], $this->get_package( $source_directory_id ) ); + } + + return $data; + } + + /** + * Translate a package string while preserving the source value as fallback. + * + * @param string $value Source value. + * @param string $string_name WPML string name. + * @param array $package WPML package definition. + * @return string + */ + private function translate_package_string( $value, $string_name, $package ) { + if ( ! is_string( $value ) || '' === trim( $value ) ) { + return $value; + } + + $error_level = error_reporting(); + error_reporting( $error_level & ~E_WARNING & ~E_DEPRECATED ); + + try { + $translated = apply_filters( 'wpml_translate_string', $value, $string_name, $package ); + } finally { + error_reporting( $error_level ); + } + + if ( ! is_string( $translated ) || '' === trim( $translated ) ) { + return $value; + } + + return $translated; + } + + /** + * Register a single builder string in a WPML package. + * + * @param array $package WPML package definition. + * @param array $string String data. + * @return void + */ + private function register_package_string( $package, $string ) { + if ( empty( $string['value'] ) || ! is_string( $string['value'] ) ) { + return; + } + + $error_level = error_reporting(); + error_reporting( $error_level & ~E_WARNING & ~E_DEPRECATED ); + + try { + do_action( + 'wpml_register_string', + $string['value'], + $string['name'], + $package, + $string['title'], + $string['type'] + ); + } finally { + error_reporting( $error_level ); + } + } + + /** + * Resolve the current directory ID from Add Listing template args. + * + * @param array $args Template args. + * @return int + */ + private function get_directory_id_from_template_args( $args ) { + if ( ! empty( $args['single_directory'] ) ) { + return (int) $args['single_directory']; + } + + if ( ! empty( $args['listing_form'] ) && is_object( $args['listing_form'] ) && method_exists( $args['listing_form'], 'get_current_listing_type' ) ) { + return (int) $args['listing_form']->get_current_listing_type(); + } + + if ( ! empty( $_REQUEST['directory_type'] ) ) { + $directory_type = sanitize_text_field( wp_unslash( $_REQUEST['directory_type'] ) ); + + if ( is_numeric( $directory_type ) ) { + return (int) $directory_type; + } + + $term = get_term_by( 'slug', $directory_type, $this->get_directory_taxonomy() ); + if ( $term && ! is_wp_error( $term ) ) { + return (int) $term->term_id; + } + } + + return function_exists( 'directorist_get_default_directory' ) ? (int) directorist_get_default_directory() : 0; + } + + /** + * Extract translatable strings from a builder meta array. + * + * @param array $data Builder meta value. + * @param string $meta_key Meta key. + * @param array $path Current nested path. + * @return array + */ + private function extract_strings( $data, $meta_key, $path = [] ) { + $strings = []; + + foreach ( $data as $key => $value ) { + $current_path = array_merge( $path, [ (string) $key ] ); + + if ( is_array( $value ) ) { + $strings = array_merge( $strings, $this->extract_strings( $value, $meta_key, $current_path ) ); + continue; + } + + if ( ! $this->is_translatable_string( $key, $value ) ) { + continue; + } + + $strings[] = $this->build_string_data( $meta_key, $current_path, $value ); + } + + return $strings; + } + + /** + * Build WPML string metadata. + * + * @param string $meta_key Meta key. + * @param array $path Nested array path. + * @param string $value Original string value. + * @return array + */ + private function build_string_data( $meta_key, $path, $value ) { + $path_key = implode( '__', array_map( [ $this, 'safe_slug' ], $path ) ); + $name = 'builder_' . $this->safe_slug( $meta_key ) . '__' . $path_key; + + if ( strlen( $name ) > 150 ) { + $name = 'builder_' . $this->safe_slug( $meta_key ) . '__' . md5( implode( '/', $path ) ); + } + + $title = $this->humanize_meta_key( $meta_key ) . ': ' . implode( ' > ', array_map( [ $this, 'humanize_path_part' ], $path ) ); + $type = strlen( wp_strip_all_tags( $value ) ) > 120 ? 'AREA' : 'LINE'; + + return [ + 'name' => $name, + 'title' => $title, + 'type' => $type, + 'value' => $value, + ]; + } + + /** + * Check whether a scalar value should be exposed for translation. + * + * @param string|int $key Array key. + * @param mixed $value Value. + * @return bool + */ + private function is_translatable_string( $key, $value ) { + if ( ! is_string( $value ) || '' === trim( $value ) ) { + return false; + } + + $key = $this->safe_slug( $key ); + + $blocked_keys = [ + 'active_template', + 'align', + 'can_move', + 'date_type', + 'field_key', + 'hook', + 'icon', + 'lock', + 'only_for_admin', + 'required', + 'type', + 'value', + 'widget_group', + 'widget_key', + 'widget_name', + 'option_value', + ]; + + if ( in_array( $key, $blocked_keys, true ) ) { + return false; + } + + $allowed_keys = [ + 'button_label', + 'description', + 'heading', + 'label', + 'option_label', + 'placeholder', + 'search_button_label', + 'search_button_text', + 'show_readmore_text', + 'submit_button_label', + 'text', + 'title', + ]; + + if ( in_array( $key, $allowed_keys, true ) ) { + return true; + } + + foreach ( [ '_label', '_placeholder', '_description', '_text', '_title', '_heading' ] as $suffix ) { + if ( strlen( $key ) > strlen( $suffix ) && substr( $key, -strlen( $suffix ) ) === $suffix ) { + return true; + } + } + + return false; + } + + /** + * Get source directory IDs in the default language. + * + * @return array + */ + private function get_source_directory_ids() { + global $wpdb; + + $taxonomy = $this->get_directory_taxonomy(); + $element_type = WPML_Helper::get_wpml_element_type( $taxonomy ); + $default_language = apply_filters( 'wpml_default_language', null ); + + if ( empty( $taxonomy ) || empty( $element_type ) || empty( $default_language ) ) { + return []; + } + + return wp_parse_id_list( + $wpdb->get_col( + $wpdb->prepare( + "SELECT DISTINCT tt.term_id + FROM {$wpdb->term_taxonomy} tt + LEFT JOIN {$wpdb->prefix}icl_translations tr + ON tr.element_id = tt.term_taxonomy_id + AND tr.element_type = %s + WHERE tt.taxonomy = %s + AND ( tr.language_code = %s OR tr.language_code IS NULL )", + $element_type, + $taxonomy, + $default_language + ) + ) + ); + } + + /** + * Get the default-language source directory type for a term. + * + * @param int $directory_id Directory type term ID. + * @return int + */ + private function get_source_directory_id( $directory_id ) { + $directory_id = (int) $directory_id; + + if ( $directory_id <= 0 ) { + return 0; + } + + $default_language = apply_filters( 'wpml_default_language', null ); + $translations = WPML_Helper::get_element_translations( $directory_id, $this->get_directory_taxonomy() ); + + if ( ! empty( $default_language ) && ! empty( $translations[ $default_language ]->term_id ) ) { + return (int) $translations[ $default_language ]->term_id; + } + + return $directory_id; + } + + /** + * Build a WPML package definition for a source directory type. + * + * @param int $source_directory_id Source directory type term ID. + * @return array + */ + private function get_package( $source_directory_id ) { + $source_directory_id = (int) $source_directory_id; + $term = get_term( $source_directory_id, $this->get_directory_taxonomy() ); + $trid = WPML_Helper::get_element_trid( $source_directory_id, $this->get_directory_taxonomy() ); + + $name = $trid > 0 ? 'directory_builder_' . $trid : 'directory_builder_' . $source_directory_id; + + return [ + 'kind' => self::PACKAGE_KIND, + 'kind_slug' => self::PACKAGE_KIND_SLUG, + 'name' => $name, + 'title' => sprintf( + 'Directorist Directory Builder: %s', + ( $term && ! is_wp_error( $term ) && ! empty( $term->name ) ) ? $term->name : $source_directory_id + ), + 'edit_link' => admin_url( 'edit.php?post_type=at_biz_dir&page=atbdp-directory-types&listing_type_id=' . $source_directory_id . '&action=edit' ), + 'view_link' => home_url( '/' ), + ]; + } + + /** + * Get raw term meta without triggering metadata filters. + * + * @param int $term_id Term ID. + * @param string $meta_key Meta key. + * @param bool $single Whether to return a single value. + * @return mixed + */ + private function get_raw_term_meta( $term_id, $meta_key, $single = true ) { + global $wpdb; + + $values = $wpdb->get_col( + $wpdb->prepare( + "SELECT meta_value FROM {$wpdb->termmeta} WHERE term_id = %d AND meta_key = %s ORDER BY meta_id ASC", + (int) $term_id, + $meta_key + ) + ); + + if ( empty( $values ) ) { + return $single ? '' : []; + } + + $values = array_map( 'maybe_unserialize', $values ); + + return $single ? $values[0] : $values; + } + + /** + * Normalize a value coming from the get_term_metadata filter. + * + * @param mixed $value Filtered metadata value. + * @return mixed|null + */ + private function normalize_filtered_meta_value( $value ) { + if ( null === $value ) { + return null; + } + + if ( is_array( $value ) && array_key_exists( 0, $value ) ) { + return $value[0]; + } + + return $value; + } + + /** + * Check if a term is a Directorist directory type. + * + * @param int $term_id Term ID. + * @return bool + */ + private function is_directory_type_term( $term_id ) { + $term = get_term( (int) $term_id ); + + return $term && ! is_wp_error( $term ) && $this->get_directory_taxonomy() === $term->taxonomy; + } + + /** + * Check if WPML string package APIs are available. + * + * @return bool + */ + private function is_wpml_active() { + return defined( 'ICL_SITEPRESS_VERSION' ) + && has_action( 'wpml_register_string' ) + && has_filter( 'wpml_translate_string' ) + && has_filter( 'wpml_object_id' ); + } + + /** + * Get Directorist directory type taxonomy key. + * + * @return string + */ + private function get_directory_taxonomy() { + return defined( 'ATBDP_DIRECTORY_TYPE' ) ? ATBDP_DIRECTORY_TYPE : 'atbdp_listing_types'; + } + + /** + * Sanitize a string for identifier use. + * + * @param string $value Raw value. + * @return string + */ + private function safe_slug( $value ) { + $slug = sanitize_key( sanitize_title( (string) $value ) ); + + return '' !== $slug ? $slug : 'item'; + } + + /** + * Make meta key readable. + * + * @param string $meta_key Meta key. + * @return string + */ + private function humanize_meta_key( $meta_key ) { + return ucwords( str_replace( '_', ' ', (string) $meta_key ) ); + } + + /** + * Make a nested path item readable in ATE. + * + * @param string $path_part Raw path item. + * @return string + */ + private function humanize_path_part( $path_part ) { + if ( is_numeric( $path_part ) ) { + return '#' . (string) $path_part; + } + + return ucwords( str_replace( [ '_', '-' ], ' ', (string) $path_part ) ); + } +} diff --git a/app/Controller/Hook/Init.php b/app/Controller/Hook/Init.php index 6a99583..2615cfe 100644 --- a/app/Controller/Hook/Init.php +++ b/app/Controller/Hook/Init.php @@ -32,6 +32,7 @@ protected function get_hooks() { Directory_Builder_Actions::class, Listings_Actions::class, Directory_Type_Meta_Translation::class, + Directory_Builder_String_Package::class, Category_Directory_Sync::class, Email_Translation::class, diff --git a/wpml-config.xml b/wpml-config.xml index 7a25849..fd54b3f 100644 --- a/wpml-config.xml +++ b/wpml-config.xml @@ -19,15 +19,27 @@ per language so editing the default language does not overwrite translated label values (comp-4370). + - search_form_fields : translate - exposes search form labels. + - single_listings_contents : translate - keeps single listing page layout and widget labels per language for the same reason (comp-4370). + - listings_card_grid_view/listings_card_list_view : translate - + exposes archive card widget labels. + + - submit_button_label : translate - exposes the directory-specific + add-listing submit button label. + - _default : copy - every language needs its own default directory type flag so Directorist always has a valid default (comp-4354). --> submission_form_fields + search_form_fields single_listings_contents + listings_card_grid_view + listings_card_list_view + submit_button_label _default From 8492e6d1138b6f85dd4c48075b02ec7c34051c9c Mon Sep 17 00:00:00 2001 From: Yasir Arafat <148990700+Arafat-plugins@users.noreply.github.com> Date: Thu, 7 May 2026 12:03:55 +0600 Subject: [PATCH 7/9] fix: broaden Directorist builder ATE coverage --- .../Hook/Directory_Builder_String_Package.php | 220 +++++++++++++++++- wpml-config.xml | 4 + 2 files changed, 214 insertions(+), 10 deletions(-) diff --git a/app/Controller/Hook/Directory_Builder_String_Package.php b/app/Controller/Hook/Directory_Builder_String_Package.php index e8587bc..c788dbb 100644 --- a/app/Controller/Hook/Directory_Builder_String_Package.php +++ b/app/Controller/Hook/Directory_Builder_String_Package.php @@ -26,6 +26,7 @@ class Directory_Builder_String_Package { private $builder_meta_keys = [ 'submission_form_fields', 'search_form_fields', + 'single_listing_header', 'single_listings_contents', 'listings_card_grid_view', 'listings_card_list_view', @@ -50,6 +51,106 @@ class Directory_Builder_String_Package { 'value' => 'Go to Next', 'title' => 'Add Listing Wizard: Next aria label', ], + 'add_listing_publish_title' => [ + 'value' => 'You are about to publish', + 'title' => 'Add Listing Publish Step: Title', + ], + 'add_listing_publish_subtitle' => [ + 'value' => 'Are you sure you want to publish this listing?', + 'title' => 'Add Listing Publish Step: Subtitle', + ], + 'add_listing_add_social' => [ + 'value' => 'Add Social', + 'title' => 'Add Listing Social Field: Add social button', + ], + 'add_listing_map_drag_info' => [ + 'value' => 'You can drag pinpoint to place the correct address manually.', + 'title' => 'Add Listing Map Field: Drag info', + ], + 'add_listing_latitude' => [ + 'value' => 'Latitude', + 'title' => 'Add Listing Map Field: Latitude label', + ], + 'add_listing_longitude' => [ + 'value' => 'Longitude', + 'title' => 'Add Listing Map Field: Longitude label', + ], + 'add_listing_latitude_placeholder' => [ + 'value' => 'Enter Latitude eg. 24.89904', + 'title' => 'Add Listing Map Field: Latitude placeholder', + ], + 'add_listing_longitude_placeholder' => [ + 'value' => 'Enter Longitude eg. 91.87198', + 'title' => 'Add Listing Map Field: Longitude placeholder', + ], + 'add_listing_generate_on_map' => [ + 'value' => 'Generate on Map', + 'title' => 'Add Listing Map Field: Generate button', + ], + 'add_listing_hide_map' => [ + 'value' => 'Hide Map', + 'title' => 'Add Listing Map Field: Hide map label', + ], + 'add_listing_upload_drop_here' => [ + 'value' => 'Drop Here', + 'title' => 'Add Listing Image Upload: Drop here label', + ], + 'add_listing_upload_preview' => [ + 'value' => 'Preview', + 'title' => 'Add Listing Image Upload: Preview label', + ], + 'add_listing_upload_drag_image' => [ + 'value' => 'Drag and drop an image', + 'title' => 'Add Listing Image Upload: Drag image label', + ], + 'add_listing_upload_or' => [ + 'value' => 'or', + 'title' => 'Add Listing Image Upload: Or separator', + ], + 'add_listing_upload_or_drag_here' => [ + 'value' => 'or drag and drop image here', + 'title' => 'Add Listing Image Upload: Drag here helper', + ], + 'add_listing_upload_add_more' => [ + 'value' => 'Add More', + 'title' => 'Add Listing Image Upload: Add more label', + ], + 'add_listing_upload_max_file_size_alert' => [ + 'value' => 'Maximum limit for a file is __DT__', + 'title' => 'Add Listing Image Upload: Max file size alert', + ], + 'add_listing_upload_max_total_size_alert' => [ + 'value' => 'Maximum limit for total file size is __DT__', + 'title' => 'Add Listing Image Upload: Max total file size alert', + ], + 'add_listing_upload_min_file_items_alert' => [ + 'value' => 'Minimum __DT__ file is required', + 'title' => 'Add Listing Image Upload: Minimum file item alert', + ], + 'add_listing_upload_max_file_items_alert' => [ + 'value' => 'Maximum limit for total file is __DT__', + 'title' => 'Add Listing Image Upload: Maximum file item alert', + ], + 'add_listing_upload_max_file_size_info' => [ + 'value' => 'Maximum allowed size per file is __DT__', + 'title' => 'Add Listing Image Upload: Max file size info', + ], + 'add_listing_upload_max_total_size_info' => [ + 'value' => 'Maximum total allowed file size is __DT__', + 'title' => 'Add Listing Image Upload: Max total file size info', + ], + 'add_listing_upload_unlimited_images' => [ + 'value' => 'Unlimited images with this plan!', + 'title' => 'Add Listing Image Upload: Unlimited images info', + ], + 'add_listing_upload_max_files_allowed' => [ + 'value' => 'Maximum __DT__ files are allowed', + 'title' => 'Add Listing Image Upload: Max files allowed info', + ], + 'add_listing_upload_max_file_allowed' => [ + 'value' => 'Maximum __DT__ file is allowed', + 'title' => 'Add Listing Image Upload: Max file allowed info', + ], ]; /** @@ -190,7 +291,7 @@ public function register_directory_builder_package( $directory_id ) { continue; } - if ( $this->is_translatable_string( $meta_key, $meta_value ) ) { + if ( $this->is_translatable_string( $meta_key, $meta_value, [ $meta_key ] ) ) { $this->register_package_string( $package, $this->build_string_data( $meta_key, [ $meta_key ], $meta_value ) @@ -244,7 +345,7 @@ public function translate_builder_term_meta( $value, $object_id, $meta_key, $sin self::$resolving_term_meta = false; } - if ( ! is_array( $meta_value ) && ! $this->is_translatable_string( $meta_key, $meta_value ) ) { + if ( ! is_array( $meta_value ) && ! $this->is_translatable_string( $meta_key, $meta_value, [ $meta_key ] ) ) { return $value; } @@ -295,9 +396,7 @@ public function translate_add_listing_template_ui( $template_output, $args ) { continue; } - $template_output = str_replace( '>' . $string_data['value'] . '<', '>' . $translated . '<', $template_output ); - $template_output = str_replace( '>' . $string_data['value'], '>' . $translated, $template_output ); - $template_output = str_replace( '"' . $string_data['value'] . '"', '"' . esc_attr( $translated ) . '"', $template_output ); + $template_output = $this->replace_template_string( $template_output, $string_data['value'], $translated ); } return $template_output; @@ -321,7 +420,7 @@ private function translate_meta_value( $data, $source_directory_id, $meta_key, $ continue; } - if ( ! $this->is_translatable_string( $key, $value ) ) { + if ( ! $this->is_translatable_string( $key, $value, $current_path ) ) { continue; } @@ -361,6 +460,53 @@ private function translate_package_string( $value, $string_name, $package ) { return $translated; } + /** + * Replace a translated template string in text nodes and exact attributes. + * + * @param string $template_output Rendered template output. + * @param string $source Source string. + * @param string $translated Translated string. + * @return string + */ + private function replace_template_string( $template_output, $source, $translated ) { + if ( ! is_string( $template_output ) || ! is_string( $source ) || ! is_string( $translated ) || $source === $translated ) { + return $template_output; + } + + $text_variants = array_unique( [ $source, esc_html( $source ) ] ); + $attribute_variants = array_unique( [ $source, esc_attr( $source ) ] ); + + foreach ( $text_variants as $variant ) { + if ( '' === $variant ) { + continue; + } + + $template_output = preg_replace_callback( + '/>(\s*)' . preg_quote( $variant, '/' ) . '(\s*)' . $matches[1] . esc_html( $translated ) . $matches[2] . '<'; + }, + $template_output + ); + } + + foreach ( $attribute_variants as $variant ) { + if ( '' === $variant ) { + continue; + } + + $template_output = preg_replace_callback( + '/(["\'])' . preg_quote( $variant, '/' ) . '\1/u', + function ( $matches ) use ( $translated ) { + return $matches[1] . esc_attr( $translated ) . $matches[1]; + }, + $template_output + ); + } + + return $template_output; + } + /** * Register a single builder string in a WPML package. * @@ -440,7 +586,7 @@ private function extract_strings( $data, $meta_key, $path = [] ) { continue; } - if ( ! $this->is_translatable_string( $key, $value ) ) { + if ( ! $this->is_translatable_string( $key, $value, $current_path ) ) { continue; } @@ -484,24 +630,61 @@ private function build_string_data( $meta_key, $path, $value ) { * @param mixed $value Value. * @return bool */ - private function is_translatable_string( $key, $value ) { + private function is_translatable_string( $key, $value, $path = [] ) { if ( ! is_string( $value ) || '' === trim( $value ) ) { return false; } - $key = $this->safe_slug( $key ); + $trimmed_value = trim( wp_strip_all_tags( html_entity_decode( $value, ENT_QUOTES, get_bloginfo( 'charset' ) ) ) ); + + if ( '' === $trimmed_value || is_numeric( $trimmed_value ) || in_array( strtolower( $trimmed_value ), [ 'true', 'false', 'yes', 'no', 'on', 'off' ], true ) ) { + return false; + } + + if ( filter_var( $trimmed_value, FILTER_VALIDATE_URL ) ) { + return false; + } + + $key = $this->safe_slug( $key ); + $path = array_map( [ $this, 'safe_slug' ], (array) $path ); $blocked_keys = [ 'active_template', 'align', 'can_move', + 'conditional_logic', + 'custom_block_classes', + 'custom_block_id', 'date_type', + 'default_radius_distance', + 'display_map_info', + 'draggable', + 'enable', + 'enable_tagline', + 'enable_title', 'field_key', + 'footer_thumbail', + 'footer_thumbnail', 'hook', 'icon', + 'id', 'lock', + 'max', + 'max_image_limit', + 'max_location_creation', + 'max_per_image_limit', + 'max_radius_distance', + 'max_total_image_limit', 'only_for_admin', + 'original_widget_key', + 'placeholderkey', + 'post_type', + 'price_range_options', + 'price_unit_field_type', + 'pricing_type', 'required', + 'section_id', + 'show_label', 'type', 'value', 'widget_group', @@ -516,13 +699,26 @@ private function is_translatable_string( $key, $value ) { $allowed_keys = [ 'button_label', + 'cancel_button_label', + 'confirmation_text', + 'confirm_button_label', + 'default_group_label', 'description', 'heading', 'label', + 'lat_long', + 'max_widget_info_text', + 'model_header_text', 'option_label', 'placeholder', + 'price_range_label', + 'price_range_placeholder', + 'price_unit_field_label', + 'price_unit_field_placeholder', 'search_button_label', 'search_button_text', + 'section_title', + 'select_files_label', 'show_readmore_text', 'submit_button_label', 'text', @@ -533,12 +729,16 @@ private function is_translatable_string( $key, $value ) { return true; } - foreach ( [ '_label', '_placeholder', '_description', '_text', '_title', '_heading' ] as $suffix ) { + foreach ( [ 'label', 'placeholder', 'description', 'text', 'title', 'heading' ] as $suffix ) { if ( strlen( $key ) > strlen( $suffix ) && substr( $key, -strlen( $suffix ) ) === $suffix ) { return true; } } + if ( in_array( 'options', $path, true ) && ! in_array( $key, [ 'value', 'option_value', 'id', 'key', 'slug' ], true ) ) { + return true; + } + return false; } diff --git a/wpml-config.xml b/wpml-config.xml index fd54b3f..0020745 100644 --- a/wpml-config.xml +++ b/wpml-config.xml @@ -27,6 +27,9 @@ - listings_card_grid_view/listings_card_list_view : translate - exposes archive card widget labels. + - single_listing_header : translate - exposes single listing header + builder widget labels. + - submit_button_label : translate - exposes the directory-specific add-listing submit button label. @@ -36,6 +39,7 @@ submission_form_fields search_form_fields + single_listing_header single_listings_contents listings_card_grid_view listings_card_list_view From 94765d867a1c418217f6e14b9a39fef171cd865e Mon Sep 17 00:00:00 2001 From: Yasir Arafat <148990700+Arafat-plugins@users.noreply.github.com> Date: Thu, 7 May 2026 14:28:51 +0600 Subject: [PATCH 8/9] Improve WPML TM integration and add directory translations shortcut --- .../Hook/Directory_Builder_String_Package.php | 40 ++++ ...ory_Type_Translation_Management_Button.php | 193 ++++++++++++++++++ app/Controller/Hook/Init.php | 1 + 3 files changed, 234 insertions(+) create mode 100644 app/Controller/Hook/Directory_Type_Translation_Management_Button.php diff --git a/app/Controller/Hook/Directory_Builder_String_Package.php b/app/Controller/Hook/Directory_Builder_String_Package.php index c788dbb..52340ec 100644 --- a/app/Controller/Hook/Directory_Builder_String_Package.php +++ b/app/Controller/Hook/Directory_Builder_String_Package.php @@ -181,6 +181,7 @@ public function __construct() { add_action( 'edited_' . $this->get_directory_taxonomy(), [ $this, 'register_term_package_on_save' ], 30, 2 ); add_action( 'directorist_after_update_directory_type', [ $this, 'refresh_directory_builder_package_on_save' ], 30, 1 ); + add_filter( 'wpml_get_translatable_item', [ $this, 'prepare_package_for_translation_management' ], 20, 3 ); add_filter( 'get_term_metadata', [ $this, 'translate_builder_term_meta' ], 30, 4 ); add_filter( 'atbdp_add_listing_page_template', [ $this, 'translate_add_listing_template_ui' ], 20, 2 ); } @@ -255,6 +256,31 @@ public function refresh_directory_builder_package_on_save( $directory_id ) { $this->register_directory_builder_package( $source_directory_id ); } + /** + * Make Directorist builder packages fully compatible with WPML TM/ATE. + * + * WPML's legacy md5/status calculation only uses package string data when + * the translatable item is explicitly marked as external. + * + * @param mixed $item Translatable item resolved by WPML. + * @param int|object $package Package identifier passed by WPML. + * @param string $type Translation element type/prefix. + * @return mixed + */ + public function prepare_package_for_translation_management( $item, $package, $type = 'package' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter + if ( ! $this->is_directory_builder_package_item( $item ) ) { + return $item; + } + + $item->external_type = true; + + if ( method_exists( $item, 'update_strings_data' ) ) { + $item->update_strings_data(); + } + + return $item; + } + /** * Register all translatable builder strings for one directory type. * @@ -883,6 +909,20 @@ private function is_directory_type_term( $term_id ) { return $term && ! is_wp_error( $term ) && $this->get_directory_taxonomy() === $term->taxonomy; } + /** + * Check whether a WPML translatable item is our builder package. + * + * @param mixed $item Translatable item. + * @return bool + */ + private function is_directory_builder_package_item( $item ) { + return is_object( $item ) + && class_exists( '\WPML_Package' ) + && is_a( $item, '\WPML_Package' ) + && ! empty( $item->kind_slug ) + && self::PACKAGE_KIND_SLUG === $item->kind_slug; + } + /** * Check if WPML string package APIs are available. * diff --git a/app/Controller/Hook/Directory_Type_Translation_Management_Button.php b/app/Controller/Hook/Directory_Type_Translation_Management_Button.php new file mode 100644 index 0000000..8b53971 --- /dev/null +++ b/app/Controller/Hook/Directory_Type_Translation_Management_Button.php @@ -0,0 +1,193 @@ +should_render_button( $hook_suffix ) ) { + return; + } + + if ( wp_script_is( self::SCRIPT_HANDLE, 'registered' ) || wp_script_is( self::SCRIPT_HANDLE, 'enqueued' ) ) { + wp_add_inline_script( self::SCRIPT_HANDLE, $this->get_inline_script(), 'after' ); + } + + if ( wp_style_is( self::STYLE_HANDLE, 'registered' ) || wp_style_is( self::STYLE_HANDLE, 'enqueued' ) ) { + wp_add_inline_style( self::STYLE_HANDLE, $this->get_inline_style() ); + } + } + + /** + * Check whether the button should render. + * + * @param string $hook_suffix Current admin page hook suffix. + * @return bool + */ + private function should_render_button( $hook_suffix ) { + if ( self::PAGE_HOOK_SUFFIX !== $hook_suffix ) { + return false; + } + + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; + $action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : ''; + + if ( 'atbdp-directory-types' !== $page || 'edit' === $action ) { + return false; + } + + if ( ! $this->is_wpml_translation_management_available() ) { + return false; + } + + return true; + } + + /** + * Check whether WPML and Translation Management are available. + * + * @return bool + */ + private function is_wpml_translation_management_available() { + return defined( 'ICL_SITEPRESS_VERSION' ) && defined( 'WPML_TM_VERSION' ); + } + + /** + * Build the Translation Management dashboard URL. + * + * @return string + */ + private function get_translation_dashboard_url() { + return (string) apply_filters( + 'directorist_wpml_integration_translation_dashboard_url', + admin_url( 'admin.php?page=tm/menu/main.php' ) + ); + } + + /** + * Build the button label. + * + * @return string + */ + private function get_button_label() { + return (string) apply_filters( + 'directorist_wpml_integration_translation_dashboard_label', + __( 'WPML Translations', 'directorist-wpml-integration' ) + ); + } + + /** + * Get the inline script that injects the button into Directorist UI. + * + * @return string + */ + private function get_inline_script() { + $config = [ + 'buttonLabel' => $this->get_button_label(), + 'buttonUrl' => esc_url_raw( $this->get_translation_dashboard_url() ), + ]; + + $script = <<<'JS' +( function() { + var config = __CONFIG__; + + if ( ! config || ! config.buttonUrl ) { + return; + } + + var createIcon = function() { + return [ + '', + '', + '' + ].join( '' ); + }; + + var injectButton = function() { + var wrapper = document.querySelector( '.directorist-total-types .directorist_link-block-wrapper' ); + + if ( ! wrapper || wrapper.querySelector( '.directorist-wpml-translation-management-button' ) ) { + return; + } + + var button = document.createElement( 'button' ); + button.type = 'button'; + button.className = 'directorist_link-block directorist_link-block-primary-outline directorist-wpml-translation-management-button'; + button.innerHTML = [ + '' + createIcon() + '', + '' + ].join( '' ); + + button.querySelector( '.directorist_link-text' ).textContent = config.buttonLabel || ''; + button.addEventListener( 'click', function() { + window.location.href = config.buttonUrl; + } ); + + wrapper.insertBefore( button, wrapper.firstElementChild ); + }; + + if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', injectButton ); + return; + } + + injectButton(); +}() ); +JS; + + return str_replace( '__CONFIG__', wp_json_encode( $config ), $script ); + } + + /** + * Get small admin style adjustments for the injected button. + * + * @return string + */ + private function get_inline_style() { + return ' + .directorist-wpml-translation-management-button { + white-space: nowrap; + } + + .directorist-wpml-translation-management-button__icon svg { + display: block; + } + '; + } +} diff --git a/app/Controller/Hook/Init.php b/app/Controller/Hook/Init.php index 2615cfe..c35d732 100644 --- a/app/Controller/Hook/Init.php +++ b/app/Controller/Hook/Init.php @@ -33,6 +33,7 @@ protected function get_hooks() { Listings_Actions::class, Directory_Type_Meta_Translation::class, Directory_Builder_String_Package::class, + Directory_Type_Translation_Management_Button::class, Category_Directory_Sync::class, Email_Translation::class, From 465e2f12c30eec50710b659703e4b5e62eecc221 Mon Sep 17 00:00:00 2001 From: Yasir Arafat <148990700+Arafat-plugins@users.noreply.github.com> Date: Thu, 7 May 2026 16:55:35 +0600 Subject: [PATCH 9/9] Fix Directorist page setup selections in WPML admin languages --- app/Controller/Hook/Init.php | 1 + .../Hook/Page_Setup_Translation.php | 177 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 app/Controller/Hook/Page_Setup_Translation.php diff --git a/app/Controller/Hook/Init.php b/app/Controller/Hook/Init.php index c35d732..d773c4d 100644 --- a/app/Controller/Hook/Init.php +++ b/app/Controller/Hook/Init.php @@ -34,6 +34,7 @@ protected function get_hooks() { Directory_Type_Meta_Translation::class, Directory_Builder_String_Package::class, Directory_Type_Translation_Management_Button::class, + Page_Setup_Translation::class, Category_Directory_Sync::class, Email_Translation::class, diff --git a/app/Controller/Hook/Page_Setup_Translation.php b/app/Controller/Hook/Page_Setup_Translation.php new file mode 100644 index 0000000..8f2c704 --- /dev/null +++ b/app/Controller/Hook/Page_Setup_Translation.php @@ -0,0 +1,177 @@ +should_translate_page_setup_options() || ! is_array( $options ) ) { + return $options; + } + + $current_language = apply_filters( 'wpml_current_language', null ); + $default_language = apply_filters( 'wpml_default_language', null ); + + if ( empty( $current_language ) || empty( $default_language ) || $current_language === $default_language ) { + return $options; + } + + foreach ( $this->page_option_keys as $option_key ) { + if ( empty( $options[ $option_key ] ) || ! is_numeric( $options[ $option_key ] ) ) { + continue; + } + + $translated_id = apply_filters( + 'wpml_object_id', + (int) $options[ $option_key ], + 'page', + false, + $current_language + ); + + if ( ! empty( $translated_id ) ) { + $options[ $option_key ] = (int) $translated_id; + } + } + + return $options; + } + + /** + * Normalize page setup selections back to the default language before save. + * + * This keeps Directorist's canonical settings in the source language even if + * an admin saves the settings while browsing the dashboard in another + * language. + * + * @param mixed $new_value New option value. + * @param mixed $old_value Previous option value. + * @param string $option Option name. + * @return mixed + */ + public function normalize_page_setup_option_ids( $new_value, $old_value, $option ) { + if ( ! $this->should_normalize_page_setup_options() || ! is_array( $new_value ) ) { + return $new_value; + } + + $current_language = apply_filters( 'wpml_current_language', null ); + $default_language = apply_filters( 'wpml_default_language', null ); + + if ( empty( $current_language ) || empty( $default_language ) || $current_language === $default_language ) { + return $new_value; + } + + foreach ( $this->page_option_keys as $option_key ) { + if ( empty( $new_value[ $option_key ] ) || ! is_numeric( $new_value[ $option_key ] ) ) { + continue; + } + + $source_id = apply_filters( + 'wpml_object_id', + (int) $new_value[ $option_key ], + 'page', + false, + $default_language + ); + + if ( ! empty( $source_id ) ) { + $new_value[ $option_key ] = (int) $source_id; + } + } + + return $new_value; + } + + /** + * Check whether we should translate page setup options for the admin screen. + * + * @return bool + */ + private function should_translate_page_setup_options() { + if ( ! $this->is_wpml_ready() || ! is_admin() ) { + return false; + } + + $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; + + if ( 'atbdp-settings' === $page ) { + return true; + } + + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; + + return false !== strpos( $request_uri, 'page=atbdp-settings' ); + } + + /** + * Check whether we should normalize page setup options during save. + * + * @return bool + */ + private function should_normalize_page_setup_options() { + if ( ! $this->is_wpml_ready() || ! is_admin() || ! wp_doing_ajax() ) { + return false; + } + + if ( empty( $_POST['action'] ) ) { + return false; + } + + return 'save_settings_data' === sanitize_text_field( wp_unslash( $_POST['action'] ) ); + } + + /** + * Check whether the WPML APIs we need are available. + * + * @return bool + */ + private function is_wpml_ready() { + return defined( 'ICL_SITEPRESS_VERSION' ) && function_exists( 'apply_filters' ) && has_filter( 'wpml_object_id' ); + } +}