*/ const ALLOWED_FIELDS = array( 'title', 'content' ); /** * 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 WordPress hooks. * * @since 1.0.0 * * @return void */ public function register() { add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_classic_editor_assets' ) ); add_action( 'add_meta_boxes', array( $this, 'register_classic_meta_box' ) ); } /** * Registers the REST API routes. * * @since 1.0.0 * * @return void */ public function register_rest_routes() { register_rest_route( AI_TRANSLATOR_REST_NAMESPACE, '/translate', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'handle_translate_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', ), 'fields' => array( 'type' => 'array', 'required' => true, 'minItems' => 1, 'uniqueItems' => true, 'items' => array( 'type' => 'string', 'enum' => self::ALLOWED_FIELDS, ), 'sanitize_callback' => 'rest_sanitize_request_arg', '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', ), ), ) ); } /** * Sanitises a locale string. * * @since 1.0.0 * * @param mixed $value Raw value. * * @return string */ public function sanitize_locale( $value ) { $value = is_string( $value ) ? $value : ''; $value = preg_replace( '/[^A-Za-z0-9_\-]/', '', $value ); if ( ! is_string( $value ) ) { return ''; } return substr( $value, 0, 20 ); } /** * REST permission callback for the translate endpoint. * * @since 1.0.0 * * @param WP_REST_Request $request The request object. * * @return bool|WP_Error True if allowed, WP_Error otherwise. */ public function check_translate_permissions( WP_REST_Request $request ) { $post_id_param = $request->get_param( 'post_id' ); $post_id = is_numeric( $post_id_param ) ? (int) $post_id_param : 0; if ( $post_id <= 0 || ! current_user_can( 'edit_post', $post_id ) ) { return new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to edit this post.', 'robotstxt-ai-translator' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * REST callback for the translate endpoint. * * @since 1.0.0 * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response|WP_Error */ public function handle_translate_request( WP_REST_Request $request ) { $post_id_param = $request->get_param( 'post_id' ); $target_locale_param = $request->get_param( 'target_locale' ); $post_id = is_numeric( $post_id_param ) ? (int) $post_id_param : 0; $fields = (array) $request->get_param( 'fields' ); $target_locale = is_string( $target_locale_param ) ? $target_locale_param : ''; $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 ) ); } $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 ) ); } $site_settings = $this->settings->get_settings(); $allowed = array(); if ( in_array( 'title', $fields, true ) && ! empty( $site_settings['translate_title'] ) ) { $allowed[] = 'title'; } if ( in_array( 'content', $fields, true ) && ! empty( $site_settings['translate_content'] ) ) { $allowed[] = 'content'; } if ( empty( $allowed ) ) { return new WP_Error( 'ai_translator_no_fields', __( 'No translatable fields were selected, or the requested fields are disabled in the settings.', 'robotstxt-ai-translator' ), array( 'status' => 400 ) ); } $response = array(); if ( in_array( 'title', $allowed, true ) ) { $translated = $this->translator->translate( $post->post_title, $target_locale ); if ( is_wp_error( $translated ) ) { return $this->error_with_status( $translated, 502 ); } $response['title'] = $translated; } if ( in_array( 'content', $allowed, true ) ) { $translated = $this->translator->translate( $post->post_content, $target_locale ); if ( is_wp_error( $translated ) ) { return $this->error_with_status( $translated, 502 ); } $response['content'] = $translated; } return new WP_REST_Response( $response, 200 ); } /** * Attaches an HTTP status to a WP_Error without overwriting existing data. * * @since 1.0.0 * * @param WP_Error $error The original error. * @param int $status HTTP status code to add. * * @return WP_Error */ private function error_with_status( WP_Error $error, $status ) { $existing = (array) $error->get_error_data(); $error->add_data( array_merge( $existing, array( 'status' => (int) $status ) ) ); return $error; } /** * Registers the Classic editor metabox. * * @since 1.0.0 * * @return void */ public function register_classic_meta_box() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; // The block editor already shows the AI Translator via the PluginSidebar // (translation icon in the top-right). Skipping metabox registration there // avoids duplicating the UI in the post settings panel. if ( $screen && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() ) { return; } $post_types = $this->get_supported_post_types(); foreach ( $post_types as $post_type ) { add_meta_box( 'ai-translator', __( 'AI Translator', 'robotstxt-ai-translator' ), array( $this, 'render_classic_meta_box' ), $post_type, 'side', 'default' ); } } /** * Renders the Classic editor metabox content. * * @since 1.0.0 * * @param WP_Post $post The current post. * * @return void */ public function render_classic_meta_box( $post ) { $languages = $this->get_language_list(); $settings = $this->settings->get_settings(); ?>