robotstxt-ai-translator/includes/class-ai-translator-translator.php
2026-05-23 10:13:56 +00:00

201 lines
5.5 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 {
/**
* Indicates whether the WordPress AI client is available.
*
* @since 1.0.0
*
* @return bool True if the dependency is active.
*/
public function is_available() {
return function_exists( 'wp_ai_client_prompt' );
}
/**
* 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.
*
* @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_unavailable',
__( 'The WordPress AI client is not available. Install and activate the WordPress AI plugin.', 'robotstxt-ai-translator' )
);
}
if ( ! $this->is_supported() ) {
return new WP_Error(
'ai_translator_unsupported',
__( 'The configured AI providers cannot serve text generation requests. Open the WordPress AI settings to configure a provider.', '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
);
try {
$result = wp_ai_client_prompt( $text )
->using_system_instruction( $system_instruction )
->generate_text();
} catch ( Exception $e ) {
return new WP_Error( 'ai_translator_exception', $e->getMessage() );
} catch ( Throwable $e ) {
return new WP_Error( 'ai_translator_exception', $e->getMessage() );
}
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 ) );
}
}
}