├── .gitignore ├── README.md ├── css └── admin.css ├── include ├── class-PolylangPostClonerAdmin.php └── class-PolylangPostClonerWatchMeta.php ├── index.php └── languages ├── polylang-fix-relationships-de_DE.mo └── polylang-fix-relationships-de_DE.po /.gitignore: -------------------------------------------------------------------------------- 1 | ## generic files to ignore 2 | *~ 3 | *.lock 4 | *.DS_Store 5 | *.swp 6 | *.out 7 | .svn/ 8 | .svnignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Polylang Fix Relationships 2 | ========================== 3 | 4 | **This Plugin is discontinued.** 5 | You might like to take a look at [Polylang Sync](https://github.com/mcguffin/polylang-sync) here on GitHub, which addresses a similar issue. 6 | 7 | --- 8 | 9 | Manage post relationships like attachments, post parents and ACF Relational fields which are not 10 | covered by [Polylang](http://polylang.wordpress.com) plugin. 11 | 12 | Compatibility 13 | ------------- 14 | Tested With WP 4.4.2 - 4.5.2, Polylang 1.8.4 - 1.9, ACF pro 5.x 15 | 16 | 17 | Features: 18 | --------- 19 | - Adds a clone post feature 20 | - Clones post attachments to corresponding languages 21 | - Changes post thumbnail and ACF Relations (like post object, image, ...) to their corresponding translations 22 | 23 | Plugin API 24 | ---------- 25 | 26 | There are two filters allowing a developer to disable one of the two two features: 27 | 28 | // disables the Fix Relationships row action 29 | add_filter( 'polylang_relationships_enable_fix', '__return_false' ); 30 | 31 | // disables the Create4 Translations row action 32 | add_filter( 'polylang_relationships_enable_clone', '__return_false' ); 33 | 34 | -------------------------------------------------------------------------------- /css/admin.css: -------------------------------------------------------------------------------- 1 | /* language columns in edit.php and edit-tags.php */ 2 | th.column-polylang_cloner_actions, td.column-polylang_cloner_actions { 3 | width: 10em; 4 | } 5 | -------------------------------------------------------------------------------- /include/class-PolylangPostClonerAdmin.php: -------------------------------------------------------------------------------- 1 | options['media_support'] ) { 38 | add_action( 'pll_save_post' , array( &$this , 'handle_attachments' ) , 15 , 3 ); 39 | } 40 | } 41 | 42 | /** 43 | * Load css 44 | * @action 'admin_enqueue_scripts' 45 | */ 46 | public function admin_enqueue_scripts() { 47 | wp_enqueue_style( 'polylang-cloner-admin' , plugins_url('css/admin.css', dirname(__FILE__)) ); 48 | } 49 | 50 | /** 51 | * Setup columns 52 | * @action 'admin_init' 53 | */ 54 | function admin_init() { 55 | add_filter('post_row_actions',array(&$this,'row_actions') , 10 , 2 ); 56 | add_filter('page_row_actions',array(&$this,'row_actions') , 10 , 2 ); 57 | 58 | // add_filter('page_row_actions',array(&$this,'row_actions') , 10 , 2 ); 59 | } 60 | 61 | /** 62 | * Add row actions 63 | * 64 | * @filter 'post_row_actions', 'page_row_actions' 65 | */ 66 | function row_actions( $actions , $post ) { 67 | if ( pll_is_translated_post_type( $post->post_type ) ) { 68 | $translations = PLL()->model->post->get_translations( $post->ID ); 69 | $languages = pll_languages_list(); 70 | $current_language = pll_get_post_language($post->ID); 71 | 72 | 73 | // translations post cloner 74 | if ( apply_filters( 'polylang_relationships_enable_clone', true ) 75 | && count($translations) != count( $languages ) ) { 76 | $missing_translations = array(); 77 | foreach ( $languages as $lang_slug ) 78 | if ( ! isset( $translations[$lang_slug] ) && $lang_slug != $current_language) 79 | $missing_translations[] = $lang_slug; 80 | 81 | $action = 'polylang_get_translation'; 82 | $url_params = array( 83 | 'polylang_action' => $action, 84 | 'new_langs' => $missing_translations, 85 | 'from_post' => $post->ID, 86 | ); 87 | 88 | $href = wp_nonce_url( add_query_arg( $url_params ) , $action ); 89 | 90 | $actions['clone_for_translation'] = sprintf( '%s', 91 | $href, _n('Create Translation','Create Translations',count($missing_translations),'polylang-fix-relationships') 92 | ); 93 | } 94 | 95 | // fix relationships 96 | if ( apply_filters( 'polylang_relationships_enable_fix', true ) 97 | && count( array_diff( array_keys( $translations ), array( $current_language ) ) ) ) { 98 | $action = 'polylang_fix_relations'; 99 | $url_params = array( 100 | 'polylang_action' => $action, 101 | 'from_post' => $post->ID, 102 | ); 103 | $href = wp_nonce_url( add_query_arg( $url_params ) , $action ); 104 | $actions['fix_relations'] = sprintf( '%s', 105 | $href, __( 'Fix Relations' , 'polylang-fix-relationships' ) 106 | ); 107 | } 108 | } 109 | return $actions; 110 | } 111 | 112 | 113 | /** 114 | * Do Post cloning 115 | * 116 | * @action 'load-edit.php', 'load-upload.php' 117 | */ 118 | function do_clone_action() { 119 | if ( isset( $_REQUEST['new_langs'] , $_REQUEST['from_post'] , $_REQUEST['polylang_action'] ) && current_user_can( 'edit_post' , (int) $_REQUEST['from_post'] ) ) { 120 | check_admin_referer( $_REQUEST['polylang_action'] ); 121 | $source_post_ids = (array) $_REQUEST['from_post']; 122 | $source_post_ids = array_filter($source_post_ids , 'intval' ); 123 | $this->translated_posts_ids = array(); 124 | foreach ( $source_post_ids as $source_post_id ) { 125 | $source_post_lang = pll_get_post_language( $source_post_id ); 126 | 127 | if ( $source_post = get_post( $source_post_id ) ) { 128 | switch ( $_REQUEST['polylang_action'] ) { 129 | case 'polylang_get_translation': 130 | $langs = (array) $_REQUEST['new_langs']; 131 | $translation_group = $this->create_translation_group( $source_post , $langs ); 132 | break; 133 | case 'polylang_fix_relations': 134 | $translation_group = PLL()->model->post->get_translations( $source_post_id ); 135 | break; 136 | } 137 | // trigger save action. 138 | unset($translation_group[$source_post_lang]); 139 | do_action( 'pll_save_post' , $source_post_id , get_post( $source_post_id ) , $translation_group ); 140 | } 141 | } 142 | if ( count( $this->translated_posts_ids ) === 1 ) { 143 | $redirect = get_edit_post_link( $this->translated_posts_ids[0] , '' ); 144 | } else { 145 | $redirect = remove_query_arg( array('new_langs','from_post','_wpnonce','polylang_action')); 146 | } 147 | wp_redirect($redirect); 148 | exit(); 149 | } 150 | } 151 | 152 | /** 153 | * Clone post. 154 | * Creates translations out of $source_post where necessary. 155 | * 156 | * @param $source_post object Master Post 157 | * @param $langs array holding the target languages 158 | * @return array translation group with language slugs as keys and post ids as values 159 | */ 160 | function create_translation_group( $source_post , $langs ) { 161 | $source_post_lang = pll_get_post_language( $source_post->ID ); 162 | $translation_group = array( 163 | $source_post_lang => $source_post->ID, 164 | ) + PLL()->model->post->get_translations( $source_post->ID ); 165 | 166 | foreach ( $langs as $lang ) { 167 | // check if is language 168 | if ( ($lang = PLL()->model->get_language($lang)) && ! isset( $translation_group[$lang->slug] ) ) { 169 | $new_post_id = $this->make_post_translation( $source_post , $lang ); 170 | $translation_group[$lang->slug] = $new_post_id; 171 | $this->translated_posts_ids[] = $new_post_id; 172 | } 173 | } 174 | pll_save_post_translations($translation_group); 175 | return $translation_group; 176 | } 177 | 178 | 179 | /** 180 | * Set parent-child relations for attachments in source posts' translation group. 181 | * 182 | * @param $source_post object Master Post holding the correct parent-child relations 183 | * @param $langs array holding the target languages 184 | */ 185 | function handle_attachments( $source_post_id , $source_post , $parent_translation_group ) { 186 | $attachments = get_children( array( 'post_parent' => $source_post_id , 'post_type' => 'attachment' ) ); 187 | foreach ( $attachments as $attachment ) { 188 | $translation_group = $this->create_translation_group( $attachment , array_keys($parent_translation_group ) ); 189 | // all good here. Nothing to be done. 190 | if ( $attachment->post_parent == $source_post_id ) 191 | continue; 192 | foreach ( $translation_group as $lang => $translated_id ) { 193 | if ( isset( $parent_translation_group[ $lang ] ) ) { 194 | $post_arr = array( 195 | 'ID' => $translated_id, 196 | 'post_parent' => $parent_translation_group[ $lang ] 197 | ); 198 | wp_update_post($post_arr); 199 | } 200 | } 201 | } 202 | } 203 | 204 | /** 205 | * Retrieve or create post translation. 206 | * If a translation can not be found it will do a deep-clone of $source_post, 207 | * including post meta but not comments and attachments. 208 | * 209 | * @param $source_post int|object Master Post (ID) holding the correct parent-child relations 210 | * @param $lang mixed language as passed to $polylang->model->get_language($lang) 211 | */ 212 | function make_post_translation( $source_post , $lang ) { 213 | if ( is_numeric( $source_post ) ) 214 | $source_post = get_post( $source_post ); 215 | 216 | if ( $lang = PLL()->model->get_language($lang) ) { 217 | $source_post_lang = pll_get_post_language( $source_post->ID ); 218 | // sourcelang is target lang, nothing to do! 219 | if ( $lang->slug == $source_post_lang ) 220 | return new WP_Error('clone',__('Source language is target language','polylang-fix-relationships')); 221 | 222 | // translation exists. Go ahead! 223 | if ( $translation = pll_get_post($source_post->ID,$lang) ) 224 | return $translation; 225 | 226 | $post_arr = get_object_vars( $source_post ); 227 | $post_arr['ID'] = 0; 228 | $post_arr['comment_count'] = 0; 229 | $post_arr['post_status'] = apply_filters( 'polylang_cloned_post_status' , $post_arr['post_status'] ); 230 | $post_arr['post_title'] .= sprintf( ' (%s)' , $lang->slug ); 231 | 232 | // set translated parent 233 | if ( $post_arr['post_parent'] && ($translated_parent = pll_get_post( $post_arr['post_parent'] , $lang ) ) ) { 234 | $post_arr['post_parent'] = $translated_parent; 235 | } 236 | 237 | // prepare taxonomies 238 | if ( $cloned_post_id = wp_insert_post( $post_arr ) ) { 239 | pll_set_post_language( $cloned_post_id , $lang ); 240 | PLL()->model->clean_languages_cache(); 241 | 242 | // clone postmeta 243 | $ignore_meta_keys = array( '_edit_lock' , '_edit_last' ); 244 | $meta = get_post_meta( $source_post->ID ); 245 | 246 | foreach ( $meta as $meta_key => $values ) { 247 | if ( in_array( $meta_key , $ignore_meta_keys ) ) 248 | continue; 249 | foreach ( $values as $value ) { 250 | update_post_meta( $cloned_post_id , $meta_key , maybe_unserialize( $value ) ); 251 | } 252 | } 253 | 254 | // done. 255 | return $cloned_post_id; 256 | } 257 | return new WP_Error('clone',__('No such Language','polylang-fix-relationships')); 258 | } 259 | } 260 | 261 | } 262 | endif; 263 | 264 | -------------------------------------------------------------------------------- /include/class-PolylangPostClonerWatchMeta.php: -------------------------------------------------------------------------------- 1 | options['media_support'] ) { 46 | $this->acf_types_to_watch[] = 'image'; 47 | $this->acf_types_to_watch[] = 'gallery'; 48 | $this->acf_types_to_watch[] = 'file'; 49 | } 50 | 51 | } 52 | 53 | /** 54 | * Setup relational ACF fields. 55 | * 56 | * @action 'init' 57 | */ 58 | function acf_init() { 59 | if ( class_exists( 'acf' ) ) { 60 | // Get all ACF fields... 61 | $all_acf_fields = get_posts(array( 62 | 'post_type' => 'acf-field', 63 | 'posts_per_page' => -1, 64 | )); 65 | 66 | // ... and add the relational ones to our watchlist. 67 | foreach( $all_acf_fields as $field ) { 68 | $field_settings = unserialize( $field->post_content ); 69 | if ( in_array($field_settings['type'] , $this->acf_types_to_watch ) ) { 70 | if ( $this->is_repeater_child( $field ) ) { 71 | $meta_key = $this->get_repeater_meta_key_parts( $field ); 72 | } else { 73 | $meta_key = $field->post_excerpt; 74 | } 75 | $this->watch_meta_keys[] = $meta_key; // add meta key name 76 | } 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * Return whether a field is part of a repeater field. 83 | * 84 | * @param $field object ACF Field Post object 85 | * 86 | * @return bool 87 | */ 88 | function is_repeater_child( $field ) { 89 | return get_post_type($field->post_parent) === 'acf-field'; 90 | } 91 | 92 | 93 | /** 94 | * Return meta key parts for an acf repeater child. 95 | * 96 | * @param $field object ACF Field Post object which is part of a repeater (check with is_repeater_child first) 97 | * 98 | * @return array holding the meta key parts 99 | */ 100 | function get_repeater_meta_key_parts( $field ) { 101 | $meta_key = array( $field->post_excerpt ); 102 | while ( ($field = get_post($field->post_parent)) && $field->post_type == 'acf-field' ) { 103 | $meta_key[] = $field->post_excerpt; 104 | } 105 | return array_reverse($meta_key); 106 | } 107 | 108 | /** 109 | * Handle meta keys of a translation group. 110 | * Resolve Relational meta keys. Tries to change post relations to their corresponding translated post. 111 | * 112 | * @param $source_post_id int source Post ID 113 | * @param $source_post object Post 114 | * @param $translation_group array like it is returned by $polylang->model->get_translations( 'post' , $post_id ), 115 | * having the language slug as key and the post id as value. 116 | * @action 'pll_save_post' 117 | */ 118 | function handle_meta( $source_post_id , $source_post , $translation_group ) { 119 | global $wpdb; 120 | $watch_meta_keys = apply_filters( 'polylang_watch_meta_keys' , $this->watch_meta_keys ); 121 | $force_update = false; 122 | foreach ( $translation_group as $lang => $new_post_id ) { 123 | // prepare meta keys 124 | $post_watch_meta_keys = array(); 125 | foreach( $watch_meta_keys as $meta_key ) { 126 | if ( is_array($meta_key) ) { 127 | $query = $wpdb->prepare("SELECT meta_key FROM $wpdb->postmeta WHERE meta_key REGEXP %s AND post_id=%d", 128 | sprintf('^%s$',implode( '_[0-9]_' ,$meta_key ) ), 129 | $new_post_id 130 | ); 131 | $meta_keys = $wpdb->get_col($query); 132 | $post_watch_meta_keys = array_merge($post_watch_meta_keys,$meta_keys); 133 | } else { 134 | $post_watch_meta_keys[] = $meta_key; 135 | } 136 | } 137 | if ( $new_post_id != $source_post_id ) { // YES, we do this chack! 138 | foreach ( $post_watch_meta_keys as $meta_key ) { 139 | $old_meta_value = $meta_value = maybe_unserialize( get_post_meta( $new_post_id , $meta_key , true ) ); 140 | 141 | if ( is_array( $meta_value ) ) { 142 | foreach ( $meta_value as $k => $v ) 143 | $meta_value[$k] = pll_get_post( $v , $lang ); 144 | } else if ( is_numeric( $meta_value ) ) { 145 | 146 | if ( $post = get_post( $meta_value ) ) { 147 | if ( pll_is_translated_post_type( $post->post_type ) ) { 148 | $meta_value = pll_get_post( $meta_value , $lang ); 149 | } else { 150 | $force_update = true; 151 | } 152 | } 153 | } 154 | 155 | if ( $force_update || ( $old_meta_value != $meta_value ) ) { 156 | update_post_meta( $new_post_id , $meta_key , $meta_value ); 157 | } 158 | } 159 | } 160 | } 161 | } 162 | 163 | } 164 | endif; 165 | 166 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | polylang plugin. 7 | Author: Jörn Lund 8 | Author URI: https://github.com/mcguffin 9 | Version: 0.0.5 10 | License: GPLv3 11 | */ 12 | 13 | if ( ! class_exists( 'PolylangPostCloner' ) ) : 14 | class PolylangPostCloner { 15 | /** 16 | * Holding the singleton instance 17 | */ 18 | private static $_instance = null; 19 | 20 | /** 21 | * @return WP_reCaptcha 22 | */ 23 | public static function instance(){ 24 | if ( is_null( self::$_instance ) ) 25 | self::$_instance = new self(); 26 | return self::$_instance; 27 | } 28 | 29 | /** 30 | * Prevent from creating more instances 31 | */ 32 | private function __clone() { } 33 | 34 | /** 35 | * Prevent from creating more than one instance 36 | */ 37 | private function __construct() { 38 | add_action( 'plugins_loaded' , array( &$this , 'plugins_loaded') ); 39 | add_option( 'polylang_clone_attachments' , true ); 40 | } 41 | 42 | /** 43 | * Setup plugin 44 | * 45 | * @action 'plugins_loaded' 46 | */ 47 | function plugins_loaded() { 48 | load_plugin_textdomain( 'polylang-fix-relationships' , false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' ); 49 | if ( is_admin() && class_exists( 'Polylang' ) && function_exists( 'PLL' ) ) { 50 | PolylangPostClonerAdmin::instance(); 51 | PolylangPostClonerWatchMeta::instance(); 52 | } 53 | } 54 | } 55 | endif; 56 | 57 | 58 | /** 59 | * Autoload Classes 60 | * 61 | * @param string $classname 62 | */ 63 | function polylang_postcloner_autoload( $classname ) { 64 | $class_path = dirname(__FILE__). sprintf('/include/class-%s.php' , $classname ) ; 65 | if ( file_exists($class_path) ) 66 | require_once $class_path; 67 | } 68 | spl_autoload_register( 'polylang_postcloner_autoload' ); 69 | 70 | // init plugin 71 | PolylangPostCloner::instance(); -------------------------------------------------------------------------------- /languages/polylang-fix-relationships-de_DE.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcguffin/polylang-fix-relationships/d81a5eb43d843408e0e516291c6da819727116f7/languages/polylang-fix-relationships-de_DE.mo -------------------------------------------------------------------------------- /languages/polylang-fix-relationships-de_DE.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: Polylang Fix Relationships v0.0.1\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2015-05-23 13:49+0100\n" 6 | "PO-Revision-Date: 2015-05-23 13:49+0100\n" 7 | "Last-Translator: joern-lund \n" 8 | "Language-Team: \n" 9 | "Language: de_DE\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 14 | "X-Generator: Poedit 1.8\n" 15 | "X-Poedit-SourceCharset: UTF-8\n" 16 | "X-Poedit-KeywordsList: __;_e;__ngettext:1,2;_n:1,2;__ngettext_noop:1,2;" 17 | "_n_noop:1,2;_c,_nc:4c,1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;_nx_noop:4c,1,2\n" 18 | "X-Poedit-Basepath: ../\n" 19 | "X-Textdomain-Support: yes\n" 20 | "X-Poedit-SearchPath-0: .\n" 21 | 22 | # @ polylang-fix-relationships 23 | #: include/class-PolylangPostClonerAdmin.php:84 24 | msgid "Create Translation" 25 | msgid_plural "Create Translations" 26 | msgstr[0] "Übersetzung erstellen" 27 | msgstr[1] "Übersetzungen erstellen" 28 | 29 | # @ polylang-fix-relationships 30 | #: include/class-PolylangPostClonerAdmin.php:96 31 | msgid "Fix Relations" 32 | msgstr "Beziehungen reparieren" 33 | 34 | # @ polylang-fix-relationships 35 | #: include/class-PolylangPostClonerAdmin.php:210 36 | msgid "Source language is target language" 37 | msgstr "Ausgangssprache entspricht der Zielsprache" 38 | 39 | # @ polylang-fix-relationships 40 | #: include/class-PolylangPostClonerAdmin.php:247 41 | msgid "No such Language" 42 | msgstr "Keine solche Sprache" 43 | --------------------------------------------------------------------------------