robotstxt-ai-translator/includes/class-ai-translator-cli.php
2026-05-25 17:12:22 +00:00

250 lines
7.4 KiB
PHP

<?php
/**
* WP-CLI commands for the AI Translator plugin.
*
* @package ROBOTSTXT\AI_Translator
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'AI_Translator_CLI' ) ) {
/**
* Translate posts from the command line.
*
* ## EXAMPLES
*
* # Translate a single post to Spanish.
* $ wp ai-translator translate --post-id=42 --locale=es_ES
*
* # Translate every post of a given type, preview only.
* $ wp ai-translator translate --post-type=post --locale=fr_FR --dry-run
*
* @since 1.0.0
*/
class AI_Translator_CLI {
/**
* Settings handler.
*
* @since 1.0.0
* @var AI_Translator_Settings
*/
private $settings;
/**
* Translator handler.
*
* @since 1.0.0
* @var AI_Translator_Translator
*/
private $translator;
/**
* Constructor.
*
* @since 1.0.0
*
* @param AI_Translator_Settings $settings Settings handler.
* @param AI_Translator_Translator $translator Translator handler.
*/
public function __construct( AI_Translator_Settings $settings, AI_Translator_Translator $translator ) {
$this->settings = $settings;
$this->translator = $translator;
}
/**
* Registers the command with WP-CLI.
*
* @since 1.0.0
*
* @return void
*/
public function register() {
if ( ! class_exists( 'WP_CLI' ) ) {
return;
}
WP_CLI::add_command( 'ai-translator', $this );
}
/**
* Translates one or more posts to a target locale.
*
* ## OPTIONS
*
* --locale=<locale>
* : WordPress locale to translate to. Must be installed on the site.
*
* [--post-id=<id>]
* : ID of a single post to translate. Mutually exclusive with --post-type.
*
* [--post-type=<type>]
* : Post type to translate in bulk. Defaults to 'post' when --post-id is omitted.
*
* [--status=<status>]
* : Post status filter when running in bulk mode. Default: 'publish'.
*
* [--limit=<n>]
* : Maximum number of posts to process when running in bulk mode. Default: 50.
*
* [--dry-run]
* : Show what would be translated without calling the AI provider and without writing to the database.
*
* ## EXAMPLES
*
* wp ai-translator translate --post-id=42 --locale=es_ES
* wp ai-translator translate --post-type=page --locale=fr_FR --limit=10 --dry-run
*
* @since 1.0.0
*
* @param array<int,string> $args Positional arguments (unused).
* @param array<string,string> $assoc_args Associative arguments.
*
* @return void
*/
public function translate( $args, $assoc_args ) {
unset( $args );
$locale = isset( $assoc_args['locale'] ) ? (string) $assoc_args['locale'] : '';
$dry_run = isset( $assoc_args['dry-run'] );
$post_id = isset( $assoc_args['post-id'] ) ? (int) $assoc_args['post-id'] : 0;
$type = isset( $assoc_args['post-type'] ) ? (string) $assoc_args['post-type'] : '';
$status = isset( $assoc_args['status'] ) ? (string) $assoc_args['status'] : 'publish';
$limit = isset( $assoc_args['limit'] ) ? max( 1, (int) $assoc_args['limit'] ) : 50;
if ( '' === $locale ) {
WP_CLI::error( __( 'The --locale flag is required.', 'robotstxt-ai-translator' ) );
}
if ( $post_id > 0 && '' !== $type ) {
WP_CLI::error( __( 'The --post-id and --post-type flags are mutually exclusive.', 'robotstxt-ai-translator' ) );
}
// Validate locale before AI checks so the user sees the right error when
// both the locale is missing and the AI provider is unconfigured.
if ( ! in_array( $locale, $this->translator->get_installed_locales(), true ) ) {
WP_CLI::error(
sprintf(
/* translators: %s: locale code. */
__( 'The locale %s is not installed on this site.', 'robotstxt-ai-translator' ),
$locale
)
);
}
$current_user = wp_get_current_user();
if ( ! $current_user || 0 === (int) $current_user->ID ) {
WP_CLI::warning( __( 'No user context. Pass --user=<id|login> so post updates carry an author and capability checks apply.', 'robotstxt-ai-translator' ) );
}
if ( ! $dry_run && ! $this->translator->is_available() ) {
WP_CLI::error( __( 'AI features are disabled for this WordPress installation.', 'robotstxt-ai-translator' ) );
}
if ( ! $dry_run && ! $this->translator->is_supported() ) {
WP_CLI::error( __( 'No AI provider is configured for text generation. Open Settings → AI to set one up.', 'robotstxt-ai-translator' ) );
}
$settings = $this->settings->get_settings();
if ( empty( $settings['translate_title'] ) && empty( $settings['translate_content'] ) && empty( $settings['translate_excerpt'] ) ) {
WP_CLI::error( __( 'No translation fields are enabled in the plugin settings.', 'robotstxt-ai-translator' ) );
}
if ( $post_id > 0 ) {
$posts = array( get_post( $post_id ) );
} else {
$type = '' !== $type ? $type : 'post';
$posts = get_posts(
array(
'post_type' => $type,
'post_status' => $status,
'numberposts' => $limit,
'ignore_sticky_posts' => true,
'suppress_filters' => false,
)
);
}
$posts = array_filter(
$posts,
static function ( $post ) {
return $post instanceof WP_Post;
}
);
if ( empty( $posts ) ) {
WP_CLI::warning( __( 'No posts matched the given criteria.', 'robotstxt-ai-translator' ) );
return;
}
$successes = 0;
$failures = 0;
foreach ( $posts as $post ) {
if ( $dry_run ) {
WP_CLI::log( sprintf( '[dry-run] would translate post #%d (%s) to %s.', $post->ID, $post->post_title, $locale ) );
++$successes;
continue;
}
$update = array( 'ID' => $post->ID );
if ( ! empty( $settings['translate_title'] ) ) {
$translated_title = $this->translator->translate( $post->post_title, $locale );
if ( is_wp_error( $translated_title ) ) {
WP_CLI::warning( sprintf( '#%d title: %s', $post->ID, $translated_title->get_error_message() ) );
++$failures;
continue;
}
$update['post_title'] = $translated_title;
}
if ( ! empty( $settings['translate_content'] ) ) {
$translated_content = $this->translator->translate( $post->post_content, $locale );
if ( is_wp_error( $translated_content ) ) {
WP_CLI::warning( sprintf( '#%d content: %s', $post->ID, $translated_content->get_error_message() ) );
++$failures;
continue;
}
$update['post_content'] = $translated_content;
}
if ( ! empty( $settings['translate_excerpt'] ) && post_type_supports( $post->post_type, 'excerpt' ) ) {
$translated_excerpt = $this->translator->translate( $post->post_excerpt, $locale );
if ( is_wp_error( $translated_excerpt ) ) {
WP_CLI::warning( sprintf( '#%d excerpt: %s', $post->ID, $translated_excerpt->get_error_message() ) );
++$failures;
continue;
}
$update['post_excerpt'] = $translated_excerpt;
}
$result = wp_update_post( $update, true );
if ( is_wp_error( $result ) ) {
WP_CLI::warning( sprintf( '#%d save: %s', $post->ID, $result->get_error_message() ) );
++$failures;
continue;
}
WP_CLI::log( sprintf( 'Updated post #%d.', $post->ID ) );
++$successes;
}
WP_CLI::success(
sprintf(
/* translators: 1: success count, 2: failure count, 3: locale. */
__( '%1$d translated, %2$d failed (locale: %3$s).', 'robotstxt-ai-translator' ),
$successes,
$failures,
$locale
)
);
}
}
}