editor = new Soft_ML_Editor(); $this->auto = new Soft_ML_Auto_Translate(); } public function menu() { add_menu_page( __( 'Softtrix Multilanguage', 'softtrix-multilanguage' ), __( 'Softtrix Multilanguage', 'softtrix-multilanguage' ), self::CAP, self::SLUG, array( $this, 'render' ), 'dashicons-translation', 58 ); foreach ( $this->tabs() as $key => $label ) { add_submenu_page( self::SLUG, $label, $label, self::CAP, 'languages' === $key ? self::SLUG : self::SLUG . '&tab=' . $key, 'languages' === $key ? array( $this, 'render' ) : '__return_null' ); } } /** * Admin CSS/JS, only on Softtrix Multilanguage screens and the Pages/Posts lists. * * @param string $hook Admin page hook. */ public function assets( $hook ) { $ours = false !== strpos( (string) $hook, self::SLUG ); if ( ! $ours && ! ( 'edit.php' === $hook && Soft_ML_Admin_List::on_list() ) ) { return; } wp_enqueue_style( 'softml-admin', SOFT_ML_URL . 'assets/admin.css', array(), SOFT_ML_VERSION ); if ( $ours ) { wp_enqueue_script( 'softml-admin', SOFT_ML_URL . 'assets/admin.js', array(), SOFT_ML_VERSION, true ); } } /** * "Settings" and "Setup wizard" links on the Plugins screen. * * @param array $links Links. * @return array */ public function action_links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'softtrix-multilanguage' ) . '', '' . esc_html__( 'Setup wizard', 'softtrix-multilanguage' ) . '' ); return $links; } /* ---------------------------------------------------------------- helpers */ private function tabs() { $tabs = array( 'languages' => __( 'Languages', 'softtrix-multilanguage' ), 'auto' => __( 'Translate', 'softtrix-multilanguage' ), 'translations' => __( 'Pages & Posts', 'softtrix-multilanguage' ), 'settings' => __( 'Settings', 'softtrix-multilanguage' ), 'tools' => __( 'Tools', 'softtrix-multilanguage' ), ); /** * Admin tabs. Add-ons render their own tab on the action "softml_admin_tab_{key}". * * @param array $tabs key => label. */ return (array) apply_filters( 'softml_admin_tabs', $tabs ); } private function url( array $args = array() ) { return add_query_arg( array_merge( array( 'page' => self::SLUG ), $args ), admin_url( 'admin.php' ) ); } private function flash( $type, $message ) { set_transient( 'softml_notice_' . get_current_user_id(), array( $type, $message ), 60 ); } private function back( $tab, array $args = array() ) { wp_safe_redirect( $this->url( array_merge( array( 'tab' => $tab ), $args ) ) ); exit; } public function notices() { $key = 'softml_notice_' . get_current_user_id(); $n = get_transient( $key ); if ( ! $n ) { return; } delete_transient( $key ); printf( '

%2$s

', 'error' === $n[0] ? 'error' : 'success', esc_html( $n[1] ) ); } /* ---------------------------------------------------------------- handlers */ public function handle_save_language() { check_admin_referer( 'softml_save_language' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $existing = isset( $_POST['existing_code'] ) ? sanitize_key( wp_unslash( $_POST['existing_code'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized in Soft_ML_Languages::validate(). $input = isset( $_POST['lang'] ) && is_array( $_POST['lang'] ) ? wp_unslash( $_POST['lang'] ) : array(); $res = Soft_ML_Languages::save( $input, $existing ? $existing : null ); if ( is_wp_error( $res ) ) { $this->flash( 'error', $res->get_error_message() ); $this->back( 'languages', $existing ? array( 'edit' => $existing ) : array() ); } $this->flash( 'success', __( 'Language saved.', 'softtrix-multilanguage' ) ); $this->back( 'languages' ); } public function handle_add_languages() { check_admin_referer( 'softml_add_languages' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $codes = isset( $_POST['codes'] ) && is_array( $_POST['codes'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['codes'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! $codes ) { $this->flash( 'error', __( 'Tick at least one language.', 'softtrix-multilanguage' ) ); $this->back( 'languages' ); } $res = Soft_ML_Languages::add_from_catalog( $codes ); if ( $res['errors'] ) { $this->flash( 'error', implode( ' ', $res['errors'] ) ); } else { /* translators: %d: number of languages */ $this->flash( 'success', sprintf( _n( '%d language added. Next: translate your pages in the Translate tab.', '%d languages added. Next: translate your pages in the Translate tab.', count( $res['added'] ), 'softtrix-multilanguage' ), count( $res['added'] ) ) ); } $this->back( 'languages' ); } public function handle_language_action() { check_admin_referer( 'softml_language_action' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $code = isset( $_POST['code'] ) ? sanitize_key( wp_unslash( $_POST['code'] ) ) : ''; $op = isset( $_POST['op'] ) ? sanitize_key( wp_unslash( $_POST['op'] ) ) : ''; switch ( $op ) { case 'enable': $res = Soft_ML_Languages::set_enabled( $code, true ); break; case 'disable': $res = Soft_ML_Languages::set_enabled( $code, false ); break; case 'default': $res = Soft_ML_Languages::set_default( $code ); break; case 'delete': $res = Soft_ML_Languages::delete( $code ); break; default: $res = new WP_Error( 'softml_op', __( 'Unknown action.', 'softtrix-multilanguage' ) ); } if ( is_wp_error( $res ) ) { $this->flash( 'error', $res->get_error_message() ); } else { $this->flash( 'success', __( 'Language updated.', 'softtrix-multilanguage' ) ); } $this->back( 'languages' ); } public function handle_save_settings() { check_admin_referer( 'softml_save_settings' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized per field in Soft_ML_Settings::sanitize(). $raw = isset( $_POST['softml'] ) && is_array( $_POST['softml'] ) ? wp_unslash( $_POST['softml'] ) : array(); Soft_ML_Settings::save( Soft_ML_Settings::sanitize( $raw ) ); $this->flash( 'success', __( 'Settings saved.', 'softtrix-multilanguage' ) ); $this->back( 'settings' ); } public function handle_export() { check_admin_referer( 'softml_export' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $payload = array( 'plugin' => 'softml', 'version' => SOFT_ML_VERSION, 'languages' => array_values( Soft_ML_Languages::all() ), 'settings' => Soft_ML_Settings::all(), ); nocache_headers(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="softml-config-' . gmdate( 'Ymd-His' ) . '.json"' ); echo wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE ); // phpcs:ignore WordPress.Security.EscapeOutput exit; } /** * Rollback preparation: set every published translation post to draft so that, with the * plugin inactive, no translation is reachable as an English-content duplicate at its * internal slug. The IDs are remembered so they can be restored. */ public function handle_unpublish_all() { check_admin_referer( 'softml_unpublish_all' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } global $wpdb; $ids = $wpdb->get_col( // phpcs:ignore WordPress.DB $wpdb->prepare( "SELECT t.object_id FROM %i t JOIN {$wpdb->posts} p ON p.ID = t.object_id WHERE t.object_type = 'post' AND t.language_code <> %s AND p.post_status = 'publish'", Soft_ML_Translations::table(), Soft_ML_Languages::default_code() ) ); $done = array(); foreach ( array_map( 'intval', (array) $ids ) as $id ) { if ( ! is_wp_error( wp_update_post( array( 'ID' => $id, 'post_status' => 'draft' ), true ) ) ) { $done[] = $id; } } $prev = get_option( 'softml_unpublished_ids', array() ); update_option( 'softml_unpublished_ids', array_values( array_unique( array_merge( (array) $prev, $done ) ) ), false ); Soft_ML_Links::bump(); /* translators: %d: number of posts. */ $this->flash( 'success', sprintf( __( '%d translation(s) set to draft. You can now deactivate the plugin without leaving duplicate pages.', 'softtrix-multilanguage' ), count( $done ) ) ); $this->back( 'tools' ); } public function handle_republish_all() { check_admin_referer( 'softml_republish_all' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $ids = array_map( 'intval', (array) get_option( 'softml_unpublished_ids', array() ) ); $done = 0; foreach ( $ids as $id ) { $row = Soft_ML_Translations::get_row( $id ); if ( $row && 'published' === $row['status'] && ! is_wp_error( wp_update_post( array( 'ID' => $id, 'post_status' => 'publish' ), true ) ) ) { ++$done; } } delete_option( 'softml_unpublished_ids' ); Soft_ML_Links::bump(); /* translators: %d: number of posts. */ $this->flash( 'success', sprintf( __( '%d translation(s) re-published.', 'softtrix-multilanguage' ), $done ) ); $this->back( 'tools' ); } public function handle_import_translations() { check_admin_referer( 'softml_import_translations' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $file = isset( $_FILES['softml_pack']['tmp_name'] ) ? $_FILES['softml_pack']['tmp_name'] : ''; // phpcs:ignore WordPress.Security if ( ! $file || ! is_uploaded_file( $file ) || filesize( $file ) > 8 * 1048576 ) { $this->flash( 'error', __( 'Choose a translation pack (.json) under 8 MB.', 'softtrix-multilanguage' ) ); $this->back( 'auto' ); } $data = json_decode( (string) file_get_contents( $file ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions if ( ! is_array( $data ) ) { $this->flash( 'error', __( 'The file is not valid JSON.', 'softtrix-multilanguage' ) ); $this->back( 'auto' ); } $langs = isset( $_POST['langs'] ) && is_array( $_POST['langs'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['langs'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $res = Soft_ML_Import::run( $data, $langs, array( 'status' => ! empty( $_POST['approve'] ) ? Soft_ML_Strings::STATUS_APPROVED : Soft_ML_Strings::STATUS_DRAFT, 'overwrite' => ! empty( $_POST['overwrite'] ), ) ); if ( is_wp_error( $res ) ) { $this->flash( 'error', $res->get_error_message() ); $this->back( 'auto' ); } $parts = array(); foreach ( $res['saved'] as $code => $n ) { $parts[] = strtoupper( $code ) . ': ' . $n; } /* translators: 1: items in file, 2: saved per language, 3: skipped, 4: rejected */ $msg = sprintf( __( 'Imported from %1$d text items. Saved: %2$s. Skipped (already translated): %3$d. Rejected: %4$d.', 'softtrix-multilanguage' ), $res['items'], $parts ? implode( ', ', $parts ) : '0', $res['skipped_existing'], $res['rejected'] ); if ( $res['examples'] ) { $msg .= ' ' . implode( ' | ', $res['examples'] ); } $this->flash( $res['rejected'] ? 'error' : 'success', $msg ); $this->back( 'auto' ); } /** * Publish every unpublished translated page of the chosen languages at once (time-boxed batches). */ public function handle_publish_all() { check_admin_referer( 'softml_publish_all' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $a = $this->tr_args( $_POST ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- each field is sanitized in tr_args(). $langs = isset( $_POST['langs'] ) && is_array( $_POST['langs'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['langs'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput if ( ! $langs ) { $this->flash( 'error', __( 'Choose at least one language.', 'softtrix-multilanguage' ) ); $this->tr_back( $a ); } $res = Soft_ML_Bulk_Publish::run( $langs, ! empty( $_POST['approve'] ), ! empty( $_POST['only_complete'] ), 20, isset( $_POST['type'] ) ? array( $a['type'] ) : array() ); /* translators: 1: items published, 2: segments approved, 3: items skipped */ $msg = sprintf( __( 'Published %1$d translation(s). Segments approved: %2$d. Skipped (text still missing): %3$d.', 'softtrix-multilanguage' ), $res['published'], $res['approved_segments'], $res['skipped_incomplete'] ); if ( $res['remaining'] ) { /* translators: %d: pages left */ $msg .= ' ' . sprintf( __( '%d translation(s) are still waiting: click "Publish all" again to continue.', 'softtrix-multilanguage' ), $res['remaining'] ); } $this->flash( 'success', $msg ); $this->tr_back( $a ); } public function handle_import() { check_admin_referer( 'softml_import' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $file = isset( $_FILES['softml_file']['tmp_name'] ) ? $_FILES['softml_file']['tmp_name'] : ''; // phpcs:ignore WordPress.Security if ( ! $file || ! is_uploaded_file( $file ) || filesize( $file ) > 1048576 ) { $this->flash( 'error', __( 'Choose a JSON file under 1 MB.', 'softtrix-multilanguage' ) ); $this->back( 'tools' ); } $data = json_decode( (string) file_get_contents( $file ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions if ( ! is_array( $data ) || empty( $data['languages'] ) || ! is_array( $data['languages'] ) ) { $this->flash( 'error', __( 'That file is not a valid configuration export.', 'softtrix-multilanguage' ) ); $this->back( 'tools' ); } $clean = array(); $skipped = 0; $slugs = array(); foreach ( $data['languages'] as $i => $l ) { $code = isset( $l['code'] ) ? strtolower( (string) $l['code'] ) : ''; $slug = isset( $l['slug'] ) ? strtolower( (string) $l['slug'] ) : ''; $locale = isset( $l['locale'] ) ? (string) $l['locale'] : ''; $name = isset( $l['name'] ) ? sanitize_text_field( (string) $l['name'] ) : ''; $native = isset( $l['native_name'] ) ? sanitize_text_field( (string) $l['native_name'] ) : ''; $flag = isset( $l['flag'] ) ? strtolower( (string) $l['flag'] ) : ''; if ( ! Soft_ML_Validator::is_valid_code( $code ) || ! Soft_ML_Validator::is_valid_slug( $slug ) || isset( $slugs[ $slug ] ) || ! Soft_ML_Validator::is_valid_locale( $locale ) || '' === $name || '' === $native || ( '' !== $flag && 1 !== preg_match( '/^[a-z]{2}$/', $flag ) ) ) { ++$skipped; continue; } $slugs[ $slug ] = true; $clean[ $code ] = array( 'code' => $code, 'name' => $name, 'native_name' => $native, 'locale' => $locale, 'flag' => $flag, 'slug' => $slug, 'hreflang' => ( isset( $l['hreflang'] ) && 1 === preg_match( '/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8}){0,2}$/', (string) $l['hreflang'] ) ) ? (string) $l['hreflang'] : '', 'enabled' => ! empty( $l['enabled'] ), 'rtl' => ! empty( $l['rtl'] ), 'order' => (int) $i, ); } if ( ! $clean ) { $this->flash( 'error', __( 'No valid languages found in the file; nothing was changed.', 'softtrix-multilanguage' ) ); $this->back( 'tools' ); } $max = Soft_ML_Languages::max_extra(); if ( $max && count( $clean ) > $max + 1 ) { $skipped += count( $clean ) - ( $max + 1 ); $clean = array_slice( $clean, 0, $max + 1, true ); } Soft_ML_Languages::replace_all( $clean ); if ( isset( $data['settings'] ) && is_array( $data['settings'] ) ) { Soft_ML_Settings::save( Soft_ML_Settings::sanitize( $data['settings'] ) ); } /* translators: 1: imported count, 2: skipped count. */ $this->flash( 'success', sprintf( __( 'Imported %1$d language(s); skipped %2$d invalid.', 'softtrix-multilanguage' ), count( $clean ), $skipped ) ); $this->back( 'tools' ); } /* ---------------------------------------------------------------- views */ public function render() { if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'softtrix-multilanguage' ), 403 ); } $tabs = $this->tabs(); $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'languages'; // phpcs:ignore WordPress.Security.NonceVerification $tab = ( isset( $tabs[ $tab ] ) || 'edit' === $tab ) ? $tab : 'languages'; echo '

