PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.1.11
AI Builder – Generate pages, blocks, images & translate with AI v2.1.11
2.7.9 2.7.8 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.3.10 2.3.11 All 121 releases
ai-builder / includes / class-translation-handler.php

class-translation-handler.php in AI Builder – Generate pages, blocks, images & translate with AI 2.1.11, at includes/class-translation-handler.php

976 lines 36.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Translation Handler Class
4 * Handles AI-powered translation of WordPress posts and pages
5 */
6
7 if (!defined('ABSPATH'))
8 exit;
9
10 class AIBUI_Translation_Handler
11 {
12 const SUPPORTED_LANGUAGES = array(
13 // Core popular languages
14 'en' => 'English',
15 'fr' => 'French',
16 'es' => 'Spanish',
17 'de' => 'German',
18 'it' => 'Italian',
19 'pt' => 'Portuguese',
20 'pt-br' => 'Portuguese (Brazil)',
21 'nl' => 'Dutch',
22 'ru' => 'Russian',
23 'zh-cn' => 'Chinese (Simplified)',
24 'zh-tw' => 'Chinese (Traditional)',
25 'ja' => 'Japanese',
26 'ko' => 'Korean',
27 'ar' => 'Arabic',
28 'hi' => 'Hindi',
29 'bn' => 'Bengali',
30 'tr' => 'Turkish',
31 'ur' => 'Urdu',
32 'pl' => 'Polish',
33 'sv' => 'Swedish',
34 'no' => 'Norwegian',
35 'da' => 'Danish',
36 'fi' => 'Finnish',
37 'cs' => 'Czech',
38 'el' => 'Greek',
39 'he' => 'Hebrew',
40 'ro' => 'Romanian',
41 'hu' => 'Hungarian',
42 'th' => 'Thai',
43 'ta' => 'Tamil',
44 'mr' => 'Marathi',
45 'pa' => 'Punjabi',
46 'id' => 'Indonesian',
47 'vi' => 'Vietnamese'
48 );
49
50 public static function get_supported_languages()
51 {
52 return self::SUPPORTED_LANGUAGES;
53 }
54
55 public function __construct()
56 {
57 // Add row actions for posts and pages
58 add_filter('post_row_actions', array($this, 'add_translate_action'), 10, 2);
59 add_filter('page_row_actions', array($this, 'add_translate_action'), 10, 2);
60
61 // Enqueue scripts and styles
62 add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
63 add_action('admin_enqueue_scripts', array($this, 'enqueue_site_editor_scripts'));
64
65 // AJAX handlers
66 add_action('wp_ajax_aibui_translate_post', array($this, 'handle_translation'));
67 add_action('wp_ajax_aibui_translate_template_part', array($this, 'handle_template_part_translation'));
68 }
69
70 /**
71 * Add "Translate with AI" action to row actions
72 */
73 public function add_translate_action($actions, $post)
74 {
75 // Only show for published posts/pages
76 if ($post->post_status !== 'publish' && $post->post_status !== 'draft') {
77 return $actions;
78 }
79
80 // Check user capabilities
81 if (!current_user_can('edit_post', $post->ID)) {
82 return $actions;
83 }
84
85 $nonce = wp_create_nonce('aibui_translate_' . $post->ID);
86 $actions['translate_ai'] = sprintf(
87 '<a href="#" class="aibui-translate-link" data-post-id="%d" data-nonce="%s">✨ Translate with AI</a>',
88 esc_attr($post->ID),
89 esc_attr($nonce)
90 );
91
92 return $actions;
93 }
94
95 /**
96 * Enqueue scripts and styles for translation feature
97 */
98 public function enqueue_scripts($hook)
99 {
100 if ($hook !== 'edit.php') {
101 return;
102 }
103
104 $screen = get_current_screen();
105 if (!$screen || !in_array($screen->post_type, array('post', 'page', 'wp_template_part'), true)) {
106 return;
107 }
108
109 // Enqueue CSS
110 wp_enqueue_style(
111 'aibui-translation-style',
112 plugin_dir_url(dirname(__FILE__)) . 'assets/css/translation.css',
113 array(),
114 AIBUI_VERSION
115 );
116
117 // Enqueue JS
118 wp_enqueue_script(
119 'aibui-translation-script',
120 plugin_dir_url(dirname(__FILE__)) . 'assets/js/translation.js',
121 array('jquery'),
122 AIBUI_VERSION,
123 true
124 );
125
126 // Localize script
127 wp_localize_script('aibui-translation-script', 'aibuiTranslation', array(
128 'ajaxurl' => admin_url('admin-ajax.php'),
129 'nonce' => wp_create_nonce('aibui_nonce'),
130 'languages' => self::SUPPORTED_LANGUAGES,
131 ));
132 }
133
134 public function enqueue_site_editor_scripts($hook)
135 {
136 if ($hook !== 'site-editor.php') {
137 return;
138 }
139
140 wp_enqueue_script(
141 'aibui-pattern-translation',
142 plugin_dir_url(dirname(__FILE__)) . 'assets/js/pattern-translation.js',
143 array('wp-data', 'wp-components', 'wp-element', 'wp-notices', 'wp-edit-site'),
144 AIBUI_VERSION,
145 true
146 );
147
148 wp_localize_script('aibui-pattern-translation', 'aibuiTranslation', array(
149 'ajaxurl' => admin_url('admin-ajax.php'),
150 'nonce' => wp_create_nonce('aibui_nonce'),
151 'languages' => self::SUPPORTED_LANGUAGES,
152 'messages' => array(
153 'success' => __('Translation created. Opening draft...', 'ai-builder'),
154 'error' => __('Translation failed. Please try again.', 'ai-builder'),
155 ),
156 ));
157 }
158
159 /**
160 * Extract text from Gutenberg blocks recursively
161 * Returns a mapped array: key => text
162 */
163 private function extract_texts_from_blocks($blocks, $prefix = 'block')
164 {
165 $text_map = array();
166 $index = 0;
167
168 foreach ($blocks as $block) {
169 if (empty($block['blockName'])) {
170 // Skip empty blocks
171 continue;
172 }
173
174 $block_key = $prefix . '_' . $index;
175 $block_name = $block['blockName'];
176
177 // Extract text based on block type
178 switch ($block_name) {
179 case 'core/paragraph':
180 if (!empty($block['innerHTML'])) {
181 $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_p'));
182 }
183 break;
184
185 case 'core/heading':
186 if (!empty($block['innerHTML'])) {
187 $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_h'));
188 }
189 break;
190
191 case 'core/list':
192 if (!empty($block['innerHTML']) && empty($block['innerBlocks'])) {
193 $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_list'));
194 }
195 break;
196
197 case 'core/list-item':
198 if (!empty($block['innerHTML'])) {
199 $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_li'));
200 }
201 break;
202
203 case 'core/button':
204 if (!empty($block['innerHTML'])) {
205 $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_btn'));
206 }
207 break;
208
209 case 'core/image':
210 // Extract alt text and caption
211 if (!empty($block['attrs']['alt'])) {
212 $text_map[$block_key . '_alt'] = $block['attrs']['alt'];
213 }
214 if (!empty($block['innerHTML'])) {
215 $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_caption'));
216 }
217 break;
218
219 case 'core/quote':
220 if (!empty($block['innerHTML'])) {
221 $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_quote'));
222 }
223 break;
224
225 case 'core/table':
226 if (!empty($block['innerHTML'])) {
227 $text_map = array_merge($text_map, $this->extract_text_nodes($block['innerHTML'], $block_key . '_table'));
228 }
229 break;
230
231 case 'core/navigation':
232 // If navigation has a ref, we need to extract from the referenced wp_navigation post
233 if (!empty($block['attrs']['ref'])) {
234 $nav_post = get_post($block['attrs']['ref']);
235 if ($nav_post && $nav_post->post_type === 'wp_navigation') {
236 $nav_blocks = parse_blocks($nav_post->post_content);
237 $nav_map = $this->extract_texts_from_blocks($nav_blocks, $block_key . '_nav');
238 $text_map = array_merge($text_map, $nav_map);
239 }
240 }
241 // Also process innerBlocks if present (inline navigation)
242 break;
243
244 case 'core/navigation-link':
245 case 'core/navigation-submenu':
246 // Extract the label from attrs
247 if (!empty($block['attrs']['label'])) {
248 $text_map[$block_key . '_navlabel'] = $block['attrs']['label'];
249 }
250 break;
251 }
252
253 // Recursively process inner blocks
254 if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
255 $inner_map = $this->extract_texts_from_blocks($block['innerBlocks'], $block_key);
256 $text_map = array_merge($text_map, $inner_map);
257 }
258
259 $index++;
260 }
261
262 return $text_map;
263 }
264
265 /**
266 * Extract plain text from HTML, removing tags
267 */
268 private function extract_text_from_html($html)
269 {
270 // Remove script and style tags
271 $html = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $html);
272 $html = preg_replace('#<style(.*?)>(.*?)</style>#is', '', $html);
273
274 // Decode HTML entities
275 $html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
276
277 // Remove HTML tags but preserve line breaks for lists
278 $text = strip_tags($html);
279
280 // Clean up whitespace
281 $text = preg_replace('/\s+/', ' ', $text);
282 $text = trim($text);
283
284 return $text;
285 }
286
287 /**
288 * Extract text nodes preserving HTML structure
289 */
290 private function extract_text_nodes($html, $key_prefix)
291 {
292 $result = array();
293 if (trim((string)$html) === '') {
294 return $result;
295 }
296
297 $dom = new DOMDocument('1.0', 'UTF-8');
298 libxml_use_internal_errors(true);
299 $dom->loadHTML('<?xml encoding="utf-8"?><div>' . $html . '</div>');
300 libxml_clear_errors();
301
302 $container = $dom->getElementsByTagName('div')->item(0);
303 if (!$container) {
304 return $result;
305 }
306
307 $index = 0;
308 $this->traverse_text_nodes($container, function ($node) use (&$result, $key_prefix, &$index) {
309 $text = $node->nodeValue;
310 if (trim($text) === '') {
311 return;
312 }
313 $result[$key_prefix . '_text' . $index] = $text;
314 $index++;
315 });
316
317 return $result;
318 }
319
320 /**
321 * Replace text nodes with translated values
322 */
323 private function replace_text_nodes($html, $key_prefix, $translated_map)
324 {
325 if (trim((string)$html) === '') {
326 return $html;
327 }
328
329 $dom = new DOMDocument('1.0', 'UTF-8');
330 libxml_use_internal_errors(true);
331 $dom->loadHTML('<?xml encoding="utf-8"?><div>' . $html . '</div>');
332 libxml_clear_errors();
333
334 $container = $dom->getElementsByTagName('div')->item(0);
335 if (!$container) {
336 return $html;
337 }
338
339 $index = 0;
340 $this->traverse_text_nodes($container, function ($node) use ($key_prefix, $translated_map, &$index) {
341 $text = $node->nodeValue;
342 if (trim($text) === '') {
343 return;
344 }
345
346 $key = $key_prefix . '_text' . $index;
347 if (isset($translated_map[$key])) {
348 $node->nodeValue = $translated_map[$key];
349 }
350 $index++;
351 });
352
353 $innerHTML = '';
354 foreach ($container->childNodes as $child) {
355 $innerHTML .= $container->ownerDocument->saveHTML($child);
356 }
357
358 return $innerHTML;
359 }
360
361 /**
362 * Traverse DOM text nodes
363 */
364 private function traverse_text_nodes($node, $callback)
365 {
366 if ($node->nodeType === XML_TEXT_NODE) {
367 $callback($node);
368 }
369
370 if ($node->hasChildNodes()) {
371 foreach ($node->childNodes as $child) {
372 $this->traverse_text_nodes($child, $callback);
373 }
374 }
375 }
376
377 /**
378 * Reconstruct blocks with translated content
379 */
380 private function reconstruct_blocks($original_blocks, $translated_map, $prefix = 'block')
381 {
382 $reconstructed = array();
383 $index = 0;
384
385 foreach ($original_blocks as $block) {
386 if (empty($block['blockName'])) {
387 // Preserve empty blocks (spacers, etc.)
388 $reconstructed[] = $block;
389 continue;
390 }
391
392 $block_key = $prefix . '_' . $index;
393 $block_name = $block['blockName'];
394 $new_block = $block;
395
396 // Replace text based on block type
397 switch ($block_name) {
398 case 'core/paragraph':
399 if (!empty($block['innerHTML'])) {
400 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_p', $translated_map);
401 $new_block['innerHTML'] = $updated_html;
402 $new_block['innerContent'] = array($updated_html);
403 }
404 break;
405
406 case 'core/heading':
407 if (!empty($block['innerHTML'])) {
408 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_h', $translated_map);
409 $new_block['innerHTML'] = $updated_html;
410 $new_block['innerContent'] = array($updated_html);
411 }
412 break;
413
414 case 'core/list':
415 if (!empty($block['innerHTML']) && empty($block['innerBlocks'])) {
416 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_list', $translated_map);
417 $new_block['innerHTML'] = $updated_html;
418 $new_block['innerContent'] = array($updated_html);
419 }
420 break;
421
422 case 'core/list-item':
423 if (!empty($block['innerHTML'])) {
424 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_li', $translated_map);
425 $new_block['innerHTML'] = $updated_html;
426 $new_block['innerContent'] = array($updated_html);
427 }
428 break;
429
430 case 'core/button':
431 if (!empty($block['innerHTML'])) {
432 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_btn', $translated_map);
433 $new_block['innerHTML'] = $updated_html;
434 $new_block['innerContent'] = array($updated_html);
435 }
436 break;
437
438 case 'core/image':
439 if (isset($translated_map[$block_key . '_alt'])) {
440 $new_block['attrs']['alt'] = sanitize_text_field($translated_map[$block_key . '_alt']);
441 }
442 if (!empty($block['innerHTML'])) {
443 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_caption', $translated_map);
444 $new_block['innerHTML'] = $updated_html;
445 if (isset($new_block['innerContent'])) {
446 $new_block['innerContent'] = array($updated_html);
447 }
448 }
449 break;
450
451 case 'core/quote':
452 if (!empty($block['innerHTML'])) {
453 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_quote', $translated_map);
454 $new_block['innerHTML'] = $updated_html;
455 $new_block['innerContent'] = array($updated_html);
456 }
457 break;
458
459 case 'core/table':
460 if (!empty($block['innerHTML'])) {
461 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_table', $translated_map);
462 $new_block['innerHTML'] = $updated_html;
463 $new_block['innerContent'] = array($updated_html);
464 }
465 break;
466
467 case 'core/navigation':
468 // If navigation has a ref, duplicate the wp_navigation post with translated content
469 if (!empty($block['attrs']['ref'])) {
470 $new_nav_id = $this->duplicate_and_translate_navigation($block['attrs']['ref'], $translated_map, $block_key . '_nav');
471 if ($new_nav_id) {
472 $new_block['attrs']['ref'] = $new_nav_id;
473 }
474 }
475 break;
476
477 case 'core/navigation-link':
478 case 'core/navigation-submenu':
479 // Replace the label in attrs
480 if (isset($translated_map[$block_key . '_navlabel'])) {
481 $new_block['attrs']['label'] = $translated_map[$block_key . '_navlabel'];
482 }
483 break;
484 }
485
486 // Recursively process inner blocks
487 if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
488 $new_block['innerBlocks'] = $this->reconstruct_blocks($block['innerBlocks'], $translated_map, $block_key);
489 }
490
491 $reconstructed[] = $new_block;
492 $index++;
493 }
494
495 return $reconstructed;
496 }
497
498 /**
499 * Duplicate a wp_navigation post and translate its content
500 */
501 private function duplicate_and_translate_navigation($nav_id, $translated_map, $prefix)
502 {
503 $nav_post = get_post($nav_id);
504 if (!$nav_post || $nav_post->post_type !== 'wp_navigation') {
505 return null;
506 }
507
508 // Parse the navigation blocks
509 $nav_blocks = parse_blocks($nav_post->post_content);
510
511 // Reconstruct with translated labels
512 $translated_nav_blocks = $this->reconstruct_blocks($nav_blocks, $translated_map, $prefix);
513
514 // Serialize back to block content
515 $translated_nav_content = serialize_blocks($translated_nav_blocks);
516
517 // Get target language from the current translation context
518 $target_lang = isset($_POST['target_lang']) ? sanitize_text_field($_POST['target_lang']) : 'translated';
519
520 // Create a new wp_navigation post
521 $new_nav_data = array(
522 'post_title' => $nav_post->post_title . ' (' . strtoupper($target_lang) . ')',
523 'post_content' => $translated_nav_content,
524 'post_status' => 'publish',
525 'post_type' => 'wp_navigation',
526 'post_author' => get_current_user_id(),
527 );
528
529 $new_nav_id = wp_insert_post($new_nav_data);
530
531 if (is_wp_error($new_nav_id)) {
532 return null;
533 }
534
535 // Copy language meta
536 update_post_meta($new_nav_id, '_ai_lang', $target_lang);
537 update_post_meta($new_nav_id, '_ai_translation_source', $nav_id);
538
539 // Link to same translation group
540 $translation_group = get_post_meta($nav_id, '_ai_translation_group', true);
541 if (empty($translation_group)) {
542 $translation_group = 'nav_' . time() . '_' . wp_generate_password(8, false);
543 update_post_meta($nav_id, '_ai_translation_group', $translation_group);
544 }
545 update_post_meta($new_nav_id, '_ai_translation_group', $translation_group);
546
547 return $new_nav_id;
548 }
549
550 /**
551 * Get meta description with fallback logic
552 */
553 private function get_meta_description($post_id)
554 {
555 // Check _ai_builder_seo_desc first (as specified by user)
556 $meta_desc = get_post_meta($post_id, '_ai_builder_seo_desc', true);
557 if (!empty($meta_desc)) {
558 return $meta_desc;
559 }
560
561 // Check plugin's own meta key
562 $meta_desc = get_post_meta($post_id, 'aibui_meta_description', true);
563 if (!empty($meta_desc)) {
564 return $meta_desc;
565 }
566
567 // Check Yoast SEO meta
568 $meta_desc = get_post_meta($post_id, '_yoast_wpseo_metadesc', true);
569 if (!empty($meta_desc)) {
570 return $meta_desc;
571 }
572
573 // Fallback to excerpt
574 $post = get_post($post_id);
575 return $post ? $post->post_excerpt : '';
576 }
577
578 /**
579 * AJAX handler for translation
580 */
581 public function handle_translation()
582 {
583 // Verify nonce
584 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'aibui_nonce')) {
585 wp_send_json_error(array('message' => 'Security check failed'));
586 }
587
588 // Get and validate post ID
589 if (!isset($_POST['post_id'])) {
590 wp_send_json_error(array('message' => 'Post ID is required'));
591 }
592
593 $post_id = intval($_POST['post_id']);
594 if (!$post_id) {
595 wp_send_json_error(array('message' => 'Invalid post ID'));
596 }
597
598 // Check user capabilities
599 if (!current_user_can('edit_post', $post_id)) {
600 wp_send_json_error(array('message' => 'Insufficient permissions'));
601 }
602
603 // Get target language
604 if (!isset($_POST['target_lang']) || !array_key_exists($_POST['target_lang'], self::SUPPORTED_LANGUAGES)) {
605 wp_send_json_error(array('message' => 'Invalid target language'));
606 }
607
608 $target_lang = sanitize_text_field($_POST['target_lang']);
609 $target_lang_name = strtolower(self::SUPPORTED_LANGUAGES[$target_lang]);
610
611 // Get original post
612 $original_post = get_post($post_id);
613 if (!$original_post) {
614 wp_send_json_error(array('message' => 'Post not found'));
615 }
616
617 // Extract data
618 $title = $original_post->post_title;
619 $meta_desc = $this->get_meta_description($post_id);
620 $content = $original_post->post_content;
621
622 // Parse blocks
623 $blocks = parse_blocks($content);
624 $content_map = $this->extract_texts_from_blocks($blocks);
625
626 // Prepare data for API
627 $api_data = array(
628 'title' => $title,
629 'meta_desc' => $meta_desc,
630 'content_map' => $content_map,
631 'target_lang' => $target_lang_name
632 );
633 $this->log_translation_debug($post_id, 'request_payload', $api_data);
634
635 // Get JWT token
636 $jwt_token = get_option('aibui_jwt_token', '');
637 if (empty($jwt_token)) {
638 wp_send_json_error(array('message' => 'Authentication required. Please sign in.'));
639 }
640
641 // Call translation API
642 $api_url = 'https://api.wordpress-ai-builder.com/api' . '/ai-transform-page/translate-post';
643
644 $response = wp_remote_post($api_url, array(
645 'timeout' => 60,
646 'headers' => array(
647 'Authorization' => 'Bearer ' . $jwt_token,
648 'Content-Type' => 'application/json',
649 ),
650 'body' => json_encode($api_data),
651 ));
652
653 if (is_wp_error($response)) {
654 wp_send_json_error(array('message' => 'API request failed: ' . $response->get_error_message()));
655 }
656
657 $response_code = wp_remote_retrieve_response_code($response);
658 $body = wp_remote_retrieve_body($response);
659 $this->log_translation_debug($post_id, 'api_response', array(
660 'status' => $response_code,
661 'body' => $body
662 ));
663
664 if ($response_code === 402) {
665 $error_message = 'You do not have enough credits to translate this page. Each translation costs 40 credits.';
666
667 // Try to merge API error message if available
668 $decoded = json_decode($body, true);
669 if (is_array($decoded) && !empty($decoded['message'])) {
670 $error_message .= ' ' . $decoded['message'];
671 }
672
673 wp_send_json_error(array(
674 'message' => $error_message,
675 'code' => 'not_enough_credits',
676 ));
677 }
678
679 if ($response_code !== 200) {
680 wp_send_json_error(array('message' => 'Translation failed. Status: ' . $response_code));
681 }
682
683 $translated_data = json_decode($body, true);
684 $this->log_translation_debug($post_id, 'translated_payload', $translated_data);
685 if (!$translated_data || !isset($translated_data['title']) || !isset($translated_data['content_map'])) {
686 wp_send_json_error(array('message' => 'Invalid response from translation API'));
687 }
688
689 // Reconstruct blocks with translated content
690 $translated_blocks = $this->reconstruct_blocks($blocks, $translated_data['content_map']);
691 $translated_content = serialize_blocks($translated_blocks);
692 $this->log_translation_debug($post_id, 'final_content', array(
693 'content_preview' => mb_substr($translated_content, 0, 1000)
694 ));
695
696 // For template parts, generate a unique slug with language suffix
697 $new_post_name = '';
698 if ($original_post->post_type === 'wp_template_part') {
699 $new_post_name = $original_post->post_name . '-' . $target_lang;
700 // Template parts need to be published for Site Editor to find them
701 $new_post_status = 'publish';
702 } else {
703 $new_post_status = 'draft';
704 }
705
706 // Create new post
707 $new_post_data = array(
708 'post_title' => $translated_data['title'],
709 'post_content' => $translated_content,
710 'post_status' => $new_post_status,
711 'post_type' => $original_post->post_type,
712 'post_author' => get_current_user_id(),
713 );
714
715 // Set explicit slug for template parts
716 if (!empty($new_post_name)) {
717 $new_post_data['post_name'] = $new_post_name;
718 }
719
720 $new_post_id = wp_insert_post($new_post_data);
721
722 if (is_wp_error($new_post_id)) {
723 wp_send_json_error(array('message' => 'Failed to create translated post: ' . $new_post_id->get_error_message()));
724 }
725
726 // Save meta description (both keys)
727 if (!empty($translated_data['meta_desc'])) {
728 update_post_meta($new_post_id, '_ai_builder_seo_desc', $translated_data['meta_desc']);
729 update_post_meta($new_post_id, '_yoast_wpseo_metadesc', $translated_data['meta_desc']);
730 }
731
732 // Copy custom CSS meta if present
733 $css_meta_keys = array(
734 'ai_builder_page_css_content',
735 'ai_builder_block_css_content',
736 'ai_builder_css_content'
737 );
738
739 foreach ($css_meta_keys as $css_key) {
740 $css_value = get_post_meta($post_id, $css_key, true);
741 if (!empty($css_value)) {
742 update_post_meta($new_post_id, $css_key, $css_value);
743 }
744 }
745
746 // Save language meta
747 update_post_meta($new_post_id, '_ai_lang', $target_lang);
748 update_post_meta($new_post_id, '_ai_translation_source', $post_id);
749 if (!get_post_meta($post_id, '_ai_translation_source', true)) {
750 update_post_meta($post_id, '_ai_translation_source', $post_id);
751 }
752
753 // Copy template part taxonomy/meta if needed
754 if ($original_post->post_type === 'wp_template_part') {
755 // Assign to current theme
756 $theme_slug = wp_get_theme()->get_stylesheet();
757 wp_set_object_terms($new_post_id, $theme_slug, 'wp_theme', false);
758
759 // Copy template part area taxonomy
760 $area_terms = wp_get_object_terms($post_id, 'wp_template_part_area', array('fields' => 'slugs'));
761 if (!is_wp_error($area_terms) && !empty($area_terms)) {
762 wp_set_object_terms($new_post_id, $area_terms, 'wp_template_part_area', false);
763 }
764
765 update_post_meta($new_post_id, '_wp_template_part_area', get_post_meta($post_id, '_wp_template_part_area', true));
766 update_post_meta($new_post_id, '_ai_template_part_slug', $original_post->post_name);
767 }
768
769 // Handle translation group
770 $translation_group = get_post_meta($post_id, '_ai_translation_group', true);
771 if (empty($translation_group)) {
772 $translation_group = 'trans_' . time() . '_' . wp_generate_password(8, false);
773 update_post_meta($post_id, '_ai_translation_group', $translation_group);
774 }
775 update_post_meta($new_post_id, '_ai_translation_group', $translation_group);
776
777 // Copy other relevant meta
778 $original_lang = get_post_meta($post_id, '_ai_lang', true);
779 if (empty($original_lang)) {
780 update_post_meta($post_id, '_ai_lang', 'en'); // Default to English if not set
781 }
782
783 // Preserve AI-created flag to keep hide-title CSS behavior
784 if (get_post_meta($post_id, '_aibui_created_by_ai', true) === '1') {
785 update_post_meta($new_post_id, '_aibui_created_by_ai', '1');
786 }
787
788 // If the original page is the front page, mark the translation as homepage for its language
789 $front_page_id = get_option('page_on_front');
790 if ($front_page_id && (int) $front_page_id === (int) $post_id) {
791 update_post_meta($new_post_id, '_ai_is_homepage', '1');
792 }
793
794 // Also copy the homepage flag if it exists on the original
795 if (get_post_meta($post_id, '_ai_is_homepage', true) === '1') {
796 update_post_meta($new_post_id, '_ai_is_homepage', '1');
797 }
798
799 // Copy featured image if exists
800 $thumbnail_id = get_post_thumbnail_id($post_id);
801 if ($thumbnail_id) {
802 set_post_thumbnail($new_post_id, $thumbnail_id);
803 }
804
805 // Return success with new post URL
806 // For template parts, use Site Editor URL instead of post.php
807 if ($original_post->post_type === 'wp_template_part') {
808 $new_post = get_post($new_post_id);
809 $theme = wp_get_theme()->get_stylesheet();
810 // Site Editor URL format: /wp-admin/site-editor.php?postType=wp_template_part&postId=theme//slug
811 $edit_url = admin_url('site-editor.php?postType=wp_template_part&postId=' . urlencode($theme . '//' . $new_post->post_name) . '&canvas=edit');
812 } else {
813 $edit_url = admin_url('post.php?post=' . $new_post_id . '&action=edit');
814 }
815
816 wp_send_json_success(array(
817 'message' => 'Translation created successfully!',
818 'post_id' => $new_post_id,
819 'edit_url' => $edit_url,
820 ));
821 }
822
823 private function log_translation_debug($post_id, $stage, $data)
824 {
825 return;
826
827 // $log_file = plugin_dir_path(__FILE__) . '../debug-unescape.log';
828 // if (is_array($data) || is_object($data)) {
829 // $data = wp_json_encode($data);
830 // }
831 // $line = sprintf("[%s][AI Builder Translation][Post %d][%s] %s\n", date('Y-m-d H:i:s'), $post_id, $stage, $data);
832 // file_put_contents($log_file, $line, FILE_APPEND);
833 }
834
835 /**
836 * AJAX handler for template part translation
837 * Template parts use slugs like "theme//slug" instead of numeric IDs
838 */
839 public function handle_template_part_translation()
840 {
841 $this->log_template_part_debug('start', array(
842 'POST' => $_POST,
843 ));
844
845 // Verify nonce
846 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'aibui_nonce')) {
847 $this->log_template_part_debug('error', 'Nonce verification failed');
848 wp_send_json_error(array('message' => 'Security check failed'));
849 }
850
851 // Get template part ID (slug format: "theme//slug")
852 if (!isset($_POST['template_part_id']) || empty($_POST['template_part_id'])) {
853 $this->log_template_part_debug('error', 'Template part ID is required');
854 wp_send_json_error(array('message' => 'Template part ID is required'));
855 }
856
857 $template_part_id = sanitize_text_field($_POST['template_part_id']);
858 $this->log_template_part_debug('template_part_id', $template_part_id);
859
860 // Get target language
861 if (!isset($_POST['target_lang']) || !array_key_exists($_POST['target_lang'], self::SUPPORTED_LANGUAGES)) {
862 $this->log_template_part_debug('error', 'Invalid target language: ' . ($_POST['target_lang'] ?? 'not set'));
863 wp_send_json_error(array('message' => 'Invalid target language'));
864 }
865
866 $target_lang = sanitize_text_field($_POST['target_lang']);
867 $target_lang_name = strtolower(self::SUPPORTED_LANGUAGES[$target_lang]);
868
869 // Parse the template part ID to get theme and slug
870 // Format: "theme//slug" e.g., "twentytwentyfive//header"
871 $parts = explode('//', $template_part_id);
872 if (count($parts) !== 2) {
873 $this->log_template_part_debug('error', 'Invalid template part ID format: ' . $template_part_id);
874 wp_send_json_error(array('message' => 'Invalid template part ID format'));
875 }
876
877 $theme = $parts[0];
878 $slug = $parts[1];
879 $this->log_template_part_debug('parsed', array('theme' => $theme, 'slug' => $slug));
880
881 // Find the template part post by slug
882 $post = $this->get_template_part_post($theme, $slug);
883
884 if (!$post) {
885 $this->log_template_part_debug('error', 'Template part not found for theme: ' . $theme . ', slug: ' . $slug);
886 wp_send_json_error(array('message' => 'Template part not found. Make sure it has been customized (not a theme default).'));
887 }
888
889 $post_id = $post->ID;
890 $this->log_template_part_debug('found_post', array('post_id' => $post_id, 'post_title' => $post->post_title));
891
892 // Check user capabilities
893 if (!current_user_can('edit_post', $post_id)) {
894 $this->log_template_part_debug('error', 'Insufficient permissions for post: ' . $post_id);
895 wp_send_json_error(array('message' => 'Insufficient permissions'));
896 }
897
898 // Now translate using the numeric post ID
899 $_POST['post_id'] = $post_id;
900 $_POST['target_lang'] = $target_lang;
901
902 $this->log_template_part_debug('calling_handle_translation', array(
903 'post_id' => $post_id,
904 'target_lang' => $target_lang,
905 ));
906
907 // Call the main translation handler
908 $this->handle_translation();
909 }
910
911 /**
912 * Get template part post by theme and slug
913 */
914 private function get_template_part_post($theme, $slug)
915 {
916 // Query for the template part
917 $query = new WP_Query(array(
918 'post_type' => 'wp_template_part',
919 'post_status' => array('publish', 'draft', 'auto-draft'),
920 'name' => $slug,
921 'posts_per_page' => 1,
922 'no_found_rows' => true,
923 'tax_query' => array(
924 array(
925 'taxonomy' => 'wp_theme',
926 'field' => 'slug',
927 'terms' => $theme,
928 ),
929 ),
930 ));
931
932 $this->log_template_part_debug('query_result', array(
933 'found_posts' => $query->found_posts,
934 'post_count' => $query->post_count,
935 ));
936
937 if ($query->have_posts()) {
938 return $query->posts[0];
939 }
940
941 // Try without theme filter (for child themes, etc.)
942 $query2 = new WP_Query(array(
943 'post_type' => 'wp_template_part',
944 'post_status' => array('publish', 'draft', 'auto-draft'),
945 'name' => $slug,
946 'posts_per_page' => 1,
947 'no_found_rows' => true,
948 ));
949
950 $this->log_template_part_debug('query2_result', array(
951 'found_posts' => $query2->found_posts,
952 'post_count' => $query2->post_count,
953 ));
954
955 if ($query2->have_posts()) {
956 return $query2->posts[0];
957 }
958
959 return null;
960 }
961
962 /**
963 * Debug logging for template part translation
964 */
965 private function log_template_part_debug($stage, $data)
966 {
967 $log_file = plugin_dir_path(__FILE__) . '../debug-template-part.log';
968 if (is_array($data) || is_object($data)) {
969 $data = wp_json_encode($data);
970 }
971 $line = sprintf("[%s][Template Part Translation][%s] %s\n", date('Y-m-d H:i:s'), $stage, $data);
972 file_put_contents($log_file, $line, FILE_APPEND);
973 }
974 }
975
976