first commit

This commit is contained in:
2025-02-24 22:33:42 +01:00
commit 737c037e85
18358 changed files with 5392983 additions and 0 deletions

View File

@@ -0,0 +1,213 @@
<?php
/**
* @package Polylang-Pro
*/
/**
* Define a Bulk_Translate option
*
* Should be registered using the pll_bulk_action_options
*
* @see PLL_Bulk_Translate
*
* @since 2.7
*/
abstract class PLL_Bulk_Translate_Option {
/**
* A reference to the current PLL_Model
*
* @since 2.7
*
* @var PLL_Model
*/
protected $model;
/**
* The name by which the option is referred to in forms
*
* @since 2.7
*
* @var string
*/
protected $name;
/**
* A short sentence to explicit what this option does
*
* @since 2.7
*
* @var string
*/
protected $description;
/**
* Determines the order in which the options should be displayed.
*
* Lower priority options are displayed before higher priority options.
*
* @since 2.7
*
* @var int
*/
protected $priority;
/**
* Constructor
*
* @since 2.7
*
* @param array $args {
* string $name The name of the option.
* string $description A short sentence too describe what this option does. Please use i18n functions.
* int $priority Determines the order of displaying th options. Default 10.
* }.
* @param PLL_Model $model An instance to the current instance of PLL_Model.
*/
public function __construct( $args, $model ) {
$this->name = $args['name'];
$this->description = $args['description'];
$this->priority = array_key_exists( 'priority', $args ) ? $args['priority'] : 10;
$this->model = $model;
}
/**
* Displays the input bulk option in the bulk translate form.
*
* @since 3.6
*
* @param string $selected The selected option name.
* @return void
*/
public function display( string $selected ) {
$bulk_translate_option = $this;
include __DIR__ . '/view-bulk-translate-option.php';
}
/**
* Getter
*
* @since 2.7
*
* @return string
*/
public function get_name() {
return $this->name;
}
/**
* Getter
*
* @since 2.7
*
* @return string
*/
public function get_description() {
return $this->description;
}
/**
* Getter.
*
* @since 2.7
*
* @return int
*/
public function get_priority() {
return $this->priority;
}
/**
* Checks whether the option should be selectable by the user.
*
* @since 2.7
*
* @return bool
*/
abstract public function is_available();
/**
* Decides whether or not an object should be translated.
*
* @since 2.7
*
* @param int $id Identify a post, media, term...
* @param string $lang A language locale.
* @return bool|int
*/
public function filter( $id, $lang ) {
return ! $this->model->post->get_translation( $id, $lang );
}
/**
* Triggers the correct functions for creating a translation of the selected content in a selected language.
*
* Has to be overridden.
*
* @since 2.7
*
* @param int $object_id Identifies the post, term, media, etc. to be translated.
* @param string $lang A language locale.
* @return void
*/
abstract public function translate( $object_id, $lang );
/**
* The actual effect of the bulk translate action
*
* @since 2.7
* @since 3.6 Returns as WP_Error instead of an array.
*
* @param int[] $object_ids An array of the id of the WordPress objects to translate.
* @param string[] $languages An array of the locales of the languages in which to translate.
* @return WP_Error Info notices to be displayed to the user.
*/
public function do_bulk_action( $object_ids, $languages ): WP_Error {
$done = 0;
$missed = 0;
if ( ! empty( $object_ids ) ) {
foreach ( $object_ids as $object_id ) {
if ( $this->model->post->get_language( $object_id ) !== false ) {
foreach ( $languages as $lang ) {
if ( $this->filter( $object_id, $lang ) ) {
$this->translate( $object_id, $lang );
++$done;
} else {
++$missed;
}
}
}
}
}
$notice = new WP_Error();
if ( 0 < $done ) {
$notice->add(
'pll_bulk_translate_success',
sprintf(
/* translators: %d is a number of posts */
_n( '%d translation created.', '%d translations created.', $done, 'polylang-pro' ),
$done
),
'success'
);
}
if ( 0 < $missed ) {
$notice->add(
'pll_bulk_translate_warning',
sprintf(
/* translators: %d is a number of posts */
_n( 'To avoid overwriting content, %d translation was not created.', 'To avoid overwriting content, %d translations were not created.', $missed, 'polylang-pro' ),
$missed
),
'warning'
);
}
return $notice;
}
}

