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 '
';
}
/**
* 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 '';
}
/**
* 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 )
);
}
}