, not mid-render. add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); add_action( 'wp_ajax_aitrfoel_save_segment_edit', [ $this, 'handle_save_segment' ] ); } // ------------------------------------------------------------------------- // Menu registration // ------------------------------------------------------------------------- public function register_page() { add_submenu_page( 'ai-translator-for-elementor-and-polylang', __( 'Review Translations', 'ai-translator-for-elementor-and-polylang' ), __( 'Review Translations', 'ai-translator-for-elementor-and-polylang' ), 'edit_posts', 'ai-translator-for-elementor-and-polylang-review', [ $this, 'render_page' ] ); } // ------------------------------------------------------------------------- // Asset enqueuing // ------------------------------------------------------------------------- public function enqueue_assets( $hook ) { // Only load on our review page. $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; if ( 'ai-translator-for-elementor-and-polylang-review' !== $page ) { return; } // dashboard.css provides the meep-hero / meep-reveal styles used by render_plugin_header(). wp_enqueue_style( 'aitrfoel-dashboard', AITRFOEL_PLUGIN_URL . 'assets/css/dashboard.css', [ 'aitrfoel-meep-admin' ], AITRFOEL_VERSION ); wp_enqueue_style( 'aitrfoel-review-editor', AITRFOEL_PLUGIN_URL . 'assets/css/review-editor.css', [ 'aitrfoel-dashboard' ], AITRFOEL_VERSION ); wp_enqueue_script( 'aitrfoel-review-editor', AITRFOEL_PLUGIN_URL . 'assets/js/review-editor.js', [ 'jquery' ], AITRFOEL_VERSION, true ); $post_id_for_type = absint( $_GET['post_id'] ?? 0 ); $content_type = ( $post_id_for_type && class_exists( 'AITRFOEL_Content_Detector' ) ) ? AITRFOEL_Content_Detector::get_instance()->detect( $post_id_for_type ) : 'classic'; wp_localize_script( 'aitrfoel-review-editor', 'aitrfoelReview', [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'aitrfoel_review_edit' ), 'post_id' => $post_id_for_type, 'source_id' => absint( $_GET['source_id'] ?? 0 ), 'content_type' => $content_type, 'strings' => [ 'save' => __( 'Save', 'ai-translator-for-elementor-and-polylang' ), 'cancel' => __( 'Cancel', 'ai-translator-for-elementor-and-polylang' ), 'saving' => __( 'Saving…', 'ai-translator-for-elementor-and-polylang' ), 'saved' => __( 'Saved', 'ai-translator-for-elementor-and-polylang' ), 'save_failed' => __( 'Save failed. Please try again.', 'ai-translator-for-elementor-and-polylang' ), ], ] ); } // ------------------------------------------------------------------------- // Page render // ------------------------------------------------------------------------- public function render_page() { if ( ! current_user_can( 'edit_posts' ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'ai-translator-for-elementor-and-polylang' ) ); } $source_id = absint( $_GET['source_id'] ?? 0 ); $post_id = absint( $_GET['post_id'] ?? 0 ); $lang = sanitize_text_field( $_GET['lang'] ?? '' ); if ( ! $source_id || ! $post_id ) { $this->render_selector_page(); return; } $source_post = get_post( $source_id ); $trans_post = get_post( $post_id ); if ( ! $source_post || ! $trans_post ) { wp_die( esc_html__( 'Post not found.', 'ai-translator-for-elementor-and-polylang' ) ); } $content_type = class_exists( 'AITRFOEL_Content_Detector' ) ? AITRFOEL_Content_Detector::get_instance()->detect( $post_id ) : 'classic'; $source_segments = $this->get_segments( $source_id ); $trans_segments = $this->get_segments( $post_id ); // Pair segments by structural path to guarantee 1-to-1 alignment between source & translation. $paired_segments = []; $all_paths = array_unique( array_merge( array_keys( $source_segments ), array_keys( $trans_segments ) ) ); foreach ( $all_paths as $path ) { $src = $source_segments[ $path ] ?? null; $trs = $trans_segments[ $path ] ?? null; $src_text = isset( $src['text'] ) ? $src['text'] : ''; $trs_text = isset( $trs['text'] ) ? $trs['text'] : ''; if ( trim( wp_strip_all_tags( $src_text ) ) === '' && trim( wp_strip_all_tags( $trs_text ) ) === '' ) { continue; } $paired_segments[] = [ 'path' => $path, 'source_text' => $src_text, 'trans_text' => $trs_text, 'type' => $src['type'] ?? ( $trs['type'] ?? $content_type ), 'label' => $src['label'] ?? ( $trs['label'] ?? '' ), ]; } $dashboard_url = admin_url( 'admin.php?page=ai-translator-for-elementor-and-polylang' ); $view_url = get_permalink( $post_id ); ?>
render_plugin_header( 'Review' ); } ?>