View File

@@ -0,0 +1,359 @@
<?php
/**
* @package Polylang-Pro
*/
/**
* A class to bulk translate posts.
*
* @since 2.4
*/
class PLL_Bulk_Translate {
/**
* @var PLL_Model
*/
protected $model;
/**
* Reference to the current WP_Screen object.
*
* @since 2.7
*
* @var WP_Screen|null
*/
protected $current_screen;
/**
* Stores the results of the bulk action when it's done.
*
* @since 2.7
*
* @var array|null
*/
protected $results;
/**
* References the options for the bulk action.
*
* @since 2.7
*
* @var PLL_Bulk_Translate_Option[]
*/
protected $options = array();
/**
* PLL_Bulk_Translate constructor.
*
* @since 2.4
*
* @param PLL_Model $model Shared instance of the current PLL_Model.
*/
public function __construct( $model ) {
$this->model = $model;
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
}
/**
* Enqueues script and style.
*
* @since 2.8
*
* @return void
*/
public function admin_enqueue_scripts() {
$screen = get_current_screen();
if ( $screen && in_array( $screen->base, array( 'edit', 'upload' ) ) ) {
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_script(
'pll_bulk_translate',
plugins_url( '/js/build/bulk-translate' . $suffix . '.js', POLYLANG_ROOT_FILE ),
array( 'jquery', 'wp-ajax-response' ),
POLYLANG_VERSION,
true
);
wp_enqueue_style(
'pll_bulk_translate',
plugins_url( '/css/build/bulk-translate' . $suffix . '.css', POLYLANG_ROOT_FILE ),
array(),
POLYLANG_VERSION
);
}
}
/**
* Registers options of the Translate bulk action.
*
* @since 2.7
*
* @param PLL_Bulk_Translate_Option[] $options An array of {@see PLL_Bulk_Translate_Option} to register.
* @return void
*/
public function register_options( $options ) {
if ( ! is_array( $options ) ) {
$options = array( $options );
}
foreach ( $options as $option ) {
if ( array_key_exists( $option->get_name(), $this->options ) ) {
if ( WP_DEBUG ) {
trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions
sprintf(
'Error when trying to register Bulk Translate option with name \'%s\' : an option with this name already exists.',
esc_attr( $option->get_name() )
)
);
}
continue;
}
$this->options[ $option->get_name() ] = $option;
}
}
/**
* Add actions and filters.
*
* Verifies the post type is allowed for translation and that the post status isn't 'trashed'.
*
* @since 2.4
* @since 2.7 hooked on 'current_screen' and takes the screen as parameter.
*
* @param WP_Screen $current_screen Instance of the current WP_Screen.
* @return void
*/
public function init( $current_screen ) {
/**
* Filter the list of post types enabling the bulk translate.
*
* @since 2.4
*
* @param string[] $post_types List of post types.
*/
$post_types = apply_filters( 'pll_bulk_translate_post_types', $this->model->get_translated_post_types() );
// phpcs:ignore WordPress.Security.NonceVerification
if ( ! in_array( $current_screen->post_type, $post_types ) || ( array_key_exists( 'post_status', $_GET ) && 'trash' === $_GET['post_status'] ) ) {
return;
}
/**
* Fires before `PLL_Bulk_Translate` init.
* This is the perfect place to register options of the Translate bulk action.
*
* @since 3.6.5
* @see PLL_Bulk_Translate::register_options()
*
* @param PLL_Bulk_Translate $bulk_translate Instance of `PLL_Bulk_Translate`.
*/
do_action( 'pll_bulk_translate_options_init', $this );
$this->options = array_filter(
$this->options,
function ( $option ) {
return $option->is_available();
}
);
$this->current_screen = $current_screen;
if ( ! empty( $this->options ) ) {
add_filter( "bulk_actions-{$current_screen->id}", array( $this, 'add_bulk_action' ) );
add_filter( "handle_bulk_actions-{$current_screen->id}", array( $this, 'handle_bulk_action' ), 10, 2 );
add_action( 'admin_footer', array( $this, 'display_form' ) );
add_action( 'admin_notices', array( $this, 'display_notices' ) );
// Special case where the wp_redirect() happens before the bulk action is triggered.
if ( 'edit' === $current_screen->base ) {
add_filter( 'wp_redirect', array( $this, 'parse_request_before_redirect' ) );
}
}
}
/**
* Retrieves the needed data in the request body and sanitize it.
*
* @since 2.7
*
* @param array $request {
* Parameters from the HTTP Request.
*
* @type int[] $post The list of post ids to bulk translate. Must be set if `$post` is not.
* @type int[] $media The list of media ids to bulk translate. Must be set if `$media` is not.
* @type string $translate The translation action ('pll_copy_post' for copy, 'pll_sync_post' for synchronization).
* @type string[] $pll-translate-lang The list of language slugs to translate to.
* }
* @return WP_Error|array {
* @type int[] $item_ids The sanitized list of post (or media) ids to translate.
* @type string $translate The sanitized translation action.
* @type string[] $pll-translate-lang The sanitized list of language slugs to translate to.
* }
*/
protected function parse_request( $request ) {
$args = array();
$screens_content_keys = array(
'upload' => 'media',
'edit' => 'post',
);
if ( ! empty( $this->current_screen ) && isset( $screens_content_keys[ $this->current_screen->base ] ) ) {
$item_key = $screens_content_keys[ $this->current_screen->base ];
if ( isset( $request[ $item_key ] ) && is_array( $request[ $item_key ] ) ) {
$args['item_ids'] = array_filter( array_map( 'absint', $request[ $item_key ] ) );
}
}
if ( empty( $args['item_ids'] ) ) {
return new WP_Error( 'pll_no_items_selected', __( 'No item has been selected. Please make sure to select at least one item to be translated.', 'polylang-pro' ) );
}
$args['translate'] = sanitize_key( $request['translate'] );
if ( isset( $request['pll-translate-lang'] ) && is_array( $request['pll-translate-lang'] ) ) {
$args['pll-translate-lang'] = array_intersect( $request['pll-translate-lang'], $this->model->get_languages_list( array( 'fields' => 'slug' ) ) );
}
if ( empty( $args['pll-translate-lang'] ) ) {
return new WP_Error( 'pll_no_target_language', __( 'Error: No target language has been selected. Please make sure to select at least one target language.', 'polylang-pro' ) );
}
return $args;
}
/**
* Handle the Translate bulk action.
*
* @since 2.4
* @since 2.7 Use a transient to store notices.
*
* @param string $sendback The URL to redirect to, with parameters.
* @param string $action Name of the requested bulk action.
*
* @return string The URL to redirect to.
*/
public function handle_bulk_action( $sendback, $action ) {
if ( 'pll_translate' !== $action ) {
return $sendback;
}
check_admin_referer( 'pll_translate', '_pll_translate_nonce' );
$query_args = $this->parse_request( $_GET ); // Errors returned by this method are already handled by `parse_request_before_redirect()`.
if ( ! is_wp_error( $query_args ) ) {
$selected_option = $this->options[ $query_args['translate'] ];
$error = $selected_option->do_bulk_action( $query_args['item_ids'], $query_args['pll-translate-lang'] );
$this->add_settings_error( $error );
}
return $sendback;
}
/**
* Add a bulk action
*
* @since 2.4
*
* @param array $actions List of bulk actions.
*
* @return array
*/
public function add_bulk_action( $actions ) {
$actions['pll_translate'] = __( 'Translate', 'polylang-pro' );
return $actions;
}
/**
* Displays the Bulk translate form.
*
* @since 2.4
*
* @return void
*/
public function display_form() {
global $post_type;
$bulk_translate_options = $this->options;
usort(
$bulk_translate_options,
function ( $first_element, $second_element ) {
return $first_element->get_priority() - $second_element->get_priority();
}
);
include __DIR__ . '/view-bulk-translate.php';
}
/**
* Displays the notices if some have been registered.
*
* Because WordPress triggers a {@see wp_redirect()}, these notices are stored in a transient.
*
* @since 2.7
*
* @return void
*/
public function display_notices() {
$transient_name = 'pll_bulk_translate_' . get_current_user_id();
/** @var string[][] */
$notices = get_transient( $transient_name );
if ( empty( $notices ) ) {
return;
}
foreach ( $notices as $notice ) {
/*
* Backward compatibility for PHP < 8.0.0.
* Unpacking operator `...` supports string-keyed associative array only since PHP 8.0.0.
*/
add_settings_error( ...array_values( $notice ) );
}
settings_errors( 'polylang' );
delete_transient( $transient_name );
}
/**
* Fixes the case when no post is selected and a redirect is fired before we can handle the bulk action.
*
* @since 2.7
*
* @param string $sendback The destination URL.
*
* @return string Unmodified $sendback.
*/
public function parse_request_before_redirect( $sendback ) {
if ( ! isset( $_GET['action'], $_REQUEST['_pll_translate_nonce'] ) || 'pll_translate' !== $_GET['action'] || ! wp_verify_nonce( $_REQUEST['_pll_translate_nonce'], 'pll_translate' ) ) {
return $sendback;
}
$error = $this->parse_request( $_GET );
if ( is_wp_error( $error ) ) {
$this->add_settings_error( $error );
}
return $sendback;
}
/**
* Registers errors if any.
*
* @since 3.6
*
* @param WP_Error $error Error object.
* @return void
*/
private function add_settings_error( WP_Error $error ) {
if ( ! $error->has_errors() ) {
return;
}
pll_add_notice( $error );
set_transient( 'pll_bulk_translate_' . get_current_user_id(), get_settings_errors( 'polylang' ) );
}
}

