'GET', 'callback' => [$this, 'handle_wpml_get_xliff_jobs'], 'permission_callback' => function () { return current_user_can('manage_options'); } ]); register_rest_route('aiutoma/v1', '/wpml-xliff-translate', [ 'methods' => 'POST', 'callback' => [$this, 'handle_wpml_xliff_translate'], 'permission_callback' => function () { return current_user_can('manage_options'); } ]); } public function render_xliff_tab() { ?>
false, 'message' => 'WPML not active.'], 500); } $cache_key = 'aiutoma_wpml_xliff_jobs'; $jobs = wp_cache_get($cache_key, 'aiutoma'); if (false === $jobs) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared $jobs = $wpdb->get_results( "SELECT j.job_id, j.title, s.status, t.source_language_code, t.language_code, s.needs_update, t.element_type, j.deadline_date, j.translator_id, t.element_id FROM {$wpdb->prefix}icl_translate_job j JOIN {$wpdb->prefix}icl_translation_status s ON j.rid = s.rid JOIN {$wpdb->prefix}icl_translations t ON s.translation_id = t.translation_id WHERE (s.status != 10 OR s.needs_update = 1) AND (j.revision IS NULL OR j.revision = '') ORDER BY j.job_id DESC LIMIT 100" ); wp_cache_set($cache_key, $jobs, 'aiutoma', 60); } $formatted_jobs = []; if ($jobs) { global $sitepress; foreach ($jobs as $job) { $status_html = ''; if ($job->needs_update == 1) { $status_html = '' . esc_html__('Needs Update', 'aiutoma') . ''; } elseif ($job->status == 2) { $status_html = '' . esc_html__('In Progress', 'aiutoma') . ''; } else { $status_html = '' . esc_html__('Pending', 'aiutoma') . ''; } $type = str_replace('post_', '', $job->element_type); $type = str_replace('tax_', 'Tax: ', $type); $type = ucfirst($type); $src_code = (string) $job->source_language_code; $tgt_code = (string) $job->language_code; $source_flag = $sitepress ? $sitepress->get_flag_url($src_code) : ''; $target_flag = $sitepress ? $sitepress->get_flag_url($tgt_code) : ''; $source_name = $sitepress ? $sitepress->get_display_language_name($src_code, 'en') : $src_code; $target_name = $sitepress ? $sitepress->get_display_language_name($tgt_code, 'en') : $tgt_code; $languages_html = ''; if ($source_flag) { $languages_html .= '' . esc_attr($source_name) . ' '; } $languages_html .= esc_html($source_name) . ' » '; if ($target_flag) { $languages_html .= '' . esc_attr($target_name) . ' '; } $languages_html .= esc_html($target_name); $translator = 'Aiutoma'; if ($job->translator_id > 0) { $user = get_userdata($job->translator_id); $translator = $user ? $user->user_login : $job->translator_id; } $deadline = !empty($job->deadline_date) ? date_i18n(get_option('date_format'), strtotime($job->deadline_date)) : '-'; $edit_url = ''; if (!empty($job->element_id)) { if (strpos($job->element_type, 'post_') === 0) { if (get_post_type($job->element_id)) { $edit_url = get_edit_post_link((int) $job->element_id, 'raw'); } } elseif (strpos($job->element_type, 'tax_') === 0) { $taxonomy = str_replace('tax_', '', $job->element_type); if (term_exists((int) $job->element_id, $taxonomy)) { $edit_url = get_edit_term_link((int) $job->element_id, $taxonomy); } } } $formatted_jobs[] = [ 'job_id' => $job->job_id, 'title' => esc_html($job->title), 'edit_url' => $edit_url ? esc_url_raw($edit_url) : '', 'type' => $type, 'languages' => $languages_html, 'translator' => $translator, 'deadline' => $deadline, 'status' => $status_html ]; } } return new \WP_REST_Response(['success' => true, 'jobs' => $formatted_jobs], 200); } public function handle_wpml_xliff_translate(\WP_REST_Request $request) { // XLIFF translation of large posts can take a while, especially with rate limits if (function_exists('set_time_limit')) { @set_time_limit(300); } $raw_job_id = $request->get_param('job_id'); $translator_id = get_current_user_id(); if (empty($raw_job_id) || empty($translator_id)) { return new \WP_REST_Response(['success' => false, 'message' => 'Missing required parameters.'], 400); } $this->log_error("Aiutoma XLIFF Translating requested raw_job_id: $raw_job_id"); global $wpdb; $actual_job_id = false; // Parse the explicit type if provided by the new JS format (e.g. 'rid_12', 'job_12', 'post_500') $type = 'unknown'; $numeric_id = 0; if (preg_match('/^(rid|job|post|raw)_(\d+)$/', $raw_job_id, $matches)) { $type = $matches[1]; $numeric_id = (int)$matches[2]; } else { $numeric_id = (int)$raw_job_id; } if ($type === 'job') { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $actual_job_id = $wpdb->get_var($wpdb->prepare("SELECT job_id FROM {$wpdb->prefix}icl_translate_job WHERE job_id = %d", $numeric_id)); } elseif ($type === 'rid' || $type === 'raw') { $this->log_error("Aiutoma: Searching for RID $numeric_id..."); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $actual_job_id = $wpdb->get_var($wpdb->prepare(" SELECT job_id FROM {$wpdb->prefix}icl_translate_job WHERE rid = %d ORDER BY job_id DESC LIMIT 1 ", $numeric_id)); } elseif ($type === 'post') { $this->log_error("Aiutoma: ID $numeric_id is a Post ID. Finding associated translation jobs..."); $post_like = $wpdb->esc_like('post_') . '%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $job_ids = $wpdb->get_col($wpdb->prepare(" SELECT j.job_id FROM {$wpdb->prefix}icl_translate_job j JOIN {$wpdb->prefix}icl_translation_status s ON j.rid = s.rid JOIN {$wpdb->prefix}icl_translations t ON s.translation_id = t.translation_id WHERE t.element_id = %d AND t.element_type LIKE %s ORDER BY j.job_id DESC ", $numeric_id, $post_like)); if (!empty($job_ids)) { $actual_job_id = $job_ids[0]; $this->log_error("Aiutoma: Resolved Post ID to Job ID: $actual_job_id"); } else { $this->log_error("Aiutoma: No translation jobs found for Post ID $numeric_id"); return new \WP_REST_Response(['success' => false, 'message' => 'No translation jobs found for this post.'], 404); } } else { // Fallback for old numeric values: try Job ID first, then RID, then Post ID // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $actual_job_id = $wpdb->get_var($wpdb->prepare("SELECT job_id FROM {$wpdb->prefix}icl_translate_job WHERE job_id = %d", $numeric_id)); if (!$actual_job_id) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $actual_job_id = $wpdb->get_var($wpdb->prepare("SELECT job_id FROM {$wpdb->prefix}icl_translate_job WHERE rid = %d ORDER BY job_id DESC LIMIT 1", $numeric_id)); } } if (!$actual_job_id) { $this->log_error("Aiutoma: Failed to resolve requested ID $raw_job_id to a valid translation job."); return new \WP_REST_Response(['success' => false, 'message' => 'Could not find a translation job for this ID.'], 404); } $job_id = (int)$actual_job_id; $this->log_error("Aiutoma: Proceeding with final Job ID: $job_id"); if (!function_exists('wpml_tm_xliff_factory')) { $this->log_error("Aiutoma: wpml_tm_xliff_factory function not found."); return new \WP_REST_Response(['success' => false, 'message' => 'WPML XLIFF functions not found.'], 500); } // 1. Export XLIFF try { $xliff_writer = wpml_tm_xliff_factory()->create_writer('12'); if (!$xliff_writer) return new \WP_REST_Response(['success' => false, 'message' => 'Failed to initialize WPML XLIFF writer.'], 500); $xliff_string = $xliff_writer->generate_job_xliff($job_id); if (!$xliff_string) { $this->log_error("Aiutoma XLIFF Error: Failed to generate XLIFF for job_id: $job_id"); return new \WP_REST_Response(['success' => false, 'message' => 'Failed to generate XLIFF.'], 500); } $this->log_error("Aiutoma XLIFF: Generated XLIFF of length " . strlen($xliff_string) . " for job_id: $job_id"); } catch (\Exception $e) { return new \WP_REST_Response(['success' => false, 'message' => 'WPML Exception: ' . $e->getMessage()], 500); } // 2. Translate with AI $prompt = "You are an expert translator. The following is an XLIFF standard translation file.\n"; $prompt .= "Your task is to translate all the text inside the tags and put the translation in the corresponding tags.\n"; $prompt .= "IMPORTANT RULES:\n"; $prompt .= "- Return ONLY the raw valid XML code. Do not wrap it in markdown code blocks like ```xml.\n"; $prompt .= "- Preserve ALL XML structure and other tags (like , ).\n"; $prompt .= "- Ensure the returned string starts with false, 'message' => 'AI Client method not available.'], 500); } $model = get_option('aiutoma_wpml_model', ''); if ($model === 'google_free' && method_exists($this, 'google_translate_free')) { $translated_xliff = $xliff_string; // Get langs from DB since XML parsing might be tricky with namespaces global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $job_row = $wpdb->get_row($wpdb->prepare( " SELECT t.source_language_code, t.language_code FROM {$wpdb->prefix}icl_translate_job j JOIN {$wpdb->prefix}icl_translation_status s ON j.rid = s.rid JOIN {$wpdb->prefix}icl_translations t ON s.translation_id = t.translation_id WHERE j.job_id = %d", $job_id )); if ($job_row) { $source_lang = $job_row->source_language_code; $target_lang = $job_row->language_code; // Strip any existing target tags to prevent duplicate XML elements $translated_xliff = preg_replace('/]*>.*?<\/target>/s', '', $translated_xliff); $translated_xliff = preg_replace('/]*\/>/s', '', $translated_xliff); // Simple regex to extract and replace sources $translated_xliff = preg_replace_callback('/<\/source>/s', function ($matches) use ($source_lang, $target_lang) { $source_text = $matches[1]; usleep(300000); // 300ms delay to prevent Google Translate rate limits $t = $this->google_translate_free($source_lang, $target_lang, $source_text); if (is_wp_error($t)) { $this->log_error("Aiutoma Google Translate Free Error (CDATA): " . $t->get_error_message()); return $matches[0] . ''; } return $matches[0] . ''; }, $translated_xliff); // Sometimes it's without CDATA $translated_xliff = preg_replace_callback('/([^<]+)<\/source>/s', function ($matches) use ($source_lang, $target_lang) { $source_text = html_entity_decode($matches[1]); if (trim($source_text) === '') { return $matches[0] . '' . $matches[1] . ''; } usleep(300000); // 300ms delay $t = $this->google_translate_free($source_lang, $target_lang, $source_text); if (is_wp_error($t)) { $this->log_error("Aiutoma Google Translate Free Error (Text): " . $t->get_error_message()); return $matches[0] . '' . $matches[1] . ''; } return $matches[0] . '' . htmlspecialchars($t) . ''; }, $translated_xliff); } } else { $translated_xliff = $this->call_ai_for_text($prompt); if (is_wp_error($translated_xliff)) { return new \WP_REST_Response(['success' => false, 'message' => $translated_xliff->get_error_message()], 500); } // Clean markdown if present $translated_xliff = preg_replace('/^```xml\s*/i', '', trim($translated_xliff)); $translated_xliff = preg_replace('/```$/', '', $translated_xliff); $translated_xliff = trim($translated_xliff); } if (strpos($translated_xliff, 'log_error("Aiutoma XLIFF Error: AI did not return valid XML. Returned text: " . substr($translated_xliff, 0, 200)); return new \WP_REST_Response(['success' => false, 'message' => 'AI did not return valid XML.'], 500); } // 3. Import XLIFF if (!class_exists('\WPML_TM_Xliff_Reader_Factory')) { $this->log_error("Aiutoma XLIFF Error: WPML XLIFF Reader Factory not found."); return new \WP_REST_Response(['success' => false, 'message' => 'WPML XLIFF Reader Factory not found.'], 500); } try { $job_factory = function_exists('wpml_tm_load_job_factory') ? wpml_tm_load_job_factory() : null; $xliff_reader_factory = new \WPML_TM_Xliff_Reader_Factory($job_factory); $xliff_reader = $xliff_reader_factory->general_xliff_reader(); $job_data = $xliff_reader->get_data($translated_xliff); if (is_wp_error($job_data)) { $this->log_error("Aiutoma XLIFF Parsing error: " . $job_data->get_error_message()); return new \WP_REST_Response(['success' => false, 'message' => 'Failed to parse AI translated XML: ' . $job_data->get_error_message()], 500); } if (!function_exists('wpml_tm_save_data')) { $wpml_dir = defined('WPML_PLUGIN_PATH') ? WPML_PLUGIN_PATH : (defined('ICL_PLUGIN_PATH') ? ICL_PLUGIN_PATH : ''); if ($wpml_dir && file_exists(trailingslashit($wpml_dir) . 'inc/wpml-private-actions-tm.php')) { require_once trailingslashit($wpml_dir) . 'inc/wpml-private-actions-tm.php'; } } // Force all fields in job_data to be finished if (isset($job_data['job_id'])) { $job_data['complete'] = 1; } if (function_exists('wpml_tm_save_data')) { kses_remove_filters(); // Log job_data structure for debugging $this->log_error("Aiutoma Job Data before save: " . wp_json_encode($job_data)); $result = wpml_tm_save_data($job_data, false); $this->log_error("Aiutoma Job Save Result: " . wp_json_encode($result)); kses_init(); if (!$result) { return new \WP_REST_Response(['success' => false, 'message' => 'WPML failed to save translation data.'], 500); } wp_cache_delete('aiutoma_wpml_xliff_jobs', 'aiutoma'); return new \WP_REST_Response(['success' => true, 'message' => 'Translation imported successfully.'], 200); } else { $this->log_error("Aiutoma XLIFF Error: WPML wpml_tm_save_data function not found and could not be loaded."); return new \WP_REST_Response(['success' => false, 'message' => 'WPML wpml_tm_save_data function not found.'], 500); } } catch (\Exception $e) { return new \WP_REST_Response(['success' => false, 'message' => 'WPML Import Exception: ' . $e->getMessage()], 500); } } }