Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions includes/Abilities/Content_Translation/Content_Translation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
<?php
/**
* Content translation WordPress Ability implementation.
*
* @package WordPress\AI
*/

declare( strict_types=1 );

namespace WordPress\AI\Abilities\Content_Translation;

use WP_Error;
use WordPress\AI\Abstracts\Abstract_Ability;
use WordPress\AI\Experiments\Content_Translation\Content_Translation as Content_Translation_Experiment;

use function WordPress\AI\count_words;

/**
* Content Translation WordPress Ability.
*
* @since x.x.x
*/
class Content_Translation extends Abstract_Ability {

/**
* The minimum word count for translation.
*
* @since x.x.x
*
* @var int
*/
protected const MIN_WORDS = 1;

/**
* {@inheritDoc}
*
* @since x.x.x
*/
protected function input_schema(): array {
return array(
'type' => 'object',
'properties' => array(
'post_id' => array(
'type' => 'integer',
'description' => esc_html__( 'The ID of the post to translate content for.', 'ai' ),
),
'content' => array(
'type' => 'string',
'description' => esc_html__( 'The block content to translate.', 'ai' ),
),
'target_language' => array(
'type' => 'string',
'enum' => Languages::get_codes(),
'default' => Languages::get_default_target_language(),
'description' => esc_html__( 'The target language for translation.', 'ai' ),
),
),
);
}

/**
* {@inheritDoc}
*
* @since x.x.x
*/
protected function output_schema(): array {
return array(
'type' => 'string',
'description' => esc_html__( 'The translated content.', 'ai' ),
);
}

/**
* {@inheritDoc}
*
* @since x.x.x
*/
protected function execute_callback( $input ) {
// Default arguments for the translation process.
$args = wp_parse_args(
$input,
array(
'post_id' => null,
'content' => null,
'target_language' => Languages::get_default_target_language(),
)
);

// Skip normalization of content to retain HTML tags.
$content = $args['content'] ?? '';

if ( empty( $content ) ) {
return new WP_Error(
'content_not_provided',
esc_html__( 'No content provided for translation.', 'ai' )
);
}

if ( count_words( wp_strip_all_tags( $content ) ) < self::MIN_WORDS ) {
return new WP_Error(
'content_too_short',
sprintf(
/* translators: %d: minimum number of words required for translation */
esc_html__( 'A minimum of %d words is required for translation.', 'ai' ),
self::MIN_WORDS
)
);
}

$prompt = sprintf( '<content>%s</content>', $content );

// Validate the target language.
$target_language = sanitize_key( (string) $args['target_language'] );
$language = Languages::get_language_name( $target_language );
if ( null === $language ) {
return new WP_Error(
'invalid_target_language',
esc_html__( 'The specified target language is not supported for translation.', 'ai' )
);
}

$result = $this->generate_translated_content( $prompt, $language );

if ( is_wp_error( $result ) ) {
return $result;
}

if ( empty( $result ) ) {
return new WP_Error(
'no_results',
esc_html__( 'No translated content was generated.', 'ai' )
);
}

return wp_kses_post( $result );
}

/**
* {@inheritDoc}
*
* @since x.x.x
*/
protected function permission_callback( $args ) {
// Ensure the user has permission to edit the post if a post ID is provided.
if ( isset( $args['post_id'] ) ) {
$post_id = absint( $args['post_id'] );
$post = get_post( $post_id );

if ( ! $post ) {
return new WP_Error(
'post_not_found',
sprintf(
/* translators: %d: post ID */
esc_html__( 'Post with ID %d not found.', 'ai' ),
$post_id
)
);
}

if ( ! current_user_can( 'edit_post', $post_id ) ) {
return new WP_Error(
'insufficient_permissions',
esc_html__( 'You do not have permission to edit this post.', 'ai' )
);
}
} elseif ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error(
'insufficient_permissions',
esc_html__( 'You do not have permission to translate content.', 'ai' )
);
}

return true;
}

/**
* {@inheritDoc}
*
* @since x.x.x
*/
protected function meta(): array {
return array(
'show_in_rest' => true,
);
}

/**
* Generates translated content using the AI Client.
*
* @since x.x.x
*
* @param string $prompt The prompt to use for the content translation.
* @param string $language The target language for the translation.
* @return string|\WP_Error The translated content, or a WP_Error if there was an error.
*/
protected function generate_translated_content( string $prompt, string $language ) {
$builder = $this->get_prompt_builder( $prompt, $language );

if ( is_wp_error( $builder ) ) {
return $builder;
}

return $builder->generate_text();
}

