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(
'
',
'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' ) . ' ';
foreach ( $tabs as $key => $label ) {
printf(
'%s ',
esc_url( $this->url( array( 'tab' => $key ) ) ),
$key === $tab ? ' nav-tab-active' : '',
esc_html( $label )
);
}
echo ' ';
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 '' . esc_html( $h ) . ' ';
}
echo ' ';
foreach ( Soft_ML_Languages::all() as $l ) {
$is_default = $l['code'] === $default;
echo '';
echo '' . esc_html( $l['code'] ) . ' ';
echo '' . esc_html( $l['name'] ) . ( $is_default ? ' (' . esc_html__( 'default', 'softtrix-multilanguage' ) . ') ' : '' ) . ' ';
echo '' . esc_html( $l['native_name'] ) . ' ';
echo '' . esc_html( $l['locale'] ) . ' ';
echo '/' . esc_html( $l['slug'] ) . '/ ';
echo '' . ( $l['enabled'] ? esc_html__( 'Enabled', 'softtrix-multilanguage' ) : esc_html__( 'Disabled', 'softtrix-multilanguage' ) ) . ' ';
echo '' . 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 ' ';
}
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 '';
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 ' ' . esc_html( $l['name'] ) . ( $l['native_name'] !== $l['name'] ? ' (' . esc_html( $l['native_name'] ) . ') ' : '' ) . ' ';
}
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 '' . esc_html( $label ) . ' ';
}
private function text_row( $name, $label, $value, $readonly = false, $help = '' ) {
echo '' . esc_html( $label ) . ' ';
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'] ) . ' ';
}
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 '';
foreach ( Soft_ML_Page_Creator::supported_types() as $pt ) {
$o = get_post_type_object( $pt );
printf(
'%s (%s) ',
esc_url( $view( array( 'type' => $pt, 'status' => 'all', 's' => '', 'paged' => 1 ) ) ),
$pt === $type ? ' nav-tab-active' : '',
esc_html( $o ? $o->labels->name : $pt ),
esc_html( number_format_i18n( $type_counts[ $pt ] ) )
);
}
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 ' ' . esc_html( $l['native_name'] ) . ' (' . (int) $pending[ $l['code'] ] . ') ';
}
}
echo ' ' . esc_html__( 'Approve all draft text of these first (otherwise unapproved text stays in the original language)', 'softtrix-multilanguage' ) . ' ';
echo ' ' . esc_html__( 'Only publish the ones whose text is fully translated', 'softtrix-multilanguage' ) . ' ';
echo '' . esc_html__( 'Publish all', 'softtrix-multilanguage' ) . ' ';
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 ' 0, true, false ) . '> ' . esc_html( $l['native_name'] ) . ' (' . esc_html( number_format_i18n( $n ) ) . ') ';
}
echo ' ' . esc_html( sprintf( /* translators: %s: content type */ __( 'All %s', 'softtrix-multilanguage' ), $plural_lc ) ) . ' ' . esc_html( sprintf( /* translators: %s: content type */ __( 'Only published %s', 'softtrix-multilanguage' ), $plural_lc ) ) . ' ';
echo '' . esc_html__( 'Approve all', 'softtrix-multilanguage' ) . ' ';
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 '' . esc_html__( 'Search', 'softtrix-multilanguage' ) . ' ';
/* translators: %s: content type, e.g. pages */
echo ' ';
echo '' . esc_html__( 'Search', 'softtrix-multilanguage' ) . ' ';
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 ' ' . esc_html( $l['native_name'] ) . ' ';
}
echo '' . esc_html__( 'Approve all draft text', 'softtrix-multilanguage' ) . ' ';
echo '' . esc_html__( 'Approve and publish', 'softtrix-multilanguage' ) . ' ';
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 '' . esc_html__( 'Content', 'softtrix-multilanguage' ) . ' ';
foreach ( $langs as $l ) {
echo '' . esc_html( $l['native_name'] ) . ' ';
}
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 ' ' . esc_html( $label ) . ' ';
};
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 .= '' . esc_html( $text ) . ' ';
} 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 .= '' . esc_html( $text ) . ' → ' . esc_html__( 'Edit', 'softtrix-multilanguage' ) . ' ' . $extra . ' ';
} elseif ( 'publish' === $p->post_status && in_array( $p->post_type, Soft_ML_Page_Creator::supported_types(), true ) ) {
$cells .= '' . esc_html__( 'Not Started', 'softtrix-multilanguage' ) . ' '
. ' '
. wp_nonce_field( 'softml_create_translation', '_wpnonce', true, false )
. '' . esc_html__( 'Create', 'softtrix-multilanguage' ) . ' ';
} else {
$cells .= '' . esc_html__( 'Not Started', 'softtrix-multilanguage' ) . ' ';
}
}
$status_note = 'publish' === $p->post_status ? '' : ' — ' . esc_html( $labels['draft'] ) . ' ';
echo ' ';
echo '' . 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( ' ' . $cells . ' ' );
}
echo '
';
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 '' . esc_html( $k ) . ' ' . esc_html( (string) $v ) . ' ';
}
echo '
';
}
}