View File

@@ -0,0 +1,57 @@
.bulk-translate-save .button {
margin-right: 20px;
}
#wpbody-content #pll-translate .pll-bulk-translate-fields-wrapper {
display: flex;
}
#wpbody-content #pll-translate fieldset {
flex-basis: fit-content;
margin-right: 10%;
}
#pll-translate .pll-translation-flag {
margin: auto 10px auto 7px;
}
.rtl #pll-translate .pll-translation-flag {
margin: auto 7px auto 10px;
}
#pll-translate .title {
display: block;
line-height: 2.5;
}
#pll-translate label {
display: flex;
align-items: center;
}
#pll-translate label span.description{
line-height: normal;
}
#pll-translate label[for="pll-select-format"] {
padding-left: 18px;
}
#pll-translate #pll-select-format {
margin-left: 0.5rem;
}
@supports(selector(:has(*))) {
#pll-translate label[for="pll-select-format"] {
display: none;
}
#pll-translate label:has([name="translate"][value="pll_export_post"]:checked) ~ [for="pll-select-format"] {
display: flex;
}
}
@media screen and ( max-width: 782px ) {
#wpbody-content #pll-translate .pll-bulk-translate-fields-wrapper {
flex-direction: column;
}
}

View File

@@ -0,0 +1,86 @@
/**
* Bulk translate
*
* @package Polylang-Pro
*/
jQuery(
function ( $ ) {
var t = this;
$( '.editinline' ).on(
'click',
function () {
$( '#pll-translate' ).find( '.cancel' ).trigger( 'click' ); // Close the form on quick edit
}
);
$( '#doaction, #doaction2' ).on(
'click',
function ( e ) {
t.whichBulkButtonId = $( this ).attr( 'id' );
var n = t.whichBulkButtonId.substr( 2 );
if ( 'pll_translate' === $( 'select[name="' + n + '"]' ).val() ) {
e.preventDefault();
if ( typeof inlineEditPost !== 'undefined' ) { // Not available for media.
inlineEditPost.revert(); // Close Bulk edit and Quick edit if open.
}
$( '#pll-translate td' ).attr( 'colspan', $( 'th:visible, td:visible', '.widefat:first thead' ).length );
// The hidden tr allows to keep the background color.
// HTML prepended is hardcoded. So prepend is safe and as no need to be escaped.
$( 'table.widefat tbody' ).prepend( $( '#pll-translate' ) ).prepend( '<tr class="hidden"></tr>' ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend
} else {
$( '#pll-translate' ).find( '.cancel' ).trigger( 'click' );
}
}
);
// Cancel
$( '#pll-translate' ).on(
'click',
'.cancel',
function () {
// Close the form on any other bulk action
$( '#pll-translate' ).siblings( '.hidden' ).remove();
// #pll-translate is built and come from server side and is well escaped when necessary
$( '#pll-bulk-translate' ).append( $( '#pll-translate' ) ); //phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
// Move focus back to the Bulk Action button that was activated.
$( '#' + t.whichBulkButtonId ).trigger( 'focus' );
}
);
// Act when pressing enter or esc
$( '#pll-translate' ).on(
'keydown',
function ( event ) {
if ( 'Enter' === event.key && ! $( event.target ).hasClass( 'cancel' ) ) {
event.preventDefault();
$( this ).find( 'input[type=submit]' ).trigger( 'click' );
}
if ( 'Escape' === event.key ) {
event.preventDefault();
$( this ).find( '.cancel' ).trigger( 'click' );
}
}
);
// Clean DOM in case of file download
$( '#posts-filter' ).on(
'submit',
function () {
$( '.settings-error' ).remove();
setTimeout(
function () {
$( 'input[type=checkbox]:checked' ).attr( 'checked', false );
$( '#pll-translate' ).find( '.cancel' ).trigger( 'click' );
},
500
);
}
);
}
);

View File

@@ -0,0 +1,13 @@
<?php
/**
* @package Polylang-Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly.
}
if ( $polylang instanceof PLL_Admin && $polylang->model->has_languages() ) {
$polylang->bulk_translate = new PLL_Bulk_Translate( $polylang->model );
add_action( 'current_screen', array( $polylang->bulk_translate, 'init' ) );
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* Outputs the bulk translate option part in the bulk translate form.
*
* @package Polylang-Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly
}
$option_name = $this->get_name();
?>
<label>
<span class="option"><input name="translate" type="radio" value="<?php echo esc_attr( $option_name ); ?>"<?php checked( $option_name, $selected ); ?>/></span>
<span class="description"><?php echo esc_html( $bulk_translate_option->get_description() ); ?></span>
</label>

View File

@@ -0,0 +1,56 @@
<?php
/**
* Outputs the bulk translate form
*
* @package Polylang-Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly
}
?>
<form method="get"><table style="display: none"><tbody id="pll-bulk-translate">
<tr id="pll-translate" class="inline-edit-row">
<td>
<div class="inline-edit-wrapper">
<span class="inline-edit-legend"><?php esc_html_e( 'Bulk translate', 'polylang-pro' ); ?></span>
<div class="pll-bulk-translate-fields-wrapper">
<fieldset>
<div class="inline-edit-col">
<span class="title"><?php esc_html_e( 'Target languages', 'polylang-pro' ); ?></span>
<?php
foreach ( $this->model->get_languages_list() as $language ) {
printf(
'<label><span class="option"><input name="pll-translate-lang[]" type="checkbox" value="%1$s" /></span><span class="pll-translation-flag">%3$s</span>%2$s</label>',
esc_attr( $language->slug ),
esc_html( $language->name ),
$language->flag // phpcs:ignore WordPress.Security.EscapeOutput
);
}
?>
</div>
</fieldset>
<fieldset>
<div class="inline-edit-col">
<span class="title"><?php esc_html_e( 'Action', 'polylang-pro' ); ?></span>
<?php
if ( isset( $bulk_translate_options ) ) {
$selected = reset( $bulk_translate_options );
foreach ( $bulk_translate_options as $bulk_translate_option ) {
$bulk_translate_option->display( $selected->get_name() );
}
}
?>
</div>
</fieldset>
</div>
<p class="submit bulk-translate-save">
<?php wp_nonce_field( 'pll_translate', '_pll_translate_nonce' ); ?>
<button type="button" class="button button-secondary cancel"><?php esc_html_e( 'Cancel', 'polylang-pro' ); ?></button>
<?php submit_button( __( 'Submit', 'polylang-pro' ), 'primary', '', false ); ?>
</p>
</div>
</td>
</tr>
</tbody></table></form>