0 && get_post( $candidate_id ) && current_user_can( 'edit_post', $candidate_id ) ) {
$from_post_id = $candidate_id;
}
}
}
// 2. If not resolved from Polylang, verify from_post query parameter with valid nonce and permission checks
if ( ! $from_post_id && isset( $_GET['from_post'] ) && ! empty( $_GET['from_post'] ) ) {
$nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_key( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
$is_valid_nonce = false;
if ( ! empty( $nonce ) ) {
if (
wp_verify_nonce( $nonce, 'new-post-translation' )
|| wp_verify_nonce( $nonce, 'update-post_' . $post_id )
|| wp_verify_nonce( $nonce, 'add-post' )
) {
$is_valid_nonce = true;
}
}
if ( $is_valid_nonce ) {
$candidate_id = absint( wp_unslash( $_GET['from_post'] ) );
if ( $candidate_id > 0 && get_post( $candidate_id ) && current_user_can( 'edit_post', $candidate_id ) ) {
$from_post_id = $candidate_id;
}
}
}
if ( $from_post_id > 0 ) {
update_post_meta( $post_id, '_translatesense_source_id', $from_post_id );
update_post_meta( $post_id, '_show_initial_prompt_modal', '1' );
}
}
/**
* Detect editor type for a given post ID.
*
* @param int $post_id
* @return string 'elementor' | 'gutenberg' | 'classic'
*/
public static function get_post_editor_type( $post_id ) {
if ( ! $post_id ) {
return 'classic';
}
// 1. Check Elementor
$elementor_edit_mode = get_post_meta( $post_id, '_elementor_edit_mode', true );
if ( 'builder' === $elementor_edit_mode ) {
return 'elementor';
}
$post = get_post( $post_id );
if ( ! $post instanceof \WP_Post ) {
return 'classic';
}
if ( function_exists( 'use_block_editor_for_post' ) ) {
if ( ! use_block_editor_for_post( $post ) ) {
return 'classic';
}
}
// 2. Check Gutenberg (has blocks)
if ( $post && function_exists( 'has_blocks' ) && has_blocks( $post->post_content ) ) {
return 'gutenberg';
}
return 'classic';
}
/**
* Get appropriate edit link based on post editor type (Elementor, Gutenberg, Classic).
*
* @param int $post_id
* @return string
*/
public static function get_post_edit_link( $post_id ): string {
if ( ! $post_id ) {
return '';
}
$editor_type = self::get_post_editor_type( (int) $post_id );
if ( 'elementor' === $editor_type ) {
if ( class_exists( '\Elementor\Plugin' ) && isset( \Elementor\Plugin::$instance->documents ) ) {
$doc = \Elementor\Plugin::$instance->documents->get( $post_id );
if ( $doc && method_exists( $doc, 'get_edit_url' ) ) {
return (string) $doc->get_edit_url();
}
}
return admin_url( 'post.php?post=' . (int) $post_id . '&action=elementor' );
}
$default_edit_link = get_edit_post_link( $post_id, 'raw' );
return $default_edit_link ? (string) $default_edit_link : admin_url( 'post.php?post=' . (int) $post_id . '&action=edit' );
}
/**
* Check if post is already translated.
*
* @param int $post_id
* @return bool
*/
public static function is_post_already_translated( int $post_id ): bool {
if ( ! $post_id ) {
return false;
}
$is_translated = get_post_meta( $post_id, '_translatesense_translated', true );
if ( ! empty( $is_translated ) && $post_id === (int) $is_translated ) {
return true;
}
return false;
}
/**
* Enqueue lightweight Editor Modal assets when source post was created in Elementor
* but target translation is opened in Gutenberg or Classic Editor.
*
* Skips loading heavy universal page editor translation assets.
*
* @param int $post_id
* @param int $source_post_id
* @param string $editor_type
* @param array $localize_data
*/
public static function enqueue_editor_mismatch_modal_assets( int $post_id, int $source_post_id, string $editor_type = 'gutenberg', array $localize_data = array() ): void {
if ( ! $post_id || ! $source_post_id ) {
return;
}
// Enqueue minimal CSS for modal rendering
wp_enqueue_style(
'translatesense-common',
esc_url( TRANSLATESENSE_PLUGIN_URL . 'assets/css/common' . sanitize_text_field( TRANSLATESENSE_ASSETS_EXT ) . '.css' ),
array(),
TRANSLATESENSE_VERSION
);
$asset_file = TRANSLATESENSE_PLUGIN_DIR . 'assets/build/page/index.asset.php';
$version = file_exists( $asset_file ) ? ( require $asset_file )['version'] : TRANSLATESENSE_VERSION;
wp_enqueue_style(
'translatesense-editor-style',
esc_url( TRANSLATESENSE_PLUGIN_URL . 'assets/build/page/index.css' ),
array( 'translatesense-common' ),
$version
);
$assets_ext = sanitize_text_field( TRANSLATESENSE_ASSETS_EXT );
$script_url = TRANSLATESENSE_PLUGIN_URL . 'assets/js/editor/editor-modal' . $assets_ext . '.js';
wp_enqueue_script(
'translatesense-editor-modal',
esc_url( $script_url ),
array(),
TRANSLATESENSE_VERSION,
true
);
$show_initial_prompt_modal = get_post_meta( $post_id, '_show_initial_prompt_modal', true );
$source_language = Polylang_Bridge::get_post_language( $source_post_id );
$target_language = Polylang_Bridge::get_post_language( $post_id );
$modal_data = array_merge(
array(
'postId' => $post_id,
'sourcePostId' => $source_post_id,
'sourceEditorType' => 'elementor',
'editorType' => $editor_type,
'currentEditorType' => $editor_type,
'sourceLanguage' => $source_language,
'targetLanguage' => $target_language,
'languages' => Polylang_Bridge::get_languages(),
'showInitialPromptModal' => ( '1' === (string) $show_initial_prompt_modal ),
'pluginUrl' => TRANSLATESENSE_PLUGIN_URL,
'restUrl' => esc_url_raw( rest_url( 'translatesense/v1/' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
),
$localize_data
);
wp_localize_script(
'translatesense-editor-modal',
'TranslateSenseEditorModalData',
$modal_data
);
}
/**
* Enqueue Universal Editor Assets (Helper).
*/
public static function enqueue_page_editor_assets( $post_id, $editor_type = 'gutenberg', $localize_data = array() ) {
if ( ! $post_id || self::is_post_already_translated( (int) $post_id ) ) {
return;
}
$source_post_id = self::get_source_post_id( $post_id );
if ( ! $source_post_id ) {
return;
}
$source_editor_type = $source_post_id ? self::get_post_editor_type( $source_post_id ) : '';
// If source page was created in Elementor but target translation is opened in Gutenberg or Classic editor:
// Do not load full page editor translation assets. Instead, load lightweight editorModal.js.
if ( 'elementor' === $source_editor_type && 'elementor' !== $editor_type ) {
self::enqueue_editor_mismatch_modal_assets( (int) $post_id, (int) $source_post_id, $editor_type, $localize_data );
return;
}
$settings = get_option( 'translatesense_settings', array() );
$enable_providers = $settings['enabled_providers'];
$active_providers = array();
if ( class_exists( Ai_Connector_Bridge::class ) ) {
$active_providers = Ai_Connector_Bridge::get_instance()->get_active_providers();
foreach ( $active_providers as $key => $value ) {
if ( ! in_array( $key, $enable_providers, true ) ) {
unset( $active_providers[ $key ] );
}
}
}
$asset_file = TRANSLATESENSE_PLUGIN_DIR . 'assets/build/page/index.asset.php';
$asset = file_exists( $asset_file )
? require $asset_file
: array(
'dependencies' => array( 'react', 'wp-element' ),
'version' => TRANSLATESENSE_VERSION,
);
wp_enqueue_style(
'translatesense-common',
TRANSLATESENSE_PLUGIN_URL . 'assets/css/common' . sanitize_text_field( TRANSLATESENSE_ASSETS_EXT ) . '.css',
array(),
TRANSLATESENSE_VERSION
);
wp_enqueue_style(
'translatesense-editor',
TRANSLATESENSE_PLUGIN_URL . 'assets/build/page/index.css',
array( 'translatesense-common' ),
$asset['version']
);
// 1. Spark MD5 (static library version. so no change).
wp_enqueue_script(
'translatesense-sparkmd',
TRANSLATESENSE_PLUGIN_URL . 'assets/js/vendor/spark-md5/index.min.js',
array(),
'3.0.2',
true
);
// 7. Universal Editor React App Bundle
$editor_deps = array_merge(
array( 'translatesense-sparkmd' ),
$asset['dependencies']
);
$editor_script_url = TRANSLATESENSE_PLUGIN_URL . 'assets/build/page/index.js';
wp_enqueue_script(
'translatesense-editor',
$editor_script_url,
$editor_deps,
$asset['version'],
true
);
// 3. Chrome Built-in Translate Provider & Languages
wp_enqueue_script(
'translatesense-chrome-languages',
TRANSLATESENSE_PLUGIN_URL . 'assets/js/translation/providers/chrome-ai/chrome-languages' . sanitize_text_field( TRANSLATESENSE_ASSETS_EXT ) . '.js',
array( 'translatesense-editor' ),
TRANSLATESENSE_VERSION,
true
);
wp_enqueue_script(
'translatesense-chrome-ai',
TRANSLATESENSE_PLUGIN_URL . 'assets/js/translation/providers/chrome-ai/chrome-built-translate' . sanitize_text_field( TRANSLATESENSE_ASSETS_EXT ) . '.js',
array( 'translatesense-chrome-languages' ),
TRANSLATESENSE_VERSION,
true
);
// 4. AI Translate Provider (OpenAI & Gemini)
wp_enqueue_script(
'translatesense-ai',
TRANSLATESENSE_PLUGIN_URL . 'assets/js/translation/providers/ai-services/ai-translate' . sanitize_text_field( TRANSLATESENSE_ASSETS_EXT ) . '.js',
array( 'translatesense-editor' ),
TRANSLATESENSE_VERSION,
true
);
// 5. Providers Aggregator / Register
wp_enqueue_script(
'translatesense-providers',
TRANSLATESENSE_PLUGIN_URL . 'assets/js/translation/providers/index' . sanitize_text_field( TRANSLATESENSE_ASSETS_EXT ) . '.js',
array( 'translatesense-chrome-ai', 'translatesense-ai' ),
TRANSLATESENSE_VERSION,
true
);
// 6. Live Editor Updaters (Meta fields, Gutenberg, Elementor, Classic)
wp_enqueue_script(
'translatesense-live-meta-fields',
TRANSLATESENSE_PLUGIN_URL . 'assets/js/editor/meta-fields' . sanitize_text_field( TRANSLATESENSE_ASSETS_EXT ) . '.js',
array(),
TRANSLATESENSE_VERSION,
true
);
wp_enqueue_script(
'translatesense-live-' . esc_attr( $editor_type ),
TRANSLATESENSE_PLUGIN_URL . 'assets/js/editor/' . esc_attr( $editor_type ) . sanitize_text_field( TRANSLATESENSE_ASSETS_EXT ) . '.js',
array( 'translatesense-live-meta-fields' ),
TRANSLATESENSE_VERSION,
true
);
wp_enqueue_script(
'translatesense-page-editor',
TRANSLATESENSE_PLUGIN_URL . 'assets/js/editor/index' . sanitize_text_field( TRANSLATESENSE_ASSETS_EXT ) . '.js',
array( 'translatesense-editor', 'translatesense-live-meta-fields', 'translatesense-live-' . esc_attr( $editor_type ) ),
TRANSLATESENSE_VERSION,
true
);
$show_initial_prompt_modal = get_post_meta( $post_id, '_show_initial_prompt_modal', true );
$source_language = $source_post_id ? Polylang_Bridge::get_post_language( $source_post_id ) : '';
$source_editor_type = $source_post_id ? self::get_post_editor_type( $source_post_id ) : '';
$localize_data = array_merge(
array(
'postId' => $post_id,
'editorType' => $editor_type,
'sourcePostId' => $source_post_id,
'sourceLanguage' => $source_language,
'sourceEditorType' => $source_editor_type,
'fromPost' => $source_post_id,
'restUrl' => esc_url_raw( rest_url( 'translatesense/v1/' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'polylangActive' => Polylang_Bridge::is_polylang_active(),
'targetLanguage' => Polylang_Bridge::get_post_language( $post_id ),
'languages' => Polylang_Bridge::get_languages(),
'defaultProvider' => $settings['provider'] ?? '',
'enabledProviders' => $active_providers,
'pluginUrl' => TRANSLATESENSE_PLUGIN_URL,
'docsUrl' => defined( 'TRANSLATESENSE_DOCS_URL' ) ? TRANSLATESENSE_DOCS_URL : 'https://docs.dragwyb.com/',
'showInitialPromptModal' => ( '1' === (string) $show_initial_prompt_modal ),
'settingsUrl' => esc_url_raw( Admin_Menu::get_dashboard_url( 'settings' ) ),
'connectorsUrl' => esc_url_raw( admin_url( 'options-connectors.php' ) ),
'hasOpenAiKey' => ! empty( $settings['openai_api_key'] ),
'hasGeminiKey' => ! empty( $settings['gemini_api_key'] ),
),
$localize_data
);
wp_localize_script(
'translatesense-editor',
'TranslateSenseEditorData',
$localize_data
);
}
/**
* Enqueue Gutenberg editor assets (only for Polylang translated post types).
*/
public static function enqueue_block_editor_assets() {
global $post;
if ( ! $post ) {
return;
}
if ( ! function_exists( 'get_current_screen' ) ) {
return;
}
if ( ! property_exists( get_current_screen(), 'is_block_editor' ) ) {
return;
}
if ( ! get_current_screen()->is_block_editor ) {
return;
}
$post_id = (int) $post->ID;
self::maybe_enqueue_block_editor_assets( $post_id );
if ( ! self::is_translate_current_post() ) {
return;
}
if ( self::is_post_already_translated( $post_id ) ) {
return;
}
self::enqueue_page_editor_assets( $post_id, 'gutenberg' );
}
/**
* Maybe enqueue Gutenberg Block Processor assets based on strict post meta condition.
*
* Only loads if BOTH '_translatesense_translated' and 'block process meta are present,
* the post has Gutenberg blocks, and the user has permission to edit the post.
*
* @param int $post_id Post ID.
*/
public static function maybe_enqueue_block_editor_assets( int $post_id ): void {
if ( $post_id <= 0 ) {
return;
}
$is_translated = get_post_meta( $post_id, '_translatesense_translated', true );
$is_block_state_pending = Block_Processor::is_block_state_pending( $post_id );
// Strictly check that BOTH conditions are fulfilled
if ( empty( $is_translated ) || '0' === (string) $is_translated || ! $is_block_state_pending ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
self::enqueue_block_state_editor_assets( $post_id );
}
/**
* Enqueue dedicated Block Processor CSS and JS assets.
*
* @param int $post_id Post ID.
*/
public static function enqueue_block_state_editor_assets( int $post_id ): void {
$assets_ext = sanitize_text_field( TRANSLATESENSE_ASSETS_EXT );
wp_enqueue_script(
'translatesense-block-editor',
TRANSLATESENSE_PLUGIN_URL . 'assets/js/translation/block-editor' . $assets_ext . '.js',
array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-data', 'wp-editor', 'wp-edit-post', 'wp-i18n', 'wp-notices', 'wp-plugins' ),
TRANSLATESENSE_VERSION,
true
);
wp_localize_script(
'translatesense-block-editor',
'TranslateSenseProcessorData',
array(
'postId' => $post_id,
'isPending' => true,
'restUrl' => esc_url_raw( rest_url( 'translatesense/v1/' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'pluginUrl' => TRANSLATESENSE_PLUGIN_URL,
)
);
}
/**
* Enqueue Classic Editor & Post Edit Admin Assets (only for Polylang translated post types).
*/
public static function enqueue_admin_editor_assets( $hook ) {
if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) {
return;
}
global $post;
if ( ! self::is_translate_current_post() ) {
return;
}
$current_screen = get_current_screen();
$post_id = (int) $post->ID;
if ( method_exists( $current_screen, 'is_block_editor' ) && ! $current_screen->is_block_editor() ) {
self::enqueue_page_editor_assets( $post_id, 'classic' );
}
}
/**
* Enqueue Elementor editor scripts (only for Polylang translated post types).
*/
public static function enqueue_elementor_editor_assets() {
$post_id = (int) get_the_ID();
if ( ! $post_id || self::is_post_already_translated( $post_id ) ) {
return;
}
if ( ! self::is_translate_current_post() ) {
return;
}
self::enqueue_page_editor_assets( $post_id, 'elementor' );
}
/**
* Get the source post ID for translation.
* Read-only resolution — never modifies the database during read/GET requests.
*
* @param int $post_id
* @return int
*/
public static function get_source_post_id( $post_id ): int {
$post_id = (int) $post_id;
if ( $post_id <= 0 ) {
return 0;
}
// 1. Check saved post meta
$source_post_id = (int) get_post_meta( $post_id, '_translatesense_source_id', true );
if ( $source_post_id > 0 && get_post( $source_post_id ) && current_user_can( 'edit_post', $source_post_id ) ) {
return $source_post_id;
}
// 2. Check Polylang default language translation
if ( Polylang_Bridge::is_polylang_active() ) {
$translations = Polylang_Bridge::get_post_translations( $post_id );
$default_lang = Polylang_Bridge::get_default_language();
if ( ! empty( $translations[ $default_lang ] ) && (int) $translations[ $default_lang ] !== $post_id ) {
$candidate_id = (int) $translations[ $default_lang ];
if ( $candidate_id > 0 && get_post( $candidate_id ) && current_user_can( 'edit_post', $candidate_id ) ) {
return $candidate_id;
}
}
foreach ( $translations as $lang => $trans_id ) {
$trans_id = (int) $trans_id;
if ( $trans_id > 0 && $trans_id !== $post_id && get_post( $trans_id ) && current_user_can( 'edit_post', $trans_id ) ) {
return $trans_id;
}
}
}
// 3. Fallback to query parameter only with valid nonce and permission verification
if ( isset( $_GET['from_post'] ) && ! empty( $_GET['from_post'] ) && isset( $_GET['_wpnonce'] ) ) {
$nonce = sanitize_key( wp_unslash( $_GET['_wpnonce'] ) );
if ( wp_verify_nonce( $nonce, 'new-post-translation' ) ) {
$candidate_id = absint( wp_unslash( $_GET['from_post'] ) );
if ( $candidate_id > 0 && get_post( $candidate_id ) && current_user_can( 'edit_post', $candidate_id ) ) {
return $candidate_id;
}
}
}
return 0;
}
private static function is_translate_current_post() {
global $post;
if ( ! $post instanceof \WP_Post ) {
return false;
}
$post_types = Polylang_Bridge::get_translated_post_types();
if ( ! is_array( $post_types ) || ! in_array( $post->post_type, $post_types, true ) ) {
return false;
}
$post_id = (int) $post->ID;
if ( self::is_post_already_translated( $post_id ) ) {
return false;
}
$current_lang = Polylang_Bridge::get_post_language( $post_id );
$default_lang = Polylang_Bridge::get_default_language( $post_id );
if ( ! $current_lang || empty( $current_lang ) || $current_lang === $default_lang ) {
return false;
}
return true;
}
/**
* Add TranslateSense Meta Box only for post types translated/supported by Polylang.
*/
public static function add_translatesense_meta_box() {
global $post;
if ( ! $post instanceof \WP_Post ) {
return;
}
if ( empty( $post->post_type ) ) {
return;
}
if ( function_exists( 'get_current_screen' ) ) {
$screen = get_current_screen();
if ( $screen && property_exists( $screen, 'is_block_editor' ) && ! $screen->is_block_editor ) {
return;
}
}
if ( ! self::is_translate_current_post() ) {
return;
}
add_meta_box(
'translatesense-editor-metabox',
__( 'TranslateSense', 'translatesense' ),
array( __CLASS__, 'render_editor_meta_box' ),
$post->post_type,
'side',
'high'
);
}
/**
* Render Meta Box HTML for Gutenberg and Classic Editor.
*/
public static function render_editor_meta_box( $post ) {
$post_id = (int) $post->ID;
$source_post_id = self::get_source_post_id( $post_id );
if ( ! $source_post_id ) {
return;
}
$current_lang = Polylang_Bridge::get_post_language( $post_id );
$source_lang = Polylang_Bridge::get_post_language( $source_post_id );
$polylang_lang = Polylang_Bridge::get_languages();
$source_lang_flag = '';
$current_lang_flag = '';
if ( ! empty( $polylang_lang[ $current_lang ]['flag'] ) ) {
$current_lang_flag = sprintf( '', esc_url( $polylang_lang[ $current_lang ]['flag'] ), esc_attr( $polylang_lang[ $current_lang ]['name'] ?? '' ) );
}
if ( ! empty( $polylang_lang[ $source_lang ]['flag'] ) ) {
$source_lang_flag = sprintf( '
', esc_url( $polylang_lang[ $source_lang ]['flag'] ), esc_attr( $polylang_lang[ $source_lang ]['name'] ?? '' ) );
}
?>