babble /class-taxonomy.php

Language PHP Lines 950
MD5 Hash cd5c2eb44328c4191f3db0312413116e
Repository https://github.com/humanmade/babble.git View Raw File View Project SPDX
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
<?php

/**
 * Manages the translations for taxonomies.
 *
 * @package Babble
 * @since Alpha 1.2
 */
class Babble_Taxonomies extends Babble_Plugin {
	
	/**
	 * A simple flag to stop infinite recursion in various places.
	 *
	 * @var boolean
	 **/
	protected $no_recursion;
	
	/**
	 * The current version for purposes of rewrite rules, any 
	 * DB updates, cache busting, etc
	 *
	 * @var int
	 **/
	protected $version = 1;

	/**
	 * The shadow taxonomies created to handle the translated terms.
	 *
	 * @var array
	 **/
	protected $taxonomies;

	/**
	 * The languages represented by each of the shadow taxonomies.
	 *
	 * @var array
	 **/
	protected $lang_map;
	
	/**
	 * Setup any add_action or add_filter calls. Initiate properties.
	 *
	 * @return void
	 **/
	public function __construct() {
		$this->setup( 'babble-taxonomy', 'plugin' );
		$this->add_action( 'bbl_created_new_shadow_post', 'created_new_shadow_post', null, 2 );
		$this->add_action( 'bbl_registered_shadow_post_types', 'registered_shadow_post_types' );
		$this->add_action( 'init', 'init_early', 0 );
		$this->add_action( 'parse_request' );
		$this->add_action( 'registered_taxonomy', null, null, 3 );
		$this->add_action( 'save_post', null, null, 2 );
		$this->add_action( 'set_object_terms', null, null, 5 );
		$this->add_filter( 'get_terms' );
		$this->add_filter( 'term_link', null, null, 3 );
		$this->add_filter( 'bbl_translated_taxonomy', null, null, 2 );
		$this->add_filter( 'admin_body_class' );

	}
	
	// WP HOOKS
	// ========

	/**
	 * Hooks the WP init action early
	 *
	 * @return void
	 **/
	public function init_early() {
		// This translation will connect each term with it's translated equivalents
		register_taxonomy( 'term_translation', 'term', array(
			'rewrite' => false,
			'public' => true,  # ?
			'show_ui' => true, # ?
			'show_in_nav_menus' => false,
			'label' => __( 'Term Translation ID', 'babble' ),
		) );
	}
	
	/**
	 * Hooks the WP registered_taxonomy action 
	 *
	 * @param string $taxonomy The name of the newly registered taxonomy 
	 * @param string|array $args The object_type(s)
	 * @param array $args The args passed to register the taxonomy
	 * @return void
	 **/
	public function registered_taxonomy( $taxonomy, $object_type, $args ) {
		if ( in_array( $taxonomy, $this->ignored_taxonomies() )  ) {
			return;
		}

		if ( $this->no_recursion ) {
			return;
		}

		$this->no_recursion = true;

		if ( ! is_array( $object_type ) ) {
			$object_type = array_unique( (array) $object_type );
		}

		// Use the Babble term counting function, unless the taxonomy registrant
		// has defined their own – in which case we'll just have to hope against 
		// hope that it's Babble aware :S
		// FIXME: Setting this in the following fashion seems hacky… I feel uncomfortable.
		if ( empty( $GLOBALS[ 'wp_taxonomies' ][ $taxonomy ]->update_count_callback ) ) {
			$GLOBALS[ 'wp_taxonomies' ][ $taxonomy ]->update_count_callback = array( & $this, 'update_post_term_count' );
		}

		// Untranslated taxonomies do not have shadow equivalents in each language,
		// but do apply to the bast post_type and all it's shadow post_types.
		if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
			// Apply this taxonomy to all the shadow post types
			// of all of the base post_types it applies to.
			foreach ( $object_type as $ot ) {
				if ( ! ( $base_post_type = bbl_get_base_post_type( $ot ) ) ) {
					continue;
				}
				$shadow_post_types = bbl_get_shadow_post_types( $base_post_type );
				foreach ( $shadow_post_types as $shadow_post_type ) {
					register_taxonomy_for_object_type( $taxonomy, $shadow_post_type );
				}
			}

			$this->no_recursion = false;
			return;
		}