/**
* Returns a prompt builder for content translation.
*
* @since x.x.x
*
* @param string $prompt The prompt to build.
* @param string $language The target language.
* @return \WP_AI_Client_Prompt_Builder|\WP_Error The prompt builder, or a WP_Error if there isn't a model that supports text generation.
*/
private function get_prompt_builder( string $prompt, string $language ) {
$prompt_builder = wp_ai_client_prompt( $prompt )
->using_system_instruction(
$this->get_system_instruction(
'system-instruction.php',
array(
'target_language' => $language,
)
)
)
->using_temperature( 0.7 );

$prompt_builder = $this->set_provider_model_preference(
$prompt_builder,
Content_Translation_Experiment::class
);

return $this->ensure_text_generation_supported(
$prompt_builder,
esc_html__( 'Content translation failed. Please ensure you have a connected provider that supports text generation.', 'ai' )
);
}
}
134 changes: 134 additions & 0 deletions includes/Abilities/Content_Translation/Languages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php
/**
* Supported languages for AI Content translation.
*
* @package WordPress\AI\Abilities\Content_Translation
*/

declare( strict_types=1 );

namespace WordPress\AI\Abilities\Content_Translation;

/**
* Class providing supported languages for AI content translation.
*
* @since x.x.x
*/
final class Languages {

/**
* The default target language for translation.
*
* @since x.x.x
*
* @var string
*/
private const DEFAULT_TARGET_LANGUAGE = 'en-us';

/**
* Returns the default target language for translation.
*
* @since x.x.x
*
* @return string The default target language code.
*/
public static function get_default_target_language(): string {
return self::DEFAULT_TARGET_LANGUAGE;
}

/**
* Returns the supported languages for AI content translation.
*
* @since x.x.x
*
* @return array<string, string> Supported languages.
*/
public static function get_supported_languages(): array {
$languages = array(
'ar' => __( 'Arabic', 'ai' ),
'zh-cn' => __( 'Chinese (Simplified)', 'ai' ),
'zh-tw' => __( 'Chinese (Traditional)', 'ai' ),
'nl-nl' => __( 'Dutch', 'ai' ),
'en-gb' => __( 'English (UK)', 'ai' ),
'en-us' => __( 'English (US)', 'ai' ),
'fr-fr' => __( 'French', 'ai' ),
'de-de' => __( 'German', 'ai' ),
'hi' => __( 'Hindi', 'ai' ),
'it-it' => __( 'Italian', 'ai' ),
'ja' => __( 'Japanese', 'ai' ),
'ko' => __( 'Korean', 'ai' ),
'pt-br' => __( 'Portuguese (Brazil)', 'ai' ),
'es-es' => __( 'Spanish', 'ai' ),
);

/**
* Filters supported target languages for AI content translation.
*
* @param array<string, string> $languages Supported languages.
*/
return (array) apply_filters( 'wpai_content_translation_languages', $languages );
}

/**
* Returns the supported languages for AI content translation in a format suitable for JavaScript.
*
* @since x.x.x
*
* @return array<int, array{code: string, name: string}> Supported languages for JavaScript.
*/
public static function get_supported_languages_for_js(): array {
$languages = self::get_supported_languages();

return array_map(
static function ( string $code, string $name ): array {
return array(
'code' => $code,
'name' => $name,
);
},
array_keys( $languages ),
$languages
);
}

/**
* Returns the name of a language given its code.
*
* @since x.x.x
*
* @param string $language_code The language code.
* @return string|null The name of the language, or null if not found.
*/
public static function get_language_name( string $language_code ): ?string {
$languages = self::get_supported_languages();

if ( array_key_exists( $language_code, $languages ) ) {
return $languages[ $language_code ];
}

return null;
}

/**
* Returns the language codes of supported languages.
*
* @since x.x.x
*
* @return string[] Array of supported language codes.
*/
public static function get_codes(): array {
return array_keys( self::get_supported_languages() );
}

/**
* Checks if a language is supported for translation.
*
* @since x.x.x
*
* @param string $language_code The language code to check.
* @return bool True if the language is supported, false otherwise.
*/
public static function is_supported( string $language_code ): bool {
return array_key_exists( $language_code, self::get_supported_languages() );
}
}
Loading
Loading