get_license_info(); $builtin_active = class_exists('AITRFOEL_Builtin_Integration') && AITRFOEL_Builtin_Integration::is_enabled(); wp_localize_script('aitrfoel-translate-all', 'aitrfoel_translate_all', [ 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aitrfoel_translate_all_nonce'), 'ajax_nonce' => wp_create_nonce('aitrfoel_ajax_nonce'), 'builtin_active' => $builtin_active, 'plan' => $license_info['plan'], 'word_limit' => $license_info['word_limit'], 'words_used' => $license_info['words_used'], 'words_left' => $license_info['words_left'], 'server_usage_factor' => self::SERVER_USAGE_FACTOR, 'upgrade_url' => $this->get_upgrade_url(), 'is_pro' => (defined('AITRFOEL_VERSION_TYPE') && AITRFOEL_VERSION_TYPE === 'pro'), 'strings' => [ 'scanning' => __('Scanning content...', 'ai-translator-for-elementor-and-polylang'), 'scan_complete' => __('Scan complete', 'ai-translator-for-elementor-and-polylang'), 'no_content' => __('No translatable content found for the selected types.', 'ai-translator-for-elementor-and-polylang'), 'confirm_start' => __('Start translation? This will translate all selected content.', 'ai-translator-for-elementor-and-polylang'), 'translating' => __('Translating...', 'ai-translator-for-elementor-and-polylang'), 'completed' => __('Translation completed!', 'ai-translator-for-elementor-and-polylang'), 'cancelled' => __('Translation cancelled.', 'ai-translator-for-elementor-and-polylang'), 'error' => __('An error occurred.', 'ai-translator-for-elementor-and-polylang'), 'words' => __('words', 'ai-translator-for-elementor-and-polylang'), 'pages' => __('pages', 'ai-translator-for-elementor-and-polylang'), 'posts' => __('posts', 'ai-translator-for-elementor-and-polylang'), 'items' => __('items', 'ai-translator-for-elementor-and-polylang'), 'exceeds_limit' => __('Total words exceed your plan limit.', 'ai-translator-for-elementor-and-polylang'), 'upgrade_prompt' => __('Upgrade your plan for more words or use scheduled translation.', 'ai-translator-for-elementor-and-polylang'), 'schedule_info' => __('Scheduled translation will process content over multiple billing cycles using your monthly word allowance.', 'ai-translator-for-elementor-and-polylang'), 'select_post_type' => __('Please select at least one content type.', 'ai-translator-for-elementor-and-polylang'), 'select_language' => __('Please select at least one target language.', 'ai-translator-for-elementor-and-polylang'), 'processing' => __('Processing...', 'ai-translator-for-elementor-and-polylang'), 'of' => __('of', 'ai-translator-for-elementor-and-polylang'), 'estimated_time' => __('Estimated time remaining', 'ai-translator-for-elementor-and-polylang'), 'cron_scheduled' => __('Scheduled translation started! It will process content using your monthly word allowance.', 'ai-translator-for-elementor-and-polylang'), 'already_translated' => __('already translated', 'ai-translator-for-elementor-and-polylang'), 'needs_translation' => __('needs translation', 'ai-translator-for-elementor-and-polylang'), 'translate_all_now' => __('Translate All Now', 'ai-translator-for-elementor-and-polylang'), 'translate_selected_now' => __('Translate Selected (%d) Now', 'ai-translator-for-elementor-and-polylang'), // Quota check panel 'good_news' => __('Good news!', 'ai-translator-for-elementor-and-polylang'), /* translators: 1: estimated word count, 2: words remaining */ 'quota_fits' => __('Your selection will use an estimated %1$s words of API quota and you have %2$s words remaining.', 'ai-translator-for-elementor-and-polylang'), 'can_translate_now' => __('You can translate everything now!', 'ai-translator-for-elementor-and-polylang'), 'already_done' => __('All content is already translated. Nothing to do!', 'ai-translator-for-elementor-and-polylang'), 'exceeds_allowance' => __('Content exceeds your current word allowance', 'ai-translator-for-elementor-and-polylang'), /* translators: 1: estimated word count, 2: words remaining */ 'quota_exceeds' => __('Your selection will use an estimated %1$s words of API quota, but you only have %2$s words remaining this month.', 'ai-translator-for-elementor-and-polylang'), 'translate_now_partial' => __('Translate Now (Partial)', 'ai-translator-for-elementor-and-polylang'), /* translators: %s: words remaining (formatted number) */ 'translate_partial_desc' => __('Translate as much as possible with your remaining %s words.', 'ai-translator-for-elementor-and-polylang'), 'translation_failed' => __('Translation failed.', 'ai-translator-for-elementor-and-polylang'), // Review prompt 'enjoying_plugin' => __('Enjoying Meep AI Translator? A quick review helps us a lot!', 'ai-translator-for-elementor-and-polylang'), 'leave_review' => __('Leave a Review', 'ai-translator-for-elementor-and-polylang'), 'no_thanks' => __('No thanks', 'ai-translator-for-elementor-and-polylang'), ], ]); } /** * Get license/usage info */ private function get_license_info() { if (class_exists('AITRFOEL_License_Manager_Server')) { $license_manager = AITRFOEL_License_Manager_Server::get_instance(); return $license_manager->get_license_status(); } return [ 'plan' => 'Free', 'word_limit' => 7200, 'words_used' => 0, 'words_left' => 7200, ]; } /** * Get upgrade URL */ private function get_upgrade_url() { if (function_exists('eatp_fs') && is_object(eatp_fs()) && method_exists(eatp_fs(), 'is_registered') && eatp_fs()->is_registered()) { return eatp_fs()->get_upgrade_url(); } return admin_url('admin.php?page=ai-translator-for-elementor-and-polylang-pricing'); } /** * Render the Translate All admin page */ public function render_page() { $this->enqueue_assets(); // Get multilingual info $multilingual = class_exists('AITRFOEL_Multilingual_Manager') ? AITRFOEL_Multilingual_Manager::get_instance() : null; $languages = []; $default_lang = ''; if ($multilingual && $multilingual->is_active()) { $languages = $multilingual->get_available_languages(); if (method_exists($multilingual, 'get_default_language')) { $default_lang = $multilingual->get_default_language(); } } // Get license info $license_info = $this->get_license_info(); // Check if there's a running cron job $cron_status = get_option('aitrfoel_translate_all_cron_status', []); ?>
render_plugin_header(__('Translate All', 'ai-translator-for-elementor-and-polylang')); ?>

0 && $license_info['word_limit'] !== -1) { $usage_pct = min(100, round(($license_info['words_used'] / $license_info['word_limit']) * 100)); } ?>
%

get_translatable_post_types(); foreach ($post_types as $type_slug => $type_info) : $precheck_type = ($type_slug === 'page' && !empty($type_info['count'])); ?>

$lang) { if ($code === $default_lang) { continue; } $target_languages_map[$code] = is_object($lang) ? ($lang->name ?? strtoupper($code)) : (is_array($lang) ? ($lang['name'] ?? strtoupper($code)) : strtoupper($code)); } } ?>
$name) : ?>
'Spanish (Español)', 'fr' => 'French (Français)', 'de' => 'German (Deutsch)', 'it' => 'Italian (Italiano)', 'nl' => 'Dutch (Nederlands)', 'pl' => 'Polish (Polski)', 'pt' => 'Portuguese (Português)', 'uk' => 'Ukrainian (Українська)', 'ru' => 'Russian (Русский)', 'ar' => 'Arabic (العربية)', 'zh' => 'Chinese (中文)', 'ja' => 'Japanese (日本語)', 'en' => 'English', 'ro' => 'Romanian (Română)', 'cs' => 'Czech (Čeština)', 'sv' => 'Swedish (Svenska)', 'da' => 'Danish (Dansk)', 'fi' => 'Finnish (Suomi)', 'no' => 'Norwegian (Norsk)', 'tr' => 'Turkish (Türkçe)', 'el' => 'Greek (Ελληνικά)', 'hu' => 'Hungarian (Magyar)', 'hr' => 'Croatian (Hrvatski)', 'bg' => 'Bulgarian (Български)', 'sk' => 'Slovak (Slovenčina)', 'sl' => 'Slovenian (Slovenščina)', 'he' => 'Hebrew (עברית)', 'hi' => 'Hindi (हिन्दी)', 'id' => 'Indonesian (Bahasa Indonesia)', 'ko' => 'Korean (한국어)', 'vi' => 'Vietnamese (Tiếng Việt)', 'th' => 'Thai (ไทย)', ]; $ta_nonce = wp_create_nonce('aitrfoel_ajax_nonce'); ?>
+
render_quick_add_language_card($multilingual); } ?>

