From 397e0ad77a526a75371d6cceb56e0ee3ddeddff7 Mon Sep 17 00:00:00 2001 From: Lauri Kallioniemi Date: Tue, 22 Jun 2021 15:34:16 +0300 Subject: [PATCH] initial commit --- README.md | 46 ++++ composer.json | 6 + include/post-type.php | 156 +++++++++++ include/taxonomy.php | 66 +++++ polylang-translate-rewrite-slugs.php | 376 +++++++++++++++++++++++++++ 5 files changed, 650 insertions(+) create mode 100644 README.md create mode 100644 composer.json create mode 100644 include/post-type.php create mode 100644 include/taxonomy.php create mode 100644 polylang-translate-rewrite-slugs.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..14b9b70 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +Polylang - Translate URL Rewrite Slugs +=============================================================================== +WordPress plugin that add rewrite url slugs translation feature to Polylang. + +Work in progress ;) + +Upgrade notice < 0.3.0 +------------------------------------------------------------------------------- +If you used a version prior to 0.3.0, the plugin will probably crash as the structure of the param for the "pll_translated_post_type_rewrite_slugs" filter has changed. + +Translate Post Type URLs +------------------------------------------------------------------------------- +Translate rewrite slugs for post types by doing 5 things: +- Remove original extra rewrite rules and permastruct for these post types; +- Translate the extra rewrite rules and permastruct for these post types; +- Stop Polylang from translating rewrite rules for these post types; +- Fix "get_permalink" for these post types. +- Fix "get_post_type_archive_link" for these post types. + +To translate a post type rewrite slug, add the filter "pll_translated_post_type_rewrite_slugs" to your functions.php file or your plugin and add the "has_archive" and "rewrite" key has you normally do for the params of the "register_post_type" Wordpress function but add it for each post type and language you want. + +Example +~~~php + array( + 'fr' => array( + 'has_archive' => true, + 'rewrite' => array( + 'slug' => 'produit', + ), + ), + 'en' => array( + 'has_archive' => true, + 'rewrite' => array( + 'slug' => 'product', + ), + ), + ), + ); + return $post_type_translated_slugs; +}); +?> +~~~ \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..99c058d --- /dev/null +++ b/composer.json @@ -0,0 +1,6 @@ +{ + "name": "jussikinnula/wp-polylang-translate-rewrite-slugs", + "description": "Polylang - Translate URL Rewrite Slugs", + "homepage": "https://github.com/jussikinnula/wp-polylang-translate-rewrite-slugs", + "type": "wordpress-plugin" +} diff --git a/include/post-type.php b/include/post-type.php new file mode 100644 index 0000000..e900ffd --- /dev/null +++ b/include/post-type.php @@ -0,0 +1,156 @@ +replace_extra_rules_top. + */ +class PLL_TRS_Post_Type { + // The post type object. + public $post_type_object; + // The translated rewrite slugs. + public $translated_slugs; + + /** + * Contructor. + */ + public function __construct($post_type_object, $translated_slugs) { + $this->post_type_object = $post_type_object; + $this->translated_slugs = $this->sanitize_translated_slugs($translated_slugs); + + // Replace "extra_rules_top", for archive. + $this->replace_extra_rules_top(); + // Replace "permastruct", for single. + $this->replace_permastruct(); + } + + private function sanitize_translated_slugs($translated_slugs) { + $post_type = $this->post_type_object->name; + + // Add defaults to translated_slugs. + $defaults = array( + 'has_archive' => false, + 'rewrite' => true, + ); + + foreach ($translated_slugs as $lang => $translated_slug) { + $args = wp_parse_args( $translated_slug, $defaults ); + $args = (object) $args; + + if ( false !== $args->rewrite && ( is_admin() || '' != get_option( 'permalink_structure' ) ) ) { + if ( ! is_array( $args->rewrite ) ) + $args->rewrite = array(); + if ( empty( $args->rewrite['slug'] ) ) + $args->rewrite['slug'] = $post_type; + if ( ! isset( $args->rewrite['with_front'] ) ) + $args->rewrite['with_front'] = true; + if ( ! isset( $args->rewrite['pages'] ) ) + $args->rewrite['pages'] = true; + if ( ! isset( $args->rewrite['feeds'] ) || ! $args->has_archive ) + $args->rewrite['feeds'] = (bool) $args->has_archive; + if ( ! isset( $args->rewrite['ep_mask'] ) ) { + if ( isset( $args->permalink_epmask ) ) + $args->rewrite['ep_mask'] = $args->permalink_epmask; + else + $args->rewrite['ep_mask'] = EP_PERMALINK; + } + } + + $translated_slugs[$lang] = $args; + } + + return $translated_slugs; + } + + /** + * Replace "extra_rules_top", for archive. + * + * This code simulate the code used in WordPress function "register_post_type" + * and execute it for each language. After that, Polylang will consider these + * rules like "individual" post types (one by lang) and will create the appropriated + * rules. + * + * @see Extra rules from WordPress (wp-include/post.php, register_post_type()). + */ + private function replace_extra_rules_top() { + global $polylang, $wp_rewrite; + + $post_type = $this->post_type_object->name; + + // Remove the original extra rules. + if ( $this->post_type_object->has_archive ) { + $archive_slug = $this->post_type_object->has_archive === true ? $this->post_type_object->rewrite['slug'] : $this->post_type_object->has_archive; + if ( $this->post_type_object->rewrite['with_front'] ) + $archive_slug = substr( $wp_rewrite->front, 1 ) . $archive_slug; + else + $archive_slug = $wp_rewrite->root . $archive_slug; + + unset($wp_rewrite->extra_rules_top["{$archive_slug}/?$"]); + if ( $this->post_type_object->rewrite['feeds'] && $wp_rewrite->feeds ) { + $feeds = '(' . trim( implode( '|', $wp_rewrite->feeds ) ) . ')'; + unset($wp_rewrite->extra_rules_top["{$archive_slug}/feed/$feeds/?$"]); + unset($wp_rewrite->extra_rules_top["{$archive_slug}/$feeds/?$"]); + } + if ( $this->post_type_object->rewrite['pages'] ) + unset($wp_rewrite->extra_rules_top["{$archive_slug}/{$wp_rewrite->pagination_base}/([0-9]{1,})/?$"]); + } + + // Add the translated extra rules for each languages. + foreach ($this->translated_slugs as $lang => $translated_slug) { + if ($translated_slug->has_archive) { + $archive_slug = $translated_slug->has_archive === true ? $translated_slug->rewrite['slug'] : $translated_slug->has_archive; + if ( $translated_slug->rewrite['with_front'] ) + $archive_slug = substr( $wp_rewrite->front, 1 ) . $archive_slug; + else + $archive_slug = $wp_rewrite->root . $archive_slug; + + add_rewrite_rule( "{$archive_slug}/?$", "index.php?post_type=$post_type", 'top' ); + if ( $translated_slug->rewrite['feeds'] && $wp_rewrite->feeds ) { + $feeds = '(' . trim( implode( '|', $wp_rewrite->feeds ) ) . ')'; + add_rewrite_rule( "{$archive_slug}/feed/$feeds/?$", "index.php?post_type=$post_type" . '&feed=$matches[1]', 'top' ); + add_rewrite_rule( "{$archive_slug}/$feeds/?$", "index.php?post_type=$post_type" . '&feed=$matches[1]', 'top' ); + } + if ( $translated_slug->rewrite['pages'] ) + add_rewrite_rule( "{$archive_slug}/{$wp_rewrite->pagination_base}/([0-9]{1,})/?$", "index.php?post_type=$post_type" . '&paged=$matches[1]', 'top' ); + } + } + } + + /** + * Replace "permastruct", for single. + * + * This code simulate the code used in WordPress function "register_post_type" + * and execute it for each language. + * + * @see Permstruct from WordPress (wp-include/post.php, register_post_type()). + */ + private function replace_permastruct() { + global $polylang, $wp_rewrite; + + $post_type = $this->post_type_object->name; + + // Remove the original permastructs. + unset($wp_rewrite->extra_permastructs[$post_type]); + + // Add the translated permastructs for each languages. + foreach ($this->translated_slugs as $lang => $translated_slug) { + $args = $translated_slug; + + if ( false !== $args->rewrite && ( is_admin() || '' != get_option( 'permalink_structure' ) ) ) { + $permastruct_args = $args->rewrite; + $permastruct_args['feed'] = $permastruct_args['feeds']; + // Set the walk_dirs to false to avoid conflict with has_archive = false and the %language% + // in the rewrite directive. Without it the archive page redirect to the frontpage if has_archive is false. + $permastruct_args['walk_dirs'] = false; + + // If "Hide URL language information for default language" option is + // set to true the rules has to be different for the default language. + if ($polylang->options['hide_default'] && $lang == pll_default_language()) { + add_permastruct( $post_type.'_'.$lang, "{$args->rewrite['slug']}/%$post_type%", $permastruct_args ); + } else { + add_permastruct( $post_type.'_'.$lang, "%language%/{$args->rewrite['slug']}/%$post_type%", $permastruct_args ); + } + } + } + } +} diff --git a/include/taxonomy.php b/include/taxonomy.php new file mode 100644 index 0000000..fff358e --- /dev/null +++ b/include/taxonomy.php @@ -0,0 +1,66 @@ +taxonomy_object = $taxonomy_object; + $this->translated_slugs = $translated_slugs; + $this->translated_struct = $translated_struct; + + // Translate the rewrite rules of the post type. + add_filter($this->taxonomy_object->name.'_rewrite_rules', array($this, 'taxonomy_rewrite_rules_filter')); + } + + /** + * Translate the rewrite rules. + */ + public function taxonomy_rewrite_rules_filter($rewrite_rules) { + global $polylang, $wp_rewrite; + + $translated_rules = array(); + + // For each lang. + foreach ($this->translated_slugs as $lang => $translated_slug) { + // If "Hide URL language information for default language" option is + // set to true the rules has to be different for the default language. + if ($polylang->options['hide_default'] && $lang == pll_default_language()) { + // For each rule. + foreach ($rewrite_rules as $rule_key => $rule_value) { + // Only translate the rewrite slug. + $translated_rules[str_replace(trim($this->taxonomy_object->rewrite['slug'], '/'), $translated_slug, $rule_key)] = $rule_value; + } + } else { + // For each rule. + foreach ($rewrite_rules as $rule_key => $rule_value) { + $taxonomy_rewrite_slug = $this->taxonomy_object->rewrite['slug']; + + // Replace the rewrite tags in slugs. + foreach ($wp_rewrite->rewritecode as $position => $code) { + $taxonomy_rewrite_slug = str_replace($code, $wp_rewrite->rewritereplace[$position], $taxonomy_rewrite_slug); + $translated_slug = str_replace($code, $wp_rewrite->rewritereplace[$position], $translated_slug); + } + + // Shift the matches up cause "lang" will be the first. + $translated_rules['('.$lang.')/'.str_replace(trim($taxonomy_rewrite_slug, '/'), $translated_slug, $rule_key)] = str_replace( + array('[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]', '[1]'), + array('[9]', '[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]'), + $rule_value + ); + } + } + } + + return $translated_rules; + } +} diff --git a/polylang-translate-rewrite-slugs.php b/polylang-translate-rewrite-slugs.php new file mode 100644 index 0000000..8425ddf --- /dev/null +++ b/polylang-translate-rewrite-slugs.php @@ -0,0 +1,376 @@ + array( + * 'fr' => array( + * 'has_archive' => true, + * 'rewrite' => array( + * 'slug' => 'produit', + * ), + * ), + * 'en' => array( + * 'has_archive' => true, + * 'rewrite' => array( + * 'slug' => 'product', + * ), + * ), + * ), + * ); + * return $post_type_translated_slugs; + * }); + * add_filter('pll_translated_taxonomy_rewrite_slugs', function($taxonomy_translated_slugs) { + * // Add translation for "color" taxonomy. + * $taxonomy_translated_slugs = array( + * 'color' => array( + * 'fr' => 'couleur' + * 'en' => 'color', + * ), + * ); + * return $taxonomy_translated_slugs; + * }); + */ +class Polylang_Translate_Rewrite_Slugs { + // Array of custom post types handle by "Polylang - Translate URL Rewrite Slugs". + public $post_types; + // Array of taxonomies handle by "Polylang - Translate URL Rewrite Slugs". + public $taxonomies; + + /** + * Contructor. + */ + public function __construct() { + // Initiate the array that will contain the "PLL_TRS_Post_Type" object. + $this->post_types = array(); + // Initiate the array that will contain the... + $this->taxonomies = array(); + + // If the Polylang plugin is active... + include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + if (is_plugin_active('polylang/polylang.php')) { + add_action('init', array($this, 'init_action'), 20); + } + } + + /** + * Trigger on "init" action. + */ + public function init_action() { + // Post types to handle. + require_once(PLL_TRS_INC . '/post-type.php'); + $post_type_translated_slugs = apply_filters('pll_translated_post_type_rewrite_slugs', array()); + foreach ($post_type_translated_slugs as $post_type => $translated_slugs) { + $this->add_post_type($post_type, $translated_slugs); + } + // Taxonomies to handle. + require_once(PLL_TRS_INC . '/taxonomy.php'); + $taxonomy_translated_slugs = apply_filters('pll_translated_taxonomy_rewrite_slugs', array()); + foreach ($taxonomy_translated_slugs as $taxonomy => $translated_slugs) { + $this->add_taxonomy($taxonomy, $translated_slugs); + } + // Fix "get_permalink" for these post types. + add_filter('post_type_link', array($this, 'post_type_link_filter'), 10, 4); + // Fix "get_post_type_archive_link" for these post types. + add_filter('post_type_archive_link', array($this, 'post_type_archive_link_filter'), 25, 2); + // Fix "get_term_link" for taxonomies. + add_filter('term_link', array($this, 'term_link_filter'), 10, 3); + + // Fix "PLL_Frontend_Links->get_translation_url". + add_filter('pll_translation_url', array($this, 'pll_translation_url_filter'), 10, 2); + // Stop Polylang from translating rewrite rules for these post types. + add_filter('pll_rewrite_rules', array($this, 'pll_rewrite_rules_filter')); + } + + /** + * Create a "PLL_TRS_Post_Type" and add it to the handled post type list. + */ + public function add_post_type($post_type, $translated_slugs) { + global $polylang; + + $languages = $polylang->model->get_languages_list(); + $post_type_object = get_post_type_object($post_type); + if (!is_null($post_type_object)) { + foreach ($languages as $language) { + // Add non specified slug translation to post type default. + if (!array_key_exists($language->slug, $translated_slugs)) { + $translated_slugs[$language->slug] = array(); + } + // Trim "/" of the slug. + if (isset($translated_slugs[$language->slug]['rewrite']['slug'])) { + $translated_slugs[$language->slug]['rewrite']['slug'] = trim($translated_slugs[$language->slug]['rewrite']['slug'], '/'); + } + } + $this->post_types[$post_type] = new PLL_TRS_Post_Type($post_type_object, $translated_slugs); + } + } + + /** + * ... + */ + public function add_taxonomy($taxonomy, $translated_slugs) { + global $polylang; + + $languages = $polylang->model->get_languages_list(); + $taxonomy_object = get_taxonomy($taxonomy); + if (!is_null($taxonomy_object)) { + $translated_struct = array(); + foreach ($languages as $language) { + // Add non specified slug translation to taxonomy default. + if (!array_key_exists($language->slug, $translated_slugs)) { + $translated_slugs[$language->slug] = $taxonomy_object->rewrite['slug']; + } + // Trim "/". + $translated_slugs[$language->slug] = trim($translated_slugs[$language->slug], '/'); + // Generate "struct" with "slug" as WordPress would do. + $translated_struct[$language->slug] = $translated_slugs[$language->slug] . "/%{$taxonomy_object->name}%"; + } + $this->taxonomies[$taxonomy] = new PLL_TRS_Taxonomy($taxonomy_object, $translated_slugs, $translated_struct); + } + } + + /** + * Fix "get_permalink" for this post type. + */ + public function post_type_link_filter($post_link, $post, $leavename, $sample) { + global $polylang; + + // We always check for the post language. Otherwise, the current language. + $post_language = PLL()->model->post->get_language($post->ID); + if ($post_language) { + $lang = $post_language->slug; + } else { + $lang = pll_default_language(); + } + + // Check if the post type is handle. + if (isset($this->post_types[$post->post_type])) { + // Build URL. Lang prefix is already handle. + return trim(home_url( '/' . $this->post_types[$post->post_type]->translated_slugs[$lang]->rewrite['slug'] . '/' . ($leavename ? "%$post->post_type%" : get_page_uri( $post->ID ) ) ), '/').'/'; + } + + return trim($post_link, '/').'/'; + } + + /** + * Fix "get_post_type_archive_link" for this post type. + */ + public function post_type_archive_link_filter($link, $archive_post_type) { + if (is_admin()) { + global $polylang; + $lang = $polylang->pref_lang->slug; + } else { + $lang = pll_current_language(); + } + + // Check if the post type is handle. + if (isset($this->post_types[$archive_post_type])) { + return $this->get_post_type_archive_link($archive_post_type, $lang); + } + + return $link; + } + + /** + * Reproduce "get_post_type_archive_link" WordPress function. + * + * @see wp-include/link-template.php, get_post_type_archive_link(). + */ + private function get_post_type_archive_link($post_type, $lang) { + global $wp_rewrite, $polylang; + + // If the post type is handle, let the "$this->get_post_type_archive_link" + // function handle this. + if (isset($this->post_types[$post_type])) { + $translated_slugs = $this->post_types[$post_type]->translated_slugs; + $translated_slug = $translated_slugs[$lang]; + + if ( ! $translated_slug->has_archive ) + return false; + + if ( get_option( 'permalink_structure' ) && is_array( $translated_slug->rewrite ) ) { + $struct = ( true === $translated_slug->has_archive ) ? $translated_slug->rewrite['slug'] : $translated_slug->has_archive; + + if ( + // If the "URL modifications" is set to "The language is set from the directory name in pretty permalinks". + $polylang->options['force_lang'] == 1 + // If NOT ("Hide URL language information for default language" option is + // set to true and the $lang is the default language.) + && !($polylang->options['hide_default'] && $lang == pll_default_language()) + ) { + $struct = $lang . '/' . $struct; + } + + if ( $translated_slug->rewrite['with_front'] ) + $struct = $wp_rewrite->front . $struct; + else + $struct = $wp_rewrite->root . $struct; + $link = home_url( user_trailingslashit( $struct, 'post_type_archive' ) ); + } else { + $link = home_url( '?post_type=' . $post_type ); + } + } + + return $link; + } + + /** + * Fix "get_term_link" for this taxonomy. + */ + public function term_link_filter($termlink, $term, $taxonomy) { + // Check if the post type is handle. + if (isset($this->taxonomies[$taxonomy])) { + global $wp_rewrite, $polylang; + + if ( !is_object($term) ) { + if ( is_int($term) ) { + $term = get_term($term, $taxonomy); + } else { + $term = get_term_by('slug', $term, $taxonomy); + } + } + + if ( !is_object($term) ) + $term = new WP_Error('invalid_term', __('Empty Term')); + + if ( is_wp_error( $term ) ) + return $term; + + // Get the term language. + $term_language = PLL()->model->term->get_language($term->term_id); + if ($term_language) { + $lang = $term_language->slug; + } else { + $lang = pll_default_language(); + } + // Check if the language is handle. + if (isset($this->taxonomies[$taxonomy]->translated_slugs[$lang])) { + $taxonomy = $term->taxonomy; + + $termlink = $this->taxonomies[$taxonomy]->translated_struct[$lang]; + + $slug = $term->slug; + $t = get_taxonomy($taxonomy); + + if ( empty($termlink) ) { + if ( 'category' == $taxonomy ) + $termlink = '?cat=' . $term->term_id; + elseif ( $t->query_var ) + $termlink = "?$t->query_var=$slug"; + else + $termlink = "?taxonomy=$taxonomy&term=$slug"; + $termlink = home_url($termlink); + } else { + if ( $t->rewrite['hierarchical'] ) { + $hierarchical_slugs = array(); + $ancestors = get_ancestors($term->term_id, $taxonomy); + foreach ( (array)$ancestors as $ancestor ) { + $ancestor_term = get_term($ancestor, $taxonomy); + $hierarchical_slugs[] = $ancestor_term->slug; + } + $hierarchical_slugs = array_reverse($hierarchical_slugs); + $hierarchical_slugs[] = $slug; + $termlink = str_replace("%$taxonomy%", implode('/', $hierarchical_slugs), $termlink); + } else { + $termlink = str_replace("%$taxonomy%", $slug, $termlink); + } + $termlink = home_url( user_trailingslashit($termlink, 'category') ); + } + // Back Compat filters. + if ( 'post_tag' == $taxonomy ) + $termlink = apply_filters( 'tag_link', $termlink, $term->term_id ); + elseif ( 'category' == $taxonomy ) + $termlink = apply_filters( 'category_link', $termlink, $term->term_id ); + } + } + + return $termlink; + } + + /** + * Fix "PLL_Frontend_Links->get_translation_url()". + */ + public function pll_translation_url_filter($url, $lang) { + global $wp_query, $polylang; + + if (is_category()) { + $term = get_category_by_slug($wp_query->get('category_name')); + $translated_term = get_term(pll_get_term($term->term_id, $lang), $term->taxonomy); + return home_url('/'.$lang.'/'.$translated_term->slug); + } + elseif (is_archive()) { + $post_type = $wp_query->query_vars['post_type']; + // If the post type is handle, let the "$this->get_post_type_archive_link" + // function handle this. + if (isset($this->post_types[$post_type])) { + return $this->get_post_type_archive_link($post_type, $lang); + } + } + + return $url; + } + + /** + * Stop Polylang from translating rewrite rules for these post types. + */ + public function pll_rewrite_rules_filter($rules) { + // We don't want Polylang to take care of these rewrite rules groups. + foreach (array_keys($this->post_types) as $post_type) { + $rule_key = array_search($post_type, $rules); + if ($rule_key) { + unset($rules[$rule_key]); + } + } + foreach (array_keys($this->taxonomies) as $taxonomy) { + $rule_key = array_search($taxonomy, $rules); + if ($rule_key) { + unset($rules[$rule_key]); + } + } + + return $rules; + } +} +new Polylang_Translate_Rewrite_Slugs();