' . esc_html__( 'Softtrix Multilanguage', 'softtrix-multilanguage' ) . '

'; if ( method_exists( $this, 'view_' . $tab ) ) { call_user_func( array( $this, 'view_' . $tab ) ); } else { do_action( 'softml_admin_tab_' . $tab ); } echo '
'; } private function view_edit() { $this->editor->render(); } private function view_auto() { $this->auto->render(); } private function view_languages() { $default = Soft_ML_Languages::default_code(); $edit = isset( $_GET['edit'] ) ? sanitize_key( wp_unslash( $_GET['edit'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification $current = $edit ? Soft_ML_Languages::get( $edit ) : null; echo '

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

'; echo ''; foreach ( array( __( 'Code', 'softtrix-multilanguage' ), __( 'Name', 'softtrix-multilanguage' ), __( 'Native name', 'softtrix-multilanguage' ), __( 'Locale', 'softtrix-multilanguage' ), __( 'URL', 'softtrix-multilanguage' ), __( 'Status', 'softtrix-multilanguage' ), __( 'Actions', 'softtrix-multilanguage' ) ) as $h ) { echo ''; } echo ''; foreach ( Soft_ML_Languages::all() as $l ) { $is_default = $l['code'] === $default; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html( $h ) . '
' . esc_html( $l['code'] ) . '' . esc_html( $l['name'] ) . ( $is_default ? ' (' . esc_html__( 'default', 'softtrix-multilanguage' ) . ')' : '' ) . '' . esc_html( $l['native_name'] ) . '' . esc_html( $l['locale'] ) . '/' . esc_html( $l['slug'] ) . '/' . ( $l['enabled'] ? esc_html__( 'Enabled', 'softtrix-multilanguage' ) : esc_html__( 'Disabled', 'softtrix-multilanguage' ) ) . '' . esc_html__( 'Edit', 'softtrix-multilanguage' ) . ' '; if ( ! $is_default ) { $this->action_button( $l['code'], $l['enabled'] ? 'disable' : 'enable', $l['enabled'] ? __( 'Disable', 'softtrix-multilanguage' ) : __( 'Enable', 'softtrix-multilanguage' ) ); $this->action_button( $l['code'], 'default', __( 'Make default', 'softtrix-multilanguage' ), __( 'Change the default language? Existing content is still treated as the previous default until you relink it.', 'softtrix-multilanguage' ) ); $this->action_button( $l['code'], 'delete', __( 'Remove', 'softtrix-multilanguage' ), __( 'Remove this language?', 'softtrix-multilanguage' ) ); } echo '
'; if ( ! $current ) { $this->quick_add(); } $v = $current ? $current : array( 'code' => '', 'name' => '', 'native_name' => '', 'locale' => '', 'flag' => '', 'slug' => '', 'enabled' => true, 'rtl' => false ); echo $current ? '' : '
' . esc_html__( 'Add a language that is not in the list', 'softtrix-multilanguage' ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '

