detect_multilingual_plugin();
// AJAX handler for dismissing TranslatePress notice
add_action('wp_ajax_aitrfoel_dismiss_translatepress_notice', [$this, 'ajax_dismiss_translatepress_notice']);
// Dynamic sorting: sync translated posts array to match original ordering attributes
add_filter('the_posts', [$this, 'sync_query_posts_order'], 25, 2);
// Filter JetEngine Listing Grid loops to preserve original post order
add_filter('jet-engine/listing/query/items', [$this, 'sync_jet_listing_items_order'], 25, 3);
// Filter Elementor Header Footer Builder template IDs
add_filter('get_hfe_header_id', [$this, 'swap_hfe_template_id'], 20, 1);
add_filter('get_hfe_footer_id', [$this, 'swap_hfe_template_id'], 20, 1);
}
/**
* AJAX handler to dismiss TranslatePress notice
*/
public function ajax_dismiss_translatepress_notice() {
check_ajax_referer('aitrfoel_dismiss_tp');
if (current_user_can('manage_options')) {
update_option('aitrfoel_translatepress_notice_dismissed', true);
}
wp_die();
}
/**
* Detect which multilingual plugin is active
* Priority: If user explicitly enabled built-in, use it regardless of other plugins.
* Otherwise: Polylang first (backward compatibility), then WPML, then built-in.
*/
private function detect_multilingual_plugin() {
// Check for TranslatePress — not supported due to fundamentally different architecture
if ($this->is_translatepress_available()) {
AITRFOEL_Logger::log('Multilingual Manager: TranslatePress detected but not supported', 'warning');
add_action('admin_notices', [$this, 'admin_notice_translatepress']);
}
// If the user explicitly enabled the built-in system, respect that choice
// even if Polylang or WPML is also installed
if ($this->is_builtin_available()) {
$this->detected_plugin = 'builtin';
$this->integration = AITRFOEL_Builtin_Integration::get_instance();
AITRFOEL_Logger::log('Multilingual Manager: Built-in language system enabled and loaded (user choice)', 'info');
return;
}
// Check for Polylang (backward compatibility)
if ($this->is_polylang_available()) {
$this->detected_plugin = 'polylang';
$this->integration = AITRFOEL_Polylang_Integration::get_instance();
AITRFOEL_Logger::log('Multilingual Manager: Polylang detected and loaded', 'info');
return;
}
// Check for WPML
if ($this->is_wpml_available()) {
$this->detected_plugin = 'wpml';
$this->integration = AITRFOEL_WPML_Integration::get_instance();
AITRFOEL_Logger::log('Multilingual Manager: WPML detected and loaded', 'info');
return;
}
// No multilingual plugin found
$this->detected_plugin = null;
$this->integration = null;
AITRFOEL_Logger::log('Multilingual Manager: No multilingual plugin detected', 'warning');
}
/**
* Check if Polylang is available
*
* @return bool
*/
private function is_polylang_available() {
return function_exists('pll_get_post_language') &&
function_exists('pll_set_post_language') &&
function_exists('pll_get_post_translations') &&
function_exists('pll_save_post_translations');
}
/**
* Check if WPML is available
*
* @return bool
*/
private function is_wpml_available() {
// WPML main plugin check
return defined('ICL_SITEPRESS_VERSION') ||
class_exists('SitePress') ||
function_exists('icl_object_id');
}
/**
* Check if the built-in language system is available and enabled
*
* @return bool
*/
private function is_builtin_available() {
return class_exists('AITRFOEL_Builtin_Integration') &&
AITRFOEL_Builtin_Integration::is_enabled();
}
/**
* Check if TranslatePress is installed/active
*/
private function is_translatepress_available() {
return class_exists('TRP_Translate_Press') ||
defined('TRP_PLUGIN_VERSION') ||
function_exists('trp_translate');
}
/**
* Admin notice for TranslatePress detection
*/
public function admin_notice_translatepress() {
// Only show on our plugin pages and dashboard
$screen = get_current_screen();
if (!$screen) return;
$our_pages = ['toplevel_page_ai-translator', 'dashboard'];
$is_our_page = in_array($screen->id, $our_pages) || strpos($screen->id, 'ai-translator') !== false;
if (!$is_our_page) return;
// Allow dismissal
$dismissed = get_option('aitrfoel_translatepress_notice_dismissed', false);
if ($dismissed) return;
?>
Supported systems: Polylang, WPML, or our Built-in Language System (recommended — includes menu and widget translation).', 'ai-translator-for-elementor-and-polylang')); ?>
Built-in Language System in Settings → Languages & Switcher to get full translation support including menus and widgets.', 'ai-translator-for-elementor-and-polylang'),
esc_url(admin_url('admin.php?page=ai-translator-for-elementor-and-polylang-settings#tab-languages'))
));
?>
integration;
}
/**
* Check if any multilingual plugin is active
*
* @return bool
*/
public function has_multilingual_plugin() {
return $this->integration !== null && $this->integration->is_active();
}
/**
* Get the name of the detected multilingual plugin
*
* @return string|null 'polylang', 'wpml', or null
*/
public function get_detected_plugin() {
return $this->detected_plugin;
}
/**
* Get the display name of the detected plugin
*
* @return string
*/
public function get_plugin_display_name() {
if ($this->integration) {
return $this->integration->get_plugin_name();
}
return __('None', 'ai-translator-for-elementor-and-polylang');
}
/**
* Check if the detected plugin is Polylang
*
* @return bool
*/
public function is_polylang() {
return $this->detected_plugin === 'polylang';
}
/**
* Check if the detected plugin is WPML
*
* @return bool
*/
public function is_wpml() {
return $this->detected_plugin === 'wpml';
}
/**
* Check if the detected system is the built-in language manager
*
* @return bool
*/
public function is_builtin() {
return $this->detected_plugin === 'builtin';
}
/**
* Check if TranslatePress is detected (unsupported)
*/
public function is_translatepress_detected() {
return $this->is_translatepress_available();
}
/**
* Force re-detection of multilingual plugin
* Useful after plugin activation/deactivation
*/
public function refresh() {
$this->integration = null;
$this->detected_plugin = null;
$this->detect_multilingual_plugin();
}
// =========================================================================
// CONVENIENCE PROXY METHODS
// These methods proxy to the active integration for easier access
// =========================================================================
/**
* Check if multilingual plugin is active
*
* @return bool
*/
public function is_active() {
return $this->integration !== null && $this->integration->is_active();
}
/**
* Get available languages
*
* @return array
*/
public function get_available_languages() {
if (!$this->integration) {
return [];
}
return $this->integration->get_available_languages();
}
/**
* Check if languages are configured (at least 2 languages for translation)
*
* @return bool
*/
public function has_languages_configured() {
if (!$this->integration) {
return false;
}
$languages = $this->integration->get_available_languages();
return count($languages) >= 2;
}
/**
* Get the number of configured languages
*
* @return int
*/
public function get_language_count() {
if (!$this->integration) {
return 0;
}
return count($this->integration->get_available_languages());
}
/**
* Get default language
*
* @return string|null
*/
public function get_default_language() {
if (!$this->integration) {
return null;
}
return $this->integration->get_default_language();
}
/**
* Get post language
*
* @param int $post_id
* @return string|null
*/
public function get_post_language($post_id) {
if (!$this->integration) {
return null;
}
return $this->integration->get_post_language($post_id);
}
/**
* Set post language
*
* @param int $post_id
* @param string $lang_code
* @return bool
*/
public function set_post_language($post_id, $lang_code) {
if (!$this->integration) {
return false;
}
return $this->integration->set_post_language($post_id, $lang_code);
}
/**
* Get post translations
*
* @param int $post_id
* @return array
*/
public function get_post_translations($post_id) {
if (!$this->integration) {
return [];
}
return $this->integration->get_post_translations($post_id);
}
/**
* Setup translation relationship
*
* @param int $original_post_id
* @param int $translated_post_id
* @param string $target_language
* @return bool|WP_Error
*/
public function setup_translation_relationship($original_post_id, $translated_post_id, $target_language) {
if (!$this->integration) {
return new WP_Error('no_multilingual_plugin', __('No multilingual plugin is active.', 'ai-translator-for-elementor-and-polylang'));
}
return $this->integration->setup_translation_relationship($original_post_id, $translated_post_id, $target_language);
}
/**
* Get translation status
*
* @param int $post_id
* @return array
*/
public function get_translation_status($post_id) {
if (!$this->integration) {
return [];
}
return $this->integration->get_translation_status($post_id);
}
/**
* Check if language exists
*
* @param string $lang_code
* @return bool
*/
public function language_exists($lang_code) {
if (!$this->integration) {
return false;
}
return $this->integration->language_exists($lang_code);
}
/**
* Get language code from name
*
* @param string $our_language
* @return string|null
*/
public function get_language_code($our_language) {
if (!$this->integration) {
return null;
}
return $this->integration->get_language_code($our_language);
}
/**
* Get pages translation status
*
* @return array
*/
public function get_pages_translation_status() {
if (!$this->integration) {
return [];
}
return $this->integration->get_pages_translation_status();
}
/**
* Get language switcher data
*
* @return array
*/
public function get_language_switcher_data() {
if (!$this->integration) {
return [];
}
return $this->integration->get_language_switcher_data();
}
/**
* Get the correct permalink for a post in its assigned language
* This ensures the multilingual plugin returns the URL with the correct language
*
* @param int $post_id Post ID
* @return string The permalink with correct language
*/
public function get_post_permalink($post_id) {
if (!$this->integration) {
return get_permalink($post_id);
}
return $this->integration->get_post_permalink($post_id);
}
/**
* Get the original post ID for a translation
* Returns the post ID of the source/original post in the default language
*
* @param int $post_id Post ID to check
* @return int|null Original post ID, or null if not a translation (or is the original)
*/
public function get_original_post_id($post_id) {
if (!$this->integration) {
return null;
}
// Get all translations for this post
$translations = $this->integration->get_post_translations($post_id);
if (empty($translations)) {
return null;
}
// Get the default language
$default_lang = $this->get_default_language();
// If the post's language is the default language, it's the original
$post_lang = $this->get_post_language($post_id);
if ($post_lang === $default_lang) {
return $post_id; // This IS the original
}
// Return the original (default language) post ID
if (isset($translations[$default_lang])) {
return intval($translations[$default_lang]);
}
// Fallback: find any post in the group that isn't this one
foreach ($translations as $lang => $tid) {
if (intval($tid) !== intval($post_id)) {
return intval($tid);
}
}
return null;
}
/**
* Sort the queried posts array on-the-fly to match the exact sorting sequence
* (menu_order or post_date) of their default-language original source posts.
* This bypasses limitations of third-party drag-and-drop post ordering plugins
* that update the database using custom SQL queries instead of wp_update_post/save_post.
*
* @param array $posts Array of WP_Post objects.
* @param WP_Query $query The query object.
* @return array
*/
public function sync_query_posts_order($posts, $query) {
if (!get_option('aitrfoel_preserve_post_order', true)) {
return $posts;
}
if (empty($posts) || !$this->integration || !$this->integration->is_active()) {
return $posts;
}
// Bypass backend admin post list tables, but run on frontend AJAX and REST queries
if (is_admin() && !wp_doing_ajax() && !defined('REST_REQUEST')) {
return $posts;
}
$default = $this->get_default_language();
$lang = $this->get_post_language($posts[0]->ID);
if (!$lang || $lang === $default) {
return $posts;
}
// Only sort for post types that use the language taxonomy or are translatable
$post_type = $query->get('post_type');
if (empty($post_type)) {
$post_type = $posts[0]->post_type;
}
$post_types = (array) $post_type;
$applies = false;
foreach ($post_types as $pt) {
if ($pt && (post_type_exists($pt) || $pt === 'post' || $pt === 'page')) {
$applies = true;
break;
}
}
if (!$applies) {
return $posts;
}
// Determine if the query is sorting by menu_order or date (safely parsing strings and arrays)
$orderby = $query->get('orderby');
$order = strtoupper((string) $query->get('order')) === 'ASC' ? 'ASC' : 'DESC';
$sort_by_menu = false;
$sort_by_date = false;
if (is_array($orderby)) {
foreach ($orderby as $key => $val) {
if (is_string($key)) {
if (strpos($key, 'menu_order') !== false) {
$sort_by_menu = true;
}
if ($key === 'date' || $key === 'post_date' || $key === 'ID') {
$sort_by_date = true;
}
}
}
} elseif (is_string($orderby)) {
$sort_by_menu = (strpos($orderby, 'menu_order') !== false);
$sort_by_date = ($orderby === '' || $orderby === 'date' || $orderby === 'post_date' || $orderby === 'ID');
} else {
$sort_by_date = true;
}
if (!$sort_by_menu && !$sort_by_date) {
return $posts;
}
// Collect original posts to sort by their attributes
$original_ids = [];
foreach ($posts as $post) {
$src_id = (int) get_post_meta($post->ID, '_aitrfoel_source_page', true);
if (!$src_id) {
$translations = $this->integration->get_post_translations($post->ID);
$src_id = isset($translations[$default]) ? (int) $translations[$default] : $post->ID;
}
$original_ids[$post->ID] = $src_id;
}
if (empty($original_ids)) {
return $posts;
}
// Fetch original post attributes in a single database query for performance
global $wpdb;
$ids_placeholder = implode(',', array_map('intval', array_unique(array_values($original_ids))));
$rows = $wpdb->get_results("SELECT ID, menu_order, post_date FROM {$wpdb->posts} WHERE ID IN ($ids_placeholder)", OBJECT_K);
// Sort the posts array
usort($posts, function($a, $b) use ($original_ids, $rows, $sort_by_menu, $order) {
$src_a_id = $original_ids[$a->ID] ?? $a->ID;
$src_b_id = $original_ids[$b->ID] ?? $b->ID;
$attr_a = isset($rows[$src_a_id]) ? $rows[$src_a_id] : null;
$attr_b = isset($rows[$src_b_id]) ? $rows[$src_b_id] : null;
if ($sort_by_menu) {
$val_a = $attr_a ? (int) $attr_a->menu_order : $a->menu_order;
$val_b = $attr_b ? (int) $attr_b->menu_order : $b->menu_order;
if ($val_a === $val_b) {
$date_a = $attr_a ? strtotime($attr_a->post_date) : strtotime($a->post_date);
$date_b = $attr_b ? strtotime($attr_b->post_date) : strtotime($b->post_date);
if ($date_a === $date_b) {
return 0;
}
return ($date_a > $date_b) ? -1 : 1; // Default to DESC (newest first)
}
} else {
$val_a = $attr_a ? strtotime($attr_a->post_date) : strtotime($a->post_date);
$val_b = $attr_b ? strtotime($attr_b->post_date) : strtotime($b->post_date);
}
if ($val_a === $val_b) {
return 0;
}
if ($order === 'ASC') {
return ($val_a < $val_b) ? -1 : 1;
} else {
return ($val_a > $val_b) ? -1 : 1;
}
});
return $posts;
}
/**
* Filter JetEngine Listing Grid items to ensure translated post order matches default-language order.
*
* @param array $items Array of objects/posts.
* @param array $settings Rendering settings.
* @param object $render_instance The rendering instance.
* @return array
*/
public function sync_jet_listing_items_order($items, $settings, $render_instance) {
if (!get_option('aitrfoel_preserve_post_order', true)) {
return $items;
}
if (empty($items) || !is_array($items) || !$this->integration || !$this->integration->is_active()) {
return $items;
}
// Only sort WP_Post objects
if (!isset($items[0]) || !($items[0] instanceof WP_Post)) {
return $items;
}
$default = $this->get_default_language();
$lang = $this->get_post_language($items[0]->ID);
if (!$lang || $lang === $default) {
return $items;
}
// Collect original posts to sort by their attributes
$original_ids = [];
foreach ($items as $post) {
$src_id = (int) get_post_meta($post->ID, '_aitrfoel_source_page', true);
if (!$src_id) {
$translations = $this->get_post_translations($post->ID);
$src_id = isset($translations[$default]) ? (int) $translations[$default] : $post->ID;
}
$original_ids[$post->ID] = $src_id;
}
if (empty($original_ids)) {
return $items;
}
// Fetch original post attributes in a single database query for performance
global $wpdb;
$ids_placeholder = implode(',', array_map('intval', array_unique(array_values($original_ids))));
$rows = $wpdb->get_results("SELECT ID, menu_order, post_date FROM {$wpdb->posts} WHERE ID IN ($ids_placeholder)", OBJECT_K);
// JetEngine listing settings for sorting
$orderby = $settings['orderby'] ?? '';
$order = strtoupper((string) ($settings['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
$sort_by_menu = (strpos($orderby, 'menu_order') !== false);
$sort_by_date = ($orderby === '' || $orderby === 'date' || $orderby === 'post_date' || $orderby === 'ID');
if (!$sort_by_menu && !$sort_by_date) {
$sort_by_date = true;
}
// Sort the items array
usort($items, function($a, $b) use ($original_ids, $rows, $sort_by_menu, $order) {
$src_a_id = $original_ids[$a->ID] ?? $a->ID;
$src_b_id = $original_ids[$b->ID] ?? $b->ID;
$attr_a = isset($rows[$src_a_id]) ? $rows[$src_a_id] : null;
$attr_b = isset($rows[$src_b_id]) ? $rows[$src_b_id] : null;
if ($sort_by_menu) {
$val_a = $attr_a ? (int) $attr_a->menu_order : $a->menu_order;
$val_b = $attr_b ? (int) $attr_b->menu_order : $b->menu_order;
if ($val_a === $val_b) {
$date_a = $attr_a ? strtotime($attr_a->post_date) : strtotime($a->post_date);
$date_b = $attr_b ? strtotime($attr_b->post_date) : strtotime($b->post_date);
if ($date_a === $date_b) {
return 0;
}
return ($date_a > $date_b) ? -1 : 1; // Default to DESC (newest first)
}
} else {
$val_a = $attr_a ? strtotime($attr_a->post_date) : strtotime($a->post_date);
$val_b = $attr_b ? strtotime($attr_b->post_date) : strtotime($b->post_date);
}
if ($val_a === $val_b) {
return 0;
}
if ($order === 'ASC') {
return ($val_a < $val_b) ? -1 : 1;
} else {
return ($val_a > $val_b) ? -1 : 1;
}
});
return $items;
}
/**
* Swap Elementor Header Footer Builder template ID to current language translation.
*
* @param int $template_id
* @return int
*/
public function swap_hfe_template_id($template_id) {
if (!$template_id) {
return $template_id;
}
return $this->get_translated_post_id($template_id);
}
/**
* Get translated post ID for the current language.
*
* @param int $post_id
* @return int
*/
public function get_translated_post_id($post_id) {
if (!$post_id || !$this->integration || !$this->integration->is_active()) {
return $post_id;
}
$current_lang = $this->integration->get_current_language();
if (!$current_lang) {
return $post_id;
}
// Check if the post itself is already in the current language
$post_lang = $this->get_post_language($post_id);
if ($post_lang === $current_lang) {
return $post_id;
}
$translations = $this->get_post_translations($post_id);
if (!empty($translations) && isset($translations[$current_lang])) {
return (int) $translations[$current_lang];
}
return $post_id;
}
}
/**
* Helper function to get the multilingual manager instance
*
* @return AITRFOEL_Multilingual_Manager
*/
function aitrfoel_multilingual() {
return AITRFOEL_Multilingual_Manager::get_instance();
}