string_manager = $string_manager; $this->query = $query; } /** * {@inheritdoc} */ public function register() { add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'admin_post_shapetranslate_save_string_translations', array( $this, 'save_translations' ) ); add_filter( 'set_screen_option_shapetranslate_strings_per_page', array( $this, 'save_per_page' ), 10, 3 ); } /** * Registers ShapeTranslate -> String Translation. * * @return void */ public function register_menu() { $this->hook_suffix = (string) add_submenu_page( 'shapetranslate', esc_html__( 'ShapeTranslate String Translation', 'shapetranslate' ), esc_html__( 'String Translation', 'shapetranslate' ), AdminModule::capability(), 'shapetranslate-string-translations', array( $this, 'render' ) ); if ( '' !== $this->hook_suffix ) { add_action( 'load-' . $this->hook_suffix, array( $this, 'add_screen_options' ) ); } } /** * Adds the standard per-page screen option. * * @return void */ public function add_screen_options() { add_screen_option( 'per_page', array( 'label' => __( 'Strings per page', 'shapetranslate' ), 'default' => 20, 'option' => 'shapetranslate_strings_per_page', ) ); } /** * Validates the per-page preference. * * @param mixed $status Existing screen option value. * @param string $option Screen option name. * @param mixed $value Submitted value. * @return mixed */ public function save_per_page( $status, $option, $value ) { if ( 'shapetranslate_strings_per_page' !== $option ) { return $status; } return min( 100, max( 1, absint( $value ) ) ); } /** * Loads admin styling only on the string translation screen. * * @param string $hook_suffix Current admin page hook. * @return void */ public function enqueue_assets( $hook_suffix ) { if ( '' === $this->hook_suffix || $this->hook_suffix !== $hook_suffix ) { return; } wp_enqueue_style( 'shapetranslate-admin', SHAPETRANSLATE_URL . 'assets/css/admin.css', array(), Assets::version( 'assets/css/admin.css' ) ); } /** * Renders filters and the registered-string editor. * * @return void */ public function render() { $this->authorize(); $this->string_manager->flush_registered_strings(); if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } $table = new StringTranslationListTable( $this->query ); $table->prepare_items(); $args = $table->get_query_args(); $languages = $table->get_languages(); $language_id = $args['language_id']; echo '
'; echo '

' . esc_html__( 'String Translation', 'shapetranslate' ) . '

'; $this->render_notice(); if ( empty( $languages ) ) { echo '

'; esc_html_e( 'Enable at least one language before translating registered strings.', 'shapetranslate' ); echo '

'; } $this->render_filters( $table, $args ); echo '
'; echo ''; echo ''; $this->render_redirect_fields( $args ); wp_nonce_field( 'shapetranslate_save_string_translations_' . $language_id ); $table->display(); if ( '' !== $language_id && $table->has_items() ) { submit_button( __( 'Save translations', 'shapetranslate' ) ); } echo '
'; } /** * Saves the submitted page of translations, one language at a time. * * Fields are always posted as `translations[language][string]`, so a page * showing every language saves through exactly the same path as a page * showing one. Each language is validated against the enabled registry * before the manager sees it, and an unknown language is skipped rather * than failing the whole page. * * @return void */ public function save_translations() { $this->authorize(); // phpcs:disable WordPress.Security.NonceVerification.Missing -- Selects the nonce action below. $view = isset( $_POST['language_id'] ) && is_scalar( $_POST['language_id'] ) ? sanitize_text_field( wp_unslash( $_POST['language_id'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Missing check_admin_referer( 'shapetranslate_save_string_translations_' . $view ); // Verified above; the manager validates every ID and value before mutation. // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $submitted = isset( $_POST['translations'] ) && is_array( $_POST['translations'] ) ? wp_unslash( $_POST['translations'] ) : array(); // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $enabled = array_values( wp_list_pluck( $this->query->get_languages(), 'id' ) ); $error = null; foreach ( $submitted as $language_id => $translations ) { $language_id = is_scalar( $language_id ) ? sanitize_text_field( (string) $language_id ) : ''; if ( ! in_array( $language_id, $enabled, true ) || ! is_array( $translations ) ) { continue; } $result = $this->string_manager->save_translations( $language_id, $translations ); if ( is_wp_error( $result ) ) { $error = $result; break; } } $this->redirect_after_save( $error ); } /** * Renders the GET filter form. * * @param StringTranslationListTable $table Prepared table. * @param array $args Normalized filters. * @return void */ private function render_filters( $table, $args ) { echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; submit_button( __( 'Filter', 'shapetranslate' ), '', 'filter_action', false ); $table->search_box( __( 'Search strings', 'shapetranslate' ), 'shapetranslate-strings' ); echo '
'; } /** * Preserves allowlisted filters after a save redirect. * * @param array $args Normalized filters. * @return void */ private function render_redirect_fields( $args ) { $fields = array( 'shapetranslate_group' => $args['group'], 's' => $args['search'], 'paged' => $args['page'], 'orderby' => $args['orderby'], 'order' => $args['order'], ); foreach ( $fields as $name => $value ) { echo ''; } } /** * Redirects to a controlled dashboard URL after a mutation. * * @param WP_Error|null $error Optional save error. * @return void */ private function redirect_after_save( $error ) { // Save nonce verified before this helper; individual values are sanitized below. // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $redirect = isset( $_POST['redirect'] ) && is_array( $_POST['redirect'] ) ? wp_unslash( $_POST['redirect'] ) : array(); $language_id = isset( $_POST['language_id'] ) && is_scalar( $_POST['language_id'] ) ? sanitize_text_field( wp_unslash( $_POST['language_id'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $args = array( 'page' => 'shapetranslate-string-translations', 'shapetranslate_language' => $language_id, 'shapetranslate_group' => isset( $redirect['shapetranslate_group'] ) && is_scalar( $redirect['shapetranslate_group'] ) ? sanitize_text_field( $redirect['shapetranslate_group'] ) : '', 's' => isset( $redirect['s'] ) && is_scalar( $redirect['s'] ) ? sanitize_text_field( $redirect['s'] ) : '', 'paged' => isset( $redirect['paged'] ) ? max( 1, absint( $redirect['paged'] ) ) : 1, 'orderby' => isset( $redirect['orderby'] ) && is_scalar( $redirect['orderby'] ) ? sanitize_key( $redirect['orderby'] ) : 'group', 'order' => isset( $redirect['order'] ) && 'DESC' === strtoupper( sanitize_text_field( $redirect['order'] ) ) ? 'DESC' : 'ASC', 'shapetranslate_string_notice' => null === $error ? 'saved' : 'invalid', ); wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) ); exit; } /** * Renders a controlled status notice. * * @return void */ private function render_notice() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only allowlisted notice. $notice = isset( $_GET['shapetranslate_string_notice'] ) ? sanitize_key( wp_unslash( $_GET['shapetranslate_string_notice'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( 'saved' === $notice ) { echo '

' . esc_html__( 'String translations saved.', 'shapetranslate' ) . '

'; } elseif ( 'invalid' === $notice ) { echo '

' . esc_html__( 'The string translations could not be saved.', 'shapetranslate' ) . '

'; } } /** * Enforces the shared ShapeTranslate management capability. * * @return void */ private function authorize() { if ( current_user_can( AdminModule::capability() ) ) { return; } wp_die( esc_html__( 'You are not allowed to manage string translations.', 'shapetranslate' ), '', array( 'response' => 403 ) ); } }