get_active_languages(); $default_lang = $sitepress->get_default_language(); $post_types = []; if ($sitepress) { $translatable_docs = $sitepress->get_translatable_documents(true); $excluded_pts = ['product_variation', 'shop_order', 'shop_order_refund', 'shop_coupon', 'shop_order_placehold', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_font_family', 'wp_font_face', 'wp_sync_storage', 'acf-field-group', 'translation_priority']; foreach ($translatable_docs as $pt_name => $pt_obj) { if (in_array($pt_name, $excluded_pts)) continue; $has_items = $wpdb->get_var($wpdb->prepare("SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE element_type = %s AND language_code = %s LIMIT 1", 'post_' . $pt_name, $default_lang)); if (!$has_items) continue; if (isset($pt_obj->label)) { $post_types[] = $pt_obj; } } } $taxonomies = []; if ($sitepress) { $translatable_tax = $sitepress->get_translatable_taxonomies(true); $excluded_tax = ['translation_priority']; foreach ($translatable_tax as $tax_name) { if (in_array($tax_name, $excluded_tax)) continue; $has_items = $wpdb->get_var($wpdb->prepare("SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE element_type = %s AND language_code = %s LIMIT 1", 'tax_' . $tax_name, $default_lang)); if (!$has_items) continue; $tax_obj = get_taxonomy($tax_name); if ($tax_obj) { $taxonomies[] = $tax_obj; } } } ?>
get_param('type')); $target_lang = sanitize_text_field($request->get_param('target_lang')); $status_filter = sanitize_text_field($request->get_param('status')); if (!$status_filter) $status_filter = 'both'; if (!$type_str || !$target_lang) return new \WP_REST_Response(['success' => false, 'message' => 'Missing parameters.'], 400); global $sitepress, $wpdb; if (!$sitepress) return new \WP_REST_Response(['success' => false, 'message' => 'WPML not active.'], 500); $items = []; $default_lang = $sitepress->get_default_language(); if (strpos($type_str, 'post_') === 0) { $post_type = substr($type_str, 5); $el_type = 'post_' . $post_type; $source_elements = $wpdb->get_results($wpdb->prepare( "SELECT p.ID, p.post_title, t.trid, t.language_code AS orig_lang FROM {$wpdb->posts} p JOIN {$wpdb->prefix}icl_translations t ON p.ID = t.element_id WHERE t.element_type = %s AND p.post_type = %s AND p.post_status IN ('publish', 'draft') AND t.source_language_code IS NULL", $el_type, $post_type )); foreach ($source_elements as $el) { $trans = $wpdb->get_row($wpdb->prepare( "SELECT t.translation_id, t.element_id, s.needs_update FROM {$wpdb->prefix}icl_translations t LEFT JOIN {$wpdb->prefix}icl_translation_status s ON t.translation_id = s.translation_id WHERE t.trid = %d AND t.language_code = %s", $el->trid, $target_lang )); $is_missing = empty($trans); $needs_update = !empty($trans) && (int)$trans->needs_update === 1; $match = false; if ($status_filter === 'not_translated' && $is_missing) $match = true; elseif ($status_filter === 'needs_update' && $needs_update) $match = true; elseif ($status_filter === 'both' && ($is_missing || $needs_update)) $match = true; if ($match) { $translations = $sitepress->get_element_translations($el->trid, $el_type); $langs_html = ''; foreach ($sitepress->get_active_languages() as $lcode => $lang) { $is_original = ($lcode === $el->orig_lang); $has_trans = isset($translations[$lcode]) && !empty($translations[$lcode]->element_id); $flag_url = $sitepress->get_flag_url($lcode); $style = 'vertical-align:middle; width:18px; height:12px;'; if (!$has_trans && !$is_original) $style .= ' opacity:0.3; filter:grayscale(100%);'; $img = '' . esc_attr($lcode) . ''; if ($is_original) $img = '' . $img . ''; if ($has_trans) { $edit_url = get_edit_post_link($translations[$lcode]->element_id, 'raw'); $langs_html .= '' . $img . ''; } else { $langs_html .= '' . $img . ''; } } $langs_html .= ''; $items[] = [ 'id' => $el->ID, 'title' => esc_html($el->post_title), 'object_type' => 'post', 'taxonomy' => '', 'edit_url' => get_edit_post_link($el->ID, 'raw'), 'status' => $needs_update ? 'needs_update' : 'missing', 'langs_html' => $langs_html ]; } } } elseif (strpos($type_str, 'tax_') === 0) { $taxonomy = substr($type_str, 4); $el_type = 'tax_' . $taxonomy; $source_elements = $wpdb->get_results($wpdb->prepare( "SELECT tm.term_id, t.name, trans.trid, trans.language_code AS orig_lang FROM {$wpdb->term_taxonomy} tm JOIN {$wpdb->terms} t ON tm.term_id = t.term_id JOIN {$wpdb->prefix}icl_translations trans ON tm.term_taxonomy_id = trans.element_id WHERE trans.element_type = %s AND tm.taxonomy = %s AND trans.source_language_code IS NULL", $el_type, $taxonomy )); foreach ($source_elements as $el) { $trans = $wpdb->get_row($wpdb->prepare( "SELECT t.translation_id, t.element_id, s.needs_update FROM {$wpdb->prefix}icl_translations t LEFT JOIN {$wpdb->prefix}icl_translation_status s ON t.translation_id = s.translation_id WHERE t.trid = %d AND t.language_code = %s", $el->trid, $target_lang )); $is_missing = empty($trans); $needs_update = !empty($trans) && (int)$trans->needs_update === 1; $match = false; if ($status_filter === 'not_translated' && $is_missing) $match = true; elseif ($status_filter === 'needs_update' && $needs_update) $match = true; elseif ($status_filter === 'both' && ($is_missing || $needs_update)) $match = true; if ($match) { $translations = $sitepress->get_element_translations($el->trid, $el_type); $langs_html = ''; foreach ($sitepress->get_active_languages() as $lcode => $lang) { $is_original = ($lcode === $el->orig_lang); $has_trans = isset($translations[$lcode]) && !empty($translations[$lcode]->element_id); $flag_url = $sitepress->get_flag_url($lcode); $style = 'vertical-align:middle; width:18px; height:12px;'; if (!$has_trans && !$is_original) $style .= ' opacity:0.3; filter:grayscale(100%);'; $img = '' . esc_attr($lcode) . ''; if ($is_original) $img = '' . $img . ''; if ($has_trans) { $edit_url = html_entity_decode(get_edit_term_link((int)$translations[$lcode]->element_id, $taxonomy, $post_type ?? '')); $langs_html .= '' . $img . ''; } else { $langs_html .= '' . $img . ''; } } $langs_html .= ''; $items[] = [ 'id' => $el->term_id, 'title' => esc_html($el->name), 'object_type' => 'term', 'taxonomy' => $taxonomy, 'edit_url' => html_entity_decode(get_edit_term_link($el->term_id, $taxonomy, $post_type ?? '')), 'status' => $needs_update ? 'needs_update' : 'missing', 'langs_html' => $langs_html ]; } } } return new \WP_REST_Response(['success' => true, 'items' => $items], 200); } public function inject_wpml_ai_buttons() { $screen = get_current_screen(); if (!$screen) return; if ($screen->base !== 'post' && $screen->base !== 'term' && $screen->base !== 'edit-tags') return; } public function handle_wpml_translate(\WP_REST_Request $request) { $object_id = intval($request->get_param('object_id')); $object_type = sanitize_text_field($request->get_param('object_type')); $target_lang = sanitize_text_field($request->get_param('target_lang')); $taxonomy = sanitize_text_field($request->get_param('taxonomy')); if (!$object_id || !$target_lang) { return new \WP_REST_Response(['success' => false, 'message' => 'Missing parameters.'], 400); } if (!class_exists('\WordPress\AiClient\AiClient')) { return new \WP_REST_Response(['success' => false, 'message' => 'AI Client not available.'], 500); } global $sitepress, $wpdb; if (!$sitepress) { return new \WP_REST_Response(['success' => false, 'message' => 'WPML not active.'], 500); } $source_lang = $sitepress->get_default_language(); $translated_title = ''; $translated_content = ''; $translated_excerpt = ''; $edit_url = ''; $new_post_id = 0; $trid = 0; if ($object_type === 'post') { $post = get_post($object_id); if (!$post) return new \WP_REST_Response(['success' => false, 'message' => 'Post not found.'], 404); $trid = $sitepress->get_element_trid($post->ID, 'post_' . $post->post_type); if (!$trid) return new \WP_REST_Response(['success' => false, 'message' => 'Translation group not found.'], 404); $translatable_meta_keys = $sitepress->get_custom_fields_translation_settings(2); // WPML_TRANSLATE_CUSTOM_FIELD = 2 $custom_fields_to_translate = []; foreach ($translatable_meta_keys as $meta_key) { $meta_values = get_post_meta($post->ID, $meta_key); foreach ($meta_values as $val) { if (is_string($val) && trim($val) !== '') { if (!isset($custom_fields_to_translate[$meta_key])) { $custom_fields_to_translate[$meta_key] = []; } $custom_fields_to_translate[$meta_key][] = $val; } } } $is_elementor = false; if (method_exists($this, 'is_elementor_post') && $this->is_elementor_post($post->ID)) { $is_elementor = true; $this->prepare_elementor_for_translation($post->ID, $custom_fields_to_translate); } $prompt = "You are an expert translator. Translate the following WordPress post into the language represented by the ISO code '{$target_lang}' (e.g. 'it' = Italian, 'fr' = French, 'es' = Spanish).\n"; $prompt .= "IMPORTANT RULES:\n"; $prompt .= "1. You MUST preserve ALL HTML tags, attributes, and Gutenberg block comments (e.g. ) EXACTLY as they are. Do not translate or modify them.\n"; $prompt .= "2. ONLY translate the visible text content inside the tags/blocks. DO NOT alter ANY image URLs (src), link URLs (href), or image/block IDs (e.g. {\"id\":123}).\n"; $prompt .= "3. DO NOT translate Shortcodes (e.g. [shortcode arg=\"value\"]) or their attributes. Leave them exactly as they are.\n"; $prompt .= "4. DO NOT translate any JSON configuration inside Gutenberg blocks. Only translate the text that is meant to be visible to the user.\n"; $custom_context = get_option('aiutoma_wpml_context', ''); if (!empty($custom_context)) { $prompt .= "\nSITE CONTEXT & GLOSSARY:\n" . $custom_context . "\n\n"; } if ($is_elementor && method_exists($this, 'get_elementor_prompt_instructions')) { $prompt .= $this->get_elementor_prompt_instructions(); } if (!empty($custom_fields_to_translate)) { if ($is_elementor) { $prompt .= "4. Respond ONLY with a valid JSON object matching this structure:\n"; } else { $prompt .= "3. Respond ONLY with a valid JSON object matching this structure:\n"; } $prompt .= "{\n \"title\": \"...\",\n \"content\": \"...\",\n \"excerpt\": \"...\",\n \"custom_fields\": {\n"; foreach ($custom_fields_to_translate as $k => $v) { if ($k === '_elementor_data_decoded' && method_exists($this, 'get_elementor_prompt_json_instruction')) { $prompt .= $this->get_elementor_prompt_json_instruction(); } else { $prompt .= " \"$k\": " . (count($v) > 1 ? "[\"...\", \"...\"]" : "\"...\"") . "\n"; } } $prompt .= " }\n}\nDo not include any other text.\n\n"; } else { $prompt .= "3. Respond ONLY with a valid JSON object containing the keys: 'title', 'content', and 'excerpt'. Do not include any other text.\n\n"; } $prompt .= "TITLE:\n" . $post->post_title . "\n\n"; $prompt .= "EXCERPT:\n" . $post->post_excerpt . "\n\n"; $prompt .= "CONTENT:\n" . $post->post_content . "\n\n"; if (!empty($custom_fields_to_translate)) { $prompt .= "CUSTOM FIELDS (translate the values):\n"; $prompt .= wp_json_encode($custom_fields_to_translate, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"; } $data_to_translate = [ 'title' => $post->post_title, 'excerpt' => $post->post_excerpt, 'content' => $post->post_content ]; if (!empty($custom_fields_to_translate)) { $data_to_translate['custom_fields'] = $custom_fields_to_translate; } $result = $this->do_translation_with_model($prompt, $data_to_translate, $source_lang, $target_lang); if (is_wp_error($result)) { return new \WP_REST_Response(['success' => false, 'message' => $result->get_error_message()], 500); } if (!$result || !isset($result['title']) || !isset($result['content'])) { return new \WP_REST_Response(['success' => false, 'message' => 'Failed to generate valid translation from AI.'], 500); } $existing_trans_id = $wpdb->get_var($wpdb->prepare( "SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE trid = %d AND language_code = %s", $trid, $target_lang )); $new_post_args = [ 'post_title' => $result['title'], 'post_content' => $result['content'], 'post_excerpt' => isset($result['excerpt']) ? $result['excerpt'] : '' ]; $orig_lang_post = null; // phpcs:ignore WordPress.Security.NonceVerification.Missing if (isset($_POST['icl_post_language'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $orig_lang_post = sanitize_text_field(wp_unslash($_POST['icl_post_language'])); } $_POST['icl_post_language'] = $target_lang; if ($existing_trans_id) { $new_post_args['ID'] = $existing_trans_id; remove_action('post_updated', [$this, 'schedule_translation_updates'], 10); wp_save_post_revision($existing_trans_id); $new_post_id = wp_update_post($new_post_args); add_action('post_updated', [$this, 'schedule_translation_updates'], 10, 3); } else { $new_post_args['post_type'] = $post->post_type; $new_post_args['post_status'] = $post->post_status; $new_post_args['post_author'] = $post->post_author; $new_post_id = wp_insert_post($new_post_args); } if ($orig_lang_post !== null) { $_POST['icl_post_language'] = $orig_lang_post; } else { unset($_POST['icl_post_language']); } if (is_wp_error($new_post_id)) { return new \WP_REST_Response(['success' => false, 'message' => $new_post_id->get_error_message()], 500); } $thumbnail_id = get_post_thumbnail_id($post->ID); if ($thumbnail_id) { $translated_thumbnail_id = apply_filters('wpml_object_id', $thumbnail_id, 'attachment', true, $target_lang); update_post_meta($new_post_id, '_thumbnail_id', $translated_thumbnail_id); } if ($is_elementor && method_exists($this, 'save_translated_elementor_data')) { $this->save_translated_elementor_data($new_post_id, $result['custom_fields']); } foreach ($result['custom_fields'] as $meta_key => $translated_val) { if ($meta_key === '_elementor_data_decoded') continue; if (is_array($translated_val)) { delete_post_meta($new_post_id, $meta_key); foreach ($translated_val as $v) { add_post_meta($new_post_id, $meta_key, $v); } } else { update_post_meta($new_post_id, $meta_key, $translated_val); } } $copy_meta_keys = $sitepress->get_custom_fields_translation_settings(1); // WPML_COPY_CUSTOM_FIELD $copy_once_meta_keys = $sitepress->get_custom_fields_translation_settings(3); // WPML_COPY_ONCE_CUSTOM_FIELD $meta_to_copy = []; if (is_array($copy_meta_keys)) $meta_to_copy = array_merge($meta_to_copy, $copy_meta_keys); if (!$existing_trans_id && is_array($copy_once_meta_keys)) $meta_to_copy = array_merge($meta_to_copy, $copy_once_meta_keys); if ($is_elementor && method_exists($this, 'get_elementor_meta_keys_to_copy')) { $meta_to_copy = array_merge($meta_to_copy, $this->get_elementor_meta_keys_to_copy()); } $meta_to_copy = array_unique($meta_to_copy); foreach ($meta_to_copy as $meta_key) { if ($meta_key === '_thumbnail_id' || $meta_key === '_elementor_data') continue; $meta_values = get_post_meta($post->ID, $meta_key); delete_post_meta($new_post_id, $meta_key); foreach ($meta_values as $v) { if ($meta_key === '_product_image_gallery' && !empty($v)) { $gallery_ids = explode(',', $v); $translated_gallery_ids = []; foreach ($gallery_ids as $gid) { $translated_gallery_ids[] = apply_filters('wpml_object_id', trim($gid), 'attachment', true, $target_lang); } $v = implode(',', $translated_gallery_ids); } add_post_meta($new_post_id, $meta_key, $v); } } $translatable_tax = $sitepress->get_translatable_taxonomies(true); foreach ($translatable_tax as $tax_name) { $terms = wp_get_post_terms($post->ID, $tax_name); if (empty($terms) || is_wp_error($terms)) continue; $translated_term_ids = []; foreach ($terms as $term) { $term_trid = $sitepress->get_element_trid($term->term_taxonomy_id, 'tax_' . $tax_name); if (!$term_trid) continue; $translated_term_tax_id = $wpdb->get_var($wpdb->prepare( "SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE trid = %d AND language_code = %s", $term_trid, $target_lang )); if (!$translated_term_tax_id) { $term_prompt = "You are an expert translator. Translate the following WordPress term into the language represented by the ISO code '{$target_lang}'.\n"; $term_prompt .= "Respond ONLY with a valid JSON object containing the keys: 'name' and 'description'. Do not include any other text.\n\n"; $term_prompt .= "NAME:\n" . $term->name . "\n\n"; $term_prompt .= "DESCRIPTION:\n" . $term->description . "\n"; $data_to_translate = [ 'name' => $term->name, 'description' => $term->description ]; $term_result = $this->do_translation_with_model($term_prompt, $data_to_translate, $source_lang, $target_lang); if (!is_wp_error($term_result) && isset($term_result['name'])) { $new_term = wp_insert_term($term_result['name'], $tax_name, [ 'description' => isset($term_result['description']) ? $term_result['description'] : '' ]); if (!is_wp_error($new_term)) { $source_term_lang = $sitepress->get_language_for_element($term->term_taxonomy_id, 'tax_' . $tax_name); if (!$source_term_lang) $source_term_lang = $sitepress->get_default_language(); $sitepress->set_element_language_details($new_term['term_taxonomy_id'], 'tax_' . $tax_name, $term_trid, $target_lang, $source_term_lang); $this->resolve_wpml_tm_job($new_term['term_taxonomy_id'], $target_lang); $translated_term_ids[] = (int)$new_term['term_id']; } } } else { $translated_term = get_term_by('term_taxonomy_id', $translated_term_tax_id, $tax_name); if ($translated_term) { $translated_term_ids[] = (int)$translated_term->term_id; } } } if (!empty($translated_term_ids)) { wp_set_object_terms($new_post_id, $translated_term_ids, $tax_name); } } update_post_meta($post->ID, '_wpml_post_translation_editor_native', 1); update_post_meta($post->ID, '_wpml_post_translation_editor_wpml', 0); update_post_meta($new_post_id, '_wpml_post_translation_editor_native', 1); update_post_meta($new_post_id, '_wpml_post_translation_editor_wpml', 0); if (!$existing_trans_id) { $sitepress->set_element_language_details($new_post_id, 'post_' . $post->post_type, $trid, $target_lang, $sitepress->get_language_for_element($post->ID, 'post_' . $post->post_type)); } $this->resolve_wpml_tm_job($new_post_id, $target_lang); $edit_url = get_edit_post_link($new_post_id, 'raw'); } elseif ($object_type === 'term') { $term = get_term($object_id, $taxonomy); if (!$term || is_wp_error($term)) return new \WP_REST_Response(['success' => false, 'message' => 'Term not found.'], 404); $trid = $sitepress->get_element_trid($term->term_taxonomy_id, 'tax_' . $taxonomy); if (!$trid) return new \WP_REST_Response(['success' => false, 'message' => 'Translation group not found.'], 404); $prompt = "You are an expert translator. Translate the following WordPress term into the language represented by the ISO code '{$target_lang}' (e.g. 'it' = Italian, 'fr' = French, 'es' = Spanish).\n"; $prompt .= "Respond ONLY with a valid JSON object containing the keys: 'name' and 'description'. Do not include any other text.\n\n"; $prompt .= "NAME:\n" . $term->name . "\n\n"; $prompt .= "DESCRIPTION:\n" . $term->description . "\n"; $data_to_translate = [ 'name' => $term->name, 'description' => $term->description ]; $result = $this->do_translation_with_model($prompt, $data_to_translate, $source_lang, $target_lang); if (is_wp_error($result)) { return new \WP_REST_Response(['success' => false, 'message' => $result->get_error_message()], 500); } if (!$result || !isset($result['name'])) { return new \WP_REST_Response(['success' => false, 'message' => 'Failed to generate valid translation from AI.'], 500); } $existing_term_tax_id = $wpdb->get_var($wpdb->prepare( "SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE trid = %d AND language_code = %s", $trid, $target_lang )); if ($existing_term_tax_id) { $existing_term = get_term_by('term_taxonomy_id', $existing_term_tax_id, $taxonomy); if ($existing_term) { $new_term = wp_update_term($existing_term->term_id, $taxonomy, [ 'name' => $result['name'], 'description' => isset($result['description']) ? $result['description'] : '' ]); } else { return new \WP_REST_Response(['success' => false, 'message' => 'Existing term translation not found.'], 500); } } else { $new_term = wp_insert_term($result['name'], $taxonomy, [ 'description' => isset($result['description']) ? $result['description'] : '' ]); } if (is_wp_error($new_term)) { return new \WP_REST_Response(['success' => false, 'message' => $new_term->get_error_message()], 500); } if (!$existing_term_tax_id) { $sitepress->set_element_language_details($new_term['term_taxonomy_id'], 'tax_' . $taxonomy, $trid, $target_lang, $sitepress->get_language_for_element($term->term_taxonomy_id, 'tax_' . $taxonomy)); } $this->resolve_wpml_tm_job($new_term['term_taxonomy_id'], $target_lang); $edit_url = get_edit_term_link($new_term['term_id'], $taxonomy, 'post_type'); } return new \WP_REST_Response(['success' => true, 'message' => 'Translated successfully.', 'edit_url' => html_entity_decode($edit_url ?? '')], 200); } private function resolve_wpml_tm_job($element_id, $target_lang) { global $wpdb; $translation_id = $wpdb->get_var($wpdb->prepare( "SELECT translation_id FROM {$wpdb->prefix}icl_translations WHERE element_id = %d AND language_code = %s", $element_id, $target_lang )); if ($translation_id) { $status_row = $wpdb->get_row($wpdb->prepare("SELECT rid FROM {$wpdb->prefix}icl_translation_status WHERE translation_id = %d", $translation_id)); $wpdb->update( $wpdb->prefix . 'icl_translation_status', [ 'status' => 10, 'translation_service' => 'local', 'needs_update' => 0 ], ['translation_id' => $translation_id] ); if ($status_row && isset($status_row->rid)) { $wpdb->update( $wpdb->prefix . 'icl_translate_job', [ 'translated' => 1, 'completed_date' => current_time('mysql') ], ['rid' => $status_row->rid] ); } } $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}icl_string_translations SET translation_service = %s WHERE translation_service = %s", 'local', '0' ) ); } public function schedule_translation_updates($post_id, $post_after, $post_before) { if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if ($post_after->post_status !== 'publish' && $post_after->post_status !== 'draft') return; if ($post_after->post_content === $post_before->post_content && $post_after->post_title === $post_before->post_title && $post_after->post_excerpt === $post_before->post_excerpt) return; if (!get_post_meta($post_id, '_aiutoma_wpml_force_retranslate', true)) { return; } global $sitepress, $wpdb; $default_lang = $sitepress->get_default_language(); $el_type = 'post_' . $post_after->post_type; $post_lang = $sitepress->get_language_for_element($post_id, $el_type); if ($post_lang !== $default_lang) return; $trid = $sitepress->get_element_trid($post_id, $el_type); if (!$trid) return; $translations = $wpdb->get_results($wpdb->prepare( "SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE trid = %d AND language_code != %s", $trid, $default_lang )); if (!empty($translations)) { wp_schedule_single_event(time() + 2, 'aiutoma_wpml_update_translations', [$post_id]); } } public function process_translation_updates($post_id) { global $sitepress, $wpdb; if (!$sitepress) return; $post = get_post($post_id); if (!$post) return; $el_type = 'post_' . $post->post_type; $default_lang = $sitepress->get_default_language(); $trid = $sitepress->get_element_trid($post_id, $el_type); if (!$trid) return; $translations = $wpdb->get_results($wpdb->prepare( "SELECT element_id, language_code FROM {$wpdb->prefix}icl_translations WHERE trid = %d AND language_code != %s", $trid, $default_lang )); foreach ($translations as $trans) { $target_lang = $trans->language_code; $trans_post_id = $trans->element_id; $prompt = "You are an expert translator. Translate the following WordPress post into language code '{$target_lang}'.\n"; $prompt .= "IMPORTANT RULES:\n"; $prompt .= "1. Preserve ALL HTML tags, Gutenberg block comments (e.g. ), and shortcodes exactly as they are. Do not translate or modify them.\n"; $prompt .= "2. ONLY translate the visible text content inside the tags/blocks. DO NOT alter ANY image URLs (src), link URLs (href), or image/block IDs (e.g. {\"id\":123}).\n"; $prompt .= "3. DO NOT translate Shortcodes (e.g. [shortcode arg=\"value\"]) or their attributes. Leave them exactly as they are.\n"; $prompt .= "4. DO NOT translate any JSON configuration inside Gutenberg blocks. Only translate the text that is meant to be visible to the user.\n"; $custom_context = get_option('aiutoma_wpml_context', ''); if (!empty($custom_context)) { $prompt .= "\nSITE CONTEXT & GLOSSARY:\n" . $custom_context . "\n\n"; } $prompt .= "5. Respond ONLY with a valid JSON object containing the keys: 'title', 'content', and 'excerpt'. Do not include any other text.\n\n"; $prompt .= "TITLE:\n" . $post->post_title . "\n\n"; $prompt .= "EXCERPT:\n" . $post->post_excerpt . "\n\n"; $prompt .= "CONTENT:\n" . $post->post_content . "\n"; $data_to_translate = [ 'title' => $post->post_title, 'excerpt' => $post->post_excerpt, 'content' => $post->post_content ]; $result = $this->do_translation_with_model($prompt, $data_to_translate, $default_lang, $target_lang); if ($result && isset($result['title']) && isset($result['content'])) { $update_args = [ 'ID' => $trans_post_id, 'post_title' => $result['title'], 'post_content' => $result['content'], 'post_excerpt' => isset($result['excerpt']) ? $result['excerpt'] : '', ]; remove_action('post_updated', [$this, 'schedule_translation_updates'], 10); wp_save_post_revision($trans_post_id); wp_update_post($update_args); add_action('post_updated', [$this, 'schedule_translation_updates'], 10, 3); } } } public function schedule_term_translation_updates($term_id, $tt_id, $taxonomy) { global $sitepress, $wpdb; if (!$sitepress) return; $default_lang = $sitepress->get_default_language(); $el_type = 'tax_' . $taxonomy; $term_lang = $sitepress->get_language_for_element($tt_id, $el_type); if ($term_lang !== $default_lang) return; $trid = $sitepress->get_element_trid($tt_id, $el_type); if (!$trid) return; $translations = $wpdb->get_results($wpdb->prepare( "SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE trid = %d AND language_code != %s", $trid, $default_lang )); if (!empty($translations)) { wp_schedule_single_event(time() + 2, 'aiutoma_wpml_update_term_translations', [$term_id, $taxonomy]); } } public function process_term_translation_updates($term_id, $taxonomy) { global $sitepress, $wpdb; if (!$sitepress) return; $term = get_term($term_id, $taxonomy); if (!$term || is_wp_error($term)) return; $el_type = 'tax_' . $taxonomy; $default_lang = $sitepress->get_default_language(); $trid = $sitepress->get_element_trid($term->term_taxonomy_id, $el_type); if (!$trid) return; $translations = $wpdb->get_results($wpdb->prepare( "SELECT element_id, language_code FROM {$wpdb->prefix}icl_translations WHERE trid = %d AND language_code != %s", $trid, $default_lang )); foreach ($translations as $trans) { $target_lang = $trans->language_code; $trans_tt_id = $trans->element_id; $trans_term = get_term_by('term_taxonomy_id', $trans_tt_id, $taxonomy); if (!$trans_term) continue; $prompt = "You are an expert translator. Translate the following WordPress term into language code '{$target_lang}'.\n"; $prompt .= "Respond ONLY with a valid JSON object containing the keys: 'name' and 'description'. Do not include any other text.\n\n"; $prompt .= "NAME:\n" . $term->name . "\n\n"; $prompt .= "DESCRIPTION:\n" . $term->description . "\n"; $data_to_translate = [ 'name' => $term->name, 'description' => $term->description ]; $result = $this->do_translation_with_model($prompt, $data_to_translate, $default_lang, $target_lang); if ($result && isset($result['name'])) { remove_action('saved_term', [$this, 'schedule_term_translation_updates'], 10); wp_update_term($trans_term->term_id, $taxonomy, [ 'name' => $result['name'], 'description' => isset($result['description']) ? $result['description'] : '' ]); add_action('saved_term', [$this, 'schedule_term_translation_updates'], 10, 3); } } } public function intercept_wpml_tm_job($job_id) { $user = get_user_by('login', 'aiutoma_translator'); if (!$user) return; $job = apply_filters('wpml_get_translation_job', null, $job_id); if ($job && isset($job->translator_id) && $job->translator_id == $user->ID) { wp_schedule_single_event(time() + 2, 'aiutoma_process_wpml_tm_job', [$job_id]); } } public function process_wpml_tm_job($job_id) { global $sitepress, $wpdb; if (!$sitepress) return; $job = apply_filters('wpml_get_translation_job', null, $job_id); if (!$job) return; $original_id = $job->original_doc_id; $target_lang = $job->language_code; $type = $job->original_post_type; if (strpos($type, 'post_') === 0) { $post = get_post($original_id); if (!$post) return; $prompt = "You are an expert translator. Translate the following WordPress post into language code '{$target_lang}'.\n"; $prompt .= "IMPORTANT RULES:\n"; $prompt .= "1. Preserve ALL HTML tags, Gutenberg block comments (e.g. ), and shortcodes exactly as they are.\n"; $prompt .= "2. ONLY translate the visible text.\n"; $prompt .= "3. Respond ONLY with a valid JSON object containing the keys: 'title', 'content', and 'excerpt'. Do not include any other text.\n\n"; $prompt .= "TITLE:\n" . $post->post_title . "\n\n"; $prompt .= "EXCERPT:\n" . $post->post_excerpt . "\n\n"; $prompt .= "CONTENT:\n" . $post->post_content . "\n"; $data_to_translate = [ 'title' => $post->post_title, 'excerpt' => $post->post_excerpt, 'content' => $post->post_content ]; $result = $this->do_translation_with_model($prompt, $data_to_translate, $sitepress->get_default_language(), $target_lang); if ($result && isset($result['title']) && isset($result['content'])) { $trid = $sitepress->get_element_trid($original_id, $type); $trans_id = $wpdb->get_var($wpdb->prepare( "SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE trid = %d AND language_code = %s", $trid, $target_lang )); if ($trans_id) { remove_action('post_updated', [$this, 'schedule_translation_updates'], 10); wp_update_post([ 'ID' => $trans_id, 'post_title' => $result['title'], 'post_content' => $result['content'], 'post_excerpt' => isset($result['excerpt']) ? $result['excerpt'] : '', ]); add_action('post_updated', [$this, 'schedule_translation_updates'], 10, 3); $wpdb->update( $wpdb->prefix . 'icl_translation_status', ['status' => 10], ['rid' => $job->rid] ); } } } } private function do_translation_with_model($prompt, $data, $source_lang, $target_lang) { $model = get_option('aiutoma_wpml_model', ''); if ($model === 'google_free' && method_exists($this, 'google_translate_free')) { $result = []; foreach ($data as $k => $v) { if ($k === 'custom_fields' && is_array($v)) { $result[$k] = []; foreach ($v as $ck => $cv) { if (is_array($cv)) { $result[$k][$ck] = []; foreach ($cv as $cvi) { $t = $this->google_translate_free($source_lang, $target_lang, $cvi); $result[$k][$ck][] = is_wp_error($t) ? $cvi : $t; } } else { $t = $this->google_translate_free($source_lang, $target_lang, $cv); $result[$k][$ck] = is_wp_error($t) ? $cv : $t; } } } else { $t = $this->google_translate_free($source_lang, $target_lang, $v); $result[$k] = is_wp_error($t) ? $v : $t; } } return $result; } return $this->call_ai_for_json($prompt); } private function call_ai_for_json($prompt) { $models_to_try = []; $requested_model = get_option('aiutoma_wpml_model', ''); if (empty($requested_model)) { $requested_model = get_user_meta(get_current_user_id(), '_aiutoma_preferred_model', true); } if (!empty($requested_model)) { $parts = explode('|', $requested_model); if (count($parts) === 2) { $models_to_try[] = $parts; } } $auto_fallback = get_option('aiutoma_wpml_auto_fallback', 0); if ($auto_fallback) { $fallback_models = get_option('aiutoma_wpml_fallback_models', []); if (empty($fallback_models)) { $all_models = []; $registry = \WordPress\AiClient\AiClient::defaultRegistry(); $requirements = new \WordPress\AiClient\Providers\Models\DTO\ModelRequirements( [\WordPress\AiClient\Providers\Models\Enums\CapabilityEnum::textGeneration()], [] ); foreach ($registry->findModelsMetadataForSupport($requirements) as $p) { foreach ($p->getModels() as $m) { $id = $m->getId(); if (stripos($id, 'dall-e') !== false || stripos($id, 'midjourney') !== false) continue; $all_models[] = $p->getProvider()->getId() . '|' . $id; } } $fallback_models = $all_models; } foreach ($fallback_models as $fm) { $parts = explode('|', $fm); if (count($parts) === 2) { $models_to_try[] = $parts; } } } if (empty($models_to_try)) { return new \WP_Error('no_models', 'No eligible AI models found. Please configure a model in Settings.'); } $models_to_try = array_map("unserialize", array_unique(array_map("serialize", $models_to_try))); $last_exception = null; foreach ($models_to_try as $model_parts) { $max_retries = 2; for ($attempt = 0; $attempt <= $max_retries; $attempt++) { try { $ai_query = \WordPress\AiClient\AiClient::prompt([ new \WordPress\AiClient\Messages\DTO\UserMessage([ new \WordPress\AiClient\Messages\DTO\MessagePart($prompt) ]) ]); $ai_query->usingModelPreference($model_parts); $ai_query->asJsonResponse(); $res = $ai_query->generateResult(); $text = $res->toText(); $text = trim(str_replace(['```json', '```'], '', $text)); $json = json_decode($text, true); if ($json) { return $json; } else { throw new \Exception("Invalid JSON returned from model. Raw text: " . $text); } } catch (\Exception $e) { $last_exception = $e; $msg = strtolower($e->getMessage()); if (strpos($msg, 'token') !== false || strpos($msg, 'quota') !== false || strpos($msg, 'credit') !== false || strpos($msg, 'rate') !== false || strpos($msg, 'limit') !== false || strpos($msg, '429') !== false || strpos($msg, '408') !== false || strpos($msg, '500') !== false || strpos($msg, '502') !== false || strpos($msg, '503') !== false || strpos($msg, 'timeout') !== false) { $this->log_error("Attempt " . ($attempt + 1) . " - API Error for model " . implode('|', $model_parts) . ": " . $e->getMessage()); if ($attempt < $max_retries) { sleep(2); continue; } } else { $this->log_error("Model " . implode('|', $model_parts) . " failed: " . $e->getMessage()); } break; } } } if ($last_exception) { $msg = $last_exception->getMessage(); if (strpos(strtolower($msg), 'token') !== false || strpos(strtolower($msg), 'quota') !== false || strpos(strtolower($msg), 'credit') !== false) { return new \WP_Error('ai_token_error', 'Insufficient API tokens/credits to complete translation. Please check your provider account.'); } return new \WP_Error('ai_exception', 'All AI models failed. Last error: ' . $msg); } return new \WP_Error('ai_unknown_error', 'Failed to generate translation.'); } }