is_active()) { add_action('init', [$this, 'init_wpml_hooks']); } } /** * Initialize WPML hooks */ public function init_wpml_hooks() { // Add any WPML-specific hooks here } /** * Get the name of the multilingual plugin * * @return string */ public function get_plugin_name() { return 'WPML'; } /** * Check if WPML is active and functional * * @return bool */ public function is_active() { global $sitepress; // Check if WPML classes exist and sitepress is properly initialized if (!defined('ICL_SITEPRESS_VERSION') && !class_exists('SitePress')) { return false; } return isset($sitepress) && is_object($sitepress) && class_exists('SitePress') && $sitepress instanceof SitePress; } /** * Get the WPML SitePress instance * * @return SitePress|null */ private function get_sitepress() { global $sitepress; if (isset($sitepress) && is_object($sitepress) && class_exists('SitePress') && $sitepress instanceof SitePress) { return $sitepress; } return null; } /** * Get available WPML languages * * @return array Array of language data */ public function get_available_languages() { if (!$this->is_active()) { return []; } $languages = []; $sitepress = $this->get_sitepress(); if ($sitepress) { $active_languages = $sitepress->get_active_languages(); foreach ($active_languages as $code => $lang_data) { $languages[$code] = (object) [ 'slug' => $code, 'code' => $code, 'name' => $lang_data['display_name'] ?? $lang_data['english_name'] ?? $code, 'native_name' => $lang_data['native_name'] ?? $lang_data['display_name'] ?? $code, 'flag' => $this->get_language_flag($code), 'is_default' => ($code === $this->get_default_language()) ]; } } return $languages; } /** * Get the default language code * * @return string|null */ public function get_default_language() { if (!$this->is_active()) { return null; } $sitepress = $this->get_sitepress(); if ($sitepress) { return $sitepress->get_default_language(); } // Fallback using WPML function if (function_exists('wpml_get_default_language')) { return wpml_get_default_language(); } return null; } /** * Get the language of a specific post * * @param int $post_id Post ID * @return string|null Language code or null */ public function get_post_language($post_id) { if (!$this->is_active()) { return null; } // Use WPML API $language_details = apply_filters('wpml_post_language_details', null, $post_id); if (is_array($language_details) && isset($language_details['language_code'])) { return $language_details['language_code']; } // Fallback method if (function_exists('wpml_get_language_information')) { $lang_info = wpml_get_language_information(null, $post_id); if (isset($lang_info['language_code'])) { return $lang_info['language_code']; } } return null; } /** * Set the language for a post * * @param int $post_id Post ID * @param string $lang_code Language code * @return bool Success */ public function set_post_language($post_id, $lang_code) { if (!$this->is_active()) { return false; } $post = get_post($post_id); if (!$post) { return false; } $post_type = $post->post_type; // Get element type for WPML (e.g., 'post_page', 'post_post') $element_type = 'post_' . $post_type; // Use WPML API to set language do_action('wpml_set_element_language_details', [ 'element_id' => $post_id, 'element_type' => $element_type, 'trid' => false, // Will create new trid 'language_code' => $lang_code, 'source_language_code' => null ]); AITRFOEL_Logger::log( sprintf('WPML: Set language "%s" for post %d', $lang_code, $post_id), 'info', $post_id ); return true; } /** * Get all translations of a post * * @param int $post_id Post ID * @return array Associative array of language_code => post_id */ public function get_post_translations($post_id) { if (!$this->is_active()) { return []; } $translations = []; // Get element type $post = get_post($post_id); if (!$post) { return []; } $element_type = 'post_' . $post->post_type; // Get trid (translation group ID) $trid = apply_filters('wpml_element_trid', null, $post_id, $element_type); if ($trid) { // Get all translations in this group $element_translations = apply_filters('wpml_get_element_translations', null, $trid, $element_type); if (is_array($element_translations)) { foreach ($element_translations as $lang_code => $translation) { if (isset($translation->element_id)) { $translations[$lang_code] = (int) $translation->element_id; } } } } return $translations; } /** * Setup translation relationship between original and translated post * * @param int $original_post_id Original post ID * @param int $translated_post_id Translated post ID * @param string $target_language Target language code or full name * @return bool|WP_Error Success or error */ public function setup_translation_relationship($original_post_id, $translated_post_id, $target_language) { if (!$this->is_active()) { AITRFOEL_Logger::log('WPML is not active', 'error', $original_post_id); return new WP_Error('wpml_inactive', 'WPML is not active.'); } // Normalize language code $wpml_lang_code = $this->normalize_language_code($target_language); if (!$wpml_lang_code || !$this->language_exists($wpml_lang_code)) { AITRFOEL_Logger::log( sprintf('Language "%s" not found in WPML.', $target_language), 'error', $original_post_id ); return new WP_Error('language_not_found', sprintf('Language "%s" not found in WPML.', $target_language)); } // Get original post info $original_post = get_post($original_post_id); $translated_post = get_post($translated_post_id); if (!$original_post || !$translated_post) { return new WP_Error('invalid_posts', 'Invalid post IDs provided.'); } $post_type = $original_post->post_type; $element_type = 'post_' . $post_type; // Get or set language for original post $original_lang_code = $this->get_post_language($original_post_id); if (!$original_lang_code) { // Set default language for original $default_lang = $this->get_default_language(); if (!$default_lang) { $available = $this->get_available_languages(); if (!empty($available)) { $first = reset($available); $default_lang = $first->slug; } } if ($default_lang) { $this->set_post_language($original_post_id, $default_lang); $original_lang_code = $default_lang; AITRFOEL_Logger::log( sprintf('WPML: Set default language "%s" for original post %d', $default_lang, $original_post_id), 'info', $original_post_id ); } else { return new WP_Error('no_default_language', 'Cannot determine default language.'); } } // Get trid from original post $trid = apply_filters('wpml_element_trid', null, $original_post_id, $element_type); if (!$trid) { // Original post doesn't have a trid - register it in WPML first AITRFOEL_Logger::log( sprintf('WPML: Original post %d has no trid, registering it first', $original_post_id), 'info', $original_post_id ); do_action('wpml_set_element_language_details', [ 'element_id' => $original_post_id, 'element_type' => $element_type, 'trid' => false, 'language_code' => $original_lang_code, 'source_language_code' => null ]); // Re-fetch the trid now that original is registered $trid = apply_filters('wpml_element_trid', null, $original_post_id, $element_type); if (!$trid) { AITRFOEL_Logger::log( sprintf('WPML: Failed to create trid for original post %d', $original_post_id), 'error', $original_post_id ); return new WP_Error('no_trid', 'Failed to create WPML translation group for original post.'); } AITRFOEL_Logger::log( sprintf('WPML: Created trid %s for original post %d', $trid, $original_post_id), 'success', $original_post_id ); } // Link translated post to original using WPML API // First, directly check DB for existing stale translation in this trid+language // WPML's API filters (wpml_get_element_translations) don't return trashed/deleted posts, // but their DB rows still exist and cause "Duplicate entry" errors on INSERT. global $wpdb; $icl_table = $wpdb->prefix . 'icl_translations'; // Check for ANY existing row with this trid+language_code $stale_row = $wpdb->get_row($wpdb->prepare( "SELECT translation_id, element_id FROM {$icl_table} WHERE trid = %d AND language_code = %s AND element_type = %s", $trid, $wpml_lang_code, $element_type )); if ($stale_row && (int) $stale_row->element_id !== $translated_post_id) { // Remove the stale entry to prevent duplicate key error $wpdb->delete($icl_table, [ 'translation_id' => $stale_row->translation_id, ]); AITRFOEL_Logger::log( sprintf('WPML: Removed stale %s DB entry (element_id: %d, translation_id: %d) from trid %s before linking new post %d', $wpml_lang_code, $stale_row->element_id, $stale_row->translation_id, $trid, $translated_post_id), 'warning', $original_post_id, $translated_post_id ); } // Also remove any existing entry for the translated post itself (might be in wrong trid) $translated_row = $wpdb->get_row($wpdb->prepare( "SELECT translation_id, trid FROM {$icl_table} WHERE element_id = %d AND element_type = %s", $translated_post_id, $element_type )); if ($translated_row && (int) $translated_row->trid !== (int) $trid) { $wpdb->delete($icl_table, [ 'translation_id' => $translated_row->translation_id, ]); AITRFOEL_Logger::log( sprintf('WPML: Removed orphan entry for post %d (was in trid %s, needs to be in trid %s)', $translated_post_id, $translated_row->trid, $trid), 'warning', $original_post_id, $translated_post_id ); } // Clear WPML caches before linking so it sees the cleaned state do_action('wpml_cache_clear'); do_action('wpml_set_element_language_details', [ 'element_id' => $translated_post_id, 'element_type' => $element_type, 'trid' => $trid, 'language_code' => $wpml_lang_code, 'source_language_code' => $original_lang_code ]); // Clear WordPress post cache to ensure get_permalink() returns correct URL clean_post_cache($translated_post_id); clean_post_cache($original_post_id); // Clear WPML caches if available global $wpml_post_translations; if (isset($wpml_post_translations) && is_object($wpml_post_translations) && method_exists($wpml_post_translations, 'reload')) { $wpml_post_translations->reload(); } // Also try to clear WPML's element translations cache if (class_exists('WPML_TM_Translation_Status')) { // Force WPML to refresh its internal caches do_action('wpml_cache_clear'); } AITRFOEL_Logger::log( sprintf('WPML: Linked translation %d (lang: %s) to original %d (lang: %s, trid: %s)', $translated_post_id, $wpml_lang_code, $original_post_id, $original_lang_code, $trid ?: 'new' ), 'info', $original_post_id, $translated_post_id ); // Verify the relationship was created (check from ORIGINAL post's perspective) $verify_translations = $this->get_post_translations($original_post_id); if (isset($verify_translations[$wpml_lang_code]) && $verify_translations[$wpml_lang_code] == $translated_post_id) { AITRFOEL_Logger::log( sprintf('✅ WPML relationship created successfully! Group: %s', json_encode($verify_translations)), 'success', $original_post_id, $translated_post_id ); return true; } else { AITRFOEL_Logger::log( sprintf('⚠️ WPML relationship may not be complete. Original post %d translations: %s (expected %s => %d)', $original_post_id, json_encode($verify_translations), $wpml_lang_code, $translated_post_id ), 'warning', $original_post_id, $translated_post_id ); // Retry once — WPML cache might be stale do_action('wpml_cache_clear'); clean_post_cache($original_post_id); clean_post_cache($translated_post_id); $retry_translations = $this->get_post_translations($original_post_id); if (isset($retry_translations[$wpml_lang_code]) && $retry_translations[$wpml_lang_code] == $translated_post_id) { AITRFOEL_Logger::log( sprintf('✅ WPML relationship confirmed on retry! Group: %s', json_encode($retry_translations)), 'success', $original_post_id, $translated_post_id ); return true; } return new WP_Error('wpml_link_failed', sprintf('WPML failed to link post %d as %s translation of post %d', $translated_post_id, $wpml_lang_code, $original_post_id)); } } /** * Get translation status for a post across all languages * * @param int $post_id Post ID * @return array Translation status array */ public function get_translation_status($post_id) { if (!$this->is_active()) { return []; } $status = []; $translations = $this->get_post_translations($post_id); $available_languages = $this->get_available_languages(); foreach ($available_languages as $lang) { $lang_code = $lang->slug; $has_translation = isset($translations[$lang_code]); $translation_id = $has_translation ? $translations[$lang_code] : null; $status[$lang_code] = [ 'language_name' => $lang->name, 'language_code' => $lang_code, 'translated' => $has_translation, 'post_id' => $translation_id, 'edit_url' => $translation_id ? get_edit_post_link($translation_id) : null, 'view_url' => $translation_id ? get_permalink($translation_id) : null, 'status' => $translation_id ? get_post_status($translation_id) : null ]; } return $status; } /** * Check if a language code exists in WPML * * @param string $lang_code Language code to check * @return bool */ public function language_exists($lang_code) { if (!$this->is_active()) { return false; } $languages = $this->get_available_languages(); foreach ($languages as $lang) { if ($lang->slug === $lang_code) { return true; } } return false; } /** * Convert language name to WPML language code * * @param string $our_language Our language name (e.g., 'Russian') * @return string|null WPML language code (e.g., 'ru') or null */ public function get_language_code($our_language) { return $this->normalize_language_code($our_language); } /** * Remove translation relationship * * @param int $post_id Post ID to remove from translations * @return bool Success */ public function remove_translation_relationship($post_id) { if (!$this->is_active()) { return false; } try { $post = get_post($post_id); if (!$post) { return false; } $element_type = 'post_' . $post->post_type; // Remove the translation link by setting trid to false do_action('wpml_set_element_language_details', [ 'element_id' => $post_id, 'element_type' => $element_type, 'trid' => false, 'language_code' => null, 'source_language_code' => null ]); AITRFOEL_Logger::log( sprintf('WPML: Removed translation relationship for post %d', $post_id), 'info', $post_id ); return true; } catch (Exception $e) { AITRFOEL_Logger::log( sprintf('Failed to remove WPML translation relationship for post %d: %s', $post_id, $e->getMessage()), 'error', $post_id ); return false; } } /** * Get all pages with their translation status * * @return array Array of pages with translation status */ public function get_pages_translation_status() { if (!$this->is_active()) { return []; } // Get all pages with Elementor data $pages = get_posts([ 'post_type' => 'page', 'posts_per_page' => -1, 'post_status' => ['publish', 'draft'], 'meta_query' => [ [ 'key' => '_elementor_data', 'compare' => 'EXISTS' ] ], 'orderby' => 'title', 'order' => 'ASC', 'suppress_filters' => true // Get posts in all languages ]); $pages_status = []; foreach ($pages as $page) { $translation_status = $this->get_translation_status($page->ID); $pages_status[] = [ 'id' => $page->ID, 'title' => $page->post_title, 'status' => $page->post_status, 'edit_url' => get_edit_post_link($page->ID), 'view_url' => get_permalink($page->ID), 'translations' => $translation_status, 'has_elementor' => !empty(get_post_meta($page->ID, '_elementor_data', true)) ]; } return $pages_status; } /** * Get language switcher data for admin * * @return array Language switcher data */ public function get_language_switcher_data() { if (!$this->is_active()) { return []; } $languages = $this->get_available_languages(); $default_lang = $this->get_default_language(); $switcher_data = []; foreach ($languages as $lang) { $switcher_data[] = [ 'code' => $lang->slug, 'name' => $lang->name, 'native_name' => $lang->native_name ?? $lang->name, 'flag' => $lang->flag ?? '', 'is_default' => $lang->slug === $default_lang ]; } return $switcher_data; } /** * Get language flag URL from WPML * * @param string $lang_code Language code * @return string Flag URL or empty string */ private function get_language_flag($lang_code) { $sitepress = $this->get_sitepress(); if ($sitepress && method_exists($sitepress, 'get_flag_url')) { return $sitepress->get_flag_url($lang_code); } // Try using WPML filter $flag_url = apply_filters('wpml_language_flag_url', '', $lang_code); return $flag_url ?: ''; } /** * Get translated post ID using WPML's icl_object_id function * * @param int $post_id Original post ID * @param string $lang_code Target language code * @param string $post_type Post type (default: 'page') * @return int|null Translated post ID or null */ public function get_translated_post_id($post_id, $lang_code, $post_type = 'page') { if (!$this->is_active()) { return null; } // Use WPML's object ID function if (function_exists('icl_object_id')) { $translated_id = icl_object_id($post_id, $post_type, false, $lang_code); return $translated_id ?: null; } // Fallback to filter $translated_id = apply_filters('wpml_object_id', $post_id, $post_type, false, $lang_code); return $translated_id ?: null; } /** * Get the correct permalink for a post in its assigned language * This ensures WPML returns the URL with the correct language parameter * * @param int $post_id Post ID * @return string The permalink with correct language */ public function get_post_permalink($post_id) { if (!$this->is_active()) { return get_permalink($post_id); } // Get the post's language $post_lang = $this->get_post_language($post_id); if (!$post_lang) { return get_permalink($post_id); } // Store current language $current_lang = apply_filters('wpml_current_language', null); // Switch to the post's language temporarily do_action('wpml_switch_language', $post_lang); // Get the permalink in the correct language context $permalink = get_permalink($post_id); // Also try the wpml_permalink filter for additional processing $permalink = apply_filters('wpml_permalink', $permalink, $post_lang); // Switch back to original language if ($current_lang) { do_action('wpml_switch_language', $current_lang); } else { do_action('wpml_switch_language', null); // Reset to default } return $permalink; } }