first commit

This commit is contained in:
2023-09-12 21:41:04 +02:00
commit 3361a7f053
13284 changed files with 2116755 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
<?php
namespace WPML\ST\DisplayAsTranslated;
use WPML\FP\Lst;
use WPML\FP\Str;
use WPML\FP\Obj;
use WPML\LIB\WP\Hooks;
use WPML\LIB\WP\Post;
use function WPML\Container\make;
use function WPML\FP\spreadArgs;
class CheckRedirect implements \IWPML_Frontend_Action {
public function add_hooks() {
Hooks::onFilter( 'wpml_is_redirected', 10, 3 )
->then( spreadArgs( [ self::class, 'checkForSlugTranslation' ] ) );
}
public static function checkForSlugTranslation( $redirect, $post_id, $q ) {
global $sitepress;
if ( $redirect ) {
$postType = Post::getType( $post_id );
if (
make( \WPML_ST_Post_Slug_Translation_Settings::class )->is_translated( $postType )
&& $sitepress->is_display_as_translated_post_type( $postType )
) {
$adjustLanguageDetails = Obj::set(
Obj::lensProp( 'language_code' ), $sitepress->get_current_language()
);
add_filter( 'wpml_st_post_type_link_filter_language_details', $adjustLanguageDetails );
if ( \WPML_Query_Parser::is_permalink_part_of_request(
get_permalink( $post_id ),
explode( '?', $_SERVER['REQUEST_URI'] )[0] )
) {
$redirect = false;
}
remove_filter( 'wpml_st_post_type_link_filter_language_details', $adjustLanguageDetails );
}
}
return $redirect;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace WPML\ST\SlugTranslation\Hooks;
class Hooks {
/** @var \WPML_Rewrite_Rule_Filter_Factory */
private $factory;
/** @var \WPML_ST_Slug_Translation_Settings $slug_translation_settings */
private $slug_translation_settings;
/** @var array|null */
private $cache;
/**
* @param \WPML_Rewrite_Rule_Filter_Factory $factory
* @param \WPML_ST_Slug_Translation_Settings $slug_translation_settings
*/
public function __construct(
\WPML_Rewrite_Rule_Filter_Factory $factory,
\WPML_ST_Slug_Translation_Settings $slug_translation_settings
) {
$this->factory = $factory;
$this->slug_translation_settings = $slug_translation_settings;
}
public function add_hooks() {
add_action( 'init', [ $this, 'init' ], \WPML_Slug_Translation_Factory::INIT_PRIORITY );
}
public function init() {
if ( $this->slug_translation_settings->is_enabled() ) {
add_filter( 'option_rewrite_rules', [ $this, 'filter' ], 1, 1 );
add_filter( 'flush_rewrite_rules_hard', [ $this, 'flushRewriteRulesHard' ] );
add_action( 'registered_post_type', [ $this, 'clearCache' ] );
add_action( 'registered_taxonomy', [ $this, 'clearCache' ] );
}
}
/**
* @param array $value
*
* @return array
*/
public function filter( $value ) {
if ( empty( $value ) || apply_filters( 'wpml_st_disable_rewrite_rules', false ) ) {
return $value;
}
if ( ! $this->cache ) {
$this->cache = $this->factory->create()->rewrite_rules_filter( $value );
}
return $this->cache;
}
public function clearCache() {
$this->cache = null;
}
/**
* @param bool $hard
*
* @return mixed
*/
public function flushRewriteRulesHard( $hard ) {
$this->clearCache();
return $hard;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace WPML\ST\SlugTranslation\Hooks;
use WPML_Rewrite_Rule_Filter_Factory;
use WPML_ST_Slug_Translation_Settings_Factory;
class HooksFactory {
/**
* We need a static property because there could be many instances of HooksFactory class but we need to guarantee
* there is only a single instance of Hooks
*
* @var Hooks
*/
private static $instance;
/**
* @return Hooks
*/
public function create() {
if ( ! self::$instance ) {
$settings_factory = new WPML_ST_Slug_Translation_Settings_Factory();
$global_settings = $settings_factory->create();
self::$instance = new Hooks( new WPML_Rewrite_Rule_Filter_Factory(), $global_settings );
}
return self::$instance;
}
}

View File

@@ -0,0 +1,60 @@
<?php
class WPML_Rewrite_Rule_Filter implements IWPML_ST_Rewrite_Rule_Filter {
/** @var WPML_ST_Slug_Translation_Custom_Types_Repository[] */
private $custom_types_repositories;
/** @var WPML_ST_Slug_New_Match_Finder */
private $new_match_finder;
/**
* @param WPML_ST_Slug_Translation_Custom_Types_Repository[] $custom_types_repositories
* @param WPML_ST_Slug_New_Match_Finder $new_match_finder
*/
public function __construct( array $custom_types_repositories, WPML_ST_Slug_New_Match_Finder $new_match_finder ) {
$this->custom_types_repositories = $custom_types_repositories;
$this->new_match_finder = $new_match_finder;
}
/**
* @param array|false|null $rules
*
* @return array
*/
function rewrite_rules_filter( $rules ) {
if ( ! is_array( $rules ) && empty( $rules ) ) {
return $rules;
}
$custom_types = $this->get_custom_types();
if ( ! $custom_types ) {
return $rules;
}
$result = array();
foreach ( $rules as $match => $query ) {
$new_match = $this->new_match_finder->get( $match, $custom_types );
$result[ $new_match->get_value() ] = $query;
if ( $new_match->should_preserve_original() ) {
$result[ $match ] = $query;
}
}
return $result;
}
private function get_custom_types() {
if ( empty( $this->custom_types_repositories ) ) {
return array();
}
$types = array();
foreach ( $this->custom_types_repositories as $repository ) {
$types[] = $repository->get();
}
return call_user_func_array( 'array_merge', $types );
}
}

View File

@@ -0,0 +1,293 @@
<?php
abstract class WPML_Slug_Translation_Records {
const CONTEXT_DEFAULT = 'default';
const CONTEXT_WORDPRESS = 'WordPress';
/** @var wpdb $wpdb */
private $wpdb;
/** @var WPML_WP_Cache_Factory $cache_factory*/
private $cache_factory;
public function __construct( wpdb $wpdb, WPML_WP_Cache_Factory $cache_factory ) {
$this->wpdb = $wpdb;
$this->cache_factory = $cache_factory;
}
/**
* @param string $type
*
* @return WPML_ST_Slug
*/
public function get_slug( $type ) {
$cache_item = $this->cache_factory->create_cache_item( $this->get_cache_group(), $type );
if ( ! $cache_item->exists() ) {
$slug = new WPML_ST_Slug();
/** @var \stdClass $original */
$original = $this->wpdb->get_row(
$this->wpdb->prepare(
"SELECT id, value, language, context, name
FROM {$this->wpdb->prefix}icl_strings
WHERE name = %s
AND (context = %s OR context = %s)",
$this->get_string_name( $type ),
self::CONTEXT_DEFAULT,
self::CONTEXT_WORDPRESS
)
);
if ( $original ) {
$slug->set_lang_data( $original );
/** @var array<\stdClass> $translations */
$translations = $this->wpdb->get_results(
$this->wpdb->prepare(
"SELECT value, language, status
FROM {$this->wpdb->prefix}icl_string_translations
WHERE string_id = %d
AND value <> ''
AND language <> %s",
$original->id,
$original->language
)
);
if ( $translations ) {
foreach ( $translations as $translation ) {
$slug->set_lang_data( $translation );
}
}
}
$cache_item->set( $slug );
}
return $cache_item->get();
}
/** @return string */
private function get_cache_group() {
return __CLASS__ . '::' . $this->get_element_type();
}
private function flush_cache() {
$cache_group = $this->cache_factory->create_cache_group( $this->get_cache_group() );
$cache_group->flush_group_cache();
}
/**
* @deprecated use `get_slug` instead.
*
* @param string $type
* @param string $lang
*
* @return null|string
*/
public function get_translation( $type, $lang ) {
$slug = $this->get_slug( $type );
if ( $slug->is_translation_complete( $lang ) ) {
return $slug->get_value( $lang );
}
return null;
}
/**
* @deprecated use `get_slug` instead.
*
* @param string $type
* @param string $lang
*
* @return null|string
*/
public function get_original( $type, $lang = '' ) {
$slug = $this->get_slug( $type );
if ( ! $lang || $slug->get_original_lang() === $lang ) {
return $slug->get_original_value();
}
return null;
}
/**
* @deprecated use `get_slug` instead.
*
* @param string $type
*
* @return int|null
*/
public function get_slug_id( $type ) {
$slug = $this->get_slug( $type );
if ( $slug->get_original_id() ) {
return $slug->get_original_id();
}
return null;
}
/**
* @param string $type
* @param string $slug
*
* @return int|null
*/
public function register_slug( $type, $slug ) {
$string_id = icl_register_string(
self::CONTEXT_WORDPRESS,
$this->get_string_name( $type ),
$slug
);
$this->flush_cache();
return $string_id;
}
/**
* @param string $type
* @param string $slug
*/
public function update_original_slug( $type, $slug ) {
$this->wpdb->update(
$this->wpdb->prefix . 'icl_strings',
array( 'value' => $slug ),
array( 'name' => $this->get_string_name( $type ) )
);
$this->flush_cache();
}
/**
* @deprecated use `get_slug` instead.
*
* @param string $type
*
* @return null|stdClass
*/
public function get_original_slug_and_lang( $type ) {
$original_slug_and_lang = null;
$slug = $this->get_slug( $type );
if ( $slug->get_original_id() ) {
$original_slug_and_lang = (object) array(
'value' => $slug->get_original_value(),
'language' => $slug->get_original_lang(),
);
}
return $original_slug_and_lang;
}
/**
* @deprecated use `get_slug` instead.
*
* @param string $type
* @param bool $only_status_complete
*
* @return array
*/
public function get_element_slug_translations( $type, $only_status_complete = true ) {
$slug = $this->get_slug( $type );
$rows = array();
foreach ( $slug->get_language_codes() as $lang ) {
if ( $slug->get_original_lang() === $lang
|| ( $only_status_complete && ! $slug->is_translation_complete( $lang ) )
) {
continue;
}
$rows[] = (object) array(
'value' => $slug->get_value( $lang ),
'language' => $lang,
'status' => $slug->get_status( $lang ),
);
}
return $rows;
}
/**
* @deprecated use `get_slug` instead.
*
* @param array $types
*
* @return array
*/
public function get_all_slug_translations( $types ) {
$rows = array();
foreach ( $types as $type ) {
$slug = $this->get_slug( $type );
foreach ( $slug->get_language_codes() as $lang ) {
if ( $slug->get_original_lang() !== $lang ) {
$rows[] = (object) array(
'value' => $slug->get_value( $lang ),
'name' => $slug->get_name(),
);
}
}
}
return $rows;
}
/**
* @deprecated use `get_slug` instead.
*
* @param string $type
*
* @return array
*/
public function get_slug_translation_languages( $type ) {
$languages = array();
$slug = $this->get_slug( $type );
foreach ( $slug->get_language_codes() as $lang ) {
if ( $slug->is_translation_complete( $lang ) ) {
$languages[] = $lang;
}
}
return $languages;
}
/**
* Use `WPML_ST_String` only for updating the values in the DB
* because it does not have any caching feature.
*
* @param string $type
*
* @return null|WPML_ST_String
*/
public function get_slug_string( $type ) {
$string_id = $this->get_slug_id( $type );
if ( $string_id ) {
return new WPML_ST_String( $string_id, $this->wpdb );
}
return null;
}
/**
* @param string $slug
*
* @return string
*/
abstract protected function get_string_name( $slug );
/** @return string */
abstract protected function get_element_type();
}

View File

@@ -0,0 +1,391 @@
<?php
class WPML_Slug_Translation implements IWPML_Action {
const STRING_DOMAIN = 'WordPress';
/** @var array $post_link_cache */
private $post_link_cache = array();
/** @var SitePress $sitepress */
private $sitepress;
/** @var WPML_Slug_Translation_Records_Factory $slug_records_factory */
private $slug_records_factory;
/** @var WPML_ST_Term_Link_Filter $term_link_filter */
private $term_link_filter;
/** @var WPML_Get_LS_Languages_Status $ls_languages_status */
private $ls_languages_status;
/** @var WPML_ST_Slug_Translation_Settings $slug_translation_settings */
private $slug_translation_settings;
private $ignore_post_type_link = false;
public function __construct(
SitePress $sitepress,
WPML_Slug_Translation_Records_Factory $slug_records_factory,
WPML_Get_LS_Languages_Status $ls_language_status,
WPML_ST_Term_Link_Filter $term_link_filter,
WPML_ST_Slug_Translation_Settings $slug_translation_settings
) {
$this->sitepress = $sitepress;
$this->slug_records_factory = $slug_records_factory;
$this->ls_languages_status = $ls_language_status;
$this->term_link_filter = $term_link_filter;
$this->slug_translation_settings = $slug_translation_settings;
}
public function add_hooks() {
add_action( 'init', array( $this, 'init' ), WPML_Slug_Translation_Factory::INIT_PRIORITY );
}
public function init() {
$this->migrate_global_enabled_setting();
if ( $this->slug_translation_settings->is_enabled() ) {
add_filter( 'post_type_link', array( $this, 'post_type_link_filter' ), apply_filters( 'wpml_post_type_link_priority', 1 ), 4 );
add_filter( 'pre_term_link', array( $this->term_link_filter, 'replace_slug_in_termlink' ), 1, 2 ); // high priority
add_filter( 'edit_post', array( $this, 'clear_post_link_cache' ), 1, 2 );
add_filter( 'query_vars', array( $this, 'add_cpt_names' ), 1, 2 );
add_filter( 'pre_get_posts', array( $this, 'filter_pre_get_posts' ), - 1000, 2 );
}
if ( is_admin() ) {
add_action( 'icl_ajx_custom_call', array( $this, 'gui_save_options' ), 10, 2 );
add_action( 'wp_loaded', array( $this, 'maybe_migrate_string_name' ), 10, 0 );
}
}
/**
* @deprecated since 2.8.0, use the class `WPML_Post_Slug_Translation_Records` instead.
*
* @param string $type
*
* @return null|string
*/
public static function get_slug_by_type( $type ) {
$slug_records_factory = new WPML_Slug_Translation_Records_Factory();
$slug_records = $slug_records_factory->create( WPML_Slug_Translation_Factory::POST );
return $slug_records->get_original( $type );
}
/**
* This method is only for CPT
*
* @deprecated use `WPML_ST_Slug::filter_value` directly of the filter hook `wpml_get_translated_slug`
*
* @param string $slug_value
* @param string $post_type
* @param string|bool $language
*
* @return string
*/
public function get_translated_slug( $slug_value, $post_type, $language = false ) {
if ( $post_type ) {
$language = $language ? $language : $this->sitepress->get_current_language();
$slug = $this->slug_records_factory->create( WPML_Slug_Translation_Factory::POST )
->get_slug( $post_type );
return $slug->filter_value( $slug_value, $language );
}
return $slug_value;
}
/**
* @param array $value
*
* @return array
* @deprecated Use WPML\ST\SlugTranslation\Hooks\Hooks::filter
*/
public static function rewrite_rules_filter( $value ) {
return ( new \WPML\ST\SlugTranslation\Hooks\HooksFactory() )->create()->filter( $value );
}
/**
* @param string $post_link
* @param WP_Post $post
* @param bool $leavename
* @param bool $sample
*
* @return mixed|string|WP_Error
*/
public function post_type_link_filter( $post_link, $post, $leavename, $sample ) {
if ( $this->ignore_post_type_link ) {
return $post_link;
}
if ( ! $this->sitepress->is_translated_post_type( $post->post_type )
|| ! ( $ld = $this->sitepress->get_element_language_details( $post->ID, 'post_' . $post->post_type ) )
) {
return $post_link;
}
$ld = apply_filters( 'wpml_st_post_type_link_filter_language_details', $ld );
$cache_key = $leavename . '#' . $sample;
$cache_key .= $this->ls_languages_status->is_getting_ls_languages() ? 'yes' : 'no';
$cache_key .= $ld->language_code;
$blog_id = get_current_blog_id();
if ( isset( $this->post_link_cache[ $blog_id ][ $post->ID ][ $cache_key ] ) ) {
$post_link = $this->post_link_cache[ $blog_id ][ $post->ID ][ $cache_key ];
} else {
$slug_settings = $this->sitepress->get_setting( 'posts_slug_translation' );
$slug_settings = ! empty( $slug_settings['types'][ $post->post_type ] ) ? $slug_settings['types'][ $post->post_type ] : null;
if ( (bool) $slug_settings === true ) {
$post_type_obj = get_post_type_object( $post->post_type );
$slug_this = isset( $post_type_obj->rewrite['slug'] ) ? trim( $post_type_obj->rewrite['slug'], '/' ) : false;
$slug_real = $this->get_translated_slug( $slug_this, $post->post_type, $ld->language_code );
if ( empty( $slug_real ) || empty( $slug_this ) || $slug_this == $slug_real ) {
return $post_link;
}
global $wp_rewrite;
if ( isset( $wp_rewrite->extra_permastructs[ $post->post_type ] ) ) {
$struct_original = $wp_rewrite->extra_permastructs[ $post->post_type ]['struct'];
/**
* This hook allows to filter the slug we want to search and replace
* in the permalink structure. This is required for 3rd party
* plugins replacing the original slug with a placeholder.
*
* @since 3.1.0
*
* @param string $slug_this The original slug.
* @param string $post_link The initial link.
* @param WP_Post $post The post.
* @param bool $leavename Whether to keep the post name.
* @param bool $sample Is it a sample permalink.
*/
$slug_this = apply_filters( 'wpml_st_post_type_link_filter_original_slug', $slug_this, $post_link, $post, $leavename, $sample );
$lslash = false !== strpos( $struct_original, '/' . $slug_this ) ? '/' : '';
$wp_rewrite->extra_permastructs[ $post->post_type ]['struct'] = preg_replace(
'@' . $lslash . $slug_this . '/@',
$lslash . $slug_real . '/',
$struct_original
);
$this->ignore_post_type_link = true;
$post_link = get_post_permalink( $post->ID, $leavename, $sample );
$this->ignore_post_type_link = false;
$wp_rewrite->extra_permastructs[ $post->post_type ]['struct'] = $struct_original;
} else {
$post_link = str_replace( $slug_this . '=', $slug_real . '=', $post_link );
}
}
$this->post_link_cache[ $blog_id ][ $post->ID ][ $cache_key ] = $post_link;
}
return $post_link;
}
/**
* @param int $post_ID
* @param \WP_Post $post
*/
public function clear_post_link_cache( $post_ID, $post ) {
$blog_id = get_current_blog_id();
unset( $this->post_link_cache[ $blog_id ][ $post_ID ] );
}
/**
* @return array
*/
private function get_all_post_slug_translations() {
$slug_translations = array();
$post_slug_translation_settings = $this->sitepress->get_setting( 'posts_slug_translation' );
if ( isset( $post_slug_translation_settings['types'] ) ) {
$types = $post_slug_translation_settings['types'];
$cache_key = 'WPML_Slug_Translation::get_all_slug_translations' . md5( json_encode( $types ) );
$slug_translations = wp_cache_get( $cache_key );
if ( ! is_array( $slug_translations ) ) {
$slug_translations = array();
$types_to_fetch = array();
foreach ( $types as $type => $state ) {
if ( $state ) {
$types_to_fetch[] = str_replace( '%', '%%', $type );
}
}
if ( $types_to_fetch ) {
$data = $this->slug_records_factory
->create( WPML_Slug_Translation_Factory::POST )
->get_all_slug_translations( $types_to_fetch );
foreach ( $data as $row ) {
foreach ( $types_to_fetch as $type ) {
if ( preg_match( '#\s' . $type . '$#', $row->name ) === 1 ) {
$slug_translations[ $row->value ] = $type;
}
}
}
}
wp_cache_set( $cache_key, $slug_translations );
}
}
return $slug_translations;
}
/**
* Adds all translated custom post type slugs as valid query variables in addition to their original values
*
* @param array $qvars
*
* @return array
*/
public function add_cpt_names( $qvars ) {
$all_slugs_translations = array_keys( $this->get_all_post_slug_translations() );
$qvars = array_merge( $qvars, $all_slugs_translations );
return $qvars;
}
/**
* @param WP_Query $query
*
* @return WP_Query
*/
public function filter_pre_get_posts( $query ) {
/** Do not alter the query if it has already resolved the post ID */
if ( ! empty( $query->query_vars['p'] ) ) {
return $query;
}
$all_slugs_translations = $this->get_all_post_slug_translations();
foreach ( $query->query as $slug => $post_name ) {
if ( isset( $all_slugs_translations[ $slug ] ) ) {
$new_slug = isset( $all_slugs_translations[ $slug ] ) ? $all_slugs_translations[ $slug ] : $slug;
unset( $query->query[ $slug ] );
$query->query[ $new_slug ] = $post_name;
$query->query['name'] = $post_name;
$query->query['post_type'] = $new_slug;
unset( $query->query_vars[ $slug ] );
$query->query_vars[ $new_slug ] = $post_name;
$query->query_vars['name'] = $post_name;
$query->query_vars['post_type'] = $new_slug;
}
}
return $query;
}
/**
* @param string $action
*/
public static function gui_save_options( $action ) {
switch ( $action ) {
case 'icl_slug_translation':
global $sitepress;
$is_enabled = intval( ! empty( $_POST['icl_slug_translation_on'] ) );
$settings = new WPML_ST_Post_Slug_Translation_Settings( $sitepress );
$settings->set_enabled( $is_enabled );
echo '1|' . $is_enabled;
break;
}
}
/**
* @param string $slug
*
* @return string
*/
public static function sanitize( $slug ) {
// we need to preserve the %
$slug = str_replace( '%', '%45', $slug );
$slug = sanitize_title_with_dashes( $slug );
$slug = str_replace( '%45', '%', $slug );
/**
* Filters the sanitized post type or taxonomy slug translation
*
* @since 2.10.0
*
* @param string $slug
*/
return apply_filters( 'wpml_st_slug_translation_sanitize', $slug );
}
/**
* @deprecated since 2.8.0, use the class `WPML_Post_Slug_Translation_Records` instead.
*/
public static function register_string_for_slug( $post_type, $slug ) {
return icl_register_string( self::STRING_DOMAIN, 'URL slug: ' . $post_type, $slug );
}
public function maybe_migrate_string_name() {
global $wpdb;
$slug_settings = $this->sitepress->get_setting( 'posts_slug_translation' );
if ( ! isset( $slug_settings['string_name_migrated'] ) ) {
$queryable_post_types = get_post_types( array( 'publicly_queryable' => true ) );
foreach ( $queryable_post_types as $type ) {
$post_type_obj = get_post_type_object( $type );
if ( null === $post_type_obj || ! isset( $post_type_obj->rewrite['slug'] ) ) {
continue;
}
$slug = trim( $post_type_obj->rewrite['slug'], '/' );
if ( $slug ) {
// First check if we should migrate from the old format URL slug: slug
$string_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT id
FROM {$wpdb->prefix}icl_strings
WHERE name = %s AND value = %s",
'URL slug: ' . $slug,
$slug
)
);
if ( $string_id ) {
// migrate it to URL slug: post_type
$st_update['name'] = 'URL slug: ' . $type;
$wpdb->update( $wpdb->prefix . 'icl_strings', $st_update, array( 'id' => $string_id ) );
}
}
}
$slug_settings['string_name_migrated'] = true;
$this->sitepress->set_setting( 'posts_slug_translation', $slug_settings, true );
}
}
/**
* Move global on/off setting to its own option WPML_ST_Slug_Translation_Settings::KEY_ENABLED_GLOBALLY
*/
private function migrate_global_enabled_setting() {
$enabled = get_option( WPML_ST_Slug_Translation_Settings::KEY_ENABLED_GLOBALLY );
if ( false === $enabled ) {
$old_setting = $this->sitepress->get_setting( WPML_ST_Post_Slug_Translation_Settings::KEY_IN_SITEPRESS_SETTINGS );
if ( array_key_exists( 'on', $old_setting ) ) {
$enabled = (int) $old_setting['on'];
} else {
$enabled = 0;
}
update_option( WPML_ST_Slug_Translation_Settings::KEY_ENABLED_GLOBALLY, $enabled );
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
class WPML_ST_Slug_Custom_Type_Factory {
/** @var SitePress $sitepress */
private $sitepress;
/** @var WPML_Slug_Translation_Records $slug_records */
private $slug_records;
/** @var WPML_ST_Slug_Translations */
private $slug_translations;
public function __construct(
SitePress $sitepress,
WPML_Slug_Translation_Records $slug_records,
WPML_ST_Slug_Translations $slug_translations
) {
$this->sitepress = $sitepress;
$this->slug_records = $slug_records;
$this->slug_translations = $slug_translations;
}
/**
* @param string $name
* @param bool $display_as_translated
*
* @return WPML_ST_Slug_Custom_Type
*/
public function create( $name, $display_as_translated ) {
$slug = $this->slug_records->get_slug( $name );
return new WPML_ST_Slug_Custom_Type(
$name,
$display_as_translated,
$slug->get_original_value(),
$this->slug_translations->get( $slug, $display_as_translated )
);
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* It may represent custom posts or custom taxonomies
*/
class WPML_ST_Slug_Custom_Type {
/** @var string */
private $name;
/** @var bool */
private $display_as_translated;
/** @var string */
private $slug;
/** @var string */
private $slug_translation;
/**
* WPML_ST_Slug_Custom_Type constructor.
*
* @param string $name
* @param bool $display_as_translated
* @param bool $slug
* @param bool $slug_translation
*/
public function __construct( $name, $display_as_translated, $slug, $slug_translation ) {
$this->name = $name;
$this->display_as_translated = $display_as_translated;
$this->slug = $slug;
$this->slug_translation = $slug_translation;
}
/**
* @return string
*/
public function get_name() {
return $this->name;
}
/**
* @return bool
*/
public function is_display_as_translated() {
return $this->display_as_translated;
}
/**
* @return string
*/
public function get_slug() {
return $this->slug;
}
/**
* @return string
*/
public function get_slug_translation() {
return $this->slug_translation;
}
/**
* @return bool
*/
public function is_using_tags() {
$pattern = '#%([^/]+)%#';
return preg_match( $pattern, $this->slug ) || preg_match( $pattern, $this->slug_translation );
}
}

View File

@@ -0,0 +1,8 @@
<?php
interface WPML_ST_Slug_Translation_Custom_Types_Repository {
/**
* @return WPML_ST_Slug_Custom_Type[]
*/
public function get();
}

View File

@@ -0,0 +1,71 @@
<?php
class WPML_ST_Slug_Translation_Post_Custom_Types_Repository implements WPML_ST_Slug_Translation_Custom_Types_Repository {
/** @var SitePress */
private $sitepress;
/** @var WPML_ST_Slug_Custom_Type_Factory */
private $custom_type_factory;
/** @var array */
private $post_slug_translation_settings;
public function __construct( SitePress $sitepress, WPML_ST_Slug_Custom_Type_Factory $custom_type_factory ) {
$this->sitepress = $sitepress;
$this->custom_type_factory = $custom_type_factory;
}
public function get() {
return array_map(
array( $this, 'build_object' ),
array_values( array_filter(
get_post_types( array( 'publicly_queryable' => true ) ),
array( $this, 'filter' )
) )
);
}
/**
* @param string $type
*
* @return bool
*/
private function filter( $type ) {
$post_slug_translation_settings = $this->get_post_slug_translation_settings();
return isset( $post_slug_translation_settings['types'][ $type ] )
&& $post_slug_translation_settings['types'][ $type ]
&& $this->sitepress->is_translated_post_type( $type );
}
/**
* @param string $type
*
* @return WPML_ST_Slug_Custom_Type
*/
private function build_object( $type ) {
return $this->custom_type_factory->create( $type, $this->is_display_as_translated( $type ) );
}
/**
* @return array
*/
private function get_post_slug_translation_settings() {
if ( null === $this->post_slug_translation_settings ) {
$this->post_slug_translation_settings = $this->sitepress->get_setting( 'posts_slug_translation', array() );
}
return $this->post_slug_translation_settings;
}
/**
* @param string $type
*
* @return bool
*/
private function is_display_as_translated( $type ) {
return $this->sitepress->is_display_as_translated_post_type( $type );
}
}

View File

@@ -0,0 +1,79 @@
<?php
class WPML_ST_Slug_Translation_Taxonomy_Custom_Types_Repository implements WPML_ST_Slug_Translation_Custom_Types_Repository {
/** @var SitePress */
private $sitepress;
/** @var WPML_ST_Slug_Custom_Type_Factory */
private $custom_type_factory;
/** @var WPML_ST_Tax_Slug_Translation_Settings $settings */
private $settings_repository;
/** @var array */
private $settings;
public function __construct(
SitePress $sitepress,
WPML_ST_Slug_Custom_Type_Factory $custom_type_factory,
WPML_ST_Tax_Slug_Translation_Settings $settings_repository
) {
$this->sitepress = $sitepress;
$this->custom_type_factory = $custom_type_factory;
$this->settings_repository = $settings_repository;
}
public function get() {
return array_map(
array( $this, 'build_object' ),
array_values( array_filter(
get_taxonomies( array( 'publicly_queryable' => true ) ),
array( $this, 'filter' )
) )
);
}
/**
* @param string $type
*
* @return bool
*/
private function filter( $type ) {
$settings = $this->get_taxonomy_slug_translation_settings();
return isset( $settings[ $type ] )
&& $settings[ $type ]
&& $this->sitepress->is_translated_taxonomy( $type );
}
/**
* @param string $type
*
* @return WPML_ST_Slug_Custom_Type
*/
private function build_object( $type ) {
return $this->custom_type_factory->create( $type, $this->is_display_as_translated( $type ) );
}
/**
* @return array
*/
private function get_taxonomy_slug_translation_settings() {
if ( null === $this->settings ) {
$this->settings = $this->settings_repository->get_types();
}
return $this->settings;
}
/**
* @param string $type
*
* @return bool
*/
private function is_display_as_translated( $type ) {
return $this->sitepress->is_display_as_translated_taxonomy( $type );
}
}

View File

@@ -0,0 +1,41 @@
<?php
class WPML_ST_Slug_Translations {
/** @var SitePress */
private $sitepress;
public function __construct( SitePress $sitepress ) {
$this->sitepress = $sitepress;
}
/**
* @param WPML_ST_Slug $slug
* @param bool $display_as_translated_mode
*
* @return string
*/
public function get( $slug, $display_as_translated_mode ) {
$current_language = $this->sitepress->get_current_language();
$default_language = $this->sitepress->get_default_language();
$slug_translation = $this->get_slug_translation_to_lang( $slug, $current_language );
if ( ! $slug_translation ) {
// check original
$slug_translation = $slug->get_original_value();
}
if ( $display_as_translated_mode && ( ! $slug_translation || $slug_translation === $slug->get_original_value() ) && $default_language != 'en' ) {
$slug_translation = $this->get_slug_translation_to_lang( $slug, $default_language );
}
return trim( $slug_translation, '/' );
}
private function get_slug_translation_to_lang( WPML_ST_Slug $slug, $lang ) {
if ( $slug->is_translation_complete( $lang ) ) {
return $slug->get_value( $lang );
}
return null;
}
}

View File

@@ -0,0 +1,7 @@
<?php
interface IWPML_ST_Rewrite_Rule_Filter {
public function rewrite_rules_filter( $rules );
}

View File

@@ -0,0 +1,113 @@
<?php
use WPML\FP\Fns;
use function WPML\FP\partial;
class WPML_ST_Slug_New_Match_Finder {
/**
* @param string $match
* @param WPML_ST_Slug_Custom_Type[] $custom_types
*
* @return WPML_ST_Slug_New_Match
*/
public function get( $match, array $custom_types ) {
$best_match = $this->find_the_best_match( $match, $this->map_to_new_matches( $match, $custom_types ) );
if ( ! $best_match ) {
$best_match = new WPML_ST_Slug_New_Match( $match, false );
}
return $best_match;
}
/**
* @param string $match
* @param WPML_ST_Slug_Custom_Type[] $custom_types
*
* @return WPML_ST_Slug_New_Match[]
*/
private function map_to_new_matches( $match, array $custom_types ) {
return Fns::map( partial( [ $this, 'find_match_of_type' ], $match ), $custom_types );
}
/**
* @param string $match
* @param WPML_ST_Slug_Custom_Type $custom_type
*
* @return WPML_ST_Slug_New_Match
*/
public function find_match_of_type( $match, WPML_ST_Slug_Custom_Type $custom_type ) {
if ( $custom_type->is_using_tags() ) {
$slug = $this->filter_slug_using_tag( $custom_type->get_slug() );
$slug_translation = $this->filter_slug_using_tag( $custom_type->get_slug_translation() );
$new_match = $this->adjust_match( $match, $slug, $slug_translation );
$result = new WPML_ST_Slug_New_Match( $new_match, $custom_type->is_display_as_translated() );
} else {
$new_match = $this->adjust_match( $match, $custom_type->get_slug(), $custom_type->get_slug_translation() );
$result = new WPML_ST_Slug_New_Match(
$new_match,
$match !== $new_match && $custom_type->is_display_as_translated()
);
}
return $result;
}
private function filter_slug_using_tag( $slug ) {
if ( preg_match( '#%([^/]+)%#', $slug ) ) {
$slug = preg_replace( '#%[^/]+%#', '.+?', $slug );
}
if ( preg_match( '#\.\+\?#', $slug ) ) {
$slug = preg_replace( '#\.\+\?#', '(.+?)', $slug );
}
return $slug;
}
/**
* @param string $match
* @param string $slug
* @param string $slug_translation
*
* @return string
*/
private function adjust_match( $match, $slug, $slug_translation ) {
if (
! empty( $slug_translation )
&& preg_match( '#^[^/]*/?\(?' . preg_quote( $slug ) . '\)?/#', $match )
&& $slug !== $slug_translation
) {
$replace = function( $match ) use ( $slug, $slug_translation ) {
return str_replace( $slug, $slug_translation, $match[0]);
};
$match = preg_replace_callback( '#^\(?' . preg_quote( addslashes( $slug ) ) . '\)?/#', $replace, $match );
}
return $match;
}
/**
* The best is that which differs the most from the original
*
* @param string $match
* @param WPML_ST_Slug_New_Match[] $new_matches
*
* @return WPML_ST_Slug_New_Match
*/
private function find_the_best_match( $match, $new_matches ) {
$similarities = array();
foreach ( $new_matches as $new_match ) {
$percent = 0;
similar_text( $match, $new_match->get_value(), $percent );
// Multiply $percent by 100 because floats as array keys are truncated to integers
// This will allow for fractional percentages.
$similarities[ intval( $percent * 100 ) ] = $new_match;
}
ksort( $similarities );
return reset( $similarities );
}
}

View File

@@ -0,0 +1,32 @@
<?php
class WPML_ST_Slug_New_Match {
/** @var string */
private $value;
/** @var bool */
private $preserve_original;
/**
* @param string $value
* @param bool $preserve_original
*/
public function __construct( $value, $preserve_original ) {
$this->value = $value;
$this->preserve_original = $preserve_original;
}
/**
* @return string
*/
public function get_value() {
return $this->value;
}
/**
* @return bool
*/
public function should_preserve_original() {
return $this->preserve_original;
}
}

View File

@@ -0,0 +1,20 @@
<?php
class WPML_Post_Slug_Translation_Records extends WPML_Slug_Translation_Records {
const STRING_NAME = 'URL slug: %s';
/**
* @param string $slug
*
* @return string
*/
protected function get_string_name( $slug ) {
return sprintf( self::STRING_NAME, $slug );
}
/** @return string */
protected function get_element_type() {
return 'post';
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* @todo: Move these settings to an independent option
* like WPML_ST_Tax_Slug_Translation_Settings::OPTION_NAME
*/
class WPML_ST_Post_Slug_Translation_Settings extends WPML_ST_Slug_Translation_Settings {
const KEY_IN_SITEPRESS_SETTINGS = 'posts_slug_translation';
/** @var SitePress $sitepress */
private $sitepress;
private $settings;
public function __construct( SitePress $sitepress ) {
$this->sitepress = $sitepress;
$this->settings = $sitepress->get_setting( self::KEY_IN_SITEPRESS_SETTINGS, array( ) );
}
/** @param bool $enabled */
public function set_enabled( $enabled ) {
parent::set_enabled( $enabled );
/**
* Backward compatibility with 3rd part plugins
* The `on` key has been replaced by an independent option
* WPML_ST_Slug_Translation_Settings::KEY_ENABLED_GLOBALLY
*/
$this->settings['on'] = (int) $enabled;
}
/**
* @param string $type
*
* @return bool
*/
public function is_translated( $type ) {
return ! empty( $this->settings['types'][ $type ] );
}
/**
* @param string $type
* @param bool $is_enabled
*/
public function set_type( $type, $is_enabled ) {
if ( $is_enabled ) {
$this->settings['types'][ $type ] = 1;
} else {
unset( $this->settings['types'][ $type ] );
}
}
public function save() {
$this->sitepress->set_setting( self::KEY_IN_SITEPRESS_SETTINGS, $this->settings, true );
}
}

View File

@@ -0,0 +1,59 @@
<?php
class WPML_ST_Tax_Slug_Translation_Settings extends WPML_ST_Slug_Translation_Settings {
const OPTION_NAME = 'wpml_tax_slug_translation_settings';
/** @var array $types */
private $types = array();
public function __construct() {
$this->init();
}
/** @param array $types */
public function set_types( array $types ) {
$this->types = $types;
}
/** @return array */
public function get_types() {
return $this->types;
}
/**
* @param string $taxonomy_name
*
* @return bool
*/
public function is_translated( $taxonomy_name ) {
return array_key_exists( $taxonomy_name, $this->types ) && (bool) $this->types[ $taxonomy_name ];
}
/**
* @param string $taxonomy_name
* @param bool $is_enabled
*/
public function set_type( $taxonomy_name, $is_enabled ) {
$this->types[ $taxonomy_name ] = (int) $is_enabled;
}
/** @return array */
private function get_properties() {
return get_object_vars( $this );
}
public function init() {
$options = get_option( self::OPTION_NAME, array() );
foreach ( $this->get_properties() as $name => $value ) {
if ( array_key_exists( $name, $options ) ) {
call_user_func( array( $this, 'set_' . $name ), $options[ $name ] );
}
}
}
public function save() {
update_option( self::OPTION_NAME, $this->get_properties() );
}
}

View File

@@ -0,0 +1,87 @@
<?php
class WPML_ST_Term_Link_Filter {
const CACHE_GROUP = 'WPML_ST_Term_Link_Filter::replace_base_in_permalink_structure';
/** @var SitePress $sitepress */
private $sitepress;
/** @var WPML_Tax_Slug_Translation_Records $slug_records */
private $slug_records;
/** @var WPML_WP_Cache_Factory $cache_factory */
private $cache_factory;
/** @var WPML_ST_Tax_Slug_Translation_Settings $tax_settings */
private $tax_settings;
public function __construct(
WPML_Tax_Slug_Translation_Records $slug_records,
SitePress $sitepress,
WPML_WP_Cache_Factory $cache_factory,
WPML_ST_Tax_Slug_Translation_Settings $tax_settings
) {
$this->slug_records = $slug_records;
$this->sitepress = $sitepress;
$this->cache_factory = $cache_factory;
$this->tax_settings = $tax_settings;
}
/**
* Filters the permalink structure for a terms before token replacement occurs
* with the hook filter `pre_term_link` available since WP 4.9.0
*
* @see get_term_link
*
* @param false|string $termlink
* @param WP_Term $term
*
* @return false|string
*/
public function replace_slug_in_termlink( $termlink, $term ) {
if ( ! $termlink || ! $this->sitepress->is_translated_taxonomy( $term->taxonomy ) ) {
return $termlink;
}
$term_lang = $this->sitepress->get_language_for_element( $term->term_taxonomy_id, 'tax_' . $term->taxonomy );
$cache_key = $termlink . $term_lang;
$cache_item = $this->cache_factory->create_cache_item( self::CACHE_GROUP, $cache_key );
if ( $cache_item->exists() ) {
$termlink = $cache_item->get();
} else {
if ( $this->tax_settings->is_translated( $term->taxonomy ) ) {
$original_slug = $this->slug_records->get_original( $term->taxonomy );
$translated_slug = $this->slug_records->get_translation( $term->taxonomy, $term_lang );
if ( $original_slug && $translated_slug && $original_slug !== $translated_slug ) {
$termlink = $this->replace_slug( $termlink, $original_slug, $translated_slug );
}
}
$cache_item->set( $termlink );
}
return $termlink;
}
/**
* @param string $termlink
* @param string $original_slug
* @param string $translated_slug
*
* @return string
*/
private function replace_slug( $termlink, $original_slug, $translated_slug ) {
if ( preg_match( '#/?' . preg_quote( $original_slug ) . '/#', $termlink ) ) {
$termlink = preg_replace(
'#^(/?)(' . addslashes( $original_slug ) . ')/#',
"$1$translated_slug/",
$termlink
);
}
return $termlink;
}
}

View File

@@ -0,0 +1,20 @@
<?php
class WPML_Tax_Slug_Translation_Records extends WPML_Slug_Translation_Records {
const STRING_NAME = 'URL %s tax slug';
/**
* @param string $slug
*
* @return string
*/
protected function get_string_name( $slug ) {
return sprintf( self::STRING_NAME, $slug );
}
/** @return string */
protected function get_element_type() {
return 'taxonomy';
}
}

View File

@@ -0,0 +1,40 @@
<?php
class WPML_Rewrite_Rule_Filter_Factory {
/**
* @param SitePress|null $sitepress
*
* @return WPML_Rewrite_Rule_Filter
*/
public function create( $sitepress = null ) {
if ( ! $sitepress ) {
global $sitepress;
}
$slug_records_factory = new WPML_Slug_Translation_Records_Factory();
$slug_translations = new WPML_ST_Slug_Translations( $sitepress );
$custom_types_repositories = array(
new WPML_ST_Slug_Translation_Post_Custom_Types_Repository(
$sitepress,
new WPML_ST_Slug_Custom_Type_Factory(
$sitepress,
$slug_records_factory->create( WPML_Slug_Translation_Factory::POST ),
$slug_translations
)
),
new WPML_ST_Slug_Translation_Taxonomy_Custom_Types_Repository(
$sitepress,
new WPML_ST_Slug_Custom_Type_Factory(
$sitepress,
$slug_records_factory->create( WPML_Slug_Translation_Factory::TAX ),
$slug_translations
),
new WPML_ST_Tax_Slug_Translation_Settings()
),
);
return new WPML_Rewrite_Rule_Filter( $custom_types_repositories, new WPML_ST_Slug_New_Match_Finder() );
}
}

View File

@@ -0,0 +1,70 @@
<?php
class WPML_Slug_Translation_Factory implements IWPML_Frontend_Action_Loader, IWPML_Backend_Action_Loader, IWPML_AJAX_Action_Loader {
const POST = 'post';
const TAX = 'taxonomy';
const INIT_PRIORITY = -1000;
public function create() {
global $sitepress;
$hooks = array();
$records_factory = new WPML_Slug_Translation_Records_Factory();
$settings_factory = new WPML_ST_Slug_Translation_Settings_Factory();
$post_records = $records_factory->create( self::POST );
$tax_records = $records_factory->create( self::TAX );
$global_settings = $settings_factory->create();
$post_settings = $settings_factory->create( self::POST );
$tax_settings = $settings_factory->create( self::TAX );
$term_link_filter = new WPML_ST_Term_Link_Filter( $tax_records, $sitepress, new WPML_WP_Cache_Factory(), $tax_settings );
$hooks['legacy_class'] = new WPML_Slug_Translation(
$sitepress,
$records_factory,
WPML_Get_LS_Languages_Status::get_instance(),
$term_link_filter,
$global_settings
);
$hooks['rewrite_rules'] = ( new \WPML\ST\SlugTranslation\Hooks\HooksFactory() )->create();
if ( is_admin() ) {
$hooks['ui_save_post'] = new WPML_ST_Slug_Translation_UI_Save(
$post_settings,
$post_records,
$sitepress,
new WPML_WP_Post_Type(),
WPML_ST_Slug_Translation_UI_Save::ACTION_HOOK_FOR_POST
);
$hooks['ui_save_tax'] = new WPML_ST_Slug_Translation_UI_Save(
$tax_settings,
$tax_records,
$sitepress,
new WPML_WP_Taxonomy(),
WPML_ST_Slug_Translation_UI_Save::ACTION_HOOK_FOR_TAX
);
if ( $global_settings->is_enabled() ) {
$hooks['sync_strings'] = new WPML_ST_Slug_Translation_Strings_Sync(
$records_factory,
$settings_factory
);
}
}
$hooks['public-api'] = new WPML_ST_Slug_Translation_API(
$records_factory,
$settings_factory,
$sitepress,
new WPML_WP_API()
);
return $hooks;
}
}

View File

@@ -0,0 +1,24 @@
<?php
class WPML_Slug_Translation_Records_Factory {
/**
* @param string $type
*
* @return WPML_Post_Slug_Translation_Records|WPML_Tax_Slug_Translation_Records
*/
public function create( $type ) {
/** @var wpdb */
global $wpdb;
$cache_factory = new WPML_WP_Cache_Factory();
if ( WPML_Slug_Translation_Factory::POST === $type ) {
return new WPML_Post_Slug_Translation_Records( $wpdb, $cache_factory );
} elseif ( WPML_Slug_Translation_Factory::TAX === $type ) {
return new WPML_Tax_Slug_Translation_Records( $wpdb, $cache_factory );
}
return null;
}
}

View File

@@ -0,0 +1,165 @@
<?php
class WPML_ST_Element_Slug_Translation_UI_Model {
/** @var SitePress $sitepress */
private $sitepress;
/** @var WPML_ST_Slug_Translation_Settings $settings */
private $settings;
/** @var WPML_Slug_Translation_Records $slug_records */
private $slug_records;
/** @var WPML_Element_Sync_Settings $sync_settings */
private $sync_settings;
/** @var WPML_Simple_Language_Selector $lang_selector */
private $lang_selector;
public function __construct(
SitePress $sitepress,
WPML_ST_Slug_Translation_Settings $settings,
WPML_Slug_Translation_Records $slug_records,
WPML_Element_Sync_Settings $sync_settings,
WPML_Simple_Language_Selector $lang_selector
) {
$this->sitepress = $sitepress;
$this->settings = $settings;
$this->slug_records = $slug_records;
$this->sync_settings = $sync_settings;
$this->lang_selector = $lang_selector;
}
/**
* @param string $type_name
* @param WP_Post_Type|WP_Taxonomy $custom_type
*
* @return null|array
*/
public function get( $type_name, $custom_type ) {
$has_rewrite_slug = isset( $custom_type->rewrite['slug'] ) && $custom_type->rewrite['slug'];
$is_translated_mode = $this->sync_settings->is_sync( $type_name );
$is_slug_translated = $this->settings->is_translated( $type_name );
if ( ! $has_rewrite_slug || ! $this->settings->is_enabled() ) {
return null;
}
$original_slug_and_lang = $this->get_original_slug_and_lang( $type_name, $custom_type );
$slug_translations = $this->get_translations( $type_name );
$model = array(
'strings' => array(
'toggle_slugs_table' => sprintf( __( 'Set different slugs in different languages for %s.', 'wpml-string-translation' ), $custom_type->labels->name ),
'slug_status_incomplete' => __( "Not marked as 'complete'. Press 'Save' to enable.", 'wpml-string-translation' ),
'original_label' => __( '(original)', 'wpml-string-translation' ),
),
'css_class_wrapper' => $is_translated_mode ? '' : 'hidden',
'type_name' => $type_name,
'slugs' => array(),
'has_missing_translations_message' => '',
);
if ( $is_slug_translated && ! $original_slug_and_lang->is_registered ) {
$model['has_missing_translations_message'] = sprintf(
esc_html__(
'%s slugs are set to be translated, but they are missing their translation',
'wpml-string-translation'
),
$custom_type->labels->name
);
}
$languages = $this->get_languages( $original_slug_and_lang->language );
foreach ( $languages as $code => $language ) {
$slug = new stdClass();
$slug->value = ! empty( $slug_translations[ $code ]['value'] )
? $slug_translations[ $code ]['value'] : '';
$slug->placeholder = $original_slug_and_lang->value . ' @' . $code;
$slug->input_id = sprintf( 'translate_slugs[%s][langs][%s]', $type_name, $code );
$slug->language_flag = $this->sitepress->get_flag_img( $code );
$slug->language_name = $language['display_name'];
$slug->language_code = $code;
$slug->is_original = $code == $original_slug_and_lang->language;
$slug->status_is_incomplete = isset( $slug_translations[ $code ] )
&& ICL_TM_COMPLETE != $slug_translations[ $code ]['status'];
if ( $slug->is_original ) {
$slug->value = $original_slug_and_lang->value;
$slug->language_selector = $this->lang_selector->render(
array(
'name' => 'translate_slugs[' . $type_name . '][original]',
'selected' => $code,
'show_please_select' => false,
'echo' => false,
'class' => 'js-translate-slug-original',
'data' => array( 'slug' => $slug->value ),
)
);
}
$model['slugs'][ $slug->language_code ] = $slug;
}
return $model;
}
/**
* @param string $type_name
* @param WP_Post_Type|WP_Taxonomy $custom_type
*
* @return stdClass
*/
private function get_original_slug_and_lang( $type_name, $custom_type ) {
$original_slug_and_lang = $this->slug_records->get_original_slug_and_lang( $type_name );
if ( $original_slug_and_lang ) {
$original_slug_and_lang->is_registered = true;
} else {
$original_slug_and_lang = new stdClass();
$original_slug_and_lang->is_registered = false;
$original_slug_and_lang->value = isset( $custom_type->slug )
? $custom_type->slug : $custom_type->rewrite['slug'];
$original_slug_and_lang->language = $this->sitepress->get_default_language();
}
return $original_slug_and_lang;
}
/**
* @param string $type_name
*
* @return array
*/
private function get_translations( $type_name ) {
$translations = array();
$rows = $this->slug_records->get_element_slug_translations( $type_name, false );
foreach( $rows as $row ) {
$translations[ $row->language ] = array(
'value' => $row->value,
'status' => $row->status
);
}
return $translations;
}
/**
* @param string $string_lang
*
* @return array
*/
private function get_languages( $string_lang ) {
$languages = $this->sitepress->get_active_languages();
if ( ! in_array( $string_lang, array_keys( $languages ) ) ) {
$all_languages = $this->sitepress->get_languages();
$languages[ $string_lang ] = $all_languages[ $string_lang ];
}
return $languages;
}
}

View File

@@ -0,0 +1,58 @@
<?php
class WPML_ST_Element_Slug_Translation_UI {
const TEMPLATE_FILE = 'slug-translation-ui.twig';
/** @var WPML_ST_Element_Slug_Translation_UI_Model $model */
private $model;
/** @var IWPML_Template_Service $template_service */
private $template_service;
public function __construct(
WPML_ST_Element_Slug_Translation_UI_Model $model,
IWPML_Template_Service $template_service
) {
$this->model = $model;
$this->template_service = $template_service;
}
/** @return WPML_ST_Element_Slug_Translation_UI */
public function init() {
wp_enqueue_script(
'wpml-custom-type-slug-ui',
WPML_ST_URL . '/res/js/wpml-custom-type-slug-ui.js',
array( 'jquery' ),
WPML_ST_VERSION,
true
);
return $this;
}
/**
* @param string $type_name
* @param WP_Post_Type|WP_Taxonomy $custom_type
*
* @return string
*/
public function render( $type_name, $custom_type ) {
$model = $this->model->get( $type_name, $custom_type );
if ( ! $model ) {
return '';
}
if ( ! empty( $model['has_missing_translations_message'] ) ) {
ICL_AdminNotifier::displayInstantMessage(
$model['has_missing_translations_message'],
'error',
'below-h2',
false
);
}
return $this->template_service->show( $model, self::TEMPLATE_FILE );
}
}

View File

@@ -0,0 +1,166 @@
<?php
class WPML_ST_Slug_Translation_API implements IWPML_Action {
/**
* The section indexes are hardcoded in `sitepress-multilingual-cms/menu/_custom_types_translation.php`
*/
const SECTION_INDEX_POST = 7;
const SECTION_INDEX_TAX = 8;
/** @var WPML_Slug_Translation_Records_Factory $records_factory */
private $records_factory;
/** @var WPML_ST_Slug_Translation_Settings_Factory $settings_factory */
private $settings_factory;
/** @var IWPML_Current_Language $current_language */
private $current_language;
/** @var WPML_WP_API $wp_api */
private $wp_api;
public function __construct(
WPML_Slug_Translation_Records_Factory $records_factory,
WPML_ST_Slug_Translation_Settings_Factory $settings_factory,
IWPML_Current_Language $current_language,
WPML_WP_API $wp_api
) {
$this->records_factory = $records_factory;
$this->settings_factory = $settings_factory;
$this->current_language = $current_language;
$this->wp_api = $wp_api;
}
public function add_hooks() {
add_action( 'init', array( $this, 'init' ), WPML_Slug_Translation_Factory::INIT_PRIORITY );
}
public function init() {
if ( $this->settings_factory->create()->is_enabled() ) {
add_filter( 'wpml_get_translated_slug', array( $this, 'get_translated_slug_filter' ), 1, 4 );
add_filter(
'wpml_get_slug_translation_languages',
array( $this, 'get_slug_translation_languages_filter' ),
1,
3
);
add_filter( 'wpml_type_slug_is_translated', array( $this, 'type_slug_is_translated_filter' ), 10, 3 );
}
add_filter( 'wpml_slug_translation_available', '__return_true', 1 );
add_action( 'wpml_activate_slug_translation', array( $this, 'activate_slug_translation_action' ), 1, 3 );
add_filter( 'wpml_get_slug_translation_url', array( $this, 'get_slug_translation_url_filter' ), 1, 3 );
}
/**
* @param string $slug_value
* @param string $type
* @param string|bool $language
* @param string $element_type WPML_Slug_Translation_Factory::POST|WPML_Slug_Translation_Factory::TAX
*
* @return string
*/
public function get_translated_slug_filter(
$slug_value,
$type,
$language = false,
$element_type = WPML_Slug_Translation_Factory::POST
) {
if ( $type ) {
$slug = $this->records_factory->create( $element_type )->get_slug( $type );
if ( ! $language ) {
$language = $this->current_language->get_current_language();
}
return $slug->filter_value( $slug_value, $language );
}
return $slug_value;
}
/**
* @param string $languages
* @param string $type
* @param string $element_type WPML_Slug_Translation_Factory::POST|WPML_Slug_Translation_Factory::TAX
*
* @return array
*/
public function get_slug_translation_languages_filter(
$languages,
$type,
$element_type = WPML_Slug_Translation_Factory::POST
) {
return $this->records_factory->create( $element_type )->get_slug_translation_languages( $type );
}
/**
* @param string $type
* @param string|null $slug_value
* @param string $element_type WPML_Slug_Translation_Factory::POST|WPML_Slug_Translation_Factory::TAX
*/
public function activate_slug_translation_action(
$type,
$slug_value = null,
$element_type = WPML_Slug_Translation_Factory::POST
) {
if ( ! $slug_value ) {
$slug_value = $type;
}
$records = $this->records_factory->create( $element_type );
$settings = $this->settings_factory->create( $element_type );
$slug = $records->get_slug( $type );
if ( ! $slug->get_original_id() ) {
$records->register_slug( $type, $slug_value );
}
if ( ! $settings->is_enabled() || ! $settings->is_translated( $type ) ) {
$settings->set_enabled( true );
$settings->set_type( $type, true );
$settings->save();
}
}
/**
* @param string $url
* @param string $element_type WPML_Slug_Translation_Factory::POST or WPML_Slug_Translation_Factory::TAX
*
* @return string
*/
public function get_slug_translation_url_filter( $url, $element_type = WPML_Slug_Translation_Factory::POST ) {
$index = self::SECTION_INDEX_POST;
if ( WPML_Slug_Translation_Factory::TAX === $element_type ) {
$index = self::SECTION_INDEX_TAX;
}
$page = $this->wp_api->constant( 'WPML_PLUGIN_FOLDER' )
. '/menu/translation-options.php#ml-content-setup-sec-' . $index;
if ( $this->wp_api->defined( 'WPML_TM_VERSION' ) ) {
$page = $this->wp_api->constant( 'WPML_TM_FOLDER' )
. '/menu/settings&sm=mcsetup#ml-content-setup-sec-' . $index;
}
return admin_url( 'admin.php?page=' . $page );
}
/**
* @param bool $is_translated
* @param string $type
* @param string $element_type WPML_Slug_Translation_Factory::POST or WPML_Slug_Translation_Factory::TAX
*
* @return bool
*/
public function type_slug_is_translated_filter(
$is_translated,
$type,
$element_type = WPML_Slug_Translation_Factory::POST
) {
return $this->settings_factory->create( $element_type )->is_translated( $type );
}
}

View File

@@ -0,0 +1,28 @@
<?php
class WPML_ST_Slug_Translation_Settings_Factory {
/**
* @throws InvalidArgumentException
* @param string $element_type
*
* @return WPML_ST_Slug_Translation_Settings
*/
public function create( $element_type = null ) {
global $sitepress;
if ( WPML_Slug_Translation_Factory::POST === $element_type ) {
return new WPML_ST_Post_Slug_Translation_Settings( $sitepress );
}
if ( WPML_Slug_Translation_Factory::TAX === $element_type ) {
return new WPML_ST_Tax_Slug_Translation_Settings();
}
if ( ! $element_type ) {
return new WPML_ST_Slug_Translation_Settings();
}
throw new InvalidArgumentException( 'Invalid element type.' );
}
}

View File

@@ -0,0 +1,28 @@
<?php
class WPML_ST_Slug_Translation_Settings {
const KEY_ENABLED_GLOBALLY = 'wpml_base_slug_translation';
/** @param bool $enabled */
public function set_enabled( $enabled ) {
update_option( self::KEY_ENABLED_GLOBALLY, (int) $enabled );
}
/** @return bool */
public function is_enabled() {
return (bool) get_option( self::KEY_ENABLED_GLOBALLY );
}
public function is_translated( $type_name ) {
throw new Exception( 'Use a child class with the proper element type: post or taxonomy.' );
}
public function set_type( $type, $is_type_enabled ) {
throw new Exception( 'Use a child class with the proper element type: post or taxonomy.' );
}
public function save() {
throw new Exception( 'Use a child class with the proper element type: post or taxonomy.' );
}
}

View File

@@ -0,0 +1,64 @@
<?php
class WPML_ST_Slug_Translation_Strings_Sync implements IWPML_Action {
/** @var WPML_Slug_Translation_Records_Factory $slug_records_factory */
private $slug_records_factory;
/** @var WPML_ST_Slug_Translation_Settings_Factory $slug_settings_factory */
private $slug_settings_factory;
public function __construct(
WPML_Slug_Translation_Records_Factory $slug_records_factory,
WPML_ST_Slug_Translation_Settings_Factory $slug_settings_factory
) {
$this->slug_records_factory = $slug_records_factory;
$this->slug_settings_factory = $slug_settings_factory;
}
public function add_hooks() {
add_action( 'registered_taxonomy', array( $this, 'run_taxonomy_sync' ), 10, 3 );
add_action( 'registered_post_type', array( $this, 'run_post_type_sync' ), 10, 2 );
}
/**
* @param string $taxonomy
* @param string|array $object_type
* @param array $taxonomy_array
*/
public function run_taxonomy_sync( $taxonomy, $object_type, $taxonomy_array ) {
if ( isset( $taxonomy_array['rewrite']['slug'] ) && $taxonomy_array['rewrite']['slug'] ) {
$this->sync_element_slug( $taxonomy_array['rewrite']['slug'], $taxonomy, WPML_Slug_Translation_Factory::TAX );
}
}
/**
* @param string $post_type
* @param WP_Post_Type $post_type_object
*/
public function run_post_type_sync( $post_type, $post_type_object ) {
if ( isset( $post_type_object->rewrite['slug'] ) && $post_type_object->rewrite['slug'] ) {
$this->sync_element_slug( trim( $post_type_object->rewrite['slug'], '/' ), $post_type, WPML_Slug_Translation_Factory::POST );
}
}
/**
* @param string $rewrite_slug
* @param string $type
* @param string $element_type
*/
public function sync_element_slug( $rewrite_slug, $type, $element_type ) {
$settings = $this->slug_settings_factory->create( $element_type );
if ( ! $settings->is_translated( $type ) ) {
return;
}
$records = $this->slug_records_factory->create( $element_type );
$slug = $records->get_slug( $type );
if ( $slug->get_original_value() !== $rewrite_slug ) {
$records->update_original_slug( $type, $rewrite_slug );
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
class WPML_ST_Slug_Translation_UI_Factory {
const POST = 'post';
const TAX = 'taxonomy';
const TEMPLATE_PATH = 'templates/slug-translation';
public function create( $type ) {
global $sitepress;
$sync_settings_factory = new WPML_Element_Sync_Settings_Factory();
$records_factory = new WPML_Slug_Translation_Records_Factory();
if ( WPML_Slug_Translation_Factory::POST === $type ) {
$settings = new WPML_ST_Post_Slug_Translation_Settings( $sitepress );
} elseif ( WPML_Slug_Translation_Factory::TAX === $type ) {
$settings = new WPML_ST_Tax_Slug_Translation_Settings();
} else {
throw new Exception( 'Unknown element type.' );
}
$records = $records_factory->create( $type );
$sync = $sync_settings_factory->create( $type );
$template_loader = new WPML_Twig_Template_Loader( array( WPML_ST_PATH . '/' . self::TEMPLATE_PATH ) );
$template_service = $template_loader->get_template();
$lang_selector = new WPML_Simple_Language_Selector( $sitepress );
$model = new WPML_ST_Element_Slug_Translation_UI_Model( $sitepress, $settings, $records, $sync, $lang_selector );
return new WPML_ST_Element_Slug_Translation_UI( $model, $template_service );
}
}

View File

@@ -0,0 +1,168 @@
<?php
use WPML\API\Sanitize;
class WPML_ST_Slug_Translation_UI_Save implements IWPML_Action {
const ACTION_HOOK_FOR_POST = 'wpml_save_cpt_sync_settings';
const ACTION_HOOK_FOR_TAX = 'wpml_save_taxonomy_sync_settings';
/** @var WPML_ST_Slug_Translation_Settings $settings */
private $settings;
/** @var WPML_Slug_Translation_Records $records */
private $records;
/** @var SitePress $sitepress */
private $sitepress;
/** @var IWPML_WP_Element_Type $wp_element_type */
private $wp_element_type;
/**
* @var string $action_hook either WPML_ST_Slug_Translation_UI_Save::ACTION_HOOK_FOR_POST
* or WPML_ST_Slug_Translation_UI_Save::ACTION_HOOK_FOR_TAX
*/
private $action_hook;
public function __construct(
WPML_ST_Slug_Translation_Settings $settings,
WPML_Slug_Translation_Records $records,
SitePress $sitepress,
IWPML_WP_Element_Type $wp_element_type,
$action_hook
) {
$this->settings = $settings;
$this->records = $records;
$this->sitepress = $sitepress;
$this->wp_element_type = $wp_element_type;
$this->action_hook = $action_hook;
}
public function add_hooks() {
add_action( $this->action_hook, array( $this, 'save_element_type_slug_translation_options' ), 1 );
}
public function save_element_type_slug_translation_options() {
if ( $this->settings->is_enabled() && ! empty( $_POST['translate_slugs'] ) ) {
foreach ( $_POST['translate_slugs'] as $type => $data ) {
$type = Sanitize::string( $type );
$data = $this->sanitize_translate_slug_data( $data );
$is_type_enabled = $this->has_translation( $data );
$this->settings->set_type( $type, $is_type_enabled );
$this->update_slug_translations( $type, $data );
}
$this->settings->save();
}
}
/**
* @param array $data
*
* @return array
*/
private function sanitize_translate_slug_data( array $data ) {
$data['original'] = Sanitize::stringProp( 'original', $data );
foreach ( $data['langs'] as $lang => $translated_slug ) {
$data['langs'][ $lang ] = Sanitize::string( $translated_slug );
}
return $data;
}
private function has_translation( array $data ) {
$slug_translations = $this->get_slug_translations( $data );
foreach ( $slug_translations as $slug ) {
if ( trim( $slug ) ) {
return true;
}
}
return false;
}
/**
* @param array $data
*
* @return array
*/
private function get_slug_translations( array $data ) {
$slugs = $data['langs'];
$original_slug_lang = $data['original'];
unset( $slugs[ $original_slug_lang ] );
return $slugs;
}
/**
* @param string $type
* @param array $data
*/
private function update_slug_translations( $type, array $data ) {
$string = $this->records->get_slug_string( $type );
if ( ! $string ) {
$string = $this->register_string_if_not_exit( $type );
}
if ( $string ) {
$original_lang = $this->sitepress->get_default_language();
if ( isset( $data['original'] ) ) {
$original_lang = $data['original'];
}
if ( $string->get_language() !== $original_lang ) {
$string->set_language( $original_lang );
}
if ( isset( $data['langs'] ) ) {
foreach ( $this->sitepress->get_active_languages() as $code => $lang ) {
if ( $code !== $original_lang ) {
$translation_value = $this->sanitize_slug( $data['langs'][ $code ] );
$translation_value = urldecode( $translation_value );
$string->set_translation( $code, $translation_value, ICL_TM_COMPLETE );
}
}
}
$string->update_status();
}
}
/**
* @param string $type
*
* @return null|WPML_ST_String
*/
private function register_string_if_not_exit( $type ) {
$slug = $this->get_registered_slug( $type );
$this->records->register_slug( $type, $slug );
return $this->records->get_slug_string( $type );
}
/**
* @param string $slug
*
* @return string
*/
private function sanitize_slug( $slug ) {
return implode( '/', array_map( array( 'WPML_Slug_Translation', 'sanitize' ), explode( '/', $slug ) ) );
}
/**
* @param string $type_name
*
* @return string
*/
private function get_registered_slug( $type_name ) {
$wp_element = $this->wp_element_type->get_wp_element_type_object( $type_name );
return $wp_element ? trim( $wp_element->rewrite['slug'], '/' ) : false;
}
}

View File

@@ -0,0 +1,122 @@
<?php
class WPML_ST_Slug {
/** @var int $original_id */
private $original_id;
/** @var string $original_lang */
private $original_lang;
/** @var string $original_value */
private $original_value;
/** @var array $langs */
private $langs = array();
/** @param stdClass $data */
public function set_lang_data( stdClass $data ) {
if ( isset( $data->language ) ) {
$this->langs[ $data->language ] = $data;
}
// Case of original string language
if ( isset( $data->id ) ) {
$this->original_id = $data->id;
$this->original_lang = $data->language;
$this->original_value = $data->value;
}
}
/** @return string */
public function get_original_lang() {
return $this->original_lang;
}
/** @return string */
public function get_original_value() {
return $this->original_value;
}
/** @return int */
public function get_original_id() {
return (int) $this->original_id;
}
/**
* @param string $lang
*
* @return string
*/
public function get_value( $lang ) {
if ( isset( $this->langs[ $lang ]->value ) ) {
return $this->langs[ $lang ]->value;
}
return $this->get_original_value();
}
/**
* @param string $lang
*
* @return int
*/
public function get_status( $lang ) {
if ( isset( $this->langs[ $lang ]->status ) ) {
return (int) $this->langs[ $lang ]->status;
}
return ICL_TM_NOT_TRANSLATED;
}
/**
* @param string $lang
*
* @return bool
*/
public function is_translation_complete( $lang ) {
return ICL_TM_COMPLETE === $this->get_status( $lang );
}
/** @return string|null */
public function get_context() {
if ( isset( $this->langs[ $this->original_lang ]->context ) ) {
return $this->langs[ $this->original_lang ]->context;
}
return null;
}
/** @return string|null */
public function get_name() {
if ( isset( $this->langs[ $this->original_lang ]->name ) ) {
return $this->langs[ $this->original_lang ]->name;
}
return null;
}
/** @return array */
public function get_language_codes() {
return array_keys( $this->langs );
}
/**
* This method is used as a filter which returns the initial `$slug_value`
* if no better value was found.
*
* @param string $slug_value
* @param string $lang
*
* @return string
*/
public function filter_value( $slug_value, $lang ) {
if ( $this->original_lang === $lang ) {
return $this->original_value;
} elseif ( in_array( $lang, $this->get_language_codes(), true ) && $this->is_translation_complete( $lang ) ) {
return $this->get_value( $lang );
}
return $slug_value;
}
}