feat: initial ACRIB WordPress deployment

- WordPress 6.9.4 (es_ES) with Kadence theme
- Homepage: Hero, La Asociación, Pilares, Beneficios, Eventos, Miembros, Hazte Miembro, Contacto
- Brand identity: #13294b navy, #a12932 burgundy, #c69c48 gold
- Fonts: Raleway (headings) + Source Sans 3 (body) + Lato (UI)
- Plugins: Kadence Blocks, Polylang, Contact Form 7
- Custom CSS with full brand styling and responsive layout
- HTTPS enforced via wp-config.php proxy detection
This commit is contained in:
Malin
2026-05-19 19:25:59 +02:00
commit f3ff7b7186
6119 changed files with 1984255 additions and 0 deletions

View File

@@ -0,0 +1,241 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Blocks\Language_Switcher;
use PLL_Language;
use PLL_Switcher;
use WP_Block_Type_Registry;
/**
* Abstract class for language switcher block.
*
* @since 3.2
* @since 3.8 Moved to Polylang Core and renamed to Language_Switcher\Abstract_Block.
*/
abstract class Abstract_Block {
/**
* @var \PLL_Links
*/
protected $links;
/**
* @var \PLL_Model
*/
protected $model;
/**
* Current lang to render the language switcher block in an admin context.
*
* @since 2.8
*
* @var string|null
*/
protected $admin_current_lang;
/**
* Is it the edit context?
*
* @var bool
*/
protected $is_edit_context = false;
/**
* Current language.
*
* @var PLL_Language|false|null
*/
private $current_language;
/**
* Constructor
*
* @since 2.8
*
* @param \PLL_Base $polylang Polylang object.
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
$this->links = &$polylang->links;
$this->current_language = &$polylang->curlang;
}
/**
* Adds the required hooks.
*
* @since 3.2
*
* @return self
*/
public function init() {
// Use rest_pre_dispatch_filter to get additional parameters for language switcher block.
add_filter( 'rest_pre_dispatch', array( $this, 'get_rest_query_params' ), 10, 3 );
// Register language switcher block.
add_action( 'init', array( $this, 'register' ) );
return $this;
}
/**
* Returns the block name with the Polylang's namespace.
*
* @since 3.2
*
* @return string The block name.
*/
abstract protected function get_block_name();
/**
* Renders the Polylang's block on server.
*
* @since 3.2
* @since 3.3 Accepts two new parameters, $content and $block.
*
* @param array $attributes The block attributes.
* @param string $content The saved content.
* @param \WP_Block $block The parsed block.
* @return string The HTML string output to serve.
*/
abstract public function render( $attributes, $content, $block );
/**
* Returns the path to the block JSON file directory.
* The directory name being used to register a block.
*
* @since 3.8
*
* @return string The path to the block.
*/
abstract protected function get_path(): string;
/**
* Registers the Polylang's block.
*
* @since 2.8
* @since 3.2 Renamed and now handle any type of block registration based on a dynamic name.
*
* @return void
*/
public function register() {
if ( WP_Block_Type_Registry::get_instance()->is_registered( $this->get_block_name() ) ) {
// Don't register a block more than once or WordPress send an error. See https://github.com/WordPress/wordpress-develop/blob/5.9/src/wp-includes/class-wp-block-type-registry.php#L82-L90
return;
}
if ( ! register_block_type(
$this->get_path(),
array(
'render_callback' => array( $this, 'render' ),
)
) ) {
return;
}
$script_handle = 'pll_blocks';
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
$script_filename = 'js/build/blocks' . $suffix . '.js';
wp_register_script(
$script_handle,
plugins_url( $script_filename, POLYLANG_ROOT_FILE ),
array(
'wp-block-editor',
'wp-blocks',
'wp-components',
'wp-element',
'wp-i18n',
'wp-server-side-render',
'lodash',
'wp-editor',
),
POLYLANG_VERSION,
true
);
wp_localize_script( $script_handle, 'pll_block_editor_blocks_settings', PLL_Switcher::get_switcher_options( 'block', 'string' ) );
// Translated strings used in JS code
wp_set_script_translations( $script_handle, 'polylang' );
// No need to render the JS variable if Polylang Pro is installed, the editor will use `lang` REST field instead.
if ( class_exists( 'WP_Syntex\Polylang_Pro\REST\Translated\Post' ) ) {
return;
}
// Fallback to default language if current language is not set, usually happens in Site Editor.
$current_language = $this->current_language;
if ( ! $current_language ) {
$current_language = $this->model->get_default_language();
}
if ( ! $current_language ) {
// Should not happen since the module is loaded only if there are languages.
return;
}
if ( str_contains( wp_scripts()->get_inline_script_data( $script_handle, 'after' ), 'pllEditorCurrentLanguageSlug' ) ) {
return;
}
wp_add_inline_script(
$script_handle,
'let pllEditorCurrentLanguageSlug = ' . wp_json_encode( $current_language->slug ) . ';',
'after'
);
}
/**
* Returns the REST parameters for language switcher block.
* Used to store the request's language and context locally.
* Previously was in the `PLL_Block_Editor_Switcher_Block` class.
*
* @see WP_REST_Server::dispatch()
*
* @since 2.8
*
* @param mixed $result Response to replace the requested version with. Can be anything
* a normal endpoint can return, or null to not hijack the request.
* @param \WP_REST_Server $server Server instance.
* @param \WP_REST_Request $request Request used to generate the response.
* @return mixed
* @template T of \WP_REST_Request
* @phpstan-param T $request
*/
public function get_rest_query_params( $result, $server, $request ) {
if ( pll_is_edit_rest_request( $request ) ) {
$this->is_edit_context = true;
$lang = $request->get_param( 'lang' );
if ( is_string( $lang ) && ! empty( $lang ) ) {
$this->admin_current_lang = $lang;
}
}
return $result;
}
/**
* Adds the attributes to render the block correctly.
* Also specifies not to echo the switcher in any case.
*
* @since 3.2
*
* @param array $attributes The attributes of the currently rendered block.
* @return array The modified attributes.
*/
protected function set_attributes_for_block( $attributes ) {
$attributes['echo'] = 0;
if ( $this->is_edit_context ) {
$attributes['admin_render'] = 1;
$attributes['admin_current_lang'] = $this->admin_current_lang;
$attributes['hide_if_empty'] = 0;
$attributes['hide_if_no_translation'] = 0; // Force not to hide the language for the block preview even if the option is checked.
}
return $attributes;
}
}

View File

@@ -0,0 +1,304 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Blocks\Language_Switcher\Navigation;
use WP_Block;
use PLL_Switcher;
use WP_HTML_Tag_Processor;
use WP_Syntex\Polylang\Blocks\Language_Switcher\Abstract_Block;
/**
* Language switcher block for navigation.
*
* @since 3.2
* @since 3.8 Moved to Polylang Core and renamed to Language_Switcher\Navigation\Block.
*/
class Block extends Abstract_Block {
/**
* Placeholder used to add language name or flag after WordPress renders the link labels.
*
* @var string
*/
const PLACEHOLDER = '%pll%';
/**
* Adds the required hooks specific to the navigation language switcher.
*
* @since 3.2
*
* @return self
*/
public function init() {
parent::init();
add_action( 'rest_api_init', array( $this, 'register_switcher_menu_item_options_meta_rest_field' ) );
add_filter( 'block_type_metadata', array( $this, 'register_custom_attributes' ) );
add_filter( 'render_block_core/navigation-link', array( $this, 'render_custom_attributes' ), 10, 3 );
add_filter( 'render_block_core/navigation-submenu', array( $this, 'render_custom_attributes' ), 10, 3 );
add_action( 'init', array( $this, 'register_editor_style' ) );
return $this;
}
/**
* Registers the editor style for the navigation language switcher block.
*
* @since 3.8
*
* @return void
*/
public function register_editor_style(): void {
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_register_style(
'pll-navigation-language-switcher-editor-style',
plugins_url( 'css/build/navigation-language-switcher-editor-style' . $suffix . '.css', POLYLANG_ROOT_FILE ),
array(),
POLYLANG_VERSION
);
}
/**
* Returns the navigation language switcher block name with the Polylang's namespace.
*
* @since 3.2
*
* @return string The block name.
*/
protected function get_block_name() {
return 'polylang/navigation-language-switcher';
}
/**
* Renders the `polylang/navigation-language-switcher` block on server.
*
* @since 3.1
* @since 3.3 Accepts two new parameters, $content and $block.
*
* @param array $attributes The block attributes.
* @param string $content The saved content. Unused.
* @param WP_Block $block The parsed block.
* @return string The HTML string output to serve.
*/
public function render( $attributes, $content, $block ) {
$attributes = $this->set_attributes_for_block( $attributes );
$switcher = new PLL_Switcher();
$switcher_elements = (array) $switcher->the_languages( $this->links, array_merge( $attributes, array( 'raw' => true ) ) );
if ( empty( $switcher_elements ) ) {
return '';
}
if ( $attributes['dropdown'] ) {
$inner_nav_link_blocks = array();
$top_level_lang = reset( $switcher_elements );
foreach ( $switcher_elements as $switcher_element ) {
$nav_link_block_args = array(
'blockName' => 'core/navigation-link',
'attrs' => $this->get_core_block_attributes( $attributes, $switcher_element ),
);
$inner_nav_link_blocks[] = new WP_Block( $nav_link_block_args, $block->context );
if ( $switcher_element['current_lang'] && ! $attributes['hide_current'] ) {
$top_level_lang = $switcher_element;
}
}
$attributes = $this->get_core_block_attributes( $attributes, $top_level_lang );
$attributes['className'] .= ' ' . wp_apply_generated_classname_support( $block->block_type )['class'];
$submenu_block_args = array(
'blockName' => 'core/navigation-submenu',
'attrs' => $attributes,
'innerBlocks' => $inner_nav_link_blocks,
);
$submenu_block = new WP_Block( $submenu_block_args, $block->context );
$output = $submenu_block->render();
} else {
$output = '';
foreach ( $switcher_elements as $switcher_element ) {
$link_attributes = $this->get_core_block_attributes( $attributes, $switcher_element );
$link_attributes['className'] .= ' ' . wp_apply_generated_classname_support( $block->block_type )['class'];
$nav_link_block_args = array(
'blockName' => 'core/navigation-link',
'attrs' => $link_attributes,
);
$link_block = new WP_Block( $nav_link_block_args, $block->context );
$output .= $link_block->render();
}
}
return $output;
}
/**
* Register switcher menu item meta options as a REST API field.
*
* @since 3.2
*
* @return void
*/
public function register_switcher_menu_item_options_meta_rest_field() {
register_post_meta(
'nav_menu_item',
'_pll_menu_item',
array(
'object_subtype' => 'nav_menu_item',
'description' => __( 'Language switcher settings', 'polylang' ),
'single' => true,
'show_in_rest' => array(
'schema' => array(
'type' => 'object',
'additionalProperties' => array(
'type' => 'boolean',
),
),
),
)
);
}
/**
* Filters core/navigation-link and core/navigation-submenu attributes during registration to add our own.
*
* @since 3.6
*
* @param array $metadata Metadata for registering a block type.
*
* @return array The filtered metadata if about a core/navigation-link.
*/
public function register_custom_attributes( $metadata ) {
if ( 'core/navigation-link' === $metadata['name'] || 'core/navigation-submenu' === $metadata['name'] ) {
$pll_attributes = array(
'hreflang' => array(
'type' => 'string',
),
'lang' => array(
'type' => 'string',
),
'pll_show_flags' => array(
'type' => 'boolean',
),
'pll_show_names' => array(
'type' => 'boolean',
),
'pll_flag' => array(
'type' => 'string',
),
'pll_name' => array(
'type' => 'string',
),
);
$metadata['attributes'] = array_merge( $metadata['attributes'], $pll_attributes );
}
return $metadata;
}
/**
* Renders a core/naviagation-link or core/naviagation-submenu block by adding hreflang and lang attributes to the <a> tag
* and also the language flag if required.
*
* @since 3.6
*
* @param string $block_content The block content.
* @param array $block The full block, including name and attributes.
* @param WP_Block $instance The block instance.
*
* @return string A formatted HTML string representing the core/navigation-link or core/navigation-submenu block.
*/
public function render_custom_attributes( $block_content, $block, $instance ) {
if ( ! isset(
$instance->attributes['pll_show_flags'],
$instance->attributes['pll_show_names'],
$instance->attributes['pll_flag'],
$instance->attributes['pll_name'],
$instance->attributes['lang'],
$instance->attributes['hreflang']
)
) {
return $block_content;
}
$content_tags = new WP_HTML_Tag_Processor( $block_content );
if ( 'core/navigation-submenu' === $instance->name ) {
// If `openSubmenusOnClick`, the submenu is rendered as a button, so there are no `<a>` to process.
if ( empty( $instance->context['openSubmenusOnClick'] ) && $content_tags->next_tag( array( 'tag_name' => 'a' ) ) ) {
$content_tags->set_attribute( 'hreflang', $instance->attributes['hreflang'] );
$content_tags->set_attribute( 'lang', $instance->attributes['lang'] );
}
if ( $content_tags->next_tag( array( 'tag_name' => 'button' ) ) ) {
$content_tags->set_attribute(
'aria-label',
str_replace(
static::PLACEHOLDER,
__( 'Languages', 'polylang' ),
(string) $content_tags->get_attribute( 'aria-label' )
)
);
}
} elseif ( $content_tags->next_tag( array( 'tag_name' => 'a' ) ) ) {
$content_tags->set_attribute( 'hreflang', $instance->attributes['hreflang'] );
$content_tags->set_attribute( 'lang', $instance->attributes['lang'] );
}
$overridden_block_content = $content_tags->get_updated_html();
$link_label = '';
if ( $instance->attributes['pll_show_flags'] ) {
$link_label .= $instance->attributes['pll_flag'];
}
if ( $instance->attributes['pll_show_names'] ) {
$link_label .= $instance->attributes['pll_show_flags'] ? ' ' . $instance->attributes['pll_name'] : $instance->attributes['pll_name'];
}
return str_replace(
static::PLACEHOLDER,
$link_label,
$overridden_block_content
);
}
/**
* Returns the path to the block JSON file directory.
* The directory name being used to register a block.
*
* @since 3.8
*
* @return string The path to the block.
*/
protected function get_path(): string {
return __DIR__;
}
/**
* Returns attributes that fit for core/navigation-link or core/navigation-submenu and specific to polylang/navigation-language-switcher.
*
* @since 3.6
*
* @param array $attributes Array of polylang/navigation-language-switcher attributes.
* @param array $switcher_item Array of a switcher item data.
* @return array Attributes to be rendered by core.
*/
private function get_core_block_attributes( $attributes, $switcher_item ) {
return array(
'label' => static::PLACEHOLDER,
'url' => $switcher_item['url'],
'pll_show_flags' => $attributes['show_flags'],
'pll_show_names' => $attributes['show_names'],
'lang' => $switcher_item['locale'],
'hreflang' => $switcher_item['locale'],
'pll_flag' => $switcher_item['flag'],
'pll_name' => $switcher_item['name'],
'className' => trim( implode( ' ', (array) $switcher_item['classes'] ) ),
);
}
}

View File

@@ -0,0 +1,65 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "polylang/navigation-language-switcher",
"version": "3.8",
"title": "Navigation Language Switcher",
"category": "design",
"parent": [ "core/navigation" ],
"description": "Language switcher suitable for navigation.",
"example": {},
"attributes": {
"dropdown": {
"type": "boolean",
"default": 0
},
"show_names": {
"type": "boolean",
"default": 1
},
"show_flags": {
"type": "boolean",
"default": 0
},
"force_home": {
"type": "boolean",
"default": 0
},
"hide_current": {
"type": "boolean",
"default": 0
},
"hide_if_no_translation": {
"type": "boolean",
"default": 0
},
"className": {
"type": "string",
"default": ""
}
},
"usesContext": [
"textColor",
"customTextColor",
"backgroundColor",
"customBackgroundColor",
"overlayTextColor",
"customOverlayTextColor",
"overlayBackgroundColor",
"customOverlayBackgroundColor",
"fontSize",
"customFontSize",
"showSubmenuIcon",
"maxNestingLevel",
"openSubmenusOnClick",
"style",
"isResponsive",
"submenuVisibility"
],
"supports": {
"contentRole": true
},
"textdomain": "polylang",
"editorScript": "pll_blocks",
"editorStyle": "pll-navigation-language-switcher-editor-style"
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Blocks\Language_Switcher\Standard;
use PLL_Switcher;
use WP_Syntex\Polylang\Blocks\Language_Switcher\Abstract_Block;
/**
* Language switcher block.
*
* @since 2.8
* @since 3.2 Extends now the PLL_Abstract_Language_Switcher_Block abstract class.
* @since 3.8 Moved to Polylang Core and renamed to Language_Switcher\Standard\Block.
*/
class Block extends Abstract_Block {
/**
* Returns the language switcher block name with the Polylang's namespace.
*
* @since 3.2
*
* @return string The block name.
*/
protected function get_block_name() {
return 'polylang/language-switcher';
}
/**
* Renders the `polylang/language-switcher` block on server.
*
* @since 2.8
* @since 3.2 Renamed according to its parent abstract class.
* @since 3.3 Accepts two new parameters, $content and $block.
*
* @param array $attributes The block attributes.
* @param string $content The saved content. Unused.
* @param \WP_Block $block The parsed block. Unused.
* @return string Returns the language switcher.
*/
public function render( $attributes, $content, $block ) { //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
static $dropdown_id = 0;
++$dropdown_id;
// Sets a unique id for dropdown in PLL_Switcher::the_language().
$attributes['dropdown'] = empty( $attributes['dropdown'] ) ? 0 : $dropdown_id;
$attributes = $this->set_attributes_for_block( $attributes );
$attributes['raw'] = false;
$switcher = new PLL_Switcher();
$switcher_output = $switcher->the_languages( $this->links, $attributes );
if ( empty( $switcher_output ) ) {
return '';
}
$aria_label = __( 'Choose a language', 'polylang' );
if ( $attributes['dropdown'] ) {
$switcher_output = '<label class="screen-reader-text" for="' . esc_attr( 'lang_choice_' . $attributes['dropdown'] ) . '">' . esc_html( $aria_label ) . '</label>' . $switcher_output;
$wrap_tag = '<div %1$s>%2$s</div>';
} else {
$wrap_tag = '<nav role="navigation" aria-label="' . esc_attr( $aria_label ) . '"><ul %1$s>%2$s</ul></nav>';
}
$wrap_attributes = get_block_wrapper_attributes();
return sprintf( $wrap_tag, $wrap_attributes, $switcher_output );
}
/**
* Returns the path to the block JSON file directory.
* The directory name being used to register a block.
*
* @since 3.8
*
* @return string The path to the block.
*/
protected function get_path(): string {
return __DIR__;
}
}

View File

@@ -0,0 +1,42 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "polylang/language-switcher",
"version": "3.8",
"title": "Language Switcher",
"category": "widgets",
"description": "Language switcher to insert in content or as a widget.",
"example": {},
"attributes": {
"dropdown": {
"type": "boolean",
"default": 0
},
"show_names": {
"type": "boolean",
"default": 1
},
"show_flags": {
"type": "boolean",
"default": 0
},
"force_home": {
"type": "boolean",
"default": 0
},
"hide_current": {
"type": "boolean",
"default": 0
},
"hide_if_no_translation": {
"type": "boolean",
"default": 0
},
"className": {
"type": "string",
"default": ""
}
},
"textdomain": "polylang",
"editorScript": "pll_blocks"
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Blocks;
use WP_Syntex\Polylang\Blocks\Language_Switcher\Standard\Block as Standard_Block;
use WP_Syntex\Polylang\Blocks\Language_Switcher\Navigation\Block as Navigation_Block;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly.
}
add_action(
'pll_init',
function ( $polylang ) {
if ( $polylang->model->has_languages() ) {
$polylang->switcher_block = ( new Standard_Block( $polylang ) )->init();
$polylang->navigation_block = ( new Navigation_Block( $polylang ) )->init();
}
}
);

View File

