262 lines
7.3 KiB
PHP
262 lines
7.3 KiB
PHP
<?php
|
|
/**
|
|
* Translation engine for the AI Translator plugin.
|
|
*
|
|
* Wraps the WordPress AI client (https://github.com/WordPress/ai) so the rest of the
|
|
* plugin does not need to know which AI provider is configured.
|
|
*
|
|
* @package ROBOTSTXT\AI_Translator
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
if ( ! class_exists( 'AI_Translator_Translator' ) ) {
|
|
|
|
/**
|
|
* Talks to the WordPress AI client to translate strings.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
class AI_Translator_Translator {
|
|
|
|
/**
|
|
* Settings handler.
|
|
*
|
|
* @since 1.2.0
|
|
* @var AI_Translator_Settings
|
|
*/
|
|
private $settings;
|
|
|
|
/**
|
|
* Timeout value (seconds) enforced during an active translate() call.
|
|
* Null when no call is in progress.
|
|
*
|
|
* @since 1.2.0
|
|
* @var int|null
|
|
*/
|
|
private $active_timeout = null;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @param AI_Translator_Settings $settings Settings handler.
|
|
*/
|
|
public function __construct( AI_Translator_Settings $settings ) {
|
|
$this->settings = $settings;
|
|
}
|
|
|
|
/**
|
|
* Filters the http_request_timeout value while an AI translation call is
|
|
* in progress. Registered and de-registered by translate() around each call.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @param mixed $timeout Current timeout value passed by WordPress.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function filter_timeout( $timeout ) {
|
|
if ( null !== $this->active_timeout ) {
|
|
return $this->active_timeout;
|
|
}
|
|
|
|
return is_numeric( $timeout ) ? (int) $timeout : 30;
|
|
}
|
|
|
|
/**
|
|
* Indicates whether AI features are enabled for this WordPress installation.
|
|
*
|
|
* `wp_ai_client_prompt()` is a native WordPress 7.0 function — the API is always
|
|
* present. This method checks `wp_supports_ai()`, which returns false only when the
|
|
* `WP_AI_SUPPORT` constant is set to false or the `wp_supports_ai` filter disables it.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @return bool True when WordPress AI is enabled for this installation.
|
|
*/
|
|
public function is_available() {
|
|
return wp_supports_ai();
|
|
}
|
|
|
|
/**
|
|
* Indicates whether the WordPress AI client supports text generation right now.
|
|
*
|
|
* The check uses a tiny dummy prompt so the AI client can answer whether at
|
|
* least one configured provider can serve text generation requests.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @return bool True if text generation is supported.
|
|
*/
|
|
public function is_supported() {
|
|
if ( ! $this->is_available() ) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$builder = wp_ai_client_prompt( 'test' );
|
|
|
|
return (bool) $builder->is_supported_for_text_generation();
|
|
} catch ( Exception $e ) {
|
|
return false;
|
|
} catch ( Throwable $e ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Translates a single piece of text to the target locale.
|
|
*
|
|
* Applies the configured request_timeout via the http_request_timeout filter
|
|
* for the duration of the AI call, then restores the previous timeout.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param string $text Source text. Plain text or HTML; preserved as-is.
|
|
* @param string $target_locale WordPress locale code (for example: es_ES, en_US, fr_FR).
|
|
*
|
|
* @return string|WP_Error Translated text, or WP_Error on failure.
|
|
*/
|
|
public function translate( $text, $target_locale ) {
|
|
$text = (string) $text;
|
|
|
|
if ( '' === trim( $text ) ) {
|
|
return '';
|
|
}
|
|
|
|
if ( ! $this->is_available() ) {
|
|
return new WP_Error(
|
|
'ai_translator_disabled',
|
|
__( 'AI features are disabled for this WordPress installation.', 'robotstxt-ai-translator' )
|
|
);
|
|
}
|
|
|
|
if ( ! $this->is_supported() ) {
|
|
return new WP_Error(
|
|
'ai_translator_unsupported',
|
|
__( 'No AI provider is configured for text generation. Open Settings → AI to set one up.', 'robotstxt-ai-translator' )
|
|
);
|
|
}
|
|
|
|
$language_name = $this->locale_to_language_name( $target_locale );
|
|
|
|
if ( '' === $language_name ) {
|
|
return new WP_Error(
|
|
'ai_translator_invalid_locale',
|
|
__( 'The target language is not installed on this site.', 'robotstxt-ai-translator' )
|
|
);
|
|
}
|
|
|
|
$system_instruction = sprintf(
|
|
/* translators: %s: target language name. */
|
|
__( 'You are a professional translator. Translate the user message into %s. Preserve any HTML, shortcodes, line breaks, and Markdown exactly as they appear. Return only the translated text, without explanations, prefaces, or quotation marks.', 'robotstxt-ai-translator' ),
|
|
$language_name
|
|
);
|
|
|
|
// Apply the configured request timeout around the AI call.
|
|
$site_settings = $this->settings->get_settings();
|
|
$this->active_timeout = max( 30, (int) $site_settings['request_timeout'] );
|
|
add_filter( 'http_request_timeout', array( $this, 'filter_timeout' ), PHP_INT_MAX );
|
|
|
|
try {
|
|
$result = wp_ai_client_prompt( $text )
|
|
->using_system_instruction( $system_instruction )
|
|
->generate_text();
|
|
} catch ( Exception $e ) {
|
|
$result = new WP_Error( 'ai_translator_exception', $e->getMessage() );
|
|
} catch ( Throwable $e ) {
|
|
$result = new WP_Error( 'ai_translator_exception', $e->getMessage() );
|
|
}
|
|
|
|
remove_filter( 'http_request_timeout', array( $this, 'filter_timeout' ), PHP_INT_MAX );
|
|
$this->active_timeout = null;
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
return $result;
|
|
}
|
|
|
|
if ( ! is_string( $result ) ) {
|
|
return new WP_Error(
|
|
'ai_translator_invalid_response',
|
|
__( 'The AI service returned an unexpected response.', 'robotstxt-ai-translator' )
|
|
);
|
|
}
|
|
|
|
return trim( $result );
|
|
}
|
|
|
|
/**
|
|
* Converts a WordPress locale code into a human-readable language name.
|
|
*
|
|
* Only locales installed on the site (en_US plus any installed .po files) are
|
|
* accepted, so the editor cannot request translations to languages that the
|
|
* site has not declared as available.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param string $locale WordPress locale code.
|
|
*
|
|
* @return string Native language name, or empty string when the locale is not installed.
|
|
*/
|
|
public function locale_to_language_name( $locale ) {
|
|
$locale = (string) $locale;
|
|
|
|
if ( '' === $locale ) {
|
|
return '';
|
|
}
|
|
|
|
$installed = $this->get_installed_locales();
|
|
|
|
if ( ! in_array( $locale, $installed, true ) ) {
|
|
return '';
|
|
}
|
|
|
|
if ( 'en_US' === $locale ) {
|
|
return 'English (United States)';
|
|
}
|
|
|
|
if ( ! function_exists( 'wp_get_available_translations' ) ) {
|
|
require_once ABSPATH . 'wp-admin/includes/translation-install.php';
|
|
}
|
|
|
|
if ( function_exists( 'wp_get_available_translations' ) ) {
|
|
$translations = wp_get_available_translations();
|
|
if ( isset( $translations[ $locale ]['native_name'] ) && '' !== $translations[ $locale ]['native_name'] ) {
|
|
return (string) $translations[ $locale ]['native_name'];
|
|
}
|
|
if ( isset( $translations[ $locale ]['english_name'] ) && '' !== $translations[ $locale ]['english_name'] ) {
|
|
return (string) $translations[ $locale ]['english_name'];
|
|
}
|
|
}
|
|
|
|
return $locale;
|
|
}
|
|
|
|
/**
|
|
* Returns the locales installed on the site, including en_US.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @return array<int,string> List of locale codes.
|
|
*/
|
|
public function get_installed_locales() {
|
|
$locales = array( 'en_US' );
|
|
|
|
if ( function_exists( 'get_available_languages' ) ) {
|
|
$installed = get_available_languages();
|
|
if ( is_array( $installed ) ) {
|
|
foreach ( $installed as $locale ) {
|
|
$locales[] = (string) $locale;
|
|
}
|
|
}
|
|
}
|
|
|
|
return array_values( array_unique( $locales ) );
|
|
}
|
|
}
|
|
}
|