		// @FIXME: Not sure this is the best way to specify languages
		$langs = bbl_get_active_langs();

		// Lose the default language as any existing taxonomies are in that language
		unset( $langs[ bbl_get_default_lang_url_prefix() ] );

		// @FIXME: Is it reckless to convert ALL object instances in $args to an array?
		foreach ( $args as $key => & $arg ) {
			if ( is_object( $arg ) )
				$arg = get_object_vars( $arg );
			// Don't set any args reserved for built-in post_types
			if ( '_' == substr( $key, 0, 1 ) )
				unset( $args[ $key ] );
		}

		#$args[ 'rewrite' ] = false;
		unset( $args[ 'name' ] );
		unset( $args[ 'object_type' ] );

		$slug = ( $args[ 'rewrite' ][ 'slug' ] ) ? $args[ 'rewrite' ][ 'slug' ] : $taxonomy;

		foreach ( $langs as $lang ) {
			$new_args = $args;
			$new_object_type = array();
			// N.B. Here we assume that the taxonomy is on a post type
			foreach( $object_type as $ot )
				$new_object_type[] = bbl_get_post_type_in_lang( $ot, $lang->code );

			if ( false !== $args[ 'rewrite' ] ) {
				if ( ! is_array( $new_args[ 'rewrite' ] ) )
					$new_args[ 'rewrite' ] = array();
				// Do I not need to add this query_var into the query_vars filter? It seems not.
				$new_args[ 'query_var' ] = $new_args[ 'rewrite' ][ 'slug' ] = $this->get_slug_in_lang( $slug, $lang->code );
			}

			// @FIXME: Note currently we are in danger of a taxonomy name being longer than 32 chars
			// Perhaps we need to create some kind of map like (taxonomy) + (lang) => (shadow translated taxonomy)
			$new_taxonomy = strtolower( "{$taxonomy}_{$lang->code}" );

			$this->taxonomies[ $new_taxonomy ] = $taxonomy;
			if ( ! isset( $this->lang_map[ $lang->code ] ) || ! is_array( $this->lang_map[ $lang->code ] ) )
				$this->lang_map[ $lang->code ] = array();
			$this->lang_map[ $lang->code ][ $taxonomy ] = $new_taxonomy;
			
			register_taxonomy( $new_taxonomy, $new_object_type, $new_args );
			
		}
		// bbl_stop_logging();