' . esc_html(strtoupper($default_lang)) . ''); ?>

render_quick_tips(); } ?>
get_default_language(); foreach ($builtin->get_available_languages() as $l) { if ($l->code !== $default_lang) { $non_default_langs[] = $l->code; } } } // Pages $types['page'] = [ 'label' => __('Pages', 'ai-translator-for-elementor-and-polylang'), 'icon' => 'page', 'count' => $this->count_default_language_posts('page', $non_default_langs), ]; // Posts $types['post'] = [ 'label' => __('Posts', 'ai-translator-for-elementor-and-polylang'), 'icon' => 'post', 'count' => $this->count_default_language_posts('post', $non_default_langs), ]; // Custom post types that support editor or title (including dynamic tag sources like Testimonials & Employees) $exclude_system_types = [ 'attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block', 'wp_navigation', 'wp_template', 'wp_template_part', 'wp_global_styles', 'elementor_library' ]; $custom_types = get_post_types([ 'show_ui' => true, '_builtin' => false, ], 'objects'); $known_labels = [ 'page' => __('Pages', 'ai-translator-for-elementor-and-polylang'), 'post' => __('Posts', 'ai-translator-for-elementor-and-polylang'), 'product' => __('Products', 'ai-translator-for-elementor-and-polylang'), 'sureforms_form' => __('SureForms', 'ai-translator-for-elementor-and-polylang'), 'jet-form-builder' => __('JetFormBuilder Forms', 'ai-translator-for-elementor-and-polylang'), 'elementor-hf' => __('Header & Footer Builder', 'ai-translator-for-elementor-and-polylang'), 'jet-theme-core' => __('JetThemeCore Templates', 'ai-translator-for-elementor-and-polylang'), 'jet-engine' => __('JetEngine Listings', 'ai-translator-for-elementor-and-polylang'), 'page-generator-pro' => __('Page Generator Pro', 'ai-translator-for-elementor-and-polylang'), ]; foreach ($custom_types as $cpt) { if (in_array($cpt->name, $exclude_system_types, true)) { continue; } // Include CPTs that support the editor OR at least a title. JetEngine // (and similar) CPTs often disable editor support and store their content // in custom meta fields, but still register a title — they are translatable // (title + JetEngine meta fields). This mirrors the dashboard, which lists // every public CPT. Truly headless types (no editor, no title) are skipped. if (!post_type_supports($cpt->name, 'editor') && !post_type_supports($cpt->name, 'title')) { continue; } $count = $this->count_default_language_posts($cpt->name, $non_default_langs); if ($count === 0) continue; // Hide empty custom post types $types[$cpt->name] = [ 'label' => isset($known_labels[$cpt->name]) ? $known_labels[$cpt->name] : $cpt->label, 'icon' => ($cpt->name === 'product' ? 'product' : 'custom'), 'count' => $count, ]; } // Elementor templates (headers, footers, sections, etc.) // These are stored as post_type=elementor_library with _elementor_template_type meta. // They are private CPTs so they won't appear in the generic loop above. if (post_type_exists('elementor_library')) { $count = $this->count_elementor_templates($non_default_langs); if ($count > 0) { $types['elementor_library'] = [ 'label' => __('Elementor Templates', 'ai-translator-for-elementor-and-polylang'), 'icon' => 'template', 'count' => $count, ]; } } return $types; } /** * Count published posts of a given type, excluding non-default language translations. */ private function count_default_language_posts($post_type, $non_default_langs) { if (empty($non_default_langs)) { $counts = wp_count_posts($post_type); return isset($counts->publish) ? (int) $counts->publish : 0; } $query = new WP_Query([ 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => false, 'tax_query' => [ [ 'taxonomy' => 'aitrfoel_language', 'field' => 'slug', 'terms' => $non_default_langs, 'operator' => 'NOT IN', ], ], ]); return (int) $query->found_posts; } /** * Count published Elementor templates (headers, footers, sections, etc.) * that have Elementor data, are not kits, and are not already translations. */ private function count_elementor_templates($non_default_langs) { $args = [ 'post_type' => 'elementor_library', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => false, 'meta_query' => [ [ 'key' => '_elementor_data', 'compare' => 'EXISTS', ], [ 'key' => '_elementor_template_type', 'value' => 'kit', 'compare' => '!=', ], ], ]; // If built-in language system is active, exclude non-default language posts if (!empty($non_default_langs)) { $args['tax_query'] = [ [ 'taxonomy' => 'aitrfoel_language', 'field' => 'slug', 'terms' => $non_default_langs, 'operator' => 'NOT IN', ], ]; } $query = new \WP_Query($args); return (int) $query->found_posts; } /** * AJAX: Scan content and calculate word counts */ public function ajax_scan_content() { check_ajax_referer('aitrfoel_translate_all_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Insufficient permissions.', 'ai-translator-for-elementor-and-polylang')); } // Wrap the scan in a Throwable guard so a fatal in any parser (malformed // _elementor_data, third-party post-meta surprises) becomes a JSON error // with the actual message instead of a generic 500. try { $this->run_scan_content(); } catch (\Throwable $e) { if (class_exists('AITRFOEL_Logger')) { AITRFOEL_Logger::log( 'Translate-All scan failed: ' . $e->getMessage() . "\n" . $e->getTraceAsString(), 'error' ); } wp_send_json_error(sprintf( /* translators: %s: PHP exception/error message */ __('Scan failed: %s', 'ai-translator-for-elementor-and-polylang'), $e->getMessage() )); } } /** * Inner scan implementation. Extracted so ajax_scan_content() can wrap it in * a Throwable guard and surface real error messages to the UI. */ private function run_scan_content() { $post_types = isset($_POST['post_types']) ? array_map('sanitize_text_field', (array) $_POST['post_types']) : []; $target_languages = isset($_POST['target_languages']) ? array_map('sanitize_text_field', (array) $_POST['target_languages']) : []; if (empty($post_types)) { wp_send_json_error(__('No content types selected.', 'ai-translator-for-elementor-and-polylang')); } if (empty($target_languages)) { wp_send_json_error(__('No target languages selected.', 'ai-translator-for-elementor-and-polylang')); } $results = []; $total_words = 0; $total_items = 0; $total_already_translated = 0; $total_needs_translation = 0; $all_post_ids = []; $posts_detail = []; // Per-post breakdown // Multilingual manager for checking existing translations $multilingual = class_exists('AITRFOEL_Multilingual_Manager') ? AITRFOEL_Multilingual_Manager::get_instance() : null; $default_lang = ($multilingual && $multilingual->is_active()) ? $multilingual->get_default_language() : ''; foreach ($post_types as $post_type) { $posts = get_posts([ 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', ]); // Filter to only source-language posts (default language) if ($multilingual && $multilingual->is_active() && $default_lang) { $posts = array_filter($posts, function($post) use ($multilingual, $default_lang) { $lang = $multilingual->get_post_language($post->ID); // If language is explicitly set, only keep default language if (!empty($lang)) { return $lang === $default_lang; } // For posts without explicit language, check translation groups $translations = $multilingual->get_post_translations($post->ID); if (!empty($translations)) { foreach ($translations as $lang_code => $post_id) { if ((int) $post_id === $post->ID && $lang_code !== $default_lang) { return false; // This post is a non-default translation } } } return true; // No language, not in a group as non-default = source content }); $posts = array_values($posts); } $type_words = 0; $type_already_translated = 0; $type_needs_translation = 0; $type_post_ids = []; foreach ($posts as $post) { // Skip Elementor global styles / Default Kit if ($post->post_type === 'elementor_library') { $template_type = get_post_meta($post->ID, '_elementor_template_type', true); if ($template_type === 'kit') { continue; } } // Calculate word count. Wrap per-post so one bad post (malformed // _elementor_data, parser exception) doesn't 500 the entire scan. try { $word_count = $this->get_post_word_count($post->ID); } catch (\Throwable $e) { if (class_exists('AITRFOEL_Logger')) { AITRFOEL_Logger::log( sprintf('Scan word-count failed for post %d: %s', $post->ID, $e->getMessage()), 'warning', $post->ID ); } $word_count = 0; } $type_words += $word_count; // Check which languages are translated and which are not $translated_count = 0; $needs_count = 0; $translated_langs = []; $missing_langs = []; if ($multilingual && $multilingual->is_active()) { $existing_translations = $multilingual->get_post_translations($post->ID); foreach ($target_languages as $lang_code) { $translation_id = isset($existing_translations[$lang_code]) ? $existing_translations[$lang_code] : 0; if ($translation_id && $translation_id != $post->ID) { $translated_count++; $translated_langs[] = strtoupper($lang_code); } else { $needs_count++; $missing_langs[] = strtoupper($lang_code); } } } else { $needs_count = count($target_languages); foreach ($target_languages as $lang_code) { $missing_langs[] = strtoupper($lang_code); } } // Content type detection $content_type = 'classic'; if (class_exists('AITRFOEL_Content_Detector')) { $content_type = AITRFOEL_Content_Detector::get_instance()->detect($post->ID); } // Get the post's own language code $post_lang = ''; if ($multilingual && $multilingual->is_active()) { $post_lang = $multilingual->get_post_language($post->ID); } $posts_detail[] = [ 'post_id' => $post->ID, 'title' => $post->post_title ?: __('(no title)', 'ai-translator-for-elementor-and-polylang'), 'post_type' => $post_type, 'post_lang' => strtoupper($post_lang), 'edit_url' => get_edit_post_link($post->ID, 'raw'), 'view_url' => get_permalink($post->ID), 'word_count' => $word_count, 'content_type' => $content_type, 'translated_langs' => $translated_langs, 'missing_langs' => $missing_langs, 'is_fully_translated' => ($needs_count === 0), ]; $type_already_translated += $translated_count; $type_needs_translation += $needs_count; $type_post_ids[] = $post->ID; } // Words needed = words per post × missing languages × server usage factor // The server counts both source and translated words, so multiply by the factor. $words_needed = 0; $factor = self::SERVER_USAGE_FACTOR; foreach ($posts as $post) { try { $wc = $this->get_post_word_count($post->ID); } catch (\Throwable $e) { $wc = 0; } if ($multilingual && $multilingual->is_active()) { $existing_translations = $multilingual->get_post_translations($post->ID); foreach ($target_languages as $lang_code) { $translation_id = isset($existing_translations[$lang_code]) ? $existing_translations[$lang_code] : 0; if (!$translation_id || $translation_id == $post->ID) { $words_needed += $wc * $factor; } } } else { $words_needed += $wc * count($target_languages) * $factor; } } $post_type_obj = get_post_type_object($post_type); $results[] = [ 'post_type' => $post_type, 'label' => $post_type_obj ? $post_type_obj->label : $post_type, 'count' => count($posts), 'total_words' => $type_words, 'words_needed' => $words_needed, 'already_translated' => $type_already_translated, 'needs_translation' => $type_needs_translation, 'post_ids' => $type_post_ids, ]; $total_words += $type_words; $total_items += count($posts); $total_already_translated += $type_already_translated; $total_needs_translation += $type_needs_translation; $all_post_ids = array_merge($all_post_ids, $type_post_ids); } // Calculate total words actually needed $total_words_needed = 0; foreach ($results as $r) { $total_words_needed += $r['words_needed']; } wp_send_json_success([ 'results' => $results, 'posts_detail' => $posts_detail, 'total_items' => $total_items, 'total_words' => $total_words, 'total_words_needed' => $total_words_needed, 'total_already' => $total_already_translated, 'total_needs' => $total_needs_translation, 'target_languages' => $target_languages, 'post_ids' => $all_post_ids, ]); } /** * Get word count for a post based on its content type. * Uses the cached word count from AITRFOEL_Content_Detector. */ private function get_post_word_count($post_id) { if (class_exists('AITRFOEL_Content_Detector')) { return AITRFOEL_Content_Detector::get_instance()->get_cached_word_count($post_id); } return 0; } /** * AJAX: Start translation (immediate or scheduled) */ public function ajax_start_translation() { check_ajax_referer('aitrfoel_translate_all_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Insufficient permissions.', 'ai-translator-for-elementor-and-polylang')); } $mode = sanitize_text_field($_POST['mode'] ?? 'now'); // 'now' or 'scheduled' $post_ids = isset($_POST['post_ids']) ? array_map('intval', (array) $_POST['post_ids']) : []; $target_languages = isset($_POST['target_languages']) ? array_map('sanitize_text_field', (array) $_POST['target_languages']) : []; $source_language = sanitize_text_field($_POST['source_language'] ?? 'auto'); if (empty($post_ids) || empty($target_languages)) { wp_send_json_error(__('No content or languages selected.', 'ai-translator-for-elementor-and-polylang')); } $job_id = 'ta_' . wp_generate_password(16, false); if ($mode === 'scheduled') { // Create scheduled job $this->create_scheduled_job($job_id, $post_ids, $target_languages, $source_language); wp_send_json_success([ 'job_id' => $job_id, 'mode' => 'scheduled', 'message' => __('Scheduled translation created. It will process content using your monthly word allowance.', 'ai-translator-for-elementor-and-polylang'), ]); } else { // Immediate translation via bulk queue $this->create_immediate_job($job_id, $post_ids, $target_languages, $source_language); wp_send_json_success([ 'job_id' => $job_id, 'mode' => 'now', 'message' => __('Translation started.', 'ai-translator-for-elementor-and-polylang'), ]); } } /** * Create a scheduled cron-based translation job */ private function create_scheduled_job($job_id, $post_ids, $target_languages, $source_language) { // Build the translation queue (list of post_id + language pairs that need translation) $queue = []; $multilingual = class_exists('AITRFOEL_Multilingual_Manager') ? AITRFOEL_Multilingual_Manager::get_instance() : null; foreach ($post_ids as $post_id) { foreach ($target_languages as $lang_code) { // Skip if already translated if ($multilingual && $multilingual->is_active()) { $existing_translations = $multilingual->get_post_translations($post_id); $existing = isset($existing_translations[$lang_code]) ? $existing_translations[$lang_code] : 0; if ($existing && $existing != $post_id) { continue; } } $queue[] = [ 'post_id' => $post_id, 'language' => $lang_code, ]; } } // Save job status update_option('aitrfoel_translate_all_cron_status', [ 'job_id' => $job_id, 'status' => 'pending', 'queue' => $queue, 'completed_items' => 0, 'total_items' => count($queue), 'source_language' => $source_language, 'errors' => [], 'created_at' => current_time('mysql'), 'next_run' => time() + 60, // Start in 1 minute ]); // Schedule the first cron run if (!wp_next_scheduled('aitrfoel_translate_all_cron_process')) { wp_schedule_event(time() + 60, 'aitrfoel_five_minutes', 'aitrfoel_translate_all_cron_process'); } } /** * Create an immediate (now) translation job using the existing bulk translation system */ private function create_immediate_job($job_id, $post_ids, $target_languages, $source_language) { // Build the translation queue (skip already translated) $queue = []; $multilingual = class_exists('AITRFOEL_Multilingual_Manager') ? AITRFOEL_Multilingual_Manager::get_instance() : null; foreach ($post_ids as $post_id) { foreach ($target_languages as $lang_code) { if ($multilingual && $multilingual->is_active()) { $existing_translations = $multilingual->get_post_translations($post_id); $existing = isset($existing_translations[$lang_code]) ? $existing_translations[$lang_code] : 0; if ($existing && $existing != $post_id) { continue; } } $queue[] = [ 'post_id' => $post_id, 'language' => $lang_code, ]; } } // Save job for status tracking update_option('aitrfoel_translate_all_job', [ 'job_id' => $job_id, 'status' => 'processing', 'queue' => $queue, 'completed_items' => 0, 'total_items' => count($queue), 'source_language' => $source_language, 'errors' => [], 'results' => [], 'created_at' => current_time('mysql'), ]); // Processing is driven by the status-poll loop (ajax_get_status), which // runs a time-budgeted batch on each call. This keeps the start request // instant and removes any WP-Cron dependency for immediate jobs. } /** * Process a batch of immediate translations. */ private function process_immediate_batch($job_id) { $job = get_option('aitrfoel_translate_all_job', []); if (empty($job) || $job['job_id'] !== $job_id) { return; } // Allow several translations per request rather than one item per poll. if (function_exists('wp_raise_memory_limit')) { wp_raise_memory_limit('admin'); } if (function_exists('set_time_limit')) { @set_time_limit(120); } @ignore_user_abort(true); $queue = $job['queue'] ?? []; $source_language = $job['source_language'] ?? 'auto'; $total = count($queue); $start = microtime(true); $budget = 12.0; // seconds of work per request, then return progress while (($job['completed_items'] ?? 0) < $total) { // Re-read the job each iteration so a pause/cancel from another // request stops the loop promptly. $fresh = get_option('aitrfoel_translate_all_job', []); if (empty($fresh) || ($fresh['job_id'] ?? '') !== $job_id) { return; } if (($fresh['status'] ?? '') !== 'processing') { return; } $job = $fresh; $completed = (int) $job['completed_items']; $item = $queue[$completed]; $result = $this->translate_single_post($item['post_id'], $item['language'], $source_language); $job['completed_items'] = $completed + 1; if (is_wp_error($result)) { $error_msg = $result->get_error_message(); $job['errors'][] = [ 'post_id' => $item['post_id'], 'language' => $item['language'], 'error' => $error_msg, ]; // Stop processing if word limit is reached if ( strpos($error_msg, 'words remaining') !== false || strpos($error_msg, 'word limit') !== false || strpos($error_msg, 'monthly limit') !== false || strpos($error_msg, 'reached your') !== false || strpos($error_msg, 'cooldown') !== false || strpos($error_msg, 'insufficient_words') !== false ) { $job['status'] = 'completed'; $job['stopped_reason'] = 'word_limit'; update_option('aitrfoel_translate_all_job', $job); return; } } else { // Increment successful translation counter for review prompt $count = (int) get_option('aitrfoel_successful_translations', 0); update_option('aitrfoel_successful_translations', $count + 1); $job['results'][] = [ 'post_id' => $item['post_id'], 'title' => html_entity_decode(get_the_title($item['post_id']), ENT_QUOTES, 'UTF-8'), 'post_type' => get_post_type($item['post_id']), 'view_url' => get_permalink($item['post_id']), 'language' => $item['language'], 'new_post_id' => $result, 'new_view_url' => $result ? get_permalink($result) : '', ]; } // Check if done if ($job['completed_items'] >= $total) { $job['status'] = 'completed'; } // Persist after each item so progress survives if the request ends. update_option('aitrfoel_translate_all_job', $job); // Stop once the per-request time budget is used up; the next poll continues. if ((microtime(true) - $start) >= $budget) { break; } } } /** * Translate a single post to a language */ private function translate_single_post($post_id, $target_language, $source_language) { if (!class_exists('AITRFOEL_Translation_Engine')) { return new WP_Error('missing_engine', __('Translation engine not available.', 'ai-translator-for-elementor-and-polylang')); } $engine = AITRFOEL_Translation_Engine::get_instance(); // Map the language code to the format expected by the engine $mapped_lang = $target_language; if (class_exists('AITRFOEL_Language_Mapper')) { $name = AITRFOEL_Language_Mapper::get_language_name($target_language, false); if ($name) { $mapped_lang = $name; } } $mapped_source = $source_language; if ($source_language !== 'auto' && class_exists('AITRFOEL_Language_Mapper')) { $name = AITRFOEL_Language_Mapper::get_language_name($source_language, false); if ($name) { $mapped_source = $name; } } return $engine->translate_page($post_id, $mapped_lang, $mapped_source); } /** * AJAX: Get translation status */ public function ajax_get_status() { check_ajax_referer('aitrfoel_translate_all_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Insufficient permissions.', 'ai-translator-for-elementor-and-polylang')); } $job_id = sanitize_text_field($_POST['job_id'] ?? ''); $mode = sanitize_text_field($_POST['mode'] ?? 'now'); if ($mode === 'scheduled') { $job = get_option('aitrfoel_translate_all_cron_status', []); } else { $job = get_option('aitrfoel_translate_all_job', []); } if (empty($job) || (isset($job['job_id']) && $job['job_id'] !== $job_id)) { wp_send_json_error(__('Job not found.', 'ai-translator-for-elementor-and-polylang')); } // For immediate mode, process next item (only if actively processing, not paused) if ($mode === 'now' && isset($job['status']) && $job['status'] === 'processing') { $this->process_immediate_batch($job_id); // Re-read updated job $job = get_option('aitrfoel_translate_all_job', []); } $total = isset($job['total_items']) ? (int) $job['total_items'] : 0; $completed = isset($job['completed_items']) ? (int) $job['completed_items'] : 0; $progress = $total > 0 ? round(($completed / $total) * 100) : 0; // Get current item info $current_item_info = ''; $queue = $job['queue'] ?? []; if ($completed < count($queue)) { $current = $queue[$completed]; $post_title = html_entity_decode(get_the_title($current['post_id']), ENT_QUOTES, 'UTF-8'); $current_item_info = sprintf('%s → %s', $post_title, strtoupper($current['language'])); } $review_eligible = !get_option('aitrfoel_review_dismissed') && ((int) get_option('aitrfoel_successful_translations', 0) >= 1); wp_send_json_success([ 'status' => $job['status'] ?? 'unknown', 'stopped_reason' => $job['stopped_reason'] ?? '', 'progress' => $progress, 'completed' => $completed, 'total' => $total, 'current_item' => $current_item_info, 'errors' => $job['errors'] ?? [], 'results' => $job['results'] ?? [], 'review_eligible' => $review_eligible, ]); } /** * AJAX: Pause translation */ public function ajax_pause() { check_ajax_referer('aitrfoel_translate_all_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Insufficient permissions.', 'ai-translator-for-elementor-and-polylang')); } $mode = sanitize_text_field($_POST['mode'] ?? 'now'); $option_key = ($mode === 'scheduled') ? 'aitrfoel_translate_all_cron_status' : 'aitrfoel_translate_all_job'; $job = get_option($option_key, []); if (!empty($job) && in_array($job['status'] ?? '', ['processing', 'pending'])) { $job['status'] = 'paused'; update_option($option_key, $job); } wp_send_json_success(['message' => __('Translation paused.', 'ai-translator-for-elementor-and-polylang')]); } /** * AJAX: Resume translation */ public function ajax_resume() { check_ajax_referer('aitrfoel_translate_all_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Insufficient permissions.', 'ai-translator-for-elementor-and-polylang')); } $mode = sanitize_text_field($_POST['mode'] ?? 'now'); $option_key = ($mode === 'scheduled') ? 'aitrfoel_translate_all_cron_status' : 'aitrfoel_translate_all_job'; $job = get_option($option_key, []); if (!empty($job) && ($job['status'] ?? '') === 'paused') { $job['status'] = 'processing'; update_option($option_key, $job); } wp_send_json_success(['message' => __('Translation resumed.', 'ai-translator-for-elementor-and-polylang')]); } /** * AJAX: Cancel translation */ public function ajax_cancel() { check_ajax_referer('aitrfoel_translate_all_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Insufficient permissions.', 'ai-translator-for-elementor-and-polylang')); } $mode = sanitize_text_field($_POST['mode'] ?? 'now'); if ($mode === 'scheduled') { // Cancel cron job $timestamp = wp_next_scheduled('aitrfoel_translate_all_cron_process'); if ($timestamp) { wp_unschedule_event($timestamp, 'aitrfoel_translate_all_cron_process'); } wp_clear_scheduled_hook('aitrfoel_translate_all_cron_process'); $job = get_option('aitrfoel_translate_all_cron_status', []); $job['status'] = 'cancelled'; update_option('aitrfoel_translate_all_cron_status', $job); } else { $job = get_option('aitrfoel_translate_all_job', []); $job['status'] = 'cancelled'; update_option('aitrfoel_translate_all_job', $job); } wp_send_json_success(['message' => __('Translation cancelled.', 'ai-translator-for-elementor-and-polylang')]); } /** * Process scheduled cron job (called by WP-Cron) */ public static function process_cron_batch() { $job = get_option('aitrfoel_translate_all_cron_status', []); if (empty($job) || !in_array($job['status'] ?? '', ['pending', 'processing'])) { // No active job or job is done, unschedule wp_clear_scheduled_hook('aitrfoel_translate_all_cron_process'); return; } // Update status to processing $job['status'] = 'processing'; update_option('aitrfoel_translate_all_cron_status', $job); $queue = $job['queue'] ?? []; $completed = $job['completed_items'] ?? 0; $source_language = $job['source_language'] ?? 'auto'; // Get current word usage $license_info = []; if (class_exists('AITRFOEL_License_Manager_Server')) { $license_manager = AITRFOEL_License_Manager_Server::get_instance(); $license_info = $license_manager->get_license_status(); } $words_left = isset($license_info['words_left']) ? $license_info['words_left'] : 0; // Process items until we run out of words or finish $words_used_this_batch = 0; $items_this_batch = 0; $max_items_per_batch = 5; // Process up to 5 items per cron run to avoid timeout $instance = self::get_instance(); while ($completed < count($queue) && $items_this_batch < $max_items_per_batch) { $item = $queue[$completed]; // Estimate words for this item (apply server usage factor) $estimated_words = $instance->get_post_word_count($item['post_id']) * self::SERVER_USAGE_FACTOR; // Check if we have enough words left (with some buffer) if ($words_left !== -1 && ($words_used_this_batch + $estimated_words) > $words_left) { // Not enough words — stop and wait for next billing cycle if (class_exists('AITRFOEL_Logger')) { AITRFOEL_Logger::log( sprintf('Translate All cron: pausing — need %d words but only %d left', $estimated_words, $words_left - $words_used_this_batch), 'info' ); } break; } $result = $instance->translate_single_post($item['post_id'], $item['language'], $source_language); $completed++; $items_this_batch++; if (is_wp_error($result)) { $job['errors'][] = [ 'post_id' => $item['post_id'], 'language' => $item['language'], 'error' => $result->get_error_message(), ]; } else { $words_used_this_batch += $estimated_words; } $job['completed_items'] = $completed; } // Check if job is complete if ($completed >= count($queue)) { $job['status'] = 'completed'; wp_clear_scheduled_hook('aitrfoel_translate_all_cron_process'); } else { // Schedule next run $job['next_run'] = time() + 300; // 5 minutes } update_option('aitrfoel_translate_all_cron_status', $job); if (class_exists('AITRFOEL_Logger')) { AITRFOEL_Logger::log( sprintf('Translate All cron: processed %d items, %d/%d total', $items_this_batch, $completed, count($queue)), 'info' ); } } }