' . esc_html( $current ? __( 'Edit language', 'softtrix-multilanguage' ) : __( 'Custom language', 'softtrix-multilanguage' ) ) . '

'; echo '
'; wp_nonce_field( 'softml_save_language' ); echo ''; echo ''; $this->text_row( 'lang[code]', __( 'Language code', 'softtrix-multilanguage' ), $v['code'], (bool) $current, __( 'e.g. fr, de, pt-br. Cannot be changed later.', 'softtrix-multilanguage' ) ); $this->text_row( 'lang[name]', __( 'Name (English)', 'softtrix-multilanguage' ), $v['name'] ); $this->text_row( 'lang[native_name]', __( 'Native name', 'softtrix-multilanguage' ), $v['native_name'] ); $this->text_row( 'lang[locale]', __( 'Locale', 'softtrix-multilanguage' ), $v['locale'], false, __( 'e.g. fr_FR', 'softtrix-multilanguage' ) ); $this->text_row( 'lang[slug]', __( 'URL slug', 'softtrix-multilanguage' ), $v['slug'], false, __( 'Directory used in URLs, e.g. fr gives /fr/. Translated page slugs are set per page.', 'softtrix-multilanguage' ) ); $this->text_row( 'lang[hreflang]', __( 'hreflang override', 'softtrix-multilanguage' ), isset( $v['hreflang'] ) ? $v['hreflang'] : '', false, __( 'Optional regional code for search engines, e.g. fr-CA. Empty = use the language code.', 'softtrix-multilanguage' ) ); $this->text_row( 'lang[flag]', __( 'Flag (2-letter country code)', 'softtrix-multilanguage' ), $v['flag'] ); echo ''; echo '

