Soft_ML_Admin::SLUG, 'tab' => 'edit', 'post' => $source, 'lang' => $lang ), $extra ), admin_url( 'admin.php' ) ); } private function flash( $type, $msg ) { set_transient( 'softml_notice_' . get_current_user_id(), array( $type, $msg ), 60 ); } /* ------------------------------------------------------------ handlers */ public function handle_create() { check_admin_referer( 'softml_create_translation' ); if ( ! current_user_can( Soft_ML_Admin::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $source = isset( $_POST['source'] ) ? absint( $_POST['source'] ) : 0; $lang = isset( $_POST['lang'] ) ? sanitize_key( wp_unslash( $_POST['lang'] ) ) : ''; $slug = isset( $_POST['slug'] ) ? sanitize_title( wp_unslash( $_POST['slug'] ) ) : ''; $new = Soft_ML_Page_Creator::create( $source, $lang, $slug ); if ( is_wp_error( $new ) ) { $this->flash( 'error', $new->get_error_message() ); wp_safe_redirect( admin_url( 'admin.php?page=' . Soft_ML_Admin::SLUG . '&tab=translations' ) ); exit; } $this->flash( 'success', __( 'Translation created as a draft. Scan the original page to load its text.', 'softtrix-multilanguage' ) ); wp_safe_redirect( $this->edit_url( $source, $lang ) ); exit; } public function handle_scan() { check_admin_referer( 'softml_scan_page' ); if ( ! current_user_can( Soft_ML_Admin::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $source = isset( $_POST['source'] ) ? absint( $_POST['source'] ) : 0; $lang = isset( $_POST['lang'] ) ? sanitize_key( wp_unslash( $_POST['lang'] ) ) : ''; if ( ! current_user_can( 'edit_post', $source ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $res = Soft_ML_Scanner::scan( $source ); if ( is_wp_error( $res ) ) { $this->flash( 'error', $res->get_error_message() ); } else { /* translators: 1: segments on the page, 2: segments not seen before. */ $this->flash( 'success', sprintf( __( 'Scan complete: %1$d text segments, %2$d new.', 'softtrix-multilanguage' ), $res['segments'], $res['new'] ) ); } wp_safe_redirect( $this->edit_url( $source, $lang ) ); exit; } public function handle_save() { check_admin_referer( 'softml_save_segments' ); if ( ! current_user_can( Soft_ML_Admin::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $source = isset( $_POST['source'] ) ? absint( $_POST['source'] ) : 0; $lang = isset( $_POST['lang'] ) ? sanitize_key( wp_unslash( $_POST['lang'] ) ) : ''; $tid = Soft_ML_Translations::get_translation_id( $source, $lang ); if ( ! $tid || ! Soft_ML_Languages::exists( $lang ) ) { wp_die( esc_html__( 'Unknown translation.', 'softtrix-multilanguage' ), 404 ); } if ( ! current_user_can( 'edit_post', $tid ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $errors = array(); // Title, slug, status. $update = array( 'ID' => $tid ); if ( isset( $_POST['title'] ) ) { $update['post_title'] = sanitize_text_field( wp_unslash( $_POST['title'] ) ); } $status = isset( $_POST['tr_status'] ) ? sanitize_key( wp_unslash( $_POST['tr_status'] ) ) : ''; if ( in_array( $status, Soft_ML_Translations::STATUSES, true ) ) { $update['post_status'] = 'published' === $status ? 'publish' : 'draft'; } if ( isset( $_POST['slug'] ) ) { $slug = sanitize_title( wp_unslash( $_POST['slug'] ) ); if ( '' === $slug ) { $errors[] = __( 'Slug cannot be empty.', 'softtrix-multilanguage' ); } elseif ( Soft_ML_Page_Creator::slug_taken( $slug, $lang, $tid ) ) { $errors[] = __( 'That slug is already used by another translation in this language.', 'softtrix-multilanguage' ); } else { update_post_meta( $tid, '_softml_slug', $slug ); } } // SEO fields first, then always re-save the post: SEO plugins rebuild their indexable on save. $seo = isset( $_POST['seo'] ) && is_array( $_POST['seo'] ) ? wp_unslash( $_POST['seo'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput Soft_ML_SEO_Meta::save( $tid, $seo ); wp_update_post( wp_slash( $update ) ); Soft_ML_Links::bump(); if ( in_array( $status, Soft_ML_Translations::STATUSES, true ) ) { Soft_ML_Translations::set_status( $tid, $status ); } // Segments. $tr = isset( $_POST['tr'] ) && is_array( $_POST['tr'] ) ? wp_unslash( $_POST['tr'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $ok = isset( $_POST['ok'] ) && is_array( $_POST['ok'] ) ? array_keys( $_POST['ok'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $saved = 0; foreach ( $tr as $hash => $text ) { if ( ! is_string( $hash ) || 1 !== preg_match( '/^[a-f0-9]{40}$/', $hash ) || ! is_string( $text ) ) { continue; } $approved = in_array( $hash, $ok, true ) ? Soft_ML_Strings::STATUS_APPROVED : Soft_ML_Strings::STATUS_DRAFT; $res = Soft_ML_Strings::set_translation( $hash, $lang, $text, $approved ); if ( is_wp_error( $res ) ) { $errors[] = $res->get_error_message(); } else { ++$saved; } } if ( $errors ) { $this->flash( 'error', implode( ' ', array_unique( $errors ) ) ); } else { /* translators: %d: number of segments saved. */ $this->flash( 'success', sprintf( __( 'Saved. %d segment(s) updated.', 'softtrix-multilanguage' ), $saved ) ); } wp_safe_redirect( $this->edit_url( $source, $lang, array( 'filter' => isset( $_POST['filter'] ) ? sanitize_key( wp_unslash( $_POST['filter'] ) ) : 'all', 'paged' => isset( $_POST['paged'] ) ? absint( $_POST['paged'] ) : 1 ) ) ); exit; } /* ------------------------------------------------------------ view */ public function render() { $source = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification $lang = isset( $_GET['lang'] ) ? sanitize_key( wp_unslash( $_GET['lang'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification $filter = isset( $_GET['filter'] ) ? sanitize_key( wp_unslash( $_GET['filter'] ) ) : 'all'; // phpcs:ignore WordPress.Security.NonceVerification $filter = in_array( $filter, array( 'all', 'todo', 'review' ), true ) ? $filter : 'all'; $paged = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification $src = get_post( $source ); $tid = $src ? Soft_ML_Translations::get_translation_id( $source, $lang ) : 0; if ( ! $src || ! $tid || ! Soft_ML_Languages::exists( $lang ) ) { echo '

' . esc_html__( 'Translation not found.', 'softtrix-multilanguage' ) . '

'; return; } $language = Soft_ML_Languages::get( $lang ); $row = Soft_ML_Translations::get_row( $tid ); $prog = Soft_ML_Strings::progress( $source, $lang ); $scanned = get_post_meta( $source, '_softml_scanned_at', true ); $pct = $prog['total'] ? (int) floor( 100 * $prog['approved'] / $prog['total'] ) : 0; echo '

' . esc_html( sprintf( '%s → %s', get_the_title( $src ), $language['native_name'] ) ) . '

'; echo '

← ' . esc_html__( 'Back to overview', 'softtrix-multilanguage' ) . ' | '; echo '' . esc_html__( 'Original', 'softtrix-multilanguage' ) . ' | '; echo '' . esc_html__( 'Translation post', 'softtrix-multilanguage' ) . ' | '; echo '' . esc_html__( 'Preview (includes unapproved text)', 'softtrix-multilanguage' ) . '

'; /* translators: 1: approved, 2: total, 3: percent. */ echo '

' . esc_html( sprintf( __( 'Approved %1$d of %2$d segments (%3$d%%)', 'softtrix-multilanguage' ), $prog['approved'], $prog['total'], $pct ) ) . ' · '; echo esc_html( sprintf( /* translators: %d: drafts */ __( '%d awaiting review', 'softtrix-multilanguage' ), $prog['draft'] ) ) . ' · '; echo esc_html( $scanned ? sprintf( /* translators: %s: date */ __( 'last scanned %s UTC', 'softtrix-multilanguage' ), $scanned ) : __( 'not scanned yet', 'softtrix-multilanguage' ) ) . '

'; echo '
'; echo ''; wp_nonce_field( 'softml_scan_page' ); submit_button( $scanned ? __( 'Re-scan original page', 'softtrix-multilanguage' ) : __( 'Scan original page', 'softtrix-multilanguage' ), 'secondary', 'submit', false ); echo '
'; list( $rows, $total ) = Soft_ML_Strings::editor_rows( $source, $lang, $filter, ( $paged - 1 ) * self::PER_PAGE, self::PER_PAGE ); echo '
'; echo ''; echo ''; wp_nonce_field( 'softml_save_segments' ); echo ''; echo ''; echo '
/' . esc_html( $language['slug'] ) . '/…
' . esc_html__( 'Status', 'softtrix-multilanguage' ) . ' ' . esc_html__( 'Publishing makes the translated page live. Unapproved segments stay in the original language.', 'softtrix-multilanguage' ) . '
'; // Machine-translation shortcuts. echo '

'; if ( Soft_ML_MT::is_configured() ) { echo '' . esc_html__( 'Auto-translate this page', 'softtrix-multilanguage' ) . ' '; } if ( $prog['draft'] > 0 ) { echo ''; } echo '

'; // SEO fields of the active SEO plugin (each translation keeps its own). $seo_fields = Soft_ML_SEO_Meta::active_fields(); echo '

' . esc_html__( 'SEO', 'softtrix-multilanguage' ) . '

'; if ( ! $seo_fields ) { echo '

' . esc_html__( 'No supported SEO plugin (Yoast SEO / Rank Math) is active, so no SEO fields are shown.', 'softtrix-multilanguage' ) . '

'; } else { echo '

' . esc_html__( 'Leave a field empty to let the SEO plugin build it from the translated title/excerpt. English SEO text is never copied into a translation.', 'softtrix-multilanguage' ) . '

'; foreach ( $seo_fields as $key => $f ) { $val = Soft_ML_SEO_Meta::get( $tid, $key ); $orig = Soft_ML_SEO_Meta::get( $source, $key ); echo ''; } echo '
'; if ( 'select' === $f['type'] ) { echo ''; } elseif ( 'textarea' === $f['type'] ) { echo ''; } else { echo ''; } if ( 'select' !== $f['type'] && '' !== $orig ) { echo '

' . esc_html__( 'Original:', 'softtrix-multilanguage' ) . ' ' . esc_html( $orig ) . '

'; } echo '
'; } // Filters. echo '

'; foreach ( array( 'all' => __( 'All', 'softtrix-multilanguage' ), 'todo' => __( 'Untranslated', 'softtrix-multilanguage' ), 'review' => __( 'Needs review', 'softtrix-multilanguage' ) ) as $k => $label ) { echo $k === $filter ? '' . esc_html( $label ) . ' ' : '' . esc_html( $label ) . ' '; echo ' '; } echo '

'; if ( ! $total ) { echo '

' . esc_html( $prog['total'] ? __( 'Nothing matches this filter.', 'softtrix-multilanguage' ) : __( 'No segments yet. Scan the original page first.', 'softtrix-multilanguage' ) ) . '

'; } else { echo ''; foreach ( $rows as $r ) { $hash = $r['hash']; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Original', 'softtrix-multilanguage' ) . '' . esc_html__( 'Translation', 'softtrix-multilanguage' ) . '' . esc_html__( 'Approved', 'softtrix-multilanguage' ) . '
' . wp_kses( $this->display_source( $r['source_text'] ), array( 'code' => array() ) ) . ( 'attr' === $r['kind'] ? ' ' . esc_html__( '(attribute)', 'softtrix-multilanguage' ) . '' : '' ) . ( (int) $r['cnt'] > 1 ? ' ×' . absint( $r['cnt'] ) . '' : '' ) . '
'; echo wp_kses_post( (string) paginate_links( array( 'base' => add_query_arg( 'paged', '%#%', $this->edit_url( $source, $lang, array( 'filter' => $filter ) ) ), 'format' => '', 'current' => $paged, 'total' => (int) ceil( $total / self::PER_PAGE ), ) ) ); } submit_button( __( 'Save translations', 'softtrix-multilanguage' ) ); echo '
'; // Separate form (forms cannot nest) used by the "Approve all drafts" button above. echo '
'; echo ''; wp_nonce_field( 'softml_approve_drafts' ); echo '
'; /** * Extra editor sections from add-ons. * * @param int $source Original post ID. * @param string $lang Language code. * @param int $tid Translation post ID. */ do_action( 'softml_editor_after', $source, $lang, $tid ); } /** * Show markers as visible chips; everything else escaped. */ private function display_source( $text ) { $parts = preg_split( '~()~', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); $out = ''; foreach ( $parts as $p ) { if ( preg_match( '~^$~', $p ) ) { $out .= '' . esc_html( $p ) . ''; } else { $out .= esc_html( html_entity_decode( $p, ENT_QUOTES, 'UTF-8' ) ); } } return $out; } }