This commit is contained in:
Javier Casares 2026-05-28 07:22:36 +00:00
commit 1ccc13da01
14 changed files with 845 additions and 197 deletions

View file

@ -119,6 +119,41 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
),
)
);
register_rest_route(
AI_TRANSLATOR_REST_NAMESPACE,
'/translate-text',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'handle_translate_text_request' ),
'permission_callback' => array( $this, 'check_translate_permissions' ),
'args' => array(
'post_id' => array(
'type' => 'integer',
'required' => true,
'minimum' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
),
'text' => array(
'type' => 'string',
'required' => true,
'minLength' => 1,
'maxLength' => 20000,
'sanitize_callback' => array( $this, 'sanitize_raw_text' ),
'validate_callback' => 'rest_validate_request_arg',
),
'target_locale' => array(
'type' => 'string',
'required' => true,
'pattern' => '^[A-Za-z]{2,3}(_[A-Za-z0-9]{2,8})?$',
'maxLength' => 20,
'sanitize_callback' => array( $this, 'sanitize_locale' ),
'validate_callback' => 'rest_validate_request_arg',
),
),
)
);
}
/**
@ -141,6 +176,79 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
return substr( $value, 0, 20 );
}
/**
* Sanitises a raw text value without stripping HTML.
*
* The /translate-text endpoint accepts HTML content (Gutenberg block markup,
* Classic editor HTML, etc.). Standard WordPress text sanitisers strip HTML
* tags; this method only casts to string and preserves markup intact.
* Security is enforced by the permission_callback (requires edit_post capability).
*
* @since 1.2.0
*
* @param mixed $value Raw value from the request.
*
* @return string
*/
public function sanitize_raw_text( $value ) {
return is_string( $value ) ? $value : '';
}
/**
* REST callback for the translate-text endpoint.
*
* Translates a raw text string (HTML or plain text) into the target locale.
* The post_id parameter is used solely for authorisation; no post content is
* read from the database. This endpoint is designed for client-side chunked
* translation where the caller supplies the text directly.
*
* @since 1.2.0
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response|WP_Error
*/
public function handle_translate_text_request( WP_REST_Request $request ) {
$post_id_param = $request->get_param( 'post_id' );
$text_param = $request->get_param( 'text' );
$target_locale_param = $request->get_param( 'target_locale' );
$post_id = is_numeric( $post_id_param ) ? (int) $post_id_param : 0;
$text = is_string( $text_param ) ? $text_param : '';
$target_locale = is_string( $target_locale_param ) ? $target_locale_param : '';
if ( '' === trim( $text ) ) {
return new WP_REST_Response( array( 'text' => '' ), 200 );
}
// Verify the post exists (post_id is already authorised by the permission callback).
$post = get_post( $post_id );
if ( ! $post instanceof WP_Post ) {
return new WP_Error(
'ai_translator_post_not_found',
__( 'Post not found.', 'robotstxt-ai-translator' ),
array( 'status' => 404 )
);
}
// Validate the locale is installed on this site.
$installed = $this->translator->get_installed_locales();
if ( ! in_array( $target_locale, $installed, true ) ) {
return new WP_Error(
'ai_translator_invalid_locale',
__( 'The selected language is not installed on this site.', 'robotstxt-ai-translator' ),
array( 'status' => 400 )
);
}
$translated = $this->translator->translate( $text, $target_locale );
if ( is_wp_error( $translated ) ) {
return $this->error_with_status( $translated, 502 );
}
return new WP_REST_Response( array( 'text' => $translated ), 200 );
}
/**
* REST permission callback for the translate endpoint.
*
@ -428,31 +536,35 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
$settings = $this->settings->get_settings();
$data = array(
'restNamespace' => AI_TRANSLATOR_REST_NAMESPACE,
'restRoute' => '/translate',
'context' => $context,
'postId' => $post_id,
'available' => $this->translator->is_available(),
'languages' => $this->get_language_list(),
'settings' => array(
'restNamespace' => AI_TRANSLATOR_REST_NAMESPACE,
'restRoute' => '/translate',
'chunkTextRoute' => '/translate-text',
'chunkSize' => 5000,
'context' => $context,
'postId' => $post_id,
'available' => $this->translator->is_available(),
'languages' => $this->get_language_list(),
'settings' => array(
'translateTitle' => ! empty( $settings['translate_title'] ),
'translateContent' => ! empty( $settings['translate_content'] ),
'translateExcerpt' => ! empty( $settings['translate_excerpt'] ),
),
'i18n' => array(
'translate' => __( 'Translate', 'robotstxt-ai-translator' ),
'translating' => __( 'Translating…', 'robotstxt-ai-translator' ),
'targetLanguage' => __( 'Target language', 'robotstxt-ai-translator' ),
'panelTitle' => __( 'AI Translator', 'robotstxt-ai-translator' ),
'success' => __( 'Translation applied. Review and save the post.', 'robotstxt-ai-translator' ),
'genericError' => __( 'Translation failed.', 'robotstxt-ai-translator' ),
'noLanguages' => __( 'No languages are installed on this site.', 'robotstxt-ai-translator' ),
'noFields' => __( 'No translation fields are enabled in settings.', 'robotstxt-ai-translator' ),
'aiUnavailable' => __( 'AI features are disabled for this WordPress installation.', 'robotstxt-ai-translator' ),
'saveBeforeWarn' => __( 'You have unsaved changes. Save the post before translating to ensure the latest content is used.', 'robotstxt-ai-translator' ),
'translateTitle' => __( 'Translate title', 'robotstxt-ai-translator' ),
'translateContent' => __( 'Translate content', 'robotstxt-ai-translator' ),
'translateExcerpt' => __( 'Translate excerpt', 'robotstxt-ai-translator' ),
'i18n' => array(
'translate' => __( 'Translate', 'robotstxt-ai-translator' ),
'translating' => __( 'Translating…', 'robotstxt-ai-translator' ),
/* translators: 1: number of chunks translated so far, 2: total number of chunks. */
'translatingProgress' => __( 'Translating… ({done}/{total})', 'robotstxt-ai-translator' ),
'targetLanguage' => __( 'Target language', 'robotstxt-ai-translator' ),
'panelTitle' => __( 'AI Translator', 'robotstxt-ai-translator' ),
'success' => __( 'Translation applied. Review and save the post.', 'robotstxt-ai-translator' ),
'genericError' => __( 'Translation failed.', 'robotstxt-ai-translator' ),
'noLanguages' => __( 'No languages are installed on this site.', 'robotstxt-ai-translator' ),
'noFields' => __( 'No translation fields are enabled in settings.', 'robotstxt-ai-translator' ),
'aiUnavailable' => __( 'AI features are disabled for this WordPress installation.', 'robotstxt-ai-translator' ),
'saveBeforeWarn' => __( 'You have unsaved changes. Save the post before translating to ensure the latest content is used.', 'robotstxt-ai-translator' ),
'translateTitle' => __( 'Translate title', 'robotstxt-ai-translator' ),
'translateContent' => __( 'Translate content', 'robotstxt-ai-translator' ),
'translateExcerpt' => __( 'Translate excerpt', 'robotstxt-ai-translator' ),
),
);