, 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' );
}
?>
| # |
|
|
$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 ); ?>)
|