'English', 'fr' => 'French', 'es' => 'Spanish', 'de' => 'German', 'it' => 'Italian', 'pt' => 'Portuguese', 'pt-br' => 'Portuguese (Brazil)', 'nl' => 'Dutch', 'ru' => 'Russian', 'zh-cn' => 'Chinese (Simplified)', 'zh-tw' => 'Chinese (Traditional)', 'ja' => 'Japanese', 'ko' => 'Korean', 'ar' => 'Arabic', 'hi' => 'Hindi', 'bn' => 'Bengali', 'tr' => 'Turkish', 'ur' => 'Urdu', 'pl' => 'Polish', 'sv' => 'Swedish', 'no' => 'Norwegian', 'da' => 'Danish', 'fi' => 'Finnish', 'cs' => 'Czech', 'el' => 'Greek', 'he' => 'Hebrew', 'ro' => 'Romanian', 'hu' => 'Hungarian', 'th' => 'Thai', 'ta' => 'Tamil', 'mr' => 'Marathi', 'pa' => 'Punjabi', 'id' => 'Indonesian', 'vi' => 'Vietnamese' ); public static function get_supported_languages() { return self::SUPPORTED_LANGUAGES; } public function __construct() { // Add row actions for posts and pages add_filter('post_row_actions', array($this, 'add_translate_action'), 10, 2); add_filter('page_row_actions', array($this, 'add_translate_action'), 10, 2); // Enqueue scripts and styles add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts')); add_action('admin_enqueue_scripts', array($this, 'enqueue_site_editor_scripts')); // AJAX handlers add_action('wp_ajax_aibui_translate_post', array($this, 'handle_translation')); add_action('wp_ajax_aibui_translate_template_part', array($this, 'handle_template_part_translation')); } /** * Add "Translate with AI" action to row actions */ public function add_translate_action($actions, $post) { // Only show for published posts/pages if ($post->post_status !== 'publish' && $post->post_status !== 'draft') { return $actions; } // Check user capabilities if (!current_user_can('edit_post', $post->ID)) { return $actions; } $nonce = wp_create_nonce('aibui_translate_' . $post->ID); $actions['translate_ai'] = sprintf( '✨ Translate with AI', esc_attr($post->ID), esc_attr($nonce) ); return $actions; } /** * Enqueue scripts and styles for translation feature */ public function enqueue_scripts($hook) { if ($hook !== 'edit.php') { return; } $screen = get_current_screen(); if (!$screen || !in_array($screen->post_type, array('post', 'page', 'wp_template_part'), true)) { return; } // Enqueue CSS wp_enqueue_style( 'aibui-translation-style', plugin_dir_url(dirname(__FILE__)) . 'assets/css/translation.css', array(), AIBUI_VERSION ); // Enqueue JS wp_enqueue_script( 'aibui-translation-script', plugin_dir_url(dirname(__FILE__)) . 'assets/js/translation.js', array('jquery'), AIBUI_VERSION, true ); // Localize script wp_localize_script('aibui-translation-script', 'aibuiTranslation', array( 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aibui_nonce'), 'languages' => self::SUPPORTED_LANGUAGES, )); } public function enqueue_site_editor_scripts($hook) { if ($hook !== 'site-editor.php') { return; } wp_enqueue_script( 'aibui-pattern-translation', plugin_dir_url(dirname(__FILE__)) . 'assets/js/pattern-translation.js', array('wp-data', 'wp-components', 'wp-element', 'wp-notices', 'wp-edit-site'), AIBUI_VERSION, true ); wp_localize_script('aibui-pattern-translation', 'aibuiTranslation', array( 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aibui_nonce'), 'languages' => self::SUPPORTED_LANGUAGES, 'messages' => array( 'success' => __('Translation created. Opening draft...', 'ai-builder'), 'error' => __('Translation failed. Please try again.', 'ai-builder'), ), )); } /** * Extract text from Gutenberg blocks recursively * Returns a mapped array: key => text */ private function extract_texts_from_blocks($blocks, $prefix = 'block') { $text_map = array(); $index = 0; foreach ($blocks as $block) { if (empty($block['blockName'])) { // Skip empty blocks continue; } $block_key = $prefix . '_' . $index; $block_name = $block['blockName']; // Extract text based on block type switch ($block_name) { case 'core/paragraph': if (!empty($block['innerHTML'])) { $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_p')); } break; case 'core/heading': if (!empty($block['innerHTML'])) { $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_h')); } break; case 'core/list': if (!empty($block['innerHTML']) && empty($block['innerBlocks'])) { $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_list')); } break; case 'core/list-item': if (!empty($block['innerHTML'])) { $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_li')); } break; case 'core/button': if (!empty($block['innerHTML'])) { $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_btn')); } break; case 'core/image': // Extract alt text and caption if (!empty($block['attrs']['alt'])) { $text_map[$block_key . '_alt'] = $block['attrs']['alt']; } if (!empty($block['innerHTML'])) { $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_caption')); } break; case 'core/quote': if (!empty($block['innerHTML'])) { $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_quote')); } break; case 'core/table': if (!empty($block['innerHTML'])) { $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_table')); } break; case 'core/navigation': // If navigation has a ref, we need to extract from the referenced wp_navigation post if (!empty($block['attrs']['ref'])) { $nav_post = get_post($block['attrs']['ref']); if ($nav_post && $nav_post->post_type === 'wp_navigation') { $nav_blocks = parse_blocks($nav_post->post_content); $nav_map = $this->extract_texts_from_blocks($nav_blocks, $block_key . '_nav'); $text_map = array_merge($text_map, $nav_map); } } // Also process innerBlocks if present (inline navigation) break; case 'core/navigation-link': case 'core/navigation-submenu': // Extract the label from attrs if (!empty($block['attrs']['label'])) { $text_map[$block_key . '_navlabel'] = $block['attrs']['label']; } break; } // Recursively process inner blocks if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) { $inner_map = $this->extract_texts_from_blocks($block['innerBlocks'], $block_key); $text_map = array_merge($text_map, $inner_map); } $index++; } return $text_map; } /** * Extract plain text from HTML, removing tags */ private function extract_text_from_html($html) { // Remove script and style tags $html = preg_replace('#(.*?)#is', '', $html); $html = preg_replace('#(.*?)#is', '', $html); // Decode HTML entities $html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8'); // Remove HTML tags but preserve line breaks for lists $text = strip_tags($html); // Clean up whitespace $text = preg_replace('/\s+/', ' ', $text); $text = trim($text); return $text; } /** * Extract text nodes preserving HTML structure */ private function extract_text_nodes($html, $key_prefix) { $result = array(); if (trim((string)$html) === '') { return $result; } $dom = new DOMDocument('1.0', 'UTF-8'); libxml_use_internal_errors(true); $dom->loadHTML('
' . $html . '
'); libxml_clear_errors(); $container = $dom->getElementsByTagName('div')->item(0); if (!$container) { return $result; } $index = 0; $this->traverse_text_nodes($container, function ($node) use (&$result, $key_prefix, &$index) { $text = $node->nodeValue; if (trim($text) === '') { return; } $result[$key_prefix . '_text' . $index] = $text; $index++; }); return $result; } /** * Replace text nodes with translated values */ private function replace_text_nodes($html, $key_prefix, $translated_map) { if (trim((string)$html) === '') { return $html; } $dom = new DOMDocument('1.0', 'UTF-8'); libxml_use_internal_errors(true); $dom->loadHTML('
' . $html . '
'); libxml_clear_errors(); $container = $dom->getElementsByTagName('div')->item(0); if (!$container) { return $html; } $index = 0; $this->traverse_text_nodes($container, function ($node) use ($key_prefix, $translated_map, &$index) { $text = $node->nodeValue; if (trim($text) === '') { return; } $key = $key_prefix . '_text' . $index; if (isset($translated_map[$key])) { $node->nodeValue = $translated_map[$key]; } $index++; }); $innerHTML = ''; foreach ($container->childNodes as $child) { $innerHTML .= $container->ownerDocument->saveHTML($child); } return $innerHTML; } /** * Traverse DOM text nodes */ private function traverse_text_nodes($node, $callback) { if ($node->nodeType === XML_TEXT_NODE) { $callback($node); } if ($node->hasChildNodes()) { foreach ($node->childNodes as $child) { $this->traverse_text_nodes($child, $callback); } } } /** * Reconstruct blocks with translated content */ private function reconstruct_blocks($original_blocks, $translated_map, $prefix = 'block') { $reconstructed = array(); $index = 0; foreach ($original_blocks as $block) { if (empty($block['blockName'])) { // Preserve empty blocks (spacers, etc.) $reconstructed[] = $block; continue; } $block_key = $prefix . '_' . $index; $block_name = $block['blockName']; $new_block = $block; // Replace text based on block type switch ($block_name) { case 'core/paragraph': if (!empty($block['innerHTML'])) { $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_p', $translated_map); $new_block['innerHTML'] = $updated_html; $new_block['innerContent'] = array($updated_html); } break; case 'core/heading': if (!empty($block['innerHTML'])) { $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_h', $translated_map); $new_block['innerHTML'] = $updated_html; $new_block['innerContent'] = array($updated_html); } break; case 'core/list': if (!empty($block['innerHTML']) && empty($block['innerBlocks'])) { $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_list', $translated_map); $new_block['innerHTML'] = $updated_html; $new_block['innerContent'] = array($updated_html); } break; case 'core/list-item': if (!empty($block['innerHTML'])) { $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_li', $translated_map); $new_block['innerHTML'] = $updated_html; $new_block['innerContent'] = array($updated_html); } break; case 'core/button': if (!empty($block['innerHTML'])) { $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_btn', $translated_map); $new_block['innerHTML'] = $updated_html; $new_block['innerContent'] = array($updated_html); } break; case 'core/image': if (isset($translated_map[$block_key . '_alt'])) { $new_block['attrs']['alt'] = sanitize_text_field($translated_map[$block_key . '_alt']); } if (!empty($block['innerHTML'])) { $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_caption', $translated_map); $new_block['innerHTML'] = $updated_html; if (isset($new_block['innerContent'])) { $new_block['innerContent'] = array($updated_html); } } break; case 'core/quote': if (!empty($block['innerHTML'])) { $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_quote', $translated_map); $new_block['innerHTML'] = $updated_html; $new_block['innerContent'] = array($updated_html); } break; case 'core/table': if (!empty($block['innerHTML'])) { $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_table', $translated_map); $new_block['innerHTML'] = $updated_html; $new_block['innerContent'] = array($updated_html); } break; case 'core/navigation': // If navigation has a ref, duplicate the wp_navigation post with translated content if (!empty($block['attrs']['ref'])) { $new_nav_id = $this->duplicate_and_translate_navigation($block['attrs']['ref'], $translated_map, $block_key . '_nav'); if ($new_nav_id) { $new_block['attrs']['ref'] = $new_nav_id; } } break; case 'core/navigation-link': case 'core/navigation-submenu': // Replace the label in attrs if (isset($translated_map[$block_key . '_navlabel'])) { $new_block['attrs']['label'] = $translated_map[$block_key . '_navlabel']; } break; } // Recursively process inner blocks if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) { $new_block['innerBlocks'] = $this->reconstruct_blocks($block['innerBlocks'], $translated_map, $block_key); } $reconstructed[] = $new_block; $index++; } return $reconstructed; } /** * Duplicate a wp_navigation post and translate its content */ private function duplicate_and_translate_navigation($nav_id, $translated_map, $prefix) { $nav_post = get_post($nav_id); if (!$nav_post || $nav_post->post_type !== 'wp_navigation') { return null; } // Parse the navigation blocks $nav_blocks = parse_blocks($nav_post->post_content); // Reconstruct with translated labels $translated_nav_blocks = $this->reconstruct_blocks($nav_blocks, $translated_map, $prefix); // Serialize back to block content $translated_nav_content = serialize_blocks($translated_nav_blocks); // Get target language from the current translation context $target_lang = isset($_POST['target_lang']) ? sanitize_text_field($_POST['target_lang']) : 'translated'; // Create a new wp_navigation post $new_nav_data = array( 'post_title' => $nav_post->post_title . ' (' . strtoupper($target_lang) . ')', 'post_content' => $translated_nav_content, 'post_status' => 'publish', 'post_type' => 'wp_navigation', 'post_author' => get_current_user_id(), ); $new_nav_id = wp_insert_post($new_nav_data); if (is_wp_error($new_nav_id)) { return null; } // Copy language meta update_post_meta($new_nav_id, '_ai_lang', $target_lang); update_post_meta($new_nav_id, '_ai_translation_source', $nav_id); // Link to same translation group $translation_group = get_post_meta($nav_id, '_ai_translation_group', true); if (empty($translation_group)) { $translation_group = 'nav_' . time() . '_' . wp_generate_password(8, false); update_post_meta($nav_id, '_ai_translation_group', $translation_group); } update_post_meta($new_nav_id, '_ai_translation_group', $translation_group); return $new_nav_id; } /** * Get meta description with fallback logic */ private function get_meta_description($post_id) { // Check _ai_builder_seo_desc first (as specified by user) $meta_desc = get_post_meta($post_id, '_ai_builder_seo_desc', true); if (!empty($meta_desc)) { return $meta_desc; } // Check plugin's own meta key $meta_desc = get_post_meta($post_id, 'aibui_meta_description', true); if (!empty($meta_desc)) { return $meta_desc; } // Check Yoast SEO meta $meta_desc = get_post_meta($post_id, '_yoast_wpseo_metadesc', true); if (!empty($meta_desc)) { return $meta_desc; } // Fallback to excerpt $post = get_post($post_id); return $post ? $post->post_excerpt : ''; } /** * AJAX handler for translation */ public function handle_translation() { // Verify nonce if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'aibui_nonce')) { wp_send_json_error(array('message' => 'Security check failed')); } // Get and validate post ID if (!isset($_POST['post_id'])) { wp_send_json_error(array('message' => 'Post ID is required')); } $post_id = intval($_POST['post_id']); if (!$post_id) { wp_send_json_error(array('message' => 'Invalid post ID')); } // Check user capabilities if (!current_user_can('edit_post', $post_id)) { wp_send_json_error(array('message' => 'Insufficient permissions')); } // Get target language if (!isset($_POST['target_lang']) || !array_key_exists($_POST['target_lang'], self::SUPPORTED_LANGUAGES)) { wp_send_json_error(array('message' => 'Invalid target language')); } $target_lang = sanitize_text_field($_POST['target_lang']); $target_lang_name = strtolower(self::SUPPORTED_LANGUAGES[$target_lang]); // Get original post $original_post = get_post($post_id); if (!$original_post) { wp_send_json_error(array('message' => 'Post not found')); } // Extract data $title = $original_post->post_title; $meta_desc = $this->get_meta_description($post_id); $content = $original_post->post_content; // Parse blocks $blocks = parse_blocks($content); $content_map = $this->extract_texts_from_blocks($blocks); // Prepare data for API $api_data = array( 'title' => $title, 'meta_desc' => $meta_desc, 'content_map' => $content_map, 'target_lang' => $target_lang_name ); $this->log_translation_debug($post_id, 'request_payload', $api_data); // Get JWT token $jwt_token = get_option('aibui_jwt_token', ''); if (empty($jwt_token)) { wp_send_json_error(array('message' => 'Authentication required. Please sign in.')); } // Call translation API $api_url = 'https://api.wordpress-ai-builder.com/api' . '/ai-transform-page/translate-post'; $response = wp_remote_post($api_url, array( 'timeout' => 180, // 5 minutes - AI translation can take longer for large pages 'headers' => array( 'Authorization' => 'Bearer ' . $jwt_token, 'Content-Type' => 'application/json', ), 'body' => json_encode($api_data), )); if (is_wp_error($response)) { wp_send_json_error(array('message' => 'API request failed: ' . $response->get_error_message())); } $response_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $this->log_translation_debug($post_id, 'api_response', array( 'status' => $response_code, 'body' => $body )); if ($response_code === 402) { $error_message = 'You do not have enough credits to translate this page. Each translation costs 40 credits.'; // Try to merge API error message if available $decoded = json_decode($body, true); if (is_array($decoded) && !empty($decoded['message'])) { $error_message .= ' ' . $decoded['message']; } wp_send_json_error(array( 'message' => $error_message, 'code' => 'not_enough_credits', )); } if ($response_code !== 200) { wp_send_json_error(array('message' => 'Translation failed. Status: ' . $response_code)); } $translated_data = json_decode($body, true); $this->log_translation_debug($post_id, 'translated_payload', $translated_data); if (!$translated_data || !isset($translated_data['title']) || !isset($translated_data['content_map'])) { wp_send_json_error(array('message' => 'Invalid response from translation API')); } // Reconstruct blocks with translated content $translated_blocks = $this->reconstruct_blocks($blocks, $translated_data['content_map']); $translated_content = serialize_blocks($translated_blocks); $this->log_translation_debug($post_id, 'final_content', array( 'content_preview' => mb_substr($translated_content, 0, 1000) )); // For template parts, generate a unique slug with language suffix $new_post_name = ''; if ($original_post->post_type === 'wp_template_part') { $new_post_name = $original_post->post_name . '-' . $target_lang; // Template parts need to be published for Site Editor to find them $new_post_status = 'publish'; } else { $new_post_status = 'draft'; } // Create new post $new_post_data = array( 'post_title' => $translated_data['title'], 'post_content' => $translated_content, 'post_status' => $new_post_status, 'post_type' => $original_post->post_type, 'post_author' => get_current_user_id(), ); // Set explicit slug for template parts if (!empty($new_post_name)) { $new_post_data['post_name'] = $new_post_name; } $new_post_id = wp_insert_post($new_post_data); if (is_wp_error($new_post_id)) { wp_send_json_error(array('message' => 'Failed to create translated post: ' . $new_post_id->get_error_message())); } // Save meta description across all supported meta keys if (!empty($translated_data['meta_desc'])) { update_post_meta($new_post_id, '_ai_builder_seo_desc', $translated_data['meta_desc']); update_post_meta($new_post_id, '_yoast_wpseo_metadesc', $translated_data['meta_desc']); update_post_meta($new_post_id, 'aibui_meta_description', $translated_data['meta_desc']); update_post_meta($new_post_id, '_ai_translation_meta_desc', $translated_data['meta_desc']); } // Copy custom CSS meta if present $css_meta_keys = array( 'ai_builder_page_css_content', 'ai_builder_block_css_content', 'ai_builder_css_content' ); foreach ($css_meta_keys as $css_key) { $css_value = get_post_meta($post_id, $css_key, true); if (!empty($css_value)) { update_post_meta($new_post_id, $css_key, $css_value); } } // Copy custom JS meta if present (so translated pages keep JS behavior) $js_meta_keys = array( 'ai_builder_page_js_content', 'ai_builder_block_js_content', 'ai_builder_js_content' ); foreach ($js_meta_keys as $js_key) { $js_value = get_post_meta($post_id, $js_key, true); if (!empty($js_value)) { update_post_meta($new_post_id, $js_key, $js_value); } } // Save language meta update_post_meta($new_post_id, '_ai_lang', $target_lang); update_post_meta($new_post_id, '_ai_translation_source', $post_id); if (!get_post_meta($post_id, '_ai_translation_source', true)) { update_post_meta($post_id, '_ai_translation_source', $post_id); } // Copy template part taxonomy/meta if needed if ($original_post->post_type === 'wp_template_part') { // Assign to current theme $theme_slug = wp_get_theme()->get_stylesheet(); wp_set_object_terms($new_post_id, $theme_slug, 'wp_theme', false); // Copy template part area taxonomy $area_terms = wp_get_object_terms($post_id, 'wp_template_part_area', array('fields' => 'slugs')); if (!is_wp_error($area_terms) && !empty($area_terms)) { wp_set_object_terms($new_post_id, $area_terms, 'wp_template_part_area', false); } update_post_meta($new_post_id, '_wp_template_part_area', get_post_meta($post_id, '_wp_template_part_area', true)); update_post_meta($new_post_id, '_ai_template_part_slug', $original_post->post_name); } // Handle translation group $translation_group = get_post_meta($post_id, '_ai_translation_group', true); if (empty($translation_group)) { $translation_group = 'trans_' . time() . '_' . wp_generate_password(8, false); update_post_meta($post_id, '_ai_translation_group', $translation_group); } update_post_meta($new_post_id, '_ai_translation_group', $translation_group); // Copy other relevant meta $original_lang = get_post_meta($post_id, '_ai_lang', true); if (empty($original_lang)) { update_post_meta($post_id, '_ai_lang', 'en'); // Default to English if not set } // Preserve AI-created flag to keep hide-title CSS behavior if (get_post_meta($post_id, '_aibui_created_by_ai', true) === '1') { update_post_meta($new_post_id, '_aibui_created_by_ai', '1'); } // If the original page is the front page, mark the translation as homepage for its language $front_page_id = get_option('page_on_front'); if ($front_page_id && (int) $front_page_id === (int) $post_id) { update_post_meta($new_post_id, '_ai_is_homepage', '1'); } // Also copy the homepage flag if it exists on the original if (get_post_meta($post_id, '_ai_is_homepage', true) === '1') { update_post_meta($new_post_id, '_ai_is_homepage', '1'); } // Copy featured image if exists $thumbnail_id = get_post_thumbnail_id($post_id); if ($thumbnail_id) { set_post_thumbnail($new_post_id, $thumbnail_id); } // Return success with new post URL // For template parts, use Site Editor URL instead of post.php if ($original_post->post_type === 'wp_template_part') { $new_post = get_post($new_post_id); $theme = wp_get_theme()->get_stylesheet(); // Site Editor URL format: /wp-admin/site-editor.php?postType=wp_template_part&postId=theme//slug $edit_url = admin_url('site-editor.php?postType=wp_template_part&postId=' . urlencode($theme . '//' . $new_post->post_name) . '&canvas=edit'); } else { $edit_url = admin_url('post.php?post=' . $new_post_id . '&action=edit'); } wp_send_json_success(array( 'message' => 'Translation created successfully!', 'post_id' => $new_post_id, 'edit_url' => $edit_url, )); } private function log_translation_debug($post_id, $stage, $data) { return; // $log_file = plugin_dir_path(__FILE__) . '../debug-unescape.log'; // if (is_array($data) || is_object($data)) { // $data = wp_json_encode($data); // } // $line = sprintf("[%s][AI Builder Translation][Post %d][%s] %s\n", date('Y-m-d H:i:s'), $post_id, $stage, $data); // file_put_contents($log_file, $line, FILE_APPEND); } /** * AJAX handler for template part translation * Template parts use slugs like "theme//slug" instead of numeric IDs */ public function handle_template_part_translation() { $this->log_template_part_debug('start', array( 'POST' => $_POST, )); // Verify nonce if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'aibui_nonce')) { $this->log_template_part_debug('error', 'Nonce verification failed'); wp_send_json_error(array('message' => 'Security check failed')); } // Get template part ID (slug format: "theme//slug") if (!isset($_POST['template_part_id']) || empty($_POST['template_part_id'])) { $this->log_template_part_debug('error', 'Template part ID is required'); wp_send_json_error(array('message' => 'Template part ID is required')); } $template_part_id = sanitize_text_field($_POST['template_part_id']); $this->log_template_part_debug('template_part_id', $template_part_id); // Get target language if (!isset($_POST['target_lang']) || !array_key_exists($_POST['target_lang'], self::SUPPORTED_LANGUAGES)) { $this->log_template_part_debug('error', 'Invalid target language: ' . ($_POST['target_lang'] ?? 'not set')); wp_send_json_error(array('message' => 'Invalid target language')); } $target_lang = sanitize_text_field($_POST['target_lang']); $target_lang_name = strtolower(self::SUPPORTED_LANGUAGES[$target_lang]); // Parse the template part ID to get theme and slug // Format: "theme//slug" e.g., "twentytwentyfive//header" $parts = explode('//', $template_part_id); if (count($parts) !== 2) { $this->log_template_part_debug('error', 'Invalid template part ID format: ' . $template_part_id); wp_send_json_error(array('message' => 'Invalid template part ID format')); } $theme = $parts[0]; $slug = $parts[1]; $this->log_template_part_debug('parsed', array('theme' => $theme, 'slug' => $slug)); // Find the template part post by slug $post = $this->get_template_part_post($theme, $slug); if (!$post) { $this->log_template_part_debug('error', 'Template part not found for theme: ' . $theme . ', slug: ' . $slug); wp_send_json_error(array('message' => 'Template part not found. Make sure it has been customized (not a theme default).')); } $post_id = $post->ID; $this->log_template_part_debug('found_post', array('post_id' => $post_id, 'post_title' => $post->post_title)); // Check user capabilities if (!current_user_can('edit_post', $post_id)) { $this->log_template_part_debug('error', 'Insufficient permissions for post: ' . $post_id); wp_send_json_error(array('message' => 'Insufficient permissions')); } // Now translate using the numeric post ID $_POST['post_id'] = $post_id; $_POST['target_lang'] = $target_lang; $this->log_template_part_debug('calling_handle_translation', array( 'post_id' => $post_id, 'target_lang' => $target_lang, )); // Call the main translation handler $this->handle_translation(); } /** * Get template part post by theme and slug */ private function get_template_part_post($theme, $slug) { // Query for the template part $query = new WP_Query(array( 'post_type' => 'wp_template_part', 'post_status' => array('publish', 'draft', 'auto-draft'), 'name' => $slug, 'posts_per_page' => 1, 'no_found_rows' => true, 'tax_query' => array( array( 'taxonomy' => 'wp_theme', 'field' => 'slug', 'terms' => $theme, ), ), )); $this->log_template_part_debug('query_result', array( 'found_posts' => $query->found_posts, 'post_count' => $query->post_count, )); if ($query->have_posts()) { return $query->posts[0]; } // Try without theme filter (for child themes, etc.) $query2 = new WP_Query(array( 'post_type' => 'wp_template_part', 'post_status' => array('publish', 'draft', 'auto-draft'), 'name' => $slug, 'posts_per_page' => 1, 'no_found_rows' => true, )); $this->log_template_part_debug('query2_result', array( 'found_posts' => $query2->found_posts, 'post_count' => $query2->post_count, )); if ($query2->have_posts()) { return $query2->posts[0]; } return null; } /** * Debug logging for template part translation */ private function log_template_part_debug($stage, $data) { $log_file = plugin_dir_path(__FILE__) . '../debug-template-part.log'; if (is_array($data) || is_object($data)) { $data = wp_json_encode($data); } $line = sprintf("[%s][Template Part Translation][%s] %s\n", date('Y-m-d H:i:s'), $stage, $data); file_put_contents($log_file, $line, FILE_APPEND); } }