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