| Server IP : 121.121.20.254 / Your IP : 216.73.216.202 Web Server : Microsoft-IIS/10.0 System : Windows NT WEB-SERVER 10.0 build 20348 (Windows Server 2022) AMD64 User : IUSR ( 0) PHP Version : 8.3.28 Disable Function : NONE MySQL : ON | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : C:/inetpub/wwwroot/AIWEBSTATION/wp-content/plugins/ai-engine-pro/premium/ |
Upload File : |
<?php
// WPML counterpart of mcp-polylang.php. WPML has no pll_*-style functions: everything
// goes through its hooks API (wpml_active_languages, wpml_element_trid,
// wpml_set_element_language_details, wpml_object_id, wpml_switch_language), and for
// terms the element IDs are term_taxonomy_ids, not term_ids. Tool outputs mirror the
// Polylang tools so agents get the same shapes on either plugin.
class MeowPro_MWAI_MCP_Wpml {
private $core = null;
#region Initialize
public function __construct( $core ) {
$this->core = $core;
add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
}
public function rest_api_init() {
add_filter( 'mwai_mcp_tools', [ $this, 'register_rest_tools' ] );
add_filter( 'mwai_mcp_callback', [ $this, 'handle_call' ], 10, 4 );
}
#endregion
#region Helpers
private function empty_schema(): array {
return [ 'type' => 'object', 'properties' => (object) [] ];
}
private function get_wpml_languages(): array {
$default = apply_filters( 'wpml_default_language', null );
$active = apply_filters( 'wpml_active_languages', null, [ 'skip_missing' => 0 ] );
$languages = [];
foreach ( (array) $active as $lang ) {
$languages[] = [
'slug' => $lang['code'],
'name' => $lang['native_name'],
'locale' => $lang['default_locale'],
'is_default' => $lang['code'] === $default,
'flag_url' => $lang['country_flag_url'] ?? null,
];
}
return $languages;
}
private function validate_language( string $lang ): bool {
foreach ( $this->get_wpml_languages() as $l ) {
if ( $l['slug'] === $lang ) {
return true;
}
}
return false;
}
private function get_post_type_label( string $post_type ): string {
$obj = get_post_type_object( $post_type );
return $obj ? $obj->labels->singular_name : $post_type;
}
private function get_post_language( int $post_id ): ?string {
$details = apply_filters( 'wpml_post_language_details', null, $post_id );
if ( is_wp_error( $details ) || !is_array( $details ) || empty( $details['language_code'] ) ) {
return null;
}
return $details['language_code'];
}
private function post_element_type( int $post_id ): string {
return 'post_' . get_post_type( $post_id );
}
// Runs a callback with WPML switched to another language ('all' works too),
// always restoring the previous language.
private function with_language( string $lang, callable $fn ) {
$current = apply_filters( 'wpml_current_language', null );
do_action( 'wpml_switch_language', $lang );
try {
return $fn();
}
finally {
do_action( 'wpml_switch_language', $current );
}
}
// Translations of a post, as [ lang => post_id ], mirroring pll_get_post_translations.
private function get_post_translations_map( int $post_id ): array {
$el_type = $this->post_element_type( $post_id );
$trid = apply_filters( 'wpml_element_trid', null, $post_id, $el_type );
if ( !$trid ) {
return [];
}
$translations = apply_filters( 'wpml_get_element_translations', null, $trid, $el_type );
$map = [];
foreach ( (array) $translations as $lang => $t ) {
if ( !empty( $t->element_id ) ) {
$map[ $lang ] = (int) $t->element_id;
}
}
return $map;
}
#endregion
#region Tools
private function tools(): array {
return [
/* ───────── Languages ───────── */
'wpml_get_languages' => [
'name' => 'wpml_get_languages',
'description' => 'List all configured languages in WPML (slug, name, locale, is_default, flag_url).',
'inputSchema' => $this->empty_schema(),
'accessLevel' => 'read',
],
/* ───────── Post Language ───────── */
'wpml_get_post_language' => [
'name' => 'wpml_get_post_language',
'description' => 'Get the language of a specific post.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'post_id' => [ 'type' => 'integer', 'description' => 'The post ID' ],
],
'required' => [ 'post_id' ],
],
'accessLevel' => 'read',
],
'wpml_set_post_language' => [
'name' => 'wpml_set_post_language',
'description' => 'Set the language of a post.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'post_id' => [ 'type' => 'integer', 'description' => 'The post ID' ],
'lang' => [ 'type' => 'string', 'description' => 'Language slug (e.g., "en", "fr", "ja")' ],
],
'required' => [ 'post_id', 'lang' ],
],
'accessLevel' => 'write',
],
/* ───────── Post Translations ───────── */
'wpml_get_post_translations' => [
'name' => 'wpml_get_post_translations',
'description' => 'Get all translations of a post. Returns an object mapping language slugs to post IDs.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'post_id' => [ 'type' => 'integer', 'description' => 'The post ID' ],
],
'required' => [ 'post_id' ],
],
'accessLevel' => 'read',
],
'wpml_link_translations' => [
'name' => 'wpml_link_translations',
'description' => 'Link multiple posts as translations of each other. All posts must have a language set and share the same post type.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'translations' => [
'type' => 'object',
'description' => 'Object mapping language slugs to post IDs, e.g., {"en": 123, "fr": 456}',
'additionalProperties' => [ 'type' => 'integer' ],
],
],
'required' => [ 'translations' ],
],
'accessLevel' => 'write',
],
/* ───────── Term Translations ───────── */
'wpml_get_term_translations' => [
'name' => 'wpml_get_term_translations',
'description' => 'Get all translations of a term. Returns an object mapping language slugs to term IDs.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'term_id' => [ 'type' => 'integer', 'description' => 'The term ID' ],
],
'required' => [ 'term_id' ],
],
'accessLevel' => 'read',
],
'wpml_translate_term' => [
'name' => 'wpml_translate_term',
'description' => 'Get a specific translation of a term. Returns the term ID in the target language, or null if not available.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'term_id' => [ 'type' => 'integer', 'description' => 'The term ID' ],
'lang' => [ 'type' => 'string', 'description' => 'Target language slug' ],
],
'required' => [ 'term_id', 'lang' ],
],
'accessLevel' => 'read',
],
/* ───────── Query Posts by Language ───────── */
'wpml_get_posts' => [
'name' => 'wpml_get_posts',
'description' => 'Get posts filtered by language. Wraps wp_get_posts with WPML language support.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'lang' => [ 'type' => 'string', 'description' => 'Language slug (e.g., "en", "ja"). Use empty string "" to get posts in all languages.' ],
'post_type' => [ 'type' => 'string', 'description' => 'Post type (default: post)' ],
'post_status' => [ 'type' => 'string', 'description' => 'Post status (default: publish)' ],
'limit' => [ 'type' => 'integer', 'description' => 'Maximum number of results (default: 10)' ],
'offset' => [ 'type' => 'integer', 'description' => 'Number of posts to skip (default: 0)' ],
'orderby' => [ 'type' => 'string', 'description' => 'Order by field (default: date)' ],
'order' => [ 'type' => 'string', 'description' => 'Order direction: ASC or DESC (default: DESC)' ],
],
'required' => [ 'lang' ],
],
'accessLevel' => 'read',
],
/* ───────── Missing Translations ───────── */
'wpml_get_posts_missing_translation' => [
'name' => 'wpml_get_posts_missing_translation',
'description' => 'Find posts that are missing a translation in a specific language.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'source_lang' => [ 'type' => 'string', 'description' => 'Source language slug to search in' ],
'target_lang' => [ 'type' => 'string', 'description' => 'Target language slug that is missing' ],
'post_type' => [ 'type' => 'string', 'description' => 'Post type to filter (default: post)' ],
'limit' => [ 'type' => 'integer', 'description' => 'Maximum number of results (default: 20)' ],
],
'required' => [ 'source_lang', 'target_lang' ],
],
'accessLevel' => 'read',
],
/* ───────── Create Translation ───────── */
'wpml_create_translation' => [
'name' => 'wpml_create_translation',
'description' => 'Create a new post as a translation of an existing post. Creates the post, sets its language, and links it to the source. Can optionally translate content, categories, and tags.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'source_post_id' => [ 'type' => 'integer', 'description' => 'The source post ID to translate from' ],
'target_lang' => [ 'type' => 'string', 'description' => 'Target language slug for the new translation' ],
'title' => [ 'type' => 'string', 'description' => 'Translated title' ],
'content' => [ 'type' => 'string', 'description' => 'Translated content (optional, copies from source if not provided)' ],
'excerpt' => [ 'type' => 'string', 'description' => 'Translated excerpt (optional)' ],
'status' => [ 'type' => 'string', 'description' => 'Post status: draft, publish, pending, private, future (default: draft)' ],
'post_date' => [ 'type' => 'string', 'description' => 'Post date in ISO 8601 or MySQL format (required for future status, e.g., "2025-12-31 10:00:00")' ],
'translate_terms' => [ 'type' => 'boolean', 'description' => 'Automatically link translated categories/tags if they exist (default: true)' ],
'copy_featured_image' => [ 'type' => 'boolean', 'description' => 'Copy the featured image to the new translation (default: true)' ],
],
'required' => [ 'source_post_id', 'target_lang', 'title' ],
],
'accessLevel' => 'write',
],
/* ───────── Translation Status ───────── */
'wpml_translation_status' => [
'name' => 'wpml_translation_status',
'description' => 'Get an overview of translation coverage. Shows how many posts exist in each language and the percentage translated.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'post_type' => [ 'type' => 'string', 'description' => 'Post type to analyze (default: post)' ],
],
],
'accessLevel' => 'read',
],
];
}
#endregion
#region Register
public function register_rest_tools( array $prev ): array {
$tools = $this->tools();
// Add category and annotations to each tool
foreach ( $tools as &$tool ) {
if ( !isset( $tool['category'] ) ) {
$tool['category'] = 'AI Engine (WPML)';
}
// Add MCP tool annotations
if ( !isset( $tool['annotations'] ) ) {
$name = $tool['name'];
// Read-only tools
$is_readonly = (
$name === 'wpml_get_languages' ||
$name === 'wpml_get_posts' ||
$name === 'wpml_translation_status' ||
strpos( $name, '_get_' ) !== false
);
// Destructive tools (none in WPML integration)
$is_destructive = false;
$tool['annotations'] = [
'readOnlyHint' => $is_readonly,
'destructiveHint' => $is_destructive,
'openWorldHint' => false,
];
}
}
$merged = array_merge( $prev, array_values( $tools ) );
return $merged;
}
#endregion
#region Callback
public function handle_call( $prev, string $tool, array $args, ?int $id ) {
if ( !empty( $prev ) || !isset( $this->tools()[ $tool ] ) ) {
return $prev; // handled elsewhere or unknown
}
if ( !current_user_can( 'administrator' ) ) {
wp_set_current_user( 1 );
}
return $this->dispatch( $tool, $args, $id );
}
#endregion
#region Dispatcher
private function dispatch( string $tool, array $a, ?int $id ) {
// Check if WPML is available
if ( !defined( 'ICL_SITEPRESS_VERSION' ) ) {
throw new Exception( 'WPML is not active' );
}
switch ( $tool ) {
/* ───────── Languages ───────── */
case 'wpml_get_languages':
return $this->get_wpml_languages();
/* ───────── Post Language ───────── */
case 'wpml_get_post_language':
$post_id = (int) ( $a['post_id'] ?? 0 );
if ( !$post_id || !get_post( $post_id ) ) {
throw new Exception( 'Invalid post ID' );
}
$lang = $this->get_post_language( $post_id );
if ( !$lang ) {
return [ 'post_id' => $post_id, 'language' => null, 'message' => 'No language set' ];
}
return [ 'post_id' => $post_id, 'language' => $lang ];
case 'wpml_set_post_language':
$post_id = (int) ( $a['post_id'] ?? 0 );
$lang = sanitize_text_field( $a['lang'] ?? '' );
if ( !$post_id || !get_post( $post_id ) ) {
throw new Exception( 'Invalid post ID' );
}
if ( !$lang || !$this->validate_language( $lang ) ) {
throw new Exception( 'Invalid language slug' );
}
$el_type = $this->post_element_type( $post_id );
// Keep the existing trid so changing the language never unlinks translations.
$trid = apply_filters( 'wpml_element_trid', null, $post_id, $el_type );
do_action( 'wpml_set_element_language_details', [
'element_id' => $post_id,
'element_type' => $el_type,
'trid' => $trid,
'language_code' => $lang,
'source_language_code' => null,
] );
return [ 'post_id' => $post_id, 'language' => $lang, 'message' => 'Language set successfully' ];
/* ───────── Post Translations ───────── */
case 'wpml_get_post_translations':
$post_id = (int) ( $a['post_id'] ?? 0 );
if ( !$post_id || !get_post( $post_id ) ) {
throw new Exception( 'Invalid post ID' );
}
$translations = $this->get_post_translations_map( $post_id );
$result = [];
foreach ( $translations as $lang => $translated_post_id ) {
$post = get_post( $translated_post_id );
$result[ $lang ] = [
'post_id' => $translated_post_id,
'title' => $post ? $post->post_title : null,
'status' => $post ? $post->post_status : null,
];
}
return [ 'source_post_id' => $post_id, 'translations' => $result ];
case 'wpml_link_translations':
$translations = $a['translations'] ?? [];
// Handle JSON strings (some MCP clients send objects as JSON strings)
if ( is_string( $translations ) ) {
$translations = json_decode( $translations, true ) ?? [];
}
if ( empty( $translations ) || !is_array( $translations ) ) {
throw new Exception( 'translations must be an object mapping language slugs to post IDs' );
}
// Validate all posts and languages; they must share one post type since the
// WPML translation group (trid) is scoped to a single element type.
$post_types = [];
foreach ( $translations as $lang => $post_id ) {
if ( !$this->validate_language( $lang ) ) {
throw new Exception( "Invalid language: $lang" );
}
if ( !get_post( $post_id ) ) {
throw new Exception( "Invalid post ID: $post_id" );
}
$post_lang = $this->get_post_language( (int) $post_id );
if ( $post_lang !== $lang ) {
throw new Exception( "Post $post_id has language '$post_lang', expected '$lang'" );
}
$post_types[ get_post_type( $post_id ) ] = true;
}
if ( count( $post_types ) > 1 ) {
throw new Exception( 'All posts must share the same post type' );
}
// Anchor the group on the default language when present, else the first entry.
$default = apply_filters( 'wpml_default_language', null );
$source_lang = isset( $translations[ $default ] ) ? $default : array_key_first( $translations );
$source_id = (int) $translations[ $source_lang ];
$el_type = $this->post_element_type( $source_id );
$trid = apply_filters( 'wpml_element_trid', null, $source_id, $el_type );
foreach ( $translations as $lang => $post_id ) {
if ( (int) $post_id === $source_id ) {
continue;
}
do_action( 'wpml_set_element_language_details', [
'element_id' => (int) $post_id,
'element_type' => $el_type,
'trid' => $trid,
'language_code' => $lang,
'source_language_code' => $source_lang,
] );
}
return [ 'linked' => $translations, 'message' => 'Posts linked as translations' ];
/* ───────── Term Translations ───────── */
case 'wpml_get_term_translations':
$term_id = (int) ( $a['term_id'] ?? 0 );
$term = $term_id ? get_term( $term_id ) : null;
if ( !$term || is_wp_error( $term ) ) {
throw new Exception( 'Invalid term ID' );
}
// WPML tracks taxonomy elements by term_taxonomy_id, not term_id.
$el_type = 'tax_' . $term->taxonomy;
$trid = apply_filters( 'wpml_element_trid', null, $term->term_taxonomy_id, $el_type );
$translations = $trid ?
apply_filters( 'wpml_get_element_translations', null, $trid, $el_type ) : [];
$result = [];
foreach ( (array) $translations as $lang => $t ) {
if ( empty( $t->element_id ) ) {
continue;
}
$translated = get_term_by( 'term_taxonomy_id', (int) $t->element_id, $term->taxonomy );
if ( $translated ) {
$result[ $lang ] = [
'term_id' => $translated->term_id,
'name' => $translated->name,
'taxonomy' => $translated->taxonomy,
];
}
}
return [ 'source_term_id' => $term_id, 'translations' => $result ];
case 'wpml_translate_term':
$term_id = (int) ( $a['term_id'] ?? 0 );
$lang = sanitize_text_field( $a['lang'] ?? '' );
$term = $term_id ? get_term( $term_id ) : null;
if ( !$term || is_wp_error( $term ) ) {
throw new Exception( 'Invalid term ID' );
}
if ( !$lang || !$this->validate_language( $lang ) ) {
throw new Exception( 'Invalid language slug' );
}
$translated_id = apply_filters( 'wpml_object_id', $term_id, $term->taxonomy, false, $lang );
if ( !$translated_id ) {
return [ 'term_id' => $term_id, 'target_lang' => $lang, 'translated_term_id' => null, 'message' => 'No translation found' ];
}
$translated = get_term( $translated_id );
return [
'term_id' => $term_id,
'target_lang' => $lang,
'translated_term_id' => (int) $translated_id,
'name' => ( $translated && !is_wp_error( $translated ) ) ? $translated->name : null,
];
/* ───────── Query Posts by Language ───────── */
case 'wpml_get_posts':
$lang = $a['lang'] ?? null;
$post_type = sanitize_text_field( $a['post_type'] ?? 'post' );
$post_status = sanitize_text_field( $a['post_status'] ?? 'publish' );
$limit = (int) ( $a['limit'] ?? 10 );
$limit = max( 1, min( 100, $limit ) );
$offset = (int) ( $a['offset'] ?? 0 );
$orderby = sanitize_text_field( $a['orderby'] ?? 'date' );
$order = strtoupper( sanitize_text_field( $a['order'] ?? 'DESC' ) );
$order = in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC';
// Validate language if provided (empty string means all languages)
if ( $lang !== '' && $lang !== null && !$this->validate_language( $lang ) ) {
throw new Exception( 'Invalid language slug' );
}
$query_args = [
'post_type' => $post_type,
'post_status' => $post_status,
'posts_per_page' => $limit,
'offset' => $offset,
'orderby' => $orderby,
'order' => $order,
'suppress_filters' => false, // Required for WPML's language filter to work
];
// WPML filters queries by the current language, so run the query switched.
$switch_to = ( $lang === '' || $lang === null ) ? 'all' : $lang;
$posts = $this->with_language( $switch_to, function () use ( $query_args ) {
return get_posts( $query_args );
} );
$result = [];
foreach ( $posts as $post ) {
$result[] = [
'ID' => $post->ID,
'post_title' => $post->post_title,
'post_type' => $post->post_type,
'post_status' => $post->post_status,
'post_date' => $post->post_date,
'language' => $this->get_post_language( $post->ID ),
'permalink' => get_permalink( $post->ID ),
];
}
return [
'lang' => $lang,
'post_type' => $post_type,
'count' => count( $result ),
'posts' => $result,
];
/* ───────── Missing Translations ───────── */
case 'wpml_get_posts_missing_translation':
$source_lang = sanitize_text_field( $a['source_lang'] ?? '' );
$target_lang = sanitize_text_field( $a['target_lang'] ?? '' );
$post_type = sanitize_text_field( $a['post_type'] ?? 'post' );
$limit = (int) ( $a['limit'] ?? 20 );
$limit = max( 1, min( 100, $limit ) );
if ( !$source_lang || !$this->validate_language( $source_lang ) ) {
throw new Exception( 'Invalid source language' );
}
if ( !$target_lang || !$this->validate_language( $target_lang ) ) {
throw new Exception( 'Invalid target language' );
}
if ( $source_lang === $target_lang ) {
throw new Exception( 'Source and target languages must be different' );
}
// Get posts in source language
$post_ids = $this->with_language( $source_lang, function () use ( $post_type ) {
return get_posts( [
'post_type' => $post_type,
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
'suppress_filters' => false,
] );
} );
$missing = [];
foreach ( $post_ids as $post_id ) {
$translation = apply_filters( 'wpml_object_id', $post_id, $post_type, false, $target_lang );
if ( !$translation ) {
$post = get_post( $post_id );
$missing[] = [
'post_id' => $post_id,
'title' => $post->post_title,
'status' => $post->post_status,
'post_type' => $post->post_type,
];
if ( count( $missing ) >= $limit ) {
break;
}
}
}
return [
'source_lang' => $source_lang,
'target_lang' => $target_lang,
'post_type' => $post_type,
'total_in_source' => count( $post_ids ),
'missing_count' => count( $missing ),
'posts' => $missing,
];
/* ───────── Create Translation ───────── */
case 'wpml_create_translation':
$source_post_id = (int) ( $a['source_post_id'] ?? 0 );
$target_lang = sanitize_text_field( $a['target_lang'] ?? '' );
$title = sanitize_text_field( $a['title'] ?? '' );
$content = $a['content'] ?? null;
$excerpt = $a['excerpt'] ?? null;
$status = sanitize_text_field( $a['status'] ?? 'draft' );
$post_date = isset( $a['post_date'] ) ? sanitize_text_field( $a['post_date'] ) : null;
$translate_terms = $a['translate_terms'] ?? true;
$copy_featured_image = $a['copy_featured_image'] ?? true;
// Validate source post
$source_post = get_post( $source_post_id );
if ( !$source_post ) {
throw new Exception( 'Invalid source post ID' );
}
// Validate target language
if ( !$target_lang || !$this->validate_language( $target_lang ) ) {
throw new Exception( 'Invalid target language' );
}
// Check if translation already exists
$existing = apply_filters( 'wpml_object_id', $source_post_id, $source_post->post_type, false, $target_lang );
if ( $existing && (int) $existing !== $source_post_id ) {
throw new Exception( "Translation already exists in $target_lang (post ID: $existing)" );
}
// Get source language
$source_lang = $this->get_post_language( $source_post_id );
if ( !$source_lang ) {
throw new Exception( 'Source post has no language set' );
}
if ( $source_lang === $target_lang ) {
throw new Exception( 'Source and target languages must be different' );
}
// Validate title
if ( empty( $title ) ) {
throw new Exception( 'Title is required' );
}
// Validate status
$allowed_statuses = [ 'draft', 'publish', 'pending', 'private', 'future' ];
if ( !in_array( $status, $allowed_statuses, true ) ) {
$status = 'draft';
}
// Validate future status requires post_date
if ( $status === 'future' && empty( $post_date ) ) {
throw new Exception( 'post_date is required when status is "future"' );
}
// Prepare post data
$post_data = [
'post_type' => $source_post->post_type,
'post_title' => $title,
'post_content' => $content !== null ? $content : $source_post->post_content,
'post_excerpt' => $excerpt !== null ? $excerpt : $source_post->post_excerpt,
'post_status' => $status,
'post_author' => $source_post->post_author,
];
// Handle scheduled posts
if ( $post_date ) {
$post_data['post_date'] = $post_date;
$post_data['post_date_gmt'] = get_gmt_from_date( $post_date );
}
// Create the new post switched to the target language, so WPML's own
// save-time hooks see a consistent language context.
$new_post_id = $this->with_language( $target_lang, function () use ( $post_data ) {
return wp_insert_post( $post_data, true );
} );
if ( is_wp_error( $new_post_id ) ) {
throw new Exception( 'Failed to create post: ' . $new_post_id->get_error_message() );
}
// Set language and link to the source's translation group (same trid)
$el_type = $this->post_element_type( $source_post_id );
$trid = apply_filters( 'wpml_element_trid', null, $source_post_id, $el_type );
do_action( 'wpml_set_element_language_details', [
'element_id' => $new_post_id,
'element_type' => $el_type,
'trid' => $trid,
'language_code' => $target_lang,
'source_language_code' => $source_lang,
] );
// Translate terms if requested
$terms_linked = [];
if ( $translate_terms ) {
$taxonomies = get_object_taxonomies( $source_post->post_type );
foreach ( $taxonomies as $taxonomy ) {
$is_translated = apply_filters( 'wpml_is_translated_taxonomy', null, $taxonomy );
if ( !$is_translated ) {
continue;
}
$terms = wp_get_object_terms( $source_post_id, $taxonomy, [ 'fields' => 'ids' ] );
$translated_terms = [];
foreach ( $terms as $term_id ) {
$translated_term = apply_filters( 'wpml_object_id', $term_id, $taxonomy, false, $target_lang );
if ( $translated_term ) {
$translated_terms[] = (int) $translated_term;
}
}
if ( !empty( $translated_terms ) ) {
$this->with_language( $target_lang, function () use ( $new_post_id, $translated_terms, $taxonomy ) {
wp_set_object_terms( $new_post_id, $translated_terms, $taxonomy );
} );
$terms_linked[ $taxonomy ] = count( $translated_terms );
}
}
}
// Copy featured image if requested
$featured_image_copied = false;
if ( $copy_featured_image ) {
$thumbnail_id = get_post_thumbnail_id( $source_post_id );
if ( $thumbnail_id ) {
set_post_thumbnail( $new_post_id, $thumbnail_id );
$featured_image_copied = true;
}
}
$result = [
'new_post_id' => $new_post_id,
'source_post_id' => $source_post_id,
'source_lang' => $source_lang,
'target_lang' => $target_lang,
'title' => $title,
'status' => $status,
'terms_linked' => $terms_linked,
'featured_image_copied' => $featured_image_copied,
'message' => 'Translation created successfully',
];
if ( $post_date ) {
$result['post_date'] = $post_date;
}
return $result;
/* ───────── Translation Status ───────── */
case 'wpml_translation_status':
$post_type = sanitize_text_field( $a['post_type'] ?? 'post' );
$languages = $this->get_wpml_languages();
$post_type_label = $this->get_post_type_label( $post_type );
$stats = [];
$total_posts = 0;
$default_lang = null;
// Count posts per language
foreach ( $languages as $lang ) {
$count = count( $this->with_language( $lang['slug'], function () use ( $post_type ) {
return get_posts( [
'post_type' => $post_type,
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
'suppress_filters' => false,
] );
} ) );
$stats[ $lang['slug'] ] = [
'name' => $lang['name'],
'count' => $count,
];
$total_posts += $count;
if ( $lang['is_default'] ) {
$default_lang = $lang['slug'];
}
}
// Calculate translation coverage
$default_count = $stats[ $default_lang ]['count'] ?? 0;
foreach ( $stats as $lang => &$data ) {
if ( $lang === $default_lang ) {
$data['coverage'] = '100%';
}
elseif ( $default_count > 0 ) {
$percentage = round( ( $data['count'] / $default_count ) * 100, 1 );
$data['coverage'] = $percentage . '%';
}
else {
$data['coverage'] = 'N/A';
}
}
return [
'post_type' => $post_type,
'post_type_label' => $post_type_label,
'default_language' => $default_lang,
'total_posts' => $total_posts,
'languages' => $stats,
];
default:
throw new Exception( 'Unknown tool' );
}
}
#endregion
}