settings = $settings; } /** * Registers WordPress hooks. * * @since 1.0.0 * * @return void */ public function register() { add_action( 'admin_menu', array( $this, 'register_site_menu' ) ); add_action( 'admin_post_ai_translator_save_site', array( $this, 'handle_site_save' ) ); if ( is_multisite() ) { add_action( 'network_admin_menu', array( $this, 'register_network_menu' ) ); add_action( 'network_admin_edit_ai_translator_save_network', array( $this, 'handle_network_save' ) ); } add_filter( 'plugin_action_links_' . AI_TRANSLATOR_BASENAME, array( $this, 'add_settings_link' ) ); if ( is_multisite() ) { add_filter( 'network_admin_plugin_action_links_' . AI_TRANSLATOR_BASENAME, array( $this, 'add_network_settings_link' ) ); } } /** * Registers the site-level settings page. * * Hidden in Multisite when the network mode is 'global', because the global * settings live in the network admin in that case. * * @since 1.0.0 * * @return void */ public function register_site_menu() { if ( is_multisite() && 'global' === $this->settings->get_network_mode() ) { return; } add_options_page( __( 'AI Translator', 'robotstxt-ai-translator' ), __( 'AI Translator', 'robotstxt-ai-translator' ), 'manage_options', self::SITE_MENU_SLUG, array( $this, 'render_site_page' ) ); } /** * Registers the network-level settings page. * * @since 1.0.0 * * @return void */ public function register_network_menu() { add_submenu_page( 'settings.php', __( 'AI Translator', 'robotstxt-ai-translator' ), __( 'AI Translator', 'robotstxt-ai-translator' ), 'manage_network_options', self::NETWORK_MENU_SLUG, array( $this, 'render_network_page' ) ); } /** * Adds a settings link in the site plugin list. * * @since 1.0.0 * * @param array $links Existing action links. * * @return array */ public function add_settings_link( $links ) { if ( is_multisite() && 'global' === $this->settings->get_network_mode() ) { return $links; } $url = admin_url( 'options-general.php?page=' . self::SITE_MENU_SLUG ); $label = esc_html__( 'Settings', 'robotstxt-ai-translator' ); array_unshift( $links, '' . $label . '' ); return $links; } /** * Adds a settings link in the network plugin list. * * @since 1.0.0 * * @param array $links Existing action links. * * @return array */ public function add_network_settings_link( $links ) { $url = network_admin_url( 'settings.php?page=' . self::NETWORK_MENU_SLUG ); $label = esc_html__( 'Settings', 'robotstxt-ai-translator' ); array_unshift( $links, '' . $label . '' ); return $links; } /** * Renders the site-level settings page. * * @since 1.0.0 * * @return void */ public function render_site_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'robotstxt-ai-translator' ) ); } $current = $this->settings->get_site_settings(); $defaults = is_multisite() ? $this->settings->get_network_settings() : array(); $updated = 1 === filter_input( INPUT_GET, 'updated', FILTER_VALIDATE_INT ); ?>

render_model_recommendations(); ?>
settings->get_network_mode(); $network = $this->settings->get_network_settings(); $updated = 1 === filter_input( INPUT_GET, 'updated', FILTER_VALIDATE_INT ); ?>