		$this->no_recursion = false;
	}

	public function ignored_taxonomies() {
		return array( 'post_translation', 'term_translation' );
	}

	public function is_taxonomy_translated( $taxonomy ) {
		if( in_array( $taxonomy, $this->ignored_taxonomies() ) ) {
			return false;
		}

		// @FIXME: Remove this when menu's are translatable
		if( 'nav_menu' == $taxonomy ) {
			return false;
		}

		return apply_filters( 'bbl_translated_taxonomy', true, $taxonomy );
	}

	/**
	 * Hooks the WP bbl_registered_shadow_post_types action to check that we've applied
	 * all untranslated taxonomies to the shadow post types created for this base
	 * post type. 
	 * 
	 * @param string $post_type The post type for which the shadow post types have been registered. 
	 * @return void
	 **/
	public function registered_shadow_post_types( $post_type ) {
		$taxonomies = get_object_taxonomies( $post_type );

		$object_type = (array) $post_type;
		
		foreach ( $taxonomies as $taxonomy ) {
			// Untranslated taxonomies do not have shadow equivalents in each language,
			// but do apply to the bast post_type and all it's shadow post_types.
			if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
				// Apply this taxonomy to all the shadow post types
				// of all of the base post_types it applies to.
				foreach ( $object_type as $ot ) {
					if ( ! ( $base_post_type = bbl_get_base_post_type( $ot ) ) ) {
						continue;
					}
					$shadow_post_types = bbl_get_shadow_post_types( $base_post_type );
					foreach ( $shadow_post_types as $shadow_post_type ) {
						register_taxonomy_for_object_type( $taxonomy, $shadow_post_type );
					}
				}
			}
		}
	}

	/**
	 * Hooks the Babble action bbl_created_new_shadow_post, which is fired
	 * when a new translation post is created, to sync any existing untranslated
	 * taxonomy terms.
	 *
	 * @param int $new_post_id The ID of the new post (to sync to)
	 * @param int $origin_post_id The ID of the originating post (to sync from)
	 * @return void
	 **/
	public function created_new_shadow_post( $new_post_id, $origin_post_id ) {
		$new_post = get_post( $new_post_id );
		if ( ! ( $origin_post = get_post( $origin_post_id ) ) ) {
			return;
		}
		
		if ( $this->no_recursion ) {
			return;
		}
		$this->no_recursion = true;
		
		$taxonomies = get_object_taxonomies( $origin_post->post_type );

		foreach ( $taxonomies as $taxonomy ) {
			if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
				$term_ids = wp_get_object_terms( $origin_post->ID, $taxonomy, array( 'fields' => 'ids' ) );
				$term_ids = array_map( 'absint', $term_ids );
				wp_set_object_terms( $new_post->ID, $term_ids, $taxonomy );
			}
		}

		$this->no_recursion = false;
	}

	/**
	 * Hooks the WP save_post action to resync data
	 * when requested.
	 *
	 * @param int $post_id The ID of the WP post
	 * @param object $post The WP Post object 
	 * @return void
	 **/
	public function save_post( $post_id, $post ) {
		$this->maybe_resync_terms( $post_id, $post );
	}

	/**
	 * Hooks the WordPress term_link filter to provide functions to provide
	 * appropriate links for the shadow taxonomies. 
	 *
	 * @see get_term_link from whence much of this was copied
	 *
	 * @param string $termlink The currently generated term URL
	 * @param object $term The WordPress term object we're generating a link for
	 * @param string $taxonomy The 
	 * @return string The term link
	 **/
	public function term_link( $termlink, $term, $taxonomy ) {
		$taxonomy = strtolower( $taxonomy );
		// No need to worry about the built in taxonomies
		if ( 'post_tag' == $taxonomy || 'category' == $taxonomy || ! isset( $this->taxonomies[ $taxonomy ] ) ) {
			return $termlink;
		}
	
		// Deal with our shadow taxonomies
		if ( ! ( $base_taxonomy = $this->get_base_taxonomy( $taxonomy ) ) ) {
			return $termlink;
		}
	
		// START copying from get_term_link, replacing $taxonomy with $base_taxonomy
		global $wp_rewrite;
	
		if ( !is_object($term) ) {
			if ( is_int($term) ) {
				$term = &get_term($term, $base_taxonomy);
			} else {
				$term = &get_term_by('slug', $term, $base_taxonomy);
			}
		}
	
		if ( !is_object($term) ) {
			$term = new WP_Error('invalid_term', __('Empty Term', 'babble'));
		}
	
		if ( is_wp_error( $term ) ) {
			return $term;
		}
	
		$termlink = $wp_rewrite->get_extra_permastruct($base_taxonomy);
	
		$slug = $term->slug;
		$t = get_taxonomy($base_taxonomy);
	
		if ( empty($termlink) ) {
			if ( 'category' == $base_taxonomy ) {
				$termlink = '?cat=' . $term->term_id;
			} elseif ( $t->query_var ) {
				$termlink = "?$t->query_var=$slug";
			} else {
				$termlink = "?taxonomy=$base_taxonomy&term=$slug";
			}
			$termlink = home_url($termlink);
		} else {
			if ( $t->rewrite['hierarchical'] ) {
				$hierarchical_slugs = array();
				$ancestors = get_ancestors($term->term_id, $base_taxonomy);
				foreach ( (array)$ancestors as $ancestor ) {
					$ancestor_term = get_term($ancestor, $base_taxonomy);
					$hierarchical_slugs[] = $ancestor_term->slug;
				}
				$hierarchical_slugs = array_reverse($hierarchical_slugs);
				$hierarchical_slugs[] = $slug;
				$termlink = str_replace("%$base_taxonomy%", implode('/', $hierarchical_slugs), $termlink);
			} else {
				$termlink = str_replace("%$base_taxonomy%", $slug, $termlink);
			}
			$termlink = home_url( user_trailingslashit($termlink, 'category') );
		}
		// STOP copying from get_term_link
	
		return $termlink;
	}

	/**
	 * Hooks the WP get_terms filter to ensure the terms all have transids.
	 *
	 * @param array $terms The terms which have been got 
	 * @return array The terms which were got
	 **/
	public function get_terms( $terms ) {
		foreach ( $terms as $term ) {
			if ( empty( $term ) ) {
				continue;
			}
			if ( isset( $this->taxonomies ) ) {
				continue;
			}
			if ( isset( $this->taxonomies[ $term->taxonomy ] ) ) {
				if ( ! $this->get_transid( $term->term_id ) ) {
					throw new exception( "ERROR: Translated term ID $term->term_id does not have a transid" );
				} else {
					continue;
				}
			}
			if ( ! $this->get_transid( $term->term_id ) ) {
				$this->set_transid( $term->term_id );
			}
		}
		return $terms;
	}

	/**
	 * Hooks the WP parse_request action 
	 *
	 * FIXME: Should I be extending and replacing the WP class?
	 *
	 * @param object $wp WP object, passed by reference (so no need to return)
	 * @return void
	 **/
	public function parse_request( $wp ) {

		if ( is_admin() ) {
			return;
		}

		// Sequester the original query, in case we need it to get the default content later
		if ( ! isset( $wp->query_vars[ 'bbl_tax_original_query' ] ) ) {
			$wp->query_vars[ 'bbl_tax_original_query' ] = $wp->query_vars;
		}

		$taxonomy 	= false;
		$terms 		= false;

		$taxonomies = get_taxonomies( null, 'objects' );
		$lang_taxonomies = array();
		foreach ( $taxonomies as $taxonomy => $tax_obj ) {
			$tax = $this->get_taxonomy_in_lang( $taxonomy, bbl_get_current_lang_code() );
			$lang_taxonomies[ $tax_obj->rewrite[ 'slug' ] ] = $tax;
		}

		if ( isset( $wp->query_vars[ 'tag' ] ) ) {
			$taxonomy = $this->get_taxonomy_in_lang( 'post_tag', $wp->query_vars[ 'lang' ] );
			$terms = $wp->query_vars[ 'tag' ];
			unset( $wp->query_vars[ 'tag' ] );
		} else if ( isset( $wp->query_vars[ 'category_name' ] ) ) {
			$taxonomy = $this->get_taxonomy_in_lang( 'category', $wp->query_vars[ 'lang' ] );
			$terms = $wp->query_vars[ 'category_name' ];
			unset( $wp->query_vars[ 'category_name' ] );
		} else {
			$taxonomies = array();
			foreach ( $lang_taxonomies as $slug => $tax ) {
				if ( isset( $wp->query_vars[ $slug ] ) ) {
					$taxonomies[] = $tax;
					break;
				}
			}
			
			if ( $taxonomies ) {
				$post_types = array();
				foreach ( $taxonomies as $taxonomy ) {
					$taxonomy = get_taxonomy( $taxonomy );
					$post_types = array_merge( $post_types, $taxonomy->object_type );
					// Filter out the post_types not in this language
					foreach ( $post_types as & $post_type ) {
						$post_type = bbl_get_post_type_in_lang( $post_type );
					}
					$post_types = array_unique( $post_types );
				}
				$wp->query_vars[ 'post_type' ] = $post_types;
			}
		}

		if ( $taxonomy && $terms ) {

			if ( ! isset( $wp->query_vars[ 'tax_query' ] ) || ! is_array( $wp->query_vars[ 'tax_query' ] ) ) {
				$wp->query_vars[ 'tax_query' ] = array();
			}
		
			$wp->query_vars[ 'tax_query' ][] = array(
				'taxonomy' => $taxonomy,
				'field' => 'slug',
				'terms' => $terms,
			);
		
		}
	}

	/**
	 * Hooks the WP set_object_terms action to sync any untranslated
	 * taxonomies across to the translations.
	 *
	 * @param int $object_id The object to relate to
	 * @param array $terms The slugs or ids of the terms
	 * @param array $tt_ids The term_taxonomy_ids
	 * @param string $taxonomy The name of the taxonomy for which terms are being set
	 * @param bool $append If false will delete difference of terms
	 * @return void
	 **/
	public function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append ) {
		if ( $this->no_recursion ) {
			return;
		}
		$this->no_recursion = true;

		// DO NOT SYNC THE TRANSID TAXONOMIES!!
		if ( in_array( $taxonomy, $this->ignored_taxonomies() ) ) {
			$this->no_recursion = false;
			return;
		}

		if ( $this->is_taxonomy_translated( $taxonomy ) ) {
			
			// Here we assume that this taxonomy is on a post type
			$translations = bbl_get_post_translations( $object_id );

			foreach ( $translations as $lang_code => & $translation ) {

				if ( bbl_get_post_lang_code( $object_id ) == $lang_code ) {
					continue;
				}

				$translated_taxonomy = bbl_get_taxonomy_in_lang( $taxonomy, $lang_code );
				$translated_terms = array();

				foreach ( $terms as $term ) {

					if ( is_int( $term ) ) {
						$_term = get_term( $term, $taxonomy );
					} else {
						$_term = get_term_by( 'name', $term, $taxonomy );
					}
					if ( is_wp_error( $_term ) or empty( $_term ) ) {
						continue;
					}

					$translated_term = $this->get_term_in_lang( $_term->term_id, $taxonomy, $lang_code, false );
					$translated_terms[] = (int) $translated_term->term_id;

				}

				$result = wp_set_object_terms( $translation->ID, $translated_terms, $translated_taxonomy, $append );
			}
			
		} else {

			// Here we assume that this taxonomy is on a post type
			$translations = bbl_get_post_translations( $object_id );
			foreach ( $translations as $lang_code => & $translation ) {
				if ( bbl_get_post_lang_code( $object_id ) == $lang_code ) {
					continue;
				}
				wp_set_object_terms( $translation->ID, $terms, $taxonomy, $append );
			}

		}

		$this->no_recursion = false;
	}
	
	// CALLBACKS
	// =========
	
	// PUBLIC METHODS
	// ==============

	public function admin_body_class( $class ) {

		$taxonomy = get_current_screen() ? get_current_screen()->taxonomy : null;
		if ( $taxonomy ) {
			$class .= ' bbl-taxonomy-' . $taxonomy;
		}

		return $class;

	}

	public function bbl_translated_taxonomy( $translated, $taxonomy ) {
		if ( 'term_translation' == $taxonomy ) {
			return false;
		}
		if ( 'nav_menu' == $taxonomy ) {
			return false;
		}
		if ( 'link_category' == $taxonomy ) {
			return false;
		}
		if ( 'post_format' == $taxonomy ) {
			return false;
		}
		return $translated;
	}

	/**
	 * Provided with a taxonomy name, e.g. `post_tag`, and a language
	 * code, will return the shadow taxonomy in that language.
	 *
	 * @param string $taxonomy The origin taxonomy 
	 * @param string $lang_code The target language code
	 * @return string The taxonomy name in that language
	 **/
	public function translated_taxonomy( $origin_taxonomy, $lang_code ) {
		return strtolower( "{$origin_taxonomy}_{$lang_code}" );
	}

	/**
	 * Get the terms which are the translations for the provided 
	 * term ID. N.B. The returned array of term objects (and false 
	 * values) will include the term for the term ID passed.
	 * 
	 * @FIXME: We should cache the translation groups, as we do for posts
	 *
	 * @param int|object $term Either a WP Term object, or a term_id 
	 * @return array Either an array keyed by the site languages, each key containing false (if no translation) or a WP Term object
	 **/
	public function get_term_translations( $term, $taxonomy ) {
		$term = get_term( $term, $taxonomy );

		$langs = bbl_get_active_langs();
		$translations = array();
		foreach ( $langs as $lang ) {
			$translations[ $lang->code ] = false;
		}

		$transid = $this->get_transid( $term->term_id );
		// I thought the fracking bug where the get_objects_in_term function returned integers
		// as strings was fixed. Seems not. See #17646 for details. Argh.
		$term_ids = array_map( 'absint', get_objects_in_term( $transid, 'term_translation' ) );

		// We're dealing with terms across multiple taxonomies
		$base_taxonomy = isset( $this->taxonomies[ $taxonomy ] ) ? $this->taxonomies[ $taxonomy ] : $taxonomy ;
		$taxonomies = array();
		$taxonomies[] = $base_taxonomy;
		foreach ( $this->lang_map as $lang_taxes ) {
			if ( $lang_taxes[ $base_taxonomy ] ) {
				$taxonomies[] = $lang_taxes[ $base_taxonomy ];
			}
		}

		// Get all the translations in one cached DB query
		$existing_terms = get_terms( $taxonomies, array( 'include' => $term_ids, 'hide_empty' => false ) );

		// Finally, we're ready to return the terms in this 
		// translation group.
		$terms = array();
		foreach ( $existing_terms as $t ) {
			$terms[ $this->get_taxonomy_lang_code( $t->taxonomy ) ] = $t;
		}
		return $terms;
	}

	/**
	 * Returns the term in a particular language, or the fallback content
	 * if there's no term available.
	 *
	 * @param int|object $term Either a WP Term object, or a term_id 
	 * @param string $lang_code The language code for the required language 
	 * @param boolean $fallback If true: if a term is not available, fallback to the default language content (defaults to true)
	 * @return object|boolean The WP Term object, or if $fallback was false and no post then returns false
	 **/
	public function get_term_in_lang( $term, $taxonomy, $lang_code, $fallback = true  ) {
		$translations = $this->get_term_translations( $term, $taxonomy );
		if ( isset( $translations[ $lang_code ] ) ) {
			return $translations[ $lang_code ];
		}
		if ( ! $fallback ) {
			return false;
		}
		return $translations[ bbl_get_default_lang_code() ];
	}

	/**
	 * Return the admin URL to create a new translation for a term in a
	 * particular language.
	 *
	 * @param int|object $default_term The term in the default language to create a new translation for, either WP Post object or post ID
	 * @param string $lang The language code 
	 * @return string The admin URL to create the new translation
	 * @access public
	 **/
	public function get_new_term_translation_url( $default_term, $lang_code, $taxonomy = null ) {
		if ( ! is_int( $default_term ) && is_null( $taxonomy ) ) {
			throw new exception( 'get_new_term_translation_url: Cannot get term from term_id without taxonomy' );
		}
		if ( ! is_null( $taxonomy ) ) {
			$default_term = get_term( $default_term, $taxonomy );
		}
		if ( is_wp_error( $default_term ) ) {
			throw new exception( 'get_new_term_translation_url: Error getting term from term_id and taxonomy: ' . print_r( $default_term, true ) );
		}
		$url = admin_url( 'post-new.php' );
		$args = array( 
			'bbl_origin_term' => $default_term->term_id,
			'bbl_origin_taxonomy' => $default_term->taxonomy,
			'lang'            => $lang_code,
			'post_type'       => 'bbl_job',
		);
		$url = add_query_arg( $args, $url );
		return $url;
	}

	/**
	 * Returns the language code associated with a particular taxonomy.
	 *
	 * @param string $taxonomy The taxonomy to get the language for 
	 * @return string The lang code
	 **/
	public function get_taxonomy_lang_code( $taxonomy ) {
		if ( ! isset( $this->taxonomies[ $taxonomy ] ) ) {
			return bbl_get_default_lang_code();
		}
		foreach ( $this->lang_map as $lang => $data ) {
			foreach ( $data as $trans_tax ) {
				if ( $taxonomy == $trans_tax ) {
					return $lang;
				}
			}
		}
		return false;
	}

	/**
	 * Return the base taxonomy (in the default language) for a 
	 * provided taxonomy.
	 *
	 * @param string $taxonomy The name of a taxonomy 
	 * @return string The name of the base taxonomy
	 **/
	public function get_base_taxonomy( $taxonomy ) {
		if ( ! isset( $this->taxonomies[ $taxonomy ] ) ) {
			return $taxonomy;
		}
		return $this->taxonomies[ $taxonomy ];
	}

	/**
	 * Returns the equivalent taxonomy in the specified language.
	 *
	 * @param string $taxonomy A taxonomy to return in a given language
	 * @param string $lang_code The language code for the required language (optional, defaults to current)
	 * @return boolean|string The taxonomy name, or false if no taxonomy was specified
	 **/
	public function get_taxonomy_in_lang( $taxonomy, $lang_code = null ) {
		// Some taxonomies are untranslated
		if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
			return $taxonomy;
		}
			
		if ( ! $taxonomy ) {
			return false; // @FIXME: Should I actually be throwing an error here?
		}

		if ( is_null( $lang_code ) ) {
			$lang_code = bbl_get_current_lang_code();
		}

		$base_taxonomy = $this->get_base_taxonomy( $taxonomy );

		if ( bbl_get_default_lang_code() == $lang_code ) {
			return $base_taxonomy;
		}

		return $this->lang_map[ $lang_code ][ $base_taxonomy ];
	}

	/**
	 * Returns a slug translated into a particular language.
	 *
	 * @TODO: This is more or less the same method as Babble_Post_Public::get_taxonomy_lang_code, do I need to DRY that up?
	 *
	 * @param string $slug The slug to translate
	 * @param string $lang_code The language code for the required language (optional, defaults to current)
	 * @return string A translated slug
	 **/
	public function get_slug_in_lang( $slug, $lang_code = null ) {
		if ( is_null( $lang_code ) ) {
			$lang_code = bbl_get_current_lang_code();
		}
		$_slug = mb_strtolower( apply_filters( 'bbl_translate_taxonomy_slug', $slug, $lang_code ) );
		// @FIXME: For some languages the translation might be the same as the original
		if ( $_slug &&  $_slug != $slug ) {
			return $_slug;
		}
		// Do we need to check that the slug is unique at this point?
		return mb_strtolower( "{$_slug}_{$lang_code}" );
	}
	

	public function initialise_translation( $origin_term, $taxonomy, $lang_code ) {

		$new_taxonomy = $this->get_slug_in_lang( $taxonomy, $lang_code );

		$transid = $this->get_transid( $origin_term->term_id );

		// Insert translation:
		$this->no_recursion = true;
		$new_term_id = wp_insert_term( $origin_term->name . ' - ' . $lang_code, $new_taxonomy );
		$this->no_recursion = false;

		$new_term = get_term( $new_term_id['term_id'], $new_taxonomy );

		// Assign transid to translation:
		$this->set_transid( $new_term_id['term_id'], $transid );

		return $new_term;

	}

	// PRIVATE/PROTECTED METHODS
	// =========================

	/**
	 * Will update term count based on object types of the current 
	 * taxonomy. Will only count the post(s) in the default language.
	 *
	 * Private function for the default callback for post_tag and category
	 * taxonomies.
	 *
	 * @param array $terms List of Term taxonomy IDs
	 * @param object $taxonomy Current taxonomy object of terms
	 */
	function update_post_term_count( $terms, $taxonomy ) {
		global $wpdb;

		$object_types = (array) $taxonomy->object_type;

		foreach ( $object_types as &$object_type ) {
			list( $object_type ) = explode( ':', $object_type );
			// Babble specific code, to only count in primary language
			$object_type = bbl_get_post_type_in_lang( $object_type, bbl_get_default_lang_code() );
		}

		$object_types = array_unique( $object_types );

		if ( false !== ( $check_attachments = array_search( 'attachment', $object_types ) ) ) {
			unset( $object_types[ $check_attachments ] );
			$check_attachments = true;
		}

		if ( $object_types ) {
			$object_types = esc_sql( array_filter( $object_types, 'post_type_exists' ) );
		}
		foreach ( (array) $terms as $term ) {
			$count = 0;

			// Attachments can be 'inherit' status, we need to base count off the parent's status if so
			if ( $check_attachments ) {
				$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 ) );
			}

			if ( $object_types ) {
				$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 ) );
			}

			do_action( 'edit_term_taxonomy', $term, $taxonomy );
			$wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) );
			do_action( 'edited_term_taxonomy', $term, $taxonomy );
		}
	}

	/**
	 * Return the translation group ID (a term ID) that the given term ID 
	 * belongs to.
	 *
	 * @param int $target_term_id The term ID to find the translation group for 
	 * @return int The transID the target term belongs to
	 **/
	public function get_transid( $target_term_id ) {
		if ( $transid = wp_cache_get( $target_term_id, 'bbl_term_transids' ) ) {
			return $transid;
		}

		if ( ! $target_term_id ) {
			throw new exception( "Please specify a target term_id" );
		}

		$transids = wp_get_object_terms( $target_term_id, 'term_translation', array( 'fields' => 'ids' ) );
		// "There can be only one" (so we'll just drop the others)
		if ( isset( $transids[ 0 ] ) ) {
			$transid = $transids[ 0 ];
		} else {
			$transid = $this->set_transid( $target_term_id );
		}

		wp_cache_add( $target_term_id, $transid, 'bbl_term_transids' );

		return $transid;
	}

	/**
	 * Set the translation group ID (a term ID) that the given term ID 
	 * belongs to.
	 *
	 * @param int $target_term_id The term ID to set the translation group for
	 * @param int $translation_group_id The ID of the translation group to add this 
	 * @return int The transID the target term belongs to
	 **/
	public function set_transid( $target_term_id, $transid = null ) {
		if ( ! $target_term_id ) {
			throw new exception( "Please specify a target term_id" );
		}

		if ( ! $transid ) {
			$transid_name = 'term_transid_' . uniqid();
			$result = wp_insert_term( $transid_name, 'term_translation', array() );
			if ( is_wp_error( $result ) ) {
				error_log( "Problem creating a new Term TransID: " . print_r( $result, true ) );
			} else {
				$transid = $result[ 'term_id' ];
			}
		}

		$result = wp_set_object_terms( $target_term_id, absint( $transid ), 'term_translation' );
		if ( is_wp_error( $result ) ) {
			error_log( "Problem associating TransID with new posts: " . print_r( $result, true ) );
		}

		wp_cache_delete( $target_term_id, 'bbl_term_transids' );
		
		return $transid;
	}

	/**
	 * Checks for the relevant POSTed field, then 
	 * resyncs the terms.
	 *
	 * @param int $post_id The ID of the WP post
	 * @param object $post The WP Post object 
	 * @return void
	 **/
	protected function maybe_resync_terms( $post_id, $post ) {
		// Check that the fields were included on the screen, we
		// can do this by checking for the presence of the nonce.
		$nonce = isset( $_POST[ '_bbl_metabox_resync' ] ) ? $_POST[ '_bbl_metabox_resync' ] : false;
		
		
		if ( ! in_array( $post->post_status, array( 'draft', 'publish' ) ) ) {
			return;
		}
		
		if ( ! $nonce ) {
			return;
		}
			
		$posted_id = isset( $_POST[ 'post_ID' ] ) ? $_POST[ 'post_ID' ] : 0;
		if ( $posted_id != $post_id ) {
			return;
		}
		// While we're at it, let's check the nonce
		check_admin_referer( "bbl_resync_translation-$post_id", '_bbl_metabox_resync' );
		
		if ( $this->no_recursion ) {
			return;
		}
		$this->no_recursion = true;

		$taxonomies = get_object_taxonomies( $post->post_type );
		$origin_post = bbl_get_post_in_lang( $post_id, bbl_get_default_lang_code() );

		// First dissociate all the terms from synced taxonomies from this post
		wp_delete_object_term_relationships( $post_id, $taxonomies );

		// Now associate terms from synced taxonomies in from the origin post
		foreach ( $taxonomies as $taxonomy ) {
			$origin_taxonomy = $taxonomy;
			if ( $this->is_taxonomy_translated( $taxonomy ) ) {
				$origin_taxonomy = bbl_get_taxonomy_in_lang( $taxonomy, bbl_get_default_lang_code() );
			}
			$term_ids = wp_get_object_terms( $origin_post->ID, $origin_taxonomy, array( 'fields' => 'ids' ) );
			$term_ids = array_map( 'absint', $term_ids );
			$result = wp_set_object_terms( $post_id, $term_ids, $taxonomy );
			if ( is_wp_error( $result, true ) ) {
				throw new exception( "Problem syncing terms: " . print_r( $terms, true ), " Error: " . print_r( $result, true ) );
			}
		}
	}

}

global $bbl_taxonomies;
$bbl_taxonomies = new Babble_Taxonomies();
Back to Top