| 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 |
|