post_title ); ?>

← post_status, [ 'draft', 'pending', 'auto-draft' ], true ); ?>

$segment ) : $trs_text = $segment['trans_text']; $trs_path = $segment['path']; $src_display = wp_strip_all_tags( $segment['source_text'] ); $trs_display = wp_strip_all_tags( $trs_text ); ?>
#
*/ private function get_segments( $post_id ) { if ( ! class_exists( 'AITRFOEL_Content_Detector' ) ) { return []; } $type = AITRFOEL_Content_Detector::get_instance()->detect( $post_id ); $translation_map = []; switch ( $type ) { case 'elementor': if ( class_exists( 'AITRFOEL_Widget_Parser' ) ) { $raw = get_post_meta( $post_id, '_elementor_data', true ); if ( ! empty( $raw ) ) { $decoded = json_decode( $raw, true ); if ( is_array( $decoded ) ) { $translation_map = AITRFOEL_Widget_Parser::get_instance()->parse_for_translation( $decoded ); } } } break; case 'gutenberg': if ( class_exists( 'AITRFOEL_Gutenberg_Parser' ) ) { $translation_map = AITRFOEL_Gutenberg_Parser::get_instance()->parse_for_translation( $post_id ); } break; case 'woocommerce': $post = get_post( $post_id ); if ( $post ) { if ( ! empty( $post->post_title ) ) { $translation_map['woocommerce/title'] = [ 'original' => (string) $post->post_title, 'path' => 'woocommerce/title', 'type' => 'text', ]; } if ( ! empty( trim( (string) $post->post_excerpt ) ) ) { $translation_map['woocommerce/excerpt'] = [ 'original' => (string) $post->post_excerpt, 'path' => 'woocommerce/excerpt', 'type' => 'text', ]; } if ( 'sc_product' !== $post->post_type && ! empty( trim( (string) $post->post_content ) ) ) { $translation_map['woocommerce/content'] = [ 'original' => (string) $post->post_content, 'path' => 'woocommerce/content', 'type' => 'html', ]; } } break; case 'classic': default: if ( class_exists( 'AITRFOEL_Classic_Parser' ) ) { $translation_map = AITRFOEL_Classic_Parser::get_instance()->parse_for_translation( $post_id ); } break; } // Custom-field text (JetEngine / ACF / Meta Box / plain meta) — editable even // when the body is empty (the common case for meta-only JetEngine CPTs that // would otherwise show "no editable segments"). if ( class_exists( 'AITRFOEL_Translation_Engine' ) ) { $engine = AITRFOEL_Translation_Engine::get_instance(); if ( method_exists( $engine, 'get_translatable_meta_map' ) ) { foreach ( $engine->get_translatable_meta_map( $post_id ) as $meta_key => $meta_val ) { $translation_map[ 'meta/' . $meta_key ] = [ 'original' => (string) $meta_val, 'path' => 'meta/' . $meta_key, 'type' => 'meta', 'label' => $meta_key, ]; } } } $segments = []; foreach ( $translation_map as $key => $item ) { $text = is_array( $item['original'] ) ? implode( "\n", $item['original'] ) : (string) $item['original']; $path = $item['path'] ?? (string) $key; $segments[ $path ] = [ 'key' => $key, 'path' => $path, 'text' => $text, 'type' => $type, 'label' => $item['label'] ?? '', ]; } return $segments; } // ------------------------------------------------------------------------- // AJAX: save a single segment // ------------------------------------------------------------------------- public function handle_save_segment() { check_ajax_referer( 'aitrfoel_review_edit', 'nonce' ); if ( ! current_user_can( 'edit_posts' ) ) { wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'ai-translator-for-elementor-and-polylang' ) ] ); } $post_id = absint( $_POST['post_id'] ?? 0 ); $path = sanitize_text_field( $_POST['path'] ?? '' ); $new_text = wp_kses_post( wp_unslash( $_POST['new_text'] ?? '' ) ); $content_type = sanitize_text_field( $_POST['content_type'] ?? '' ); if ( ! $post_id || $path === '' ) { wp_send_json_error( [ 'message' => __( 'Missing parameters.', 'ai-translator-for-elementor-and-polylang' ) ] ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { wp_send_json_error( [ 'message' => __( 'Insufficient permissions for this post.', 'ai-translator-for-elementor-and-polylang' ) ] ); } // Custom-field segments (JetEngine / ACF / other meta) — identified by a // "meta/{key}" path — save straight to post meta regardless of content type. if ( strpos( $path, 'meta/' ) === 0 ) { $meta_key = substr( $path, strlen( 'meta/' ) ); if ( $meta_key === '' ) { wp_send_json_error( [ 'message' => __( 'Invalid field.', 'ai-translator-for-elementor-and-polylang' ) ] ); } update_post_meta( $post_id, $meta_key, $new_text ); wp_send_json_success( [ 'message' => __( 'Segment saved.', 'ai-translator-for-elementor-and-polylang' ) ] ); } switch ( $content_type ) { case 'elementor': $result = $this->save_elementor_segment( $post_id, $path, $new_text ); break; case 'gutenberg': $result = $this->save_gutenberg_segment( $post_id, $path, $new_text ); break; case 'woocommerce': $result = $this->save_woocommerce_segment( $post_id, $path, $new_text ); break; default: $result = $this->save_classic_segment( $post_id, $path, $new_text ); break; } if ( is_wp_error( $result ) ) { wp_send_json_error( [ 'message' => $result->get_error_message() ] ); } wp_send_json_success( [ 'message' => __( 'Segment saved.', 'ai-translator-for-elementor-and-polylang' ) ] ); } // ------------------------------------------------------------------------- // Save helpers // ------------------------------------------------------------------------- private function save_classic_segment( $post_id, $path, $new_text ) { // path format: classic/N $parts = explode( '/', $path ); $index = isset( $parts[1] ) ? absint( $parts[1] ) : -1; $post = get_post( $post_id ); if ( ! $post ) { return new WP_Error( 'not_found', __( 'Post not found.', 'ai-translator-for-elementor-and-polylang' ) ); } if ( ! class_exists( 'AITRFOEL_Classic_Parser' ) ) { return new WP_Error( 'no_parser', __( 'Classic parser unavailable.', 'ai-translator-for-elementor-and-polylang' ) ); } $chunks = AITRFOEL_Classic_Parser::get_instance()->extract_text_from_html( $post->post_content ); if ( $index < 0 || ! isset( $chunks[ $index ] ) ) { return new WP_Error( 'invalid_index', __( 'Segment not found.', 'ai-translator-for-elementor-and-polylang' ) ); } $old_chunk = $chunks[ $index ]; $new_content = str_replace( $old_chunk, $new_text, $post->post_content ); $result = wp_update_post( [ 'ID' => $post_id, 'post_content' => $new_content ], true ); return is_wp_error( $result ) ? $result : true; } private function save_gutenberg_segment( $post_id, $path, $new_text ) { $post = get_post( $post_id ); if ( ! $post ) { return new WP_Error( 'not_found', __( 'Post not found.', 'ai-translator-for-elementor-and-polylang' ) ); } if ( ! class_exists( 'AITRFOEL_Gutenberg_Parser' ) ) { return new WP_Error( 'no_parser', __( 'Gutenberg parser unavailable.', 'ai-translator-for-elementor-and-polylang' ) ); } // Look up the old text via the parser, then do a targeted replacement $translation_map = AITRFOEL_Gutenberg_Parser::get_instance()->parse_for_translation( $post_id ); $old_text = null; foreach ( $translation_map as $item ) { if ( ( $item['path'] ?? '' ) === $path ) { $old_text = $item['original']; break; } } if ( $old_text === null ) { return new WP_Error( 'not_found', __( 'Segment not found in Gutenberg content.', 'ai-translator-for-elementor-and-polylang' ) ); } $new_content = str_replace( $old_text, $new_text, $post->post_content ); $result = wp_update_post( [ 'ID' => $post_id, 'post_content' => $new_content ], true ); return is_wp_error( $result ) ? $result : true; } private function save_elementor_segment( $post_id, $path, $new_text ) { $raw = get_post_meta( $post_id, '_elementor_data', true ); if ( empty( $raw ) ) { return new WP_Error( 'no_data', __( 'No Elementor data found.', 'ai-translator-for-elementor-and-polylang' ) ); } $data = json_decode( $raw, true ); if ( ! is_array( $data ) ) { return new WP_Error( 'bad_json', __( 'Elementor data could not be decoded.', 'ai-translator-for-elementor-and-polylang' ) ); } // Navigate the dot-path and write the value directly. // This bypasses apply_translations() whose validate_elementor_structure() // guard can silently return the original array on validation failure. $keys = explode( '.', $path ); $current = &$data; $last = array_pop( $keys ); foreach ( $keys as $key ) { $k = is_numeric( $key ) ? (int) $key : $key; if ( ! isset( $current[ $k ] ) || ! is_array( $current[ $k ] ) ) { return new WP_Error( 'bad_path', __( 'Segment path not found in Elementor data.', 'ai-translator-for-elementor-and-polylang' ) ); } $current = &$current[ $k ]; } $last_key = is_numeric( $last ) ? (int) $last : $last; if ( ! array_key_exists( $last_key, $current ) ) { return new WP_Error( 'bad_path', __( 'Segment path not found in Elementor data.', 'ai-translator-for-elementor-and-polylang' ) ); } $current[ $last_key ] = $new_text; $encoded = wp_json_encode( $data ); if ( false === $encoded ) { return new WP_Error( 'encode_fail', __( 'Failed to encode Elementor data.', 'ai-translator-for-elementor-and-polylang' ) ); } update_post_meta( $post_id, '_elementor_data', wp_slash( $encoded ) ); // Clear Elementor's file-based cache so the frontend reflects the change immediately. delete_post_meta( $post_id, '_elementor_css' ); if ( class_exists( '\Elementor\Plugin' ) && isset( \Elementor\Plugin::$instance->files_manager ) ) { \Elementor\Plugin::$instance->files_manager->clear_cache(); } clean_post_cache( $post_id ); return true; } private function save_woocommerce_segment( $post_id, $path, $new_text ) { $post = get_post( $post_id ); if ( ! $post ) { return new WP_Error( 'not_found', __( 'Post not found.', 'ai-translator-for-elementor-and-polylang' ) ); } switch ( $path ) { case 'woocommerce/title': $result = wp_update_post( [ 'ID' => $post_id, 'post_title' => wp_strip_all_tags( $new_text ), ], true ); break; case 'woocommerce/excerpt': $result = wp_update_post( [ 'ID' => $post_id, 'post_excerpt' => $new_text, ], true ); break; case 'woocommerce/content': $result = wp_update_post( [ 'ID' => $post_id, 'post_content' => $new_text, ], true ); break; default: return new WP_Error( 'bad_path', __( 'Segment path not found for WooCommerce content.', 'ai-translator-for-elementor-and-polylang' ) ); } if ( is_wp_error( $result ) ) { return $result; } clean_post_cache( $post_id ); return true; } /** * Render translation selector screen when no post_id is provided. */ public function render_selector_page() { $admin_menu = class_exists( 'AITRFOEL_Admin_Menu' ) ? AITRFOEL_Admin_Menu::get_instance() : null; $pages_data = []; if ( $admin_menu ) { if ( method_exists( $admin_menu, 'get_pages_with_translations' ) ) { $pages_data = $admin_menu->get_pages_with_translations(); } elseif ( method_exists( $admin_menu, 'get_all_pages_with_translations' ) ) { $pages_data = $admin_menu->get_all_pages_with_translations(); } } // Filter for items that have at least one translation $translated_items = []; foreach ( $pages_data as $group ) { if ( ! empty( $group['translations'] ) ) { $translated_items[] = $group; } } ?>
render_plugin_header( __( 'Review Translations', 'ai-translator-for-elementor-and-polylang' ) ); } ?>

post_type ); if ( $src->post_type === 'elementor-hf' ) { $pt_label = 'Header/Footer'; } elseif ( $src->post_type === 'elementor_library' ) { $pt_label = 'Elementor Template'; } elseif ( strpos( $src->post_type, 'form' ) !== false ) { $pt_label = 'Form'; } ?>
post_title ); ?>
ID #ID ); ?> · ID ) ); ?>
$tpost ) : $review_url = admin_url( sprintf( 'admin.php?page=ai-translator-for-elementor-and-polylang-review&source_id=%d&post_id=%d&lang=%s', $src->ID, $tpost->ID, urlencode( $tlang ) ) ); ?> (post_status ); ?>)