PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.2.4
AI Builder – Generate pages, blocks, images & translate with AI v2.2.4
2.7.10 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 All 122 releases
ai-builder / includes / class-translation-handler.php

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

978 lines 36.4 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' => 180, // 5 minutes - AI translation can take longer for large pages
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 across all supported meta 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 update_post_meta($new_post_id, 'aibui_meta_description', $translated_data['meta_desc']);
731 update_post_meta($new_post_id, '_ai_translation_meta_desc', $translated_data['meta_desc']);
732 }
733
734 // Copy custom CSS meta if present
735 $css_meta_keys = array(
736 'ai_builder_page_css_content',
737 'ai_builder_block_css_content',
738 'ai_builder_css_content'
739 );
740
741 foreach ($css_meta_keys as $css_key) {
742 $css_value = get_post_meta($post_id, $css_key, true);
743 if (!empty($css_value)) {
744 update_post_meta($new_post_id, $css_key, $css_value);
745 }
746 }
747
748 // Save language meta
749 update_post_meta($new_post_id, '_ai_lang', $target_lang);
750 update_post_meta($new_post_id, '_ai_translation_source', $post_id);
751 if (!get_post_meta($post_id, '_ai_translation_source', true)) {
752 update_post_meta($post_id, '_ai_translation_source', $post_id);
753 }
754
755 // Copy template part taxonomy/meta if needed
756 if ($original_post->post_type === 'wp_template_part') {
757 // Assign to current theme
758 $theme_slug = wp_get_theme()->get_stylesheet();
759 wp_set_object_terms($new_post_id, $theme_slug, 'wp_theme', false);
760
761 // Copy template part area taxonomy
762 $area_terms = wp_get_object_terms($post_id, 'wp_template_part_area', array('fields' => 'slugs'));
763 if (!is_wp_error($area_terms) && !empty($area_terms)) {
764 wp_set_object_terms($new_post_id, $area_terms, 'wp_template_part_area', false);
765 }
766
767 update_post_meta($new_post_id, '_wp_template_part_area', get_post_meta($post_id, '_wp_template_part_area', true));
768 update_post_meta($new_post_id, '_ai_template_part_slug', $original_post->post_name);
769 }
770
771 // Handle translation group
772 $translation_group = get_post_meta($post_id, '_ai_translation_group', true);
773 if (empty($translation_group)) {
774 $translation_group = 'trans_' . time() . '_' . wp_generate_password(8, false);
775 update_post_meta($post_id, '_ai_translation_group', $translation_group);
776 }
777 update_post_meta($new_post_id, '_ai_translation_group', $translation_group);
778
779 // Copy other relevant meta
780 $original_lang = get_post_meta($post_id, '_ai_lang', true);
781 if (empty($original_lang)) {
782 update_post_meta($post_id, '_ai_lang', 'en'); // Default to English if not set
783 }
784
785 // Preserve AI-created flag to keep hide-title CSS behavior
786 if (get_post_meta($post_id, '_aibui_created_by_ai', true) === '1') {
787 update_post_meta($new_post_id, '_aibui_created_by_ai', '1');
788 }
789
790 // If the original page is the front page, mark the translation as homepage for its language
791 $front_page_id = get_option('page_on_front');
792 if ($front_page_id && (int) $front_page_id === (int) $post_id) {
793 update_post_meta($new_post_id, '_ai_is_homepage', '1');
794 }
795
796 // Also copy the homepage flag if it exists on the original
797 if (get_post_meta($post_id, '_ai_is_homepage', true) === '1') {
798 update_post_meta($new_post_id, '_ai_is_homepage', '1');
799 }
800
801 // Copy featured image if exists
802 $thumbnail_id = get_post_thumbnail_id($post_id);
803 if ($thumbnail_id) {
804 set_post_thumbnail($new_post_id, $thumbnail_id);
805 }
806
807 // Return success with new post URL
808 // For template parts, use Site Editor URL instead of post.php
809 if ($original_post->post_type === 'wp_template_part') {
810 $new_post = get_post($new_post_id);
811 $theme = wp_get_theme()->get_stylesheet();
812 // Site Editor URL format: /wp-admin/site-editor.php?postType=wp_template_part&postId=theme//slug
813 $edit_url = admin_url('site-editor.php?postType=wp_template_part&postId=' . urlencode($theme . '//' . $new_post->post_name) . '&canvas=edit');
814 } else {
815 $edit_url = admin_url('post.php?post=' . $new_post_id . '&action=edit');
816 }
817
818 wp_send_json_success(array(
819 'message' => 'Translation created successfully!',
820 'post_id' => $new_post_id,
821 'edit_url' => $edit_url,
822 ));
823 }
824
825 private function log_translation_debug($post_id, $stage, $data)
826 {
827 return;
828
829 // $log_file = plugin_dir_path(__FILE__) . '../debug-unescape.log';
830 // if (is_array($data) || is_object($data)) {
831 // $data = wp_json_encode($data);
832 // }
833 // $line = sprintf("[%s][AI Builder Translation][Post %d][%s] %s\n", date('Y-m-d H:i:s'), $post_id, $stage, $data);
834 // file_put_contents($log_file, $line, FILE_APPEND);
835 }
836
837 /**
838 * AJAX handler for template part translation
839 * Template parts use slugs like "theme//slug" instead of numeric IDs
840 */
841 public function handle_template_part_translation()
842 {
843 $this->log_template_part_debug('start', array(
844 'POST' => $_POST,
845 ));
846
847 // Verify nonce
848 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'aibui_nonce')) {
849 $this->log_template_part_debug('error', 'Nonce verification failed');
850 wp_send_json_error(array('message' => 'Security check failed'));
851 }
852
853 // Get template part ID (slug format: "theme//slug")
854 if (!isset($_POST['template_part_id']) || empty($_POST['template_part_id'])) {
855 $this->log_template_part_debug('error', 'Template part ID is required');
856 wp_send_json_error(array('message' => 'Template part ID is required'));
857 }
858
859 $template_part_id = sanitize_text_field($_POST['template_part_id']);
860 $this->log_template_part_debug('template_part_id', $template_part_id);
861
862 // Get target language
863 if (!isset($_POST['target_lang']) || !array_key_exists($_POST['target_lang'], self::SUPPORTED_LANGUAGES)) {
864 $this->log_template_part_debug('error', 'Invalid target language: ' . ($_POST['target_lang'] ?? 'not set'));
865 wp_send_json_error(array('message' => 'Invalid target language'));
866 }
867
868 $target_lang = sanitize_text_field($_POST['target_lang']);
869 $target_lang_name = strtolower(self::SUPPORTED_LANGUAGES[$target_lang]);
870
871 // Parse the template part ID to get theme and slug
872 // Format: "theme//slug" e.g., "twentytwentyfive//header"
873 $parts = explode('//', $template_part_id);
874 if (count($parts) !== 2) {
875 $this->log_template_part_debug('error', 'Invalid template part ID format: ' . $template_part_id);
876 wp_send_json_error(array('message' => 'Invalid template part ID format'));
877 }
878
879 $theme = $parts[0];
880 $slug = $parts[1];
881 $this->log_template_part_debug('parsed', array('theme' => $theme, 'slug' => $slug));
882
883 // Find the template part post by slug
884 $post = $this->get_template_part_post($theme, $slug);
885
886 if (!$post) {
887 $this->log_template_part_debug('error', 'Template part not found for theme: ' . $theme . ', slug: ' . $slug);
888 wp_send_json_error(array('message' => 'Template part not found. Make sure it has been customized (not a theme default).'));
889 }
890
891 $post_id = $post->ID;
892 $this->log_template_part_debug('found_post', array('post_id' => $post_id, 'post_title' => $post->post_title));
893
894 // Check user capabilities
895 if (!current_user_can('edit_post', $post_id)) {
896 $this->log_template_part_debug('error', 'Insufficient permissions for post: ' . $post_id);
897 wp_send_json_error(array('message' => 'Insufficient permissions'));
898 }
899
900 // Now translate using the numeric post ID
901 $_POST['post_id'] = $post_id;
902 $_POST['target_lang'] = $target_lang;
903
904 $this->log_template_part_debug('calling_handle_translation', array(
905 'post_id' => $post_id,
906 'target_lang' => $target_lang,
907 ));
908
909 // Call the main translation handler
910 $this->handle_translation();
911 }
912
913 /**
914 * Get template part post by theme and slug
915 */
916 private function get_template_part_post($theme, $slug)
917 {
918 // Query for the template part
919 $query = new WP_Query(array(
920 'post_type' => 'wp_template_part',
921 'post_status' => array('publish', 'draft', 'auto-draft'),
922 'name' => $slug,
923 'posts_per_page' => 1,
924 'no_found_rows' => true,
925 'tax_query' => array(
926 array(
927 'taxonomy' => 'wp_theme',
928 'field' => 'slug',
929 'terms' => $theme,
930 ),
931 ),
932 ));
933
934 $this->log_template_part_debug('query_result', array(
935 'found_posts' => $query->found_posts,
936 'post_count' => $query->post_count,
937 ));
938
939 if ($query->have_posts()) {
940 return $query->posts[0];
941 }
942
943 // Try without theme filter (for child themes, etc.)
944 $query2 = new WP_Query(array(
945 'post_type' => 'wp_template_part',
946 'post_status' => array('publish', 'draft', 'auto-draft'),
947 'name' => $slug,
948 'posts_per_page' => 1,
949 'no_found_rows' => true,
950 ));
951
952 $this->log_template_part_debug('query2_result', array(
953 'found_posts' => $query2->found_posts,
954 'post_count' => $query2->post_count,
955 ));
956
957 if ($query2->have_posts()) {
958 return $query2->posts[0];
959 }
960
961 return null;
962 }
963
964 /**
965 * Debug logging for template part translation
966 */
967 private function log_template_part_debug($stage, $data)
968 {
969 $log_file = plugin_dir_path(__FILE__) . '../debug-template-part.log';
970 if (is_array($data) || is_object($data)) {
971 $data = wp_json_encode($data);
972 }
973 $line = sprintf("[%s][Template Part Translation][%s] %s\n", date('Y-m-d H:i:s'), $stage, $data);
974 file_put_contents($log_file, $line, FILE_APPEND);
975 }
976 }
977
978