'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);
}
}