' . esc_html__( 'Fix spacing or font size for this language only. Pages of this language get the body classes softml-lang- and softml-dir-ltr/rtl, and is set, so you can target them.', 'softtrix-multilanguage' ) . '

' . esc_html__( 'Options', 'softtrix-multilanguage' ) . ''; echo '
'; echo ''; echo '
'; submit_button( $current ? __( 'Update language', 'softtrix-multilanguage' ) : __( 'Add language', 'softtrix-multilanguage' ) ); echo '
'; echo $current ? '' : '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Pick languages from the built-in list. */ private function quick_add() { $have = Soft_ML_Languages::all(); $list = array_diff_key( Soft_ML_Catalog::all(), $have ); uasort( $list, static function ( $a, $b ) { return strcmp( $a['name'], $b['name'] ); } ); echo '

' . esc_html__( 'Add languages', 'softtrix-multilanguage' ) . '

'; if ( ! Soft_ML_Languages::can_add_more() ) { /* translators: %d: number of languages */ echo '

' . esc_html( sprintf( __( 'This site allows %d extra language(s).', 'softtrix-multilanguage' ), Soft_ML_Languages::max_extra() ) ) . '

'; return; } echo '

' . esc_html__( 'Tick every language you want your website in, then click "Add selected languages". Each language gets its own address, for example /fr/ for French.', 'softtrix-multilanguage' ) . '

'; echo '
'; wp_nonce_field( 'softml_add_languages' ); echo '

'; echo '
'; foreach ( $list as $code => $l ) { echo ''; } echo '
'; submit_button( __( 'Add selected languages', 'softtrix-multilanguage' ) ); echo '
'; } private function action_button( $code, $op, $label, $confirm = '' ) { echo '
'; echo ''; echo ''; wp_nonce_field( 'softml_language_action' ); echo '
'; } private function text_row( $name, $label, $value, $readonly = false, $help = '' ) { echo ''; echo ''; if ( $help ) { echo '

' . esc_html( $help ) . '

'; } echo ''; } private function view_settings() { $values = Soft_ML_Settings::all(); echo '
'; wp_nonce_field( 'softml_save_settings' ); foreach ( Soft_ML_Settings::schema() as $section => $def ) { echo '

' . esc_html( $def['label'] ) . '

'; foreach ( $def['fields'] as $key => $f ) { $name = "softml[{$section}][{$key}]"; $val = $values[ $section ][ $key ]; echo ''; } echo '
' . esc_html( $f['label'] ) . ''; switch ( $f['type'] ) { case 'bool': echo ''; break; case 'enum': echo ''; break; case 'lang': echo ''; break; case 'int': echo ''; break; case 'menu': echo ''; break; case 'map': $lines = array(); foreach ( (array) $val as $cc => $lg ) { $lines[] = $cc . '=' . $lg; } echo ''; break; default: echo ''; } if ( ! empty( $f['help'] ) ) { echo '

' . esc_html( $f['help'] ) . '

'; } echo '
'; } submit_button(); echo '
'; } /** * Translations tab filters: content type, status filter, search and page. Handlers pass $_POST after * their nonce check; the list view passes $_GET (read-only filters). * * @param array $src Request fields. * @return array type, status, s, paged */ private function tr_args( array $src ) { $type = isset( $src['type'] ) ? sanitize_key( wp_unslash( $src['type'] ) ) : 'page'; $status = isset( $src['status'] ) ? sanitize_key( wp_unslash( $src['status'] ) ) : 'all'; $search = isset( $src['s'] ) ? trim( sanitize_text_field( wp_unslash( $src['s'] ) ) ) : ''; $paged = isset( $src['paged'] ) ? max( 1, absint( $src['paged'] ) ) : 1; return array( 'type' => in_array( $type, Soft_ML_Page_Creator::supported_types(), true ) ? $type : 'page', 'status' => array_key_exists( $status, $this->status_filters() ) ? $status : 'all', 's' => $search, 'paged' => $paged, ); } /** * Status filters of the Translations tab: key => [ label, post statuses ]. * * @return array */ private function status_filters() { return array( 'all' => array( __( 'All', 'softtrix-multilanguage' ), array( 'publish', 'draft', 'pending', 'private', 'future' ) ), 'publish' => array( __( 'Published', 'softtrix-multilanguage' ), array( 'publish' ) ), 'unpublished' => array( __( 'Unpublished', 'softtrix-multilanguage' ), array( 'draft', 'pending', 'private', 'future' ) ), 'tr_pending' => array( __( 'Translations not published yet', 'softtrix-multilanguage' ), array( 'publish', 'draft', 'pending', 'private', 'future' ) ), ); } /** * Hidden fields that bring a form back to the same Translations view. * * @param array $a tr_args(). */ private function tr_hidden( array $a ) { foreach ( array( 'type', 'status', 's', 'paged' ) as $k ) { echo ''; } } private function tr_back( array $a ) { $this->back( 'translations', array_filter( array( 'type' => $a['type'], 'status' => 'all' === $a['status'] ? '' : $a['status'], 's' => $a['s'], 'paged' => $a['paged'] > 1 ? $a['paged'] : '' ) ) ); } /** * Originals of a type whose translation group has a translation that is not published yet. * * @param string $type Post type. * @return int[] */ private function with_unpublished_translations( $type ) { global $wpdb; $t = Soft_ML_Translations::table(); return array_map( 'intval', (array) $wpdb->get_col( // phpcs:ignore WordPress.DB $wpdb->prepare( "SELECT DISTINCT src.object_id FROM %i tr JOIN {$wpdb->posts} p ON p.ID = tr.object_id JOIN %i src ON src.group_id = tr.group_id AND src.language_code = %s AND src.object_type = 'post' WHERE tr.object_type = 'post' AND tr.language_code <> %s AND p.post_type = %s AND p.post_status IN ('draft','pending')", $t, $t, Soft_ML_Languages::default_code(), Soft_ML_Languages::default_code(), $type ) ) ); } /** * Approve draft text from the Translations / Auto-translate tabs: * one cell (page x language), one row (page x every language), the ticked rows, or every page/post of a type. */ public function handle_approve() { check_admin_referer( 'softml_approve' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'softtrix-multilanguage' ), 403 ); } $a = $this->tr_args( $_POST ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- each field is sanitized in tr_args(). $default = Soft_ML_Languages::default_code(); $all = array_values( array_diff( array_keys( Soft_ML_Languages::enabled() ), array( $default ) ) ); $langs = isset( $_POST['langs'] ) && is_array( $_POST['langs'] ) ? array_values( array_intersect( array_map( 'sanitize_key', wp_unslash( $_POST['langs'] ) ), $all ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $return = isset( $_POST['return'] ) && 'auto' === $_POST['return'] ? 'auto' : 'translations'; $n = 0; $pub = null; if ( ! empty( $_POST['pair'] ) ) { // "123:fr" = one page in one language, "123:*" = one page in every language. list( $id, $code ) = array_pad( explode( ':', sanitize_text_field( wp_unslash( $_POST['pair'] ) ), 2 ), 2, '' ); $id = absint( $id ); $langs = '*' === $code ? $all : array_intersect( array( sanitize_key( $code ) ), $all ); if ( $id && $langs && current_user_can( 'edit_post', $id ) ) { $n = Soft_ML_Strings::approve_drafts_many( array( $id ), $langs ); } } elseif ( ! $langs ) { $this->flash( 'error', __( 'Choose at least one language.', 'softtrix-multilanguage' ) ); 'auto' === $return ? $this->back( 'auto' ) : $this->tr_back( $a ); } elseif ( isset( $_POST['scope'] ) && 'selected' === $_POST['scope'] ) { $ids = isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) ? array_map( 'absint', wp_unslash( $_POST['ids'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput if ( ! $ids ) { $this->flash( 'error', __( 'Tick at least one page or post in the list.', 'softtrix-multilanguage' ) ); 'auto' === $return ? $this->back( 'auto' ) : $this->tr_back( $a ); } $n = Soft_ML_Strings::approve_drafts_many( $ids, $langs ); if ( ! empty( $_POST['publish'] ) ) { $pub = Soft_ML_Bulk_Publish::run( $langs, false, false, 20, array(), $ids ); } } else { // Every scanned page (or post) of the type. $type = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( $_POST['type'] ) ) : 'page'; $type = in_array( $type, Soft_ML_Page_Creator::supported_types(), true ) ? $type : 'page'; $n = Soft_ML_Strings::approve_drafts_many( Soft_ML_Strings::scanned_ids( $type, isset( $_POST['scope'] ) && 'published' === $_POST['scope'] ), $langs ); } /* translators: %d: number of text segments */ $msg = sprintf( _n( '%d draft text segment approved.', '%d draft text segments approved.', $n, 'softtrix-multilanguage' ), $n ); if ( $pub ) { /* translators: %d: number of pages */ $msg .= ' ' . sprintf( __( 'Translations published: %d.', 'softtrix-multilanguage' ), $pub['published'] ); if ( $pub['remaining'] ) { /* translators: %d: number of pages */ $msg .= ' ' . sprintf( __( '%d are still waiting: run it again to continue.', 'softtrix-multilanguage' ), $pub['remaining'] ); } } $this->flash( 'success', $msg ); 'auto' === $return ? $this->back( 'auto', array( 'type' => $a['type'] ) ) : $this->tr_back( $a ); } private function view_translations() { global $wpdb; $langs = Soft_ML_Languages::enabled(); $default = Soft_ML_Languages::default_code(); $a = $this->tr_args( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput -- read-only list filters, sanitized in tr_args(). $type = $a['type']; $filters = $this->status_filters(); $paged = $a['paged']; $search = $a['s']; $others = array_filter( $langs, static function ( $l ) use ( $default ) { return $l['code'] !== $default; } ); $tobj = get_post_type_object( $type ); $plural = $tobj ? $tobj->labels->name : $type; $plural_lc = function_exists( 'mb_strtolower' ) ? mb_strtolower( $plural ) : strtolower( $plural ); // Originals only: exclude posts linked as a non-default language. $translated = array_map( 'absint', (array) $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prepare( "SELECT object_id FROM %i WHERE object_type = 'post' AND language_code <> %s", Soft_ML_Translations::table(), $default ) ) ); // Counts per content type and per status filter (originals only). $type_counts = array(); foreach ( Soft_ML_Page_Creator::supported_types() as $pt ) { $type_counts[ $pt ] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status IN ('publish','draft','pending','private','future') AND ID NOT IN (SELECT object_id FROM %i WHERE object_type = 'post' AND language_code <> %s)", $pt, Soft_ML_Translations::table(), $default ) ); // phpcs:ignore WordPress.DB } $status_counts = array(); foreach ( $filters as $k => $f ) { if ( 'tr_pending' === $k ) { $status_counts[ $k ] = count( $this->with_unpublished_translations( $type ) ); continue; } $status_counts[ $k ] = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status IN (" . implode( ',', array_fill( 0, count( $f[1] ), '%s' ) ) . ") AND ID NOT IN (SELECT object_id FROM %i WHERE object_type = 'post' AND language_code <> %s)", array_merge( array( $type ), array_values( $f[1] ), array( Soft_ML_Translations::table(), $default ) ) ) ); } // Search: every word must appear in the title or the URL slug. $where = null; if ( '' !== $search ) { $words = array_slice( array_filter( preg_split( '/\s+/', $search ) ), 0, 8 ); $where = static function ( $sql ) use ( $words, $wpdb ) { foreach ( $words as $w ) { $like = '%' . $wpdb->esc_like( $w ) . '%'; $sql .= $wpdb->prepare( " AND ( {$wpdb->posts}.post_title LIKE %s OR {$wpdb->posts}.post_name LIKE %s )", $like, $like ); // phpcs:ignore WordPress.DB } return $sql; }; add_filter( 'posts_where', $where ); } $args = array( 'post__not_in' => $translated, 'post_type' => $type, 'post_status' => $filters[ $a['status'] ][1], 'posts_per_page' => 100, 'paged' => $paged, 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => false, 'suppress_filters' => false, ); if ( 'tr_pending' === $a['status'] ) { $ids = $this->with_unpublished_translations( $type ); $args['post__in'] = $ids ? $ids : array( 0 ); } $q = new WP_Query( $args ); if ( $where ) { remove_filter( 'posts_where', $where ); } $view = function ( array $over ) use ( $a ) { $v = array_merge( $a, $over ); return $this->url( array_filter( array( 'tab' => 'translations', 'type' => $v['type'], 'status' => 'all' === $v['status'] ? '' : $v['status'], 's' => $v['s'], 'paged' => $v['paged'] > 1 ? $v['paged'] : '' ) ) ); }; echo '

' . esc_html__( 'Translation status', 'softtrix-multilanguage' ) . '

'; echo '

' . esc_html__( 'Create a translation for a published page, post or other item (for example a location), then open it to scan the original and translate its text segments.', 'softtrix-multilanguage' ) . '

'; // Pages | Posts. echo ''; // All | Published | Unpublished | Translations not published yet. echo '' ); $box = 'margin:10px 0;padding:10px 14px;background:#fff;border:1px solid #c3c4c7'; // Bulk publish: every unpublished translation of this content type in the chosen languages. $pending = Soft_ML_Bulk_Publish::pending_counts( array( $type ) ); if ( array_sum( $pending ) ) { echo '
'; wp_nonce_field( 'softml_publish_all' ); $this->tr_hidden( $a ); /* translators: %s: content type, e.g. pages */ echo '' . esc_html( sprintf( __( 'Publish all translated %s', 'softtrix-multilanguage' ), $plural_lc ) ) . ' '; foreach ( $others as $l ) { if ( ! empty( $pending[ $l['code'] ] ) ) { echo ''; } } echo '
'; echo '
'; echo ''; echo '
'; } // Approve all: every draft text of this content type in the chosen languages. $drafts = Soft_ML_Strings::draft_counts( $type ); echo '
'; wp_nonce_field( 'softml_approve' ); $this->tr_hidden( $a ); /* translators: %s: content type, e.g. pages */ echo '' . esc_html( sprintf( __( 'Approve all draft text of %s', 'softtrix-multilanguage' ), $plural_lc ) ) . ' '; foreach ( $others as $l ) { $n = isset( $drafts[ $l['code'] ] ) ? $drafts[ $l['code'] ] : 0; echo ''; } echo '
'; echo ''; echo '

' . esc_html__( 'Numbers are draft text segments. Text shared by several pages (header, footer) is approved for all of them. You can run this as often as you like, per language.', 'softtrix-multilanguage' ) . '

'; echo '
'; // Search box (GET form: results are shareable and survive paging). echo '
'; echo ''; echo ''; /* translators: %s: content type, e.g. pages */ echo ' '; echo ''; if ( '' !== $search ) { echo ' ' . esc_html__( 'Clear', 'softtrix-multilanguage' ) . ''; } echo ' ' . esc_html( number_format_i18n( (int) $q->found_posts ) . ' ' . $plural_lc ) . ''; echo '
'; // Ticked rows: approve (and optionally publish) in the chosen languages. echo '
'; wp_nonce_field( 'softml_approve' ); $this->tr_hidden( $a ); echo '' . esc_html__( 'Ticked rows:', 'softtrix-multilanguage' ) . ' '; foreach ( $others as $l ) { echo ''; } echo ' '; echo ''; echo '
'; // Single cell / row "Approve" buttons submit this form (forms cannot nest inside the table). echo '
'; wp_nonce_field( 'softml_approve' ); $this->tr_hidden( $a ); echo '
'; if ( ! $q->posts ) { echo '

' . esc_html__( 'Nothing matches this view.', 'softtrix-multilanguage' ) . '

'; } echo ''; foreach ( $langs as $l ) { echo ''; } echo ''; $labels = array( 'draft' => __( 'Draft', 'softtrix-multilanguage' ), 'in_progress' => __( 'In Progress', 'softtrix-multilanguage' ), 'published' => __( 'Published', 'softtrix-multilanguage' ), ); $approve_btn = static function ( $pair, $label, $title ) { return ' '; }; foreach ( $q->posts as $p ) { $group = Soft_ML_Translations::get_translations( $p->ID, 'post' ); $cells = ''; $row_drafts = 0; foreach ( $langs as $l ) { if ( $l['code'] === $default ) { $text = 'publish' === $p->post_status ? __( 'Published', 'softtrix-multilanguage' ) : __( 'Draft', 'softtrix-multilanguage' ); $cells .= ''; } elseif ( isset( $group[ $l['code'] ] ) ) { $st = $group[ $l['code'] ]['status']; $text = isset( $labels[ $st ] ) ? $labels[ $st ] : $st; // How much of the page's text is approved in this language (the rest shows in English). $pr = Soft_ML_Strings::progress( $p->ID, $l['code'] ); $extra = ''; if ( $pr['total'] > 0 ) { $pct = (int) floor( 100 * $pr['approved'] / $pr['total'] ); $color = 100 === $pct ? '#1a7f37' : ( $pct >= 80 ? '#9a6700' : '#b32d2e' ); $extra .= ' ' . $pct . '%'; if ( $pr['draft'] > 0 ) { $row_drafts += $pr['draft']; /* translators: %d: number of draft segments */ $extra .= $approve_btn( $p->ID . ':' . $l['code'], sprintf( __( 'Approve %d', 'softtrix-multilanguage' ), $pr['draft'] ), sprintf( /* translators: 1: number, 2: language */ __( 'Approve %1$d draft text segment(s) in %2$s', 'softtrix-multilanguage' ), $pr['draft'], $l['native_name'] ) ); } } else { $extra .= ' ' . esc_html__( 'not scanned', 'softtrix-multilanguage' ) . ''; } $cells .= ''; } elseif ( 'publish' === $p->post_status && in_array( $p->post_type, Soft_ML_Page_Creator::supported_types(), true ) ) { $cells .= ''; } else { $cells .= ''; } } $status_note = 'publish' === $p->post_status ? '' : ' — ' . esc_html( $labels['draft'] ) . ''; echo ''; echo '' . $cells . '' ); } echo '
' . esc_html__( 'Content', 'softtrix-multilanguage' ) . '' . esc_html( $l['native_name'] ) . '
' . esc_html( $text ) . '' . esc_html( $text ) . ' → ' . esc_html__( 'Edit', 'softtrix-multilanguage' ) . '' . $extra . '' . esc_html__( 'Not Started', 'softtrix-multilanguage' ) . ' ' . '
' . wp_nonce_field( 'softml_create_translation', '_wpnonce', true, false ) . '
' . esc_html__( 'Not Started', 'softtrix-multilanguage' ) . '
' . esc_html( wp_strip_all_tags( get_the_title( $p ) ) ) . '' . wp_kses( $status_note, array( 'em' => array() ) ); if ( $row_drafts > 1 && count( $others ) > 1 ) { Soft_ML_Kses::echo_html( '
' . $approve_btn( $p->ID . ':*', __( 'Approve all languages', 'softtrix-multilanguage' ), __( 'Approve the draft text of this item in every language', 'softtrix-multilanguage' ) ) ); } Soft_ML_Kses::echo_html( '
'; echo wp_kses_post( (string) paginate_links( array( 'base' => add_query_arg( 'paged', '%#%', $view( array( 'paged' => 1 ) ) ), 'format' => '', 'current' => $paged, 'total' => (int) $q->max_num_pages, ) ) ); wp_reset_postdata(); } private function view_tools() { echo '