@@ -0,0 +1,62 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\REST;
use PLL_Model;
defined( 'ABSPATH' ) || exit;
/**
* Sets all Polylang REST controllers up.
*
* @since 3.7
*/
class API {
/**
* REST languages.
*
* @var V1\Languages|null
*/
public $languages;
/**
* REST settings.
*
* @var V1\Settings|null
*/
public $settings;
/**
* @var PLL_Model
*/
private $model;
/**
* Constructor.
*
* @since 3.7
*
* @param PLL_Model $model Polylang's model.
*/
public function __construct( PLL_Model $model ) {
$this->model = $model;
}
/**
* Adds hooks and registers endpoints.
*
* @since 3.7
*
* @return void
*/
public function init(): void {
$this->languages = new V1\Languages( $this->model );
$this->languages->register_routes();
$this->settings = new V1\Settings( $this->model );
$this->settings->register_routes();
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\REST;
use WP_Error;
use WP_REST_Controller;
defined( 'ABSPATH' ) || exit;
/**
* Abstract REST controller.
*
* @since 3.7
*/
abstract class Abstract_Controller extends WP_REST_Controller {
/**
* Adds a status code to the given error and returns the error.
*
* @since 3.7
*
* @param WP_Error $error A `WP_Error` object.
* @param int $status_code Optional. A status code. Default is 400.
* @return WP_Error
*/
protected function add_status_to_error( WP_Error $error, int $status_code = 400 ): WP_Error {
$error->add_data( array( 'status' => $status_code ) );
return $error;
}
}

View File

@@ -0,0 +1,266 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\REST;
use Closure;
use PLL_Model;
use PLL_Language;
use WP_REST_Request;
use WP_REST_Posts_Controller;
use WP_REST_Terms_Controller;
/**
* Class that mediates the current request.
*
* @since 3.8
*/
class Request {
/**
* @var WP_REST_Request|null
*
* @phpstan-var WP_REST_Request<array>|null
*/
private $request;
/**
* @var array|null
*/
private $handler;
/**
* @var PLL_Model
*/
private $model;
/**
* Constructor.
*
* @since 3.8
*
* @param PLL_Model $model Instance of PLL_Model.
*/
public function __construct( PLL_Model $model ) {
$this->model = $model;
/*
* Priorities 0 and 10000 allow to have a stored request from the very beginning until the very end of the
* process. This allows to filter queries (see `Filtered_Object\Post::parse_query()` for example) that may be located
* in callbacks hooked to `rest_request_before_callbacks` and `rest_request_after_callbacks`.
*/
add_filter( 'rest_request_before_callbacks', Closure::fromCallable( array( $this, 'save_request' ) ), 0, 3 );
add_filter( 'rest_request_after_callbacks', Closure::fromCallable( array( $this, 'reset_request' ) ), 10000 );
}
/**
* Stores the request to use, for example, parameters when filtering queries.
*
* @since 3.2
* @since 3.8 Added the `$server` parameter.
* Hooked to `rest_pre_dispatch`.
* Moved from PLL_REST_Filtered_Object.
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
* @return WP_REST_Response|WP_HTTP_Response|WP_Error|mixed Response to send to the client.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
private function save_request( $response, $handler, $request ) {
$this->request = $request;
$this->handler = $handler;
return $response;
}
/**
* Resets the stored request.
* Prevents to keep it after the request ends.
*
* @since 3.8
*
* @param mixed $result Response.
* @return mixed
*/
private function reset_request( $result ) {
$this->request = null;
$this->handler = null;
return $result;
}
/**
* Returns a parameter from the current request if defined.
*
* @since 3.8
*
* @param string $param Parameter name.
* @return mixed|null Parameter value or null if not defined.
*/
public function get_param( string $param ) {
if ( ! $this->request ) {
return null;
}
return $this->request->get_param( $param );
}
/**
* Returns the parameters of the current request.
*
* @since 3.8
*
* @return array Parameters of the current request if any.
*/
public function get_params(): array {
if ( ! $this->request ) {
return array();
}
return $this->request->get_params();
}
/**
* Returns the language of the current request.
*
* @since 3.8
*
* @return PLL_Language|null Language of the current request, or null if no request is set or the language is not found.
*/
public function get_language(): ?PLL_Language {
if ( ! $this->request ) {
return null;
}
$lang = $this->get_param( 'lang' );
if ( empty( $lang ) || ! is_string( $lang ) ) {
return null;
}
$lang = $this->model->get_language( $lang );
if ( ! $lang ) {
return null;
}
return $lang;
}
/**
* Returns the ID of the current requested object if defined.
*
* @since 3.8
*
* @return int|null ID of the current requested object, or null if no request is set or the ID is not defined.
*/
public function get_id(): ?int {
if ( ! $this->request ) {
return null;
}
$id = $this->get_param( 'id' );
if ( empty( $id ) || ! is_numeric( $id ) ) {
return null;
}
return (int) $id;
}
/**
* Returns the attributes of the current request.
*
* @since 3.8
*
* @return array|null Attributes of the current request, or null if no request is set.
*/
public function get_attributes(): ?array {
if ( ! $this->request ) {
return null;
}
return $this->request->get_attributes();
}
/**
* Tells if the current request is a "read only" request, i.e. not `POST`, `PUT`, `PATCH`, `DELETE`.
*
* @since 3.8
*
* @return bool
*/
public function is_read_only(): bool {
return ! empty( $this->request ) && ! in_array( $this->request->get_method(), array( 'POST', 'PUT', 'PATCH', 'DELETE' ), true );
}
/**
* Returns the object type of the current request.
*
* @since 3.8
*
* @return string|null Object type of the current request, or null if not defined.
* Returned values are 'post' and 'term'.
*
* @phpstan-return 'post'|'term'|null
*/
public function get_object_type(): ?string {
if ( ! $this->request || ! $this->handler ) {
return null;
}
/**
* Filters the object type of the current request.
*
* @since 3.8
*
* @param string|null $type Object type of the current request, or null if not defined.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
* Accepted values are 'post' and 'term'.
*/
$type = apply_filters( 'pll_rest_request_object_type', null, $this->handler, $this->request );
if ( in_array( $type, array( 'post', 'term' ), true ) ) {
return $type;
}
if ( ! is_array( $this->handler['callback'] ) ) {
return null;
}
$controller = reset( $this->handler['callback'] );
if ( $controller instanceof WP_REST_Posts_Controller ) {
return 'post';
} elseif ( $controller instanceof WP_REST_Terms_Controller ) {
return 'term';
}
return null;
}
/**
* Returns the route of the current request.
*
* @since 3.8
*
* @return string Route of the current request, or empty string if no request is set.
*/
public function get_route(): string {
return $this->request && is_string( $this->request->get_route() ) ? $this->request->get_route() : '';
}
/**
* Returns the HTTP method of the current request.
*
* @since 3.8
*
* @return string
*/
public function get_method(): string {
return $this->request ? $this->request->get_method() : '';
}
}

View File

@@ -0,0 +1,706 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\REST\V1;
use PLL_Language;
use PLL_Model;
use PLL_Translatable_Objects;
use stdClass;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WP_Syntex\Polylang\Capabilities\Capabilities;
use WP_Syntex\Polylang\Model\Languages as Languages_Model;
use WP_Syntex\Polylang\REST\Abstract_Controller;
defined( 'ABSPATH' ) || exit;
/**
* Languages REST controller.
*
* @since 3.7
*/
class Languages extends Abstract_Controller {
/**
* @var Languages_Model
*/
private $languages;
/**
* @var PLL_Translatable_Objects
*/
private $translatable_objects;
/**
* Constructor.
*
* @since 3.7
*
* @param PLL_Model $model Polylang's model.
*/
public function __construct( PLL_Model $model ) {
$this->namespace = 'pll/v1';
$this->rest_base = 'languages';
$this->languages = $model->languages;
$this->translatable_objects = $model->translatable_objects;
}
/**
* Registers the routes for languages.
*
* @since 3.7
*
* @return void
*/
public function register_routes(): void {
register_rest_route(
$this->namespace,
"/{$this->rest_base}",
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
'allow_batch' => array( 'v1' => true ),
)
);
$readable = array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
);
register_rest_route(
$this->namespace,
"/{$this->rest_base}/(?P<term_id>[\d]+)",
array(
'args' => array(
'term_id' => array(
'description' => __( 'Unique identifier for the language.', 'polylang' ),
'type' => 'integer',
),
),
$readable,
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
'allow_batch' => array( 'v1' => true ),
)
);
register_rest_route(
$this->namespace,
sprintf( '/%1$s/(?P<slug>%2$s)', $this->rest_base, Languages_Model::INNER_SLUG_PATTERN ),
array(
'args' => array(
'slug' => array(
'description' => __( 'Language code - preferably 2-letters ISO 639-1 (for example: en).', 'polylang' ),
'type' => 'string',
),
),
$readable,
'schema' => array( $this, 'get_public_item_schema' ),
'allow_batch' => array( 'v1' => true ),
)
);
}
/**
* Retrieves all languages.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function get_items( $request ) {
$response = array();
foreach ( $this->languages->get_list() as $language ) {
$language = $this->prepare_item_for_response( $language, $request );
$response[] = $this->prepare_response_for_collection( $language );
}
return rest_ensure_response( $response );
}
/**
* Creates one language from the collection.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function create_item( $request ) {
if ( isset( $request['term_id'] ) ) {
return new WP_Error(
'rest_exists',
__( 'Cannot create existing language.', 'polylang' ),
array( 'status' => 400 )
);
}
/**
* @phpstan-var array{
* locale: non-empty-string,
* slug?: non-empty-string,
* name?: non-empty-string,
* is_rtl?: bool,
* term_group?: int,
* flag?: non-empty-string,
* no_default_cat?: bool
* } $args
*/
$args = $request->get_params();
$language = $this->languages->add( $args );
if ( is_wp_error( $language ) ) {
return $this->add_status_to_error( $language );
}
return $this->prepare_item_for_response( $language, $request );
}
/**
* Retrieves one language from the collection.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function get_item( $request ) {
$language = $this->get_language( $request );
if ( is_wp_error( $language ) ) {
return $language;
}
return $this->prepare_item_for_response( $language, $request );
}
/**
* Updates one language from the collection.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function update_item( $request ) {
$language = $this->get_language( $request );
if ( is_wp_error( $language ) ) {
return $language;
}
/**
* @phpstan-var array{
* term_id: int,
* locale?: non-empty-string,
* slug?: non-empty-string,
* name?: non-empty-string,
* is_rtl?: bool,
* term_group?: int,
* flag?: non-empty-string
* } $args
*/
$args = $request->get_params();
$args['lang_id'] = $language->term_id;
$language = $this->languages->update( $args );
if ( is_wp_error( $language ) ) {
return $this->add_status_to_error( $language );
}
return $this->prepare_item_for_response( $language, $request );
}
/**
* Deletes one language from the collection.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function delete_item( $request ) {
$language = $this->get_language( $request );
if ( is_wp_error( $language ) ) {
return $language;
}
$this->languages->delete( $language->term_id );
$previous = $this->prepare_item_for_response( $language, $request );
$response = new WP_REST_Response();
$response->set_data(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
return $response;
}
/**
* Checks if a given request has access to get the languages.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function get_items_permissions_check( $request ) {
if ( 'edit' === $request['context'] && ! $this->check_update_permission() ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit languages.', 'polylang' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Checks if a given request has access to create a language.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create languages, WP_Error object otherwise.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function create_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! $this->check_update_permission() ) {
return new WP_Error(
'rest_cannot_create',
__( 'Sorry, you are not allowed to create a language.', 'polylang' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Checks if a given request has access to get a specific language.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the language, WP_Error object otherwise.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function get_item_permissions_check( $request ) {
return $this->get_items_permissions_check( $request );
}
/**
* Checks if a given request has access to update a specific language.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the language, WP_Error object otherwise.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function update_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! $this->check_update_permission() ) {
return new WP_Error(
'rest_cannot_update',
__( 'Sorry, you are not allowed to edit this language.', 'polylang' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Checks if a given request has access to delete a specific language.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the language, WP_Error object otherwise.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function delete_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! $this->check_update_permission() ) {
return new WP_Error(
'rest_cannot_delete',
__( 'Sorry, you are not allowed to delete this language.', 'polylang' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Prepares the language for the REST response.
*
* @since 3.7
*
* @param PLL_Language $item Language object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function prepare_item_for_response( $item, $request ) {
$data = $item->to_array();
$fields = $this->get_fields_for_response( $request );
$response = array();
$data['is_rtl'] = (bool) $data['is_rtl'];
$data['host'] = (string) $data['host'];
foreach ( $data as $language_prop => $prop_value ) {
if ( rest_is_field_included( $language_prop, $fields ) ) {
$response[ $language_prop ] = $prop_value;
}
}
/** @var WP_REST_Response */
return rest_ensure_response( $response );
}
/**
* Retrieves the language's schema, conforming to JSON Schema.
*
* @since 3.7
*
* @return array Item schema data.
*/
public function get_item_schema(): array {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'language',
'type' => 'object',
'properties' => array(
'term_id' => array(
'description' => __( 'Unique identifier for the language.', 'polylang' ),
'type' => 'integer',
'minimum' => 1,
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'The name is how it is displayed on your site (for example: English).', 'polylang' ),
'type' => 'string',
'minLength' => 1,
'context' => array( 'view', 'edit' ),
),
'slug' => array(
'description' => __( 'Language code - preferably 2-letters ISO 639-1 (for example: en).', 'polylang' ),
'type' => 'string',
'pattern' => Languages_Model::SLUG_PATTERN,
'context' => array( 'view', 'edit' ),
),
'locale' => array(
'description' => __( 'WordPress Locale for the language (for example: en_US).', 'polylang' ),
'type' => 'string',
'pattern' => Languages_Model::LOCALE_PATTERN,
'context' => array( 'view', 'edit' ),
),
'w3c' => array(
'description' => __( 'W3C Locale for the language (for example: en-US).', 'polylang' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'facebook' => array(
'description' => __( 'Facebook Locale for the language (for example: en_US).', 'polylang' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'is_rtl' => array(
'description' => sprintf(
/* translators: %s is a value. */
__( 'Text direction. %s for right-to-left.', 'polylang' ),
'`true`'
),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
),
'term_group' => array(
'description' => __( 'Position of the language in the language switcher.', 'polylang' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'flag_code' => array(
'description' => __( 'Flag code corresponding to ISO 3166-1 (for example: us for the United States flag).', 'polylang' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'flag_url' => array(
'description' => __( 'Flag URL.', 'polylang' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'flag' => array(
'description' => __( 'HTML tag for the flag.', 'polylang' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'custom_flag_url' => array(
'description' => __( 'Custom flag URL.', 'polylang' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'custom_flag' => array(
'description' => __( 'HTML tag for the custom flag.', 'polylang' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'is_default' => array(
'description' => __( 'Tells whether the language is the default one.', 'polylang' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'active' => array(
'description' => __( 'Tells whether the language is active.', 'polylang' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'home_url' => array(
'description' => __( 'Home URL in this language.', 'polylang' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'search_url' => array(
'description' => __( 'Search URL in this language.', 'polylang' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'host' => array(
'description' => __( 'Host for this language.', 'polylang' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'page_on_front' => array(
'description' => __( 'Page on front ID in this language.', 'polylang' ),
'type' => 'integer',
'minimum' => 0,
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'page_for_posts' => array(
'description' => __( 'Identifier of the page for posts in this language.', 'polylang' ),
'type' => 'integer',
'minimum' => 0,
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'fallbacks' => array(
'description' => __( 'List of language locale fallbacks.', 'polylang' ),
'type' => 'array',
'uniqueItems' => true,
'items' => array(
'type' => 'string',
'pattern' => Languages_Model::LOCALE_PATTERN,
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'term_props' => array(
'description' => __( 'Language properties.', 'polylang' ),
'type' => 'object',
'properties' => array(),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'no_default_cat' => array(
'description' => __( 'Tells whether the default category must be created when creating a new language.', 'polylang' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'default' => false,
),
),
);
foreach ( $this->translatable_objects as $translatable_object ) {
$this->schema['properties']['term_props']['properties'][ $translatable_object->get_tax_language() ] = array(
'description' => $translatable_object->get_rest_description(),
'type' => 'object',
'properties' => array(
'term_id' => array(
/* translators: %s is the name of the term property (`term_id` or `term_taxonomy_id`). */
'description' => sprintf( __( 'The %s of the language term for this translatable entity.', 'polylang' ), '`term_id`' ),
'type' => 'integer',
'minimum' => 1,
),
'term_taxonomy_id' => array(
/* translators: %s is the name of the term property (`term_id` or `term_taxonomy_id`). */
'description' => sprintf( __( 'The %s of the language term for this translatable entity.', 'polylang' ), '`term_taxonomy_id`' ),
'type' => 'integer',
'minimum' => 1,
),
'count' => array(
'description' => __( 'Number of items of this type of content in this language.', 'polylang' ),
'type' => 'integer',
'minimum' => 0,
),
),
);
}
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves an array of endpoint arguments from the item schema for the controller.
* Ensures that the `no_default_cat` property is returned only for `CREATABLE` requests.
*
* @since 3.7
*
* @param string $method Optional. HTTP method of the request. Default WP_REST_Server::CREATABLE.
* @return array Endpoint arguments.
*/
public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
$schema = $this->get_item_schema();
if ( WP_REST_Server::CREATABLE !== $method ) {
unset( $schema['properties']['no_default_cat'] );
} else {
$schema['properties']['locale']['required'] = true;
}
return rest_get_endpoint_args_for_schema( $schema, $method );
}
/**
* Tells if languages can be edited.
*
* @since 3.7
*
* @return bool
*/
protected function check_update_permission(): bool {
return current_user_can( Capabilities::LANGUAGES );
}
/**
* Returns the language, if the ID is valid.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return PLL_Language|WP_Error Language object if the ID or slug is valid, WP_Error otherwise.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
private function get_language( WP_REST_Request $request ) {
if ( isset( $request['term_id'] ) ) {
$error = new WP_Error(
'rest_invalid_id',
__( 'Invalid language ID', 'polylang' ),
array( 'status' => 404 )
);
if ( $request['term_id'] <= 0 ) {
return $error;
}
$language = $this->languages->get( (int) $request['term_id'] );
if ( ! $language instanceof PLL_Language ) {
return $error;
}
return $language;
}
if ( isset( $request['slug'] ) ) {
$language = $this->languages->get( (string) $request['slug'] );
if ( ! $language instanceof PLL_Language ) {
return new WP_Error(
'rest_invalid_slug',
__( 'Invalid language slug', 'polylang' ),
array( 'status' => 404 )
);
}
return $language;
}
// Should not happen.
return new WP_Error(
'rest_invalid_identifier',
__( 'Invalid language identifier', 'polylang' ),
array( 'status' => 404 )
);
}
}

View File

@@ -0,0 +1,204 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\REST\V1;
use PLL_Model;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WP_Syntex\Polylang\Model\Languages;
use WP_Syntex\Polylang\REST\Abstract_Controller;
defined( 'ABSPATH' ) || exit;
/**
* Settings REST controller.
*
* @since 3.7
*/
class Settings extends Abstract_Controller {
/**
* @var \WP_Syntex\Polylang\Options\Options
*/
private $options;
/**
* @var Languages
*/
private $languages;
/**
* Constructor.
*
* @since 3.7
*
* @param PLL_Model $model Polylang's model.
*/
public function __construct( PLL_Model $model ) {
$this->namespace = 'pll/v1';
$this->rest_base = 'settings';
$this->options = $model->options;
$this->languages = $model->languages;
}
/**
* Registers the routes for options.
*
* @since 3.7
*
* @return void
*/
public function register_routes(): void {
register_rest_route(
$this->namespace,
"/{$this->rest_base}",
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
'allow_batch' => array( 'v1' => true ),
)
);
}
/**
* Retrieves all options.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function get_item( $request ) {
return $this->prepare_item_for_response( $this->options->get_all(), $request );
}
/**
* Updates option(s).
* This allows to update one or several options.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function update_item( $request ) {
$errors = new WP_Error();
$schema = $this->options->get_schema();
$options = array_intersect_key(
$request->get_params(),
rest_get_endpoint_args_for_schema( $schema, WP_REST_Server::EDITABLE ) // Remove fields with `readonly`.
);
foreach ( $options as $option_name => $new_value ) {
$previous_value = $this->options->get( $option_name );
if ( 'default_lang' === $option_name ) {
$result = $this->languages->update_default( $new_value );
} else {
$result = $this->options->set( $option_name, $new_value );
}
if ( $result->has_errors() ) {
$errors->merge_from( $result );
continue;
}
if ( $this->options->get( $option_name ) === $previous_value ) {
continue;
}
switch ( $option_name ) {
case 'rewrite':
case 'force_lang':
case 'hide_default':
flush_rewrite_rules();
}
}
if ( $errors->has_errors() ) {
return $this->add_status_to_error( $errors );
}
return $this->prepare_item_for_response( $this->options->get_all(), $request );
}
/**
* Checks if a given request has access to update the options.
*
* @since 3.7
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the option, WP_Error object otherwise.
*
* @phpstan-template T of array
* @phpstan-param WP_REST_Request<T> $request
*/
public function update_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit options.', 'polylang' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Prepares the option value for the REST response.
*
* @since 3.7
*
* @param array $item Option values.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*
* @phpstan-template T of array
* @phpstan-param array<non-falsy-string, mixed> $item
* @phpstan-param WP_REST_Request<T> $request
*/
public function prepare_item_for_response( $item, $request ) {
$fields = $this->get_fields_for_response( $request );
$response = array();
foreach ( $item as $option => $value ) {
if ( rest_is_field_included( $option, $fields ) ) {
$response[ $option ] = $value;
}
}
/** @var WP_REST_Response */
return rest_ensure_response( $response );
}
/**
* Retrieves the options' schema, conforming to JSON Schema.
*
* @since 3.7
*
* @return array Item schema data.
*/
public function get_item_schema(): array {
return $this->add_additional_fields_schema( $this->options->get_schema() );
}
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\REST;
defined( 'ABSPATH' ) || exit;
add_action(
'pll_init',
function ( $polylang ) {
$polylang->rest = new API( $polylang->model );
add_action( 'rest_api_init', array( $polylang->rest, 'init' ) );
}
);

View File

@@ -0,0 +1,18 @@
<?php
/**
* Loads the settings module for Machine Translation.
*
* @package Polylang
*/
defined( 'ABSPATH' ) || exit;
if ( $polylang->model->has_languages() ) {
add_filter(
'pll_settings_modules',
function ( $modules ) {
$modules[] = 'PLL_Settings_Preview_Machine_Translation';
return $modules;
}
);
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* @package Polylang
*/
defined( 'ABSPATH' ) || exit;
/**
* Class to advertize the Machine Translation module.
*
* @since 3.6
*/
class PLL_Settings_Preview_Machine_Translation extends PLL_Settings_Module {
/**
* Stores the display order priority.
*
* @var int
*/
public $priority = 90;
/**
* Constructor.
*
* @since 3.6
*
* @param PLL_Settings $polylang Polylang object.
* @param array $args Optional. Addition arguments.
*
* @phpstan-param array{
* module?: non-falsy-string,
* title?: string,
* description?: string,
* active_option?: non-falsy-string
* } $args
*/
public function __construct( &$polylang, array $args = array() ) {
$default = array(
'module' => 'machine_translation',
'title' => __( 'Machine Translation', 'polylang' ),
'description' => __( 'Allows linkage to DeepL Translate.', 'polylang' ),
'active_option' => 'preview',
);
parent::__construct( $polylang, array_merge( $default, $args ) );
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* /!\ DO NOT DIRECTLY EDIT THIS FILE, THIS FILE IS AUTO-GENERATED AS PART OF THE BUILD PROCESS.
*/
return array(
'Blocks',
'REST',
'machine-translation',
'share-slug',
'site-health',
'sitemaps',
'sync',
'translate-slugs',
'wizard',
'wpml',
);

View File

@@ -0,0 +1,20 @@
<?php
/**
* Loads the settings module for shared slugs.
*
* @package Polylang
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly
}
if ( $polylang->model->has_languages() ) {
add_filter(
'pll_settings_modules',
function ( $modules ) {
$modules[] = 'PLL_Settings_Preview_Share_Slug';
return $modules;
}
);
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* @package Polylang
*/
/**
* A class to advertize the Share slugs module.
*
* @since 1.9
* @since 3.1 Renamed from PLL_Settings_Share_Slug.
*/
class PLL_Settings_Preview_Share_Slug extends PLL_Settings_Module {
/**
* Stores the display order priority.
*
* @var int
*/
public $priority = 70;
/**
* Constructor.
*
* @since 1.9
*
* @param PLL_Settings $polylang Polylang object.
* @param array $args Optional. Addition arguments.
*
* @phpstan-param array{
* module?: non-falsy-string,
* title?: string,
* description?: string,
* active_option?: non-falsy-string
* } $args
*/
public function __construct( &$polylang, array $args = array() ) {
$default = array(
'module' => 'share-slugs',
'title' => __( 'Share slugs', 'polylang' ),
'description' => $this->get_description(),
'active_option' => 'preview',
);
parent::__construct( $polylang, array_merge( $default, $args ) );
}
/**
* Returns the module description.
*
* @since 3.1
*
* @return string
*/
protected function get_description() {
return __( 'Allows to share the same URL slug across languages for posts and terms.', 'polylang' );
}
}

View File

@@ -0,0 +1,387 @@
<?php
/**
* @package Polylang
*/
/**
* Class PLL_Admin_Site_Health to add debug info in WP Site Health.
*
* @see https://make.wordpress.org/core/2019/04/25/site-health-check-in-5-2/ since WordPress 5.2
*
* @since 2.8
*/
class PLL_Admin_Site_Health {
/**
* A reference to the PLL_Model instance.
*
* @since 2.8
*
* @var PLL_Model
*/
protected $model;
/**
* A reference to the PLL_Admin_Static_Pages instance.
*
* @since 2.8
*
* @var PLL_Admin_Static_Pages|null
*/
protected $static_pages;
/**
* PLL_Admin_Site_Health constructor.
*
* @since 2.8
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
$this->static_pages = &$polylang->static_pages;
// Information tab.
add_filter( 'debug_information', array( $this, 'info_options' ), 15 );
add_filter( 'debug_information', array( $this, 'info_languages' ), 15 );
add_filter( 'debug_information', array( $this, 'info' ), 15 );
// Tests Tab.
add_filter( 'site_status_tests', array( $this, 'status_tests' ) );
add_filter( 'site_status_test_php_modules', array( $this, 'site_status_test_php_modules' ) ); // Require simplexml in Site health.
}
/**
* Returns a list of keys to exclude from the site health information.
*
* @since 2.8
*
* @return string[] List of option keys to ignore.
*/
protected function exclude_options_keys() {
return array(
'uninstall',
'first_activation',
);
}
/**
* Returns a list of keys to exclude from the site health information.
*
* @since 2.8
*
* @return string[] List of language keys to ignore.
*/
protected function exclude_language_keys() {
return array(
'flag',
'host',
'taxonomy',
'description',
'parent',
'filter',
'custom_flag',
);
}
/**
* Add Polylang Options to Site Health Information tab.
*
* @since 2.8
* @param array $debug_info The debug information to be added to the core information page.
*
* @return array
*/
public function info_options( $debug_info ) {
$fields = $this->model->options->get_site_health_info();
// Get effective translated post types and taxonomies. The options doesn't show all translated ones.
if ( ! empty( $this->model->get_translated_post_types() ) ) {
$fields['cpt']['label'] = __( 'Translated post types', 'polylang' );
$fields['cpt']['value'] = implode( ', ', $this->model->get_translated_post_types() );
}
if ( ! empty( $this->model->get_translated_taxonomies() ) ) {
$fields['taxonomies']['label'] = __( 'Translated taxonomies', 'polylang' );
$fields['taxonomies']['value'] = implode( ', ', $this->model->get_translated_taxonomies() );
}
$debug_info['pll_options'] = array(
/* translators: placeholder is the plugin name */
'label' => sprintf( __( '%s options', 'polylang' ), POLYLANG ),
'fields' => $fields,
);
return $debug_info;
}
/**
* Adds Polylang Languages settings to Site Health Information tab.
*
* @since 2.8
*
* @param array $debug_info The debug information to be added to the core information page.
* @return array
*/
public function info_languages( $debug_info ) {
foreach ( $this->model->get_languages_list() as $language ) {
$fields = array();
foreach ( $language->to_array() as $key => $value ) {
if ( in_array( $key, $this->exclude_language_keys(), true ) ) {
continue;
}
if ( empty( $value ) ) {
$value = '0';
}
$fields[ $key ]['label'] = $key;
if ( 'term_props' === $key && is_array( $value ) ) {
$fields[ $key ]['value'] = $this->get_info_term_props( $value );
} else {
$fields[ $key ]['value'] = $value;
}
if ( 'term_group' === $key ) {
$fields[ $key ]['label'] = 'order'; // Changed for readability but not translated as other keys are not.
}
}
$debug_info[ 'pll_language_' . $language->slug ] = array(
/* translators: %1$s placeholder is the language name, %2$s is the language code */
'label' => sprintf( __( 'Language: %1$s - %2$s', 'polylang' ), $language->name, $language->slug ),
/* translators: placeholder is the flag image */
'description' => sprintf( esc_html__( 'Flag used in the language switcher: %s', 'polylang' ), $this->get_flag( $language ) ),
'fields' => $fields,
);
}
return $debug_info;
}
/**
* Adds term props data to the info languages array.
*
* @since 3.4
*
* @param array $value The term props data.
* @return array The term props data formatted for the info languages tab.
*/
protected function get_info_term_props( $value ) {
$return_value = array();
foreach ( $value as $language_taxonomy => $item ) {
$language_taxonomy_array = array_fill( 0, count( $item ), $language_taxonomy );
$keys_with_language_taxonomy = array_map(
function ( $key, $language_taxonomy ) {
return "{$language_taxonomy}/{$key}";
},
array_keys( $item ),
$language_taxonomy_array
);
$value = array_combine( $keys_with_language_taxonomy, $item );
if ( is_array( $value ) ) {
$return_value = array_merge( $return_value, $value );
}
}
return $return_value;
}
/**
* Returns the flag used in the language switcher.
*
* @since 2.8
*
* @param PLL_Language $language Language object.
* @return string
*/
protected function get_flag( $language ) {
$flag = $language->get_display_flag();
return empty( $flag ) ? '<span>' . esc_html__( 'Undefined', 'polylang' ) . '</span>' : $flag;
}
/**
* Add a Site Health test on homepage translation.
*
* @since 2.8
*
* @param array $tests Array with tests declaration data.
* @return array
*/
public function status_tests( $tests ) {
// Add the test only if the homepage displays static page.
if ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_on_front' ) ) {
$tests['direct']['pll_homepage'] = array(
'label' => esc_html__( 'Homepage translated', 'polylang' ),
'test' => array( $this, 'homepage_test' ),
);
}
return $tests;
}
/**
* Test if the home page is translated or not.
*
* @since 2.8
*
* @return array $result Array with test results.
*/
public function homepage_test() {
$result = array(
'label' => __( 'All languages have a translated homepage', 'polylang' ),
'status' => 'good',
'badge' => array(
'label' => POLYLANG,
'color' => 'blue',
),
'description' => sprintf(
'<p>%s</p>',
esc_html__( 'It is mandatory to translate the static front page in all languages.', 'polylang' )
),
'actions' => '',
'test' => 'pll_homepage',
);
$message = $this->static_pages->get_must_translate_message();
if ( ! empty( $message ) ) {
$result['status'] = 'critical';
$result['label'] = __( 'The homepage is not translated in all languages', 'polylang' );
$result['description'] = sprintf( '<p>%s</p>', $message );
}
return $result;
}
/**
* Add Polylang Warnings to Site Health Information tab.
*
* @since 3.1
*
* @param array $debug_info The debug information to be added to the core information page.
* @return array
*/
public function info( $debug_info ) {
$fields = array();
// Add Post Types without languages.
$posts_no_lang = $this->get_post_ids_without_lang();
if ( ! empty( $posts_no_lang ) ) {
$fields['post-no-lang']['label'] = __( 'Posts without language', 'polylang' );
$fields['post-no-lang']['value'] = $posts_no_lang;
}
$terms_no_lang = $this->get_term_ids_without_lang();
if ( ! empty( $terms_no_lang ) ) {
$fields['term-no-lang']['label'] = __( 'Terms without language', 'polylang' );
$fields['term-no-lang']['value'] = $terms_no_lang;
}
// Add WPML files.
$wpml_files = PLL_WPML_Config::instance()->get_files();
if ( ! empty( $wpml_files ) ) {
$fields['wpml']['label'] = 'wpml-config.xml files';
$fields['wpml']['value'] = $wpml_files;
if ( ! extension_loaded( 'simplexml' ) ) {
$fields['simplexml']['label'] = __( 'PHP SimpleXML extension', 'polylang' );
$fields['simplexml']['value'] = __( 'Not loaded. Contact your host provider.', 'polylang' );
}
}
// Multisite
if ( is_multisite() ) {
if ( is_plugin_active_for_network( POLYLANG_BASENAME ) ) {
$network_activated = __( 'Yes', 'polylang' );
} else {
$network_activated = __( 'No', 'polylang' );
}
$fields['network_activated'] = array(
'label' => __( 'Network activated', 'polylang' ),
'value' => $network_activated,
);
}
// Create the section.
if ( ! empty( $fields ) ) {
$debug_info['pll_warnings'] = array(
/* translators: placeholder is the plugin name */
'label' => sprintf( __( '%s information', 'polylang' ), POLYLANG ),
'fields' => $fields,
);
}
return $debug_info;
}
/**
* Get an array with post_type as key and post ids as value.
*
* @since 3.1
*
* @param int $limit Max number of posts to show per post type. `-1` to return all of them. Default is 5.
*
* @return string[] An associative array where the keys are post types and the values
* are comma-separated strings of post IDs without a language.
*
* @phpstan-param -1|positive-int $limit *
*/
public function get_post_ids_without_lang( $limit = 5 ) {
$posts = array();
foreach ( $this->model->get_translated_post_types() as $post_type ) {
$post_ids_with_no_language = $this->model->get_posts_with_no_lang( $post_type, $limit );
if ( ! empty( $post_ids_with_no_language ) ) {
$posts[ $post_type ] = implode( ',', $post_ids_with_no_language );
}
}
return $posts;
}
/**
* Get an array with taxonomy as key and term ids as value.
*
* @since 3.1
* @param int $limit Max number of terms to show per post type. `-1` to return all of them. Default is 5.
*
* @return string[] An associative array where the keys are post types and the values
* are comma-separated strings of post IDs without a language.
* @phpstan-param -1|positive-int $limit
*/
public function get_term_ids_without_lang( $limit = 5 ) {
$terms = array();
foreach ( $this->model->get_translated_taxonomies() as $taxonomy ) {
$term_ids_with_no_language = $this->model->get_terms_with_no_lang( $taxonomy, $limit );
if ( ! empty( $term_ids_with_no_language ) ) {
$terms[ $taxonomy ] = implode( ',', $term_ids_with_no_language );
}
}
return $terms;
}
/**
* Requires the simplexml PHP module when a wpml-config.xml has been found.
*
* @since 3.1
* @since 3.2 Moved from PLL_WPML_Config
*
* @param array $modules An associative array of modules to test for.
* @return array
*/
public function site_status_test_php_modules( $modules ) {
$files = PLL_WPML_Config::instance()->get_files();
if ( ! empty( $files ) ) {
$modules['simplexml'] = array(
'extension' => 'simplexml',
'required' => true,
);
}
return $modules;
}
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* Loads the site health.
*
* @package Polylang
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly.
}
if ( $polylang instanceof PLL_Admin && $polylang->model->has_languages() ) {
$polylang->site_health = new PLL_Admin_Site_Health( $polylang );
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* @package Polylang
*/
/**
* Common class for handling the core sitemaps.
*
* The child classes must called the init() method.
*
* @since 3.0
*/
abstract class PLL_Abstract_Sitemaps {
/**
* Setups actions and filters.
*
* @since 2.8
*
* @return void
*/
public function init() {
add_filter( 'pll_home_url_white_list', array( $this, 'home_url_white_list' ) );
}
/**
* Whitelists the home url filter for the sitemaps.
*
* @since 2.8
*
* @param array $whitelist White list.
* @return array
*/
public function home_url_white_list( $whitelist ) {
$whitelist[] = array( 'file' => 'class-wp-sitemaps-posts' );
return $whitelist;
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* @package Polylang
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly.
}
if ( $polylang->model->has_languages() ) {
if ( $polylang->links_model instanceof PLL_Links_Abstract_Domain ) {
$polylang->sitemaps = new PLL_Sitemaps_Domain( $polylang );
} else {
$polylang->sitemaps = new PLL_Sitemaps( $polylang );
}
$polylang->sitemaps->init();
}

View File

@@ -0,0 +1,218 @@
<?php
/**
* @package Polylang
*/
/**
* Decorator to add multilingual capability to sitemaps providers
*
* @since 2.8
*/
class PLL_Multilingual_Sitemaps_Provider extends WP_Sitemaps_Provider {
/**
* The decorated sitemaps provider.
*
* @since 2.8
*
* @var WP_Sitemaps_Provider
*/
protected $provider;
/**
* The PLL_Links_Model instance.
*
* @since 2.8
*
* @var PLL_Links_Model
*/
protected $links_model;
/**
* The PLL_Model instance.
*
* @since 2.8
*
* @var PLL_Model
*/
protected $model;
/**
* Language used to filter queries for the sitemap index.
*
* @since 2.8
*
* @var string
*/
private static $filter_lang = '';
/**
* Constructor.
*
* @since 2.8
*
* @param WP_Sitemaps_Provider $provider An instance of a WP_Sitemaps_Provider child class.
* @param PLL_Links_Model $links_model The PLL_Links_Model instance.
*/
public function __construct( $provider, &$links_model ) {
$this->name = $provider->name;
$this->object_type = $provider->object_type;
$this->provider = $provider;
$this->links_model = &$links_model;
$this->model = &$links_model->model;
}
/**
* Gets a URL list for a sitemap.
*
* @since 2.8
*
* @param int $page_num Page of results.
* @param string $object_subtype Optional. Object subtype name. Default empty.
* @return array Array of URLs for a sitemap.
*/
public function get_url_list( $page_num, $object_subtype = '' ) {
return $this->provider->get_url_list( $page_num, $object_subtype );
}
/**
* Gets the max number of pages available for the object type.
*
* @since 2.8
*
* @param string $object_subtype Optional. Object subtype. Default empty.
* @return int Total number of pages.
*/
public function get_max_num_pages( $object_subtype = '' ) {
return $this->provider->get_max_num_pages( $object_subtype );
}
/**
* Filters the query arguments to add the language.
*
* @since 2.8
*
* @param array $args Sitemap provider WP_Query or WP_Term_Query arguments.
* @return array
*/
public static function query_args( $args ) {
if ( ! empty( self::$filter_lang ) ) {
$args['lang'] = self::$filter_lang;
}
return $args;
}
/**
* Gets data for a given sitemap type.
*
* @since 2.8
*
* @param string $object_subtype_name Object subtype name if any.
* @param string $lang Optional language name.
* @return array
*/
protected function get_sitemap_data( $object_subtype_name, $lang = '' ) {
$object_subtype_name = (string) $object_subtype_name;
if ( ! empty( $lang ) ) {
self::$filter_lang = $lang;
}
$return = array(
'name' => implode( '-', array_filter( array( $object_subtype_name, $lang ) ) ),
'pages' => $this->get_max_num_pages( $object_subtype_name ),
);
self::$filter_lang = '';
return $return;
}
/**
* Gets data about each sitemap type.
*
* @since 2.8
*
* @return array[] Array of sitemap types including object subtype name and number of pages.
*/
public function get_sitemap_type_data() {
$sitemap_data = array();
add_filter( 'wp_sitemaps_posts_query_args', array( self::class, 'query_args' ) );
add_filter( 'wp_sitemaps_taxonomies_query_args', array( self::class, 'query_args' ) );
$object_subtypes = $this->get_object_subtypes();
if ( empty( $object_subtypes ) ) {
foreach ( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) as $language ) {
$sitemap_data[] = $this->get_sitemap_data( '', $language );
}
}
switch ( $this->provider->name ) {
case 'posts':
$func = array( $this->model, 'is_translated_post_type' );
break;
case 'taxonomies':
$func = array( $this->model, 'is_translated_taxonomy' );
break;
default:
return $sitemap_data;
}
foreach ( array_keys( $object_subtypes ) as $object_subtype_name ) {
if ( call_user_func( $func, $object_subtype_name ) ) {
foreach ( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) as $language ) {
$sitemap_data[] = $this->get_sitemap_data( $object_subtype_name, $language );
}
} else {
$sitemap_data[] = $this->get_sitemap_data( $object_subtype_name );
}
}
return $sitemap_data;
}
/**
* Gets the URL of a sitemap entry.
*
* @since 2.8
*
* @param string $name The name of the sitemap.
* @param int $page The page of the sitemap.
* @return string The composed URL for a sitemap entry.
*/
public function get_sitemap_url( $name, $page ) {
// Check if a language was added in $name.
$pattern = '#(' . implode( '|', $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) . ')$#';
if ( preg_match( $pattern, $name, $matches ) ) {
$lang = $this->model->get_language( $matches[1] );
if ( ! empty( $lang ) ) {
$name = preg_replace( '#(-?' . $lang->slug . ')$#', '', $name );
$url = $this->provider->get_sitemap_url( $name, $page );
return $this->links_model->add_language_to_link( $url, $lang );
}
}
// If no language is present in $name, we may attempt to get the current sitemap url (e.g. in redirect_canonical() ).
if ( get_query_var( 'lang' ) ) {
$lang = $this->model->get_language( get_query_var( 'lang' ) );
$url = $this->provider->get_sitemap_url( $name, $page );
return $this->links_model->add_language_to_link( $url, $lang );
}
return $this->provider->get_sitemap_url( $name, $page );
}
/**
* Returns the list of supported object subtypes exposed by the provider.
*
* @since 2.8
*
* @return array List of object subtypes objects keyed by their name.
*/
public function get_object_subtypes() {
return $this->provider->get_object_subtypes();
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* @package Polylang
*/
/**
* Handles the core sitemaps for subdomains and multiple domains.
*
* @since 3.0
*/
class PLL_Sitemaps_Domain extends PLL_Abstract_Sitemaps {
/**
* @var PLL_Links_Abstract_Domain
*/
protected $links_model;
/**
* Constructor.
*
* @since 3.0
*
* @param object $polylang Main Polylang object.
*/
public function __construct( &$polylang ) {
$this->links_model = &$polylang->links_model;
}
/**
* Setups actions and filters.
*
* @since 3.0
*
* @return void
*/
public function init() {
parent::init();
add_filter( 'wp_sitemaps_index_entry', array( $this, 'index_entry' ) );
add_filter( 'wp_sitemaps_stylesheet_url', array( $this->links_model, 'site_url' ) );
add_filter( 'wp_sitemaps_stylesheet_index_url', array( $this->links_model, 'site_url' ) );
add_filter( 'home_url', array( $this, 'sitemap_url' ) );
}
/**
* Filters the sitemap index entries for subdomains and multiple domains.
*
* @since 2.8
*
* @param array $sitemap_entry Sitemap entry for the post.
* @return array
*/
public function index_entry( $sitemap_entry ) {
$sitemap_entry['loc'] = $this->links_model->site_url( $sitemap_entry['loc'] );
return $sitemap_entry;
}
/**
* Makes sure that the sitemap urls are always evaluated on the current domain.
*
* @since 2.8.4
*
* @param string $url A sitemap url.
* @return string
*/
public function sitemap_url( $url ) {
if ( false !== strpos( $url, '/wp-sitemap' ) ) {
$url = $this->links_model->site_url( $url );
}
return $url;
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* @package Polylang
*/
/**
* Handles the core sitemaps for sites using a single domain.
*
* @since 2.8
*/
class PLL_Sitemaps extends PLL_Abstract_Sitemaps {
/**
* @var PLL_Links_Model
*/
protected $links_model;
/**
* @var PLL_Model
*/
protected $model;
/**
* Stores the plugin options.
*
* @var \WP_Syntex\Polylang\Options\Options
*/
protected $options;
/**
* Constructor.
*
* @since 2.8
*
* @param PLL_Base $polylang Main Polylang object.
*/
public function __construct( PLL_Base &$polylang ) {
$this->links_model = &$polylang->links_model;
$this->model = &$polylang->model;
$this->options = $polylang->options;
}
/**
* Setups actions and filters.
*
* @since 2.8
*
* @return void
*/
public function init() {
parent::init();
add_filter( 'pll_set_language_from_query', array( $this, 'set_language_from_query' ), 10, 2 );
add_filter( 'rewrite_rules_array', array( $this, 'rewrite_rules' ) );
add_filter( 'wp_sitemaps_add_provider', array( $this, 'replace_provider' ) );
}
/**
* Assigns the current language to the default language when the sitemap url
* doesn't include any language.
*
* @since 2.8
* @since 3.8 Returns a language object instead of a language slug.
*
* @param PLL_Language|false $lang Current language code, false if not set yet.
* @param WP_Query $query Main WP query object.
* @return PLL_Language|false
*/
public function set_language_from_query( $lang, $query ) {
if ( isset( $query->query['sitemap'] ) && empty( $query->query['lang'] ) ) {
return $this->model->languages->get_default();
}
return $lang;
}
/**
* Filters the sitemaps rewrite rules to take the languages into account.
*
* @since 2.8
*
* @param string[] $rules Rewrite rules.
* @return string[] Modified rewrite rules.
*/
public function rewrite_rules( $rules ) {
global $wp_rewrite;
$languages = $this->model->languages
->filter( $this->options['hide_default'] ? 'hide_default' : '' )
->get_list( array( 'fields' => 'slug' ) );
if ( empty( $languages ) ) {
return $rules;
}
$slug = $wp_rewrite->root . ( $this->options['rewrite'] ? '^' : '^language/' ) . '(' . implode( '|', $languages ) . ')/';
$newrules = array();
foreach ( $rules as $key => $rule ) {
if ( false !== strpos( $rule, 'sitemap=$matches[1]' ) ) {
$newrules[ str_replace( '^wp-sitemap', $slug . 'wp-sitemap', $key ) ] = str_replace(
array( '[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]', '[1]', '?' ),
array( '[9]', '[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]', '?lang=$matches[1]&' ),
$rule
); // Should be enough!
}
$newrules[ $key ] = $rule;
}
return $newrules;
}
/**
* Replaces a sitemap provider by our decorator.
*
* @since 2.8
*
* @param WP_Sitemaps_Provider $provider Instance of a WP_Sitemaps_Provider.
* @return WP_Sitemaps_Provider
*/
public function replace_provider( $provider ) {
if ( $provider instanceof WP_Sitemaps_Provider ) {
$provider = new PLL_Multilingual_Sitemaps_Provider( $provider, $this->links_model );
}
return $provider;
}
}

View File

@@ -0,0 +1,248 @@
<?php
/**
* @package Polylang
*/
/**
* Manages copy and synchronization of terms and post metas
*
* @since 1.2
*/
class PLL_Admin_Sync extends PLL_Sync {
/**
* @var PLL_Admin_Links
*/
private $links;
/**
* Constructor
*
* @since 1.2
*
* @param PLL_Admin_Base $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
$this->links = &$polylang->links;
add_filter( 'wp_insert_post_parent', array( $this, 'wp_insert_post_parent' ) );
add_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ) );
add_filter( 'use_block_editor_for_post', array( $this, 'new_post_translation' ), 5000 ); // After content duplication.
}
/**
* Translates the post parent if it exists when using "Add new" (translation).
*
* @since 0.6
* @since 3.8 Removed second and third parameters.
*
* @param int $post_parent Post parent ID.
* @return int
*/
public function wp_insert_post_parent( $post_parent ) {
$context_data = $this->links->get_data_from_new_post_translation_request();
if ( empty( $context_data ) ) {
return $post_parent;
}
// Make sure not to impact media translations created at the same time.
$parent_id = wp_get_post_parent_id( $context_data['from_post'] );
if ( empty( $parent_id ) ) {
return $post_parent;
}
$tr_parent = $this->model->post->get_translation( $parent_id, $context_data['new_lang'] );
if ( empty( $tr_parent ) ) {
return $post_parent;
}
return $tr_parent;
}
/**
* Copies menu order, comment, ping status and optionally the date when creating a new translation.
*
* @since 2.5
*
* @param array $data An array of slashed post data.
* @return array
*/
public function wp_insert_post_data( $data ) {
$context_data = $this->links->get_data_from_new_post_translation_request();
if ( empty( $context_data ) ) {
return $data;
}
foreach ( array( 'menu_order', 'comment_status', 'ping_status' ) as $property ) {
$data[ $property ] = $context_data['from_post']->$property;
}
// Copy the date only if the synchronization is activated.
if ( in_array( 'post_date', $this->options['sync'], true ) ) {
$data['post_date'] = $context_data['from_post']->post_date;
$data['post_date_gmt'] = $context_data['from_post']->post_date_gmt;
}
return $data;
}
/**
* Copies post metas and taxonomies when using "Add new" (translation).
*
* @since 2.5
* @since 3.1 Use of use_block_editor_for_post filter instead of rest_api_init which is triggered too early in WP 5.8.
*
* @param bool $is_block_editor Whether the post can be edited or not.
* @return bool
*/
public function new_post_translation( $is_block_editor ) {
global $post;
static $done = array();
if ( empty( $post ) ) {
return $is_block_editor;
}
$context_data = $this->links->get_data_from_new_post_translation_request();
if ( empty( $context_data ) || ! empty( $done[ $context_data['from_post']->ID ] ) ) {
return $is_block_editor;
}
$lang = $this->model->get_language( $context_data['new_lang'] );
if ( empty( $lang ) ) {
return $is_block_editor;
}
$done[ $context_data['from_post']->ID ] = true; // Avoid a second duplication in the block editor. Using an array only to allow multiple phpunit tests.
$this->taxonomies->copy( $context_data['from_post']->ID, $post->ID, $lang->slug );
$this->post_metas->copy( $context_data['from_post']->ID, $post->ID, $lang->slug );
if ( is_sticky( $context_data['from_post']->ID ) ) {
stick_post( $post->ID );
}
return $is_block_editor;
}
/**
* Get post fields to synchronize.
*
* @since 2.4
*
* @param WP_Post $post Post object.
* @return array Fields to synchronize.
*/
protected function get_fields_to_sync( $post ) {
global $wpdb;
$postarr = parent::get_fields_to_sync( $post );
$context_data = $this->links->get_data_from_new_post_translation_request();
// For new drafts, save the date now otherwise it is overridden by WP. Thanks to JoryHogeveen. See #32.
if ( ! empty( $context_data ) && in_array( 'post_date', $this->options['sync'], true ) ) {
unset( $postarr['post_date'] );
unset( $postarr['post_date_gmt'] );
$wpdb->update(
$wpdb->posts,
array(
'post_date' => $context_data['from_post']->post_date,
'post_date_gmt' => $context_data['from_post']->post_date_gmt,
),
array( 'ID' => $post->ID )
);
}
if ( isset( $GLOBALS['post_type'] ) ) {
$post_type = $GLOBALS['post_type'];
} elseif ( isset( $_REQUEST['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// 2nd case for quick edit.
$post_type = sanitize_key( $_REQUEST['post_type'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
// Make sure not to impact media translations when creating them at the same time as post
if ( in_array( 'post_parent', $this->options['sync'], true ) && ( ! isset( $post_type ) || $post_type !== $post->post_type ) ) {
unset( $postarr['post_parent'] );
}
return $postarr;
}
/**
* Synchronizes post fields in translations.
*
* @since 1.2
*
* @param int $post_id Post id.
* @param WP_Post $post Post object.
* @param int[] $translations Post translations.
*/
public function pll_save_post( $post_id, $post, $translations ) {
parent::pll_save_post( $post_id, $post, $translations );
// Sticky posts
if ( in_array( 'sticky_posts', $this->options['sync'] ) ) {
$stickies = get_option( 'sticky_posts' );
if ( isset( $_REQUEST['sticky'] ) && 'sticky' === $_REQUEST['sticky'] ) { // phpcs:ignore WordPress.Security.NonceVerification
$stickies = array_merge( $stickies, array_values( $translations ) );
} else {
$stickies = array_diff( $stickies, array_values( $translations ) );
}
update_option( 'sticky_posts', array_unique( $stickies ) );
}
}
/**
* Some backward compatibility with Polylang < 2.3
* allows to call PLL()->sync->copy_post_metas() and PLL()->sync->copy_taxonomies()
* used for example in Polylang for WooCommerce
* the compatibility is however only partial as the 4th argument $sync is lost
*
* @since 2.3
*
* @throws BadMethodCallException If the method is not found.
*
* @param string $func Function name
* @param array $args Function arguments
* @return mixed|void
*/
public function __call( $func, $args ) {
$obj = substr( $func, 5 );
if ( is_object( $this->$obj ) && method_exists( $this->$obj, 'copy' ) ) {
if ( WP_DEBUG ) {
$debug = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
$i = 1 + empty( $debug[1]['line'] ); // The file and line are in $debug[2] if the function was called using call_user_func
trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions
sprintf(
'%1$s was called incorrectly in %3$s on line %4$s: the call to PLL()->sync->%1$s() has been deprecated in Polylang 2.3, use PLL()->sync->%2$s->copy() instead.' . "\nError handler",
esc_html( $func ),
esc_html( $obj ),
esc_html( $debug[ $i ]['file'] ),
absint( $debug[ $i ]['line'] )
)
);
}
return call_user_func_array( array( $this->$obj, 'copy' ), $args );
}
$debug = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
throw new BadMethodCallException(
sprintf(
'Call to undefined method PLL()->sync->%1$s() in %2$s on line %3$s' . "\nError handler",
esc_html( $func ),
esc_html( $debug[0]['file'] ),
absint( $debug[0]['line'] )
)
);
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Loads the module for general synchronization such as metas and taxonomies.
*
* @package Polylang
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly
}
if ( $polylang->model->has_languages() ) {
if ( $polylang instanceof PLL_Admin_Base ) {
$polylang->sync = new PLL_Admin_Sync( $polylang );
} else {
$polylang->sync = new PLL_Sync( $polylang );
}
add_filter(
'pll_settings_modules',
function ( $modules ) {
$modules[] = 'PLL_Settings_Sync';
return $modules;
}
);
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* @package Polylang
*/
/**
* Settings class for synchronization settings management
*
* @since 1.8
*/
class PLL_Settings_Sync extends PLL_Settings_Module {
/**
* Stores the display order priority.
*
* @var int
*/
public $priority = 50;
/**
* Constructor
*
* @since 1.8
*
* @param PLL_Settings $polylang The polylang object.
*/
public function __construct( &$polylang ) {
parent::__construct(
$polylang,
array(
'module' => 'sync',
'title' => __( 'Synchronization', 'polylang' ),
'description' => __( 'The synchronization options allow to maintain exact same values (or translations in the case of taxonomies and page parent) of meta content between the translations of a post or page.', 'polylang' ),
)
);
}
/**
* Deactivates the module
*
* @since 1.8
*/
public function deactivate() {
$this->options['sync'] = array();
}
/**
* Displays the settings form
*
* @since 1.8
*/
protected function form() {
?>
<ul class="pll-inline-block-list">
<?php
foreach ( self::list_metas_to_sync() as $key => $str ) {
printf(
'<li><label><input name="sync[%s]" type="checkbox" value="1" %s /> %s</label></li>',
esc_attr( $key ),
checked( in_array( $key, $this->options['sync'] ), true, false ),
esc_html( $str )
);
}
?>
</ul>
<?php
}
/**
* Prepare the received data before saving.
*
* @since 3.7
*
* @param array $options Raw values to save.
* @return array
*/
protected function prepare_raw_data( array $options ): array {
// Take care to return only validated options.
return array( 'sync' => empty( $options['sync'] ) ? array() : array_keys( $options['sync'], 1 ) );
}
/**
* Get the row actions.
*
* @since 1.8
*
* @return string[] Row actions.
*/
protected function get_actions() {
return empty( $this->options['sync'] ) ? array( 'configure' ) : array( 'configure', 'deactivate' );
}
/**
* Get the list of synchronization settings.
*
* @since 1.0
*
* @return string[] Array synchronization options.
*
* @phpstan-return non-empty-array<non-falsy-string, string>
*/
public static function list_metas_to_sync() {
return array(
'taxonomies' => __( 'Taxonomies', 'polylang' ),
'post_meta' => __( 'Custom fields', 'polylang' ),
'comment_status' => __( 'Comment status', 'polylang' ),
'ping_status' => __( 'Ping status', 'polylang' ),
'sticky_posts' => __( 'Sticky posts', 'polylang' ),
'post_date' => __( 'Published date', 'polylang' ),
'post_format' => __( 'Post format', 'polylang' ),
'post_parent' => __( 'Page parent', 'polylang' ),
'_wp_page_template' => __( 'Page template', 'polylang' ),
'menu_order' => __( 'Page order', 'polylang' ),
'_thumbnail_id' => __( 'Featured image', 'polylang' ),
);
}
}

View File

@@ -0,0 +1,406 @@
<?php
/**
* @package Polylang
*/
/**
* Abstract class to manage the copy and synchronization of metas
*
* @since 2.3
*/
abstract class PLL_Sync_Metas {
/**
* @var PLL_Model
*/
public $model;
/**
* Meta type. Typically 'post' or 'term'.
*
* @var string
*/
protected $meta_type;
/**
* Stores the previous values when updating a meta.
*
* @var array
*/
protected $prev_value = array();
/**
* Stores the metas to synchronize before deleting them.
*
* @var array
*/
protected $to_copy = array();
/**
* Constructor.
*
* @since 2.3
*
* @param PLL_Base $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
add_filter( "add_{$this->meta_type}_metadata", array( $this, 'can_synchronize_metadata' ), 1, 3 );
add_filter( "update_{$this->meta_type}_metadata", array( $this, 'can_synchronize_metadata' ), 1, 3 );
add_filter( "delete_{$this->meta_type}_metadata", array( $this, 'can_synchronize_metadata' ), 1, 3 );
$this->add_all_meta_actions();
add_action( "pll_save_{$this->meta_type}", array( $this, 'save_object' ), 10, 3 );
}
/**
* Removes "added_{$this->meta_type}_meta" action
*
* @since 2.3
*
* @return void
*/
protected function remove_add_meta_action() {
remove_action( "added_{$this->meta_type}_meta", array( $this, 'add_meta' ) );
}
/**
* Removes all meta synchronization actions and filters
*
* @since 2.3
*
* @return void
*/
public function remove_all_meta_actions() {
$this->remove_add_meta_action();
remove_filter( "update_{$this->meta_type}_metadata", array( $this, 'update_metadata' ), 999 );
remove_action( "update_{$this->meta_type}_meta", array( $this, 'update_meta' ) );
remove_action( "delete_{$this->meta_type}_meta", array( $this, 'store_metas_to_sync' ) );
remove_action( "deleted_{$this->meta_type}_meta", array( $this, 'delete_meta' ) );
}
/**
* Adds "added_{$this->meta_type}_meta" action
*
* @since 2.3
*
* @return void
*/
protected function restore_add_meta_action() {
add_action( "added_{$this->meta_type}_meta", array( $this, 'add_meta' ), 10, 4 );
}
/**
* Adds meta synchronization actions and filters
*
* @since 2.3
*
* @return void
*/
public function add_all_meta_actions() {
$this->restore_add_meta_action();
add_filter( "update_{$this->meta_type}_metadata", array( $this, 'update_metadata' ), 999, 5 ); // Very late in case a filter prevents the meta to be updated
add_action( "update_{$this->meta_type}_meta", array( $this, 'update_meta' ), 10, 4 );
add_action( "delete_{$this->meta_type}_meta", array( $this, 'store_metas_to_sync' ), 10, 2 );
add_action( "deleted_{$this->meta_type}_meta", array( $this, 'delete_meta' ), 10, 4 );
}
/**
* Maybe modify ("translate") a meta value when it is copied or synchronized
*
* @since 2.3
*
* @param mixed $value Meta value
* @param string $key Meta key
* @param int $from Id of the source
* @param int $to Id of the target
* @param string $lang Language of target
* @return mixed
*/
protected function maybe_translate_value( $value, $key, $from, $to, $lang ) {
/**
* Filter a meta value before is copied or synchronized
*
* @since 2.3
*
* @param mixed $value Meta value
* @param string $key Meta key
* @param string $lang Language of target
* @param int $from Id of the source
* @param int $to Id of the target
*/
return apply_filters( "pll_translate_{$this->meta_type}_meta", $value, $key, $lang, $from, $to );
}
/**
* Get the custom fields to copy or synchronize.
*
* @since 2.3
*
* @param int $from Id of the post from which we copy information.
* @param int $to Id of the post to which we paste information.
* @param string $lang Language slug.
* @param bool $sync True if it is synchronization, false if it is a copy.
* @return string[] List of meta keys.
*/
protected function get_metas_to_copy( $from, $to, $lang, $sync = false ) {
/**
* Filters the custom fields to copy or synchronize.
*
* @since 0.6
* @since 1.9.2 The `$from`, `$to`, `$lang` parameters were added.
*
* @param string[] $keys List of custom fields names.
* @param bool $sync True if it is synchronization, false if it is a copy.
* @param int $from Id of the post from which we copy information.
* @param int $to Id of the post to which we paste information.
* @param string $lang Language slug.
*/
return array_unique( apply_filters( "pll_copy_{$this->meta_type}_metas", array(), $sync, $from, $to, $lang ) );
}
/**
* Disallow modifying synchronized meta if the current user can not modify translations
*
* @since 2.6
*
* @param null|bool $check Whether to allow adding/updating/deleting metadata.
* @param int $id Object ID.
* @param string $meta_key Meta key.
* @return null|bool
*/
public function can_synchronize_metadata( $check, $id, $meta_key ) {
if ( ! $this->model->{$this->meta_type}->current_user_can_synchronize( $id ) ) {
$tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
foreach ( $tr_ids as $lang => $tr_id ) {
if ( $tr_id != $id ) {
$to_copy = $this->get_metas_to_copy( $id, $tr_id, $lang, true );
if ( in_array( $meta_key, $to_copy ) ) {
return false;
}
}
}
}
return $check;
}
/**
* Synchronize added metas across translations
*
* @since 2.3
*
* @param int $mid Meta id.
* @param int $id Object ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value. Must be serializable if non-scalar.
* @return void
*/
public function add_meta( $mid, $id, $meta_key, $meta_value ) {
static $avoid_recursion = false;
if ( ! $avoid_recursion ) {
$avoid_recursion = true;
$tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
foreach ( $tr_ids as $lang => $tr_id ) {
if ( $tr_id != $id ) {
$to_copy = $this->get_metas_to_copy( $id, $tr_id, $lang, true );
if ( in_array( $meta_key, $to_copy ) ) {
$meta_value = $this->maybe_translate_value( $meta_value, $meta_key, $id, $tr_id, $lang );
add_metadata( $this->meta_type, $tr_id, wp_slash( $meta_key ), is_object( $meta_value ) ? $meta_value : wp_slash( $meta_value ) );
}
}
}
$avoid_recursion = false;
}
}
/**
* Stores the previous value when updating metas
*
* @since 2.3
*
* @param null|bool $r Not used
* @param int $id Object ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value. Must be serializable if non-scalar.
* @param mixed $prev_value If specified, only update existing metadata entries with the specified value.
* @return null|bool Unchanged
*/
public function update_metadata( $r, $id, $meta_key, $meta_value, $prev_value ) {
if ( null === $r ) {
$hash = md5( "$id|$meta_key|" . maybe_serialize( $meta_value ) );
$this->prev_value[ $hash ] = $prev_value;
}
return $r;
}
/**
* Synchronize updated metas across translations
*
* @since 2.3
*
* @param int $mid Meta id.
* @param int $id Object ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value. Must be serializable if non-scalar.
* @return void
*/
public function update_meta( $mid, $id, $meta_key, $meta_value ) {
static $avoid_recursion = false;
$id = (int) $id;
if ( ! $avoid_recursion ) {
$avoid_recursion = true;
$hash = md5( "$id|$meta_key|" . maybe_serialize( $meta_value ) );
$prev_meta = get_metadata_by_mid( $this->meta_type, $mid );
if ( $prev_meta ) {
$this->remove_add_meta_action(); // We don't want to sync back the new metas
$tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
foreach ( $tr_ids as $lang => $tr_id ) {
if ( $tr_id != $id && in_array( $meta_key, $this->get_metas_to_copy( $id, $tr_id, $lang, true ) ) ) {
if ( empty( $this->prev_value[ $hash ] ) || $this->prev_value[ $hash ] === $prev_meta->meta_value ) {
$prev_value = $this->maybe_translate_value( $prev_meta->meta_value, $meta_key, $id, $tr_id, $lang );
$meta_value = $this->maybe_translate_value( $meta_value, $meta_key, $id, $tr_id, $lang );
update_metadata( $this->meta_type, $tr_id, wp_slash( $meta_key ), is_object( $meta_value ) ? $meta_value : wp_slash( $meta_value ), $prev_value );
}
}
}
$this->restore_add_meta_action();
}
unset( $this->prev_value[ $hash ] );
$avoid_recursion = false;
}
}
/**
* Store metas to synchronize before deleting them.
*
* @since 2.3
*
* @param int[] $mids Not used.
* @param int $id Object ID.
* @return void
*/
public function store_metas_to_sync( $mids, $id ) {
$tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
foreach ( $tr_ids as $lang => $tr_id ) {
$this->to_copy[ $id ][ $tr_id ] = $this->get_metas_to_copy( $id, $tr_id, $lang, true );
}
}
/**
* Synchronizes deleted meta across translations.
*
* @since 2.3
*
* @param int[] $mids Not used.
* @param int $id Object ID.
* @param string $key Meta key.
* @param mixed $value Meta value.
* @return void
*/
public function delete_meta( $mids, $id, $key, $value ) {
static $avoid_recursion = false;
if ( ! $avoid_recursion ) {
$avoid_recursion = true;
$tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
foreach ( $tr_ids as $lang => $tr_id ) {
if ( $tr_id != $id ) {
if ( in_array( $key, $this->to_copy[ $id ][ $tr_id ] ) ) {
if ( '' !== $value && null !== $value && false !== $value ) { // Same test as WP
$value = $this->maybe_translate_value( $value, $key, $id, $tr_id, $lang );
}
delete_metadata( $this->meta_type, $tr_id, wp_slash( $key ), is_object( $value ) ? $value : wp_slash( $value ) );
}
}
}
}
$avoid_recursion = false;
}
/**
* Copy or synchronize metas
*
* @since 2.3
*
* @param int $from Id of the source object
* @param int $to Id of the target object
* @param string $lang Language code of the target object
* @param bool $sync Optional, defaults to true. True if it is synchronization, false if it is a copy
* @return void
*/
public function copy( $from, $to, $lang, $sync = false ) {
$this->remove_all_meta_actions();
$to_copy = $this->get_metas_to_copy( $from, $to, $lang, $sync );
$metas = get_metadata( $this->meta_type, $from );
$metas = is_array( $metas ) ? $metas : array();
$tr_metas = get_metadata( $this->meta_type, $to );
$tr_metas = is_array( $tr_metas ) ? $tr_metas : array();
foreach ( $to_copy as $key ) {
if ( empty( $metas[ $key ] ) ) {
if ( ! empty( $tr_metas[ $key ] ) ) {
// If the meta key is not present in the source object, delete all values
delete_metadata( $this->meta_type, $to, wp_slash( $key ) );
}
} elseif ( ! empty( $tr_metas[ $key ] ) && 1 === count( $metas[ $key ] ) && 1 === count( $tr_metas[ $key ] ) ) {
// One custom field to update
$value = reset( $metas[ $key ] );
$value = maybe_unserialize( $value );
$to_value = $this->maybe_translate_value( $value, $key, $from, $to, $lang );
update_metadata( $this->meta_type, $to, wp_slash( $key ), is_object( $to_value ) ? $to_value : wp_slash( $to_value ) );
} else {
// Multiple custom fields, either in the source or the target
if ( ! empty( $tr_metas[ $key ] ) ) {
// The synchronization of multiple values custom fields is easier if we delete all metas first
delete_metadata( $this->meta_type, $to, wp_slash( $key ) );
}
foreach ( $metas[ $key ] as $value ) {
$value = maybe_unserialize( $value );
$to_value = $this->maybe_translate_value( $value, $key, $from, $to, $lang );
add_metadata( $this->meta_type, $to, wp_slash( $key ), is_object( $to_value ) ? $to_value : wp_slash( $to_value ) );
}
}
}
$this->add_all_meta_actions();
}
/**
* If synchronized custom fields were previously not synchronized, it is expected
* that saving a post (or term) will synchronize them.
*
* @since 2.3
*
* @param int $object_id Id of the object being saved.
* @param object $obj Not used.
* @param int[] $translations The list of translations object ids.
* @return void
*/
public function save_object( $object_id, $obj, $translations ) {
foreach ( $translations as $tr_lang => $tr_id ) {
if ( $tr_id != $object_id ) {
$this->copy( $object_id, $tr_id, $tr_lang, true );
}
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* @package Polylang
*/
/**
* A class to manage copy and synchronization of post metas.
*
* @since 2.3
*/
class PLL_Sync_Post_Metas extends PLL_Sync_Metas {
/**
* Stores the plugin options.
*
* @var \WP_Syntex\Polylang\Options\Options
*/
public $options;
/**
* Constructor.
*
* @since 2.3
*
* @param PLL_Base $polylang The Polylang object.
*/
public function __construct( PLL_Base &$polylang ) {
$this->meta_type = 'post';
parent::__construct( $polylang );
$this->options = $polylang->options;
add_filter( 'pll_translate_post_meta', array( $this, 'translate_thumbnail_id' ), 10, 3 );
}
/**
* Get the custom fields to copy or synchronize.
*
* @since 2.3
*
* @param int $from Id of the post from which we copy information.
* @param int $to Id of the post to which we paste information.
* @param string $lang Language slug.
* @param bool $sync True if it is synchronization, false if it is a copy.
* @return string[] List of meta keys.
*/
protected function get_metas_to_copy( $from, $to, $lang, $sync = false ) {
$keys = array();
// Get public meta keys ( including from translated post in case we just deleted a custom field ).
if ( ! $sync || in_array( 'post_meta', $this->options['sync'] ) ) {
$from_keys = (array) get_post_custom_keys( $from );
$to_keys = (array) get_post_custom_keys( $to );
$keys = array_unique( array_merge( $from_keys, $to_keys ) );
foreach ( $keys as $k => $meta_key ) {
if ( is_protected_meta( $meta_key ) ) {
unset( $keys[ $k ] );
}
}
}
// Add page template and featured image.
foreach ( array( '_wp_page_template', '_thumbnail_id' ) as $meta ) {
if ( ! $sync || in_array( $meta, $this->options['sync'] ) ) {
$keys[] = $meta;
}
}
if ( $this->options['media_support'] ) {
$keys[] = '_wp_attached_file';
$keys[] = '_wp_attachment_metadata';
$keys[] = '_wp_attachment_backup_sizes';
$keys[] = '_wp_attachment_is_custom_header'; // Random header image.
}
/** This filter is documented in src/modules/sync/sync-metas.php */
return array_unique( apply_filters( 'pll_copy_post_metas', $keys, $sync, $from, $to, $lang ) );
}
/**
* Translates the thumbnail id.
*
* @since 2.3
*
* @param int $value Thumbnail id.
* @param string $key Meta key.
* @param string $lang Language code.
* @return int
*/
public function translate_thumbnail_id( $value, $key, $lang ) {
if ( ! $this->options['media_support'] || '_thumbnail_id' !== $key ) {
return $value;
}
$to_value = $this->model->post->get_translation( $value, $lang );
return $to_value ?: $value;
}
}

View File

@@ -0,0 +1,310 @@
<?php
/**
* @package Polylang
*/
/**
* A class to manage the synchronization of taxonomy terms across posts translations
*
* @since 2.3
*/
class PLL_Sync_Tax {
/**
* Stores the plugin options.
*
* @var \WP_Syntex\Polylang\Options\Options
*/
protected $options;
/**
* @var PLL_Model
*/
protected $model;
/**
* Constructor.
*
* @since 2.3
*
* @param PLL_Base $polylang The Polylang object.
*/
public function __construct( PLL_Base &$polylang ) {
$this->model = &$polylang->model;
$this->options = $polylang->options;
add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 5 );
add_action( 'pll_save_term', array( $this, 'create_term' ), 10, 3 );
add_action( 'pre_delete_term', array( $this, 'pre_delete_term' ) );
add_action( 'delete_term', array( $this, 'delete_term' ) );
}
/**
* Get the list of taxonomies to copy or to synchronize.
*
* @since 1.7
* @since 2.1 The `$from`, `$to`, `$lang` parameters were added.
* @since 3.2 Changed visibility from protected to public.
*
* @param bool $sync True if it is synchronization, false if it is a copy.
* @param int $from Id of the post from which we copy information, optional, defaults to null.
* @param int $to Id of the post to which we paste information, optional, defaults to null.
* @param string $lang Language slug, optional, defaults to null.
* @return string[] List of taxonomy names.
*/
public function get_taxonomies_to_copy( $sync, $from = null, $to = null, $lang = null ) {
$taxonomies = ! $sync || in_array( 'taxonomies', $this->options['sync'] ) ? $this->model->get_translated_taxonomies() : array();
if ( ! $sync || in_array( 'post_format', $this->options['sync'] ) ) {
$taxonomies[] = 'post_format';
}
/**
* Filters the taxonomies to copy or synchronize.
*
* @since 1.7
* @since 2.1 The `$from`, `$to`, `$lang` parameters were added.
*
* @param string[] $taxonomies List of taxonomy names.
* @param bool $sync True if it is synchronization, false if it is a copy.
* @param int $from Id of the post from which we copy information.
* @param int $to Id of the post to which we paste information.
* @param string $lang Language slug.
*/
return array_unique( apply_filters( 'pll_copy_taxonomies', $taxonomies, $sync, $from, $to, $lang ) );
}
/**
* When copying or synchronizing terms, translate terms in translatable taxonomies
*
* @since 2.3
*
* @param int $object_id Object ID.
* @param int[] $terms List of terms ids assigned to the source post.
* @param string $taxonomy Taxonomy name.
* @param string $lang Language slug.
* @return int[] List of terms ids to assign to the target post.
*/
protected function maybe_translate_terms( $object_id, $terms, $taxonomy, $lang ) {
if ( is_array( $terms ) && $this->model->is_translated_taxonomy( $taxonomy ) ) {
$newterms = array();
// Convert to term ids if we got tag names
$strings = array_map( 'is_string', $terms );
if ( in_array( true, $strings, true ) ) {
$terms = get_the_terms( $object_id, $taxonomy );
$terms = wp_list_pluck( $terms, 'term_id' );
}
foreach ( $terms as $term ) {
/**
* Filter the translated term when a post translation is created or synchronized
*
* @since 2.3
*
* @param int $tr_term Translated term id
* @param int $term Source term id
* @param string $lang Language slug
*/
if ( $term_id = apply_filters( 'pll_maybe_translate_term', (int) $this->model->term->get_translation( $term, $lang ), $term, $lang ) ) {
$newterms[] = (int) $term_id; // Cast is important otherwise we get 'numeric' tags
}
}
return $newterms;
}
return $terms; // Empty $terms or untranslated taxonomy
}
/**
* Maybe copy taxonomy terms from one post to the other.
*
* @since 2.6
*
* @param int $object_id Source object ID.
* @param int $tr_id Target object ID.
* @param string $lang Target language.
* @param array $terms An array of object terms.
* @param string $taxonomy Taxonomy slug.
* @param bool $append Whether to append new terms to the old terms.
* @return void
*/
protected function copy_object_terms( $object_id, $tr_id, $lang, $terms, $taxonomy, $append ) {
$to_copy = $this->get_taxonomies_to_copy( true, $object_id, $tr_id, $lang );
if ( in_array( $taxonomy, $to_copy ) ) {
$newterms = $this->maybe_translate_terms( $object_id, $terms, $taxonomy, $lang );
// For some reasons, the user may have untranslated terms in the translation. Don't forget them.
if ( $this->model->is_translated_taxonomy( $taxonomy ) ) {
$tr_terms = get_the_terms( $tr_id, $taxonomy );
if ( is_array( $tr_terms ) ) {
foreach ( $tr_terms as $term ) {
if ( ! $this->model->term->get_translation( $term->term_id, $this->model->post->get_language( $object_id ) ) ) {
$newterms[] = (int) $term->term_id;
}
}
}
}
wp_set_object_terms( $tr_id, $newterms, $taxonomy, $append );
}
}
/**
* When assigning terms to a post, assign translated terms to the translated posts (synchronisation).
*
* @since 2.3
*
* @param int $object_id Object ID.
* @param array $terms An array of object terms.
* @param int[] $tt_ids An array of term taxonomy IDs.
* @param string $taxonomy Taxonomy slug.
* @param bool $append Whether to append new terms to the old terms.
* @return void
*/
public function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append ) {
static $avoid_recursion = false;
$taxonomy_object = get_taxonomy( $taxonomy );
// Make sure that the taxonomy is registered for a post type
if ( ! $avoid_recursion && ! empty( $taxonomy_object ) && array_filter( $taxonomy_object->object_type, 'post_type_exists' ) ) {
$avoid_recursion = true;
$tr_ids = $this->model->post->get_translations( $object_id );
foreach ( $tr_ids as $lang => $tr_id ) {
if ( $tr_id !== $object_id ) {
if ( $this->model->post->current_user_can_synchronize( $object_id ) ) {
$this->copy_object_terms( $object_id, $tr_id, $lang, $terms, $taxonomy, $append );
} else {
// No permission to synchronize, so let's synchronize in reverse order
$orig_lang = array_search( $object_id, $tr_ids );
$tr_terms = get_the_terms( $tr_id, $taxonomy );
if ( false === $tr_terms ) {
$tr_terms = array();
}
if ( is_string( $orig_lang ) && is_array( $tr_terms ) ) {
$tr_terms = wp_list_pluck( $tr_terms, 'term_id' );
$this->copy_object_terms( $tr_id, $object_id, $orig_lang, $tr_terms, $taxonomy, $append );
}
break;
}
}
}
$avoid_recursion = false;
}
}
/**
* Copy terms from one post to a translation, does not sync
*
* @since 2.3
*
* @param int $from Id of the source post
* @param int $to Id of the target post
* @param string $lang Language slug
* @return void
*/
public function copy( $from, $to, $lang ) {
remove_action( 'set_object_terms', array( $this, 'set_object_terms' ) );
// Get taxonomies to sync for this post type
$taxonomies = array_intersect( get_post_taxonomies( $from ), $this->get_taxonomies_to_copy( false, $from, $to, $lang ) );
// Update the term cache to reduce the number of queries in the loop
update_object_term_cache( array( $from ), get_post_type( $from ) );
// Copy
foreach ( $taxonomies as $tax ) {
if ( $terms = get_the_terms( $from, $tax ) ) {
$terms = array_map( 'intval', wp_list_pluck( $terms, 'term_id' ) );
$newterms = $this->maybe_translate_terms( $from, $terms, $tax, $lang );
if ( ! empty( $newterms ) ) {
wp_set_object_terms( $to, $newterms, $tax );
}
}
}
add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 5 );
}
/**
* When creating a new term, associate it to posts having translations associated to the translated terms.
*
* @since 2.3
*
* @param int $term_id Id of the created term.
* @param string $taxonomy Taxonomy.
* @param int[] $translations Ids of the translations of the created term.
* @return void
*/
public function create_term( $term_id, $taxonomy, $translations ) {
if ( doing_action( 'create_term' ) && in_array( $taxonomy, $this->get_taxonomies_to_copy( true ) ) ) {
// Get all posts associated to the translated terms
$tr_posts = get_posts(
array(
'numberposts' => -1,
'nopaging' => true,
'post_type' => 'any',
'post_status' => 'any',
'fields' => 'ids',
'tax_query' => array(
array(
'taxonomy' => $taxonomy,
'field' => 'id',
'terms' => array_merge( array( $term_id ), array_values( $translations ) ),
'include_children' => false,
),
),
)
);
$lang = $this->model->term->get_language( $term_id ); // Language of the created term
$posts = array();
foreach ( $tr_posts as $post_id ) {
$post = $this->model->post->get_translation( $post_id, $lang );
if ( $post ) {
$posts[] = $post;
}
}
$posts = array_unique( $posts );
foreach ( $posts as $post_id ) {
if ( current_user_can( 'assign_term', $term_id ) ) {
wp_set_object_terms( $post_id, $term_id, $taxonomy, true );
}
}
}
}
/**
* Deactivate the synchronization of terms before deleting a term
* to avoid translated terms to be removed from translated posts
*
* @since 2.3.2
*
* @return void
*/
public function pre_delete_term() {
remove_action( 'set_object_terms', array( $this, 'set_object_terms' ) );
}
/**
* Re-activate the synchronization of terms after a term is deleted
*
* @since 2.3.2
*
* @return void
*/
public function delete_term() {
add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 5 );
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @package Polylang
*/
/**
* A class to manage the copy and synchronization of term metas.
*
* @since 2.3
*/
class PLL_Sync_Term_Metas extends PLL_Sync_Metas {
/**
* Constructor.
*
* @since 2.3
*
* @param PLL_Base $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->meta_type = 'term';
parent::__construct( $polylang );
}
}

View File

@@ -0,0 +1,279 @@
<?php
/**
* @package Polylang
*/
/**
* Manages copy and synchronization of terms and post metas on front
*
* @since 2.4
*/
class PLL_Sync {
/**
* @var PLL_Sync_Tax
*/
public $taxonomies;
/**
* @var PLL_Sync_Post_Metas
*/
public $post_metas;
/**
* @var PLL_Sync_Term_Metas
*/
public $term_metas;
/**
* Stores the plugin options.
*
* @var \WP_Syntex\Polylang\Options\Options
*/
protected $options;
/**
* @var PLL_Model
*/
protected $model;
/**
* Constructor
*
* @since 1.2
*
* @param PLL_Base $polylang The Polylang object.
* @param-out PLL_Base $polylang
*/
public function __construct( PLL_Base &$polylang ) {
$this->model = &$polylang->model;
$this->options = $polylang->options;
$this->taxonomies = new PLL_Sync_Tax( $polylang );
$this->post_metas = new PLL_Sync_Post_Metas( $polylang );
$this->term_metas = new PLL_Sync_Term_Metas( $polylang );
add_filter( 'wp_insert_post_parent', array( $this, 'can_sync_post_parent' ), 10, 3 );
add_filter( 'wp_insert_post_data', array( $this, 'can_sync_post_data' ), 10, 2 );
add_action( 'pll_save_post', array( $this, 'pll_save_post' ), 10, 3 );
add_action( 'created_term', array( $this, 'sync_term_parent' ), 10, 3 );
add_action( 'edited_term', array( $this, 'sync_term_parent' ), 10, 3 );
add_action( 'pll_duplicate_term', array( $this->term_metas, 'copy' ), 10, 3 );
if ( $this->options['media_support'] ) {
add_action( 'pll_translate_media', array( $this->taxonomies, 'copy' ), 10, 3 );
add_action( 'pll_translate_media', array( $this->post_metas, 'copy' ), 10, 3 );
add_action( 'edit_attachment', array( $this, 'edit_attachment' ) );
}
add_filter( 'pre_update_option_sticky_posts', array( $this, 'sync_sticky_posts' ), 10, 2 );
}
/**
* Get post fields to synchronize.
*
* @since 2.4
*
* @param WP_Post $post Post object.
* @return array
*/
protected function get_fields_to_sync( $post ) {
$postarr = array();
foreach ( array( 'comment_status', 'ping_status', 'menu_order' ) as $property ) {
if ( in_array( $property, $this->options['sync'] ) ) {
$postarr[ $property ] = $post->$property;
}
}
if ( in_array( 'post_date', $this->options['sync'] ) ) {
$postarr['post_date'] = $post->post_date;
$postarr['post_date_gmt'] = $post->post_date_gmt;
}
if ( in_array( 'post_parent', $this->options['sync'] ) ) {
$postarr['post_parent'] = wp_get_post_parent_id( $post->ID );
}
return $postarr;
}
/**
* Prevents synchronized post parent modification if the current user hasn't enough rights
*
* @since 2.6
*
* @param int $post_parent Post parent ID
* @param int $post_id Post ID, unused
* @param array $postarr Array of parsed post data
* @return int
*/
public function can_sync_post_parent( $post_parent, $post_id, $postarr ) {
if ( ! empty( $postarr['ID'] ) && ! $this->model->post->current_user_can_synchronize( $postarr['ID'] ) ) {
$tr_ids = $this->model->post->get_translations( $postarr['ID'] );
foreach ( $tr_ids as $tr_id ) {
if ( $tr_id !== $postarr['ID'] && $post = get_post( $tr_id ) ) {
$post_parent = $post->post_parent;
break;
}
}
}
return $post_parent;
}
/**
* Prevents synchronized post data modification if the current user hasn't enough rights
*
* @since 2.6
*
* @param array $data An array of slashed post data.
* @param array $postarr An array of sanitized, but otherwise unmodified post data.
* @return array
*/
public function can_sync_post_data( $data, $postarr ) {
if ( ! empty( $postarr['ID'] ) && ! $this->model->post->current_user_can_synchronize( $postarr['ID'] ) ) {
foreach ( $this->model->post->get_translations( $postarr['ID'] ) as $tr_id ) {
if ( $tr_id !== $postarr['ID'] && $post = get_post( $tr_id ) ) {
$to_sync = $this->get_fields_to_sync( $post );
$data = array_merge( $data, $to_sync );
break;
}
}
}
return $data;
}
/**
* Synchronizes post fields in translations.
*
* @since 2.4
*
* @param int $post_id Post id.
* @param WP_Post $post Post object.
* @param int[] $translations Post translations.
* @return void
*/
public function pll_save_post( $post_id, $post, $translations ) {
global $wpdb;
if ( $this->model->post->current_user_can_synchronize( $post_id ) ) {
$postarr = $this->get_fields_to_sync( $post );
if ( ! empty( $postarr ) ) {
foreach ( $translations as $lang => $tr_id ) {
if ( ! $tr_id || $tr_id === $post_id ) {
continue;
}
$tr_arr = $postarr;
unset( $tr_arr['post_parent'] );
// Do not update the translation parent if the user set a parent with no translation.
if ( isset( $postarr['post_parent'] ) ) {
$post_parent = $postarr['post_parent'] ? $this->model->post->get_translation( $postarr['post_parent'], $lang ) : 0;
if ( ! ( $postarr['post_parent'] && ! $post_parent ) ) {
$tr_arr['post_parent'] = $post_parent;
}
}
// Update all the rows at once.
if ( ! empty( $tr_arr ) ) {
// Don't use wp_update_post to avoid infinite loop.
$wpdb->update( $wpdb->posts, $tr_arr, array( 'ID' => $tr_id ) );
clean_post_cache( $tr_id );
}
}
}
}
}
/**
* Synchronize term parent in translations.
* Calling clean_term_cache *after* this is mandatory otherwise the $taxonomy_children option is not correctly updated
*
* @since 2.3
*
* @param int $term_id Term id.
* @param int $tt_id Term taxonomy id, not used.
* @param string $taxonomy Taxonomy name.
* @return void
*/
public function sync_term_parent( $term_id, $tt_id, $taxonomy ) {
global $wpdb;
if ( ! is_taxonomy_hierarchical( $taxonomy ) || ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
return;
}
$term = get_term( $term_id );
if ( ! $term instanceof WP_Term ) {
return;
}
$translations = $this->model->term->get_translations( $term_id );
foreach ( $translations as $lang => $tr_id ) {
if ( $tr_id === $term_id ) {
continue;
}
$tr_parent = $this->model->term->get_translation( $term->parent, $lang );
$tr_term = get_term( (int) $tr_id, $taxonomy );
if ( str_starts_with( (string) current_filter(), 'created_' ) && 0 === $tr_parent ) {
// Do not remove the existing hierarchy of translations when creating new term without parent.
continue;
}
if ( $tr_term instanceof WP_Term && ! ( $term->parent && empty( $tr_parent ) ) ) {
$wpdb->update(
$wpdb->term_taxonomy,
array( 'parent' => $tr_parent ?: 0 ),
array( 'term_taxonomy_id' => $tr_term->term_taxonomy_id )
);
clean_term_cache( $tr_id, $taxonomy ); // OK since WP 3.9.
}
}
}
/**
* Synchronizes terms and metas in translations for media
*
* @since 1.8
*
* @param int $post_id post id
* @return void
*/
public function edit_attachment( $post_id ) {
$this->pll_save_post( $post_id, get_post( $post_id ), $this->model->post->get_translations( $post_id ) );
}
/**
* Synchronize sticky posts.
*
* @since 2.3
*
* @param int[] $value New option value.
* @param int[] $old_value Old option value.
* @return int[]
*/
public function sync_sticky_posts( $value, $old_value ) {
if ( in_array( 'sticky_posts', $this->options['sync'] ) ) {
// Stick post
if ( $sticked = array_diff( $value, $old_value ) ) {
$translations = $this->model->post->get_translations( reset( $sticked ) );
$value = array_unique( array_merge( $value, array_values( $translations ) ) );
}
// Unstick post
if ( $unsticked = array_diff( $old_value, $value ) ) {
$translations = $this->model->post->get_translations( reset( $unsticked ) );
$value = array_unique( array_diff( $value, array_values( $translations ) ) );
}
}
return $value;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Loads the settings module for translated slugs.
*
* @package Polylang
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly
}
if ( $polylang->model->has_languages() ) {
add_filter(
'pll_settings_modules',
function ( $modules ) {
$modules[] = 'PLL_Settings_Preview_Translate_Slugs';
return $modules;
}
);
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* @package Polylang
*/
/**
* Class to advertize the Translate slugs module.
*
* @since 1.9
* @since 3.1 Renamed from PLL_Settings_Translate_Slugs.
*/
class PLL_Settings_Preview_Translate_Slugs extends PLL_Settings_Module {
/**
* Stores the display order priority.
*
* @var int
*/
public $priority = 80;
/**
* Constructor.
*
* @since 1.9
*
* @param PLL_Settings $polylang Polylang object.
* @param array $args Optional. Addition arguments.
*
* @phpstan-param array{
* module?: non-falsy-string,
* title?: string,
* description?: string,
* active_option?: non-falsy-string
* } $args
*/
public function __construct( &$polylang, array $args = array() ) {
$default = array(
'module' => 'translate-slugs',
'title' => __( 'Translate slugs', 'polylang' ),
'description' => $this->get_description(),
'active_option' => 'preview',
);
parent::__construct( $polylang, array_merge( $default, $args ) );
}
/**
* Returns the module description.
*
* @since 3.1
*
* @return string
*/
protected function get_description() {
return __( 'Allows to translate custom post types and taxonomies slugs in URLs.', 'polylang' );
}
}

View File

@@ -0,0 +1,952 @@
@charset "UTF-8";
body {
margin: 65px auto 24px;
box-shadow: none;
background: #f1f1f1;
padding: 0;
border: 0; /* fix-pro #856 override WP install.css */
}
#pll-logo {
border: 0;
margin: 0 0 24px;
padding: 0;
text-align: center;
font-family: sans-serif;
font-size: 64px;
text-transform: uppercase;
color: #000;
line-height: normal;
}
#pll-logo a {
display: flex;
justify-content: center;
color: #000;
text-decoration: none;
}
#pll-logo img {
max-width: 100%;
margin-right: 16px;
}
.rtl #pll-logo img {
margin-right: 0;
margin-left: 16px;
}
.pll-wizard-footer {
text-align: center
}
.pll-wizard .select2-container {
text-align: left;
width: auto
}
.pll-wizard .hidden {
display: none
}
.pll-wizard-content {
box-shadow: 0 1px 3px rgba(0, 0, 0, .13);
padding: 2em;
margin: 0 0 20px;
background: #fff;
overflow: hidden;
zoom: 1;
text-align: left;
}
.rtl .pll-wizard-content{
text-align: right;
}
.pll-wizard-content h1,
.pll-wizard-content h2,
.pll-wizard-content h3,
.pll-wizard-content table {
margin: 0 0 20px;
border: 0;
padding: 0;
color: #666;
clear: none;
font-weight: 500
}
.pll-wizard-content p {
margin: 20px 0;
font-size: 1em;
line-height: 1.75em;
color: #666
}
.pll-wizard-content table {
font-size: 1em;
line-height: 1.75em;
color: #666;
width: 100%;
margin-top: 20px;
}
.pll-wizard-content table td span{
display: inline-block;
}
.pll-wizard-content table caption {
caption-side: bottom;
font-style: italic;
text-align: right;
}
.rtl .pll-wizard-content table caption {
text-align: left;
}
.pll-wizard-content table caption .icon-default-lang{
font-style: normal;
}
.pll-wizard-content a {
color: #a03f3f;
}
.pll-wizard-content a:focus,
.pll-wizard-content a:hover,
.pll-wizard-footer-links:hover {
color: #dd5454
}
.pll-wizard-content .pll-wizard-next-steps {
overflow: hidden;
margin: 0 0 24px;
padding-bottom: 2px
}
.pll-wizard-content .pll-wizard-next-steps h2 {
margin-bottom: 12px
}
.pll-wizard-content .pll-wizard-next-steps .pll-wizard-next-steps-first {
float: left;
width: 50%;
box-sizing: border-box
}
.pll-wizard-content .pll-wizard-next-steps .pll-wizard-next-steps-last {
float: right;
width: 50%;
box-sizing: border-box
}
.pll-wizard-content .pll-wizard-next-steps ul {
padding: 0 2em 0 0;
list-style: none outside;
margin: 0
}
.pll-wizard-content .pll-wizard-next-steps ul li a {
display: block;
padding: 0 0 .75em
}
.pll-wizard-content .pll-wizard-next-steps ul li a::before {
color: #82878c;
font: normal 20px/1 dashicons;
speak: none;
display: inline-block;
padding: 0 10px 0 0;
top: 1px;
position: relative;
text-decoration: none!important;
vertical-align: top
}
.pll-wizard-steps {
padding: 0 0 24px;
margin: 0;
list-style: none outside;
overflow: hidden;
color: #ccc;
width: 100%;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: inline-flex
}
.pll-wizard-steps li {
width: 100%;
float: left;
padding: 0 0 .8em;
margin: 0;
text-align: center;
position: relative;
border-bottom: 4px solid #ccc;
line-height: 1.4em
}
.pll-wizard-steps li a {
color: #a03f3f;
text-decoration: none;
padding: 1.5em;
margin: -1.5em;
position: relative;
z-index: 1
}
.pll-wizard-steps li a:focus,
.pll-wizard-steps li a:hover {
color: #dd5454;
text-decoration: underline
}
.pll-wizard-steps li::before {
content: "";
border: 4px solid #ccc;
border-radius: 100%;
width: 4px;
height: 4px;
position: absolute;
bottom: 0;
left: 50%;
margin-left: -6px;
margin-bottom: -8px;
background: #fff
}
.pll-wizard-steps li.active {
border-color: #a03f3f;
color: #a03f3f;
font-weight: 700
}
.pll-wizard-steps li.active::before {
border-color: #a03f3f
}
.pll-wizard-steps li.done {
border-color: #a03f3f;
color: #a03f3f
}
.pll-wizard-steps li.done::before {
border-color: #a03f3f;
background: #a03f3f
}
.pll-wizard .pll-wizard-actions {
overflow: hidden;
margin: 20px 0 0;
position: relative
}
.pll-wizard .pll-wizard-actions .button {
font-size: 16px;
font-weight: 300;
padding: 1em 2em;
line-height: 1em;
margin-right: .5em;
margin-bottom: 2px;
margin-top: 10px;
height: auto;
border-radius: 4px;
box-shadow: none;
min-width: auto;
border-color: #a03f3f;
color: #a03f3f;
}
.pll-wizard .pll-wizard-content .button {
border-color: #a03f3f;
color: #a03f3f;
}
.pll-wizard .pll-wizard-content .button-primary,
.pll-wizard .pll-wizard-actions .button-primary {
background-color: #a03f3f;
border-color: #a03f3f;
color: #fff;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 0 #a03f3f;
text-shadow: 0 -1px 1px #a03f3f, 1px 0 1px #a03f3f, 0 1px 1px #a03f3f, -1px 0 1px #a03f3f;
margin: 0;
opacity: 1
}
.pll-wizard .pll-wizard-content .button-small .dashicons {
font-size: 15px;
height: auto;
vertical-align: middle;
}
.pll-wizard .button-primary:active,
.pll-wizard .button-primary:focus,
.pll-wizard input[type="checkbox"]:focus + label.button-primary,
.pll-wizard .button-primary:hover {
background: #dd5454;
border-color: #dd5454;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 0 #dd5454
}
.pll-wizard .pll-wizard-actions .button-primary[disabled],
.pll-wizard .pll-wizard-actions .button-primary:disabled,
.pll-wizard .pll-wizard-actions .button-primary.disabled {
cursor: wait;
background-color: #bb5454 !important;
border-color: #bb5454 !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 0 #bb5454 !important;
text-shadow: 0 -1px 1px #bb5454, 1px 0 1px #bb5454, 0 1px 1px #bb5454, -1px 0 1px #bb5454 !important;
color: #ffa3a3 !important;
}
.pll-wizard-content p:last-child {
margin-bottom: 0
}
.pll-wizard-footer-links {
font-size: .85em;
color: #7b7b7b;
margin: 1.18em auto;
display: inline-block;
text-align: center
}
.pll-wizard-services {
border: 1px solid #eee;
padding: 0;
margin: 0 0 1em;
list-style: none outside;
border-radius: 4px;
overflow: hidden
}
.pll-wizard-services p {
margin: 0 0 1em 0;
padding: 0;
font-size: 1em;
line-height: 1.5em
}
.pll-wizard-service-item {
display: -webkit-box;
display: -webkit-flex;
display: flex;
-webkit-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
justify-content: space-between;
padding: 0;
border-bottom: 1px solid #eee;
color: #666;
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center
}
.media-step .pll-wizard-service-item{
border: 0;
}
.media-step .pll-wizard-service-item:last-child{
display: block;
}
.media-step .pll-wizard-service-item .pll-wizard-service-enable{
padding-bottom: 0;
}
.pll-wizard-service-item:last-child {
border-bottom: 0
}
.pll-wizard-service-item .pll-wizard-service-name {
-webkit-flex-basis: 0;
flex-basis: 0;
min-width: 160px;
text-align: center;
font-weight: 700;
padding: 2em 0;
-webkit-align-self: stretch;
align-self: stretch;
display: -webkit-box;
display: -webkit-flex;
display: flex;
-webkit-box-align: baseline;
-webkit-align-items: baseline;
align-items: baseline
}
.pll-wizard-service-item .pll-wizard-service-name img {
max-width: 75px
}
.pll-wizard-service-item .pll-wizard-service-description {
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
flex-grow: 1;
padding: 20px
}
.pll-wizard-service-item .pll-wizard-service-example {
padding: 0 20px 20px
}
.pll-wizard-service-item .pll-wizard-service-example p{
text-align: right;
}
.rtl .pll-wizard-service-item .pll-wizard-service-example p{
text-align: left;
}
.pll-wizard-service-item .pll-wizard-service-description p {
margin-bottom: 1em
}
.pll-wizard-service-item .pll-wizard-service-description p:last-child {
margin-bottom: 0
}
.pll-wizard-service-item .pll-wizard-service-description .pll-wizard-service-settings-description {
display: block;
font-style: italic;
color: #999
}
.pll-wizard-service-item .pll-wizard-service-enable {
-webkit-flex-basis: 0;
flex-basis: 0;
min-width: 75px;
text-align: center;
cursor: pointer;
padding: 2em 0;
position: relative;
max-height: 1.5em;
-webkit-align-self: flex-start;
align-self: flex-start;
-webkit-box-ordinal-group: 4;
-webkit-order: 3;
order: 3
}
.pll-wizard-service-item .pll-wizard-service-toggle {
position: relative
}
.pll-wizard-service-item .pll-wizard-service-toggle input[type=checkbox] {
position:absolute;
opacity: 0;
}
.pll-wizard-service-item .pll-wizard-service-toggle input[type=checkbox] + label {
position: relative;
display: inline-block;
width: 44px;
height: 20px;
border-radius: 10em;
cursor: pointer;
text-indent: -9999px;
}
.pll-wizard-service-item .pll-wizard-service-toggle input[type=checkbox]:focus + label {
border:1px dashed #777;
}
.pll-wizard-service-item .pll-wizard-service-toggle input[type=checkbox] + label::before,
.pll-wizard-service-item .pll-wizard-service-toggle input[type=checkbox] + label::after {
content: '';
position: absolute;
}
.pll-wizard-service-item .pll-wizard-service-toggle input[type=checkbox] + label::before {
left: 0;
top: 0;
width: 44px;
height: 20px;
background: #ddd;
border-radius: 10em;
transition: background-color .2s;
}
.pll-wizard-service-item .pll-wizard-service-toggle input[type=checkbox] + label::after {
width: 16px;
height: 16px;
transition: all .2s;
border-radius: 50%;
background: #fff;
margin: 2px;
top: 0;
left: 0;
}
.pll-wizard-service-item .pll-wizard-service-toggle input[type=checkbox]:checked + label::before {
background:#a03f3f;
}
.pll-wizard-service-item .pll-wizard-service-toggle input[type=checkbox]:checked + label::after {
right: 0;
left:auto;
}
.pll-wizard-service-item .pll-wizard-service-settings {
display: none;
margin-top: .75em;
margin-bottom: 0;
cursor: default
}
.pll-wizard-service-item .pll-wizard-service-settings.hide {
display: none
}
.pll-wizard-service-item.checked .pll-wizard-service-settings {
display: inline-block
}
.pll-wizard-service-item.checked .pll-wizard-service-settings.hide {
display: none
}
.pll-wizard-service-item.closed {
border-bottom: 0
}
.step {
text-align: center
}
.pll-wizard .button .dashicons{
vertical-align: middle;
line-height: 1;
}
.rtl .dashicons-arrow-right-alt2:before {
content: "\f341";
}
.pll-wizard .pll-wizard-actions .button:active,
.pll-wizard .pll-wizard-actions .button:focus,
.pll-wizard .pll-wizard-actions .button:hover {
box-shadow: none
}
.pll-wizard-next-steps {
border: 1px solid #eee;
border-radius: 4px;
list-style: none;
padding: 0
}
.pll-wizard-next-steps li {
padding: 0
}
.pll-wizard-next-steps .pll-wizard-next-step-item {
display: -webkit-box;
display: -webkit-flex;
display: flex;
border-top: 1px solid #eee
}
.pll-wizard-next-steps .pll-wizard-next-step-item.no-border,
.pll-wizard-next-steps .pll-wizard-next-step-item:first-child {
border-top: 0
}
.pll-wizard-next-steps .pll-wizard-next-step-description {
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
flex-grow: 1;
margin: 1.5em
}
.pll-wizard-next-steps .pll-wizard-next-step-action {
-webkit-box-flex: 0;
-webkit-flex-grow: 0;
flex-grow: 0;
display: -webkit-box;
display: -webkit-flex;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center
}
.pll-wizard-next-steps .pll-wizard-next-step-action .button {
margin: 1em 1.5em
}
.pll-wizard-next-steps .pll-wizard-next-step-item.no-border .pll-wizard-next-step-description,
.pll-wizard-next-steps .pll-wizard-next-step-item.no-border .pll-wizard-actions,
.pll-wizard-next-steps .pll-wizard-next-step-item.no-border .pll-wizard-next-step-action .button{
margin-top: 0;
}
.pll-wizard-next-steps p.next-step-heading {
margin: 0;
font-size: .95em;
font-weight: 400;
font-variant: all-petite-caps
}
.pll-wizard-next-steps p.next-step-extra-info {
margin: 0
}
.pll-wizard-next-steps h3.next-step-description {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.pll-wizard-next-steps .pll-wizard-additional-steps {
border-top: 1px solid #eee;
}
.pll-wizard-next-steps .pll-wizard-additional-steps .pll-wizard-next-step-description {
margin-bottom: 0
}
.pll-wizard-next-steps .pll-wizard-additional-steps .pll-wizard-actions {
margin: 0 0 1.5em 0;
}
.pll-wizard-next-steps .pll-wizard-additional-steps .pll-wizard-actions .button {
font-size: 15px;
margin: 1em 0 1em 1.5em;
}
.rtl .pll-wizard-next-steps .pll-wizard-additional-steps .pll-wizard-actions .button {
margin: 1em 1.5em 1em 0;
}
.pll-wizard-next-steps .pll-wizard-additional-steps .pll-wizard-actions .button::last-child {
margin-right: 1.5em;
}
.pll-wizard-content img{
max-width: 100%;
margin-right: 0.5em;
}
.rtl .pll-wizard-content img{
margin-left: 0.5em;
}
.pll-wizard-content .form-field label{
margin-bottom: 5px;
display: block;
}
.pll-wizard-content .form-field select{
padding: 3px;
}
.pll-wizard-content .languages-step select,
.pll-wizard-content .untranslated-contents-step select{
width: 100%;
}
.languages-step .form-field .button{
margin-left: 15px;
}
.languages-step .form-field .button > span{
margin-right: 0.3em;
}
.rtl .languages-step .form-field .button{
margin-left: 0;
margin-right: 15px;
}
.rtl .languages-step .form-field .button > span{
margin-left: 0.3em;
margin-right: 0;
}
.pll-wizard-content .languages-step .select-language-field{
display: flex;
}
.pll-wizard-content #languages{
display: none;
}
.pll-wizard-content #languages tr th:first-child{
width: 80%;
}
.pll-wizard-content #languages .dashicons{
color: #a03f3f;
}
.pll-wizard-content #languages img{
margin-right: 5px;
}
.pll-wizard-content .error{
color: #a03f3f;
font-weight: bold;
}
.pll-wizard-content #messages .error{
background: #fccfcf;
padding: 0.5rem;
border: 1px solid #a03f3f;
margin-bottom: 0.5rem;
}
.pll-wizard-content #slide-toggle{
position:absolute;
opacity: 0;
}
.pll-wizard-content #slide-toggle + label{
position:relative;
}
.pll-wizard-content #slide-toggle + label + span{
display: block;
}
.pll-wizard-content #slide-toggle + label .dashicons{
margin-right: 0.3em;
}
.rtl .pll-wizard-content #slide-toggle + label .dashicons{
margin-left: 0.3em;
margin-right: 0;
}
.pll-wizard-content #slide-toggle ~ #screenshot > img {
max-height: 500px;
margin-top: 10px;
-webkit-transition: all .5s cubic-bezier(0, 1, 0.5, 1);
transition: all .5s cubic-bezier(0, 1, 0.5, 1);
}
.pll-wizard-content #slide-toggle:checked ~ #screenshot > img {
max-height: 0;
}
.hide {
display: none;
}
input[type="text"].field-in-error,
input[type="password"].field-in-error,
input[type="checkbox"].field-in-error,
input[type="color"].field-in-error,
input[type="date"].field-in-error,
input[type="datetime"].field-in-error,
input[type="datetime-local"].field-in-error,
input[type="email"].field-in-error,
input[type="month"].field-in-error,
input[type="number"].field-in-error,
input[type="search"].field-in-error,
input[type="radio"].field-in-error,
input[type="tel"].field-in-error,
input[type="text"].field-in-error,
input[type="time"].field-in-error,
input[type="url"].field-in-error,
input[type="week"].field-in-error,
select.field-in-error,
textarea.field-in-error,
span.field-in-error,
.field-in-error{
border-color: #a03f3f;
}
input[type="text"].field-in-error:focus,
input[type="password"].field-in-error:focus,
input[type="checkbox"].field-in-error:focus,
input[type="color"].field-in-error:focus,
input[type="date"].field-in-error:focus,
input[type="datetime"].field-in-error:focus,
input[type="datetime-local"].field-in-error:focus,
input[type="email"].field-in-error:focus,
input[type="month"].field-in-error:focus,
input[type="number"].field-in-error:focus,
input[type="search"].field-in-error:focus,
input[type="radio"].field-in-error:focus,
input[type="tel"].field-in-error:focus,
input[type="text"].field-in-error:focus,
input[type="time"].field-in-error:focus,
input[type="url"].field-in-error:focus,
input[type="week"].field-in-error:focus,
select.field-in-error:focus,
textarea.field-in-error:focus,
span.field-in-error:focus,
.field-in-error:focus{
border: 1px solid #a03f3f;
box-shadow: 0 0 2px rgba(160, 63, 63, 0.8);
outline-color: #a03f3f;
outline-style: auto;
outline-width: thin;
}
/* override install styles by returning back to forms styles */
.form-table input.regular-text{
width: 25em;
}
.form-table input.field-in-error{
border-color: #a03f3f;
}
#pll-licenses-table td{
padding: 10px 9px;
}
#pll-licenses-table .license-valid td p{
min-width: 35em;
}
#pll-licenses-table .pll-deactivate-license{
margin: 0 0 0 20px;
}
.rtl #pll-licenses-table .pll-deactivate-license{
margin: 0 10px 0 0;
}
.pll-wizard-content .documentation {
padding: 24px 24px 0;
margin: 0 0 24px;
overflow: hidden;
background: #f5f5f5
}
.pll-wizard-content .documentation p {
padding: 0;
margin: 0 0 12px;
}
.documentation-container {
display: -webkit-box;
display: -webkit-flex;
display: flex;
justify-content: flex-end;
}
.documentation-container .documentation-button-container {
-webkit-box-flex: 0;
-webkit-flex-grow: 0;
flex-grow: 0;
}
.wc-setup .wc-setup-actions .button.documentation-button {
height: 42px;
padding: 0 1em;
margin: 0;
}
#dialog{
display: none;
}
.pll-wizard .ui-dialog.ui-widget-content{
max-height: none;
}
.pll-wizard .ui-dialog-title::before{
content: "\f534";
font-family: dashicons;
display: inline-block;
line-height: 1;
font-weight: 400;
font-style: normal;
speak: none;
text-decoration: inherit;
text-transform: none;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
width: 20px;
height: 20px;
font-size: 20px;
vertical-align: middle;
text-align: center;
margin: 0 5px 5px 0;
transition: color 0.1s ease-in;
}
.rtl.pll-wizard .ui-dialog-title::before{
margin-right: 0;
margin-left: 5px;
}
.pll-wizard .ui-dialog ul{
list-style: disc;
padding-left: 20px;
}
.rtl.pll-wizard .ui-dialog ul{
padding-left: 0;
padding-right: 20px;
}
.pll-wizard li{
margin-bottom: 0;
}
#translations{
border-collapse: collapse;
}
#translations tbody:nth-child(odd){
background-color: #f9f9f9;
}
#translations.striped > tbody > :nth-child(odd) {
background-color: transparent; /* Override common WordPress style */
}
.pll-wizard-content mark{
background: transparent none;
}
.pll-wizard-content mark{
color: #7ad03a;
}
@media screen and (max-width: 782px) {
/* Override WordPress button css rules */
.languages-step .form-field .button{
font-size: 13px;
line-height: 26px;
height: 28px;
padding: 0 10px 1px;
vertical-align: top;
}
#pll-licenses-table .pll-deactivate-license{
margin: 10px 0 5px;
}
}
@media only screen and (max-width:620px) {
/* Override dialog width rule */
.ui-dialog{
width: 100% !important;
}
}
@media only screen and (max-width:500px) {
#pll-logo a,
.select-language-field{
flex-direction: column;
}
.select-language-field .action-buttons{
display: flex;
justify-content: flex-end;
}
.languages-step .form-field .button{
margin: 5px 0 0;
}
}
@media only screen and (max-width:400px) {
#pll-logo {
font-size: 56px;
}
.pll-wizard-steps {
display: none
}
.pll-wizard-service-item {
-webkit-flex-wrap: wrap;
flex-wrap: wrap
}
.pll-wizard-service-item .pll-wizard-service-enable {
-webkit-box-ordinal-group: 3;
-webkit-order: 2;
order: 2;
padding: 20px 0 0
}
.pll-wizard-service-item .pll-wizard-service-description {
-webkit-box-ordinal-group: 4;
-webkit-order: 3;
order: 3
}
.pll-wizard-service-item .pll-wizard-service-name {
padding: 20px 20px 0;
text-align: left;
-webkit-box-pack: justify!important;
-webkit-justify-content: space-between!important;
justify-content: space-between!important
}
.pll-wizard-service-item .pll-wizard-service-name img {
margin: 0
}
.pll-wizard-next-steps .pll-wizard-next-step-item {
-webkit-flex-wrap: wrap;
flex-wrap: wrap
}
.pll-wizard-next-steps .pll-wizard-next-step-item .pll-wizard-next-step-description {
margin-bottom: 0
}
.pll-wizard-next-steps .pll-wizard-next-step-item .pll-wizard-next-step-action p {
margin: 0
}
}
@media only screen and (max-width:360px) {
#pll-logo {
font-size: 48px;
}
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* Displays the wizard notice content
*
* @package Polylang
*
* @since 2.7
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly.
}
$wizard_url = add_query_arg(
array(
'page' => 'mlang_wizard',
),
admin_url( 'admin.php' )
);
?>
<p>
<strong>
<?php
printf(
/* translators: %s is the plugin name */
esc_html__( 'Welcome to %s', 'polylang' ),
esc_html( POLYLANG )
);
?>
</strong>
<?php
echo ' &#8211; ';
esc_html_e( 'You&lsquo;re almost ready to translate your contents!', 'polylang' );
?>
</p>
<p class="buttons">
<a
href="<?php echo esc_url( $wizard_url ); ?>"
class="button button-primary"
>
<?php esc_html_e( 'Run the Setup Wizard', 'polylang' ); ?>
</a>
<a
class="button button-secondary skip"
href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'pll-hide-notice', 'wizard' ), 'wizard', '_pll_notice_nonce' ) ); ?>"
>
<?php esc_html_e( 'Skip setup', 'polylang' ); ?>
</a>
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

View File

@@ -0,0 +1,302 @@
/**
* @package Polylang
*/
jQuery(
function ( $ ) {
var addLanguageForm = $( '.languages-step' ); // Form element.
var languageFields = $( '#language-fields' ); // Element where to append hidden fields for creating language.
var languagesTable = $( '#languages' ); // Table element contains languages list to create.
var languagesListTable = $( '#languages tbody' ); // Table rows with languages list to create.
var definedLanguagesListTable = $( '#defined-languages tbody' ); // Table rows with already defined languages list.
var languagesList = $( '#lang_list' ); // Select form element with predefined languages without already created languages.
var nextStepButton = $( '[name="save_step"]' ); // The button for continuing to the next step.
var messagesContainer = $( '#messages' ); // Element where to display error messages.
var languagesMap = new Map(); // Languages map object for managing the languages to create.
var dialog = $( '#dialog' ); // Dialog box for alerting the language selected has not been added to the list.
/**
* Add a language in the list to create it in Polylang settings
*
* @param {object} language The language object
*/
function addLanguage( language ) {
// language properties come from the select dropdown which is built server side and well escaped.
// see template view-wizard-step-languages.php.
var languageValueHtml = $( '<td />' ).text( language.text ).prepend( language.flagUrl ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend
var languageTrashIconHtml = $( '<td />' )
.append( // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
$( '<span />' )
.addClass( 'dashicons dashicons-trash' )
.attr( 'data-language', language.locale )
.append( // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
$( '<span />' )
.addClass( 'screen-reader-text' )
.text( pll_wizard_params.i18n_remove_language_icon )
)
);
// see the comment and the hardcoded code above. languageTrashIconHtml and languageValueHtml are safe.
var languageLineHtml = $( '<tr />' ).prepend( languageTrashIconHtml ).prepend( languageValueHtml ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend
var languageFieldHtml = $( '<input />' ).attr(
{
type: 'hidden',
name: 'languages[]'
}
).val( language.locale );
languagesList.val( '' );
languagesList.selectmenu( 'refresh' ); // Refresh jQuery selectmenu widget after changing the value.
languagesMap.set( language.locale, language );
// see above how languageLineHtml is built.
languagesListTable.append( languageLineHtml ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
// Bind click event on trash icon.
languagesListTable.on(
'click',
'span[data-language=' + language.locale + ']',
function ( event ) {
event.preventDefault();
// Remove line in languages table.
$( this ).parents( 'tr' ).remove();
// Remove input field.
var languageField = languageFields.children( 'input[value=' + $( this ).data( 'language' ) + ']' ).remove();
// If there is no more languages hide languages table.
if ( languagesListTable.children().length <= 0 ) {
languagesTable.hide();
}
// Remove language from the Map.
languagesMap.delete( $( this ).data( 'language' ) );
// Hide error message.
hideError();
}
);
// see above how languageFieldHtml is built.
// Add hidden input field for posting the form.
languageFields.append( languageFieldHtml ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
}
/**
* Display an error message
*
* @param {string} message The message to display
*/
function showError( message ) {
messagesContainer.empty();
// html is hardcoded and use of jQuery text method which is safe to add message value.
// In addition message is i18n value which is initialized server side in PLL_Wizard::add_step_languages and correctly escaped.
messagesContainer.prepend( $( '<p/>' ).addClass( 'error' ).text( message ) ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend
}
/**
* Hide all error messages and fields in error
*/
function hideError() {
messagesContainer.empty();
addLanguageForm.find( '.error' ).removeClass( 'error field-in-error' );
}
/**
* Style the field to indicate where the error is
*
* @param {object} field The jQuery element which is in error
*/
function showFieldInError( field ) {
field.addClass( 'error field-in-error' );
}
/**
* Focus on a specific element
*
* @param {object} field The jQuery element which will be focused
*/
function focusOnField( field ) {
field.trigger( 'focus' );
}
/**
* Disable a specific button
*
* @param {object} button
*/
function disableButton( button ){
button.prop( 'disabled', true );
// Because the button is disabled we need to add the value of the button to ensure it will pass in the request.
addLanguageForm.append( // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
$( '<input />' ).prop(
{
type: 'hidden',
name: button.prop( 'name' ),
value: button.prop( 'value' )
}
)
);
}
/**
* Remove error when a new selection is done in languages list.
*/
languagesList.on(
'selectmenuchange',
function () {
hideError();;
}
);
/**
* Bind click event on "Add language" button
*/
$( '#add-language' ).on(
'click',
function ( event ) {
hideError();
var selectedOption = event.currentTarget.form.lang_list.options[event.currentTarget.form.lang_list.selectedIndex];
if ( '' !== selectedOption.value && ! languagesMap.has( selectedOption.value ) ) {
addLanguage(
{
locale: selectedOption.value,
text: selectedOption.innerText,
name: $( selectedOption ).data( 'language-name' ),
flagUrl: $( selectedOption ).data( 'flag-html' )
}
);
// Show table of languages.
languagesTable.show();
// Put back the focus on the select language field after clicking on "Add language button".
focusOnField( $( '#lang_list-button' ) );
} else {
var message = pll_wizard_params.i18n_no_language_selected;
if ( languagesMap.has( selectedOption.value ) ) {
message = pll_wizard_params.i18n_language_already_added;
}
showError( message );
showFieldInError( languagesList.next( 'span.ui-selectmenu-button' ) );
focusOnField( $( '#lang_list-button' ) );
}
}
);
/**
* Bind submit event on "add_lang" form
*/
addLanguageForm.on(
'submit',
function ( event ) {
// Verify if there is at least one language.
var isLanguagesAlreadyDefined = definedLanguagesListTable.children().length > 0;
var selectedLanguage = $( '#lang_list' ).val();
if ( languagesMap.size <= 0 && ! isLanguagesAlreadyDefined ) {
if ( '' === selectedLanguage ) {
showError( pll_wizard_params.i18n_no_language_added );
showFieldInError( languagesList.next( 'span.ui-selectmenu-button' ) );
focusOnField( $( '#lang_list-button' ) );
} else {
showError( pll_wizard_params.i18n_add_language_needed );
showFieldInError( languagesList.next( 'span.ui-selectmenu-button' ) );
focusOnField( $( '#add-language' ) ); // Put the focus on the "Add language" button.
}
return false;
}
// Verify if the language has been added in the list otherwise display a dialog box to confirm what to do.
if ( '' !== selectedLanguage ) {
// Verify we don't add a duplicate language before opening the dialog box otherwise display an error message.
if ( ! languagesMap.has( selectedLanguage ) ) {
dialog.dialog( 'open' );
} else {
showError( pll_wizard_params.i18n_language_already_added );
showFieldInError( languagesList.next( 'span.ui-selectmenu-button' ) );
focusOnField( $( '#lang_list-button' ) );
}
return false;
}
disableButton( nextStepButton );
}
);
// Is there an error return by PHP ?
var searchParams = new URLSearchParams( document.location.search );
if ( searchParams.has( 'activate_error' ) ) {
// If the error code exists, display it.
if ( undefined !== pll_wizard_params[ searchParams.get( 'activate_error' ) ] ) {
showError( pll_wizard_params[ searchParams.get( 'activate_error' ) ] );
}
}
function confirmDialog( what ) {
switch ( what ) {
case 'yes':
var selectedOption = $( '#lang_list' ).children( ':selected' );
addLanguage(
{
locale: selectedOption[0].value,
text: selectedOption[0].innerText,
name: $( selectedOption ).data( 'language-name' ),
flagUrl: $( selectedOption ).data( 'flag-html' )
}
);
break;
case 'no':
// Empty select form field and submit again the form.
languagesList.val( '' );
break;
case 'ignore':
}
dialog.dialog( 'close' );
if ( 'ignore' === what ) {
focusOnField( $( '#lang_list-button' ) );
} else {
addLanguageForm.submit();
}
}
// Initialize dialog box in the case a language is selected but not added in the list.
dialog.dialog(
{
autoOpen: false,
modal: true,
draggable: false,
resizable: false,
title: pll_wizard_params.i18n_dialog_title,
minWidth: 600,
maxWidth: '100%',
open: function ( event, ui ) {
// Change dialog box position for rtl language
if ( $( 'body' ).hasClass( 'rtl' ) ) {
$( this ).parent().css(
{
right: $( this ).parent().css( 'left' ),
left: 'auto'
}
);
}
// Display language name and flag information in dialog box.
$( this ).find( '#dialog-language' ).text( $( '#lang_list' ).children( ':selected' ).first().text() );
// language properties come from the select dropdown #lang_list which is built server side and well escaped.
// see template view-wizard-step-languages.php.
$( this ).find( '#dialog-language-flag' ).empty().prepend( $( '#lang_list' ).children( ':selected' ).data( 'flag-html' ) ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend
},
buttons: [
{
text: pll_wizard_params.i18n_dialog_yes_button,
click: function ( event ) {
confirmDialog( 'yes' );
}
},
{
text: pll_wizard_params.i18n_dialog_no_button,
click: function ( event ) {
confirmDialog( 'no' );
}
},
{
text: pll_wizard_params.i18n_dialog_ignore_button,
click: function ( event ) {
confirmDialog( 'ignore' );
}
}
]
}
)
}
);

View File

@@ -0,0 +1,14 @@
<?php
/**
* Loads the setup wizard.
*
* @package Polylang
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly
}
if ( $polylang instanceof PLL_Admin_Base ) {
$polylang->wizard = new PLL_Wizard( $polylang );
}

View File

@@ -0,0 +1,122 @@
<?php
/**
* Displays the wizard
*
* @package Polylang
*
* @since 2.7
*
* @var array[] $steps {
* List of steps.
*
* @type array {
* List of step properties.
*
* @type string $name I18n string which names the step.
* @type callable $view The callback function use to display the step content.
* @type callable $handler The callback function use to process the step after it is submitted.
* @type array $scripts List of script handles needed by the step.
* @type array $styles The list of style handles needed by the step.
* }
* }
* @var string $current_step Current step.
* @var string[] $styles List of wizard page styles.
*/
defined( 'ABSPATH' ) || exit;
$admin_body_class = array( 'pll-wizard', 'wp-core-ui' );
if ( is_rtl() ) {
$admin_body_class[] = 'rtl';
}
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>
<?php
printf(
/* translators: %s is the plugin name */
esc_html__( '%s &rsaquo; Setup', 'polylang' ),
esc_html( POLYLANG )
);
?>
</title>
<script>
var ajaxurl = '<?php echo esc_url( admin_url( 'admin-ajax.php', 'relative' ) ); ?>';
</script>
<?php wp_print_scripts( $steps[ $current_step ]['scripts'] ); ?>
<?php wp_print_styles( array_merge( $styles, $steps[ $current_step ]['styles'] ) ); ?>
<?php do_action( 'admin_head' ); ?>
</head>
<body class="<?php echo join( ' ', array_map( 'sanitize_key', $admin_body_class ) ); ?>">
<h1 id="pll-logo">
<a href="https://polylang.pro/" class="title">
<span><img src="<?php echo esc_url( plugins_url( '/src/modules/wizard/images/polylang-logo.png', POLYLANG_FILE ) ); ?>" /></span>
<?php echo esc_html( POLYLANG ); ?>
</a>
</h1>
<ol class="pll-wizard-steps">
<?php
foreach ( $steps as $step_key => $step ) {
$is_completed = array_search( $current_step, array_keys( $steps ), true ) > array_search( $step_key, array_keys( $steps ), true );
if ( $step_key === $current_step ) {
?>
<li class="active"><?php echo esc_html( $step['name'] ); ?></li>
<?php
} elseif ( $is_completed ) {
?>
<li class="done">
<a
href="<?php echo esc_url( add_query_arg( 'step', $step_key, remove_query_arg( 'activate_error' ) ) ); ?>"
>
<?php echo esc_html( $step['name'] ); ?>
</a>
</li>
<?php
} else {
?>
<li><?php echo esc_html( $step['name'] ); ?></li>
<?php
}
}
?>
</ol>
<div class="pll-wizard-content">
<form method="post" class="<?php echo esc_attr( "{$current_step}-step" ); ?>">
<?php
wp_nonce_field( 'pll-wizard', '_pll_nonce' );
if ( ! empty( $steps[ $current_step ]['view'] ) ) {
call_user_func( $steps[ $current_step ]['view'] );
}
?>
<?php if ( 'last' !== $current_step ) : ?>
<p class="pll-wizard-actions step">
<button
type="submit"
class="button-primary button button-large button-next"
value="continue"
name="save_step"
>
<?php esc_html_e( 'Continue', 'polylang' ); ?><span class="dashicons dashicons-arrow-right-alt2"></span>
</button>
</p>
<?php endif; ?>
</form>
</div>
<div class="pll-wizard-footer">
<?php if ( 'last' !== $current_step ) : ?>
<a
class="pll-wizard-footer-links"
href="<?php echo esc_url( admin_url() ); ?>"
>
<?php esc_html_e( 'Not right now', 'polylang' ); ?>
</a>
<?php endif; ?>
</div>
</body>
</html>

View File

@@ -0,0 +1,125 @@
<?php
/**
* Displays the wizard home page step
*
* @package Polylang
*
* @since 2.7
*
* @var PLL_Model $model `PLL_Model` instance.
* @var WP_Post $home_page Home page defined in WordPress options.
* @var PLL_Language $home_page_language Home page language if already assigned.
* @var int[] $translations Ids of home page translations.
* @var PLL_Language[] $untranslated_languages List of languages in which the home page isn't translated.
*/
defined( 'ABSPATH' ) || exit;
?>
<input type="hidden" name="home_page" value="<?php echo esc_attr( (string) $home_page->ID ); ?>" />
<input type="hidden" name="home_page_title" value="<?php echo esc_attr( $home_page->post_title ); ?>" />
<input type="hidden" name="home_page_language" value="<?php echo esc_attr( $home_page_language->slug ); ?>" />
<h2><?php esc_html_e( 'Homepage', 'polylang' ); ?></h2>
<p>
<?php
printf(
/* translators: %s is the post title of the front page */
esc_html__( 'You defined this page as your static homepage: %s.', 'polylang' ),
'<strong>' . esc_html( $home_page->post_title ) . '</strong>'
);
?>
<br />
<?php
printf(
/* translators: %s is the language of the front page ( flag, native name and locale ) */
esc_html__( 'Its language is : %s.', 'polylang' ),
$home_page_language->flag . ' <strong>' . esc_html( $home_page_language->name ) . ' ' . esc_html( $home_page_language->locale ) . '</strong>' //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
?>
<br />
<?php esc_html_e( 'For your site to work correctly, this page must be translated in all available languages.', 'polylang' ); ?>
</p>
<p>
<?php esc_html_e( 'After the pages is created, it is up to you to put the translated content in each page linked to each language.', 'polylang' ); ?>
</p>
<?php if ( $translations ) : ?>
<table id="translated-languages" class="striped">
<thead>
<tr>
<th><?php esc_html_e( 'Your static homepage is already translated in', 'polylang' ); ?></th>
</tr>
</thead>
<tbody>
<?php
foreach ( array_keys( $translations ) as $lang ) {
$language = $model->languages->get( $lang );
if ( empty( $language ) ) {
continue;
}
?>
<tr>
<td>
<?php
echo $language->flag; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo ' ' . esc_html( $language->name ) . ' ' . esc_html( $language->locale ) . ' ';
?>
<?php if ( $language->is_default ) : ?>
<span class="icon-default-lang">
<span class="screen-reader-text">
<?php esc_html_e( 'Default language', 'polylang' ); ?>
</span>
</span>
<?php endif; ?>
<input type="hidden" name="translated_languages[]" value="<?php echo esc_attr( $language->slug ); ?>" />
</td>
</tr>
<?php
}
?>
</tbody>
</table>
<?php endif; ?>
<table id="untranslated-languages" class="striped">
<caption><span class="icon-default-lang"></span> <?php esc_html_e( 'Default language', 'polylang' ); ?></caption>
<thead>
<?php if ( count( $untranslated_languages ) >= 1 ) : ?>
<tr>
<th><?php esc_html_e( 'We are going to prepare this page in', 'polylang' ); ?></th>
</tr>
<?php else : ?>
<tr>
<th>
<span class="dashicons dashicons-info"></span>
<?php esc_html_e( 'One language is well defined and assigned to your home page.', 'polylang' ); ?>
</th>
</tr>
<tr>
<td><?php esc_html_e( "If you add a new language, don't forget to translate your homepage.", 'polylang' ); ?></td>
</tr>
<?php endif; ?>
</thead>
<tbody>
<?php
foreach ( $untranslated_languages as $lg ) {
?>
<tr>
<td>
<?php
echo $lg->flag; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo ' ' . esc_html( $lg->name ) . ' ' . esc_html( $lg->locale ) . ' ';
?>
<?php if ( $lg->is_default ) : ?>
<span class="icon-default-lang">
<span class="screen-reader-text">
<?php esc_html_e( 'Default language', 'polylang' ); ?>
</span>
</span>
<?php endif; ?>
<input type="hidden" name="untranslated_languages[]" value="<?php echo esc_attr( $lg->slug ); ?>" />
</td>
</tr>
<?php
}
?>
</tbody>
</table>

View File

@@ -0,0 +1,136 @@
<?php
/**
* Displays the wizard languages step
*
* @package Polylang
*
* @since 2.7
*
* @var PLL_Model $model `PLL_Model` instance.
*/
defined( 'ABSPATH' ) || exit;
$existing_languages = $model->languages->get_list();
$default_language = $model->languages->get_default();
$languages_list = array_diff_key(
PLL_Settings::get_predefined_languages(),
wp_list_pluck( $existing_languages, 'locale', 'locale' )
);
?>
<div id="language-fields"></div>
<p class="languages-setup">
<?php esc_html_e( 'This wizard will help you configure your Polylang settings, and get you started quickly with your multilingual website.', 'polylang' ); ?>
</p>
<p class="languages-setup">
<?php esc_html_e( 'First we are going to define the languages that you will use on your website.', 'polylang' ); ?>
</p>
<h2><?php esc_html_e( 'Languages', 'polylang' ); ?></h2>
<div id="messages">
</div>
<div class="form-field">
<label for="lang_list"><?php esc_html_e( 'Select a language to be added', 'polylang' ); ?></label>
<div class="select-language-field">
<select name="lang_list" id="lang_list">
<option value=""></option>
<?php
foreach ( $languages_list as $language ) {
printf(
'<option value="%1$s" data-flag-html="%3$s" data-language-name="%2$s" >%2$s - %1$s</option>' . "\n",
esc_attr( $language['locale'] ),
esc_attr( $language['name'] ),
esc_attr( PLL_Language::get_predefined_flag( $language['flag'] ) )
);
}
?>
</select>
<div class="action-buttons">
<button type="button"
class="button-primary button"
value="<?php esc_attr_e( 'Add new language', 'polylang' ); ?>"
id="add-language"
name="add-language"
>
<span class="dashicons dashicons-plus"></span><?php esc_html_e( 'Add new language', 'polylang' ); ?>
</button>
</div>
</div>
</div>
<table id="languages" class="striped">
<thead>
<tr>
<th><?php esc_html_e( 'Language', 'polylang' ); ?></th>
<th><?php esc_html_e( 'Remove', 'polylang' ); ?></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<table id="defined-languages" class="striped<?php echo empty( $existing_languages ) ? ' hide' : ''; ?>">
<?php if ( ! empty( $default_language ) ) : ?>
<caption><span class="icon-default-lang"></span> <?php esc_html_e( 'Default language', 'polylang' ); ?></caption>
<?php endif; ?>
<thead>
<tr>
<th><?php esc_html_e( 'Languages already defined', 'polylang' ); ?></th>
</tr>
</thead>
<tbody>
<?php
foreach ( $existing_languages as $lg ) {
printf(
'<tr><td>%3$s<span>%2$s - %1$s</span> %4$s</td></tr>' . "\n",
esc_attr( $lg->locale ),
esc_html( $lg->name ),
$lg->flag, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$lg->is_default ? ' <span class="icon-default-lang"><span class="screen-reader-text">' . esc_html__( 'Default language', 'polylang' ) . '</span></span>' : ''
);
}
?>
</tbody>
</table>
<div id="dialog">
<p>
<?php
printf(
/* translators: %1$s: is a language flag image, %2$s: is a language native name */
esc_html__( 'You selected %1$s %2$s but you didn\'t add it to the list before continuing to the next step.', 'polylang' ),
'<span id="dialog-language-flag"></span>',
'<strong id="dialog-language"></strong>'
);
?>
</p>
<p>
<?php esc_html_e( 'Do you want to add this language before continuing to the next step?', 'polylang' ); ?>
</p>
<ul>
<li>
<?php
printf(
/* translators: %s: is the translated label of the 'Yes' button */
esc_html__( '%s: add this language and continue to the next step', 'polylang' ),
'<strong>' . esc_html__( 'Yes', 'polylang' ) . '</strong >'
);
?>
</li>
<li>
<?php
printf(
/* translators: %s: is the translated label of the 'No' button */
esc_html__( "%s: don't add this language and continue to the next step", 'polylang' ),
'<strong>' . esc_html__( 'No', 'polylang' ) . '</strong >'
);
?>
</li>
<li>
<?php
printf(
/* translators: %s: is the translated label of the 'Ignore' button */
esc_html__( '%s: stay at this step', 'polylang' ),
'<strong>' . esc_html__( 'Ignore', 'polylang' ) . '</strong >'
);
?>
</li>
</ul>
</div>

View File

@@ -0,0 +1,112 @@
<?php
/**
* Displays the wizard last step
*
* @package Polylang
*
* @since 2.7
*/
defined( 'ABSPATH' ) || exit;
?>
<h2><?php esc_html_e( "You're ready to translate your contents!", 'polylang' ); ?></h2>
<div class="documentation">
<p><?php esc_html_e( "You're now able to translate your contents such as posts, pages, categories and tags. You can learn how to use Polylang by reading the documentation.", 'polylang' ); ?></p>
<div class="documentation-container">
<p class="pll-wizard-actions step documentation-button-container">
<a
class="button button-primary button-large documentation-button"
href="https://polylang.pro/documentation/support/getting-started/"
target="blank"
>
<?php esc_html_e( 'Read documentation', 'polylang' ); ?>
</a>
</p>
</div>
</div>
<ul class="pll-wizard-next-steps">
<li class="pll-wizard-next-step-item">
<div class="pll-wizard-next-step-description">
<p class="next-step-heading"><?php esc_html_e( 'Next step', 'polylang' ); ?></p>
<h3 class="next-step-description"><?php esc_html_e( 'Create menus', 'polylang' ); ?></h3>
<p class="next-step-extra-info">
<?php esc_html_e( 'To get your website ready, there are still two steps you need to perform manually: add menus in each language, and add a language switcher to allow your visitors to select their preferred language.', 'polylang' ); ?>
</p>
</div>
<div class="pll-wizard-next-step-action">
<p class="pll-wizard-actions step">
<a class="button button-primary button-large" href="https://polylang.pro/documentation/support/getting-started/create-menus/">
<?php esc_html_e( 'Read documentation', 'polylang' ); ?>
</a>
</p>
</div>
</li>
<li class="pll-wizard-next-step-item">
<div class="pll-wizard-next-step-description">
<p class="next-step-heading"><?php esc_html_e( 'Next step', 'polylang' ); ?></p>
<h3 class="next-step-description"><?php esc_html_e( 'Translate some pages', 'polylang' ); ?></h3>
<p class="next-step-extra-info"><?php esc_html_e( "You're ready to translate the posts on your website.", 'polylang' ); ?></p>
</div>
<div class="pll-wizard-next-step-action">
<p class="pll-wizard-actions step">
<a class="button button-large" href="<?php echo esc_url( admin_url( 'edit.php?post_type=page' ) ); ?>">
<?php esc_html_e( 'View pages', 'polylang' ); ?>
</a>
</p>
</div>
</li>
<?php if ( ! defined( 'POLYLANG_PRO' ) && ! defined( 'WOOCOMMERCE_VERSION' ) ) : ?>
<li class="pll-wizard-next-step-item">
<div class="pll-wizard-next-step-description">
<p class="next-step-heading"><?php esc_html_e( 'Polylang Pro', 'polylang' ); ?></p>
<h3 class="next-step-description"><?php esc_html_e( 'Upgrade to Polylang Pro', 'polylang' ); ?></h3>
<p class="next-step-extra-info">
<?php esc_html_e( 'Thank you for activating Polylang. If you want more advanced features - duplication, synchronization, REST API support, integration with other plugins, etc. - or further help provided by our Premium support, we recommend you upgrade to Polylang Pro.', 'polylang' ); ?>
</p>
</div>
<div class="pll-wizard-next-step-action">
<p class="pll-wizard-actions step">
<a class="button button-primary button-large" href="https://polylang.pro/pricing/polylang-pro/">
<?php esc_html_e( 'Buy now', 'polylang' ); ?>
</a>
</p>
</div>
</li>
<?php endif; ?>
<?php if ( ! defined( 'POLYLANG_PRO' ) && defined( 'WOOCOMMERCE_VERSION' ) && ! defined( 'PLLWC_VERSION' ) ) : ?>
<li class="pll-wizard-next-step-item">
<div class="pll-wizard-next-step-description">
<p class="next-step-heading"><?php esc_html_e( 'WooCommerce', 'polylang' ); ?></p>
<h3 class="next-step-description"><?php esc_html_e( 'Purchase Polylang Business Pack', 'polylang' ); ?></h3>
<p class="next-step-extra-info">
<?php
printf(
/* translators: %s is the name of Polylang Business Pack product */
esc_html__( 'We have noticed that you are using Polylang with WooCommerce. To ensure a better compatibility, we recommend you use %s which includes both Polylang Pro and Polylang For WooCommerce.', 'polylang' ),
'<strong>' . esc_html__( 'Polylang Business Pack', 'polylang' ) . '</strong>'
);
?>
</p>
</div>
<div class="pll-wizard-next-step-action">
<p class="pll-wizard-actions step">
<a class="button button-primary button-large" href="https://polylang.pro/pricing/polylang-for-woocommerce/">
<?php esc_html_e( 'Buy now', 'polylang' ); ?>
</a>
</p>
</div>
</li>
<?php endif; ?>
<li class="pll-wizard-additional-steps">
<div class="pll-wizard-next-step-action">
<p class="pll-wizard-actions step">
<a class="button button-large" href="<?php echo esc_url( admin_url() ); ?>">
<?php esc_html_e( 'Return to the Dashboard', 'polylang' ); ?>
</a>
</p>
</div>
</li>
</ul>

View File

@@ -0,0 +1,42 @@
<?php
/**
* Displays the wizard licenses step
*
* @package Polylang
*
* @since 2.7
*/
defined( 'ABSPATH' ) || exit;
$licenses = apply_filters( 'pll_settings_licenses', array() );
$is_error = isset( $_GET['activate_error'] ) && 'i18n_license_key_error' === sanitize_key( $_GET['activate_error'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
?>
<p>
<?php esc_html_e( 'You are using plugins which require a license key.', 'polylang' ); ?>
<?php
if ( 1 === count( $licenses ) ) {
echo esc_html( __( 'Please enter your license key:', 'polylang' ) );
} else {
echo esc_html( __( 'Please enter your license keys:', 'polylang' ) );
}
?>
</p>
<h2><?php esc_html_e( 'Licenses', 'polylang' ); ?></h2>
<div id="messages">
<?php if ( $is_error ) : ?>
<p class="error"><?php esc_html_e( 'There is an error with a license key.', 'polylang' ); ?></p>
<?php endif; ?>
</div>
<div class="form-field">
<table id="pll-licenses-table" class="form-table pll-table-top">
<tbody>
<?php
foreach ( $licenses as $license ) {
// Escaping is already done in get_form_field method.
echo $license->get_form_field(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
?>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,58 @@
<?php
/**
* Displays the wizard media step
*
* @package Polylang
*
* @since 2.7
*
* @var \WP_Syntex\Polylang\Options\Options $options Polylang's options.
*/
defined( 'ABSPATH' ) || exit;
$help_screenshot = '/src/modules/wizard/images/media-screen' . ( is_rtl() ? '-rtl' : '' ) . '.png';
?>
<h2><?php esc_html_e( 'Media', 'polylang' ); ?></h2>
<p>
<?php esc_html_e( 'Polylang allows you to translate the text attached to your media, for example the title, the alternative text, the caption, or the description.', 'polylang' ); ?>
<?php esc_html_e( 'When you translate a media, the file is not duplicated on your disk, however you will see one entry per language in the media library.', 'polylang' ); ?>
<?php esc_html_e( 'When you want to insert media in a post, only the media in the language of the current post will be displayed.', 'polylang' ); ?>
</p>
<p>
<?php esc_html_e( 'You must activate media translation if you want to translate the title, the alternative text, the caption, or the description. Otherwise you can safely deactivate it.', 'polylang' ); ?>
</p>
<ul class="pll-wizard-services">
<li class="pll-wizard-service-item">
<div class="pll-wizard-service-enable">
<span class="pll-wizard-service-toggle">
<input
id="pll-wizard-service-media"
type="checkbox"
name="media_support"
value="yes" <?php checked( $options['media_support'] ); ?>
/>
<label for="pll-wizard-service-media" />
</span>
</div>
<div class="pll-wizard-service-description">
<p>
<?php esc_html_e( 'Allow Polylang to translate media', 'polylang' ); ?>
</p>
</div>
</li>
<li class="pll-wizard-service-item">
<div class="pll-wizard-service-example">
<p>
<input id="slide-toggle" type="checkbox" checked="checked">
<label for="slide-toggle" class="button button-primary button-small">
<span class="dashicons dashicons-visibility"></span><?php esc_html_e( 'Help', 'polylang' ); ?>
</label>
<span id="screenshot">
<img src="<?php echo esc_url( plugins_url( $help_screenshot, POLYLANG_FILE ) ); ?>" />
</span>
</p>
</div>
</li>
</ul>

View File

@@ -0,0 +1,37 @@
<?php
/**
* Displays the wizard unstranslated content step
*
* @package Polylang
*
* @since 2.7
*
* @var PLL_Model $model `PLL_Model` instance.
*/
defined( 'ABSPATH' ) || exit;
$languages_list = $model->languages->get_list();
?>
<h2><?php esc_html_e( 'Content without language', 'polylang' ); ?></h2>
<p>
<?php esc_html_e( 'There are posts, pages, categories or tags without language.', 'polylang' ); ?><br />
<?php esc_html_e( 'For your site to work correctly, you need to assign a language to all your contents.', 'polylang' ); ?><br />
<?php esc_html_e( 'The selected language below will be applied to all your content without an assigned language.', 'polylang' ); ?>
</p>
<div class="form-field">
<label for="lang_list"><?php esc_html_e( 'Choose the language to be assigned', 'polylang' ); ?></label>
<select name="language" id="lang_list">
<?php
foreach ( $languages_list as $lg ) {
printf(
'<option value="%1$s" data-flag-html="%3$s" data-language-name="%2$s"%4$s>%2$s - %1$s</option>' . "\n",
esc_attr( $lg->locale ),
esc_html( $lg->name ),
esc_html( $lg->flag ),
$lg->is_default ? ' selected="selected"' : ''
);
}
?>
</select>
</div>

View File

@@ -0,0 +1,888 @@
<?php
/**
* @package Polylang
*/
defined( 'ABSPATH' ) || exit;
use WP_Syntex\Polylang\Options\Options;
/**
* Main class for Polylang wizard.
*
* @since 2.7
*/
class PLL_Wizard {
/**
* Reference to the model object
*
* @var PLL_Model
*/
protected $model;
/**
* Reference to the Polylang options array.
*
* @var Options
*/
protected $options;
/**
* List of steps.
*
* @var array[] $steps {
* @type array {
* List of step properties.
*
* @type string $name I18n string which names the step.
* @type callable $view The callback function use to display the step content.
* @type callable $handler The callback function use to process the step after it is submitted.
* @type array $scripts List of scripts handle needed by the step.
* @type array $styles The list of styles handle needed by the step.
* }
* }
*/
protected $steps = array();
/**
* The current step.
*
* @var string|null
*/
protected $current_step;
/**
* List of WordPress CSS file handles.
*
* @var string[]
*/
protected $styles = array();
/**
* Constructor
*
* @param PLL_Admin_Base $polylang Reference to Polylang global object.
* @since 2.7
*/
public function __construct( PLL_Admin_Base &$polylang ) {
$this->options = $polylang->options;
$this->model = &$polylang->model;
// Display Wizard page before any other action to ensure displaying it outside the WordPress admin context.
// Hooked on admin_init with priority 40 to ensure PLL_Wizard_Pro is correctly initialized.
add_action( 'admin_init', array( $this, 'setup_wizard_page' ), 40 );
// Add Wizard submenu.
add_filter( 'pll_settings_tabs', array( $this, 'settings_tabs' ), 10, 1 );
// Add filter to select screens where to display the notice.
add_filter( 'pll_can_display_notice', array( $this, 'can_display_notice' ), 10, 2 );
// Default steps.
add_filter( 'pll_wizard_steps', array( $this, 'add_step_licenses' ), 100 );
add_filter( 'pll_wizard_steps', array( $this, 'add_step_languages' ), 200 );
add_filter( 'pll_wizard_steps', array( $this, 'add_step_media' ), 300 );
add_filter( 'pll_wizard_steps', array( $this, 'add_step_untranslated_contents' ), 400 );
add_filter( 'pll_wizard_steps', array( $this, 'add_step_home_page' ), 500 );
add_filter( 'pll_wizard_steps', array( $this, 'add_step_last' ), 999 );
}
/**
* Save an activation transient when Polylang is activating to redirect to the wizard
*
* @since 2.7
*
* @param bool $network_wide if activated for all sites in the network.
* @return void
*/
public static function start_wizard( $network_wide ) {
$options = (array) get_option( Options::OPTION_NAME, array() );
if ( wp_doing_ajax() || $network_wide || ! empty( $options['version'] ) ) {
return;
}
set_transient( 'pll_activation_redirect', 1, 30 );
}
/**
* Redirect to the wizard depending on the context
*
* @since 2.7
*
* @return void
*/
public function redirect_to_wizard() {
if ( get_transient( 'pll_activation_redirect' ) ) {
$do_redirect = true;
if ( ( isset( $_GET['page'] ) && 'mlang_wizard' === sanitize_key( $_GET['page'] ) ) || isset( $_GET['activate-multi'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
delete_transient( 'pll_activation_redirect' );
$do_redirect = false;
}
if ( $do_redirect ) {
wp_safe_redirect(
sanitize_url(
add_query_arg(
array(
'page' => 'mlang_wizard',
),
admin_url( 'admin.php' )
)
)
);
exit;
}
}
}
/**
* Add an admin Polylang submenu to access the wizard
*
* @since 2.7
*
* @param string[] $tabs Submenus list.
* @return string[] Submenus list updated.
*/
public function settings_tabs( $tabs ) {
$tabs['wizard'] = esc_html__( 'Setup', 'polylang' );
return $tabs;
}
/**
* Returns true if the media step is displayable, false otherwise.
*
* @since 2.7
*
* @param PLL_Language[] $languages List of language objects.
* @return bool
*/
public function is_media_step_displayable( $languages ) {
$media = array();
// If there is no language or only one the media step is displayable.
if ( ! $languages || count( $languages ) < 2 ) {
return true;
}
foreach ( $languages as $language ) {
$media[ $language->slug ] = $this->model->count_posts(
$language,
array(
'post_type' => array( 'attachment' ),
'post_status' => 'inherit',
)
);
}
return count( array_filter( $media ) ) === 0;
}
/**
* Check if the licenses step is displayable
*
* @since 2.7
*
* @return bool
*/
public function is_licenses_step_displayable() {
$licenses = apply_filters( 'pll_settings_licenses', array() );
return count( $licenses ) > 0;
}
/**
* Setup the wizard page
*
* @since 2.7
*
* @return void
*/
public function setup_wizard_page() {
PLL_Admin_Notices::add_notice( 'wizard', $this->wizard_notice() );
$this->redirect_to_wizard();
if ( ! Polylang::is_wizard() ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Sorry, you are not allowed to manage options for this site.', 'polylang' ) );
}
// Enqueue scripts and styles especially for the wizard.
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
$this->steps = apply_filters( 'pll_wizard_steps', $this->steps );
$step = isset( $_GET['step'] ) ? sanitize_key( $_GET['step'] ) : false; // phpcs:ignore WordPress.Security.NonceVerification
$this->current_step = $step && array_key_exists( $step, $this->steps ) ? $step : current( array_keys( $this->steps ) );
$has_languages = $this->model->has_languages();
if ( ! $has_languages && ! in_array( $this->current_step, array( 'licenses', 'languages' ) ) ) {
wp_safe_redirect( sanitize_url( $this->get_step_link( 'languages' ) ) );
exit;
}
if ( $has_languages && $this->model->get_objects_with_no_lang( 1 ) && ! in_array( $this->current_step, array( 'licenses', 'languages', 'media', 'untranslated-contents' ) ) ) {
wp_safe_redirect( sanitize_url( $this->get_step_link( 'untranslated-contents' ) ) );
exit;
}
// Call the handler of the step for going to the next step.
// Be careful nonce verification with check_admin_referer must be done in each handler.
if ( ! empty( $_POST['save_step'] ) && isset( $this->steps[ $this->current_step ]['handler'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
call_user_func( $this->steps[ $this->current_step ]['handler'] );
}
$this->display_wizard_page();
// Ensure nothing is done after including the page.
exit;
}
/**
* Adds some admin screens where to display the wizard notice
*
* @since 2.7
*
* @param bool $can_display_notice Whether the notice can be displayed.
* @param string $notice The notice name.
* @return bool
*/
public function can_display_notice( $can_display_notice, $notice ) {
if ( ! $can_display_notice && 'wizard' === $notice ) {
$screen = get_current_screen();
$can_display_notice = ! empty( $screen ) && in_array(
$screen->base,
array(
'edit',
'upload',
'options-general',
)
);
}
return $can_display_notice;
}
/**
* Return html code of the wizard notice
*
* @since 2.7
*
* @return string
*/
public function wizard_notice() {
ob_start();
include __DIR__ . '/html-wizard-notice.php';
return ob_get_clean();
}
/**
* Display the wizard page
*
* @since 2.7
*
* @return void
*/
public function display_wizard_page() {
set_current_screen( 'pll-wizard' );
do_action( 'admin_enqueue_scripts' );
$steps = $this->steps;
$current_step = $this->current_step;
$styles = $this->styles;
include __DIR__ . '/view-wizard-page.php';
}
/**
* Enqueue scripts and styles for the wizard
*
* @since 2.7
*
* @return void
*/
public function enqueue_scripts() {
wp_enqueue_style( 'polylang_admin', plugins_url( '/css/build/admin' . $this->get_suffix() . '.css', POLYLANG_ROOT_FILE ), array(), POLYLANG_VERSION );
wp_enqueue_style( 'pll-wizard', plugins_url( '/css/build/wizard' . $this->get_suffix() . '.css', POLYLANG_ROOT_FILE ), array( 'dashicons', 'install', 'common', 'forms' ), POLYLANG_VERSION );
$this->styles = array( 'polylang_admin', 'pll-wizard' );
}
/**
* Get the suffix to enqueue non minified files in a Debug context
*
* @since 2.7
*
* @return string Empty when SCRIPT_DEBUG equal to true
* otherwise .min
*/
public function get_suffix() {
return defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
}
/**
* Get the URL for the step's screen.
*
* @since 2.7
*
* @param string $step slug (default: current step).
* @return string URL for the step if it exists.
* Empty string on failure.
*/
public function get_step_link( $step = '' ) {
if ( ! $step ) {
$step = $this->current_step;
}
$keys = array_keys( $this->steps );
$step_index = array_search( $step, $keys, true );
if ( false === $step_index ) {
return '';
}
return add_query_arg( 'step', $keys[ $step_index ], remove_query_arg( 'activate_error' ) );
}
/**
* Get the URL for the next step's screen.
*
* @since 2.7
*
* @param string $step slug (default: current step).
* @return string URL for next step if a next step exists.
* Admin URL if it's the last step.
* Empty string on failure.
*/
public function get_next_step_link( $step = '' ) {
if ( ! $step ) {
$step = $this->current_step;
}
$keys = array_keys( $this->steps );
if ( end( $keys ) === $step ) {
return admin_url();
}
$step_index = array_search( $step, $keys, true );
if ( false === $step_index ) {
return '';
}
return add_query_arg( 'step', $keys[ $step_index + 1 ], remove_query_arg( 'activate_error' ) );
}
/**
* Add licenses step to the wizard
*
* @since 2.7
*
* @param array $steps List of steps.
* @return array List of steps updated.
*/
public function add_step_licenses( $steps ) {
// Add ajax action on deactivate button in licenses step.
add_action( 'wp_ajax_pll_deactivate_license', array( $this, 'deactivate_license' ) );
// Be careful `settings` script is enqueued here without dependency except jquery because only code useful for deactivate license button is needed.
// To be really loaded the script needs to be passed to the `$steps['licenses']['scripts']` array below with the same handle than in `wp_enqueue_script()`.
wp_enqueue_script( 'pll_settings', plugins_url( '/js/build/settings' . $this->get_suffix() . '.js', POLYLANG_ROOT_FILE ), array( 'jquery' ), POLYLANG_VERSION, true );
wp_localize_script( 'pll_settings', 'pll_settings', array( 'dismiss_notice' => esc_html__( 'Dismiss this notice.', 'polylang' ) ) );
if ( $this->is_licenses_step_displayable() ) {
$steps['licenses'] = array(
'name' => esc_html__( 'Licenses', 'polylang' ),
'view' => array( $this, 'display_step_licenses' ),
'handler' => array( $this, 'save_step_licenses' ),
'scripts' => array( 'pll_settings' ), // Polylang admin script used by deactivate license button.
'styles' => array(),
);
}
return $steps;
}
/**
* Display the languages step form
*
* @since 2.7
*
* @return void
*/
public function display_step_licenses() {
include __DIR__ . '/view-wizard-step-licenses.php';
}
/**
* Execute the languages step
*
* @since 2.7
*
* @return void
*/
public function save_step_licenses() {
check_admin_referer( 'pll-wizard', '_pll_nonce' );
$redirect = $this->get_next_step_link();
$licenses = apply_filters( 'pll_settings_licenses', array() );
foreach ( $licenses as $license ) {
if ( ! empty( $_POST['licenses'][ $license->id ] ) ) {
$updated_license = $license->activate_license( sanitize_key( $_POST['licenses'][ $license->id ] ) );
if ( ! empty( $updated_license->license_data ) && false === $updated_license->license_data->success ) {
// Stay on this step with an error.
$redirect = add_query_arg(
array(
'step' => $this->current_step,
'activate_error' => 'i18n_license_key_error',
)
);
}
}
}
wp_safe_redirect( sanitize_url( $redirect ) );
exit;
}
/**
* Ajax method to deactivate a license
*
* @since 2.7
*
* @return void
*/
public function deactivate_license() {
check_ajax_referer( 'pll-wizard', '_pll_nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( -1 );
}
if ( ! isset( $_POST['id'] ) ) {
wp_die( 0 );
}
$id = substr( sanitize_text_field( wp_unslash( $_POST['id'] ) ), 11 );
$licenses = apply_filters( 'pll_settings_licenses', array() );
$license = $licenses[ $id ];
$license->deactivate_license();
wp_send_json(
array(
'id' => $id,
'html' => $license->get_form_field(),
)
);
}
/**
* Add languages step to the wizard
*
* @since 2.7
*
* @param array $steps List of steps.
* @return array List of steps updated.
*/
public function add_step_languages( $steps ) {
wp_deregister_script( 'pll_settings' ); // Deregister after the licenses step enqueue to update jquery-ui-selectmenu dependency.
// The wp-ajax-response and postbox dependencies is useless in wizard steps especially postbox which triggers a javascript error otherwise.
// To be really loaded the script needs to be passed to the `$steps['languages']['scripts']` array below with the same handle than in `wp_enqueue_script()`.
wp_enqueue_script( 'pll_settings', plugins_url( '/js/build/settings' . $this->get_suffix() . '.js', POLYLANG_ROOT_FILE ), array( 'jquery', 'jquery-ui-selectmenu' ), POLYLANG_VERSION, true );
wp_localize_script( 'pll_settings', 'pll_settings', array( 'dismiss_notice' => esc_html__( 'Dismiss this notice.', 'polylang' ) ) );
wp_register_script( 'pll-wizard-languages', plugins_url( '/js/build/languages-step' . $this->get_suffix() . '.js', POLYLANG_ROOT_FILE ), array( 'jquery', 'jquery-ui-dialog' ), POLYLANG_VERSION, true );
wp_localize_script(
'pll-wizard-languages',
'pll_wizard_params',
array(
'i18n_no_language_selected' => esc_html__( 'You need to select a language to be added.', 'polylang' ),
'i18n_language_already_added' => esc_html__( 'You already added this language.', 'polylang' ),
'i18n_no_language_added' => esc_html__( 'You need to add at least one language.', 'polylang' ),
'i18n_add_language_needed' => esc_html__( 'You selected a language, however, to be able to continue, you need to add it.', 'polylang' ),
'i18n_pll_add_language' => esc_html__( 'Impossible to add the language.', 'polylang' ),
'i18n_pll_invalid_locale' => esc_html__( 'Enter a valid WordPress locale', 'polylang' ),
'i18n_pll_invalid_slug' => esc_html__( 'The language code contains invalid characters', 'polylang' ),
'i18n_pll_non_unique_slug' => esc_html__( 'The language code must be unique', 'polylang' ),
'i18n_pll_invalid_name' => esc_html__( 'The language must have a name', 'polylang' ),
'i18n_pll_invalid_flag' => esc_html__( 'The flag does not exist', 'polylang' ),
'i18n_dialog_title' => esc_html__( "A language wasn't added.", 'polylang' ),
'i18n_dialog_yes_button' => esc_html__( 'Yes', 'polylang' ),
'i18n_dialog_no_button' => esc_html__( 'No', 'polylang' ),
'i18n_dialog_ignore_button' => esc_html__( 'Ignore', 'polylang' ),
'i18n_remove_language_icon' => esc_html__( 'Remove this language', 'polylang' ),
)
);
wp_enqueue_script( 'pll-wizard-languages' );
wp_enqueue_style( 'pll-wizard-selectmenu', plugins_url( '/css/build/selectmenu' . $this->get_suffix() . '.css', POLYLANG_ROOT_FILE ), array( 'dashicons', 'install', 'common', 'wp-jquery-ui-dialog' ), POLYLANG_VERSION );
$steps['languages'] = array(
'name' => esc_html__( 'Languages', 'polylang' ),
'view' => array( $this, 'display_step_languages' ),
'handler' => array( $this, 'save_step_languages' ),
'scripts' => array( 'pll-wizard-languages', 'pll_settings' ),
'styles' => array( 'pll-wizard-selectmenu' ),
);
return $steps;
}
/**
* Display the languages step form
*
* @since 2.7
*
* @return void
*/
public function display_step_languages() {
$model = $this->model;
include __DIR__ . '/view-wizard-step-languages.php';
}
/**
* Execute the languages step
*
* @since 2.7
*
* @return void
*/
public function save_step_languages() {
check_admin_referer( 'pll-wizard', '_pll_nonce' );
$all_languages = include POLYLANG_DIR . '/src/settings/languages.php';
$languages = isset( $_POST['languages'] ) && is_array( $_POST['languages'] ) ? array_map( 'sanitize_locale_name', $_POST['languages'] ) : false;
$saved_languages = array();
// If there is no language added or defined.
if ( empty( $languages ) && ! $this->model->has_languages() ) {
// Stay on this step with an error.
wp_safe_redirect(
sanitize_url(
add_query_arg(
array(
'step' => $this->current_step,
'activate_error' => 'i18n_no_language_added',
)
)
)
);
exit;
}
// Otherwise process the languages to add or skip the step if no language has been added.
if ( ! empty( $languages ) ) {
require_once ABSPATH . 'wp-admin/includes/translation-install.php';
// Remove duplicate values.
$languages = array_unique( $languages );
// For each language add it in Polylang settings.
foreach ( $languages as $locale ) {
$saved_languages = $all_languages[ $locale ];
$saved_languages['slug'] = $saved_languages['code'];
$saved_languages['rtl'] = (int) ( 'rtl' === $saved_languages['dir'] );
$saved_languages['term_group'] = 0; // Default term_group.
$language_added = $this->model->add_language( $saved_languages );
if ( $language_added instanceof WP_Error && array_key_exists( 'pll_non_unique_slug', $language_added->errors ) ) {
// Get the slug from the locale : lowercase and dash instead of underscore.
$saved_languages['slug'] = strtolower( str_replace( '_', '-', $saved_languages['locale'] ) );
$language_added = $this->model->add_language( $saved_languages );
}
if ( $language_added instanceof WP_Error ) {
// Stay on this step with an error.
$error_keys = array_keys( $language_added->errors );
wp_safe_redirect(
sanitize_url(
add_query_arg(
array(
'step' => $this->current_step,
'activate_error' => 'i18n_' . reset( $error_keys ),
)
)
)
);
exit;
}
if ( 'en_US' !== $locale && current_user_can( 'install_languages' ) ) {
wp_download_language_pack( $locale );
}
}
}
wp_safe_redirect( sanitize_url( $this->get_next_step_link() ) );
exit;
}
/**
* Add the media step to the wizard.
*
* @since 2.7
*
* @param array $steps List of steps.
* @return array List of steps updated.
*/
public function add_step_media( $steps ) {
$languages = $this->model->get_languages_list();
if ( $this->is_media_step_displayable( $languages ) ) {
$steps['media'] = array(
'name' => esc_html__( 'Media', 'polylang' ),
'view' => array( $this, 'display_step_media' ),
'handler' => array( $this, 'save_step_media' ),
'scripts' => array(),
'styles' => array(),
);
}
return $steps;
}
/**
* Display the media step form
*
* @since 2.7
*
* @return void
*/
public function display_step_media() {
$options = $this->options;
include __DIR__ . '/view-wizard-step-media.php';
}
/**
* Execute the media step
*
* @since 2.7
*
* @return void
*/
public function save_step_media() {
check_admin_referer( 'pll-wizard', '_pll_nonce' );
$media_support = isset( $_POST['media_support'] ) ? sanitize_key( $_POST['media_support'] ) === 'yes' : false;
$this->options['media_support'] = $media_support;
update_option( 'polylang', $this->options );
wp_safe_redirect( sanitize_url( $this->get_next_step_link() ) );
exit;
}
/**
* Add untranslated contents step to the wizard
*
* @since 2.7
*
* @param array $steps List of steps.
* @return array List of steps updated.
*/
public function add_step_untranslated_contents( $steps ) {
if ( ! $this->model->has_languages() || $this->model->get_objects_with_no_lang( 1 ) ) {
// Even if `pll_settings` is already enqueued with the same dependencies by the languages step, it is interesting to keep that it's also useful for the untranslated-contents step.
// To be really loaded the script needs to be passed to the `$steps['untranslated-contents']['scripts']` array below with the same handle than in `wp_enqueue_script()`.
wp_enqueue_script( 'pll_settings', plugins_url( '/js/build/settings' . $this->get_suffix() . '.js', POLYLANG_ROOT_FILE ), array( 'jquery', 'jquery-ui-selectmenu' ), POLYLANG_VERSION, true );
wp_localize_script( 'pll_settings', 'pll_settings', array( 'dismiss_notice' => esc_html__( 'Dismiss this notice.', 'polylang' ) ) );
wp_enqueue_style( 'pll-wizard-selectmenu', plugins_url( '/css/build/selectmenu' . $this->get_suffix() . '.css', POLYLANG_ROOT_FILE ), array( 'dashicons', 'install', 'common' ), POLYLANG_VERSION );
$steps['untranslated-contents'] = array(
'name' => esc_html__( 'Content', 'polylang' ),
'view' => array( $this, 'display_step_untranslated_contents' ),
'handler' => array( $this, 'save_step_untranslated_contents' ),
'scripts' => array( 'pll_settings' ),
'styles' => array( 'pll-wizard-selectmenu' ),
);
}
return $steps;
}
/**
* Display the untranslated contents step form
*
* @since 2.7
*
* @return void
*/
public function display_step_untranslated_contents() {
$model = $this->model;
include __DIR__ . '/view-wizard-step-untranslated-contents.php';
}
/**
* Execute the untranslated contents step
*
* @since 2.7
*
* @return void
*/
public function save_step_untranslated_contents() {
check_admin_referer( 'pll-wizard', '_pll_nonce' );
$lang = ! empty( $_POST['language'] ) && is_string( $_POST['language'] ) ? sanitize_locale_name( $_POST['language'] ) : false;
if ( empty( $lang ) ) {
$lang = $this->options['default_lang'];
}
$language = $this->model->get_language( $lang );
if ( $language instanceof PLL_Language ) {
$this->model->set_language_in_mass( $language );
}
wp_safe_redirect( sanitize_url( $this->get_next_step_link() ) );
exit;
}
/**
* Add home page step to the wizard
*
* @since 2.7
*
* @param array $steps List of steps.
* @return array List of steps updated.
*/
public function add_step_home_page( $steps ) {
$languages = $this->model->get_languages_list();
$home_page_id = get_option( 'page_on_front' );
$translations = $this->model->post->get_translations( $home_page_id );
if ( $home_page_id > 0 && ( ! $languages || count( $languages ) === 1 || count( $translations ) !== count( $languages ) ) ) {
$steps['home-page'] = array(
'name' => esc_html__( 'Homepage', 'polylang' ),
'view' => array( $this, 'display_step_home_page' ),
'handler' => array( $this, 'save_step_home_page' ),
'scripts' => array(),
'styles' => array(),
);
}
return $steps;
}
/**
* Display the home page step form
*
* @since 2.7
*
* @return void
*/
public function display_step_home_page() {
$model = $this->model;
$languages = $model->languages->get_list();
$home_page_id = get_option( 'page_on_front' );
$home_page_id = is_numeric( $home_page_id ) ? (int) $home_page_id : 0;
$translations = $model->post->get_translations( $home_page_id );
$home_page = $home_page_id > 0 ? get_post( $home_page_id ) : null;
$home_page_language = $model->post->get_language( $home_page_id );
$untranslated_languages = array();
if ( empty( $home_page ) ) {
return;
}
foreach ( $languages as $language ) {
if ( ! $model->post->get( $home_page_id, $language ) ) {
$untranslated_languages[] = $language;
}
}
include __DIR__ . '/view-wizard-step-home-page.php';
}
/**
* Execute the home page step
*
* @since 2.7
*
* @return void
*/
public function save_step_home_page() {
check_admin_referer( 'pll-wizard', '_pll_nonce' );
$default_language = $this->model->has_languages() ? $this->options['default_lang'] : null;
$home_page = isset( $_POST['home_page'] ) ? sanitize_key( $_POST['home_page'] ) : false;
$home_page_title = isset( $_POST['home_page_title'] ) ? sanitize_text_field( wp_unslash( $_POST['home_page_title'] ) ) : esc_html__( 'Homepage', 'polylang' );
$home_page_language = isset( $_POST['home_page_language'] ) ? sanitize_key( $_POST['home_page_language'] ) : false;
$untranslated_languages = isset( $_POST['untranslated_languages'] ) ? array_map( 'sanitize_key', $_POST['untranslated_languages'] ) : array();
call_user_func(
apply_filters( 'pll_wizard_create_home_page_translations', array( $this, 'create_home_page_translations' ) ),
$default_language,
$home_page,
$home_page_title,
$home_page_language,
$untranslated_languages
);
$this->model->clean_languages_cache();
wp_safe_redirect( sanitize_url( $this->get_next_step_link() ) );
exit;
}
/**
* Create home page translations for each language defined.
*
* @since 2.7
*
* @param string $default_language Slug of the default language; null if no default language is defined.
* @param int $home_page Post ID of the home page if it's defined, false otherwise.
* @param string $home_page_title Home page title if it's defined, 'Homepage' otherwise.
* @param string $home_page_language Slug of the home page if it's defined, false otherwise.
* @param string[] $untranslated_languages Array of languages which needs to have a home page translated.
* @return void
*/
public function create_home_page_translations( $default_language, $home_page, $home_page_title, $home_page_language, $untranslated_languages ) {
$translations = $this->model->post->get_translations( $home_page );
foreach ( $untranslated_languages as $language ) {
$language_properties = $this->model->get_language( $language );
$id = wp_insert_post(
array(
'post_title' => $home_page_title . ' - ' . $language_properties->name,
'post_type' => 'page',
'post_status' => 'publish',
)
);
$translations[ $language ] = $id;
pll_set_post_language( $id, $language );
}
pll_save_post_translations( $translations );
}
/**
* Add last step to the wizard
*
* @since 2.7
*
* @param array $steps List of steps.
* @return array List of steps updated.
*/
public function add_step_last( $steps ) {
$steps['last'] = array(
'name' => esc_html__( 'Ready!', 'polylang' ),
'view' => array( $this, 'display_step_last' ),
'handler' => array( $this, 'save_step_last' ),
'scripts' => array(),
'styles' => array(),
);
return $steps;
}
/**
* Display the last step form
*
* @since 2.7
*
* @return void
*/
public function display_step_last() {
// We ran the wizard once. So we can dismiss its notice.
PLL_Admin_Notices::dismiss( 'wizard' );
include __DIR__ . '/view-wizard-step-last.php';
}
/**
* Execute the last step
*
* @since 2.7
*
* @return void
*/
public function save_step_last() {
check_admin_referer( 'pll-wizard', '_pll_nonce' );
wp_safe_redirect( sanitize_url( $this->get_next_step_link() ) );
exit;
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* Loads the WPML compatibility mode.
*
* @package Polylang
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly.
}
if ( $polylang->model->has_languages() ) {
if ( ! defined( 'PLL_WPML_COMPAT' ) || PLL_WPML_COMPAT ) {
PLL_WPML_Compat::instance(); // WPML API.
PLL_WPML_Config::instance(); // wpml-config.xml.
}
}

View File

@@ -0,0 +1,500 @@
<?php
/**
* @package Polylang
*/
/**
* A class to handle the WPML API based on hooks, introduced since WPML 3.2.
* It partly relies on the legacy API.
*
* @see https://wpml.org/documentation/support/wpml-coding-api/wpml-hooks-reference/
*
* @since 2.0
*/
class PLL_WPML_API {
/**
* Stores the original language when the language is switched.
*
* @var PLL_Language|null
*/
private static $original_language = null;
/**
* Constructor.
*
* @since 2.0
*/
public function __construct() {
/*
* Site Wide Language information.
*/
add_filter( 'wpml_active_languages', array( $this, 'wpml_active_languages' ), 10, 2 );
add_filter( 'wpml_display_language_names', array( $this, 'wpml_display_language_names' ), 10, 2 ); // Because we don't translate language names, 3rd to 5th parameters are not supported.
// wpml_translated_language_name => not applicable.
add_filter( 'wpml_current_language', 'pll_current_language', 10, 0 );
add_filter( 'wpml_default_language', 'pll_default_language', 10, 0 );
// wpml_add_language_selector => not implemented.
// wpml_footer_language_selector => not applicable.
add_action( 'wpml_add_language_form_field', array( $this, 'wpml_add_language_form_field' ) );
add_filter( 'wpml_language_is_active', array( $this, 'wpml_language_is_active' ), 10, 2 );
add_filter( 'wpml_is_rtl', array( $this, 'wpml_is_rtl' ) );
// wpml_language_form_input_field => See wpml_add_language_form_field
// wpml_language_has_switched => See wpml_switch_language
add_filter( 'wpml_element_trid', array( $this, 'wpml_element_trid' ), 10, 3 );
add_filter( 'wpml_get_element_translations', array( $this, 'wpml_get_element_translations' ), 10, 3 );
// wpml_language_switcher => not implemented.
// wpml_browser_redirect_language_params => not implemented.
// wpml_enqueue_browser_redirect_language => not applicable.
// wpml_enqueued_browser_redirect_language => not applicable.
// wpml_encode_string => not applicable.
// wpml_decode_string => not applicable.
/*
* Retrieving Language Information for Content.
*/
add_filter( 'wpml_post_language_details', 'wpml_get_language_information', 10, 2 );
add_action( 'wpml_switch_language', array( self::class, 'wpml_switch_language' ), 10, 2 );
add_filter( 'wpml_element_language_code', array( $this, 'wpml_element_language_code' ), 10, 2 );
// wpml_element_language_details => not applicable.
/*
* Retrieving Localized Content.
*/
add_filter( 'wpml_home_url', 'pll_home_url', 10, 0 );
add_filter( 'wpml_element_link', 'icl_link_to_element', 10, 7 );
add_filter( 'wpml_object_id', 'icl_object_id', 10, 4 );
add_filter( 'wpml_translate_single_string', array( $this, 'wpml_translate_single_string' ), 10, 4 );
// wpml_translate_string => not applicable.
// wpml_unfiltered_admin_string => not implemented.
add_filter( 'wpml_permalink', array( $this, 'wpml_permalink' ), 10, 2 );
// wpml_elements_without_translations => not implemented.
add_filter( 'wpml_get_translated_slug', array( $this, 'wpml_get_translated_slug' ), 10, 3 );
/*
* Finding the Translation State of Content.
*/
// wpml_element_translation_type => not implemented.
add_filter( 'wpml_element_has_translations', array( $this, 'wpml_element_has_translations' ), 10, 3 );
// wpml_master_post_from_duplicate => not applicable.
// wpml_post_duplicates => not applicable.
/*
* Inserting Content.
*/
// wpml_admin_make_post_duplicates => not applicable.
// wpml_make_post_duplicates => not applicable.
add_action( 'wpml_register_single_string', 'icl_register_string', 10, 3 );
// wpml_register_string => not applicable.
// wpml_register_string_packages => not applicable.
// wpml_delete_package_action => not applicable.
// wpml_show_package_language_ui => not applicable.
// wpml_set_element_language_details => not implemented.
// wpml_multilingual_options => not applicable.
/*
* Miscellaneous
*/
// wpml_element_type => not applicable.
// wpml_setting => not applicable.
// wpml_sub_setting => not applicable.
// wpml_editor_cf_to_display => not applicable.
// wpml_tm_save_translation_cf => not implemented.
// wpml_tm_xliff_export_translated_cf => not applicable.
// wpml_tm_xliff_export_original_cf => not applicable.
// wpml_duplicate_generic_string => not applicable.
// wpml_translatable_user_meta_fields => not implemented.
// wpml_cross_domain_language_data => not applicable.
// wpml_get_cross_domain_language_data => not applicable.
// wpml_loaded => not applicable.
// wpml_st_loaded => not applicable.
// wpml_tm_loaded => not applicable.
// wpml_hide_management_column (3.4.1) => not applicable.
// wpml_ls_directories_to_scan => not applicable.
// wpml_ls_model_css_classes => not applicable.
// wpml_ls_model_language_css_classes => not applicable.
// wpml_tf_feedback_open_link => not applicable.
// wpml_sync_custom_field => not implemented.
// wpml_sync_all_custom_fields => not implemented.
// wpml_is_redirected => not implemented.
/*
* Updating Content
*/
// wpml_set_translation_mode_for_post_type => not implemented.
/*
* Undocumented
*/
add_filter( 'wpml_is_translated_post_type', array( $this, 'wpml_is_translated_post_type' ), 10, 2 );
add_filter( 'wpml_is_translated_taxonomy', array( $this, 'wpml_is_translated_taxonomy' ), 10, 2 );
}
/**
* Get a list of the languages enabled for a site.
*
* @since 2.0
*
* @param mixed $null Not used.
* @param array| string $args See arguments of icl_get_languages().
* @return array Array of arrays per language.
*/
public function wpml_active_languages( $null, $args = '' ) {
return icl_get_languages( $args );
}
/**
* In WPML, get a language's native and translated name for display in a custom language switcher
* Since Polylang does not implement the translated name, always returns only the native name,
* so the 3rd, 4th and 5th parameters are not used.
*
* @since 2.2
*
* @param mixed $null Not used.
* @param string $native_name The language native name.
* @return string
*/
public function wpml_display_language_names( $null, $native_name ) {
return $native_name;
}
/**
* Returns an HTML hidden input field with name=”lang” and as value the current language.
*
* @since 2.0
*
* @return void
*/
public function wpml_add_language_form_field() {
$lang = pll_current_language();
if ( empty( $lang ) ) {
return;
}
$field = sprintf( '<input type="hidden" name="lang" value="%s" />', esc_attr( $lang ) );
$field = apply_filters( 'wpml_language_form_input_field', $field, $lang );
echo $field; // phpcs:ignore WordPress.Security.EscapeOutput
}
/**
* Find out if a specific language is enabled for the site.
*
* @since 2.0
*
* @param mixed $null Not used.
* @param string $slug Language code.
* @return bool
*/
public function wpml_language_is_active( $null, $slug ) {
$language = PLL()->model->get_language( $slug );
return ! empty( $language ) && $language->active;
}
/**
* Find out whether the current language text direction is RTL or not.
*
* @since 2.0
*
* @return bool
*/
public function wpml_is_rtl() {
return pll_current_language( 'is_rtl' );
}
/**
* Returns the id of the translation group of a translated element.
*
* @since 3.4
*
* @param mixed $empty_value Not used.
* @param int $element_id The id of the item, post id for posts, term_taxonomy_id for terms.
* @param string $element_type Optional. The type of an element.
* @return int
*/
public function wpml_element_trid( $empty_value, $element_id, $element_type = 'post_post' ) {
if ( 0 === strpos( $element_type, 'tax_' ) ) {
$element = get_term_by( 'term_taxonomy_id', $element_id );
if ( $element instanceof WP_Term ) {
$tr_term = PLL()->model->term->get_object_term( $element->term_id, 'term_translations' );
}
}
if ( 0 === strpos( $element_type, 'post_' ) ) {
$tr_term = PLL()->model->post->get_object_term( $element_id, 'post_translations' );
}
if ( isset( $tr_term ) && $tr_term instanceof WP_Term ) {
return $tr_term->term_id;
}
return 0;
}
/**
* Returns the element translations info using the ID of the translation group.
*
* @since 3.4
*
* @param mixed $empty_value Not used.
* @param int $trid The ID of the translation group.
* @param string $element_type Optional. The type of an element.
* @return stdClass[]
*/
public function wpml_get_element_translations( $empty_value, $trid, $element_type = 'post_post' ) {
$return = array();
if ( 0 === strpos( $element_type, 'tax_' ) ) {
$translations = PLL()->model->term->get_translations_from_term_id( $trid );
if ( empty( $translations ) ) {
return array();
}
$original = min( $translations ); // We suppose that the original is the first term created.
$source_lang = array_search( $original, $translations );
$args = array(
'include' => $translations,
'hide_empty' => false,
);
$_terms = get_terms( $args );
if ( ! is_array( $_terms ) ) {
return array();
}
$terms = array();
foreach ( $_terms as $term ) {
$terms[ $term->term_id ] = $term;
}
foreach ( $translations as $lang => $term_id ) {
if ( empty( $terms[ $term_id ] ) ) {
continue;
}
/*
* It seems that WPML fills the `instances` property with the total number of posts
* related to this term, while `WP_Term::$count` includes only *published* posts.
* We intentionally accept this difference to avoid extra DB queries.
*/
$return[ $lang ] = (object) array(
'translation_id' => '0', // We have nothing equivalent.
'language_code' => $lang,
'element_id' => (string) $terms[ $term_id ]->term_taxonomy_id,
'source_language_code' => $source_lang === $lang ? null : $source_lang,
'element_type' => $element_type,
'original' => $original === $term_id ? '1' : '0',
'name' => $terms[ $term_id ]->name,
'term_id' => (string) $term_id,
'instances' => (string) $terms[ $term_id ]->count,
);
}
}
if ( 0 === strpos( $element_type, 'post_' ) ) {
$translations = PLL()->model->post->get_translations_from_term_id( $trid );
if ( empty( $translations ) ) {
return array();
}
$original = min( $translations ); // We suppose that the original is the first post created.
$source_lang = array_search( $original, $translations );
$args = array(
'post__in' => $translations,
'no_paging' => true,
'posts_per_page' => -1,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'lang' => '',
);
$_posts = get_posts( $args );
$posts = array();
foreach ( $_posts as $post ) {
$posts[ $post->ID ] = $post;
}
foreach ( $translations as $lang => $post_id ) {
if ( empty( $posts[ $post_id ] ) ) {
continue;
}
$return[ $lang ] = (object) array(
'translation_id' => '0', // We have nothing equivalent.
'language_code' => $lang,
'element_id' => (string) $post_id,
'source_language_code' => $source_lang === $lang ? null : $source_lang,
'element_type' => $element_type,
'original' => $original === $post_id ? '1' : '0',
'post_title' => $posts[ $post_id ]->post_title,
'post_status' => $posts[ $post_id ]->post_status,
);
}
}
return $return;
}
/**
* Switches whole site to the given language or restores the language that was set when first calling this function.
* Unlike the WPML original action, it is not possible to set the current language and the cookie to different values.
*
* @since 2.7
*
* @param null|string $lang Language code to switch into, restores the original language if null.
* @param bool|string $cookie Optionally also switches the cookie.
* @return void
*/
public static function wpml_switch_language( $lang = null, $cookie = false ) {
if ( null === self::$original_language ) {
self::$original_language = PLL()->curlang;
}
if ( empty( $lang ) ) {
PLL()->curlang = self::$original_language;
} elseif ( 'all' === $lang ) {
PLL()->curlang = null;
} elseif ( in_array( $lang, pll_languages_list() ) ) {
PLL()->curlang = PLL()->model->get_language( $lang );
}
if ( $cookie && isset( PLL()->choose_lang ) ) {
PLL()->choose_lang->maybe_setcookie();
}
do_action( 'wpml_language_has_switched', $lang, $cookie, self::$original_language );
}
/**
* Get the language code for a translatable element.
*
* @since 2.0
*
* @param mixed $language_code A 2-letter language code.
* @param array $args An array with two keys element_id => post_id or term_taxonomy_id, element_type => post type or taxonomy
* @return string|null
*/
public function wpml_element_language_code( $language_code, $args ) {
$type = $args['element_type'];
$id = $args['element_id'];
if ( 'post' === $type || pll_is_translated_post_type( $type ) ) {
$language = pll_get_post_language( $id );
return is_string( $language ) ? $language : null;
}
if ( 'term' === $type || pll_is_translated_taxonomy( $type ) ) {
$term = get_term_by( 'term_taxonomy_id', $id );
if ( $term instanceof WP_Term ) {
$id = $term->term_id;
}
$language = pll_get_term_language( $id );
return is_string( $language ) ? $language : null;
}
return null;
}
/**
* Translates a string.
*
* @since 2.0
*
* @param string $string The string's original value.
* @param string $context The string's registered context.
* @param string $name The string's registered name.
* @param null|string $lang Optional, return the translation in this language, defaults to current language.
* @return string The translated string.
*/
public function wpml_translate_single_string( $string, $context, $name, $lang = null ) {
$has_translation = null; // Passed by reference.
return icl_translate( $context, $name, $string, false, $has_translation, $lang );
}
/**
* Converts a permalink to a language specific permalink.
*
* @since 2.2
*
* @param string $url The url to filter.
* @param null|string $lang Language code, optional, defaults to the current language.
* @return string
*/
public function wpml_permalink( $url, $lang = '' ) {
$lang = PLL()->model->get_language( $lang );
if ( empty( $lang ) && ! empty( PLL()->curlang ) ) {
$lang = PLL()->curlang;
}
return empty( $lang ) ? $url : PLL()->links_model->switch_language_in_link( $url, $lang );
}
/**
* Translates a post type slug.
*
* @since 2.2
*
* @param string $slug Post type slug.
* @param string $post_type Post type name.
* @param string $lang Optional language code (defaults to current language).
* @return string
*/
public function wpml_get_translated_slug( $slug, $post_type, $lang = null ) {
if ( isset( PLL()->translate_slugs ) ) {
if ( empty( $lang ) ) {
$lang = pll_current_language();
}
$slug = PLL()->translate_slugs->slugs_model->get_translated_slug( $post_type, $lang );
}
return $slug;
}
/**
* Find out whether a post type or a taxonomy term is translated.
*
* @since 2.0
*
* @param mixed $null Not used.
* @param int $id The post_id or term_id.
* @param string $type The post type or taxonomy.
* @return bool
*/
public function wpml_element_has_translations( $null, $id, $type ) {
if ( 'post' === $type || pll_is_translated_post_type( $type ) ) {
return count( pll_get_post_translations( $id ) ) > 1;
} elseif ( 'term' === $type || pll_is_translated_taxonomy( $type ) ) {
return count( pll_get_term_translations( $id ) ) > 1;
}
return false;
}
/**
* Returns true if languages and translations are managed for this post type.
*
* @since 3.4
*
* @param mixed $value Not used.
* @param string $post_type The post type name.
* @return bool
*/
public function wpml_is_translated_post_type( $value, $post_type ) {
return pll_is_translated_post_type( $post_type );
}
/**
* Returns true if languages and translations are managed for this taxonomy.
*
* @since 3.4
*
* @param mixed $value Not used.
* @param string $taxonomy The taxonomy name.
* @return bool
*/
public function wpml_is_translated_taxonomy( $value, $taxonomy ) {
return pll_is_translated_taxonomy( $taxonomy );
}
}

View File

@@ -0,0 +1,193 @@
<?php
/**
* @package Polylang
*/
/**
* WPML Compatibility class
* Defines some WPML constants
* Registers strings in a persistent way as done by WPML
*
* @since 1.0.2
*/
class PLL_WPML_Compat {
/**
* Singleton instance
*
* @var PLL_WPML_Compat|null
*/
protected static $instance;
/**
* Stores the strings registered with the WPML API.
*
* @var array
*/
protected static $strings = array();
/**
* @var PLL_WPML_API
*/
public $api;
/**
* Constructor.
*
* @since 1.0.2
*/
protected function __construct() {
// Load the WPML API.
require_once __DIR__ . '/wpml-legacy-api.php';
$this->api = new PLL_WPML_API();
$strings = get_option( 'polylang_wpml_strings' );
if ( is_array( $strings ) ) {
self::$strings = $strings;
add_filter( 'pll_get_strings', array( $this, 'get_strings' ) );
}
add_action( 'pll_language_defined', array( $this, 'define_constants' ) );
add_action( 'pll_no_language_defined', array( $this, 'define_constants' ) );
}
/**
* Access to the single instance of the class
*
* @since 1.7
*
* @return PLL_WPML_Compat
*/
public static function instance() {
if ( empty( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Defines two WPML constants once the language has been defined
* The compatibility with WPML is not perfect on admin side as the constants are defined
* in 'setup_theme' by Polylang (based on user info) and 'plugins_loaded' by WPML (based on cookie).
*
* @since 0.9.5
*
* @return void
*/
public function define_constants() {
if ( ! empty( PLL()->curlang ) ) {
if ( ! defined( 'ICL_LANGUAGE_CODE' ) ) {
define( 'ICL_LANGUAGE_CODE', PLL()->curlang->slug );
}
if ( ! defined( 'ICL_LANGUAGE_NAME' ) ) {
define( 'ICL_LANGUAGE_NAME', PLL()->curlang->name );
}
} elseif ( ! PLL() instanceof PLL_Frontend ) {
if ( ! defined( 'ICL_LANGUAGE_CODE' ) ) {
define( 'ICL_LANGUAGE_CODE', 'all' );
}
if ( ! defined( 'ICL_LANGUAGE_NAME' ) ) {
define( 'ICL_LANGUAGE_NAME', '' );
}
}
}
/**
* Unlike pll_register_string, icl_register_string stores the string in database
* so we need to do the same as some plugins or themes may expect this.
* We use a serialized option to store these strings.
*
* @since 1.0.2
*
* @param string|string[] $context The group in which the string is registered.
* @param string $name A unique name for the string.
* @param string $string The string to register.
* @return void
*/
public function register_string( $context, $name, $string ) {
if ( ! $string || ! is_scalar( $string ) ) {
return;
}
/*
* WPML accepts arrays as context and internally converts them to strings.
* See WPML_Register_String_Filter::truncate_name_and_context().
* This possibility is used by Types.
*/
if ( is_array( $context ) ) {
$name = isset( $context['context'] ) ? $name . $context['context'] : $name;
$context = $context['domain'] ?? '';
}
// If a string has already been registered with the same name and context, let's replace it.
$exist_string = $this->get_string_by_context_and_name( $context, $name );
if ( $exist_string && $exist_string !== $string ) {
$languages = PLL()->model->get_languages_list();
// Assign translations of the old string to the new string, except for the default language.
foreach ( $languages as $language ) {
if ( $language->is_default ) {
continue;
}
$mo = new PLL_MO();
$mo->import_from_db( $language );
$mo->add_entry( $mo->make_entry( $string, $mo->translate( $exist_string ) ) );
$mo->export_to_db( $language );
}
$this->unregister_string( $context, $name );
}
// Registers the string if it does not exist yet (multiline as in WPML).
$to_register = array( 'context' => $context, 'name' => $name, 'string' => $string, 'multiline' => true, 'icl' => true );
if ( ! in_array( $to_register, self::$strings ) ) {
$key = md5( "$context | $name" );
self::$strings[ $key ] = $to_register;
update_option( 'polylang_wpml_strings', self::$strings );
}
}
/**
* Removes a string from the registered strings list
*
* @since 1.0.2
*
* @param string $context The group in which the string is registered.
* @param string $name A unique name for the string.
* @return void
*/
public function unregister_string( $context, $name ) {
$key = md5( "$context | $name" );
if ( isset( self::$strings[ $key ] ) ) {
unset( self::$strings[ $key ] );
update_option( 'polylang_wpml_strings', self::$strings );
}
}
/**
* Adds strings registered by icl_register_string to those registered by pll_register_string
*
* @since 1.0.2
*
* @param array $strings existing registered strings
* @return array registered strings with added strings through WPML API
*/
public function get_strings( $strings ) {
return empty( self::$strings ) ? $strings : array_merge( $strings, self::$strings );
}
/**
* Get a registered string by its context and name
*
* @since 2.0
*
* @param string $context The group in which the string is registered.
* @param string $name A unique name for the string.
* @return string The registered string, empty if none was found.
*/
public function get_string_by_context_and_name( $context, $name ) {
$key = md5( "$context | $name" );
return isset( self::$strings[ $key ] ) ? self::$strings[ $key ]['string'] : '';
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,437 @@
<?php
/**
* @package Polylang
*/
/**
* Compatibility with WPML legacy API
* Deprecated since WPML 3.2 and no more documented
* But a lot of 3rd party plugins / themes are still using these functions
*/
if ( ! function_exists( 'icl_get_home_url' ) ) {
/**
* Link to the home page in the active language
*
* @since 0.9.4
*
* @return string
*/
function icl_get_home_url() {
return pll_home_url();
}
}
if ( ! function_exists( 'icl_get_languages' ) ) {
/**
* Used for building custom language selectors
* available only on frontend
*
* List of parameters accepted in $args:
*
* skip_missing => whether to skip missing translation or not, 0 or 1, defaults to 0
* orderby => 'id', 'code', 'name', defaults to 'id'
* order => 'ASC' or 'DESC', defaults to 'ASC'
* link_empty_to => link to use when the translation is missing {$lang} is replaced by the language code
*
* List of parameters returned per language:
*
* id => the language id
* active => whether this is the active language or no, 0 or 1
* native_name => the language name
* missing => whether the translation is missing or not, 0 or 1
* translated_name => empty, does not exist in Polylang
* language_code => the language code ( slug )
* country_flag_url => the url of the flag
* url => the url of the translation
*
* @since 1.0
*
* @param string|array $args optional
* @return array array of arrays per language
*/
function icl_get_languages( $args = '' ) {
$args = wp_parse_args( $args, array( 'skip_missing' => 0, 'orderby' => 'id', 'order' => 'ASC' ) );
$orderby = ( isset( $args['orderby'] ) && 'code' == $args['orderby'] ) ? 'slug' : ( isset( $args['orderby'] ) && 'name' == $args['orderby'] ? 'name' : 'id' );
$order = ( ! empty( $args['order'] ) && 'desc' == $args['order'] ) ? 'DESC' : 'ASC';
$arr = array();
// NB: When 'skip_missing' is false, WPML returns all languages even if there is no content
$languages = PLL()->model->languages
->filter( $args['skip_missing'] ? 'hide_empty' : '' )
->get_list();
$languages = wp_list_sort( $languages, $orderby, $order ); // Since WP 4.7
foreach ( $languages as $lang ) {
// We can find a translation only on frontend once the global $wp_query object has been instantiated
if ( PLL()->links instanceof PLL_Frontend_Links && ! empty( $GLOBALS['wp_query'] ) ) {
$url = PLL()->links->get_translation_url( $lang );
}
// It seems that WPML does not bother of skip_missing parameter on admin side and before the $wp_query object has been filled
if ( empty( $url ) && ! empty( $args['skip_missing'] ) && ! is_admin() && did_action( 'parse_query' ) ) {
continue;
}
$arr[ $lang->slug ] = array(
'id' => $lang->term_id,
'active' => isset( PLL()->curlang->slug ) && PLL()->curlang->slug == $lang->slug ? 1 : 0,
'native_name' => $lang->name,
'missing' => empty( $url ) ? 1 : 0,
'translated_name' => '', // Does not exist in Polylang
'language_code' => $lang->slug,
'country_flag_url' => $lang->get_display_flag_url(),
'url' => ! empty( $url ) ? $url :
( empty( $args['link_empty_to'] ) ? $lang->get_home_url() :
str_replace( '{$lang}', $lang->slug, $args['link_empty_to'] ) ),
);
}
// Apply undocumented WPML filter
$arr = apply_filters( 'icl_ls_languages', $arr );
return $arr;
}
}
if ( ! function_exists( 'icl_link_to_element' ) ) {
/**
* Used for creating language dependent links in themes
*
* @since 1.0
* @since 2.0 add support for arguments 6 and 7
*
* @param int $id object id
* @param string $type optional, post type or taxonomy name of the object, defaults to 'post'
* @param string $text optional, the link text. If not specified will produce the name of the element in the current language
* @param array $args optional, an array of arguments to add to the link, defaults to empty
* @param string $anchor optional, the anchor to add to the link, defaults to empty
* @param bool $echo optional, whether to echo the link, defaults to true
* @param bool $return_original_if_missing optional, whether to return a value if the translation is missing
* @return string a language dependent link
*/
function icl_link_to_element( $id, $type = 'post', $text = '', $args = array(), $anchor = '', $echo = true, $return_original_if_missing = true ) {
if ( 'tag' == $type ) {
$type = 'post_tag';
}
$pll_type = ( 'post' == $type || pll_is_translated_post_type( $type ) ) ? 'post' : ( 'term' == $type || pll_is_translated_taxonomy( $type ) ? 'term' : false );
if ( $pll_type && ( $lang = pll_current_language() ) && ( $tr_id = PLL()->model->$pll_type->get_translation( $id, $lang ) ) && ( 'term' === $pll_type || PLL()->model->post->current_user_can_read( $tr_id ) ) ) {
$id = $tr_id;
} elseif ( ! $return_original_if_missing ) {
return '';
}
if ( post_type_exists( $type ) ) {
$link = get_permalink( $id );
if ( empty( $text ) ) {
$text = get_the_title( $id );
}
} elseif ( taxonomy_exists( $type ) ) {
$link = get_term_link( $id, $type );
if ( empty( $text ) && ( $term = get_term( $id, $type ) ) && $term instanceof WP_Term ) {
$text = $term->name;
}
}
if ( empty( $link ) || is_wp_error( $link ) ) {
return '';
}
if ( ! empty( $args ) ) {
$link .= ( false === strpos( $link, '?' ) ? '?' : '&' ) . http_build_query( $args );
}
if ( ! empty( $anchor ) ) {
$link .= '#' . $anchor;
}
$link = sprintf( '<a href="%s">%s</a>', esc_url( $link ), esc_html( $text ) );
if ( $echo ) {
echo $link; // phpcs:ignore WordPress.Security.EscapeOutput
}
return $link;
}
}
if ( ! function_exists( 'icl_object_id' ) ) {
/**
* Returns an elements ID in the current language or in another specified language.
*
* @since 0.9.5
*
* @param int $element_id Object id.
* @param string $element_type Optional, post type or taxonomy name of the object, defaults to 'post'.
* @param bool $return_original_if_missing Optional, true if Polylang should return the original id if the translation is missing, defaults to false.
* @param string|null $ulanguage_code Optional, language code, defaults to the current language.
* @return int|null The object id of the translation, null if the translation is missing and $return_original_if_missing set to false.
*/
function icl_object_id( $element_id, $element_type = 'post', $return_original_if_missing = false, $ulanguage_code = null ) {
if ( empty( $element_id ) ) {
return null;
}
$element_id = (int) $element_id;
if ( 'any' === $element_type ) {
$element_type = get_post_type( $element_id );
}
if ( empty( $element_type ) ) {
return null;
}
if ( empty( $ulanguage_code ) ) {
$ulanguage_code = pll_current_language();
}
if ( empty( $ulanguage_code ) ) {
return $return_original_if_missing ? $element_id : null;
}
if ( 'nav_menu' === $element_type ) {
$tr_id = false;
$theme = get_option( 'stylesheet' );
if ( isset( PLL()->options['nav_menus'][ $theme ] ) ) {
foreach ( PLL()->options['nav_menus'][ $theme ] as $menu ) {
if ( array_search( $element_id, $menu ) && ! empty( $menu[ $ulanguage_code ] ) ) {
$tr_id = $menu[ $ulanguage_code ];
break;
}
}
}
} elseif ( pll_is_translated_post_type( $element_type ) ) {
$tr_id = PLL()->model->post->get_translation( $element_id, $ulanguage_code );
} elseif ( pll_is_translated_taxonomy( $element_type ) ) {
$tr_id = PLL()->model->term->get_translation( $element_id, $ulanguage_code );
} else {
return $element_id; // WPML doesn't honor $return_original_if_missing if the post type or taxonomy is not translated, @see {SitePress::get_object_id()}.
}
if ( empty( $tr_id ) ) {
if ( $return_original_if_missing ) {
return $element_id;
}
return null;
}
return (int) $tr_id;
}
}
if ( ! function_exists( 'wpml_object_id_filter' ) ) {
/**
* Undocumented alias of `icl_object_id` introduced in WPML 3.2, used by Yith WooCommerce compare
*
* @since 2.2.4
*
* @param int $id object id
* @param string $type optional, post type or taxonomy name of the object, defaults to 'post'
* @param bool $return_original_if_missing optional, true if Polylang should return the original id if the translation is missing, defaults to false
* @param string $lang optional, language code, defaults to current language
* @return int|null the object id of the translation, null if the translation is missing and $return_original_if_missing set to false
*/
function wpml_object_id_filter( $id, $type = 'post', $return_original_if_missing = false, $lang = null ) {
return icl_object_id( $id, $type, $return_original_if_missing, $lang );
}
}
if ( ! function_exists( 'wpml_get_language_information' ) ) {
/**
* Undocumented function used by the theme Maya
* returns the post language
*
* @see https://wpml.org/forums/topic/canonical-urls-for-wpml-duplicated-posts/#post-52198 for the original WPML code
*
* @since 1.8
*
* @param null $empty optional, not used
* @param int $post_id optional, post id, defaults to current post
* @return array|WP_Error
*/
function wpml_get_language_information( $empty = null, $post_id = null ) {
if ( null === $post_id ) {
$post_id = get_the_ID();
}
if ( empty( $post_id ) ) {
return new WP_Error( 'missing_id', __( 'Missing post ID', 'polylang' ) );
}
$post = get_post( $post_id );
if ( empty( $post ) ) {
/* translators: %d is a Post ID. */
return new WP_Error( 'missing_post', sprintf( __( 'No such post for ID = %d', 'polylang' ), $post_id ) );
}
$lang = PLL()->model->post->get_language( $post_id );
// FIXME WPML may return a WP_Error object
return false === $lang ? array() : array(
'language_code' => $lang->slug,
'locale' => $lang->locale,
'text_direction' => (bool) $lang->is_rtl,
'display_name' => $lang->name, // Seems to be the post language name displayed in the current language, not a feature in Polylang
'native_name' => $lang->name,
'different_language' => pll_current_language() !== $lang->slug,
);
}
}
if ( ! function_exists( 'icl_register_string' ) ) {
/**
* Registers a string for translation in the "strings translation" panel
*
* The 4th and 5th parameters $allow_empty_value and $source_lang are not used by Polylang.
*
* @since 0.9.3
*
* @param string $context the group in which the string is registered, defaults to 'polylang'
* @param string $name a unique name for the string
* @param string $string the string to register
* @return void
*/
function icl_register_string( $context, $name, $string ) {
PLL_WPML_Compat::instance()->register_string( $context, $name, $string );
}
}
if ( ! function_exists( 'icl_unregister_string' ) ) {
/**
* Removes a string from the "strings translation" panel
*
* @since 1.0.2
*
* @param string $context the group in which the string is registered, defaults to 'polylang'
* @param string $name a unique name for the string
* @return void
*/
function icl_unregister_string( $context, $name ) {
PLL_WPML_Compat::instance()->unregister_string( $context, $name );
}
}
if ( ! function_exists( 'icl_t' ) ) {
/**
* Gets the translated value of a string ( previously registered with icl_register_string or pll_register_string )
*
* @since 0.9.3
* @since 1.9.2 argument 3 is optional
* @since 2.0 add support for arguments 4 to 6
*
* @param string $context the group in which the string is registered
* @param string $name a unique name for the string
* @param string $string the string to translate, optional for strings registered with icl_register_string
* @param bool|null $has_translation optional, not supported in Polylang
* @param bool $bool optional, not used
* @param string|null $lang optional, return the translation in this language, defaults to current language
* @return string the translated string
*/
function icl_t( $context, $name, $string = '', &$has_translation = null, $bool = false, $lang = null ) {
return icl_translate( $context, $name, $string, false, $has_translation, $lang );
}
}
if ( ! function_exists( 'icl_translate' ) ) {
/**
* Undocumented function used by NextGen Gallery
* used in PLL_Plugins_Compat for Jetpack with only 3 arguments
*
* @since 1.0.2
* @since 2.0 add support for arguments 5 and 6, strings are no more automatically registered
*
* @param string $context the group in which the string is registered
* @param string $name a unique name for the string
* @param string $string the string to translate, optional for strings registered with icl_register_string
* @param bool $bool optional, not used
* @param bool|null $has_translation optional, not supported in Polylang
* @param string|null $lang optional, return the translation in this language, defaults to current language
* @return string the translated string
*/
function icl_translate( $context, $name, $string = '', $bool = false, &$has_translation = null, $lang = null ) {
// FIXME WPML can automatically registers the string based on an option
if ( empty( $string ) ) {
$string = PLL_WPML_Compat::instance()->get_string_by_context_and_name( $context, $name );
}
return empty( $lang ) ? pll__( $string ) : pll_translate_string( $string, $lang );
}
}
if ( ! function_exists( 'wpml_get_copied_fields_for_post_edit' ) ) {
/**
* Undocumented function used by Types
* FIXME: tested only with Types
* probably incomplete as Types locks the custom fields for a new post, but not when edited
* This is probably linked to the fact that WPML has always an original post in the default language and not Polylang :)
*
* @since 1.1.2
*
* @return array
*/
function wpml_get_copied_fields_for_post_edit() {
if ( empty( $_GET['from_post'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return array();
}
$arr = array( 'original_post_id' => (int) $_GET['from_post'] ); // phpcs:ignore WordPress.Security.NonceVerification
// Don't know what WPML does but Polylang does copy all public meta keys by default.
$keys = get_post_custom_keys( $arr['original_post_id'] );
if ( is_array( $keys ) ) {
foreach ( $keys as $k => $meta_key ) {
if ( is_protected_meta( $meta_key ) ) {
unset( $keys[ $k ] );
}
}
}
// Apply our filter and fill the expected output ( see /types/embedded/includes/fields-post.php )
/** This filter is documented in src/modules/sync/admin-sync.php */
$arr['fields'] = array_unique( apply_filters( 'pll_copy_post_metas', empty( $keys ) ? array() : $keys, false ) );
return $arr;
}
}
if ( ! function_exists( 'icl_get_default_language' ) ) {
/**
* Undocumented function used by Warp 6 by Yootheme
*
* @since 1.0.5
*
* @return string|false default language code
*/
function icl_get_default_language() {
return pll_default_language();
}
}
if ( ! function_exists( 'wpml_get_default_language' ) ) {
/**
* Undocumented function reported to be used by Table Rate Shipping for WooCommerce
*
* @see https://wordpress.org/support/topic/add-wpml-compatibility-function
*
* @since 1.8.2
*
* @return string|false default language code
*/
function wpml_get_default_language() {
return pll_default_language();
}
}
if ( ! function_exists( 'icl_get_current_language' ) ) {
/**
* Undocumented function used by Ultimate Member
*
* @since 2.2.4
*
* @return string Current language code
*/
function icl_get_current_language() {
return (string) pll_current_language();
}
}