Repository: Automattic/babble Branch: main Commit: 7f89ef408984 Files: 54 Total size: 354.0 KB Directory structure: gitextract_5i66y00t/ ├── CONTRIBUTING.md ├── api.php ├── babble.php ├── class-admin-bar.php ├── class-babble-log.php ├── class-comment.php ├── class-jobs.php ├── class-languages.php ├── class-locale.php ├── class-meta.php ├── class-plugin.php ├── class-post-public.php ├── class-switcher-content.php ├── class-switcher-interface.php ├── class-taxonomy.php ├── class-translator.php ├── class-updates.php ├── composer.json ├── css/ │ ├── jobs-admin.css │ └── languages-options.css ├── deprecated.php ├── external-update-api/ │ ├── external-update-api/ │ │ ├── euapi.php │ │ ├── handler-files.php │ │ ├── handler-github.php │ │ ├── handler.php │ │ ├── info.php │ │ ├── item-plugin.php │ │ ├── item-theme.php │ │ ├── item.php │ │ └── update.php │ ├── external-update-api.php │ └── readme.md ├── js/ │ └── post-public-admin.js ├── languages/ │ ├── fa_IR.mo │ ├── fa_IR.php │ ├── fr_FR.mo │ ├── pt_BR.mo │ ├── pt_BR.po │ ├── tr.mo │ └── tr.po ├── miscellaneous.php ├── readme.md ├── readme.txt ├── templates-admin/ │ ├── options-available-languages.php │ ├── switcher-interface.php │ ├── translation-editor-meta.php │ ├── translation-editor-post-excerpt.php │ ├── translation-editor-terms.php │ ├── translation-editor.php │ └── translation-groups.php ├── translation-fields.php ├── translation-group-tool-sorter.php ├── translation-group-tool.php └── widget.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: CONTRIBUTING.md ================================================ Contribute ========== First off: thanks for looking into contributing to Babble, we appreciate your help. Setting up ---------- 1. Clone this git repository on your WordPress development site. 2. Switch to the develop branch Submitting patches ------------------ Whether you want to fix a bug or implement a new feature, the process is much the same: 0. [Search existing issues](https://github.com/cftp/babble/issues); if you can't find anything related to what you want to work on, open a new issue so that you can get some initial feedback. 1. [Fork](https://github.com/cftp/babble/fork) the repository. 2. Push the code changes from your local clone to your fork. 3. Open a pull request to the *develop* branch. It doesn't matter if the code isn't perfect. The idea is to get it reviewed early and iterate on it. Please follow the [WordPress Coding Standards](http://make.wordpress.org/core/handbook/coding-standards/). Licence ------- The Babble plugin is released under the [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html), by contributing code to Babble you grant its use under the GNU General Public License v2 (or later). ================================================ FILE: api.php ================================================ get_content_lang(); } /** * Returns the current interface language code. * * @FIXME: Currently does not check for language validity, though perhaps we should check that elsewhere and redirect? * * @return string A language code * @access public **/ function bbl_get_current_interface_lang_code() { global $bbl_locale; return $bbl_locale->get_interface_lang(); } /** * Returns the current (content) language code. * * @return string A language code * @access public **/ function bbl_get_current_lang_code() { return bbl_get_current_content_lang_code(); } /** * Given a lang object or lang code, this checks whether the * language is public or not. * * @param string $lang_code A language code * @return boolean True if public * @access public **/ function bbl_is_public_lang( $lang_code ) { global $bbl_languages; return $bbl_languages->is_public_lang( $lang_code ); } /** * Set the current (content) lang. * * @uses Babble_Locale::switch_to_lang to do the actual work * @see switch_to_blog for similarities * * @param string $lang The language code to switch to * @return void **/ function bbl_switch_to_lang( $lang ) { global $bbl_locale; $bbl_locale->switch_to_lang( $lang ); } /** * Restore the previous lang. * * @uses Babble_Locale::restore_lang to do the actual work * @see restore_current_blog for similarities * * @return void **/ function bbl_restore_lang() { global $bbl_locale; $bbl_locale->restore_lang(); } /** * 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. * * @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 Post object * @access public **/ function bbl_get_term_translations( $term, $taxonomy ) { global $bbl_taxonomies; return $bbl_taxonomies->get_term_translations( $term, $taxonomy ); } /** * Get the posts which are the translation jobs for the provided * term ID. * * @param int|object $term Either a WP Term object, or a term_id * @return array An array keyed by the site languages, each key containing a WP Post object * @access public **/ function bbl_get_term_jobs( $term, $taxonomy ) { global $bbl_jobs; return $bbl_jobs->get_term_jobs( $term, $taxonomy ); } /** * 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 * @param string $taxonomy The taxonomy * @return string The admin URL to create the new translation * @access public **/ function bbl_get_new_term_translation_url( $default_term, $lang, $taxonomy = null ) { global $bbl_taxonomies; return $bbl_taxonomies->get_new_term_translation_url( $default_term, $lang, $taxonomy ); } /** * Returns the language code associated with a particular taxonomy. * * @param string $taxonomy The taxonomy to get the language for * @return string The lang code **/ function bbl_get_taxonomy_lang_code( $taxonomy ) { global $bbl_taxonomies; return $bbl_taxonomies->get_taxonomy_lang_code( $taxonomy ); } /** * 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 **/ function bbl_get_base_taxonomy( $taxonomy ) { global $bbl_taxonomies; return $bbl_taxonomies->get_base_taxonomy( $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 string The taxonomy name **/ function bbl_get_taxonomy_in_lang( $taxonomy, $lang_code = null ) { global $bbl_taxonomies; return $bbl_taxonomies->get_taxonomy_in_lang( $taxonomy, $lang_code ); } /** * Test whether a particular taxonomy is translated or not. * * @param string $taxonomy The name of the taxonomy to check * @return bool True if this is a translated taxonomy */ function bbl_is_translated_taxonomy( $taxonomy ) { return (bool) apply_filters( 'bbl_translated_taxonomy', true, $taxonomy ); } /** * Test whether a particular post type is translated or not. * * @param string $post_type The name of the post type to check * @return bool True if this is a translated post type */ function bbl_is_translated_post_type( $post_type ) { return (bool) apply_filters( 'bbl_translated_post_type', true, $post_type ); } /** * Returns a taxonomy slug translated into a particular language. * * @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 **/ function bbl_get_taxonomy_slug_in_lang( $slug, $lang_code = null ) { global $bbl_taxonomies; return $bbl_taxonomies->get_slug_in_lang( $slug, $lang_code ); } /** * Get the posts which are the translations for the provided * post ID. N.B. The returned array of post objects (and false * values) will include the post for the post ID passed. * * @param int|object $post Either a WP Post object, or a post ID * @return array Either an array keyed by the site languages, each key containing false (if no translation) or a WP Post object * @access public **/ function bbl_get_post_translations( $post ) { global $bbl_post_public; return $bbl_post_public->get_post_translations( $post ); } /** * Get the posts which are the translation jobs for the provided * post ID. * * @param int|object $post Either a WP Post object, or a post ID * @return array Either an array keyed by the site languages, each key containing a WP Post object * @access public **/ function bbl_get_incomplete_post_jobs( $post ) { global $bbl_jobs; return $bbl_jobs->get_incomplete_post_jobs( $post ); } /** * Returns the post ID for the post in the default language from which * this post was translated. * * @param int|object $post Either a WP Post object, or a post ID * @return int The ID of the default language equivalent post * @access public **/ function bbl_get_default_lang_post( $post ) { global $bbl_post_public; return $bbl_post_public->get_default_lang_post( $post ); } /** * Return the language code for the language a given post is written for/in. * * @param int|object $post Either a WP Post object, or a post ID * @return string|object Either a language code, or a WP_Error object * @access public **/ function bbl_get_post_lang_code( $post ) { global $bbl_post_public; return $bbl_post_public->get_post_lang_code( $post ); } /** * Return the admin URL to create a new translation for a post in a * particular language. * * @param int|object $default_post The post 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 **/ function bbl_get_new_post_translation_url( $default_post, $lang ) { global $bbl_post_public; return $bbl_post_public->get_new_post_translation_url( $default_post, $lang ); } /** * Return the post type name for the equivalent post type for the * supplied original post type in the requested language. * * @param string $post_type The originating post type * @param string $lang_code The language code for the required language (optional, defaults to current) * @return string A post type name, e.g. "page" or "post" **/ function bbl_get_post_type_in_lang( $original_post_type, $lang_code = null ) { global $bbl_post_public; if ( is_null( $lang_code ) ) $lang_code = bbl_get_current_lang_code(); return $bbl_post_public->get_post_type_in_lang( $original_post_type, $lang_code ); } add_filter( 'bbl_get_content_post_type', 'bbl_get_post_type_in_lang' ); /** * Is the query for a single page or translation or a single page? * * If the $page parameter is specified, this function will additionally * check if the query is for one of the pages specified. * * @see is_page() * * @param mixed $page Page ID, title, slug, or array of such. * @return bool */ function bbl_is_page( $page = '' ) { $base_page = bbl_get_post_in_lang( get_the_ID(), bbl_get_default_lang_code() ); if ( ! $page ) return 'page' == $base_page->post_type; if ( is_int( $page ) ) return $page == $base_page->ID; if ( $page == $base_page->post_name ) return true; if ( $page == $base_page->post_title ) return true; if ( $page == (string) $base_page->ID ) return true; return false; } /** * Returns the post in a particular language * * @param int|object $post Either a WP Post object, or a post ID * @param string $lang_code The language code for the required language * @param boolean $fallback If true: if a post is not available, fallback to the default language content (defaults to true) * @return object|boolean The WP Post object, or if $fallback was false and no post then returns false **/ function bbl_get_post_in_lang( $post, $lang_code, $fallback = true ) { global $bbl_post_public; return $bbl_post_public->get_post_in_lang( $post, $lang_code, $fallback ); } /** * Returns the term in a particular language * * @param int|object $term Either a term object, or a term ID * @param string $taxonomy The term taxonomy * @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 term object, or if $fallback was false and no term then returns false **/ function bbl_get_term_in_lang( $term, $taxonomy, $lang_code, $fallback = true ) { global $bbl_taxonomies; return $bbl_taxonomies->get_term_in_lang( $term, $taxonomy, $lang_code, $fallback ); } /** * Returns a post_type slug translated into a particular language. * * @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 **/ function bbl_get_post_type_slug_in_lang( $slug, $lang_code = null ) { global $bbl_post_public; $lang = bbl_get_lang( $lang_code ); return $bbl_post_public->get_slug_in_lang( $slug, $lang ); } /** * Echoes the title of a post, in the requested language (if available). * * @param int|object $post Either a WP Post object, or a post ID * @param string $lang_code The code for the language the title is requested in * @param bool $fallback Whether to provide a fallback title in the default language if the requested language is unavailable (defaults to false) * @return void **/ function bbl_the_title_in_lang( $post = null, $lang_code = null, $fallback = false ) { echo bbl_get_the_title_in_lang( $post, $lang_code, $fallback ); } /** * Returns the title of a post, in the requested language (if available). * * @param int|object $post Either a WP Post object, or a post ID * @param string $lang_code The code for the language the title is requested in * @param bool $fallback Whether to provide a fallback title in the default language if the requested language is unavailable (defaults to false) * @return void **/ function bbl_get_the_title_in_lang( $post = null, $lang_code = null, $fallback = false ) { $post = get_post( $post ); if ( is_null( $lang_code ) ) $lang_code = bbl_get_current_lang_code(); // Hopefully we find the post in the right language if ( $lang_post = bbl_get_post_in_lang( $post, $lang_code, $fallback ) ) return apply_filters( 'bbl_the_title_in_lang', get_the_title( $lang_post->ID ), $lang_code ); // We have failed… return ''; } /** * Echoes the permalink of a post, in the requested language (if available). * * @param int|object $post Either a WP Post object, or a post ID * @param string $lang_code The code for the language the title is requested in * @param bool $fallback Whether to provide a fallback title in the default language if the requested language is unavailable (defaults to false) * @return void **/ function bbl_the_permalink_in_lang( $post = null, $lang_code = null, $fallback = false ) { echo bbl_get_the_permalink_in_lang( $post, $lang_code, $fallback ); } /** * Returns the permalink of a post, in the requested language (if available). * * @param int|object $post Either a WP Post object, or a post ID * @param string $lang_code The code for the language the title is requested in * @param bool $fallback Whether to provide a fallback title in the default language if the requested language is unavailable (defaults to false) * @return void **/ function bbl_get_the_permalink_in_lang( $post = null, $lang_code = null, $fallback = false ) { $post = get_post( $post ); if ( is_null( $lang_code ) ) $lang_code = bbl_get_current_lang_code(); // Hopefully we find the post in the right language if ( $lang_post = bbl_get_post_in_lang( $post, $lang_code, $fallback ) ) return apply_filters( 'bbl_permalink_in_lang', get_permalink( $lang_post->ID ), $lang_code ); // We have failed… return ''; } /** * Returns the link to a post type in a particular language. * * @param string $post_type A post type for which you want a translated archive link * @param string $lang_code The code for the language the link is requested in * @return void **/ function bbl_get_post_type_archive_link_in_lang( $post_type, $lang_code = null ) { if ( is_null( $lang_code ) ) $lang_code = bbl_get_current_lang_code(); bbl_switch_to_lang( $lang_code ); $lang_post_type = bbl_get_post_type_in_lang( $post_type, $lang_code ); $link = get_post_type_archive_link( $lang_post_type ); bbl_restore_lang(); return apply_filters( 'bbl_post_type_archive_link_in_lang', $link ); } /** * Return the base post type (in the default language) for a * provided post type. * * @param string $post_type The name of a post type * @return string The name of the base post type **/ function bbl_get_base_post_type( $post_type ) { global $bbl_post_public; return $bbl_post_public->get_base_post_type( $post_type ); } /** * Return all the base post types (in the default language). * * @return array An array of post_type objects **/ function bbl_get_base_post_types() { global $bbl_post_public; return $bbl_post_public->get_base_post_types(); } /** * Returns an array of all the shadow post types associated with * this post type. * * @param string $base_post_type The post type to look up shadow post types for * @return array The names of all the related shadow post types **/ function bbl_get_shadow_post_types( $base_post_type ) { global $bbl_post_public; return $bbl_post_public->get_shadow_post_types( $base_post_type ); } /** * Return the active language objects for the current site. A * language object looks like: * 'ar' => * object(stdClass) * public 'name' => string 'Arabic' * public 'code' => string 'ar' * public 'url_prefix' => string 'ar' * public 'text_direction' => string 'rtl' * public 'display_name' => string 'Arabic' * * @uses Babble_Languages::get_active_langs to do the actual work * * @return array An array of Babble language objects **/ function bbl_get_active_langs() { global $bbl_languages; return $bbl_languages->get_active_langs(); } /** * Returns the requested language object. * * @param string $code A language code, e.g. "fr_BE" * @return object|boolean A Babble language object **/ function bbl_get_lang( $lang_code ) { global $bbl_languages; return $bbl_languages->get_lang( $lang_code ); } /** * Returns the current language object, respecting any * language switches; i.e. if your request was for * Arabic, but the language is currently switched to * French, this will return French. * * @return object A Babble language object **/ function bbl_get_current_lang() { global $bbl_languages; return $bbl_languages->get_current_lang(); } /** * Returns the default language code for this site. * * @return string A language code, e.g. "he_IL" **/ function bbl_get_default_lang_code() { global $bbl_languages; return $bbl_languages->get_default_lang_code(); } /** * Returns the default language for this site. * * @return object A language object **/ function bbl_get_default_lang() { global $bbl_languages; return $bbl_languages->get_default_lang(); } /** * Checks whether either the provided language code, * if provided, or the current language code are * the default language. * * i.e. is this language the default language * * n.b. the current language could have been switched * using bbl_switch_to_lang * * @param string $lang_code The language code to check (optional) * @return bool True if the default language **/ function bbl_is_default_lang( $lang_code = null ) { if ( is_null( $lang_code ) ) $lang = bbl_get_current_lang(); else if ( is_string( $lang_code ) ) // In case someone passes a lang object $lang = bbl_get_lang( $lang_code ); return ( bbl_get_default_lang_code() == $lang->code ); } /** * Returns the default language code for this site. * * @return string The language URL prefix set by the admin, e.g. "de" **/ function bbl_get_default_lang_url_prefix() { global $bbl_languages; $code = $bbl_languages->get_default_lang_code(); return $bbl_languages->get_url_prefix_from_code( $code ); } /** * Returns the language code for the provided URL prefix. * * @param string $url_prefix The URL prefix to find the language code for * @return string The language code, or false **/ function bbl_get_lang_from_prefix( $url_prefix ) { global $bbl_languages; return $bbl_languages->get_code_from_url_prefix( $url_prefix ); } /** * Returns the language code for the provided URL prefix. * * @param string $lang_code The language code to look up * @return string The language URL prefix set by the admin, e.g. "de" **/ function bbl_get_prefix_from_lang_code( $lang_code ) { global $bbl_languages; return $bbl_languages->get_url_prefix_from_code( $lang_code ); } /** * Returns the switch links for the current content. * * @param string $id_prefix A prefix to the ID for each item * @return array An array of admin menu nodes **/ function bbl_get_switcher_links( $id_prefix = '' ) { global $bbl_switcher_menu; return $bbl_switcher_menu->get_switcher_links( $id_prefix ); } /** * Start logging for Babble * * @return void **/ function bbl_start_logging() { global $bbl_log; $bbl_log->logging = true; } /** * Stop logging for Babble * * @return void **/ function bbl_stop_logging() { global $bbl_log; $bbl_log->logging = false; } /** * Log a message. * * @param string $msg Log this message * @return void **/ function bbl_log( $msg ) { global $bbl_log; if ( $bbl_log ) $bbl_log->log( $msg ); else error_log( "Full Babble logging unavailable: $msg" ); } /** * Whether Babble is logging right now. * * @return boolean True for yes, natch **/ function bbl_is_logging() { global $bbl_log; return $bbl_log->logging; } ?> ================================================ FILE: babble.php ================================================ setup( 'babble-switcher-menu', 'plugin' ); $this->add_action( 'admin_bar_menu', null, 100 ); } /** * Hooks the WP admin_bar_menu action * * @param object $wp_admin_bar The WP Admin Bar, passed by reference * @return void **/ public function admin_bar_menu( $wp_admin_bar ) { $links = bbl_get_switcher_links( 'bbl-admin-bar' ); $current_lang = bbl_get_current_lang(); // Remove the current language unset( $links[ $current_lang->code ] ); $parent_id = "bbl-admin-bar-{$current_lang->url_prefix}"; $wp_admin_bar->add_menu( array( 'children' => array(), 'href' => '#', 'id' => $parent_id, 'meta' => array( 'class' => "bbl_lang_{$current_lang->code} bbl_lang" ), 'title' => $current_lang->display_name, 'parent' => false, ) ); foreach ( $links as & $link ) { $link[ 'parent' ] = $parent_id; $wp_admin_bar->add_menu( $link ); } } } global $bbl_admin_bar; $bbl_admin_bar = new Babble_Admin_bar(); ================================================ FILE: class-babble-log.php ================================================ session = uniqid(); } /** * Hooks the WP admin_init action * * @return void **/ public function log( $msg ) { if ( $this->logging ) error_log( "[$this->session] BABBLE LOG: $msg" ); } } global $bbl_log; $bbl_log = new Babble_Log(); ?> ================================================ FILE: class-comment.php ================================================ setup( 'babble-comment', 'plugin' ); $this->add_filter( 'comments_template_args' ); $this->add_filter( 'preprocess_comment' ); $this->add_filter( 'get_comments_number', null, null, 2 ); } /** * Hooks the comments_template_args on Bbl_Comment_Query, * and hopefully soon on WP_Comment_Query (Trac #19623), * in order to ensure we get the comments from all the * translated posts in this translation group. * * @param array $args The args for WP_Comment_Query in comments_template * @return array The args for WP_Comment_Query in comments_template **/ public function comments_template_args( $args ) { if ( isset( $args[ 'post_id' ] ) && ! empty( $args[ 'post_id' ] ) ) { $posts = bbl_get_post_translations( $args[ 'post_id' ] ); if ( isset( $args[ 'post__in' ] ) && ! is_array( $args[ 'post__in' ] ) ) $args[ 'post__in' ] = array(); foreach ( $posts as & $post ) $args[ 'post__in' ][] = $post->ID; unset( $args[ 'post_id' ] ); } return $args; } /** * Hooks the WP preprocess_comment filter to ensure that when someone * replies to a comment which has been included in a merged comment * stream on a post in a different language, the reply is assigned * language post of the parent comment. * * @param array $comment_data The comment data * @return void **/ public function preprocess_comment( $comment_data ) { // If comment_post_ID exists in the data, the only acceptable // value is the same as the parent comment's comment_post_ID $parent_comment = get_comment( $comment_data[ 'comment_parent' ] ); if ( $parent_comment && $comment_data[ 'comment_post_ID' ] ) $comment_data[ 'comment_post_ID' ] = $parent_comment->comment_post_ID; return $comment_data; } /** * Hooks the WP get_comments_number filter to get the number of comments * across all posts in the translation group. * * @param int $count The number of comments on the single translation * @param int $post_id The post ID of the single translation * @return int The count of all comments on published posts in this translation group **/ public function get_comments_number( $count, $post_id ) { $translations = bbl_get_post_translations( $post_id ); $count = 0; foreach ( $translations as & $translation ) { $post_status = get_post_status_object( $translation->post_status ); // FIXME: I'm not entirely sure about using publicly_queryable here… what I want to avoid is draft, private, etc statii. if ( $post_status->publicly_queryable ) $count += $translation->comment_count; } return $count; } // PUBLIC METHODS // ============== // PRIVATE/PROTECTED METHODS // ========================= } global $bbl_comment; $bbl_comment = new Babble_Comment(); ?> ================================================ FILE: class-jobs.php ================================================ setup( 'babble-job', 'plugin' ); $this->add_action( 'add_meta_boxes' ); $this->add_action( 'add_meta_boxes_bbl_job', null, 999 ); $this->add_action( 'admin_init' ); $this->add_action( 'admin_menu' ); $this->add_action( 'babble_create_empty_translation', 'create_empty_translation' ); $this->add_action( 'bbl_translation_post_meta_boxes', null, 10, 3 ); $this->add_action( 'bbl_translation_submit_meta_boxes', null, 10, 2 ); $this->add_action( 'bbl_translation_terms_meta_boxes', null, 10, 2 ); $this->add_action( 'bbl_translation_meta_meta_boxes', null, 10, 2 ); $this->add_action( 'edit_form_after_title' ); $this->add_action( 'init', 'init_early', 0 ); $this->add_action( 'load-post.php', 'load_post_edit' ); $this->add_action( 'manage_bbl_job_posts_custom_column', 'action_column', null, 2 ); $this->add_action( 'pre_get_posts' ); $this->add_action( 'save_post', 'save_job', null, 2 ); $this->add_action( 'save_post', null, null, 2 ); $this->add_action( 'wp_before_admin_bar_render' ); $this->add_filter( 'admin_title', null, null, 2 ); $this->add_filter( 'bbl_translated_post_type', null, null, 2 ); $this->add_filter( 'bbl_translated_taxonomy', null, null, 2 ); $this->add_filter( 'get_edit_post_link', null, null, 3 ); $this->add_filter( 'manage_bbl_job_posts_columns', 'filter_columns' ); $this->add_filter( 'post_updated_messages' ); $this->add_filter( 'query_vars' ); $this->add_filter( 'user_has_cap', null, null, 3 ); $this->add_filter( 'wp_insert_post_empty_content', null, null, 2 ); $this->version = 1.1; } public function add_meta_boxes_bbl_job( WP_Post $post ) { # Unapologetically remove all meta boxes from the translation screen: global $wp_meta_boxes; unset( $wp_meta_boxes['bbl_job'] ); } public function wp_insert_post_empty_content( $maybe_empty, $postarr ) { // Allow translations to have empty content if ( bbl_get_base_post_type( $postarr['post_type'] ) != $postarr['post_type'] ) return false; return $maybe_empty; } public function bbl_translated_post_type( $translated, $post_type ) { if ( 'bbl_job' == $post_type ) return false; return $translated; } public function bbl_translated_taxonomy( $translated, $taxonomy ) { if ( 'bbl_job_language' == $taxonomy ) return false; return $translated; } /** * Add our post type updated messages. * * The messages are as follows: * * 1 => "Post updated. {View Post}" * 2 => "Custom field updated." * 3 => "Custom field deleted." * 4 => "Post updated." * 5 => "Post restored to revision from [date]." * 6 => "Post published. {View post}" * 7 => "Post saved." * 8 => "Post submitted. {Preview post}" * 9 => "Post scheduled for: [date]. {Preview post}" * 10 => "Post draft updated. {Preview post}" * * @param array $messages An associative array of post updated messages with post type as keys. * @return array Updated array of post updated messages. */ public function post_updated_messages( array $messages ) { $messages['bbl_job'] = array( 1 => __( 'Translation job updated.', 'babble' ), 4 => __( 'Translation job updated.', 'babble' ), 8 => __( 'Translation job submitted.', 'babble' ), 10 => __( 'Translation job draft updated.', 'babble' ), ); return $messages; } /** * Hooks the WP admin_init action to enqueue some stuff. * * @return void **/ public function admin_init() { wp_enqueue_style( 'bbl-jobs-admin', $this->url( 'css/jobs-admin.css' ), array(), filemtime( $this->dir( 'css/jobs-admin.css' ) ) ); } /** * Hooks the WP action load-post.php to detect people * trying to edit translated posts, and instead kick * redirect them to an existing translation job or * create a translation job and direct them to that. * * @TODO this should be in the post-public class * * @action load-post.php * * @return void **/ public function load_post_edit() { $post_id = isset( $_GET[ 'post' ] ) ? absint( $_GET[ 'post' ] ) : false; if ( ! $post_id ) $post_id = isset( $_POST[ 'post_ID' ] ) ? absint( $_POST[ 'post_ID' ] ) : false; $translated_post = get_post( $post_id ); if ( ! $translated_post ) return; if ( ! bbl_is_translated_post_type( $translated_post->post_type ) ) return; $canonical_post = bbl_get_default_lang_post( $translated_post ); $lang_code = bbl_get_post_lang_code( $translated_post ); if ( bbl_get_default_lang_code() == $lang_code ) return; // @TODO Check capabilities include editing a translation post // - If not, the button shouldn't be on the Admin Bar // - But we also need to not process at this point $existing_jobs = $this->get_incomplete_post_jobs( $canonical_post ); if ( isset( $existing_jobs[ $lang_code ] ) ) { $url = get_edit_post_link( $existing_jobs[ $lang_code ], 'url' ); wp_redirect( $url ); exit; } // Create a new translation job for the current language $lang_codes = array( $lang_code ); $jobs = $this->create_post_jobs( $canonical_post, $lang_codes ); // Redirect to the translation job $url = get_edit_post_link( $jobs[0], 'url' ); wp_redirect( $url ); exit; } /** * Hooks the WP admin_title filter to give some context to the * page titles. * * @filter admin_title * * @param string $admin_title The admin title (for the TITLE element) * @param string $title The title used in the H2 element above the edit form * @return string The admin title **/ public function admin_title( $admin_title, $title ) { $screen = get_current_screen(); if ( 'post' == $screen->base && 'bbl_job' == $screen->post_type ) { $pto = get_post_type_object( 'bbl_job' ); $job = get_post(); if ( 'add' == $screen->action ) { if ( isset( $_GET['lang'] ) ) { $lang = bbl_get_lang( $_GET['lang'] ); $admin_title = sprintf( $pto->labels->add_item_context, $lang->display_name ); } } else { $lang = $this->get_job_language( $job ); $admin_title = sprintf( $pto->labels->edit_item_context, $lang->display_name ); } $GLOBALS[ 'title' ] = $admin_title; } return $admin_title; } /** * Filters the public query vars and adds some of our own * * @filter query_vars * @param array $vars Public query vars * @return array Updated public query vars */ public function query_vars( array $vars ) { if ( is_admin() ) { $vars[] = 'bbl_job_post'; $vars[] = 'bbl_job_term'; $vars[] = 'bbl_job_meta'; } return $vars; } /** * Filter the user's capabilities so they can be added/removed on the fly. * * @TODO description of what this does * * @filter user_has_cap * @param array $user_caps User's capabilities * @param array $required_caps Actual required capabilities for the requested capability * @param array $args Arguments that accompany the requested capability check: * [0] => Requested capability from current_user_can() * [1] => Current user ID * [2] => Optional second parameter from current_user_can() * @return array User's capabilities */ public function user_has_cap( array $user_caps, array $required_caps, array $args ) { $user = new WP_User( $args[1] ); switch ( $args[0] ) { case 'edit_post': case 'edit_bbl_job': case 'delete_post': case 'delete_bbl_job': case 'publish_post': case 'publish_bbl_job': $job = get_post( $args[2] ); if ( ! $job or ( 'bbl_job' != $job->post_type ) ) { break; } $objects = $this->get_job_objects( $job ); $pto = get_post_type_object( $job->post_type ); $cap = str_replace( 'bbl_job', 'post', $args[0] ); if ( isset( $objects['post'] ) && $objects['post']->post_type != 'bbl_job' ) { # This directly maps the ability to edit/delete/publish the job with the ability to do the same to the job's post: $can = user_can( $user, $cap, $objects['post']->ID ); foreach ( $required_caps as $required ) { if ( ! isset( $user_caps[$required] ) ) { $user_caps[$required] = $can; } } } else { # else if isset object terms } break; case 'edit_bbl_jobs': # Special case for displaying the admin menu: # By default, Translators will have this cap: if ( isset( $user_caps[$args[0]] ) ) break; # Cycle through post types with show_ui true, give edit_bbl_jobs cap to the user if they can edit any of the post types foreach ( get_post_types( array( 'show_ui' => true ), 'objects' ) as $pto ) { // Don't check the capability we already checked. if ( $args[0] == $pto->cap->edit_posts ) { continue; } if ( user_can( $user, $pto->cap->edit_posts ) ) { $user_caps[$args[0]] = true; break; } } break; } return $user_caps; } /** * Hooks the WP pre_get_posts ref action in the WP_Query. Sets the meta query * that's necessary for filtering jobs by their objects. * * @param WP_Query $wp_query A WP_Query object, passed by reference * @return void (param passed by reference) **/ public function pre_get_posts( WP_Query & $query ) { if ( $job_post = $query->get( 'bbl_job_post' ) ) { $query->set( 'meta_key', 'bbl_job_post' ); $query->set( 'meta_value', $job_post ); } else if ( $job_term = $query->get( 'bbl_job_term' ) ) { $query->set( 'meta_key', 'bbl_job_term' ); $query->set( 'meta_value', $job_term ); } else if ( $job_meta = $query->get( 'bbl_job_meta' ) ) { $query->set( 'meta_key', 'bbl_job_meta' ); $query->set( 'meta_value', $job_meta ); } } /** * Hooks the WP filter get_edit_post_link * * @filter get_edit_post_link * @param string $url The edit post link URL * @param int $post_ID The ID of the post to edit * @param string $context The link context. * * @return string The edit post link URL * @author Simon Wheatley **/ public function get_edit_post_link( $url, $post_ID, $context ) { if ( $this->no_recursion ) { return $url; } if ( bbl_get_default_lang_code() == bbl_get_post_lang_code( $post_ID ) ) { return $url; } $completed_jobs = $this->get_completed_post_jobs( bbl_get_default_lang_post( $post_ID ) ); if ( ! isset( $completed_jobs[ bbl_get_current_lang_code() ] ) ) { return $url; } $job = $completed_jobs[ bbl_get_current_lang_code() ]; if ( ! current_user_can( 'publish_post', $job->ID ) ) { return $url; } $this->no_recursion = true; $url = get_edit_post_link( $completed_jobs[ bbl_get_current_lang_code() ]->ID ); $this->no_recursion = false; return $url; } public function edit_form_after_title() { $screen = get_current_screen(); if ( 'bbl_job' != $screen->post_type ) { return; } $job = get_post(); $items = $objects = $vars = array(); if ( ( 'add' == $screen->action ) and isset( $_GET['lang'] ) ) { $vars['lang_code'] = stripslashes( $_GET['lang'] ); if ( isset( $_GET['bbl_origin_post'] ) ) { $post = get_post( absint( $_GET['bbl_origin_post' ] ) ); $terms = $this->get_post_terms_to_translate( $post, $_GET['lang'] ); $meta = $this->get_post_meta_to_translate( $post, $_GET['lang'] ); $objects['post'] = $post; if ( !empty( $terms ) ) { $objects['terms'] = $terms; } if ( !empty( $meta ) ) { $objects['meta'] = $meta; } $vars['origin_post'] = $post->ID; } else if ( isset( $_GET['bbl_origin_term'] ) and isset( $_GET['bbl_origin_taxonomy'] ) ) { $term = get_term( $_GET['bbl_origin_term'], $_GET['bbl_origin_taxonomy'] ); $objects['terms'][$term->taxonomy][$term->term_id] = $term; $vars['origin_term'] = $term->term_id; $vars['origin_taxonomy'] = $term->taxonomy; } } else { $objects = $this->get_job_objects( $job ); } if ( isset( $objects['post'] ) ) { $post = $objects['post']; $post_translation = get_post_meta( $job->ID, "bbl_post_{$post->ID}", true ); if ( empty( $post_translation ) ) { $post_translation = get_default_post_to_edit( $post->post_type ); } $items['post'] = array( 'original' => $post, 'translation' => (object) $post_translation, ); } if ( isset( $objects['meta'] ) ) { foreach ( $objects['meta'] as $meta_key => $meta_field ) { $meta_translation = get_post_meta( $job->ID, "bbl_meta_{$meta_key}", true ); if ( empty( $meta_translation ) ) { $meta_translation = ''; } $items['meta'][$meta_key] = array( 'original' => $meta_field, 'translation' => $meta_translation, ); } } if ( isset( $objects['terms'] ) ) { foreach ( $objects['terms'] as $taxo => $terms ) { foreach ( $terms as $term ) { $term_translation = get_post_meta( $job->ID, "bbl_term_{$term->term_id}", true ); if ( empty( $term_translation ) ) { $term_translation = array( 'name' => '', 'slug' => '' ); } $items['terms'][$taxo][] = array( 'original' => $term, 'translation' => (object) $term_translation, ); } } } $statuses = array( 'in-progress' => get_post_status_object( 'in-progress' )->label, ); if ( ( 'pending' == $job->post_status ) or !current_user_can( 'publish_post', $job->ID ) ) { $statuses['pending'] = get_post_status_object( 'pending' )->label; } if ( current_user_can( 'publish_post', $job->ID ) ) { $statuses['complete'] = get_post_status_object( 'complete' )->label; } $statuses = apply_filters( 'bbl_job_statuses', $statuses, $job, $objects ); $vars['job'] = $job; $vars['items'] = $items; $vars['statuses'] = $statuses; $this->render_admin( 'translation-editor.php', $vars ); } public function admin_menu() { # Remove the 'Add New' submenu for Translations. remove_submenu_page( 'edit.php?post_type=bbl_job', 'post-new.php?post_type=bbl_job' ); } public function wp_before_admin_bar_render() { global $wp_admin_bar; # Remove the '+New -> Translation Job' admin bar menu. $wp_admin_bar->remove_node( 'new-bbl_job' ); } /** * undocumented function * * @param * @return void **/ public function add_meta_boxes( $post_type ) { if ( bbl_is_translated_post_type( $post_type ) ) { add_meta_box( 'bbl_translations', _x( 'Translations', 'Translations meta box title', 'babble' ), array( $this, 'metabox_post_translations' ), $post_type, 'side', 'high' ); } } public function bbl_translation_post_meta_boxes( $type, $original, $translation ) { if ( !empty( $original->post_excerpt ) or !empty( $translation->post_excerpt ) ) { add_meta_box( 'postexcerpt', __( 'Excerpt', 'babble' ), array( $this, 'metabox_translation_post_excerpt' ), $type, 'post' ); } } public function bbl_translation_terms_meta_boxes( $type, $items ) { foreach ( $items as $taxo => $terms ) { $tax = get_taxonomy( $taxo ); add_meta_box( "{$taxo}_terms", $tax->labels->name, array( $this, 'metabox_translation_terms' ), $type, $taxo ); } } public function bbl_translation_meta_meta_boxes( $type, $items ) { $i = 0; foreach ( $items as $meta_key => $meta_field ) { add_meta_box( "meta_{$i}", esc_html( $meta_field['original']->get_title() ), array( $this, 'metabox_translation_meta' ), $type, $meta_key ); $i++; } } public function bbl_translation_submit_meta_boxes( $type, $job ) { add_meta_box( 'bbl_job_submit', __( 'Save Translation' , 'babble'), array( $this, 'metabox_translation_submit' ), $type, 'submit' ); } public function metabox_translation_terms( array $items ) { $vars = $items; $this->render_admin( 'translation-editor-terms.php', $vars ); } public function metabox_translation_meta( array $items ) { $vars = $items; $this->render_admin( 'translation-editor-meta.php', $vars ); } public function metabox_translation_post_excerpt( array $items ) { $vars = $items; $this->render_admin( 'translation-editor-post-excerpt.php', $vars ); } public function metabox_translation_submit( array $items ) { $vars = $items; $this->render_admin( 'translation-editor-submit.php', $vars ); } public function save_job( $job_id, WP_Post $job ) { global $bbl_post_public, $bbl_taxonomies; if ( $this->no_recursion ) return; if ( 'bbl_job' != $job->post_type ) return; $edit_post_nonce = isset( $_POST[ '_bbl_translation_edit_post' ] ) ? $_POST[ '_bbl_translation_edit_post' ] : false; $edit_terms_nonce = isset( $_POST[ '_bbl_translation_edit_terms' ] ) ? $_POST[ '_bbl_translation_edit_terms' ] : false; $edit_meta_nonce = isset( $_POST[ '_bbl_translation_edit_meta' ] ) ? $_POST[ '_bbl_translation_edit_meta' ] : false; $origin_post_nonce = isset( $_POST[ '_bbl_translation_origin_post' ] ) ? $_POST[ '_bbl_translation_origin_post' ] : false; $origin_term_nonce = isset( $_POST[ '_bbl_translation_origin_term' ] ) ? $_POST[ '_bbl_translation_origin_term' ] : false; $lang_code_nonce = isset( $_POST[ '_bbl_translation_lang_code' ] ) ? $_POST[ '_bbl_translation_lang_code' ] : false; if ( $lang_code_nonce and wp_verify_nonce( $lang_code_nonce, "bbl_translation_lang_code_{$job->ID}" ) ) { wp_set_object_terms( $job->ID, stripslashes( $_POST['bbl_lang_code'] ), 'bbl_job_language', false ); } $language = get_the_terms( $job, 'bbl_job_language' ); if ( empty( $language ) ) return false; else $lang_code = reset( $language )->name; if ( $origin_post_nonce and wp_verify_nonce( $origin_post_nonce, "bbl_translation_origin_post_{$job->ID}") ) { if ( $origin_post = get_post( absint( $_POST['bbl_origin_post'] ) ) ) { add_post_meta( $job->ID, 'bbl_job_post', "{$origin_post->post_type}|{$origin_post->ID}", true ); foreach ( $this->get_post_terms_to_translate( $origin_post, $lang_code ) as $taxo => $terms ) { foreach ( $terms as $term_id => $term ) add_post_meta( $job->ID, 'bbl_job_term', "{$taxo}|{$term_id}", false ); } foreach ( $this->get_post_meta_to_translate( $origin_post, $lang_code ) as $key => $field ) { add_post_meta( $job->ID, 'bbl_job_meta', $key, false ); } } # @TODO else wp_die()? } # @TODO not implemented: if ( $origin_term_nonce and wp_verify_nonce( $origin_term_nonce, "bbl_translation_origin_term_{$job->ID}") ) { if ( $origin_term = get_term( absint( $_POST['bbl_origin_term'] ), $_POST['bbl_origin_taxonomy'] ) ) add_post_meta( $job->ID, 'bbl_job_term', "{$origin_term->taxonomy}|{$origin_term->term_id}", false ); # @TODO else wp_die()? } if ( $edit_post_nonce and wp_verify_nonce( $edit_post_nonce, "bbl_translation_edit_post_{$job->ID}" ) ) { $post_data = stripslashes_deep( $_POST['bbl_translation']['post'] ); if ( $post_data['post_name'] ) $post_data['post_name'] = sanitize_title( $post_data['post_name'] ); $post_info = get_post_meta( $job->ID, 'bbl_job_post', true ); list( $post_type, $post_id ) = explode( '|', $post_info ); $post = get_post( $post_id ); update_post_meta( $job->ID, "bbl_post_{$post_id}", $post_data ); if ( 'pending' == $job->post_status ) { # Nothing. } if ( 'complete' == $job->post_status ) { # The ability to complete a translation of a post directly # maps to the ability to publish the origin post. if ( current_user_can( 'publish_post', $job->ID ) ) { if ( !$trans = $bbl_post_public->get_post_in_lang( $post, $lang_code, false ) ) $trans = $bbl_post_public->initialise_translation( $post, $lang_code ); $post_data['ID'] = $trans->ID; $post_data['post_status'] = $post->post_status; $this->no_recursion = true; wp_update_post( $post_data, true ); $this->no_recursion = false; } else { # Just in case. Switch the job back to in-progress status. # It would be nice to be able to use the 'publish' status because then we get the built-in # publish_post cap checks, but we can't control the post status label on a per-post-type basis yet. $this->no_recursion = true; wp_update_post( array( 'ID' => $job->ID, 'post_status' => 'in-progress', ), true ); $this->no_recursion = false; } } if ( $edit_meta_nonce and wp_verify_nonce( $edit_meta_nonce, "bbl_translation_edit_meta_{$job->ID}" ) ) { $meta_data = stripslashes_deep( $_POST['bbl_translation']['meta'] ); foreach ( $meta_data as $meta_key => $meta_value ) { update_post_meta( $job->ID, "bbl_meta_{$meta_key}", $meta_value ); if ( 'complete' == $job->post_status ) { if ( current_user_can( 'publish_post', $job->ID ) ) { update_post_meta( $trans->ID, $meta_key, $meta_value ); } } } } } if ( $edit_terms_nonce and wp_verify_nonce( $edit_terms_nonce, "bbl_translation_edit_terms_{$job->ID}") ) { $terms_data = stripslashes_deep( $_POST['bbl_translation']['terms'] ); $terms = get_post_meta( $job->ID, 'bbl_job_term', false ); foreach ( $terms as $term_info ) { list( $taxo, $term_id ) = explode( '|', $term_info ); $term = get_term( $term_id, $taxo ); $terms_data[$term_id]['slug'] = sanitize_title( $terms_data[$term_id]['slug'] ); update_post_meta( $job->ID, "bbl_term_{$term_id}", $terms_data[$term_id] ); if ( 'complete' == $job->post_status ) { # @TODO if current user can edit term $trans = $bbl_taxonomies->get_term_in_lang( $term, $taxo, $lang_code, false ); if ( !$trans ) $trans = $bbl_taxonomies->initialise_translation( $term, $taxo, $lang_code ); $terms_data[$term->term_id]['term_id'] = $trans->term_id; $args = array( 'name' => $terms_data[$term->term_id]['name'], 'slug' => '', ); wp_update_term( absint( $trans->term_id ), $trans->taxonomy, $args ); } } } } public function save_post( $post_id, WP_Post $post ) { if ( $this->no_recursion ) return; if ( !bbl_is_translated_post_type( $post->post_type ) ) return; $nonce = isset( $_POST[ '_bbl_ready_for_translation' ] ) ? $_POST[ '_bbl_ready_for_translation' ] : false; if ( !$nonce ) return; if ( !wp_verify_nonce( $nonce, "bbl_ready_for_translation-{$post->ID}" ) ) return; if ( !isset( $_POST['babble_ready_for_translation'] ) ) return; # @TODO individual language selection when marking post as translation ready $langs = bbl_get_active_langs(); $lang_codes = wp_list_pluck( $langs, 'code' ); $this->create_post_jobs( $post->ID, $lang_codes ); } /** * Hooks the WP init action early to register the * job post type. * * @return void **/ public function init_early() { $labels = array( 'name' => _x( 'Translation Jobs', 'translation jobs general name', 'babble' ), 'singular_name' => _x( 'Translation Job', 'translation jobs singular name', 'babble' ), 'menu_name' => _x( 'Translations', 'translation jobs menu name', 'babble' ), 'add_new' => _x( 'Add New', 'translation job', 'babble' ), 'add_new_item' => _x( 'Create New Job', 'translation job', 'babble' ), 'add_item_context' => _x( 'Add Translation Job (%s)', 'translation job; e.g. "Add Translation Job (French)"', 'babble' ), 'edit_item' => _x( 'Edit Translation', 'translation job', 'babble' ), 'edit_item_context' => _x( 'Edit Translation (%s)', 'translation job; e.g. "Edit Translation (French)"', 'babble' ), 'new_item' => _x( 'New Job', 'translation job', 'babble' ), 'view_item' => _x( 'View Job', 'translation job', 'babble' ), 'search_items' => _x( 'Search Jobs', 'translation job', 'babble' ), 'not_found' => _x( 'No translation jobs found.', 'translation job', 'babble' ), 'not_found_in_trash' => _x( 'No translation jobs found in Trash.', 'translation job', 'babble' ), 'all_items' => _x( 'All Translation Jobs', 'translation job', 'babble' ), ); $args = array( 'public' => false, 'publicly_queryable' => false, 'show_ui' => true, 'show_in_menu' => true, 'menu_icon' => 'dashicons-clipboard', 'query_var' => false, 'labels' => $labels, 'can_export' => true, 'supports' => false, 'capability_type' => 'bbl_job', 'map_meta_cap' => true, ); register_post_type( 'bbl_job', $args ); register_post_status( 'new', array( 'label' => __( 'New', 'babble' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'label_count' => _n_noop( 'New (%s)', 'New (%s)', 'babble' ), 'protected' => true, ) ); register_post_status( 'in-progress', array( 'label' => __( 'In Progress', 'babble' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'label_count' => _n_noop( 'In Progress (%s)', 'In Progress (%s)', 'babble' ), 'protected' => true, ) ); register_post_status( 'complete', array( 'label' => __( 'Complete', 'babble' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'label_count' => _n_noop( 'Complete (%s)', 'Complete (%s)', 'babble' ), 'protected' => true, ) ); $args = array( 'public' => false, 'show_ui' => false, ); register_taxonomy( 'bbl_job_language', array( 'bbl_job' ), $args ); } // CALLBACKS // ========= public function filter_columns( $cols ) { $new_cols = array(); foreach ( $cols as $col_id => $col ) { if ( 'date' != $col_id ) { $new_cols[$col_id] = $col; } else { $new_cols['bbl_language'] = __( 'Language', 'babble' ); $new_cols['bbl_type'] = __( 'Items', 'babble' ); $new_cols['bbl_status'] = __( 'Status', 'babble' ); $new_cols['date'] = $col; } } return $new_cols; } public function action_column( $col, $post_id ) { $post = get_post( $post_id ); $status = get_post_status_object( $post->post_status ); switch ( $col ) { case 'bbl_language': echo $this->get_job_language( $post )->display_name; break; case 'bbl_type': echo implode( ', ', $this->get_job_type( $post ) ); break; case 'bbl_status': echo $status->label; break; } } public function metabox_post_translations( WP_Post $post, array $metabox ) { $trans = bbl_get_post_translations( $post ); $incomplete_jobs = $this->get_incomplete_post_jobs( $post ); $completed_jobs = $this->get_completed_post_jobs( $post ); $default = bbl_get_default_lang_code(); # The ability to create a translation of a post directly # maps to the ability to publish the canonical post. $capable = current_user_can( 'publish_post', $post->ID ); unset( $trans[$default] ); if ( !empty( $trans ) ) { if ( !empty( $completed_jobs ) and $capable ) { ?>
$job ) { $lang = bbl_get_lang( $lang_code ); ?>%s', $lang->display_name, get_edit_post_link( $job->ID ), __( 'View', 'babble' ) ); ?>
get_job_language( $job ); $status = get_post_status_object( $job->post_status ); ?>display_name, $status->label ); ?> 'bbl_job', 'bbl_job_post' => "{$post->post_type}|{$post->ID}", ); ?>
ID}", '_bbl_ready_for_translation' ); ?> get_object_jobs( $post->ID, 'post', $post->post_type, array( 'new', 'in-progress' ) ); } /** * Return the array of completed jobs for a Post, keyed * by lang code. * * @param WP_Post|int $post A WP Post object or a post ID * @return array An array of WP Translation Job Post objects */ public function get_completed_post_jobs( $post ) { $post = get_post( $post ); return $this->get_object_jobs( $post->ID, 'post', $post->post_type, array( 'complete' ) ); } /** * Return the array of jobs for a Term, keyed * by lang code. * * @param object $term A WP Term object or a term ID * @return array An array of WP Translation Job Post objects */ public function get_term_jobs( $term, $taxonomy ) { $term = get_term( $term, $taxonomy ); return $this->get_object_jobs( $term->term_id, 'term', $term->taxonomy ); } /** * Return the array of jobs for a term or post, keyed * by lang code. * * @param int The ID of the object (eg. post ID or term ID) * @param string $type Either 'term' or 'post' * @param string $name The post type name or the term's taxonomy name * @return array An array of translation job WP_Post objects */ public function get_object_jobs( $id, $type, $name, $statuses = array( 'new', 'in-progress', 'complete' ) ) { $jobs = get_posts( array( 'bbl_translate' => false, 'post_type' => 'bbl_job', 'post_status' => $statuses, 'meta_key' => "bbl_job_{$type}", 'meta_value' => "{$name}|{$id}", 'posts_per_page' => -1, ) ); if ( empty( $jobs ) ) return array(); $return = array(); foreach ( $jobs as $job ) { if ( $lang = $this->get_job_language( $job ) ) $return[$lang->code] = $job; } return $return; } public function get_job_language( $job ) { $job = get_post( $job ); $languages = get_the_terms( $job, 'bbl_job_language' ); if ( empty( $languages ) ) return false; return bbl_get_lang( reset( $languages )->name ); } public function get_job_type( $job ) { $job = get_post( $job ); $post = get_post_meta( $job->ID, 'bbl_job_post', true ); $terms = get_post_meta( $job->ID, 'bbl_job_term', false ); $return = array(); if ( !empty( $post ) ) { list( $post_type, $post_id ) = explode( '|', $post ); $return[] = get_post_type_object( $post_type )->labels->singular_name; } if ( !empty( $terms ) ) { foreach ( $terms as $term ) { list( $taxonomy, $term_id ) = explode( '|', $term ); $return[] = get_taxonomy( $taxonomy )->labels->name; } } return array_unique( $return ); } public function get_job_objects( $job ) { $job = get_post( $job ); $post = get_post_meta( $job->ID, 'bbl_job_post', true ); $terms = get_post_meta( $job->ID, 'bbl_job_term', false ); $meta = get_post_meta( $job->ID, 'bbl_job_meta', false ); $lang = $this->get_job_language( $job ); $return = array(); if ( !empty( $post ) ) { list( $post_type, $post_id ) = explode( '|', $post ); # @TODO in theory a translation job could actually include more than one post. # we should implement this earlier rather than later to save potential headaches down the road. $return['post'] = get_post( $post_id ); } if ( !empty( $terms ) ) { foreach ( $terms as $term ) { list( $taxonomy, $term_id ) = explode( '|', $term ); $return['terms'][$taxonomy][] = get_term( $term_id, $taxonomy ); } } if ( !empty( $meta ) ) { $post = get_post_meta( $job->ID, 'bbl_job_post', true ); list( $post_type, $post_id ) = explode( '|', $post ); $post = get_post( $post_id ); foreach ( $this->get_post_meta_to_translate( $post, $lang->code ) as $meta_key => $meta_field ) { $return['meta'][$meta_key] = $meta_field; } } return $return; } /** * Create empty translations of a post for all languages. Called via WP-Cron on the `babble_create_empty_translation` hook. * * @param array $args Args array containing a `post_id` element. */ public function create_empty_translation( array $args ) { global $bbl_post_public; if ( !$post = get_post( $args['post_id'] ) ) { return; } foreach ( bbl_get_active_langs() as $lang ) { if ( !$trans = $bbl_post_public->get_post_in_lang( $post, $lang->code, false ) ) { $trans = $bbl_post_public->initialise_translation( $post, $lang->code ); } $post_data = array( 'ID' => $trans->ID, 'post_status' => $post->post_status, 'post_name' => $post->post_name, ); $this->no_recursion = true; wp_update_post( $post_data, true ); $this->no_recursion = false; } } /** * Create some translation jobs. * * @param int $post_id The ID of the post to create translation jobs for * @param array $lang_codes The language codes to create translation jobs of this post for * @return array An array of Translation Job post IDs **/ public function create_post_jobs( $post_id, array $lang_codes ) { $post = get_post( $post_id ); // @TODO Validate that the $post is in the default language, otherwise fail $jobs = array(); foreach ( $lang_codes as $lang_code ) { if ( bbl_get_default_lang_code() == $lang_code ) continue; if ( apply_filters( 'bbl_create_empty_translation', false, $post ) ) { wp_schedule_single_event( time(), 'babble_create_empty_translation', array( array( 'post_id' => $post->ID, ) ) ); } $this->no_recursion = true; $job = wp_insert_post( array( 'post_type' => 'bbl_job', 'post_status' => 'new', 'post_author' => get_current_user_id(), 'post_title' => get_the_title( $post ), ) ); $this->no_recursion = false; // @TODO If a translation already exists, populate the translation job with the translation $jobs[] = $job; add_post_meta( $job, 'bbl_job_post', "{$post->post_type}|{$post->ID}", true ); wp_set_object_terms( $job, $lang_code, 'bbl_job_language' ); foreach ( $this->get_post_terms_to_translate( $post, $lang_code ) as $taxo => $terms ) { foreach ( $terms as $term_id => $term ) add_post_meta( $job, 'bbl_job_term', "{$taxo}|{$term_id}", false ); } foreach ( $this->get_post_meta_to_translate( $post, $lang_code ) as $key => $field ) { add_post_meta( $job, 'bbl_job_meta', $key, false ); } } return $jobs; } public function get_post_terms_to_translate( $post_id, $lang_code ) { $post = get_post( $post_id ); $taxos = get_object_taxonomies( $post->post_type ); $trans_terms = array(); foreach ( $taxos as $key => $taxo ) { if ( !bbl_is_translated_taxonomy( $taxo ) ) continue; $terms = get_the_terms( $post, $taxo ); if ( empty( $terms ) ) continue; foreach ( $terms as $term ) { $trans = bbl_get_term_translations( $term->term_id, $term->taxonomy ); if ( !isset( $trans[$lang_code] ) ) $trans_terms[$taxo][$term->term_id] = $term; } } return $trans_terms; } /** * Return an array of a post's meta fields which are to be translated. The array keys are the post meta keys and the * array values are the meta value for that key. * * @param WP_Post $post_id A post object. * @param string $lang_code The language code for the translation job for this post. * @return array An array of post meta keys which should be translated for this post. */ public function get_post_meta_to_translate( WP_Post $post, $lang_code ) { $meta = get_post_meta( $post->ID ); if ( empty( $meta ) ) { return array(); } $fields = $this->get_translated_meta_fields( $post ); return array_intersect_key( $fields, $meta ); } /** * Return an array of meta field keys which should be translated. Array contains `Babble_Meta_Field` objects with * the meta keys as the array keys. * * @param WP_Post A post object. * @return Babble_Meta_Field[] An array of Babble meta field handlers. */ public function get_translated_meta_fields( WP_Post $post ) { $fields = (array) apply_filters( 'bbl_translated_meta_fields', array(), $post ); foreach ( $fields as $key => $field ) { if ( ! ( $field instanceof Babble_Meta_Field ) ) { unset( $fields[ $key ] ); } } return $fields; } } global $bbl_jobs; $bbl_jobs = new Babble_Jobs(); ================================================ FILE: class-languages.php ================================================ setup( 'babble-languages', 'plugin' ); $this->add_action( 'admin_menu', 'admin_menu' ); $this->add_action( 'admin_notices', 'admin_notices' ); $this->add_action( 'load-settings_page_babble_languages', 'load_options' ); $this->initiate(); } /** * (Re)initiates the properties of this object. * * @return void **/ public function initiate() { if ( ! ( $this->available_langs = $this->get_option( 'available_langs', false ) ) ) { $this->parse_available_languages(); } $this->active_langs = $this->get_option( 'active_langs', array() ); $this->langs = $this->get_option( 'langs', array() ); $this->lang_prefs = $this->get_option( 'lang_prefs', array() ); $this->langs = $this->merge_lang_sets( $this->langs, $this->lang_prefs ); $this->default_lang = $this->get_option( 'default_lang', 'en_US' ); $this->public_langs = $this->get_option( 'public_langs', array( $this->default_lang ) ); // @FIXME: Add something in so the user gets setup with the single language they are currently using if ( ! $this->get_option( 'active_langs', false ) || ! $this->get_option( 'default_lang', false ) ) $this->set_defaults(); } // WP HOOKS // ======== /** * Hooks the WP admin_notices action to warn the admin * if the available languages need to be set up. * * @return void **/ public function admin_notices() { if ( get_current_screen()->id == 'settings_page_babble_languages' ) return; if ( ! $this->get_option( 'active_langs', false ) || ! $this->get_option( 'default_lang', false ) ) { printf( '%s
%s
$msg
"; } } /** * Returns a section of user display code, returning the rendered markup. * * @param string $ug_name Name of the admin file (without extension) * @param string $array Array of variable name=>value that is available to the display code (optional) * @return void * @author © John Godley **/ protected function capture( $template_file, $vars = null ) { ob_start(); $this->render( $template_file, $vars ); $output = ob_get_contents(); ob_end_clean(); return $output; } /** * Returns a section of user display code, returning the rendered markup. * * @param string $ug_name Name of the admin file (without extension) * @param string $array Array of variable name=>value that is available to the display code (optional) * @return void * @author © John Godley **/ protected function capture_admin( $template_file, $vars = null ) { ob_start(); $this->render_admin( $template_file, $vars ); $output = ob_get_contents(); ob_end_clean(); return $output; } /** * Hooks the WP admin_notices action to render any notices * that have been set with the set_admin_notice method. * * @return void * @author Simon Wheatley **/ public function _admin_notices() { $notices = $this->get_option( 'admin_notices' ); $errors = $this->get_option( 'admin_errors' ); if ( $errors ) { foreach ( $errors as $error ) { $this->render_admin_error( $error ); $this->delete_option( 'admin_errors' ); } } if ( $notices ) { foreach ( $notices as $notice ) { $this->render_admin_notice( $notice ); $this->delete_option( 'admin_notices' ); } } } /** * Echoes some HTML for an admin notice. * * @param string $notice The notice * @return void * @author Simon Wheatley **/ protected function render_admin_notice( $notice ) { echo "$notice
$error
$msg
"; } /** * Register a WordPress meta box * * @param string $id ID for the box, also used as a function name if none is given * @param string $title Title for the box * @param int $page The type of edit page on which to show the box (post, page, link). * @param string $function Function name (optional) * @param string $context e.g. 'advanced' or 'core' (optional) * @param int $priority Priority, rough effect on the ordering (optional) * @param mixed $args Some arguments to pass to the callback function as part of a larger object (optional) * @return void * @author © John Godley **/ function add_meta_box( $id, $title, $function = '', $page, $context = 'advanced', $priority = 'default', $args = null ) { require_once( ABSPATH . 'wp-admin/includes/template.php' ); add_meta_box( $id, $title, array( $this, $function == '' ? $id : $function ), $page, $context, $priority, $args ); } /** * Add hook for shortcode tag. * * There can only be one hook for each shortcode. Which means that if another * plugin has a similar shortcode, it will override yours or yours will override * theirs depending on which order the plugins are included and/or ran. * * @param string $tag Shortcode tag to be searched in post content. * @param callable $func Hook to run when shortcode is found. */ protected function add_shortcode( $tag, $function = null ) { add_shortcode( $tag, array( $this, $function == '' ? $tag : $function ) ); } /** * Returns the filesystem path for a file/dir within this plugin. * * @param $path string The path within this plugin, e.g. '/js/clever-fx.js' * @return string Filesystem path * @author Simon Wheatley **/ protected function dir( $path ) { return trailingslashit( $this->dir ) . trim( $path, '/' ); } /** * Returns the URL for for a file/dir within this plugin. * * @param $path string The path within this plugin, e.g. '/js/clever-fx.js' * @return string URL * @author Simon Wheatley **/ protected function url( $path ) { return esc_url( trailingslashit( $this->url ) . trim( $path, '/' ) ); } /** * Gets the value of an option named as per this plugin. * * @return mixed Whatever * @author Simon Wheatley **/ protected function get_all_options() { return get_option( $this->name ); } /** * Sets the value of an option named as per this plugin. * * @return mixed Whatever * @author Simon Wheatley **/ protected function update_all_options( $value ) { return update_option( $this->name, $value ); } /** * Gets the value from an array index on an option named as per this plugin. * * @param string $key A string * @return mixed Whatever * @author Simon Wheatley **/ public function get_option( $key, $value = null ) { $option = get_option( $this->name ); if ( ! is_array( $option ) || ! isset( $option[ $key ] ) ) return $value; return $option[ $key ]; } /** * Sets the value on an array index on an option named as per this plugin. * * @param string $key A string * @param mixed $value Whatever * @return bool False if option was not updated and true if option was updated. * @author Simon Wheatley **/ protected function update_option( $key, $value ) { $option = get_option( $this->name ); $option[ $key ] = $value; return update_option( $this->name, $option ); } /** * Deletes the array index on an option named as per this plugin. * * @param string $key A string * @return bool False if option was not updated and true if option was updated. * @author Simon Wheatley **/ protected function delete_option( $key ) { $option = get_option( $this->name ); if ( isset( $option[ $key ] ) ) unset( $option[ $key ] ); return update_option( $this->name, $option ); } /** * Echoes out some JSON indicating that stuff has gone wrong. * * @param string $msg The error message * @return void * @author Simon Wheatley **/ protected function ajax_die( $msg ) { $data = array( 'msg' => $msg, 'success' => false ); echo json_encode( $data ); // N.B. No 500 header exit; } /** * Truncates a string in a human friendly way. * * @param string $str The string to truncate * @param int $num_words The number of words to truncate to * @return string The truncated string * @author Simon Wheatley **/ protected function truncate( $str, $num_words ) { $str = strip_tags( $str ); $words = explode(' ', $str ); if ( count( $words ) > $num_words) { $k = $num_words; $use_dotdotdot = 1; } else { $k = count( $words ); $use_dotdotdot = 0; } $words = array_slice( $words, 0, $k ); $excerpt = trim( join( ' ', $words ) ); $excerpt .= ($use_dotdotdot) ? '…' : ''; return $excerpt; } } // END Babble_Plugin class ?> ================================================ FILE: class-post-public.php ================================================ setup( 'babble-post-public', 'plugin' ); $this->add_action( 'added_post_meta', null, null, 4 ); $this->add_action( 'admin_init' ); $this->add_action( 'clean_post_cache' ); $this->add_action( 'body_class', null, null, 2 ); $this->add_action( 'before_delete_post', 'clean_post_cache' ); $this->add_action( 'deleted_post', 'clean_post_cache' ); $this->add_action( 'deleted_post_meta', null, null, 4 ); $this->add_action( 'load-post-new.php', 'load_post_new' ); $this->add_action( 'manage_pages_custom_column', 'manage_posts_custom_column', null, 2 ); $this->add_action( 'manage_posts_custom_column', 'manage_posts_custom_column', null, 2 ); $this->add_action( 'parse_request' ); $this->add_action( 'post_updated' ); $this->add_action( 'pre_get_posts', null, 11 ); $this->add_action( 'registered_post_type', null, null, 2 ); $this->add_action( 'transition_post_status', null, null, 3 ); $this->add_action( 'updated_post_meta', null, null, 4 ); $this->add_action( 'wp_before_admin_bar_render' ); $this->add_filter( 'add_menu_classes' ); $this->add_filter( 'add_post_metadata', null, null, 5 ); $this->add_filter( 'bbl_sync_meta_key', 'sync_meta_key', null, 2 ); $this->add_filter( 'manage_posts_columns', 'manage_posts_columns', null, 2 ); $this->add_filter( 'page_link', null, null, 2 ); $this->add_filter( 'post_link', 'post_type_link', null, 3 ); $this->add_filter( 'post_type_archive_link', null, null, 2 ); $this->add_filter( 'post_type_link', null, null, 3 ); $this->add_filter( 'get_sample_permalink', null, null, 5 ); $this->add_filter( 'single_template' ); $this->add_filter( 'the_posts', null, null, 2 ); $this->add_filter( 'bbl_translated_taxonomy', null, null, 2 ); $this->add_filter( 'admin_body_class' ); $this->initiate(); } /** * Initiates * * @return void **/ public function initiate() { $this->lang_map = array(); $this->post_types = array(); $this->slugs_and_vars = array(); $this->no_meta_recursion = false; $this->deleting_post_ids = array(); $this->version = 9; // Ensure we catch any existing language shadow post_types already registered $core_post_types = array( 'post', 'page', 'attachment' ); if ( is_array( $this->post_types ) ) $post_types = array_merge( $core_post_types, array_keys( $this->post_types ) ); else $post_types = $core_post_types; register_taxonomy( 'post_translation', $post_types, array( 'rewrite' => false, 'public' => false, 'show_ui' => false, 'show_in_nav_menus' => false, ) ); } /** * Hooks the WP admin_init action to * * @return void **/ public function admin_init() { $this->maybe_upgrade(); $post_type = false; if ( isset( $_GET[ 'post_type' ] ) ) { $post_type = $_GET[ 'post_type' ]; } else if ( isset( $_GET[ 'post' ] ) ) { $post = (int) $_GET[ 'post' ]; $post = get_post( $post ); $post_type = $post->post_type; } $menu_id = false; if ( isset( $this->post_types[ $post_type ] ) ) $menu_id = '#menu-posts-' . $this->post_types[ $post_type ]; $data = array( 'menu_id' => $menu_id, 'is_default_lang' => (bool) ( bbl_get_current_lang_code() == bbl_get_default_lang_code() ), 'is_bbl_post_type' => (bool) ( 0 === strpos( $post_type, 'bbl_' ) ), ); wp_enqueue_script( 'post-public-admin', $this->url( 'js/post-public-admin.js' ), array( 'jquery' ), filemtime( $this->dir( 'js/post-public-admin.js' ) ) ); wp_localize_script( 'post-public-admin', 'bbl_post_public', $data ); } /** * Initialise a translation for the given post. * * @param WP_Post|int $origin_post The origin post object or post ID * @param string $lang_code The language code for the new translation * @return WP_Post The translation post */ public function initialise_translation( $origin_post, $lang_code ) { $origin_post = get_post( $origin_post ); $new_post_type = $this->get_post_type_in_lang( $origin_post->post_type, $lang_code ); $transid = $this->get_transid( $origin_post->ID ); // Insert translation: $this->no_recursion = true; $new_post_id = wp_insert_post( array( 'post_type' => $new_post_type, 'post_status' => 'draft', ), true ); $this->no_recursion = false; $new_post = get_post( $new_post_id ); // Assign transid to translation: $this->set_transid( $new_post, $transid ); // Copy all the metadata across $this->sync_post_meta( $new_post->ID ); // Copy the various core post properties across $this->sync_properties( $origin_post->ID, $new_post->ID ); do_action( 'bbl_created_new_shadow_post', $new_post->ID, $origin_post->ID ); return $new_post; } /** * Hooks the WP load-post-new.php action to stop translators * creating new posts in languages other than the default. * * @return void **/ public function load_post_new() { $screen = get_current_screen(); if ( 'post' != $screen->base || 'add' != $screen->action ) return; if ( bbl_get_current_lang_code() == bbl_get_default_lang_code() ) return; if ( !bbl_is_translated_post_type( $screen->post_type ) ) return; wp_die( __( 'You can only create content in your site\'s default language. Please consult your editorial team.', 'babble' ), '', array( 'back_link' => true ) ); } /** * Hooks the WP wp_before_admin_bar_render action * to prune out unneeded post type add controls from * the add menu. * * @return void **/ public function wp_before_admin_bar_render() { global $wp_admin_bar; if ( ! bbl_is_default_lang() ) $wp_admin_bar->remove_node( 'new-content' ); } /** * Hooks the WP registered_post_type action. * * @param string $post_type The post type which has just been registered. * @param object $args The arguments with which the post type was registered * @return void **/ public function registered_post_type( $post_type, $args ) { // Don't bother with non-public post_types for now // @FIXME: This may need to change for menus? if ( false === $args->public ) return; // Don't shadow shadow post types, it's going to get silly if ( in_array( $post_type, $this->post_types ) ) return; if ( $this->no_recursion ) return; $this->no_recursion = 'registered_post_type'; $langs = bbl_get_active_langs(); // Lose the default language as any existing post types are in that language unset( $langs[ bbl_get_default_lang_url_prefix() ] ); // $args is an object at this point, but register_post_type needs an array $args = get_object_vars( $args ); // @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 ] ); } $features = $this->get_features_supported_by_post_type( $post_type ); $args[ 'supports' ] = array(); foreach ( $features as $feature => $true ) $args[ 'supports' ][] = $feature; // I am a little concerned that this argument may make things // brittle, e.g. the UI might stop showing up in the shadow // post type edit screens, p'raps. $args[ 'show_ui' ] = true; $slug = ( $args[ 'rewrite' ][ 'slug' ] ) ? $args[ 'rewrite' ][ 'slug' ] : $post_type; $archive_slug = false; if ( $archive_slug = $args[ 'has_archive' ] ) if ( ! is_string( $args[ 'has_archive' ] ) ) $archive_slug = $slug; $current_lang_code = bbl_get_current_lang_code(); foreach ( $langs as $lang ) { $new_args = $args; // @FIXME: We are in danger of a post_type name being longer than 20 chars // I would prefer to keep the post_type human readable, as human devs and sysadmins always // end up needing to read this kind of thing. // @FIXME: Should I be sanitising these values? $new_post_type = strtolower( "{$post_type}_{$lang->code}" ); if ( strlen( $new_post_type ) > 20 ) { trigger_error( sprintf( __( 'Warning: The translated name for the post type %s is longer than %d characters. This *will* cause problems.', 'babble' ), esc_html( $post_type ), 20 ) ); } if ( false !== $args[ 'rewrite' ] ) { if ( ! is_array( $new_args[ 'rewrite' ] ) ) $new_args[ 'rewrite' ] = array(); $new_args[ 'query_var' ] = $new_args[ 'rewrite' ][ 'slug' ] = $this->get_slug_in_lang( $slug, $lang, $args ); $new_args[ 'has_archive' ] = $this->get_slug_in_lang( $archive_slug, $lang ); } $this->slugs_and_vars[ $lang->code . '_' . $post_type ] = array( 'query_var' => $new_args[ 'query_var' ], 'has_archive' => $new_args[ 'has_archive' ], ); // Don't let the translated post types show up in the search if their // language is not the current language. if ( $lang->code != $current_lang_code ) { $new_args['exclude_from_search'] = true; $new_args['capabilities']['create_posts'] = 'do_not_allow'; } $result = register_post_type( $new_post_type, $new_args ); if ( is_wp_error( $result ) ) { error_log( "Error creating shadow post_type for $new_post_type: " . print_r( $result, true ) ); } else { $this->post_types[ $new_post_type ] = $post_type; $this->lang_map[ $new_post_type ] = $lang->code; // @TODO: Refactor the $this::lang_map array so we can use this new structure instead if ( ! isset( $this->lang_map2[ $lang->code ] ) || ! is_array( $this->lang_map2[ $lang->code ] ) ) $this->lang_map2[ $lang->code ] = array(); $this->lang_map2[ $lang->code ][ $post_type ] = $new_post_type; // This will not work until init has run at the early priority used // to register the post_translation taxonomy. However we catch all the // post_types registered before the hook runs, so we don't miss any // (take a look at where we register post_translation for more info). register_taxonomy_for_object_type( 'post_translation', $new_post_type ); } } // Exclude the registered post type from search if it's language isn't // the current language. if ( $current_lang_code != bbl_get_default_lang_code() ) { $post_type_obj = get_post_type_object( $post_type ); $post_type_obj->exclude_from_search = true; } do_action( 'bbl_registered_shadow_post_types', $post_type ); $this->no_recursion = false; } /** * Store whether a particular meta_key is unique or not. Pretty hacky. * * @param null $null Follows a pattern for actions/filters relating to meta, but meta ID not set yet so null * @param int $post_id The ID for the WordPress Post object this meta relates to * @param string $meta_key The key for this meta entry * @param mixed $meta_value The new value for this meta entry * @param bool $unique Whether the meta_key should be unique * @return null Always return null, or we are bypassing the meta save to DB **/ public function add_post_metadata( $null, $post_id, $meta_key, $meta_value, $unique ) { if ( $unique ) { $this->unique_meta_keys[ $meta_key ] = $meta_key; } return null; } /** * Hooks the WP added_post_meta action to sync metadata across to the * translations in shadow post types. * * @param int $meta_id The ID for this meta entry * @param int $post_id The ID for the WordPress Post object this meta relates to * @param string $meta_key The key for this meta entry * @param mixed $meta_value The new value for this meta entry * @return void **/ public function added_post_meta( $meta_id, $post_id, $meta_key, $meta_value ) { // Some metadata shouldn't be synced if ( ! apply_filters( 'bbl_sync_meta_key', true, $meta_key ) ) return; if ( $this->no_meta_recursion ) return; $this->no_meta_recursion = 'added_post_meta'; $unique = isset( $this->unique_meta_keys[ $meta_key ] ); $translations = $this->get_post_translations( $post_id ); foreach ( $translations as $lang_code => & $translation ) { if ( $this->get_post_lang_code( $post_id ) == $lang_code ) continue; add_post_meta( $translation->ID, $meta_key, $meta_value, $unique ); } $this->no_meta_recursion = false; } /** * Hooks the WP updated_post_meta action to sync metadata across to the * translations in shadow post types. * * @param int $meta_id The ID for this meta entry * @param int $post_id The ID for the WordPress Post object this meta relates to * @param string $meta_key The key for this meta entry * @param mixed $meta_value The new value for this meta entry * @return void **/ public function updated_post_meta( $meta_id, $post_id, $meta_key, $meta_value ) { // Some metadata shouldn't be synced if ( ! apply_filters( 'bbl_sync_meta_key', true, $meta_key ) ) return; if ( $this->no_meta_recursion ) return; $this->no_meta_recursion = 'updated_post_meta'; $translations = $this->get_post_translations( $post_id ); foreach ( $translations as $lang_code => & $translation ) { if ( $this->get_post_lang_code( $post_id ) == $lang_code ) continue; update_post_meta( $translation->ID, $meta_key, $meta_value ); } $this->no_meta_recursion = false; } /** * Hooks the WP deleted_post_meta action to sync metadata across to the * translations in shadow post types. * * @param int $meta_id The ID for this meta entry * @param int $post_id The ID for the WordPress Post object this meta relates to * @param string $meta_key The key for this meta entry * @param mixed $meta_value The new value for this meta entry * @return void **/ public function deleted_post_meta( $meta_id, $post_id, $meta_key, $meta_value ) { // When we are deleting posts, we don't want to sync // the metadata deletion across the other posts in // the same translation group if ( in_array( $post_id, $this->deleting_post_ids ) ) return; // Some metadata shouldn't be synced if ( ! apply_filters( 'bbl_sync_meta_key', true, $meta_key ) ) return; if ( $this->no_meta_recursion ) return; $this->no_meta_recursion = 'deleted_post_meta'; $translations = $this->get_post_translations( $post_id ); foreach ( $translations as $lang_code => & $translation ) { if ( $this->get_post_lang_code( $post_id ) == $lang_code ) continue; delete_post_meta( $translation->ID, $meta_key ); } $this->no_meta_recursion = false; } /** * Hooks the WP pre_get_posts ref action in the WP_Query, * for the main query it does nothing, for other queries * it switches the post types to our shadow post types. * * @param WP_Query $wp_query A WP_Query object, passed by reference * @return void (param passed by reference) **/ public function pre_get_posts( WP_Query & $query ) { if ( false === $query->get( 'bbl_translate' ) ) { return; } if ( $query->is_main_query() ) { return; } # @TODO we should scrap this and more intelligently filter the QVs rather than basing it on whether we're on a media tab if ( $this->is_media_upload_tab( 'gallery' ) ) { return; } if ( $this->is_media_manager() ) { return; } $query->query_vars = $this->translate_query_vars( $query->query_vars ); } /** * Hooks the WP parse_request action * * FIXME: Should I be extending and replacing the WP class? * * @param WP $wp WP object, passed by reference (so no need to return) * @return void **/ public function parse_request( WP & $wp ) { if ( isset( $wp->query_vars['bbl_translate'] ) and ( false === $wp->query_vars['bbl_translate'] ) ) { return; } if ( is_admin() ) { return; } $wp->query_vars = $this->translate_query_vars( $wp->query_vars, $wp->request ); } /** * Hooks the WP the_posts filter on WP_Query. * * Check the post_title, post_excerpt, post_content and substitute from * the default language where appropriate. * * @param array $posts The posts retrieved by WP_Query, passed by reference * @param WP_Query $wp_query The WP_Query, passed by reference * @return array The posts **/ public function the_posts( array $posts, WP_Query & $wp_query ) { if ( is_admin() ) return $posts; // Get fallback content in the default language $subs_index = array(); foreach ( $posts as & $post ) { if ( empty( $post->post_title ) || empty( $post->post_excerpt ) || empty( $post->post_content ) ) { if ( $default_post = bbl_get_default_lang_post( $post->ID ) ) $subs_index[ $post->ID ] = $default_post->ID; } if ( ! $this->get_transid( $post ) && bbl_get_default_lang_code() == bbl_get_post_lang_code( $post ) ) $this->set_transid( $post ); } if ( ! $subs_index ) return $posts; $subs_posts = get_posts( array( 'include' => array_values( $subs_index ), 'post_status' => 'publish' ) ); // @FIXME: Check the above get_posts call results are cached somewhere… I think they are // @FIXME: Alternative approach: hook on save_post to save the current value to the translation, BUT content could get out of date – in post_content_filtered foreach ( $posts as & $post ) { // @TODO why does this only override the title/excerpt/content? Why not override the post object entirely? // @FIXME: I'm assuming this get_post call is cached, which it seems to be if( isset( $subs_index[ $post->ID ] ) ) { $default_post = get_post( $subs_index[ $post->ID ] ); if ( empty( $post->post_title ) ) $post->post_title = $default_post->post_title; if ( empty( $post->post_excerpt ) ) $post->post_excerpt = $default_post->post_excerpt; if ( empty( $post->post_content ) ) $post->post_content = $default_post->post_content; } } return $posts; } /** * Hooks the WP body_class filter to add classes to the * body element. * * @param array $classes An array of class strings, poss with some indexes containing more than one space separated class * @param string|array $class One or more classes which have been added to the class list. * @return array An array of class strings, poss with some indexes containing more than one space separated class **/ public function body_class( array $classes, $class ) { // Shadow post_type archives also get the post_type class for // the default language if ( is_post_type_archive() && ! bbl_is_default_lang() ) $classes[] = 'post-type-archive-' . bbl_get_post_type_in_lang( get_query_var( 'post_type' ), bbl_get_default_lang_code() ); if ( is_single() ) $classes[] = 'single-' . bbl_get_base_post_type( get_post_type() ); return $classes; } /** * Hooks the WP post_type_link filter * * @param string $post_link The permalink * @param object $post The WP Post object being linked to * @return string The permalink **/ public function post_type_link( $post_link, $post, $leavename ) { global $wp_rewrite; // Regular ol' post types, and other types added by other plugins, etc if ( 'post' == $post->post_type || 'page' == $post->post_type || ! isset( $this->post_types[ $post->post_type ] ) ) return user_trailingslashit( $post_link ); // Deal with our shadow post types if ( ! ( $base_post_type = $this->get_base_post_type( $post->post_type ) ) ) return user_trailingslashit( $post_link ); // Deal with post_types shadowing the post post_type if ( 'post' == $base_post_type ) { // @FIXME: Probably move this into another function // @FIXME: Is there any way I can provide an appropriate permastruct so I can avoid having to copy all this code, with the associated maintenance headaches? // START copying from get_permalink function // N.B. The $permalink var is replaced with $post_link $rewritecode = array( '%year%', '%monthnum%', '%day%', '%hour%', '%minute%', '%second%', '%postname%', '%post_id%', '%category%', '%author%', '%pagename%', ); $post_link = get_option('permalink_structure'); // @FIXME: Should I somehow fake this, so plugin authors who hook it still get some consequence? // $post_link = apply_filters('pre_post_link', $post_link, $post, $leavename); if ( '' != $post_link && ! in_array( $post->post_status, array( 'draft', 'pending', 'auto-draft' ) ) ) { $unixtime = strtotime($post->post_date); $category = ''; if ( strpos($post_link, '%category%') !== false ) { $cats = get_the_category($post->ID); if ( $cats ) { usort($cats, '_usort_terms_by_ID'); // order by ID $category = $cats[0]->slug; if ( $parent = $cats[0]->parent ) $category = get_category_parents($parent, false, '/', true) . $category; } // show default category in permalinks, without // having to assign it explicitly if ( empty($category) ) { $default_category = get_category( get_option( 'default_category' ) ); $category = is_wp_error( $default_category ) ? '' : $default_category->slug; } } $author = ''; if ( strpos($post_link, '%author%') !== false ) { $authordata = get_userdata($post->post_author); $author = $authordata->user_nicename; } $date = explode(" ",date('Y m d H i s', $unixtime)); $rewritereplace = array( $date[0], $date[1], $date[2], $date[3], $date[4], $date[5], $post->post_name, $post->ID, $category, $author, $post->post_name, ); $lang = bbl_get_post_lang_code( $post ); bbl_switch_to_lang( $lang ); $post_link = home_url( str_replace( $rewritecode, $rewritereplace, $post_link ) ); bbl_restore_lang(); $post_link = user_trailingslashit($post_link, 'single'); // END copying from get_permalink function return $post_link; } else { // if they're not using the fancy permalink option the link won't work. Known bug. :) return $post_link; } } else if ( 'page' == $base_post_type ) { return get_page_link( $post->ID, $leavename ); } return user_trailingslashit( $post_link ); } /** * Hooks the get_sample_permalink filter to provide a correct sample permalink * in situations where the post_name has been hacked for a particular context. * * @filter get_sample_permalink (not yet in existence, see http://core.trac.wordpress.org/attachment/ticket/22338) * * @param array $permalink The array, like array( $permalink, $post_name ) * @param string $title A desired title (could be null) * @param string $name A desired post name (could be null) * @param int $id The Post ID * @param object $post A (hacked) Post object * @return array The array, like array( $permalink, $post_name ) */ public function get_sample_permalink( $permalink, $title, $name, $id, $post ) { $permalink[ 0 ] = $this->post_type_link( $permalink[ 0 ], $post, $leavename ); return $permalink; } /** * Hooks the WP page_link filter to ensure correct virtual language directory prefix, etc. * * @param string $link The permalink for the page * @param int $id The ID for the post represented by this permalink * @return string **/ public function page_link( $link, $post_id ) { if ( $this->no_recursion ) return $link; // Deal with the language front pages if ( 'page' == get_option('show_on_front') && $page_on_front = get_option( 'page_on_front' ) ) { $front_page_transid = $this->get_transid( $page_on_front ); $this_transid = $this->get_transid( $post_id ); if ( $front_page_transid == $this_transid ) { bbl_switch_to_lang( bbl_get_post_lang_code( $post_id ) ); $link = home_url(); bbl_restore_lang(); return $link; } } $this->no_recursion = 'page_link'; $lang = bbl_get_post_lang_code( $post_id ); bbl_switch_to_lang( $lang ); $link = get_page_link( $post_id ); bbl_restore_lang(); $this->no_recursion = false; return $link; } /** * Hooks the WP post_type_archive_link filter to return the correct * post type archive link for the current language. * * @param string $link The link to the post type archive (probably wrong for this language) * @param string $post_type The post_type we need an archive for (though we'll probably need to use a translated (shadow) post_type) * @return string A URL for the translated (shadow) post_type archive **/ public function post_type_archive_link( $link, $post_type ) { if ( $this->no_recursion ) return $link; $this->no_recursion = 'post_type_archive_link'; $lang_post_type = $this->get_post_type_in_lang( $post_type, bbl_get_current_lang_code() ); bbl_switch_to_lang( bbl_get_current_lang_code() ); $link = get_post_type_archive_link( $lang_post_type ); bbl_restore_lang(); $this->no_recursion = false; return $link; } /** * Hooks the WP clean_post_cache action to clear the Babble * post translation and transid caches. * * Occasionally called directly by within this class. * * @param int $post_id The ID of the post to clear the caches for * @return void **/ function clean_post_cache( $post_id ) { wp_cache_delete( $post_id, 'bbl_post_transids' ); // clean_post_cache gets called in some situations where // the post is already deleted, in which case do not // force the creation of a transid. if ( ! $transid = $this->get_transid( $post_id, false ) ) { return; } wp_cache_delete( $transid, 'bbl_post_translations' ); } /** * Hooks the WP post_updated action to ensure that the * required properties are copied to the other posts in * this translation group. * * @param int $post_id The ID of the post being updated * @return void **/ public function post_updated( $post_id ) { if ( $this->no_recursion ) return; $this->no_recursion = 'post_updated'; $transid = $this->get_transid( $post_id ); $this->clean_post_cache( $post_id ); $translations = $this->get_post_translations( $post_id ); foreach ( $translations as $lang_code => & $translation ) { if ( $translation->ID == $post_id ) continue; // Copy the various core post properties across $this->sync_properties( $post_id, $translation->ID ); } // Revert comment status, which often gets turned off by // auto drafts. $post_lang_code = bbl_get_post_lang_code( $post_id ); if ( bbl_get_default_lang_code() != $post_lang_code ) { $source_post = bbl_get_post_in_lang( $post_id, bbl_get_default_lang_code() ); $target_post = get_post( $post_id ); $post_data = array( 'ID' => $post_id, 'comment_status' => $source_post->comment_status, 'post_modified' => $target_post->post_modified, 'post_modified_gmt' => $target_post->post_modified_gmt, ); wp_update_post( $post_data ); } $this->no_recursion = false; } /** * Hooks the WP transition_post_status action which fires whenever * a post status changes through use of wp_transition_post_status. * * @param string $new_status The new status * @param string $old_status The old status * @param object $post The post object * @return void **/ public function transition_post_status( $new_status, $old_status, $post ) { if ( $new_status == $old_status ) return; if ( $this->no_recursion ) { return; } $this->no_recursion = 'transition_post_status'; if ( 'publish' == $new_status && $new_status != $old_status ) { // Ensure the date of publication of a translation gets // sync'd immediately with the original language post. if ( bbl_get_default_lang_code() != bbl_get_post_lang_code( $post->ID ) ) { $source_post = bbl_get_post_in_lang( $post->ID, bbl_get_default_lang_code() ); $postdata = array( 'ID' => $post->ID, 'post_date' =>$source_post->post_date, ); wp_update_post( $postdata ); } } $this->no_recursion = false; } /** * Hooks the WP add_menu_classes filter to fixup the side * admin menu. * * @param array $menu The WP admin menu * @return array The WP admin menu **/ public function add_menu_classes( $menu ) { global $submenu; $lang = bbl_get_current_lang_code(); $default = bbl_get_default_lang_code(); // Remove "new post" links from submenu(s) for non-default languages if ( $lang != $default ) { foreach ( $submenu as $parent => $items ) { foreach ( $items as $key => $item ) { if ( 'post-new.php' == substr( $item[ 2 ], 0, 12 ) ) { unset( $submenu[ $parent ][ $key ] ); } } } } // Remove links to shadow post types foreach ( $menu as $key => $item ) { $vars = array(); $url_info = parse_url( $item[ 2 ] ); if ( ! isset( $url_info[ 'query' ] ) ) continue; parse_str( $url_info[ 'query' ], $vars ); if ( ! isset( $vars[ 'post_type' ] ) || ! isset( $this->post_types[ $vars[ 'post_type' ] ] ) ) continue; unset( $menu[ $key ] ); } return $menu; } /** * Hooks the WP filter single_template to deal with the shadow post * types for pages and singular templates, ensuring they use the * right template. * * @param string $template Path to a template file * @return Path to a template file **/ public function single_template( $template ) { if( bbl_is_default_lang() ) return $template; // Deal with the language front pages and custom page templates $post = get_post( get_the_ID() ); if ( 'page' == get_option('show_on_front') ) { $front_page_transid = $this->get_transid( get_option( 'page_on_front' ) ); $this_transid = $this->get_transid( get_the_ID() ); // Check if this is a translation of the page on the front of the site if ( $front_page_transid == $this_transid ) { // global $wp_query, $wp; if ( 'page' == $this->get_base_post_type( $post->post_type ) ) { if ( $custom_page_template = get_post_meta( get_option( 'page_on_front' ), '_wp_page_template', true ) ) $templates = (array) $custom_page_template; else $templates = (array) 'page.php'; if ( $_template = locate_template( $templates ) ) { return $_template; } } } } // Check if we're dealing with a page or a translation of a page if ( 'page' == $this->get_base_post_type( $post->post_type ) ) { $custom_page_template = get_post_meta( get_the_ID(), '_wp_page_template', true ); if ( $custom_page_template && 'default' != $custom_page_template ) $templates = (array) $custom_page_template; else $templates = array( 'page.php' ); if ( $_template = locate_template( $templates ) ) { return $_template; } } $templates[] = "single-{$this->get_base_post_type($post->post_type)}.php"; $templates[] = "single.php"; $template = get_query_template( 'bbl-single', $templates ); return $template; } /** * Hooks the bbl_sync_meta_key filter from this class which checks * if a meta_key should be synced. If we return false, it won't be. * * @TODO correct inline docs **/ function sync_meta_key( $sync, $meta_key ) { $sync_not = array( '_edit_last', // Related to edit lock, should be individual to translations '_edit_lock', // The edit lock, should be individual to translations '_bbl_default_text_direction', // The text direction, should be individual to translations '_wp_trash_meta_status', '_wp_trash_meta_time', ); if ( in_array( $meta_key, $sync_not ) ) $sync = false; return $sync; } /** * Hooks the WP manage_posts_columns filter to add our “link” column. * * @param array $cols The columns for this post type lists table * @param string $post_type The post type for this lists table * @return array The columns **/ public function manage_posts_columns( array $columns, $post_type ) { // Insert our cols just before comments, or date. if ( $post_type == bbl_get_post_type_in_lang( $post_type, bbl_get_default_lang_code() ) ) return $columns; # @TODO is this phrase localisable? Might need changing. $columns[ 'bbl_link' ] = __( 'Translation of', 'babble' ); return $columns; } /** * Hooks the WP manage_posts_custom_column action to add our “link” content. * * @param string $column_name The name of this column * @param int $post_id The ID for the post for the row which parents this column * @return void **/ public function manage_posts_custom_column( $column_name, $post_id ) { if ( 'bbl_link' != $column_name ) return; $default_post = bbl_get_post_in_lang( $post_id, bbl_get_default_lang_code() ); if ( ! $default_post ) { echo '' . __( 'No link', 'babble' ) . ''; return; } $edit_link = get_edit_post_link( $default_post->ID ); $edit_link = add_query_arg( array( 'lang' => bbl_get_default_lang_code() ), $edit_link ); bbl_switch_to_lang( bbl_get_default_lang_code() ); $view_link = get_permalink( $default_post->ID ); bbl_restore_lang(); $edit_title = esc_attr( sprintf( __( 'Edit the originating post: “%s”', 'babble' ), get_the_title( $default_post->ID ) ) ); $view_title = esc_attr( sprintf( __( 'View the originating post: “%s”', 'babble' ), get_the_title( $default_post->ID ) ) ); echo "" . __( 'View', 'babble' ) . " | " . __( 'Edit', 'babble' ) . ""; } // PUBLIC METHODS // ============== public function bbl_translated_taxonomy( $translated, $taxonomy ) { if ( 'post_translation' == $taxonomy ) return false; return $translated; } public function admin_body_class( $class ) { $post_type = get_current_screen() ? get_current_screen()->post_type : null; if ( $post_type ) $class .= ' bbl-post-type-' . $post_type; return $class; } /** * Takes a set of query vars and amends them to show the content * in the current language. * * @param array $query_vars A set of WordPress query vars (sometimes called query arguments) * @param string|boolean $request If this is called on the parse_request hook, $request contains the root relative URL * @return array $query_vars A set of WordPress query vars **/ protected function translate_query_vars( array $query_vars, $request = false ) { // Sequester the original query, in case we need it to get the default content later $query_vars[ 'bbl_original_query' ] = $query_vars; // We've done this already (avoid re-translating the vars) if ( isset( $query_vars[ 'bbl_done_translation' ] ) && $query_vars[ 'bbl_done_translation' ] ) return $query_vars; $query_vars[ 'bbl_done_translation' ] = true; $lang_url_prefix = isset( $query_vars[ 'lang_url_prefix' ] ) ? $query_vars[ 'lang_url_prefix' ] : get_query_var( 'lang_url_prefix' ); $lang = isset( $query_vars[ 'lang' ] ) ? $query_vars[ 'lang' ] : get_query_var( 'lang' ); // Detect language specific homepages if ( $request == $lang_url_prefix ) { unset( $query_vars[ 'error' ] ); // @FIXME: Cater for front pages which don't list the posts if ( 'page' == get_option('show_on_front') && $page_on_front = get_option('page_on_front') ) { // @TODO: Get translated page ID $query_vars[ 'p' ] = $this->get_post_in_lang( $page_on_front, bbl_get_current_lang_code() )->ID; $query_vars[ 'post_type' ] = $this->get_post_type_in_lang( 'page', bbl_get_current_lang_code() ); return $query_vars; } // Trigger the archive listing for the relevant shadow post type // of 'post' for this language. if ( bbl_get_default_lang_code() != $lang && empty( $query_vars['s'] ) ) { $post_type = isset( $query_vars[ 'post_type' ] ) ? $query_vars[ 'post_type' ] : 'post'; $query_vars[ 'post_type' ] = $this->get_post_type_in_lang( $post_type, bbl_get_current_lang_code() ); } return $query_vars; } // If we're asking for the default content, it's fine if ( bbl_get_default_lang_code() == $lang ) { return $query_vars; } // Now swap the query vars so we get the content in the right language post_type // @FIXME: Do I need to change $wp->matched query? I think $wp->matched_rule is fine? // @FIXME: Danger of post type slugs clashing?? if ( isset( $query_vars[ 'pagename' ] ) && $query_vars[ 'pagename' ] ) { // Substitute post_type for $query_vars[ 'name' ] = $query_vars[ 'pagename' ]; $query_vars[ bbl_get_post_type_in_lang( 'page', $query_vars[ 'lang' ] ) ] = $query_vars[ 'pagename' ]; $query_vars[ 'post_type' ] = bbl_get_post_type_in_lang( 'page', bbl_get_current_lang_code() ); // Trigger a listing of translated posts if this is meant to // be the blog page. if ( 'page' == get_option( 'show_on_front' ) ) { // Test if the current page is in the same translation group as // the 'page_for_posts. $current_post = get_page_by_path( $query_vars[ 'pagename' ], null, $query_vars[ 'post_type' ] ); if ( $this->get_transid( get_option( 'page_for_posts' ) ) == $this->get_transid( $current_post ) ) { $query_vars[ 'post_type' ] = bbl_get_post_type_in_lang( 'post', bbl_get_current_lang_code() ); unset( $query_vars[ 'name' ] ); unset( $query_vars[ bbl_get_post_type_in_lang( 'page', $query_vars[ 'lang' ] ) ] ); } } unset( $query_vars[ 'page' ] ); unset( $query_vars[ 'pagename' ] ); } elseif ( isset( $query_vars[ 'year' ] ) && $query_vars[ 'year' ] ) { // @FIXME: This is not a reliable way to detect queries for the 'post' post_type. $query_vars[ 'post_type' ] = bbl_get_post_type_in_lang( 'post', bbl_get_current_lang_code() ); } elseif ( isset( $query_vars[ 'post_type' ] ) ) { if ( is_array( $query_vars[ 'post_type' ] ) ) { $new_post_types = array(); foreach ( $query_vars[ 'post_type' ] as $post_type ) { $new_post_types[] = bbl_get_post_type_in_lang( $post_type, bbl_get_current_lang_code() ); } $query_vars[ 'post_type' ] = $new_post_types; } else { $query_vars[ 'post_type' ] = bbl_get_post_type_in_lang( $query_vars[ 'post_type' ], bbl_get_current_lang_code() ); } } else { $query_vars[ 'post_type' ] = bbl_get_post_type_in_lang( 'post', bbl_get_current_lang_code() ); } return $query_vars; } /** * Discover whether a post is set as the front page * for the site in a particular language. * * @param int $post_id The ID of a post * @return boolean True if this post is used as the front page of the site for a language **/ public function is_language_front_page( $post_id = null, $lang_code = null ) { if ( 'page' != get_option('show_on_front') ) return false; $post = get_post( $post_id ); // If we have a lang code, and it doesn't match the requested post lang then this // is not the right front page if ( ! is_null( $lang_code ) && $lang_code != $this->get_post_lang_code( $post->ID ) ) return false; $front_page_transid = $this->get_transid( get_option( 'page_on_front' ) ); $this_transid = $this->get_transid( get_the_ID() ); if ( $front_page_transid != $this_transid ) return false; return true; } /** * Return the language code for the language a given post is written for/in. * * @param int|object $post Either a WP Post object, or a post ID * @return string|object Either a language code, or a WP_Error object * @access public **/ public function get_post_lang_code( $post ) { $post = get_post( $post ); if ( ! $post ) return new WP_Error( 'bbl_invalid_post', __( 'Invalid Post passed to get_post_lang_code', 'babble' ) ); if ( isset( $this->lang_map[ $post->post_type ] ) ) return $this->lang_map[ $post->post_type ]; return bbl_get_default_lang_code(); } /** * Return the admin URL to create a new translation for a post in a * particular language. * * @param int|object $default_post The post 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 **/ public function get_new_post_translation_url( $default_post, $lang_code ) { $default_post = get_post( $default_post ); $url = admin_url( 'post-new.php' ); $args = array( 'bbl_origin_post' => $default_post->ID, 'lang' => $lang_code, 'post_type' => 'bbl_job', ); $url = add_query_arg( $args, $url ); return $url; } /** * Returns the post ID for the post in the default language from which * this post was translated. * * @param int|WP_Post $post Either a WP Post object, or a post ID * @return int The ID of the default language equivalent post **/ public function get_default_lang_post( $post ) { $post = get_post( $post ); $translations = bbl_get_post_translations( $post->ID ); if ( isset( $translations[ bbl_get_default_lang_code() ] ) ) return $translations[ bbl_get_default_lang_code() ]; return false; } /** * Get the posts which are the translations for the provided * post ID. N.B. The returned array of post objects (and false * values) will include the post for the post ID passed. * * @param int|WP_Post $post Either a WP Post object, or a post ID * @return array Either an array keyed by the site languages, each key containing false (if no translation) or a WP Post object **/ public function get_post_translations( $post ) { $post = get_post( $post ); // @FIXME: Is it worth caching here, or can we just rely on the caching in get_objects_in_term and get_posts? $transid = $this->get_transid( $post ); if ( $translations = wp_cache_get( $transid, 'bbl_post_translations' ) ) { return $translations; } # @TODO A transid should never be a wp_error. Check and fix. if ( is_wp_error( $transid ) ) error_log( "Error getting transid: " . print_r( $transid, true ) ); $post_ids = get_objects_in_term( $transid, 'post_translation' ); // Work out all the translated equivalent post types $post_types = array(); $langs = bbl_get_active_langs(); foreach ( $langs as $lang ) $post_types[] = bbl_get_post_type_in_lang( $post->post_type, $lang->code ); // Get all the translations in one cached DB query $args = array( // We want a clean listing, without any particular language 'bbl_translate' => false, 'include' => $post_ids, 'post_type' => $post_types, 'post_status' => array( 'publish', 'pending', 'draft', 'future' ), ); $posts = get_posts( $args ); $translations = array(); foreach ( $posts as $post ) $translations[ $this->get_post_lang_code( $post ) ] = $post; wp_cache_add( $transid, $translations, 'bbl_post_translations' ); return $translations; } /** * Return the base post type (in the default language) for a * provided post type. * * @param string $post_type The name of a post type * @return string The name of the base post type **/ public function get_base_post_type( $post_type ) { if ( ! isset( $this->post_types[ $post_type ] ) ) return $post_type; return $this->post_types[ $post_type ]; } /** * Return all the base post types (in the default language). * * @return array An array of post_type objects **/ public function get_base_post_types() { $post_types = array(); foreach ( $this->post_types as $post_type ) $post_types[ $post_type ] = get_post_type_object( $post_type ); return $post_types; } /** * Returns the equivalent post_type in the specified language. * * @param string $post_type A post_type to return in a given language * @param string $lang_code The language code for the required language * @return string The equivalent post_type name, or given post_type if it doesn't exist **/ public function get_post_type_in_lang( $post_type, $lang_code ) { $base_post_type = $this->get_base_post_type( $post_type ); if ( bbl_get_default_lang_code() == $lang_code ) { return $base_post_type; } // Some post types are untranslated… if ( ! bbl_is_translated_post_type( $post_type ) ) { return $post_type; } // Return the original post type if we couldn't find it in our array if ( ! isset( $this->lang_map2[ $lang_code ][ $base_post_type ] ) ) { return $post_type; } return $this->lang_map2[ $lang_code ][ $base_post_type ]; } /** * Returns an array of all the shadow post types associated with * this post type. * * @param string $base_post_type The post type to look up shadow post types for * @return array The names of all the related shadow post types **/ public function get_shadow_post_types( $base_post_type ) { $post_types = array(); $langs = bbl_get_active_langs(); foreach ( $langs as $lang ) { if ( isset( $this->lang_map2[ $lang->code ][ $base_post_type ] ) ) $post_types[] = $this->lang_map2[ $lang->code ][ $base_post_type ]; } return $post_types; } /** * Returns the post in a particular language, or the fallback content * if there's no post available. * * @param int|WP_Post $post Either a WP Post object, or a post ID * @param string $lang_code The language code for the required language * @param boolean $fallback If true: if a post is not available, fallback to the default language content (defaults to true) * @return object|boolean The WP Post object, or if $fallback was false and no post then returns false **/ public function get_post_in_lang( $post, $lang_code, $fallback = true ) { $translations = $this->get_post_translations( $post ); if ( isset( $translations[ $lang_code ] ) ) { return $translations[ $lang_code ]; } if ( ! $fallback ) { return false; } return $translations[ bbl_get_default_lang_code() ]; } /** * Returns a slug translated into a particular language. * * @param string $slug The slug to translate * @param string $lang A Babble language object * @param array $post_type_args The args for the post type associated with this post type * @return void **/ public function get_slug_in_lang( $slug, $lang ) { $_slug = mb_strtolower( apply_filters( 'bbl_translate_post_type_slug', $slug, $lang->code ) ); // @FIXME: For some languages the translation might be the same as the original if ( $_slug && $_slug != $slug ) return $_slug; // FIXME: Do we need to check that the slug is unique at this point? return mb_strtolower( "{$_slug}_{$lang->code}" ); } // PRIVATE/PROTECTED METHODS // ========================= /** * Copy various properties from one post to another. * * @param int $source_id The source post, to copy FROM * @param int $target_id The target post, to copy TO * @return void **/ public function sync_properties( $source_id, $target_id ) { if ( ! ( $source_post = get_post( $source_id ) ) ) return; $source_lang_code = bbl_get_post_lang_code( $source_id ); $target_lang_code = bbl_get_post_lang_code( $target_id ); $target_parent_post = false; if ( $source_post->post_parent ) { $source_parent_post = $this->get_post_in_lang( $source_post->post_parent, $source_lang_code ); $target_parent_post = $this->get_post_in_lang( $source_parent_post, $target_lang_code ); } $target_post = get_post( $target_id ); $postdata = array( 'ID' => $target_id, 'post_author' => $source_post->post_author, 'post_modified' => $target_post->post_modified, 'post_modified_gmt' => $target_post->post_modified_gmt, 'ping_status' => $source_post->ping_status, 'post_password' => $source_post->post_password, 'menu_order' => $source_post->menu_order, 'post_mime_type' => $source_post->post_mime_type, ); if ( $target_parent_post ) $postdata[ 'post_parent' ] = $target_parent_post->ID; else $postdata[ 'post_parent' ] = 0; if ( bbl_get_default_lang_code() == $source_lang_code ) { $postdata[ 'post_date' ] = $source_post->post_date; $postdata[ 'post_date_gmt' ] = $source_post->post_date_gmt; } // Comment status only synced when going from the default lang code if ( bbl_get_default_lang_code() == $source_lang_code ) $postdata[ 'comment_status' ] = $source_post->comment_status; $postdata = apply_filters( 'bbl_pre_sync_properties', $postdata, $source_id ); wp_update_post( $postdata ); } /** * Resync all (synced) post meta data from the post in * the default language to this post. * * @param $int The post ID to sync TO * @return void **/ function sync_post_meta( $post_id ) { if ( $this->no_meta_recursion ) return; $this->no_meta_recursion = 'updated_post_meta'; // First delete all the synced meta from this post $current_metas = (array) get_post_meta( $post_id ); foreach ( $current_metas as $current_meta_key => & $current_meta_values ) { // Some metadata shouldn't be synced, this filter allows a dev to return // false if the particular meta_key is one which shouldn't be synced. // If you find a core meta_key which is currently synced and should NOT be, // please submit a patch to the sync_meta_key method on this class. Thanks. if ( apply_filters( 'bbl_sync_meta_key', true, $current_meta_key ) ) delete_post_meta( $post_id, $current_meta_key ); } // Now add meta in again from the origin post $origin_post = bbl_get_post_in_lang( $post_id, bbl_get_default_lang_code() ); $metas = get_post_meta( $origin_post->ID ); if ( ! $metas ) return; foreach ( $metas as $meta_key => & $meta_value ) { // Some metadata shouldn't be synced if ( ! apply_filters( 'bbl_sync_meta_key', true, $meta_key ) ) continue; // The meta could be an array stored in a single postmeta row or an // array of values from multiple rows; work out which we have. $val_multi = get_post_meta( $origin_post->ID, $meta_key ); foreach ( $val_multi as & $val_single ) { add_post_meta( $post_id, $meta_key, $val_single ); } } $this->no_meta_recursion = false; } /** * Get the transID for this post, this is an identifier linking all the translations * for a single piece of content together. * * Marked private as we may change how translations are linked. Please use API, or * raise an issue. * * @param int|object $post The WP Post object, or the ID of a post * @return string The transid * @access private **/ function get_transid( $post, $create = true ) { $post = get_post( $post ); if ( ! $post->ID ) return false; if ( $transid = wp_cache_get( $post->ID, 'bbl_post_transids' ) ) { return $transid; } $transids = (array) wp_get_object_terms( $post->ID, 'post_translation', array( 'fields' => 'ids' ) ); // "There can be only one" (so we'll just drop the others) $transid = false; if ( isset( $transids[ 0 ] ) ) { $transid = $transids[ 0 ]; } else { if ( $create ) { $transid = $this->set_transid( $post ); } } if ( ! $transid ) { return false; } wp_cache_add( $post->ID, $transid, 'bbl_post_transids' ); return $transid; } /** * Create and assign a new TransID to a post. * * @param int|object $post Either a Post ID or a WP Post object * @param string $transid (optional) A transid to associate with the post * @return string The transid which has just been set * @access private **/ function set_transid( $post, $transid = false ) { $post = get_post( $post ); if ( ! isset( $post->ID ) ) return false; // @FIXME: Abstract the code for generating and associating a new TransID if ( ! $transid ) { $transid_name = 'post_transid_' . uniqid(); $result = wp_insert_term( $transid_name, 'post_translation', array() ); if ( is_wp_error( $result ) ) error_log( "Problem creating a new Post TransID: " . print_r( $result, true ) ); else $transid = $result[ 'term_id' ]; // Delete anything in there currently wp_cache_delete( $transid, 'bbl_post_translations' ); } $result = wp_set_object_terms( $post->ID, $transid, 'post_translation' ); if ( is_wp_error( $result ) ) error_log( "Problem associating TransID with new posts: " . print_r( $result, true ) ); $this->clean_post_cache( $post->ID ); return $transid; } /** * Return a list of features supported by a post_type. * * Hello there, code investigator. I imagine you're wondering * why I'm accessing a global prefixed by an underscore? I realise * these are nominally private variables, prone to change, but * I need to access a list of all features supported by a post * type, in order to shadow it for the various translations, * and there's no core function to allow me to do this. * * @TODO: Raise a Trac ticket for adding this functionality to the post type API * * @param string $post_type The name of the post type for which to get the features supported * @return array An array of features supported by this post type **/ protected function get_features_supported_by_post_type( $post_type ) { global $_wp_post_type_features; return (array) $_wp_post_type_features[$post_type]; } /** * Are we on the media upload gallery tab? * * @param string $tab A specific tab to detect * @return boolean True if we are on media upload generally, and the specific tab if specified **/ protected function is_media_upload_tab( $tab = null ) { if ( ! is_admin() ) return false; if ( 'media-upload.php' != basename( $_SERVER[ 'SCRIPT_NAME' ] ) ) { return false; } if ( is_null( $tab ) ) { return true; } if ( isset( $_GET[ 'tab' ] ) || $tab == $_GET[ 'tab' ] ) { return true; } return false; } /** * Are we viewing the (3.5+) media manager? * * @return boolean True if we are viewing the media manager **/ protected function is_media_manager() { if ( ! is_admin() ) return false; if ( !isset( $_POST['action'] ) ) { return false; } if ( 'query-attachments' == $_POST['action'] ) { return true; } return false; } /** * Remove over-synced post metas. * * @return void **/ protected function prune_post_meta() { global $wpdb; $meta_keys = array( '_thumbnail_id', '_wp_old_slug' , '_wp_page_template', '_wp_trash_meta_status', '_wp_trash_meta_time', ); foreach ( $meta_keys as $meta_key ) { $prepared_sql = $wpdb->prepare( "SELECT COUNT(*) AS count, post_id, meta_key, meta_value FROM $wpdb->postmeta WHERE meta_key = %s GROUP BY post_id, meta_key, meta_value HAVING count > 1", $meta_key ); $metas = $wpdb->get_results( $prepared_sql ); foreach ( $metas as $meta ) { if ( $meta->count < 2 ) { continue; } $prepared_sql = $wpdb->prepare( "DELETE FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = %s AND meta_value = %s LIMIT %d", $meta->post_id, $meta->meta_key, $meta->meta_value, (int) $meta->count - 1 ); $wpdb->query( $prepared_sql ); } } } /** * Checks the DB structure is up to date, rewrite rules, * theme image size options are set, etc. * * @return void **/ protected function maybe_upgrade() { global $wpdb; $option_name = 'bbl_post_public_version'; $version = get_option( $option_name, 0 ); if ( $version == $this->version ) return; if ( $start_time = get_option( "{$option_name}_running", false ) ) { $time_diff = time() - $start_time; // Check the lock is less than 30 mins old, and if it is, bail if ( $time_diff < ( 60 * 30 ) ) { error_log( "Babble Post Public: Existing update routine has been running for less than 30 minutes" ); return; } error_log( "Babble Post Public: Update routine is running, but older than 30 minutes; going ahead regardless" ); } else { add_option( "{$option_name}_running", time(), null, 'no' ); } if ( $version < 9 ) { error_log( "Babble Post Public: Start pruning metadata" ); $this->prune_post_meta(); error_log( "Babble Post Public: Remove excess post meta" ); } // N.B. Remember to increment $this->version above when you add a new IF update_option( $option_name, $this->version ); delete_option( "{$option_name}_running", true, null, 'no' ); error_log( "Babble Post Public: Done upgrade, now at version " . $this->version ); } /** * Checks for duplicated metadata in some key meta_keys. * * @return boolean * @author Simon Wheatley */ function have_duplicate_metadata() { global $wpdb; $sql = " SELECT COUNT(*) AS count, post_id, meta_key, meta_value FROM $wpdb->postmeta WHERE meta_key IN ( '_extmedia-youtube', '_extmedia-duration', '_thumbnail_id', '_wp_trash_meta_time', '_wp_page_template', '_wp_trash_meta_status' ) GROUP BY post_id, meta_key, meta_value HAVING count > 1 ORDER BY count, post_id, meta_key "; return (bool) count( $wpdb->get_results( $sql ) ); } } global $bbl_post_public; $bbl_post_public = new Babble_Post_Public(); ================================================ FILE: class-switcher-content.php ================================================ populate_links(); return $this->links; } // PRIVATE/PROTECTED METHODS // ========================= /** * undocumented function * * @return void **/ protected function populate_links() { if ( is_array( $this->links ) && ! empty( $this->links ) ) return; // Already done $this->links = array(); // @FIXME: Not sure this is the best way to specify languages $alt_langs = bbl_get_active_langs(); $this->screen = is_admin() ? get_current_screen() : false; // Create a handy flag for whether we're editing a post or listing posts $editing_post = false; $listing_posts = false; if ( is_admin() ) { $editing_post = ( is_admin() && 'post' == $this->screen->base && isset( $_GET[ 'post' ] ) ); $listing_posts = ( is_admin() && 'edit' == $this->screen->base && ! isset( $_GET[ 'post' ] ) ); } // Create a handy flag for whether we're editing a term or listing terms $editing_term = false; $listing_terms = false; if ( is_admin() ) { $editing_term = ( is_admin() && 'edit-tags' == $this->screen->base && isset( $_GET[ 'tag_ID' ] ) ); $listing_terms = ( is_admin() && 'edit-tags' == $this->screen->base && ! isset( $_GET[ 'tag_ID' ] ) ); } if ( is_singular() || is_single() || $editing_post ) { $this->translations = bbl_get_post_translations( get_the_ID() ); $this->jobs = bbl_get_incomplete_post_jobs( get_the_ID() ); } else if ( 'page' == get_option( 'show_on_front' ) && is_home() ) { $this->translations = bbl_get_post_translations( get_option( 'page_for_posts' ) ); $this->jobs = bbl_get_incomplete_post_jobs( get_option( 'page_for_posts' ) ); } else if ( ( !is_admin() and ( is_tax() || is_category() ) ) || $editing_term ) { if ( isset( $_REQUEST[ 'tag_ID' ] ) ) $term = get_term( (int) @ $_REQUEST[ 'tag_ID' ], $this->screen->taxonomy ); else $term = get_queried_object(); $this->translations = bbl_get_term_translations( $term->term_id, $term->taxonomy ); $this->jobs = bbl_get_term_jobs( $term->term_id, $term->taxonomy ); } foreach ( $alt_langs as $i => & $alt_lang ) { // @TODO: Convert to a switch statement, convert all the vars to a single property on the class if ( is_admin() ) { if ( $editing_post ) { // Admin: Editing post link $this->add_admin_post_link( $alt_lang ); } else if ( $editing_term ) { // Admin: Editing term link $this->add_admin_term_link( $alt_lang ); } else if ( $listing_posts ) { // Admin: Listing posts link $this->add_admin_list_posts_link( $alt_lang ); } else if ( $listing_terms ) { // Admin: Listing terms link $this->add_admin_list_terms_link( $alt_lang ); } else { // Admin: Generic link link $this->add_admin_generic_link( $alt_lang ); } continue; } if ( is_singular() || is_single() || ( 'page' == get_option( 'show_on_front' ) && is_home() ) ) { // Single posts, pages, blog homepage $this->add_post_link( $alt_lang ); continue; } // Don't add a switcher link if the language is not public and // the user cannot edit any posts (as a rough guide to whether // they are more than just a subscriber). // @TODO this cap check should move into each add_*_link() method: if ( ! bbl_is_public_lang( $alt_lang->code ) && ! current_user_can( 'edit_posts' ) ) continue; if ( is_front_page() ) { // Language homepage // is_front_page works for language homepages, phew $this->add_front_page_link( $alt_lang ); } else if ( is_post_type_archive() ) { // Post type archives $this->add_post_type_archive_link( $alt_lang ); } else if ( is_tax() || is_category() ) { // Category or taxonomy archive $this->add_taxonomy_archive_link( $alt_lang ); } else { // 404's, amongst other things $this->add_arbitrary_link( $alt_lang ); } } // Make up the class attribute on all links foreach ( $this->links as $lang_code => & $link ){ $link[ 'class' ] = implode( ' ', $link[ 'classes' ] ); $link[ 'active' ] = $lang_code == bbl_get_current_lang_code(); } } /** * Add an admin link to the same page, but with the language switch GET * parameter set. * * @param object $lang A Babble language object for this link * @return void **/ protected function add_admin_generic_link( $lang ) { $classes = array(); $href = add_query_arg( array( 'lang' => $lang->code ) ); $href = apply_filters( 'bbl_switch_admin_generic_link', $href, $lang ); $title = sprintf( __( 'Switch to %s', 'babble' ), $lang->display_name ); $classes[] = "bbl-lang-$lang->code bbl-lang-$lang->url_prefix"; $classes[] = 'bbl-admin'; $classes[] = 'bbl-admin-generic'; $classes[] = 'bbl-lang'; if ( $lang->code == bbl_get_current_lang_code() ) $classes[] = 'bbl-active'; // Preventing errors on initial plugin load - before settings saved the first time if ( empty( $lang->display_name ) ) { $lang->display_name = ''; } $this->links[ $lang->code ] = array( 'classes' => $classes, 'href' => $href, 'id' => $lang->url_prefix, 'meta' => array( 'class' => strtolower( join( ' ', array_unique( $classes ) ) ) ), 'title' => $title, 'lang' => $lang, ); } /** * Add an admin term link to the parent link provided (by reference). * * @param object $lang A Babble language object for this link * @return void **/ protected function add_admin_term_link( $lang ) { $classes = array(); if ( isset( $this->translations[ $lang->code ]->term_id ) ) { // Translation exists $args = array( 'lang' => $lang->code, 'taxonomy' => $this->translations[ $lang->code ]->taxonomy, 'tag_ID' => $this->translations[ $lang->code ]->term_id ); $href = add_query_arg( $args ); $title = sprintf( __( 'Switch to %s', 'babble' ), $lang->display_name ); $classes[] = 'bbl-existing-edit'; $classes[] = 'bbl-existing-edit-term'; } else if ( isset( $this->jobs[ $lang->code ]->ID ) ) { // Translation job exists $href = get_edit_post_link( $this->jobs[ $lang->code ]->ID, 'url' ); $title = sprintf( _x( '%s: %s', 'Translation job status and language (example: In Progress: French)', 'babble' ), get_post_status_object( $this->jobs[ $lang->code ]->post_status )->label, $lang->display_name ); $classes[] = 'bbl-job-edit'; $classes[] = 'bbl-job-edit-term'; } else { // Translation does not exist $default_term = (int) $_GET[ 'tag_ID' ]; $href = bbl_get_new_term_translation_url( $default_term, $lang->code, $this->screen->taxonomy ); $title = sprintf( __( 'Create for %s', 'babble' ), $lang->display_name ); $classes[] = 'bbl-add'; $classes[] = 'bbl-add-term'; } $href = apply_filters( 'bbl_switch_admin_term_link', $href, $lang, $this->translations ); $classes[] = "bbl-lang-$lang->code bbl-lang-$lang->url_prefix"; $classes[] = 'bbl-admin'; $classes[] = 'bbl-admin-taxonomy'; $classes[] = 'bbl-admin-edit-term'; $classes[] = 'bbl-lang'; if ( $lang->code == bbl_get_current_lang_code() ) $classes[] = 'bbl-active'; $this->links[ $lang->code ] = array( 'classes' => $classes, 'href' => $href, 'id' => $lang->url_prefix, 'meta' => array( 'class' => strtolower( join( ' ', array_unique( $classes ) ) ) ), 'title' => $title, 'lang' => $lang, ); } /** * Add an admin list terms screen link to the parent link provided (by reference). * * @param object $lang A Babble language object for this link * @return void **/ protected function add_admin_list_terms_link( $lang ) { $classes = array(); $args = array( 'lang' => $lang->code, 'taxonomy' => bbl_get_taxonomy_in_lang( $this->screen->taxonomy, $lang->code ), ); $href = add_query_arg( $args ); $href = apply_filters( 'bbl_switch_admin_list_terms_link', $href, $lang ); $title = sprintf( __( 'Switch to %s', 'babble' ), $lang->display_name ); $classes[] = "bbl-lang-$lang->code bbl-lang-$lang->url_prefix"; $classes[] = 'bbl-admin'; $classes[] = 'bbl-admin-taxonomy'; $classes[] = 'bbl-admin-list-terms'; $classes[] = 'bbl-lang'; if ( $lang->code == bbl_get_current_lang_code() ) $classes[] = 'bbl-active'; $this->links[ $lang->code ] = array( 'classes' => $classes, 'href' => $href, 'id' => $lang->url_prefix, 'meta' => array( 'class' => strtolower( join( ' ', array_unique( $classes ) ) ) ), 'title' => $title, 'lang' => $lang, ); } /** * Add an admin post link to the parent link provided (by reference) * * @param object $lang A Babble language object for this link * @return void **/ protected function add_admin_post_link( $lang ) { $classes = array(); if ( isset( $this->translations[ $lang->code ]->ID ) ) { // Translation exists $href = add_query_arg( array( 'lang' => $lang->code, 'post' => $this->translations[ $lang->code ]->ID ) ); $href = remove_query_arg( 'message', $href ); $title = sprintf( __( 'Switch to %s', 'babble' ), $lang->display_name ); $classes[] = 'bbl-existing-edit'; $classes[] = 'bbl-existing-edit-post'; } else if ( isset( $this->jobs[ $lang->code ]->ID ) ) { // Translation job exists $href = add_query_arg( array( 'lang' => $lang->code, 'post' => $this->jobs[ $lang->code ]->ID ) ); $href = remove_query_arg( 'message', $href ); $title = sprintf( _x( '%s: %s', 'Translation job status and language (example: In Progress: French)', 'babble' ), get_post_status_object( $this->jobs[ $lang->code ]->post_status )->label, $lang->display_name ); $classes[] = 'bbl-job-edit'; $classes[] = 'bbl-job-edit-post'; } else { // Translation does not exist if ( isset( $this->translations[ bbl_get_default_lang_code() ] ) ) { $default_post = $this->translations[ bbl_get_default_lang_code() ]; $href = bbl_get_new_post_translation_url( $default_post, $lang->code ); $title = sprintf( __( 'Create for %s', 'babble' ), $lang->display_name ); $classes[] = 'bbl-add'; $classes[] = 'bbl-add-post'; } else { return; // Don't create the switcher menu items yet } } $href = apply_filters( 'bbl_switch_admin_post_link', $href, $lang, $this->translations ); $classes[] = "bbl-lang-$lang->code bbl-lang-$lang->url_prefix"; $classes[] = 'bbl-admin'; $classes[] = 'bbl-admin-edit-post'; $classes[] = 'bbl-admin-post-type'; $classes[] = 'bbl-lang'; $classes[] = 'bbl-post'; if ( $lang->code == bbl_get_current_lang_code() ) $classes[] = 'bbl-active'; $this->links[ $lang->code ] = array( 'classes' => $classes, 'href' => $href, 'id' => $lang->url_prefix, 'meta' => array( 'class' => strtolower( join( ' ', array_unique( $classes ) ) ) ), 'title' => $title, 'lang' => $lang, ); } /** * Add an admin list posts screen link to the parent link provided (by reference). * * @param object $lang A Babble language object for this link * @return void **/ protected function add_admin_list_posts_link( $lang ) { $classes = array(); $args = array( 'lang' => $lang->code, 'post_type' => bbl_get_post_type_in_lang( $this->screen->post_type, $lang->code ), ); $href = add_query_arg( $args ); $href = apply_filters( 'bbl_switch_admin_list_posts_link', $href, $lang ); $title = sprintf( __( 'Switch to %s', 'babble' ), $lang->display_name ); $classes[] = "bbl-lang-$lang->code bbl-lang-$lang->url_prefix"; $classes[] = 'bbl-admin'; $classes[] = 'bbl-admin-edit-post'; $classes[] = 'bbl-admin-post-type'; $classes[] = 'bbl-lang'; if ( $lang->code == bbl_get_current_lang_code() ) $classes[] = 'bbl-active'; $this->links[ $lang->code ] = array( 'classes' => $classes, 'href' => $href, 'id' => $lang->url_prefix, 'meta' => array( 'class' => strtolower( join( ' ', array_unique( $classes ) ) ) ), 'title' => $title, 'lang' => $lang, ); } /** * Add a post link to the parent link provided (by reference) * * @param object $lang A Babble language object for this link * @return void **/ protected function add_post_link( $lang ) { $classes = array(); if ( isset( $this->translations[ $lang->code ] ) ) { // Translation exists // Don't add this link if the user cannot edit THIS post and // the language is not public. if ( ! bbl_is_public_lang( $lang->code ) && ! current_user_can( 'edit_post', $this->translations[ $lang->code ]->ID ) ) { return; } bbl_switch_to_lang( $lang->code ); $href = get_permalink( $this->translations[ $lang->code ]->ID ); bbl_restore_lang(); $title = sprintf( __( 'Switch to %s', 'babble' ), $lang->display_name ); $classes[] = 'bbl-existing'; $classes[] = 'bbl-existing-post'; } else if ( current_user_can( 'edit_post', $this->translations[ bbl_get_default_lang_code() ]->ID ) ) { // Generate a URL to create the translation $default_post = $this->translations[ bbl_get_default_lang_code() ]; $href = bbl_get_new_post_translation_url( $default_post, $lang->code ); $title = sprintf( __( 'Create for %s', 'babble' ), $lang->display_name ); $classes[] = 'bbl-add'; $classes[] = 'bbl-add-post'; } else { // Don't show links to non-public languages if ( ! bbl_is_public_lang( $lang->code ) ) return; // Show a blank link for unavailable translations $href = false; $title = sprintf( __( 'This content is unavailable in %s', 'babble' ), $lang->display_name ); $classes[] = 'bbl-unavailable'; } $href = apply_filters( 'bbl_switch_post_link', $href, $lang, $this->translations ); $classes[] = "bbl-lang-$lang->code bbl-lang-$lang->url_prefix"; $classes[] = 'bbl-lang'; $classes[] = 'bbl-post'; if ( $lang->code == bbl_get_current_lang_code() ) $classes[] = 'bbl-active'; $this->links[ $lang->code ] = array( 'classes' => $classes, 'href' => $href, 'id' => $lang->url_prefix, 'meta' => array( 'class' => strtolower( join( ' ', array_unique( $classes ) ) ) ), 'title' => $title, 'lang' => $lang, ); } /** * Add a link to a language specific front page. * * @TODO: Is this any different from a regular post link? * * @param object $lang A Babble language object for this link * @return void **/ protected function add_front_page_link( $lang ) { global $bbl_locale; $classes = array(); remove_filter( 'home_url', array( $bbl_locale, 'home_url'), null, 2 ); $href = home_url( "$lang->url_prefix/" ); add_filter( 'home_url', array( $bbl_locale, 'home_url'), null, 2 ); $href = apply_filters( 'bbl_switch_front_page_link', $href, $lang ); $title = sprintf( __( 'Switch to %s', 'babble' ), $lang->display_name ); $classes[] = "bbl-lang-$lang->code bbl-lang-$lang->url_prefix"; $classes[] = 'bbl-existing'; $classes[] = 'bbl-front-page'; $classes[] = 'bbl-lang'; if ( $lang->code == bbl_get_current_lang_code() ) $classes[] = 'bbl-active'; $this->links[ $lang->code ] = array( 'classes' => $classes, 'id' => $lang->url_prefix, 'href' => $href, 'meta' => array( 'class' => strtolower( join( ' ', array_unique( $classes ) ) ) ), 'title' => $title, 'lang' => $lang, ); } /** * Add a link to a post_type archive. * * @param object $lang A Babble language object for this link * @return void **/ protected function add_post_type_archive_link( $lang ) { $classes = array(); bbl_switch_to_lang( $lang->code ); $href = get_post_type_archive_link( get_query_var( 'post_type' ) ); bbl_restore_lang(); $href = apply_filters( 'bbl_switch_post_type_archive_link', $href, $lang ); $title = sprintf( __( 'Switch to %s', 'babble' ), $lang->display_name ); $classes[] = "bbl-lang-$lang->code bbl-lang-$lang->url_prefix"; $classes[] = 'bbl-existing'; $classes[] = 'bbl-post-type-archive'; $classes[] = 'bbl-lang'; if ( $lang->code == bbl_get_current_lang_code() ) $classes[] = 'bbl-active'; $this->links[ $lang->code ] = array( 'classes' => $classes, 'id' => $lang->url_prefix, 'href' => $href, 'meta' => array( 'class' => strtolower( join( ' ', array_unique( $classes ) ) ) ), 'title' => $title, 'lang' => $lang, ); } /** * Add a link to a taxonomy archive. * * @param object $lang A Babble language object for this link * @return void **/ protected function add_taxonomy_archive_link( $lang ) { $classes = array(); $queried_object = get_queried_object(); if ( ! bbl_is_translated_taxonomy( $queried_object->taxonomy ) ) { $this->add_arbitrary_link( $lang ); return; } elseif ( isset( $this->translations[ $lang->code ]->term_id ) ) { // Translation exists bbl_switch_to_lang( $lang->code ); $href = get_term_link( $this->translations[ $lang->code ], bbl_get_base_taxonomy( $queried_object->taxonomy ) ); bbl_restore_lang(); $title = sprintf( __( 'Switch to %s', 'babble' ), $lang->display_name ); $classes[] = 'bbl-existing'; $classes[] = 'bbl-existing-term'; } else { // Translation does not exist // Generate a URL to create the translation $default_term = $this->translations[ bbl_get_default_lang_code() ]; $href = bbl_get_new_term_translation_url( $default_term->term_id, $lang->code, $default_term->taxonomy ); $title = sprintf( __( 'Create for %s', 'babble' ), $lang->display_name ); $classes[] = 'bbl-add'; $classes[] = 'bbl-add-term'; } $href = apply_filters( 'bbl_switch_taxonomy_archive_link', $href, $lang, $this->translations ); $classes[] = "bbl-lang-$lang->code bbl-lang-$lang->url_prefix"; $classes[] = 'bbl-lang'; $classes[] = 'bbl-term'; if ( $lang == bbl_get_current_lang_code() ) $classes[] = 'bbl-active'; $this->links[ $lang->code ] = array( 'classes' => $classes, 'href' => $href, 'id' => $lang->url_prefix, 'meta' => array( 'class' => strtolower( join( ' ', array_unique( $classes ) ) ) ), 'title' => $title, 'lang' => $lang, ); } /** * Add a link to an arbitrary link, e.g. 404, within the site. * * @param object $lang A Babble language object for this link * @return void **/ protected function add_arbitrary_link( $lang ) { $classes = array(); if ( ! preg_match( '|^/[^/]+/(.*)?|', $_SERVER[ 'REQUEST_URI' ], $matches ) ) return; bbl_switch_to_lang( $lang->code ); $href = home_url( $matches[ 1 ] ); bbl_restore_lang(); $href = apply_filters( 'bbl_switch_arbitrary_link', $href, $lang ); $title = sprintf( __( 'Switch to %s', 'babble' ), $lang->display_name ); $classes[] = 'bbl-existing'; $classes[] = 'bbl-existing-term'; $classes[] = "bbl-lang-$lang->code bbl-lang-$lang->url_prefix"; $classes[] = 'bbl-lang'; $classes[] = 'bbl-term'; if ( $lang == bbl_get_current_lang_code() ) $classes[] = 'bbl-active'; $this->links[ $lang->code ] = array( 'classes' => $classes, 'href' => $href, 'id' => $lang->url_prefix, 'meta' => array( 'class' => strtolower( join( ' ', array_unique( $classes ) ) ) ), 'title' => $title, 'lang' => $lang, ); } } global $bbl_switcher_menu; $bbl_switcher_menu = new Babble_Switcher_Menu(); ================================================ FILE: class-switcher-interface.php ================================================ setup( 'babble-switcher-interface', 'plugin' ); $this->add_action( 'personal_options' ); } public function personal_options( WP_User $user ) { $langs = bbl_get_active_langs(); $current = bbl_get_current_interface_lang_code(); if ( empty( $langs ) ) return; $vars = compact( 'langs', 'current' ); $this->render_admin( 'switcher-interface.php', $vars ); } } global $bbl_switcher_interface; $bbl_switcher_interface = new Babble_Switcher_Interface; ================================================ FILE: class-taxonomy.php ================================================ 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( 'body_class', null, null, 2 ); $this->add_filter( 'taxonomy_template' ); $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', array(), array( 'rewrite' => false, 'public' => false, 'show_ui' => false, '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; } /** * Hooks the WP body_class filter to add classes to the * body element. * * @param array $classes An array of class strings, poss with some indexes containing more than one space separated class * @param string|array $class One or more classes which have been added to the class list. * @return array An array of class strings, poss with some indexes containing more than one space separated class **/ public function body_class( array $classes, $class ) { if ( is_tax() ) { $taxonomy = get_queried_object(); $base_taxonomy = bbl_get_term_in_lang( get_queried_object(), $taxonomy->taxonomy, bbl_get_default_lang_code() ); if ( 'category' == $base_taxonomy->taxonomy ) { $classes[] = 'category'; if ( isset( $base_taxonomy->term_id ) ) { $classes[] = 'category-' . sanitize_html_class( $base_taxonomy->slug, $base_taxonomy->term_id ); $classes[] = 'category-' . $base_taxonomy->term_id; } } elseif ( 'post_tag' == $base_taxonomy->taxonomy ) { $classes[] = 'tag'; if ( isset( $base_taxonomy->term_id ) ) { $classes[] = 'tag-' . sanitize_html_class( $base_taxonomy->slug, $base_taxonomy->term_id ); $classes[] = 'tag-' . $base_taxonomy->term_id; } } else { if ( isset( $base_taxonomy->term_id ) ) { $classes[] = 'tax-' . sanitize_html_class( $base_taxonomy->taxonomy ); $classes[] = 'term-' . sanitize_html_class( $base_taxonomy->slug, $base_taxonomy->term_id ); $classes[] = 'term-' . $base_taxonomy->term_id; } } } return $classes; } /** * Hooks the WP filter taxonomy_template to deal with the shadow terms, * ensuring they use the right template. * * @param string $template Path to a template file * @return Path to a template file **/ public function taxonomy_template( $template ) { if( bbl_is_default_lang() ) { return $template; } $term = get_queried_object(); $base_taxonomy = $this->get_base_taxonomy( $term->taxonomy ); if ( 'category' == $base_taxonomy ) { if ( ! empty( $term->slug ) ) { $templates[] = "category-{$term->slug}.php"; $templates[] = "category-{$term->term_id}.php"; } $templates[] = 'category.php'; } else if ( 'post_tag' == $base_taxonomy ) { if ( ! empty( $term->slug ) ) { $templates[] = "tag-{$term->slug}.php"; $templates[] = "tag-{$term->term_id}.php"; } $templates[] = 'tag.php'; } else { if ( ! empty( $term->slug ) ) { $taxonomy = $term->taxonomy; $templates[] = "taxonomy-$taxonomy-{$term->slug}.php"; $templates[] = "taxonomy-$taxonomy.php"; } $templates[] = 'taxonomy.php'; } $template = get_query_template( 'bbl-taxonomy', $templates ); return $template; } // 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(); ================================================ FILE: class-translator.php ================================================ setup( 'babble-translator', 'plugin' ); $this->add_action( 'admin_init', 'maybe_upgrade' ); $this->version = 1; } /** * Called by admin_init, this method ensures we are all up to date and * so on. * * @return void **/ public function maybe_upgrade() { # @TODO should we amalgamate each class' version numbers into one? $option = 'bbl-translator-version'; $role_name = _x( 'Translator', 'Translator role', 'babble' ); switch ( get_option( $option, 0 ) ) { case 0: if ( !$role = get_role( 'translator' ) ) $role = add_role( 'translator', $role_name ); $role->add_cap( 'read' ); $role->add_cap( 'edit_bbl_jobs' ); $role->add_cap( 'edit_others_bbl_jobs' ); $role->add_cap( 'edit_published_bbl_jobs' ); $role->add_cap( 'edit_private_bbl_jobs' ); $role->add_cap( 'publish_bbl_jobs' ); $role->add_cap( 'delete_bbl_jobs' ); $role->add_cap( 'delete_others_bbl_jobs' ); $role->add_cap( 'delete_published_bbl_jobs' ); $role->add_cap( 'delete_private_bbl_jobs' ); update_option( $option, $this->version ); break; } } } global $bbl_translator; $bbl_translator = new Babble_translator(); ================================================ FILE: class-updates.php ================================================ setup( 'babble-updates', 'plugin' ); $this->add_action( 'plugins_loaded' ); $this->add_filter( 'euapi_plugin_handler', null, 10, 2 ); } /** * Include the EUAPI if it's not already present. */ public function plugins_loaded() { $dir = dirname( __FILE__ ); if ( !class_exists( 'EUAPI' ) ) { include_once $dir . '/external-update-api/external-update-api.php'; } register_activation_hook( $dir . '/babble.php', 'euapi_flush_transients' ); register_deactivation_hook( $dir . '/babble.php', 'euapi_flush_transients' ); } /** * Hooks into the EUAPI update mechanism and tells it to fetch Babble updates from GitHub. * * @param EUAPI_Handler|null $handler Usually null. Can be an EUAPI_Handler object if one has been set. * @param EUAPI_Item $item An EUAPI_Item for the current plugin. * @return EUAPI_Handler|null An EUAPI_Handler if we're overriding updates for this plugin, null if not. */ public function euapi_plugin_handler( EUAPI_Handler $handler = null, EUAPI_Item $item ) { if ( 'http://babbleplugin.com/' == $item->url ) { $handler = new EUAPI_Handler_GitHub( array( 'type' => $item->type, 'file' => $item->file, 'github_url' => 'https://github.com/cftp/babble', 'http' => array( 'sslverify' => false, ), ) ); } return $handler; } } global $bbl_updates; $bbl_updates = new Babble_Updates; ================================================ FILE: composer.json ================================================ { "name": "cftp/babble", "description": "A WordPress plugin to handle translating content into a variety of languages.", "license": "GPL-2.0+", "type": "wordpress-plugin", "authors": [ { "name": "Code For The People", "email": "hello@codeforthepeople.com" } ], "require": { "composer/installers": "~1.0" } } ================================================ FILE: css/jobs-admin.css ================================================ body.bbl-post-type-bbl_job .postbox-container { display: none; } body.bbl-post-type-bbl_job #poststuff #post-body { margin-right: 0; } #bbl-translation-editor .bbl-translation-property { float: left; width: 49%; } #bbl-translation-editor .bbl-translation-original { float: right; width: 49%; } #bbl-translation-editor .bbl-translation-group { border-top: 1px solid #ddd; /* FIXME: We should hook into the WP colours at this point, particularly with MP6 */ margin-top: 20px; padding-top: 15px; } #bbl-translation-editor .bbl-translation-group:first-child { border-top: none; margin-top: 0; padding-top: 0; } #bbl-translation-editor .bbl-translation-section { clear: both; /* @TODO change to clearfix */ overflow: auto; margin-bottom: 10px; } #bbl-translation-editor .bbl-translation-section-post_title, #bbl-translation-editor .bbl-translation-section-post_content { margin-bottom: 30px; } #bbl-translation-editor .bbl-translation-property textarea, #bbl-translation-editor .bbl-translation-original textarea { box-sizing: border-box; resize: vertical; height: 12em; width: 100%; margin: 0; display: block; } #bbl-translation-editor .bbl-translation-property input[type="text"], #bbl-translation-editor .bbl-translation-original input[type="text"] { box-sizing: border-box; width: 100%; margin: 0; } #bbl-translation-editor .bbl-translation-property-post_content textarea, #bbl-translation-editor .bbl-translation-original-post_content textarea { height: auto; } #bbl-translation-editor .bbl-translation-property-post_title input { width: 100%; box-sizing: border-box; padding: 3px 8px; font-size: 1.7em; line-height: 115%; margin: 0; } #bbl-translation-editor .bbl-translation-original .quicktags-toolbar input, #bbl-translation-editor .bbl-translation-original .mce-btn-group { visibility: hidden; } #bbl-translation-editor .bbl-translation-original-post_title { font-size: 1.7em; line-height: 115%; padding-top: 4px; } #bbl-translation-editor .bbl-translation-submit { width: 50%; /*float: right;*/ } #bbl-translation-editor .bbl-translation-original-term_name { padding-top: 4px; } #bbl-translation-editor .meta-box-sortables .inside { /* @TODO change to clearfix */ overflow: auto;; } #bbl-translation-editor .meta-box-sortables .inside h4 { margin: 5px 0 0; } ================================================ FILE: css/languages-options.css ================================================ table.babble_languages .column-active { width: 10%; } table.babble_languages .column-public { width: 10%; } table.babble_languages .column-language-code { width: 10%; } table.babble_languages .column-language { width: 20%; } table.babble_languages .column-display_name { width: 20%; } table.babble_languages .column-url_prefix { width: 15%; } table.babble_languages .column-text_direction { width: 15%; } .lang-rtl { direction: rtl !important; /* Forgive me this one !important, please */ text-align: right; } .lang-ltr { direction: ltr !important; /* Forgive me this one !important, please */ text-align: left; } input.babble-error { background-color: #ffebe8; border-color: #c00; color: #000; } ================================================ FILE: deprecated.php ================================================ plugins|themes)/update-check/(?P| ID | Type | Status | Lang |
|---|---|---|---|
| ID | Type | Status | Lang |
Translation Group: term_id; ?> |
|||
| WARNING: Post does not exist – remove from all groups | |||
|
ID ?> edit | term_id" ); ?>">delete | term_id" ); ?>">trash | ID ) ) : ?> term_id" ); ?>">remove from group |
post_type ?> | post_status ?> | ID ) ?> |
| no posts found for this translation group | |||
No translation groups found.
The option to specify a TransID will only show up if something terminal has happened to this translation. Do not change this if you do not know what you are doing.