PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.1.4
AI Builder – Generate pages, blocks, images & translate with AI v2.1.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.1.4, at includes/class-translation-handler.php

874 lines 31.6 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
232 // Recursively process inner blocks
233 if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
234 $inner_map = $this->extract_texts_from_blocks($block['innerBlocks'], $block_key);
235 $text_map = array_merge($text_map, $inner_map);
236 }
237
238 $index++;
239 }
240
241 return $text_map;
242 }
243
244 /**
245 * Extract plain text from HTML, removing tags
246 */
247 private function extract_text_from_html($html)
248 {
249 // Remove script and style tags
250 $html = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $html);
251 $html = preg_replace('#<style(.*?)>(.*?)</style>#is', '', $html);
252
253 // Decode HTML entities
254 $html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
255
256 // Remove HTML tags but preserve line breaks for lists
257 $text = strip_tags($html);
258
259 // Clean up whitespace
260 $text = preg_replace('/\s+/', ' ', $text);
261 $text = trim($text);
262
263 return $text;
264 }
265
266 /**
267 * Extract text nodes preserving HTML structure
268 */
269 private function extract_text_nodes($html, $key_prefix)
270 {
271 $result = array();
272 if (trim((string)$html) === '') {
273 return $result;
274 }
275
276 $dom = new DOMDocument('1.0', 'UTF-8');
277 libxml_use_internal_errors(true);
278 $dom->loadHTML('<?xml encoding="utf-8"?><div>' . $html . '</div>');
279 libxml_clear_errors();
280
281 $container = $dom->getElementsByTagName('div')->item(0);
282 if (!$container) {
283 return $result;
284 }
285
286 $index = 0;
287 $this->traverse_text_nodes($container, function ($node) use (&$result, $key_prefix, &$index) {
288 $text = $node->nodeValue;
289 if (trim($text) === '') {
290 return;
291 }
292 $result[$key_prefix . '_text' . $index] = $text;
293 $index++;
294 });
295
296 return $result;
297 }
298
299 /**
300 * Replace text nodes with translated values
301 */
302 private function replace_text_nodes($html, $key_prefix, $translated_map)
303 {
304 if (trim((string)$html) === '') {
305 return $html;
306 }
307
308 $dom = new DOMDocument('1.0', 'UTF-8');
309 libxml_use_internal_errors(true);
310 $dom->loadHTML('<?xml encoding="utf-8"?><div>' . $html . '</div>');
311 libxml_clear_errors();
312
313 $container = $dom->getElementsByTagName('div')->item(0);
314 if (!$container) {
315 return $html;
316 }
317
318 $index = 0;
319 $this->traverse_text_nodes($container, function ($node) use ($key_prefix, $translated_map, &$index) {
320 $text = $node->nodeValue;
321 if (trim($text) === '') {
322 return;
323 }
324
325 $key = $key_prefix . '_text' . $index;
326 if (isset($translated_map[$key])) {
327 $node->nodeValue = $translated_map[$key];
328 }
329 $index++;
330 });
331
332 $innerHTML = '';
333 foreach ($container->childNodes as $child) {
334 $innerHTML .= $container->ownerDocument->saveHTML($child);
335 }
336
337 return $innerHTML;
338 }
339
340 /**
341 * Traverse DOM text nodes
342 */
343 private function traverse_text_nodes($node, $callback)
344 {
345 if ($node->nodeType === XML_TEXT_NODE) {
346 $callback($node);
347 }
348
349 if ($node->hasChildNodes()) {
350 foreach ($node->childNodes as $child) {
351 $this->traverse_text_nodes($child, $callback);
352 }
353 }
354 }
355
356 /**
357 * Reconstruct blocks with translated content
358 */
359 private function reconstruct_blocks($original_blocks, $translated_map, $prefix = 'block')
360 {
361 $reconstructed = array();
362 $index = 0;
363
364 foreach ($original_blocks as $block) {
365 if (empty($block['blockName'])) {
366 // Preserve empty blocks (spacers, etc.)
367 $reconstructed[] = $block;
368 continue;
369 }
370
371 $block_key = $prefix . '_' . $index;
372 $block_name = $block['blockName'];
373 $new_block = $block;
374
375 // Replace text based on block type
376 switch ($block_name) {
377 case 'core/paragraph':
378 if (!empty($block['innerHTML'])) {
379 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_p', $translated_map);
380 $new_block['innerHTML'] = $updated_html;
381 $new_block['innerContent'] = array($updated_html);
382 }
383 break;
384
385 case 'core/heading':
386 if (!empty($block['innerHTML'])) {
387 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_h', $translated_map);
388 $new_block['innerHTML'] = $updated_html;
389 $new_block['innerContent'] = array($updated_html);
390 }
391 break;
392
393 case 'core/list':
394 if (!empty($block['innerHTML']) && empty($block['innerBlocks'])) {
395 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_list', $translated_map);
396 $new_block['innerHTML'] = $updated_html;
397 $new_block['innerContent'] = array($updated_html);
398 }
399 break;
400
401 case 'core/list-item':
402 if (!empty($block['innerHTML'])) {
403 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_li', $translated_map);
404 $new_block['innerHTML'] = $updated_html;
405 $new_block['innerContent'] = array($updated_html);
406 }
407 break;
408
409 case 'core/button':
410 if (!empty($block['innerHTML'])) {
411 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_btn', $translated_map);
412 $new_block['innerHTML'] = $updated_html;
413 $new_block['innerContent'] = array($updated_html);
414 }
415 break;
416
417 case 'core/image':
418 if (isset($translated_map[$block_key . '_alt'])) {
419 $new_block['attrs']['alt'] = sanitize_text_field($translated_map[$block_key . '_alt']);
420 }
421 if (!empty($block['innerHTML'])) {
422 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_caption', $translated_map);
423 $new_block['innerHTML'] = $updated_html;
424 if (isset($new_block['innerContent'])) {
425 $new_block['innerContent'] = array($updated_html);
426 }
427 }
428 break;
429
430 case 'core/quote':
431 if (!empty($block['innerHTML'])) {
432 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_quote', $translated_map);
433 $new_block['innerHTML'] = $updated_html;
434 $new_block['innerContent'] = array($updated_html);
435 }
436 break;
437
438 case 'core/table':
439 if (!empty($block['innerHTML'])) {
440 $updated_html = $this->replace_text_nodes($block['innerHTML'], $block_key . '_table', $translated_map);
441 $new_block['innerHTML'] = $updated_html;
442 $new_block['innerContent'] = array($updated_html);
443 }
444 break;
445 }
446
447 // Recursively process inner blocks
448 if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
449 $new_block['innerBlocks'] = $this->reconstruct_blocks($block['innerBlocks'], $translated_map, $block_key);
450 }
451
452 $reconstructed[] = $new_block;
453 $index++;
454 }
455
456 return $reconstructed;
457 }
458
459 /**
460 * Get meta description with fallback logic
461 */
462 private function get_meta_description($post_id)
463 {
464 // Check _ai_builder_seo_desc first (as specified by user)
465 $meta_desc = get_post_meta($post_id, '_ai_builder_seo_desc', true);
466 if (!empty($meta_desc)) {
467 return $meta_desc;
468 }
469
470 // Check plugin's own meta key
471 $meta_desc = get_post_meta($post_id, 'aibui_meta_description', true);
472 if (!empty($meta_desc)) {
473 return $meta_desc;
474 }
475
476 // Check Yoast SEO meta
477 $meta_desc = get_post_meta($post_id, '_yoast_wpseo_metadesc', true);
478 if (!empty($meta_desc)) {
479 return $meta_desc;
480 }
481
482 // Fallback to excerpt
483 $post = get_post($post_id);
484 return $post ? $post->post_excerpt : '';
485 }
486
487 /**
488 * AJAX handler for translation
489 */
490 public function handle_translation()
491 {
492 // Verify nonce
493 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'aibui_nonce')) {
494 wp_send_json_error(array('message' => 'Security check failed'));
495 }
496
497 // Get and validate post ID
498 if (!isset($_POST['post_id'])) {
499 wp_send_json_error(array('message' => 'Post ID is required'));
500 }
501
502 $post_id = intval($_POST['post_id']);
503 if (!$post_id) {
504 wp_send_json_error(array('message' => 'Invalid post ID'));
505 }
506
507 // Check user capabilities
508 if (!current_user_can('edit_post', $post_id)) {
509 wp_send_json_error(array('message' => 'Insufficient permissions'));
510 }
511
512 // Get target language
513 if (!isset($_POST['target_lang']) || !array_key_exists($_POST['target_lang'], self::SUPPORTED_LANGUAGES)) {
514 wp_send_json_error(array('message' => 'Invalid target language'));
515 }
516
517 $target_lang = sanitize_text_field($_POST['target_lang']);
518 $target_lang_name = strtolower(self::SUPPORTED_LANGUAGES[$target_lang]);
519
520 // Get original post
521 $original_post = get_post($post_id);
522 if (!$original_post) {
523 wp_send_json_error(array('message' => 'Post not found'));
524 }
525
526 // Extract data
527 $title = $original_post->post_title;
528 $meta_desc = $this->get_meta_description($post_id);
529 $content = $original_post->post_content;
530
531 // Parse blocks
532 $blocks = parse_blocks($content);
533 $content_map = $this->extract_texts_from_blocks($blocks);
534
535 // Prepare data for API
536 $api_data = array(
537 'title' => $title,
538 'meta_desc' => $meta_desc,
539 'content_map' => $content_map,
540 'target_lang' => $target_lang_name
541 );
542 $this->log_translation_debug($post_id, 'request_payload', $api_data);
543
544 // Get JWT token
545 $jwt_token = get_option('aibui_jwt_token', '');
546 if (empty($jwt_token)) {
547 wp_send_json_error(array('message' => 'Authentication required. Please sign in.'));
548 }
549
550 // Call translation API
551 $api_url = 'https://api.wordpress-ai-builder.com/api' . '/ai-transform-page/translate-post';
552
553 $response = wp_remote_post($api_url, array(
554 'timeout' => 60,
555 'headers' => array(
556 'Authorization' => 'Bearer ' . $jwt_token,
557 'Content-Type' => 'application/json',
558 ),
559 'body' => json_encode($api_data),
560 ));
561
562 if (is_wp_error($response)) {
563 wp_send_json_error(array('message' => 'API request failed: ' . $response->get_error_message()));
564 }
565
566 $response_code = wp_remote_retrieve_response_code($response);
567 $body = wp_remote_retrieve_body($response);
568 $this->log_translation_debug($post_id, 'api_response', array(
569 'status' => $response_code,
570 'body' => $body
571 ));
572
573 if ($response_code === 402) {
574 $error_message = 'You do not have enough credits to translate this page. Each translation costs 40 credits.';
575
576 // Try to merge API error message if available
577 $decoded = json_decode($body, true);
578 if (is_array($decoded) && !empty($decoded['message'])) {
579 $error_message .= ' ' . $decoded['message'];
580 }
581
582 wp_send_json_error(array(
583 'message' => $error_message,
584 'code' => 'not_enough_credits',
585 ));
586 }
587
588 if ($response_code !== 200) {
589 wp_send_json_error(array('message' => 'Translation failed. Status: ' . $response_code));
590 }
591
592 $translated_data = json_decode($body, true);
593 $this->log_translation_debug($post_id, 'translated_payload', $translated_data);
594 if (!$translated_data || !isset($translated_data['title']) || !isset($translated_data['content_map'])) {
595 wp_send_json_error(array('message' => 'Invalid response from translation API'));
596 }
597
598 // Reconstruct blocks with translated content
599 $translated_blocks = $this->reconstruct_blocks($blocks, $translated_data['content_map']);
600 $translated_content = serialize_blocks($translated_blocks);
601 $this->log_translation_debug($post_id, 'final_content', array(
602 'content_preview' => mb_substr($translated_content, 0, 1000)
603 ));
604
605 // For template parts, generate a unique slug with language suffix
606 $new_post_name = '';
607 if ($original_post->post_type === 'wp_template_part') {
608 $new_post_name = $original_post->post_name . '-' . $target_lang;
609 // Template parts need to be published for Site Editor to find them
610 $new_post_status = 'publish';
611 } else {
612 $new_post_status = 'draft';
613 }
614
615 // Create new post
616 $new_post_data = array(
617 'post_title' => $translated_data['title'],
618 'post_content' => $translated_content,
619 'post_status' => $new_post_status,
620 'post_type' => $original_post->post_type,
621 'post_author' => get_current_user_id(),
622 );
623
624 // Set explicit slug for template parts
625 if (!empty($new_post_name)) {
626 $new_post_data['post_name'] = $new_post_name;
627 }
628
629 $new_post_id = wp_insert_post($new_post_data);
630
631 if (is_wp_error($new_post_id)) {
632 wp_send_json_error(array('message' => 'Failed to create translated post: ' . $new_post_id->get_error_message()));
633 }
634
635 // Save meta description (both keys)
636 if (!empty($translated_data['meta_desc'])) {
637 update_post_meta($new_post_id, '_ai_builder_seo_desc', $translated_data['meta_desc']);
638 update_post_meta($new_post_id, '_yoast_wpseo_metadesc', $translated_data['meta_desc']);
639 }
640
641 // Copy custom CSS meta if present
642 $css_meta_keys = array(
643 'ai_builder_page_css_content',
644 'ai_builder_block_css_content',
645 'ai_builder_css_content'
646 );
647
648 foreach ($css_meta_keys as $css_key) {
649 $css_value = get_post_meta($post_id, $css_key, true);
650 if (!empty($css_value)) {
651 update_post_meta($new_post_id, $css_key, $css_value);
652 }
653 }
654
655 // Save language meta
656 update_post_meta($new_post_id, '_ai_lang', $target_lang);
657 update_post_meta($new_post_id, '_ai_translation_source', $post_id);
658 if (!get_post_meta($post_id, '_ai_translation_source', true)) {
659 update_post_meta($post_id, '_ai_translation_source', $post_id);
660 }
661
662 // Copy template part taxonomy/meta if needed
663 if ($original_post->post_type === 'wp_template_part') {
664 // Assign to current theme
665 $theme_slug = wp_get_theme()->get_stylesheet();
666 wp_set_object_terms($new_post_id, $theme_slug, 'wp_theme', false);
667
668 // Copy template part area taxonomy
669 $area_terms = wp_get_object_terms($post_id, 'wp_template_part_area', array('fields' => 'slugs'));
670 if (!is_wp_error($area_terms) && !empty($area_terms)) {
671 wp_set_object_terms($new_post_id, $area_terms, 'wp_template_part_area', false);
672 }
673
674 update_post_meta($new_post_id, '_wp_template_part_area', get_post_meta($post_id, '_wp_template_part_area', true));
675 update_post_meta($new_post_id, '_ai_template_part_slug', $original_post->post_name);
676 }
677
678 // Handle translation group
679 $translation_group = get_post_meta($post_id, '_ai_translation_group', true);
680 if (empty($translation_group)) {
681 $translation_group = 'trans_' . time() . '_' . wp_generate_password(8, false);
682 update_post_meta($post_id, '_ai_translation_group', $translation_group);
683 }
684 update_post_meta($new_post_id, '_ai_translation_group', $translation_group);
685
686 // Copy other relevant meta
687 $original_lang = get_post_meta($post_id, '_ai_lang', true);
688 if (empty($original_lang)) {
689 update_post_meta($post_id, '_ai_lang', 'en'); // Default to English if not set
690 }
691
692 // Preserve AI-created flag to keep hide-title CSS behavior
693 if (get_post_meta($post_id, '_aibui_created_by_ai', true) === '1') {
694 update_post_meta($new_post_id, '_aibui_created_by_ai', '1');
695 }
696
697 // Copy featured image if exists
698 $thumbnail_id = get_post_thumbnail_id($post_id);
699 if ($thumbnail_id) {
700 set_post_thumbnail($new_post_id, $thumbnail_id);
701 }
702
703 // Return success with new post URL
704 // For template parts, use Site Editor URL instead of post.php
705 if ($original_post->post_type === 'wp_template_part') {
706 $new_post = get_post($new_post_id);
707 $theme = wp_get_theme()->get_stylesheet();
708 // Site Editor URL format: /wp-admin/site-editor.php?postType=wp_template_part&postId=theme//slug
709 $edit_url = admin_url('site-editor.php?postType=wp_template_part&postId=' . urlencode($theme . '//' . $new_post->post_name) . '&canvas=edit');
710 } else {
711 $edit_url = admin_url('post.php?post=' . $new_post_id . '&action=edit');
712 }
713
714 wp_send_json_success(array(
715 'message' => 'Translation created successfully!',
716 'post_id' => $new_post_id,
717 'edit_url' => $edit_url,
718 ));
719 }
720
721 private function log_translation_debug($post_id, $stage, $data)
722 {
723 return;
724
725 // $log_file = plugin_dir_path(__FILE__) . '../debug-unescape.log';
726 // if (is_array($data) || is_object($data)) {
727 // $data = wp_json_encode($data);
728 // }
729 // $line = sprintf("[%s][AI Builder Translation][Post %d][%s] %s\n", date('Y-m-d H:i:s'), $post_id, $stage, $data);
730 // file_put_contents($log_file, $line, FILE_APPEND);
731 }
732
733 /**
734 * AJAX handler for template part translation
735 * Template parts use slugs like "theme//slug" instead of numeric IDs
736 */
737 public function handle_template_part_translation()
738 {
739 $this->log_template_part_debug('start', array(
740 'POST' => $_POST,
741 ));
742
743 // Verify nonce
744 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'aibui_nonce')) {
745 $this->log_template_part_debug('error', 'Nonce verification failed');
746 wp_send_json_error(array('message' => 'Security check failed'));
747 }
748
749 // Get template part ID (slug format: "theme//slug")
750 if (!isset($_POST['template_part_id']) || empty($_POST['template_part_id'])) {
751 $this->log_template_part_debug('error', 'Template part ID is required');
752 wp_send_json_error(array('message' => 'Template part ID is required'));
753 }
754
755 $template_part_id = sanitize_text_field($_POST['template_part_id']);
756 $this->log_template_part_debug('template_part_id', $template_part_id);
757
758 // Get target language
759 if (!isset($_POST['target_lang']) || !array_key_exists($_POST['target_lang'], self::SUPPORTED_LANGUAGES)) {
760 $this->log_template_part_debug('error', 'Invalid target language: ' . ($_POST['target_lang'] ?? 'not set'));
761 wp_send_json_error(array('message' => 'Invalid target language'));
762 }
763
764 $target_lang = sanitize_text_field($_POST['target_lang']);
765 $target_lang_name = strtolower(self::SUPPORTED_LANGUAGES[$target_lang]);
766
767 // Parse the template part ID to get theme and slug
768 // Format: "theme//slug" e.g., "twentytwentyfive//header"
769 $parts = explode('//', $template_part_id);
770 if (count($parts) !== 2) {
771 $this->log_template_part_debug('error', 'Invalid template part ID format: ' . $template_part_id);
772 wp_send_json_error(array('message' => 'Invalid template part ID format'));
773 }
774
775 $theme = $parts[0];
776 $slug = $parts[1];
777 $this->log_template_part_debug('parsed', array('theme' => $theme, 'slug' => $slug));
778
779 // Find the template part post by slug
780 $post = $this->get_template_part_post($theme, $slug);
781
782 if (!$post) {
783 $this->log_template_part_debug('error', 'Template part not found for theme: ' . $theme . ', slug: ' . $slug);
784 wp_send_json_error(array('message' => 'Template part not found. Make sure it has been customized (not a theme default).'));
785 }
786
787 $post_id = $post->ID;
788 $this->log_template_part_debug('found_post', array('post_id' => $post_id, 'post_title' => $post->post_title));
789
790 // Check user capabilities
791 if (!current_user_can('edit_post', $post_id)) {
792 $this->log_template_part_debug('error', 'Insufficient permissions for post: ' . $post_id);
793 wp_send_json_error(array('message' => 'Insufficient permissions'));
794 }
795
796 // Now translate using the numeric post ID
797 $_POST['post_id'] = $post_id;
798 $_POST['target_lang'] = $target_lang;
799
800 $this->log_template_part_debug('calling_handle_translation', array(
801 'post_id' => $post_id,
802 'target_lang' => $target_lang,
803 ));
804
805 // Call the main translation handler
806 $this->handle_translation();
807 }
808
809 /**
810 * Get template part post by theme and slug
811 */
812 private function get_template_part_post($theme, $slug)
813 {
814 // Query for the template part
815 $query = new WP_Query(array(
816 'post_type' => 'wp_template_part',
817 'post_status' => array('publish', 'draft', 'auto-draft'),
818 'name' => $slug,
819 'posts_per_page' => 1,
820 'no_found_rows' => true,
821 'tax_query' => array(
822 array(
823 'taxonomy' => 'wp_theme',
824 'field' => 'slug',
825 'terms' => $theme,
826 ),
827 ),
828 ));
829
830 $this->log_template_part_debug('query_result', array(
831 'found_posts' => $query->found_posts,
832 'post_count' => $query->post_count,
833 ));
834
835 if ($query->have_posts()) {
836 return $query->posts[0];
837 }
838
839 // Try without theme filter (for child themes, etc.)
840 $query2 = new WP_Query(array(
841 'post_type' => 'wp_template_part',
842 'post_status' => array('publish', 'draft', 'auto-draft'),
843 'name' => $slug,
844 'posts_per_page' => 1,
845 'no_found_rows' => true,
846 ));
847
848 $this->log_template_part_debug('query2_result', array(
849 'found_posts' => $query2->found_posts,
850 'post_count' => $query2->post_count,
851 ));
852
853 if ($query2->have_posts()) {
854 return $query2->posts[0];
855 }
856
857 return null;
858 }
859
860 /**
861 * Debug logging for template part translation
862 */
863 private function log_template_part_debug($stage, $data)
864 {
865 $log_file = plugin_dir_path(__FILE__) . '../debug-template-part.log';
866 if (is_array($data) || is_object($data)) {
867 $data = wp_json_encode($data);
868 }
869 $line = sprintf("[%s][Template Part Translation][%s] %s\n", date('Y-m-d H:i:s'), $stage, $data);
870 file_put_contents($log_file, $line, FILE_APPEND);
871 }
872 }
873
874