' . esc_html__( 'Settings backup', 'softtrix-multilanguage' ) . '

'; echo '
'; wp_nonce_field( 'softml_export' ); submit_button( __( 'Download languages and settings (JSON)', 'softtrix-multilanguage' ), 'secondary', 'submit', false ); echo '

' . esc_html__( 'Use it to copy the same languages and settings to another website. Translated pages are normal WordPress pages: back them up with your usual site backup.', 'softtrix-multilanguage' ) . '

'; echo '
'; wp_nonce_field( 'softml_import' ); echo ' '; submit_button( __( 'Import languages and settings', 'softtrix-multilanguage' ), 'secondary', 'submit', false ); echo '
'; echo '

' . esc_html__( 'Before deactivating', 'softtrix-multilanguage' ) . '

'; echo '

' . esc_html__( 'Translations are normal WordPress pages. If you deactivate the plugin they stay online at an internal address as untranslated copies. Unpublish them first; you can re-publish them later.', 'softtrix-multilanguage' ) . '

'; foreach ( array( 'softml_unpublish_all' => __( 'Unpublish all translations', 'softtrix-multilanguage' ), 'softml_republish_all' => __( 'Re-publish translations unpublished this way', 'softtrix-multilanguage' ) ) as $act => $label ) { echo '
'; wp_nonce_field( $act ); submit_button( $label, 'secondary', 'submit', false ); echo '
'; } /** * Extra tools from add-ons. */ do_action( 'softml_tools_after' ); global $wpdb, $wp_version; $table = Soft_ML_Translations::table(); $exists = $table === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $yes = __( 'Yes', 'softtrix-multilanguage' ); $no = __( 'No', 'softtrix-multilanguage' ); $rows = array( __( 'Plugin version', 'softtrix-multilanguage' ) => SOFT_ML_VERSION, __( 'DB schema version', 'softtrix-multilanguage' ) => get_option( 'softml_db_version', '-' ) . ' / ' . SOFT_ML_DB_VERSION, __( 'Translations table present', 'softtrix-multilanguage' ) => $exists ? $yes : $no, __( 'WordPress / PHP', 'softtrix-multilanguage' ) => $wp_version . ' / ' . PHP_VERSION, __( 'Permalink structure', 'softtrix-multilanguage' ) => get_option( 'permalink_structure' ) ? get_option( 'permalink_structure' ) : __( 'Plain - language addresses need pretty permalinks (Settings > Permalinks)', 'softtrix-multilanguage' ), __( 'SEO plugin', 'softtrix-multilanguage' ) => Soft_ML_SEO_Meta::plugin(), __( 'Translation method', 'softtrix-multilanguage' ) => (string) Soft_ML_Settings::get( 'general', 'method' ), __( 'Translation API', 'softtrix-multilanguage' ) => Soft_ML_MT::is_configured() ? Soft_ML_MT::settings()['provider'] : __( '(not connected)', 'softtrix-multilanguage' ), __( 'Enabled languages', 'softtrix-multilanguage' ) => implode( ', ', array_keys( Soft_ML_Languages::enabled() ) ), __( 'Linked translations', 'softtrix-multilanguage' ) => $exists ? (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $table ) ) : 0, // phpcs:ignore WordPress.DB ); echo '

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

'; foreach ( $rows as $k => $v ) { echo ''; } echo '
' . esc_html( $k ) . '' . esc_html( (string) $v ) . '
'; } }