PageRenderTime 585ms CodeModel.GetById 61ms app.highlight 376ms RepoModel.GetById 68ms app.codeStats 2ms

/class-taxonomy.php

https://github.com/humanmade/babble
PHP | 949 lines | 541 code | 144 blank | 264 comment | 117 complexity | cd5c2eb44328c4191f3db0312413116e MD5 | raw file
  1<?php
  2
  3/**
  4 * Manages the translations for taxonomies.
  5 *
  6 * @package Babble
  7 * @since Alpha 1.2
  8 */
  9class Babble_Taxonomies extends Babble_Plugin {
 10	
 11	/**
 12	 * A simple flag to stop infinite recursion in various places.
 13	 *
 14	 * @var boolean
 15	 **/
 16	protected $no_recursion;
 17	
 18	/**
 19	 * The current version for purposes of rewrite rules, any 
 20	 * DB updates, cache busting, etc
 21	 *
 22	 * @var int
 23	 **/
 24	protected $version = 1;
 25
 26	/**
 27	 * The shadow taxonomies created to handle the translated terms.
 28	 *
 29	 * @var array
 30	 **/
 31	protected $taxonomies;
 32
 33	/**
 34	 * The languages represented by each of the shadow taxonomies.
 35	 *
 36	 * @var array
 37	 **/
 38	protected $lang_map;
 39	
 40	/**
 41	 * Setup any add_action or add_filter calls. Initiate properties.
 42	 *
 43	 * @return void
 44	 **/
 45	public function __construct() {
 46		$this->setup( 'babble-taxonomy', 'plugin' );
 47		$this->add_action( 'bbl_created_new_shadow_post', 'created_new_shadow_post', null, 2 );
 48		$this->add_action( 'bbl_registered_shadow_post_types', 'registered_shadow_post_types' );
 49		$this->add_action( 'init', 'init_early', 0 );
 50		$this->add_action( 'parse_request' );
 51		$this->add_action( 'registered_taxonomy', null, null, 3 );
 52		$this->add_action( 'save_post', null, null, 2 );
 53		$this->add_action( 'set_object_terms', null, null, 5 );
 54		$this->add_filter( 'get_terms' );
 55		$this->add_filter( 'term_link', null, null, 3 );
 56		$this->add_filter( 'bbl_translated_taxonomy', null, null, 2 );
 57		$this->add_filter( 'admin_body_class' );
 58
 59	}
 60	
 61	// WP HOOKS
 62	// ========
 63
 64	/**
 65	 * Hooks the WP init action early
 66	 *
 67	 * @return void
 68	 **/
 69	public function init_early() {
 70		// This translation will connect each term with it's translated equivalents
 71		register_taxonomy( 'term_translation', 'term', array(
 72			'rewrite' => false,
 73			'public' => true,  # ?
 74			'show_ui' => true, # ?
 75			'show_in_nav_menus' => false,
 76			'label' => __( 'Term Translation ID', 'babble' ),
 77		) );
 78	}
 79	
 80	/**
 81	 * Hooks the WP registered_taxonomy action 
 82	 *
 83	 * @param string $taxonomy The name of the newly registered taxonomy 
 84	 * @param string|array $args The object_type(s)
 85	 * @param array $args The args passed to register the taxonomy
 86	 * @return void
 87	 **/
 88	public function registered_taxonomy( $taxonomy, $object_type, $args ) {
 89		if ( in_array( $taxonomy, $this->ignored_taxonomies() )  ) {
 90			return;
 91		}
 92
 93		if ( $this->no_recursion ) {
 94			return;
 95		}
 96
 97		$this->no_recursion = true;
 98
 99		if ( ! is_array( $object_type ) ) {
100			$object_type = array_unique( (array) $object_type );
101		}
102
103		// Use the Babble term counting function, unless the taxonomy registrant
104		// has defined their own – in which case we'll just have to hope against 
105		// hope that it's Babble aware :S
106		// FIXME: Setting this in the following fashion seems hacky… I feel uncomfortable.
107		if ( empty( $GLOBALS[ 'wp_taxonomies' ][ $taxonomy ]->update_count_callback ) ) {
108			$GLOBALS[ 'wp_taxonomies' ][ $taxonomy ]->update_count_callback = array( & $this, 'update_post_term_count' );
109		}
110
111		// Untranslated taxonomies do not have shadow equivalents in each language,
112		// but do apply to the bast post_type and all it's shadow post_types.
113		if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
114			// Apply this taxonomy to all the shadow post types
115			// of all of the base post_types it applies to.
116			foreach ( $object_type as $ot ) {
117				if ( ! ( $base_post_type = bbl_get_base_post_type( $ot ) ) ) {
118					continue;
119				}
120				$shadow_post_types = bbl_get_shadow_post_types( $base_post_type );
121				foreach ( $shadow_post_types as $shadow_post_type ) {
122					register_taxonomy_for_object_type( $taxonomy, $shadow_post_type );
123				}
124			}
125
126			$this->no_recursion = false;
127			return;
128		}
129
130		// @FIXME: Not sure this is the best way to specify languages
131		$langs = bbl_get_active_langs();
132
133		// Lose the default language as any existing taxonomies are in that language
134		unset( $langs[ bbl_get_default_lang_url_prefix() ] );
135
136		// @FIXME: Is it reckless to convert ALL object instances in $args to an array?
137		foreach ( $args as $key => & $arg ) {
138			if ( is_object( $arg ) )
139				$arg = get_object_vars( $arg );
140			// Don't set any args reserved for built-in post_types
141			if ( '_' == substr( $key, 0, 1 ) )
142				unset( $args[ $key ] );
143		}
144
145		#$args[ 'rewrite' ] = false;
146		unset( $args[ 'name' ] );
147		unset( $args[ 'object_type' ] );
148
149		$slug = ( $args[ 'rewrite' ][ 'slug' ] ) ? $args[ 'rewrite' ][ 'slug' ] : $taxonomy;
150
151		foreach ( $langs as $lang ) {
152			$new_args = $args;
153			$new_object_type = array();
154			// N.B. Here we assume that the taxonomy is on a post type
155			foreach( $object_type as $ot )
156				$new_object_type[] = bbl_get_post_type_in_lang( $ot, $lang->code );
157
158			if ( false !== $args[ 'rewrite' ] ) {
159				if ( ! is_array( $new_args[ 'rewrite' ] ) )
160					$new_args[ 'rewrite' ] = array();
161				// Do I not need to add this query_var into the query_vars filter? It seems not.
162				$new_args[ 'query_var' ] = $new_args[ 'rewrite' ][ 'slug' ] = $this->get_slug_in_lang( $slug, $lang->code );
163			}
164
165			// @FIXME: Note currently we are in danger of a taxonomy name being longer than 32 chars
166			// Perhaps we need to create some kind of map like (taxonomy) + (lang) => (shadow translated taxonomy)
167			$new_taxonomy = strtolower( "{$taxonomy}_{$lang->code}" );
168
169			$this->taxonomies[ $new_taxonomy ] = $taxonomy;
170			if ( ! isset( $this->lang_map[ $lang->code ] ) || ! is_array( $this->lang_map[ $lang->code ] ) )
171				$this->lang_map[ $lang->code ] = array();
172			$this->lang_map[ $lang->code ][ $taxonomy ] = $new_taxonomy;
173			
174			register_taxonomy( $new_taxonomy, $new_object_type, $new_args );
175			
176		}
177		// bbl_stop_logging();
178
179		$this->no_recursion = false;
180	}
181
182	public function ignored_taxonomies() {
183		return array( 'post_translation', 'term_translation' );
184	}
185
186	public function is_taxonomy_translated( $taxonomy ) {
187		if( in_array( $taxonomy, $this->ignored_taxonomies() ) ) {
188			return false;
189		}
190
191		// @FIXME: Remove this when menu's are translatable
192		if( 'nav_menu' == $taxonomy ) {
193			return false;
194		}
195
196		return apply_filters( 'bbl_translated_taxonomy', true, $taxonomy );
197	}
198
199	/**
200	 * Hooks the WP bbl_registered_shadow_post_types action to check that we've applied
201	 * all untranslated taxonomies to the shadow post types created for this base
202	 * post type. 
203	 * 
204	 * @param string $post_type The post type for which the shadow post types have been registered. 
205	 * @return void
206	 **/
207	public function registered_shadow_post_types( $post_type ) {
208		$taxonomies = get_object_taxonomies( $post_type );
209
210		$object_type = (array) $post_type;
211		
212		foreach ( $taxonomies as $taxonomy ) {
213			// Untranslated taxonomies do not have shadow equivalents in each language,
214			// but do apply to the bast post_type and all it's shadow post_types.
215			if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
216				// Apply this taxonomy to all the shadow post types
217				// of all of the base post_types it applies to.
218				foreach ( $object_type as $ot ) {
219					if ( ! ( $base_post_type = bbl_get_base_post_type( $ot ) ) ) {
220						continue;
221					}
222					$shadow_post_types = bbl_get_shadow_post_types( $base_post_type );
223					foreach ( $shadow_post_types as $shadow_post_type ) {
224						register_taxonomy_for_object_type( $taxonomy, $shadow_post_type );
225					}
226				}
227			}
228		}
229	}
230
231	/**
232	 * Hooks the Babble action bbl_created_new_shadow_post, which is fired
233	 * when a new translation post is created, to sync any existing untranslated
234	 * taxonomy terms.
235	 *
236	 * @param int $new_post_id The ID of the new post (to sync to)
237	 * @param int $origin_post_id The ID of the originating post (to sync from)
238	 * @return void
239	 **/
240	public function created_new_shadow_post( $new_post_id, $origin_post_id ) {
241		$new_post = get_post( $new_post_id );
242		if ( ! ( $origin_post = get_post( $origin_post_id ) ) ) {
243			return;
244		}
245		
246		if ( $this->no_recursion ) {
247			return;
248		}
249		$this->no_recursion = true;
250		
251		$taxonomies = get_object_taxonomies( $origin_post->post_type );
252
253		foreach ( $taxonomies as $taxonomy ) {
254			if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
255				$term_ids = wp_get_object_terms( $origin_post->ID, $taxonomy, array( 'fields' => 'ids' ) );
256				$term_ids = array_map( 'absint', $term_ids );
257				wp_set_object_terms( $new_post->ID, $term_ids, $taxonomy );
258			}
259		}
260
261		$this->no_recursion = false;
262	}
263
264	/**
265	 * Hooks the WP save_post action to resync data
266	 * when requested.
267	 *
268	 * @param int $post_id The ID of the WP post
269	 * @param object $post The WP Post object 
270	 * @return void
271	 **/
272	public function save_post( $post_id, $post ) {
273		$this->maybe_resync_terms( $post_id, $post );
274	}
275
276	/**
277	 * Hooks the WordPress term_link filter to provide functions to provide
278	 * appropriate links for the shadow taxonomies. 
279	 *
280	 * @see get_term_link from whence much of this was copied
281	 *
282	 * @param string $termlink The currently generated term URL
283	 * @param object $term The WordPress term object we're generating a link for
284	 * @param string $taxonomy The 
285	 * @return string The term link
286	 **/
287	public function term_link( $termlink, $term, $taxonomy ) {
288		$taxonomy = strtolower( $taxonomy );
289		// No need to worry about the built in taxonomies
290		if ( 'post_tag' == $taxonomy || 'category' == $taxonomy || ! isset( $this->taxonomies[ $taxonomy ] ) ) {
291			return $termlink;
292		}
293	
294		// Deal with our shadow taxonomies
295		if ( ! ( $base_taxonomy = $this->get_base_taxonomy( $taxonomy ) ) ) {
296			return $termlink;
297		}
298	
299		// START copying from get_term_link, replacing $taxonomy with $base_taxonomy
300		global $wp_rewrite;
301	
302		if ( !is_object($term) ) {
303			if ( is_int($term) ) {
304				$term = &get_term($term, $base_taxonomy);
305			} else {
306				$term = &get_term_by('slug', $term, $base_taxonomy);
307			}
308		}
309	
310		if ( !is_object($term) ) {
311			$term = new WP_Error('invalid_term', __('Empty Term', 'babble'));
312		}
313	
314		if ( is_wp_error( $term ) ) {
315			return $term;
316		}
317	
318		$termlink = $wp_rewrite->get_extra_permastruct($base_taxonomy);
319	
320		$slug = $term->slug;
321		$t = get_taxonomy($base_taxonomy);
322	
323		if ( empty($termlink) ) {
324			if ( 'category' == $base_taxonomy ) {
325				$termlink = '?cat=' . $term->term_id;
326			} elseif ( $t->query_var ) {
327				$termlink = "?$t->query_var=$slug";
328			} else {
329				$termlink = "?taxonomy=$base_taxonomy&term=$slug";
330			}
331			$termlink = home_url($termlink);
332		} else {
333			if ( $t->rewrite['hierarchical'] ) {
334				$hierarchical_slugs = array();
335				$ancestors = get_ancestors($term->term_id, $base_taxonomy);
336				foreach ( (array)$ancestors as $ancestor ) {
337					$ancestor_term = get_term($ancestor, $base_taxonomy);
338					$hierarchical_slugs[] = $ancestor_term->slug;
339				}
340				$hierarchical_slugs = array_reverse($hierarchical_slugs);
341				$hierarchical_slugs[] = $slug;
342				$termlink = str_replace("%$base_taxonomy%", implode('/', $hierarchical_slugs), $termlink);
343			} else {
344				$termlink = str_replace("%$base_taxonomy%", $slug, $termlink);
345			}
346			$termlink = home_url( user_trailingslashit($termlink, 'category') );
347		}
348		// STOP copying from get_term_link
349	
350		return $termlink;
351	}
352
353	/**
354	 * Hooks the WP get_terms filter to ensure the terms all have transids.
355	 *
356	 * @param array $terms The terms which have been got 
357	 * @return array The terms which were got
358	 **/
359	public function get_terms( $terms ) {
360		foreach ( $terms as $term ) {
361			if ( empty( $term ) ) {
362				continue;
363			}
364			if ( isset( $this->taxonomies ) ) {
365				continue;
366			}
367			if ( isset( $this->taxonomies[ $term->taxonomy ] ) ) {
368				if ( ! $this->get_transid( $term->term_id ) ) {
369					throw new exception( "ERROR: Translated term ID $term->term_id does not have a transid" );
370				} else {
371					continue;
372				}
373			}
374			if ( ! $this->get_transid( $term->term_id ) ) {
375				$this->set_transid( $term->term_id );
376			}
377		}
378		return $terms;
379	}
380
381	/**
382	 * Hooks the WP parse_request action 
383	 *
384	 * FIXME: Should I be extending and replacing the WP class?
385	 *
386	 * @param object $wp WP object, passed by reference (so no need to return)
387	 * @return void
388	 **/
389	public function parse_request( $wp ) {
390
391		if ( is_admin() ) {
392			return;
393		}
394
395		// Sequester the original query, in case we need it to get the default content later
396		if ( ! isset( $wp->query_vars[ 'bbl_tax_original_query' ] ) ) {
397			$wp->query_vars[ 'bbl_tax_original_query' ] = $wp->query_vars;
398		}
399
400		$taxonomy 	= false;
401		$terms 		= false;
402
403		$taxonomies = get_taxonomies( null, 'objects' );
404		$lang_taxonomies = array();
405		foreach ( $taxonomies as $taxonomy => $tax_obj ) {
406			$tax = $this->get_taxonomy_in_lang( $taxonomy, bbl_get_current_lang_code() );
407			$lang_taxonomies[ $tax_obj->rewrite[ 'slug' ] ] = $tax;
408		}
409
410		if ( isset( $wp->query_vars[ 'tag' ] ) ) {
411			$taxonomy = $this->get_taxonomy_in_lang( 'post_tag', $wp->query_vars[ 'lang' ] );
412			$terms = $wp->query_vars[ 'tag' ];
413			unset( $wp->query_vars[ 'tag' ] );
414		} else if ( isset( $wp->query_vars[ 'category_name' ] ) ) {
415			$taxonomy = $this->get_taxonomy_in_lang( 'category', $wp->query_vars[ 'lang' ] );
416			$terms = $wp->query_vars[ 'category_name' ];
417			unset( $wp->query_vars[ 'category_name' ] );
418		} else {
419			$taxonomies = array();
420			foreach ( $lang_taxonomies as $slug => $tax ) {
421				if ( isset( $wp->query_vars[ $slug ] ) ) {
422					$taxonomies[] = $tax;
423					break;
424				}
425			}
426			
427			if ( $taxonomies ) {
428				$post_types = array();
429				foreach ( $taxonomies as $taxonomy ) {
430					$taxonomy = get_taxonomy( $taxonomy );
431					$post_types = array_merge( $post_types, $taxonomy->object_type );
432					// Filter out the post_types not in this language
433					foreach ( $post_types as & $post_type ) {
434						$post_type = bbl_get_post_type_in_lang( $post_type );
435					}
436					$post_types = array_unique( $post_types );
437				}
438				$wp->query_vars[ 'post_type' ] = $post_types;
439			}
440		}
441
442		if ( $taxonomy && $terms ) {
443
444			if ( ! isset( $wp->query_vars[ 'tax_query' ] ) || ! is_array( $wp->query_vars[ 'tax_query' ] ) ) {
445				$wp->query_vars[ 'tax_query' ] = array();
446			}
447		
448			$wp->query_vars[ 'tax_query' ][] = array(
449				'taxonomy' => $taxonomy,
450				'field' => 'slug',
451				'terms' => $terms,
452			);
453		
454		}
455	}
456
457	/**
458	 * Hooks the WP set_object_terms action to sync any untranslated
459	 * taxonomies across to the translations.
460	 *
461	 * @param int $object_id The object to relate to
462	 * @param array $terms The slugs or ids of the terms
463	 * @param array $tt_ids The term_taxonomy_ids
464	 * @param string $taxonomy The name of the taxonomy for which terms are being set
465	 * @param bool $append If false will delete difference of terms
466	 * @return void
467	 **/
468	public function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append ) {
469		if ( $this->no_recursion ) {
470			return;
471		}
472		$this->no_recursion = true;
473
474		// DO NOT SYNC THE TRANSID TAXONOMIES!!
475		if ( in_array( $taxonomy, $this->ignored_taxonomies() ) ) {
476			$this->no_recursion = false;
477			return;
478		}
479
480		if ( $this->is_taxonomy_translated( $taxonomy ) ) {
481			
482			// Here we assume that this taxonomy is on a post type
483			$translations = bbl_get_post_translations( $object_id );
484
485			foreach ( $translations as $lang_code => & $translation ) {
486
487				if ( bbl_get_post_lang_code( $object_id ) == $lang_code ) {
488					continue;
489				}
490
491				$translated_taxonomy = bbl_get_taxonomy_in_lang( $taxonomy, $lang_code );
492				$translated_terms = array();
493
494				foreach ( $terms as $term ) {
495
496					if ( is_int( $term ) ) {
497						$_term = get_term( $term, $taxonomy );
498					} else {
499						$_term = get_term_by( 'name', $term, $taxonomy );
500					}
501					if ( is_wp_error( $_term ) or empty( $_term ) ) {
502						continue;
503					}
504
505					$translated_term = $this->get_term_in_lang( $_term->term_id, $taxonomy, $lang_code, false );
506					$translated_terms[] = (int) $translated_term->term_id;
507
508				}
509
510				$result = wp_set_object_terms( $translation->ID, $translated_terms, $translated_taxonomy, $append );
511			}
512			
513		} else {
514
515			// Here we assume that this taxonomy is on a post type
516			$translations = bbl_get_post_translations( $object_id );
517			foreach ( $translations as $lang_code => & $translation ) {
518				if ( bbl_get_post_lang_code( $object_id ) == $lang_code ) {
519					continue;
520				}
521				wp_set_object_terms( $translation->ID, $terms, $taxonomy, $append );
522			}
523
524		}
525
526		$this->no_recursion = false;
527	}
528	
529	// CALLBACKS
530	// =========
531	
532	// PUBLIC METHODS
533	// ==============
534
535	public function admin_body_class( $class ) {
536
537		$taxonomy = get_current_screen() ? get_current_screen()->taxonomy : null;
538		if ( $taxonomy ) {
539			$class .= ' bbl-taxonomy-' . $taxonomy;
540		}
541
542		return $class;
543
544	}
545
546	public function bbl_translated_taxonomy( $translated, $taxonomy ) {
547		if ( 'term_translation' == $taxonomy ) {
548			return false;
549		}
550		if ( 'nav_menu' == $taxonomy ) {
551			return false;
552		}
553		if ( 'link_category' == $taxonomy ) {
554			return false;
555		}
556		if ( 'post_format' == $taxonomy ) {
557			return false;
558		}
559		return $translated;
560	}
561
562	/**
563	 * Provided with a taxonomy name, e.g. `post_tag`, and a language
564	 * code, will return the shadow taxonomy in that language.
565	 *
566	 * @param string $taxonomy The origin taxonomy 
567	 * @param string $lang_code The target language code
568	 * @return string The taxonomy name in that language
569	 **/
570	public function translated_taxonomy( $origin_taxonomy, $lang_code ) {
571		return strtolower( "{$origin_taxonomy}_{$lang_code}" );
572	}
573
574	/**
575	 * Get the terms which are the translations for the provided 
576	 * term ID. N.B. The returned array of term objects (and false 
577	 * values) will include the term for the term ID passed.
578	 * 
579	 * @FIXME: We should cache the translation groups, as we do for posts
580	 *
581	 * @param int|object $term Either a WP Term object, or a term_id 
582	 * @return array Either an array keyed by the site languages, each key containing false (if no translation) or a WP Term object
583	 **/
584	public function get_term_translations( $term, $taxonomy ) {
585		$term = get_term( $term, $taxonomy );
586
587		$langs = bbl_get_active_langs();
588		$translations = array();
589		foreach ( $langs as $lang ) {
590			$translations[ $lang->code ] = false;
591		}
592
593		$transid = $this->get_transid( $term->term_id );
594		// I thought the fracking bug where the get_objects_in_term function returned integers
595		// as strings was fixed. Seems not. See #17646 for details. Argh.
596		$term_ids = array_map( 'absint', get_objects_in_term( $transid, 'term_translation' ) );
597
598		// We're dealing with terms across multiple taxonomies
599		$base_taxonomy = isset( $this->taxonomies[ $taxonomy ] ) ? $this->taxonomies[ $taxonomy ] : $taxonomy ;
600		$taxonomies = array();
601		$taxonomies[] = $base_taxonomy;
602		foreach ( $this->lang_map as $lang_taxes ) {
603			if ( $lang_taxes[ $base_taxonomy ] ) {
604				$taxonomies[] = $lang_taxes[ $base_taxonomy ];
605			}
606		}
607
608		// Get all the translations in one cached DB query
609		$existing_terms = get_terms( $taxonomies, array( 'include' => $term_ids, 'hide_empty' => false ) );
610
611		// Finally, we're ready to return the terms in this 
612		// translation group.
613		$terms = array();
614		foreach ( $existing_terms as $t ) {
615			$terms[ $this->get_taxonomy_lang_code( $t->taxonomy ) ] = $t;
616		}
617		return $terms;
618	}
619
620	/**
621	 * Returns the term in a particular language, or the fallback content
622	 * if there's no term available.
623	 *
624	 * @param int|object $term Either a WP Term object, or a term_id 
625	 * @param string $lang_code The language code for the required language 
626	 * @param boolean $fallback If true: if a term is not available, fallback to the default language content (defaults to true)
627	 * @return object|boolean The WP Term object, or if $fallback was false and no post then returns false
628	 **/
629	public function get_term_in_lang( $term, $taxonomy, $lang_code, $fallback = true  ) {
630		$translations = $this->get_term_translations( $term, $taxonomy );
631		if ( isset( $translations[ $lang_code ] ) ) {
632			return $translations[ $lang_code ];
633		}
634		if ( ! $fallback ) {
635			return false;
636		}
637		return $translations[ bbl_get_default_lang_code() ];
638	}
639
640	/**
641	 * Return the admin URL to create a new translation for a term in a
642	 * particular language.
643	 *
644	 * @param int|object $default_term The term in the default language to create a new translation for, either WP Post object or post ID
645	 * @param string $lang The language code 
646	 * @return string The admin URL to create the new translation
647	 * @access public
648	 **/
649	public function get_new_term_translation_url( $default_term, $lang_code, $taxonomy = null ) {
650		if ( ! is_int( $default_term ) && is_null( $taxonomy ) ) {
651			throw new exception( 'get_new_term_translation_url: Cannot get term from term_id without taxonomy' );
652		}
653		if ( ! is_null( $taxonomy ) ) {
654			$default_term = get_term( $default_term, $taxonomy );
655		}
656		if ( is_wp_error( $default_term ) ) {
657			throw new exception( 'get_new_term_translation_url: Error getting term from term_id and taxonomy: ' . print_r( $default_term, true ) );
658		}
659		$url = admin_url( 'post-new.php' );
660		$args = array( 
661			'bbl_origin_term' => $default_term->term_id,
662			'bbl_origin_taxonomy' => $default_term->taxonomy,
663			'lang'            => $lang_code,
664			'post_type'       => 'bbl_job',
665		);
666		$url = add_query_arg( $args, $url );
667		return $url;
668	}
669
670	/**
671	 * Returns the language code associated with a particular taxonomy.
672	 *
673	 * @param string $taxonomy The taxonomy to get the language for 
674	 * @return string The lang code
675	 **/
676	public function get_taxonomy_lang_code( $taxonomy ) {
677		if ( ! isset( $this->taxonomies[ $taxonomy ] ) ) {
678			return bbl_get_default_lang_code();
679		}
680		foreach ( $this->lang_map as $lang => $data ) {
681			foreach ( $data as $trans_tax ) {
682				if ( $taxonomy == $trans_tax ) {
683					return $lang;
684				}
685			}
686		}
687		return false;
688	}
689
690	/**
691	 * Return the base taxonomy (in the default language) for a 
692	 * provided taxonomy.
693	 *
694	 * @param string $taxonomy The name of a taxonomy 
695	 * @return string The name of the base taxonomy
696	 **/
697	public function get_base_taxonomy( $taxonomy ) {
698		if ( ! isset( $this->taxonomies[ $taxonomy ] ) ) {
699			return $taxonomy;
700		}
701		return $this->taxonomies[ $taxonomy ];
702	}
703
704	/**
705	 * Returns the equivalent taxonomy in the specified language.
706	 *
707	 * @param string $taxonomy A taxonomy to return in a given language
708	 * @param string $lang_code The language code for the required language (optional, defaults to current)
709	 * @return boolean|string The taxonomy name, or false if no taxonomy was specified
710	 **/
711	public function get_taxonomy_in_lang( $taxonomy, $lang_code = null ) {
712		// Some taxonomies are untranslated…
713		if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
714			return $taxonomy;
715		}
716			
717		if ( ! $taxonomy ) {
718			return false; // @FIXME: Should I actually be throwing an error here?
719		}
720
721		if ( is_null( $lang_code ) ) {
722			$lang_code = bbl_get_current_lang_code();
723		}
724
725		$base_taxonomy = $this->get_base_taxonomy( $taxonomy );
726
727		if ( bbl_get_default_lang_code() == $lang_code ) {
728			return $base_taxonomy;
729		}
730
731		return $this->lang_map[ $lang_code ][ $base_taxonomy ];
732	}
733
734	/**
735	 * Returns a slug translated into a particular language.
736	 *
737	 * @TODO: This is more or less the same method as Babble_Post_Public::get_taxonomy_lang_code, do I need to DRY that up?
738	 *
739	 * @param string $slug The slug to translate
740	 * @param string $lang_code The language code for the required language (optional, defaults to current)
741	 * @return string A translated slug
742	 **/
743	public function get_slug_in_lang( $slug, $lang_code = null ) {
744		if ( is_null( $lang_code ) ) {
745			$lang_code = bbl_get_current_lang_code();
746		}
747		$_slug = mb_strtolower( apply_filters( 'bbl_translate_taxonomy_slug', $slug, $lang_code ) );
748		// @FIXME: For some languages the translation might be the same as the original
749		if ( $_slug &&  $_slug != $slug ) {
750			return $_slug;
751		}
752		// Do we need to check that the slug is unique at this point?
753		return mb_strtolower( "{$_slug}_{$lang_code}" );
754	}
755	
756
757	public function initialise_translation( $origin_term, $taxonomy, $lang_code ) {
758
759		$new_taxonomy = $this->get_slug_in_lang( $taxonomy, $lang_code );
760
761		$transid = $this->get_transid( $origin_term->term_id );
762
763		// Insert translation:
764		$this->no_recursion = true;
765		$new_term_id = wp_insert_term( $origin_term->name . ' - ' . $lang_code, $new_taxonomy );
766		$this->no_recursion = false;
767
768		$new_term = get_term( $new_term_id['term_id'], $new_taxonomy );
769
770		// Assign transid to translation:
771		$this->set_transid( $new_term_id['term_id'], $transid );
772
773		return $new_term;
774
775	}
776
777	// PRIVATE/PROTECTED METHODS
778	// =========================
779
780	/**
781	 * Will update term count based on object types of the current 
782	 * taxonomy. Will only count the post(s) in the default language.
783	 *
784	 * Private function for the default callback for post_tag and category
785	 * taxonomies.
786	 *
787	 * @param array $terms List of Term taxonomy IDs
788	 * @param object $taxonomy Current taxonomy object of terms
789	 */
790	function update_post_term_count( $terms, $taxonomy ) {
791		global $wpdb;
792
793		$object_types = (array) $taxonomy->object_type;
794
795		foreach ( $object_types as &$object_type ) {
796			list( $object_type ) = explode( ':', $object_type );
797			// Babble specific code, to only count in primary language
798			$object_type = bbl_get_post_type_in_lang( $object_type, bbl_get_default_lang_code() );
799		}
800
801		$object_types = array_unique( $object_types );
802
803		if ( false !== ( $check_attachments = array_search( 'attachment', $object_types ) ) ) {
804			unset( $object_types[ $check_attachments ] );
805			$check_attachments = true;
806		}
807
808		if ( $object_types ) {
809			$object_types = esc_sql( array_filter( $object_types, 'post_type_exists' ) );
810		}
811		foreach ( (array) $terms as $term ) {
812			$count = 0;
813
814			// Attachments can be 'inherit' status, we need to base count off the parent's status if so
815			if ( $check_attachments ) {
816				$count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts p1 WHERE p1.ID = $wpdb->term_relationships.object_id AND ( post_status = 'publish' OR ( post_status = 'inherit' AND post_parent > 0 AND ( SELECT post_status FROM $wpdb->posts WHERE ID = p1.post_parent ) = 'publish' ) ) AND post_type = 'attachment' AND term_taxonomy_id = %d", $term ) );
817			}
818
819			if ( $object_types ) {
820				$count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts WHERE $wpdb->posts.ID = $wpdb->term_relationships.object_id AND post_status = 'publish' AND post_type IN ('" . implode("', '", $object_types ) . "') AND term_taxonomy_id = %d", $term ) );
821			}
822
823			do_action( 'edit_term_taxonomy', $term, $taxonomy );
824			$wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) );
825			do_action( 'edited_term_taxonomy', $term, $taxonomy );
826		}
827	}
828
829	/**
830	 * Return the translation group ID (a term ID) that the given term ID 
831	 * belongs to.
832	 *
833	 * @param int $target_term_id The term ID to find the translation group for 
834	 * @return int The transID the target term belongs to
835	 **/
836	public function get_transid( $target_term_id ) {
837		if ( $transid = wp_cache_get( $target_term_id, 'bbl_term_transids' ) ) {
838			return $transid;
839		}
840
841		if ( ! $target_term_id ) {
842			throw new exception( "Please specify a target term_id" );
843		}
844
845		$transids = wp_get_object_terms( $target_term_id, 'term_translation', array( 'fields' => 'ids' ) );
846		// "There can be only one" (so we'll just drop the others)
847		if ( isset( $transids[ 0 ] ) ) {
848			$transid = $transids[ 0 ];
849		} else {
850			$transid = $this->set_transid( $target_term_id );
851		}
852
853		wp_cache_add( $target_term_id, $transid, 'bbl_term_transids' );
854
855		return $transid;
856	}
857
858	/**
859	 * Set the translation group ID (a term ID) that the given term ID 
860	 * belongs to.
861	 *
862	 * @param int $target_term_id The term ID to set the translation group for
863	 * @param int $translation_group_id The ID of the translation group to add this 
864	 * @return int The transID the target term belongs to
865	 **/
866	public function set_transid( $target_term_id, $transid = null ) {
867		if ( ! $target_term_id ) {
868			throw new exception( "Please specify a target term_id" );
869		}
870
871		if ( ! $transid ) {
872			$transid_name = 'term_transid_' . uniqid();
873			$result = wp_insert_term( $transid_name, 'term_translation', array() );
874			if ( is_wp_error( $result ) ) {
875				error_log( "Problem creating a new Term TransID: " . print_r( $result, true ) );
876			} else {
877				$transid = $result[ 'term_id' ];
878			}
879		}
880
881		$result = wp_set_object_terms( $target_term_id, absint( $transid ), 'term_translation' );
882		if ( is_wp_error( $result ) ) {
883			error_log( "Problem associating TransID with new posts: " . print_r( $result, true ) );
884		}
885
886		wp_cache_delete( $target_term_id, 'bbl_term_transids' );
887		
888		return $transid;
889	}
890
891	/**
892	 * Checks for the relevant POSTed field, then 
893	 * resyncs the terms.
894	 *
895	 * @param int $post_id The ID of the WP post
896	 * @param object $post The WP Post object 
897	 * @return void
898	 **/
899	protected function maybe_resync_terms( $post_id, $post ) {
900		// Check that the fields were included on the screen, we
901		// can do this by checking for the presence of the nonce.
902		$nonce = isset( $_POST[ '_bbl_metabox_resync' ] ) ? $_POST[ '_bbl_metabox_resync' ] : false;
903		
904		
905		if ( ! in_array( $post->post_status, array( 'draft', 'publish' ) ) ) {
906			return;
907		}
908		
909		if ( ! $nonce ) {
910			return;
911		}
912			
913		$posted_id = isset( $_POST[ 'post_ID' ] ) ? $_POST[ 'post_ID' ] : 0;
914		if ( $posted_id != $post_id ) {
915			return;
916		}
917		// While we're at it, let's check the nonce
918		check_admin_referer( "bbl_resync_translation-$post_id", '_bbl_metabox_resync' );
919		
920		if ( $this->no_recursion ) {
921			return;
922		}
923		$this->no_recursion = true;
924
925		$taxonomies = get_object_taxonomies( $post->post_type );
926		$origin_post = bbl_get_post_in_lang( $post_id, bbl_get_default_lang_code() );
927
928		// First dissociate all the terms from synced taxonomies from this post
929		wp_delete_object_term_relationships( $post_id, $taxonomies );
930
931		// Now associate terms from synced taxonomies in from the origin post
932		foreach ( $taxonomies as $taxonomy ) {
933			$origin_taxonomy = $taxonomy;
934			if ( $this->is_taxonomy_translated( $taxonomy ) ) {
935				$origin_taxonomy = bbl_get_taxonomy_in_lang( $taxonomy, bbl_get_default_lang_code() );
936			}
937			$term_ids = wp_get_object_terms( $origin_post->ID, $origin_taxonomy, array( 'fields' => 'ids' ) );
938			$term_ids = array_map( 'absint', $term_ids );
939			$result = wp_set_object_terms( $post_id, $term_ids, $taxonomy );
940			if ( is_wp_error( $result, true ) ) {
941				throw new exception( "Problem syncing terms: " . print_r( $terms, true ), " Error: " . print_r( $result, true ) );
942			}
943		}
944	}
945
946}
947
948global $bbl_taxonomies;
949$bbl_taxonomies = new Babble_Taxonomies();