render_model_recommendations(); ?>
settings->update_site_settings( $this->sanitize_settings( $raw ) ); $redirect = add_query_arg( array( 'page' => self::SITE_MENU_SLUG, 'updated' => 1, ), admin_url( 'options-general.php' ) ); wp_safe_redirect( $redirect ); exit; } /** * Handles the network-level form submission. * * @since 1.0.0 * * @return void */ public function handle_network_save() { if ( ! current_user_can( 'manage_network_options' ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'robotstxt-ai-translator' ) ); } check_admin_referer( self::NONCE_NETWORK ); $mode_raw = filter_input( INPUT_POST, 'ai_translator_mode', FILTER_UNSAFE_RAW ); $raw_mode = is_string( $mode_raw ) ? sanitize_key( $mode_raw ) : 'global'; $mode = in_array( $raw_mode, array( 'global', 'per-site' ), true ) ? $raw_mode : 'global'; $this->settings->update_network_mode( $mode ); $raw = isset( $_POST['ai_translator_settings'] ) && is_array( $_POST['ai_translator_settings'] ) ? wp_unslash( $_POST['ai_translator_settings'] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitised by sanitize_settings() below. : array(); $this->settings->update_network_settings( $this->sanitize_settings( $raw ) ); $redirect = add_query_arg( array( 'page' => self::NETWORK_MENU_SLUG, 'updated' => 1, ), network_admin_url( 'settings.php' ) ); wp_safe_redirect( $redirect ); exit; } /** * Renders the model recommendations table. * * Displayed below both the site and network settings pages as guidance for * choosing a provider in the WordPress AI plugin. Purely informational; the * table is not interactive and stores no data. * * @since 1.0.0 * * @return void */ private function render_model_recommendations() { $rows = $this->get_model_recommendations(); ?>

*/ private function get_model_recommendations() { $rows = array( array( 'use_case' => __( 'European languages (ES, CA, FR, DE, IT, PT)', 'robotstxt-ai-translator' ), 'model' => __( 'DeepL API Pro', 'robotstxt-ai-translator' ), 'reason' => __( 'Best fluency and naturalness (92/100 in benchmarks). Formality control and custom glossaries.', 'robotstxt-ai-translator' ), ), array( 'use_case' => __( 'Asian languages (ZH, JA, KO)', 'robotstxt-ai-translator' ), 'model' => __( 'GPT-4o / GPT-5 or Claude Sonnet 4', 'robotstxt-ai-translator' ), 'reason' => __( 'Better handling of implicit subjects, honorifics, and cultural references. DeepL falls behind here.', 'robotstxt-ai-translator' ), ), array( 'use_case' => __( 'Marketing / brand tone', 'robotstxt-ai-translator' ), 'model' => __( 'Claude Sonnet 4 / Opus 4', 'robotstxt-ai-translator' ), 'reason' => __( 'Better preservation of tone, brand voice, and nuance. Ideal for creative copy.', 'robotstxt-ai-translator' ), ), array( 'use_case' => __( 'Technical documentation / code', 'robotstxt-ai-translator' ), 'model' => __( 'GPT-4o / GPT-5', 'robotstxt-ai-translator' ), 'reason' => __( 'Higher accuracy with variables, structured formats, and technical terminology.', 'robotstxt-ai-translator' ), ), array( 'use_case' => __( 'Long documents (100+ pages)', 'robotstxt-ai-translator' ), 'model' => __( 'Gemini 2.5 Pro', 'robotstxt-ai-translator' ), 'reason' => __( '1M token context window. Maintains terminological consistency across long texts.', 'robotstxt-ai-translator' ), ), array( 'use_case' => __( 'High volume / low cost', 'robotstxt-ai-translator' ), 'model' => __( 'DeepSeek-V3', 'robotstxt-ai-translator' ), 'reason' => __( 'Quality comparable to GPT-5 at ~$0.14/M tokens (20-50x cheaper than Claude/GPT).', 'robotstxt-ai-translator' ), ), array( 'use_case' => __( 'Rare / indigenous languages', 'robotstxt-ai-translator' ), 'model' => __( 'Claude Sonnet or Taskade Translate (multi-model routing)', 'robotstxt-ai-translator' ), 'reason' => __( 'Better coverage for uncommon language pairs.', 'robotstxt-ai-translator' ), ), array( 'use_case' => __( 'Maximum coverage (133+ languages)', 'robotstxt-ai-translator' ), 'model' => __( 'Google Cloud Translation', 'robotstxt-ai-translator' ), 'reason' => __( 'The most complete option, though with slightly lower quality on European languages.', 'robotstxt-ai-translator' ), ), ); /** * Filters the model recommendations shown in the settings pages. * * @since 1.0.0 * * @param array $rows Recommendation rows. */ return (array) apply_filters( 'ai_translator_model_recommendations', $rows ); } /** * Sanitises a raw settings array into a typed schema. * * This is the single sanitisation boundary for all form input from both the * site and network settings pages. Raw POST values arrive via wp_unslash() * and are converted here to strict types. Any new field added to this schema * MUST be sanitised in this method before being stored. * * @since 1.0.0 * * @param array $raw Raw input from the form. * * @return array */ private function sanitize_settings( array $raw ) { return array( 'translate_title' => ! empty( $raw['translate_title'] ), 'translate_content' => ! empty( $raw['translate_content'] ), 'translate_excerpt' => ! empty( $raw['translate_excerpt'] ), 'auto_translate_on_mlp_create' => ! empty( $raw['auto_translate_on_mlp_create'] ), 'request_timeout' => max( 30, min( 300, isset( $raw['request_timeout'] ) && is_numeric( $raw['request_timeout'] ) ? (int) $raw['request_timeout'] : 60 ) ), ); } } }