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

', sprintf( __( 'Babble setup: Please visit the Available Languages settings and setup your available languages and the default language.', 'babble' ), admin_url( 'options-general.php?page=babble_languages' ) ) ); } } /** * Hooks the WP admin_menu action * * @return void **/ public function admin_menu() { add_options_page( __( 'Available Languages', 'babble' ), __( 'Available Languages' , 'babble'), 'manage_options', 'babble_languages', array( $this, 'options' ) ); } /** * Hooks the load action for the options page. * * @return void **/ public function load_options() { wp_enqueue_style( 'babble_languages_options', $this->url( '/css/languages-options.css' ), null, filemtime( $this->dir( 'css/languages-options.css' ) ) ); $this->maybe_process_languages(); } // CALLBACKS // ========= /** * Callback function to provide the HTML for the "Available Languages" * options page. * * @return void **/ public function options() { // Refresh the current languages $this->parse_available_languages(); // Merge in our previously set language settings $langs = $this->merge_lang_sets( $this->available_langs, $this->lang_prefs ); // Merge in any POSTed field values foreach ( $langs as $code => & $lang ) { $lang->url_prefix = ( @ isset( $_POST[ 'url_prefix_' . $code ] ) ) ? $_POST[ "url_prefix_$code" ] : @ $lang->url_prefix; if ( ! $lang->url_prefix ) $lang->url_prefix = $lang->url_prefix; $lang->text_direction = $lang->text_direction; // This line must come after the text direction value is set $lang->input_lang_class = ( 'rtl' == $lang->text_direction ) ? 'lang-rtl' : 'lang-ltr' ; $lang->display_name = ( @ isset( $_POST[ "display_name_$code" ] ) ) ? $_POST[ "display_name_$code" ] : @ $lang->display_name; if ( ! $lang->display_name ) $lang->display_name = $lang->name; // Note any url_prefix errors $lang->url_prefix_error = ( @ $this->errors[ "url_prefix_$code" ] ) ? 'babble-error' : '0' ; // Flag the active languages $lang->active = false; if ( in_array( $code, $this->active_langs ) ) $lang->active = true; } $vars = array(); $vars[ 'langs' ] = $langs; $vars[ 'default_lang' ] = $this->default_lang; $vars[ 'active_langs' ] = $this->get_active_langs(); $this->render_admin( 'options-available-languages.php', $vars ); } // PUBLIC METHODS // ============== /** * Set the active language objects for the current site, keyed * by URL prefix. * * @return array An array of Babble language objects **/ public function set_active_langs( $lang_codes ) { $this->parse_available_languages(); error_log( "SW: WP_LANG_DIR: " . WP_LANG_DIR ); $this->active_langs = $lang_codes; } /** * Return the active language objects for the current site, keyed * by URL prefix. 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' * * @return array An array of Babble language objects **/ public function get_active_langs() { $langs = array(); foreach ( $this->active_langs as $url_prefix => $code ) $langs[ $url_prefix ] = $this->langs[ $code ]; return $langs; } /** * 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 **/ public function is_public_lang( $lang_code ) { if ( ! is_string( $lang_code ) ) throw new exception( 'Please provide a lang_code for the is_public_lang method.' ); return in_array( $lang_code, $this->public_langs ); } /** * Returns the requested language object. * * @param string $code A language code, e.g. "fr_BE" * @return object|boolean A Babble language object **/ public function get_lang( $lang_code ) { if ( ! isset( $this->langs[ $lang_code ] ) ) return false; return $this->langs[ $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|boolean A Babble language object **/ public function get_current_lang() { global $bbl_locale; return $this->get_lang( $bbl_locale->get_lang() ); } /** * Returns the default language code for this site. * * @return string A language code, e.g. "he_IL" **/ public function get_default_lang_code() { return $this->default_lang; } /** * Returns the default language for this site. * * @return object The language object for the default language **/ public function get_default_lang() { return bbl_get_lang( $this->default_lang ); } /** * Given a language code, return the URL prefix. * * @param string $code A language code, e.g. "fr_BE" * @return bool|string A URL prefix, as set by the admin when editing the lang prefs, or false if no language **/ public function get_url_prefix_from_code( $code ) { if ( ! isset( $this->langs[ $code ]->url_prefix ) ) return false; return $this->langs[ $code ]->url_prefix; } /** * Given a URL prefix, return the language code. * * @param string $code A URL prefix, e.g. "de", as set by the admin * @return bool|string A language code, e.g. "de_DE", or false if no language **/ public function get_code_from_url_prefix( $url_prefix ) { if ( ! isset( $this->active_langs[ $url_prefix ] ) ) return false; return $this->active_langs[ $url_prefix ]; } // PRIVATE/PROTECTED METHODS // ========================= /** * Merge two arrays of language objects. If a language exists in * $langs_b that doesn't in $langs_a, it will be added to the * final array. If a language has a property in both arrays, the * property value from $langs_b will overwrite the property value * in $langs_a. If a language in $langs_b has a property that * doesn't exist in $langs_a then it will be added to that * language in the final array. * * @param array $langs_a An array of language objects * @param array $langs_b An array of language objects * @return array An array of language objects **/ protected function merge_lang_sets( $langs_a, $langs_b ) { $langs = array(); foreach ( $langs_a as $code => $lang_a ) { // Langs only in A get copied from A, simple. if ( ! isset( $langs_b[ $code ] ) ) { $langs[ $code ] = $lang_a; continue; } // The properties of langs in both A & B are merged $langs[ $code ] = $lang_a; $lang_b = $langs_b[ $code ]; foreach ( $lang_b as $p => $v ) $langs[ $code ]->$p = $v; } return $langs; } /** * Checks if there is a POSTed request to process. Checks it's properly * nonced up. Processes it. Redirects if there's no errors. * * @return void **/ protected function maybe_process_languages() { if ( ! isset( $_POST[ '_babble_nonce' ] ) ) return; check_admin_referer( 'babble_lang_prefs', '_babble_nonce' ); // Now save the language preferences for all languages $lang_prefs = array(); $url_prefixes = array(); foreach ( $this->available_langs as $code => $lang ) { $lang_pref = new stdClass; $lang_pref->display_name = @ $_POST[ 'display_name_' . $code ]; $lang_pref->url_prefix = @ $_POST[ 'url_prefix_' . $code ]; // Check we don't have more than one language using the same url prefix if ( array_key_exists( $lang_pref->url_prefix, $url_prefixes ) ) { $lang_1 = $this->format_code_lang( $code ); $lang_2 = $this->format_code_lang( $url_prefixes[ $lang_pref->url_prefix ] ); $msg = sprintf( __( 'The languages "%1$s" and "%2$s" are using the same URL Prefix. Each URL prefix should be unique.', 'babble' ), $lang_1, $lang_2 ); $this->set_admin_error( $msg ); $this->errors[ 'url_prefix_' . $lang_pref->url_prefix ] = true; $this->errors[ "url_prefix_$code" ] = true; } else { $url_prefixes[ $lang_pref->url_prefix ] = $code; } $lang_prefs[ $code ] = $lang_pref; } error_log( "SW: Available langs: " . print_r( $this->available_langs, true ) ); error_log( "SW: Lang prefs: " . print_r( $lang_prefs, true ) ); // Now save the active languages, i.e. the selected languages if ( ! $this->errors ) { $langs = $this->merge_lang_sets( $this->available_langs, $this->lang_prefs ); $active_langs = array(); foreach ( (array) @ $_POST[ 'active_langs' ] as $code ) $active_langs[ $langs[ $code ]->url_prefix ] = $code; if ( count( $active_langs ) < 2 ) { $this->set_admin_error( __( 'You must set at least two languages as active.', 'babble' ) ); } else { $this->active_langs = $active_langs; $this->update_option( 'active_langs', $this->active_langs ); $this->langs = $langs; $this->update_option( 'langs', $this->langs ); } if ( ! isset( $_POST[ 'public_langs' ] ) ) { $this->set_admin_error( __( 'You must set at least your default language as public.', 'babble' ) ); } else { $public_langs = (array) $_POST[ 'public_langs' ]; if ( ! in_array( @ $_POST[ 'default_lang' ], $public_langs ) ) $this->set_admin_error( __( 'You must set your default language as public.', 'babble' ) ); } } // Finish up, redirecting if we're all OK if ( ! $this->errors ) { // Save the public languages $this->update_option( 'public_langs', $public_langs ); // First the default language $default_lang = @ $_POST[ 'default_lang' ]; $this->update_option( 'default_lang', $default_lang ); // Now the prefs $this->update_option( 'lang_prefs', $lang_prefs ); // Now set a reassuring message and redirect back to the clean settings page $this->set_admin_notice( __( 'Your language settings have been saved.', 'babble' ) ); $url = admin_url( 'options-general.php?page=babble_languages' ); wp_redirect( $url ); exit; } } /** * Parse the files in wp-content/languages and work out what * languages we've got available. Populates self::available_langs * with an array of language objects which look like: * 'ar' => * object(stdClass) * public 'name' => string 'Arabic' * public 'code' => string 'ar' * public 'url_prefix' => string 'ar' * public 'text_direction' => string 'rtl' * * @return void **/ protected function parse_available_languages() { unset( $this->available_langs ); $this->available_langs = array(); foreach ( get_available_languages() as $lang_code ) { list( $prefix ) = explode( '_', $lang_code ); $lang = array( 'name' => $this->format_code_lang( $prefix ), 'code' => $lang_code, 'url_prefix' => $prefix, 'text_direction' => $this->is_rtl( $lang_code ), ); // Cast to an object, in case we want to start using actual classes // at some point in the future. $this->available_langs[ $lang_code ] = (object) $lang; } // Add in US English, which is the default on WordPress and has no language files $en = new stdClass; $en->name = 'English (US)'; $en->code = 'en_US'; $en->url_prefix = 'en'; $en->text_direction = 'ltr'; $this->available_langs[ 'en_US' ] = $en; $this->available_langs = apply_filters( 'bbl_available_langs', $this->available_langs ); ksort( $this->available_langs ); $this->update_option( 'available_langs', $this->available_langs ); } /** * Parse (DON'T require or include) the [lang_code].php locale file in the languages * directory to work if the specified language is right to left. (We can't include or * require because it may contain function names which clash with other locale files.) * * @param string $lang The language code to retrieve RTL info for * @return bool True if the language is RTL **/ protected function is_rtl( $lang ) { $locale_file = WP_LANG_DIR . "/$lang.php"; if ( ( 0 === validate_file( $lang ) ) && is_readable( $locale_file ) ) { $locale_file_code = file_get_contents( $locale_file ); // Regex to find something looking like: $text_direction = 'rtl'; return ( (bool) preg_match( '/\$text_direction\s?=\s?[\'|"]rtl[\'|"]\s?;/i', $locale_file_code ) ) ? 'rtl' : 'ltr'; } return 'ltr'; } /** * Return the language name for the provided language code. * * This method is an identical copy of format_code_lang * in wp-admin/includes/ms.php which is only available on Multisite. * * @FIXME: We end up with a load of anglicised names, which doesn't seem super-friendly, internationally speaking. * * @see format_code_lang() * * @param string $lang_short The language short code, e.g. 'en' (not 'en_GB') * @return string The language name, e.g. 'English' **/ protected function format_code_lang( $code ) { $code = strtolower( substr( $code, 0, 2 ) ); $lang_codes = array( 'aa' => 'Afar', 'ab' => 'Abkhazian', 'af' => 'Afrikaans', 'ak' => 'Akan', 'sq' => 'Albanian', 'am' => 'Amharic', 'ar' => 'Arabic', 'an' => 'Aragonese', 'hy' => 'Armenian', 'as' => 'Assamese', 'av' => 'Avaric', 'ae' => 'Avestan', 'ay' => 'Aymara', 'az' => 'Azerbaijani', 'ba' => 'Bashkir', 'bm' => 'Bambara', 'eu' => 'Basque', 'be' => 'Belarusian', 'bn' => 'Bengali', 'bh' => 'Bihari', 'bi' => 'Bislama', 'bs' => 'Bosnian', 'br' => 'Breton', 'bg' => 'Bulgarian', 'my' => 'Burmese', 'ca' => 'Catalan; Valencian', 'ch' => 'Chamorro', 'ce' => 'Chechen', 'zh' => 'Chinese', 'cu' => 'Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic', 'cv' => 'Chuvash', 'kw' => 'Cornish', 'co' => 'Corsican', 'cr' => 'Cree', 'cs' => 'Czech', 'da' => 'Danish', 'dv' => 'Divehi; Dhivehi; Maldivian', 'nl' => 'Dutch; Flemish', 'dz' => 'Dzongkha', 'en' => 'English', 'eo' => 'Esperanto', 'et' => 'Estonian', 'ee' => 'Ewe', 'fo' => 'Faroese', 'fj' => 'Fijjian', 'fi' => 'Finnish', 'fr' => 'French', 'fy' => 'Western Frisian', 'ff' => 'Fulah', 'ka' => 'Georgian', 'de' => 'German', 'gd' => 'Gaelic; Scottish Gaelic', 'ga' => 'Irish', 'gl' => 'Galician', 'gv' => 'Manx', 'el' => 'Greek, Modern', 'gn' => 'Guarani', 'gu' => 'Gujarati', 'ht' => 'Haitian; Haitian Creole', 'ha' => 'Hausa', 'he' => 'Hebrew', 'hz' => 'Herero', 'hi' => 'Hindi', 'ho' => 'Hiri Motu', 'hu' => 'Hungarian', 'ig' => 'Igbo', 'is' => 'Icelandic', 'io' => 'Ido', 'ii' => 'Sichuan Yi', 'iu' => 'Inuktitut', 'ie' => 'Interlingue', 'ia' => 'Interlingua (International Auxiliary Language Association)', 'id' => 'Indonesian', 'ik' => 'Inupiaq', 'it' => 'Italian', 'jv' => 'Javanese', 'ja' => 'Japanese', 'kl' => 'Kalaallisut; Greenlandic', 'kn' => 'Kannada', 'ks' => 'Kashmiri', 'kr' => 'Kanuri', 'kk' => 'Kazakh', 'km' => 'Central Khmer', 'ki' => 'Kikuyu; Gikuyu', 'rw' => 'Kinyarwanda', 'ky' => 'Kirghiz; Kyrgyz', 'kv' => 'Komi', 'kg' => 'Kongo', 'ko' => 'Korean', 'kj' => 'Kuanyama; Kwanyama', 'ku' => 'Kurdish', 'lo' => 'Lao', 'la' => 'Latin', 'lv' => 'Latvian', 'li' => 'Limburgan; Limburger; Limburgish', 'ln' => 'Lingala', 'lt' => 'Lithuanian', 'lb' => 'Luxembourgish; Letzeburgesch', 'lu' => 'Luba-Katanga', 'lg' => 'Ganda', 'mk' => 'Macedonian', 'mh' => 'Marshallese', 'ml' => 'Malayalam', 'mi' => 'Maori', 'mr' => 'Marathi', 'ms' => 'Malay', 'mg' => 'Malagasy', 'mt' => 'Maltese', 'mo' => 'Moldavian', 'mn' => 'Mongolian', 'na' => 'Nauru', 'nv' => 'Navajo; Navaho', 'nr' => 'Ndebele, South; South Ndebele', 'nd' => 'Ndebele, North; North Ndebele', 'ng' => 'Ndonga', 'ne' => 'Nepali', 'nn' => 'Norwegian Nynorsk; Nynorsk, Norwegian', 'nb' => 'Bokmål, Norwegian, Norwegian Bokmål', 'no' => 'Norwegian', 'ny' => 'Chichewa; Chewa; Nyanja', 'oc' => 'Occitan, Provençal', 'oj' => 'Ojibwa', 'or' => 'Oriya', 'om' => 'Oromo', 'os' => 'Ossetian; Ossetic', 'pa' => 'Panjabi; Punjabi', 'fa' => 'Persian', 'pi' => 'Pali', 'pl' => 'Polish', 'pt' => 'Portuguese', 'ps' => 'Pushto', 'qu' => 'Quechua', 'rm' => 'Romansh', 'ro' => 'Romanian', 'rn' => 'Rundi', 'ru' => 'Russian', 'sg' => 'Sango', 'sa' => 'Sanskrit', 'sr' => 'Serbian', 'hr' => 'Croatian', 'si' => 'Sinhala; Sinhalese', 'sk' => 'Slovak', 'sl' => 'Slovenian', 'se' => 'Northern Sami', 'sm' => 'Samoan', 'sn' => 'Shona', 'sd' => 'Sindhi', 'so' => 'Somali', 'st' => 'Sotho, Southern', 'es' => 'Spanish; Castilian', 'sc' => 'Sardinian', 'ss' => 'Swati', 'su' => 'Sundanese', 'sw' => 'Swahili', 'sv' => 'Swedish', 'ty' => 'Tahitian', 'ta' => 'Tamil', 'tt' => 'Tatar', 'te' => 'Telugu', 'tg' => 'Tajik', 'tl' => 'Tagalog', 'th' => 'Thai', 'bo' => 'Tibetan', 'ti' => 'Tigrinya', 'to' => 'Tonga (Tonga Islands)', 'tn' => 'Tswana', 'ts' => 'Tsonga', 'tk' => 'Turkmen', 'tr' => 'Turkish', 'tw' => 'Twi', 'ug' => 'Uighur; Uyghur', 'uk' => 'Ukrainian', 'ur' => 'Urdu', 'uz' => 'Uzbek', 've' => 'Venda', 'vi' => 'Vietnamese', 'vo' => 'Volapük', 'cy' => 'Welsh','wa' => 'Walloon','wo' => 'Wolof', 'xh' => 'Xhosa', 'yi' => 'Yiddish', 'yo' => 'Yoruba', 'za' => 'Zhuang; Chuang', 'zu' => 'Zulu' ); $lang_codes = apply_filters( 'lang_codes', $lang_codes, $code ); $lang_codes = apply_filters( 'bbl_lang_codes', $lang_codes, $code ); return strtr( $code, $lang_codes ); } /** * Setup some initial language data, so the user's site doesn't immediately * fail when the plugin is activated. * * @return void **/ protected function set_defaults() { $locale = get_option( 'WPLANG' ); if ( empty( $locale ) and is_multisite() ) { $locale = get_site_option( 'WPLANG' ); } if ( empty( $locale ) and defined( 'WPLANG' ) ) { // The WPLANG constant is deprecated since WordPress 4.0. $locale = WPLANG; } if ( empty( $locale ) ) { $locale = 'en_US'; } $url_prefix = strtolower( substr( $locale, 0, 2 ) ); $this->active_langs = array( $url_prefix => $locale ); $this->langs = array( $locale => $this->available_langs[ $locale ] ); $this->langs[ $locale ]->url_prefix = $url_prefix; $this->langs[ $locale ]->display_name = $this->langs[ $locale ]->name; $this->default_lang = $locale; $this->public_langs = array( $locale ); } } global $bbl_languages; $bbl_languages = new Babble_Languages(); ================================================ FILE: class-locale.php ================================================ content_lang_cookie = $wpdb->prefix . '_bbl_content_lang_' . COOKIEHASH; $this->interface_lang_cookie = $wpdb->prefix . '_bbl_interface_lang_' . COOKIEHASH; } /** * Hooks the WP admin_init action * * @return void **/ public function admin_init() { add_filter( 'home_url', array( $this, 'home_url' ), null, 2 ); $this->maybe_update(); $this->maybe_set_cookie_content_lang(); $this->maybe_set_cookie_interface_lang(); } /** * Hooks the WP admin_notices action to warn the admin * if the permalinks aren't pretty enough. * * @return void **/ public function admin_notices() { if ( ! get_option( 'permalink_structure' ) ) { printf( '

%s

', sprintf( __( 'Babble problem: Fancy permalinks are disabled. Please enable them in order to have language prefixed URLs work correctly.', 'babble' ), admin_url( '/options-permalink.php' ) ) ); } } /** * Ensure we keep the standard WP rewrite rules. * * @param string $rules The mod_rewrite rules block generated by WP * @return string A mod_rewrite rules block **/ public function mod_rewrite_rules( $rules ) { global $wp_rewrite; if ( $this->no_recursion ) return $rules; $this->no_recursion = true; // We need the WP_Rewrite mod_rewrite_rules method to run // home_url without a lang query var set, or it generates // an inaccurate RewriteBase and last RewriteRule. remove_filter( 'home_url', array( $this, 'home_url' ), null, 2 ); $rules = $wp_rewrite->mod_rewrite_rules(); add_filter( 'home_url', array( $this, 'home_url' ), null, 2 ); $this->no_recursion = false; return $rules; } /** * Hooks the WP pre_update_option_rewrite_rules filter to add * a prefix to the URL to pick up the virtual sub-dir specifying * the language. The redirect portion can and should remain perfectly * ignorant of it though, as we change it in parse_request. * * @param array $langs The language codes * @return array An array of language codes utilised for this site. **/ public function internal_rewrite_rules_filter( $rules ){ global $wp_rewrite; // Some rules need to be at the root of the site, without a // language prefix, e.g. http://www.example.com/humans.txt. // The following filter allows plugin and theme devs to add // to this list of site root level URLs which are untranslated. $non_translated_rewrite_rules = apply_filters( 'bbl_non_translated_queries', array( 'humans\.txt$', 'robots\.txt$', ) ); foreach( (array) $rules as $regex => $query ) { if ( in_array( $regex, $non_translated_rewrite_rules ) ) { $new_rules[ $regex ] = $query; continue; } if ( substr( $regex, 0, 1 ) == '^' ) { $new_rules[ '^[a-zA-Z_]+/' . substr( $regex, 1 ) ] = $query; } else { $new_rules[ '[a-zA-Z_]+/' . $regex ] = $query; } } // The WP robots.txt rewrite rule will not have worked, as the // code objects to the language prefix. Here we add it in again. $hooked = false; if ( has_filter( 'home_url' ) ) { remove_filter( 'home_url', array( $this, 'home_url' ), null, 2 ); $hooked = true; } $home_path = parse_url( home_url() ); if ( $hooked ) { add_filter( 'home_url', array( $this, 'home_url' ), null, 2 ); } if ( empty( $home_path['path'] ) || '/' == $home_path['path'] ) { $new_rules[ 'robots\.txt$' ] = $wp_rewrite->index . '?robots=1'; } return $new_rules; } /** * Hooks the WP locale filter to switch locales whenever we gosh darned want. * * @param string $locale The locale * @return string The locale **/ public function set_locale( $locale ) { // Deal with the special case of wp-comments-post.php if ( false !== stristr( $_SERVER[ 'REQUEST_URI' ], 'wp-comments-post.php' ) ) { // @TODO we should be able to hook into an action here (pre_comment_post) rather than looking at the URL. if ( $comment_post_ID = ( isset( $_POST[ 'comment_post_ID' ] ) ) ? (int) $_POST[ 'comment_post_ID' ] : false ) { if ( ! isset( $this->content_lang ) ) { $this->set_content_lang( bbl_get_post_lang_code( $comment_post_ID ) ); } return $this->content_lang; } } if ( is_admin() ) { if ( isset( $this->interface_lang ) ) { return $this->interface_lang; } } else { if ( isset( $this->content_lang ) ) { return $this->content_lang; } } // $current_user = wp_get_current_user(); if ( $lang = $this->get_cookie_interface_lang() ) { $this->set_interface_lang( $lang ); } // $current_user = wp_get_current_user(); if ( $lang = $this->get_cookie_content_lang() ) { $this->set_content_lang( $lang ); } if ( is_admin() ) { // @FIXME: At this point a mischievous XSS "attack" could set a user's admin area language for them if ( isset( $_POST[ 'interface_lang' ] ) ) { $this->set_interface_lang( $_POST[ 'interface_lang' ] ); } // @FIXME: At this point a mischievous XSS "attack" could set a user's content language for them if ( isset( $_GET[ 'lang' ] ) ) { $this->set_content_lang( $_GET[ 'lang' ] ); } } else { // Front end // @FIXME: Should probably check the available languages here if ( preg_match( $this->lang_regex, $this->get_request_string(), $matches ) ) $this->set_content_lang_from_prefix( $matches[ 0 ] ); } if ( ! isset( $this->content_lang ) || ! $this->content_lang ) $this->set_content_lang( bbl_get_default_lang_code() ); if ( ! isset( $this->interface_lang ) || ! $this->interface_lang ) $this->set_interface_lang( bbl_get_default_lang_code() ); if ( is_admin() ) return $this->interface_lang; else return $this->content_lang; } /** * Hooks the WP parse_request action * * FIXME: Should I be extending and replacing the WP class? * * @param WP $wp The WP object, passed by reference (so no need to return) * @return void **/ public function parse_request_early( WP $wp ) { // If this is the site root, redirect to default language homepage if ( ! $wp->request ) { remove_filter( 'home_url', array( $this, 'home_url' ), null, 2 ); wp_redirect( home_url( bbl_get_default_lang_url_prefix() ) ); exit; } // Otherwise, simply set the lang for this request $wp->query_vars[ 'lang' ] = $this->content_lang; $wp->query_vars[ 'lang_url_prefix' ] = $this->url_prefix; } /** * Hooks the WP query_vars filter to add the home_url filter. * * @param array $query_vars An array of the public query vars * @return array An array of the public query vars **/ public function query_vars( array $query_vars ) { # @TODO why is this here? add_filter( 'home_url', array( $this, 'home_url' ), null, 2 ); return array_merge( $query_vars, array( 'lang', 'lang_url_prefix' ) ); } /** * Hooks the WP pre_comment_on_post action to add the * home_url filter. * * @return void **/ public function pre_comment_on_post() { # @TODO why is this here? add_filter( 'home_url', array( $this, 'home_url' ), null, 2 ); } /** * Hooks the WP home_url action * * Hackity hack: this function is attached with add_filter within * the query_vars filter and the pre_comment_on_post action. * @TODO: Can't remember why this is attached like this… investigate. * * @param string $url The URL * @param string $path The path * @param string $orig_scheme The original scheme * @param int $blog_id The ID of the blog * @return string The URL **/ public function home_url( $url, $path ) { $base_url = get_option( 'home' ); $url = trailingslashit( $base_url ) . $this->url_prefix; if ( $path && is_string( $path ) ) $url .= '/' . ltrim( $path, '/' ); return $url; } /** * Hooks the WP body_class filter to add some language specific classes. * * @param array $classes The body classes * @return array The body classes **/ public function body_class( array $classes ) { $lang = bbl_get_current_lang(); $classes[] = 'bbl-' . $lang->text_direction; # @TODO I don't think this class should be included: $classes[] = 'bbl-' . sanitize_title( $lang->name ); $classes[] = 'bbl-' . sanitize_title( $lang->url_prefix ); $classes[] = 'bbl-' . sanitize_title( $lang->code ); # @TODO I don't think this class should be included: $classes[] = 'bbl-' . sanitize_title( $lang->display_name ); return $classes; } /** * Hooks the WP post_class filter to add some language specific classes. * * @param array $classes The post classes * @param array $class One or more classes which have been added to the class list. * @param int $post_id The ID of the post we're providing classes for * @return array The body classes **/ public function post_class( array $classes, $class, $post_id ) { $post = get_post( $post_id ); $post_lang_code = bbl_get_post_lang_code( $post ); $lang = bbl_get_lang( $post_lang_code ); if ( self::use_default_text_direction( $post ) ) { $default_lang = bbl_get_default_lang(); $classes[] = 'bbl-post-' . $default_lang->text_direction; } else { $classes[] = 'bbl-post-' . $lang->text_direction; } # @TODO I don't think this class should be included: $classes[] = 'bbl-post-' . sanitize_title( $lang->name ); $classes[] = 'bbl-post-' . sanitize_title( $lang->url_prefix ); $classes[] = 'bbl-post-' . sanitize_title( $lang->code ); # @TODO I don't think this class should be included: $classes[] = 'bbl-post-' . sanitize_title( $lang->display_name ); return $classes; } // Public Methods // -------------- /** * Return whether the post should use the default language's text direction or not. * * @param WP_Post $post The post object. * @return bool True if the post should use the default language text direction. False if not. */ public static function use_default_text_direction( WP_Post $post ) { if ( get_post_meta( $post->ID, '_bbl_default_text_direction', true ) ) { return true; } else if ( empty( $post->post_content ) ) { return true; } return false; } /** * Get the current (content) lang for this class, which is also the * current lang in the Query Vars. * * @TODO deprecate * * @return string **/ public function get_lang() { return $this->get_content_lang(); } /** * Get the current content lang for this class, which is also the * current lang in the Query Vars. * * @return string **/ public function get_content_lang() { return $this->content_lang; } /** * Get the current interface lang for this class. * * @return string **/ public function get_interface_lang() { return $this->interface_lang; } /** * Set the current (content) lang for this class, and in Query Vars. * * @param string $lang The language code to switch to * @return void **/ public function switch_to_lang( $lang ) { // @FIXME: Need to validate language here if ( ! is_array( $this->lang_stack ) ) $this->lang_stack = array(); $this->lang_stack[] = $this->content_lang; $this->set_content_lang( $lang ); set_query_var( 'lang', $this->content_lang ); } /** * Restore the previous lang from the switched stack. * * @return void **/ public function restore_lang() { $this->set_content_lang( array_pop( $this->lang_stack ) ); set_query_var( 'lang', $this->content_lang ); } // Non-public Methods // ------------------ /** * Set the content language code and URL prefix for any * subsequent requests. * * @FIXME: Currently we don't check that the language is valid * * @param string $code A language code * @return void **/ protected function set_content_lang( $code ) { global $bbl_languages; // Set the content language in the application $this->content_lang = $code; $this->url_prefix = $bbl_languages->get_url_prefix_from_code( $this->content_lang ); } /** * Set the interace language code. * * @FIXME: Currently we don't check that the language is valid * * @param string $code A language code * @return void **/ protected function set_interface_lang( $code ) { // Set the interface language in the application $this->interface_lang = $code; } /** * Set the content language for the URL prefix provided. * * @param string $url_prefix A URL prefix, e.g. "de" * @return void **/ protected function set_content_lang_from_prefix( $url_prefix ) { global $bbl_languages; $this->set_content_lang( bbl_get_lang_from_prefix( $url_prefix ) ); } /** * Get the request string for the request, using code copied * straight from WP->parse_request. * * @return string The request **/ protected function get_request_string() { global $wp_rewrite; // @FIXME: Copying a huge hunk of code from WP->parse_request here, feels ugly. // START: Huge hunk of WP->parse_request if ( isset($_SERVER['PATH_INFO']) ) $pathinfo = $_SERVER['PATH_INFO']; else $pathinfo = ''; $pathinfo_array = explode('?', $pathinfo); $pathinfo = str_replace("%", "%25", $pathinfo_array[0]); $req_uri = $_SERVER['REQUEST_URI']; $req_uri_array = explode('?', $req_uri); $req_uri = $req_uri_array[0]; $self = $_SERVER['PHP_SELF']; $home_path = parse_url(home_url()); if ( isset($home_path['path']) ) $home_path = $home_path['path']; else $home_path = ''; $home_path = trim($home_path, '/'); // Trim path info from the end and the leading home path from the // front. For path info requests, this leaves us with the requesting // filename, if any. For 404 requests, this leaves us with the // requested permalink. $req_uri = str_replace($pathinfo, '', $req_uri); $req_uri = trim($req_uri, '/'); $req_uri = preg_replace("|^$home_path|", '', $req_uri); $req_uri = trim($req_uri, '/'); $pathinfo = trim($pathinfo, '/'); $pathinfo = preg_replace("|^$home_path|", '', $pathinfo); $pathinfo = trim($pathinfo, '/'); $self = trim($self, '/'); $self = preg_replace("|^$home_path|", '', $self); $self = trim($self, '/'); // The requested permalink is in $pathinfo for path info requests and // $req_uri for other requests. if ( ! empty($pathinfo) && !preg_match('|^.*' . $wp_rewrite->index . '$|', $pathinfo) ) { $request = $pathinfo; } else { // If the request uri is the index, blank it out so that we don't try to match it against a rule. if ( is_object( $wp_rewrite ) && $req_uri == $wp_rewrite->index ) $req_uri = ''; $request = $req_uri; } // END: Huge hunk of WP->parse_request return $request; } /** * Sets the content language cookie where necessary. We are using cookies * as we cannot get userdata at the set_locale action, which is where * we need to read the user's language. * * @return void **/ protected function maybe_set_cookie_content_lang() { // @FIXME: At this point a mischievous XSS "attack" could set a user's content language for them if ( $requested_lang = ( isset( $_GET[ 'lang' ] ) ) ? $_GET[ 'lang' ] : false ) setcookie( $this->content_lang_cookie, $requested_lang, time() + 31536000, COOKIEPATH, COOKIE_DOMAIN); } /** * Sets the admin language cookie where necessary. We are using cookies * as we cannot get userdata at the set_locale action, which is where * we need to read the user's language. * * @return void **/ protected function maybe_set_cookie_interface_lang() { // @FIXME: At this point a mischievous XSS "attack" could set a user's admin area language for them if ( $requested_lang = ( isset( $_POST[ 'interface_lang' ] ) ) ? $_POST[ 'interface_lang' ] : false ) setcookie( $this->interface_lang_cookie, $requested_lang, time() + 31536000, COOKIEPATH, COOKIE_DOMAIN); } /** * Gets the language code from the content language cookie. * * @TODO: This should use a cookie that's keyed to the current user when present * * @return string A language code **/ protected function get_cookie_content_lang() { return ( isset( $_COOKIE[ $this->content_lang_cookie ] ) ) ? $_COOKIE[ $this->content_lang_cookie ] : ''; } /** * Gets the language code from the interface language cookie. * * @TODO: This should use a cookie that's keyed to the current user when present * * @return string A language code **/ protected function get_cookie_interface_lang() { return ( isset( $_COOKIE[ $this->interface_lang_cookie] ) ) ? $_COOKIE[ $this->interface_lang_cookie ] : ''; } /** * Checks the DB structure is up to date. * * @return void * @author Simon Wheatley **/ protected function maybe_update() { global $wpdb; $option_name = 'bbl-locale-version'; $version = get_option( $option_name, 0 ); if ( $this->version == $version ) return; if ( $version < 1 ) { error_log( "Babble Locale: Flushing rewrite rules" ); flush_rewrite_rules(); } error_log( "Babble Locale: Done updates" ); update_option( $option_name, $this->version ); } } global $bbl_locale; $bbl_locale = new Babble_Locale(); ================================================ FILE: class-meta.php ================================================ post = $post; $this->meta_key = $meta_key; $this->meta_title = $meta_title; $this->meta_value = get_post_meta( $this->post->ID, $this->meta_key, true ); $this->args = $args; } abstract public function get_input( $name, $value ); public function get_output() { return esc_html( $this->get_value() ); } public function get_title() { return $this->meta_title; } public function get_value() { return $this->meta_value; } public function get_key() { return $this->meta_key; } public function update( $value, WP_Post $job ) { return $value; } } class Babble_Meta_Field_Text extends Babble_Meta_Field { public function get_input( $name, $value ) { return sprintf( '', esc_attr( $name ), esc_attr( $value ) ); } } class Babble_Meta_Field_Textarea extends Babble_Meta_Field { public function get_input( $name, $value ) { return sprintf( '', esc_attr( $name ), esc_textarea( $value ) ); } public function get_output() { return nl2br( esc_html( $this->get_value() ) ); } } class Babble_Meta_Field_Editor extends Babble_Meta_Field { public function get_input( $name, $value ) { $args = array( 'textarea_name' => $name, ); # see _WP_Editors()::parse_settings() for available editor settings if ( !empty( $this->args['editor_settings'] ) ) { $args = array_merge( $args, $this->args['editor_settings'] ); } ob_start(); wp_editor( $value, sprintf( 'meta-input-%s', $this->get_key() ), $args ); return ob_get_clean(); } public function get_output() { $args = array( 'textarea_name' => 'doesnotmatter', 'media_buttons' => false, 'tinymce' => array( 'readonly' => 1, ), ); ob_start(); wp_editor( $this->get_value(), sprintf( 'meta-output-%s', $this->get_key() ), $args ); return ob_get_clean(); } } ================================================ FILE: class-plugin.php ================================================ Display Rendering * * The class uses a similar technique to Ruby On Rails views, whereby the display HTML is kept * in a separate directory and file from the main code. A display is 'rendered' (sent to the browser) * or 'captured' (returned to the calling function). * * Template files are separated into two areas: admin and user. Admin templates are only for display in * the WordPress admin interface, while user templates are typically for display on the site (although neither * of these are enforced). All templates are PHP code, but are referred to without .php extension. * * The reason for this separation is that one golden rule of plugin creation is that someone will * always want to change the formatting and style of your output. Rather than forcing them to * modify the plugin (bad), or modify files within the plugin (equally bad), the class allows * user templates to be overridden with files contained within the theme. * * An additional benefit is that it leads to code re-use, especially with regards to Ajax (i.e. * your display code can be called from many locations) * * @package Babble * @author Simon Wheatley * @copyright Copyright (C) Simon Wheatley (except where noted) **/ class Babble_Plugin { /** * The name of this plugin * * @var string **/ protected $name; /** * The filepath to the directory containing this plugin * * @var string **/ protected $dir; /** * The URL for the directory containing this plugin * * @var string **/ protected $url; /** * Useful for switching between debug and compressed scripts. * * @var string **/ protected $suffix; /** * Records the type of this class, either 'plugin' or 'theme'. * * @var string **/ protected $type; /** * Note the name of the function to call when the theme is activated. * * @var string **/ protected $theme_activation_function; /** * Initiate! * * @return void * @author Simon Wheatley **/ public function setup( $name = '', $type = null ) { if ( ! $name ) throw new exception( "Please pass the name parameter into the setup method." ); $this->name = $name; // Attempt to handle a Windows $ds = ( defined( 'DIRECTORY_SEPARATOR' ) ) ? DIRECTORY_SEPARATOR : '\\'; $file = str_replace( $ds, '/', __FILE__ ); $plugins_dir = str_replace( $ds, '/', dirname( __FILE__ ) ); // Setup the dir and url for this plugin/theme if ( 'theme' == $type ) { // This is a theme $this->type = 'theme'; $this->dir = get_stylesheet_directory(); $this->url = get_stylesheet_directory_uri(); } elseif ( stripos( $file, $plugins_dir ) !== false || 'plugin' == $type ) { // This is a plugin $this->type = 'plugin'; // Allow someone to override the assumptions we're making here about where // the plugin is held. For example, if this plugin is included as part of // the files for a theme, in wp-content/themes/[your theme]/plugins/ then // you could hook `sil_plugins_dir` and `sil_plugins_url` to correct // our assumptions. // N.B. Because this code is running when the file is required, other plugins // may not be loaded and able to hook these filters! $plugins_dir = apply_filters( 'sil_plugins_dir', $plugins_dir, $this->name ); $plugins_url = apply_filters( 'sil_plugins_url', plugins_url( '', __FILE__ ), $this->name ); $this->dir = trailingslashit( $plugins_dir ); $this->url = trailingslashit( $plugins_url ); } else { // WTF? error_log( 'PLUGIN/THEME ERROR: Cannot find ' . $plugins_dir . ' or "themes" in ' . $file ); } // Suffix for enqueuing $this->suffix = defined('SCRIPT_DEBUG') && SCRIPT_DEBUG ? '.dev' : ''; if ( is_admin() ) { // Admin notices $this->add_action( 'admin_notices', '_admin_notices' ); } $this->add_action( 'init', 'load_locale' ); } /** * Hook called to change the locale directory. * * @return void * @author © John Godley **/ function load_locale() { // Here we manually fudge the plugin locale as WP doesnt allow many options $locale = get_locale(); if( empty( $locale ) ) $locale = 'en_US'; $mofile = $this->dir( "/locale/$locale.mo" ); load_textdomain( $this->name, $mofile ); } /** * Register a WordPress action and map it back to the calling object * * @param string $action Name of the action * @param string $function Function name (optional) * @param int $priority WordPress priority (optional) * @param int $accepted_args Number of arguments the function accepts (optional) * @return void * @author © John Godley **/ function add_action ($action, $function = '', $priority = 10, $accepted_args = 1) { if ( $priority === null ) $priority = 10; add_action ($action, array ($this, $function == '' ? $action : $function), $priority, $accepted_args); } /** * Register a WordPress filter and map it back to the calling object * * @param string $action Name of the action * @param string $function Function name (optional) * @param int $priority WordPress priority (optional) * @param int $accepted_args Number of arguments the function accepts (optional) * @return void * @author © John Godley **/ function add_filter ($filter, $function = '', $priority = 10, $accepted_args = 1) { add_filter ($filter, array ($this, $function == '' ? $filter : $function), $priority, $accepted_args); } /** * De-register a WordPress filter and map it back to the calling object * * @param string $action Name of the action * @param string $function Function name (optional) * @param int $priority WordPress priority (optional) * @param int $accepted_args Number of arguments the function accepts (optional) * @return void * @author © John Godley **/ function remove_filter ($filter, $function = '', $priority = 10, $accepted_args = 1) { remove_filter ($filter, array ($this, $function == '' ? $filter : $function), $priority, $accepted_args); } /** * Special activation function that takes into account the plugin directory * * @param string $pluginfile The plugin file location (i.e. __FILE__) * @param string $function Optional function name, or default to 'activate' * @return void * @author © John Godley **/ function register_activation ( $pluginfile = __FILE__, $function = '' ) { if ( $this->type == 'plugin' ) { add_action ('activate_'.basename (dirname ($pluginfile)).'/'.basename ($pluginfile), array ($this, $function == '' ? 'activate' : $function)); } elseif ( $this->type == 'theme' ) { $this->theme_activation_function = ( $function ) ? $function : 'activate'; add_action ('load-themes.php', array ( $this, 'theme_activation' ) ); } } /** * Hack to catch theme activation. We hook the load-themes.php action, look for the * "activated" GET param and make a big fat assumption if we find it. * * @return void * @author Simon Wheatley **/ public function theme_activation() { $activated = (bool) @ $_GET[ 'activated' ]; if ( ! $activated ) return; if ( ! $this->theme_activation_function ) return; // Looks like the theme might just have been activated, call the registered function $this->{$this->theme_activation_function}(); } /** * Special deactivation function that takes into account the plugin directory * * @param string $pluginfile The plugin file location (i.e. __FILE__) * @param string $function Optional function name, or default to 'deactivate' * @return void * @author © John Godley **/ function register_deactivation ($pluginfile, $function = '') { add_action ('deactivate_'.basename (dirname ($pluginfile)).'/'.basename ($pluginfile), array ($this, $function == '' ? 'deactivate' : $function)); } /** * Renders a template, looking first for the template file in the theme directory * and afterwards in this plugin's /theme/ directory. * * @return void * @author Simon Wheatley **/ protected function render( $template_file, $vars = null ) { // Maybe override the template with our own file $template_file = $this->locate_template( $template_file ); // Ensure we have the same vars as regular WP templates global $posts, $post, $wp_did_header, $wp_did_template_redirect, $wp_query, $wp_rewrite, $wpdb, $wp_version, $wp, $id, $comment, $user_ID; if ( is_array($wp_query->query_vars) ) extract($wp_query->query_vars, EXTR_SKIP); // Plus our specific template vars if ( is_array( $vars ) ) extract( $vars ); require( $template_file ); } /** * Renders an admin template from this plugin's /templates-admin/ directory. * * @return void * @author Simon Wheatley **/ protected function render_admin( $template_file, $vars = null ) { // Plus our specific template vars if ( is_array( $vars ) ) extract( $vars ); // Try to render if ( file_exists( $this->dir( "templates-admin/$template_file" ) ) ) { require( $this->dir( "templates-admin/$template_file" ) ); } else { $msg = sprintf( __( "This plugin admin template could not be found: %s" ), $this->dir( "templates-admin/$template_file" ) ); error_log( "Plugin template error: $msg" ); echo "

$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

"; } /** * Echoes some HTML for an admin error. * * @param string $error The error * @return void * @author Simon Wheatley **/ protected function render_admin_error( $error ) { echo "

$error

"; } /** * Sets a string as an admin notice. * * @param string $msg A *localised* admin notice message * @return void * @author Simon Wheatley **/ protected function set_admin_notice( $msg ) { $notices = (array) $this->get_option( 'admin_notices' ); $notices[] = $msg; $this->update_option( 'admin_notices', $notices ); } /** * Sets a string as an admin error. * * @param string $msg A *localised* admin error message * @return void * @author Simon Wheatley **/ protected function set_admin_error( $msg ) { $errors = (array) $this->get_option( 'admin_errors' ); $errors[] = $msg; $this->update_option( 'admin_errors', $errors ); } /** * Takes a filename and attempts to find that in the designated plugin templates * folder in the theme (defaults to main theme directory, but uses a custom filter * to allow theme devs to specify a sub-folder for all plugin template files using * this system). * * Searches in the STYLESHEETPATH before TEMPLATEPATH to cope with themes which * inherit from a parent theme by just overloading one file. * * @param string $template_file A template filename to search for * @return string The path to the template file to use * @author Simon Wheatley **/ protected function locate_template( $template_file ) { $located = ''; $sub_dir = apply_filters( 'sw_plugin_tpl_dir', '' ); if ( $sub_dir ) $sub_dir = trailingslashit( $sub_dir ); // If there's a tpl in a (child theme or theme with no child) if ( file_exists( STYLESHEETPATH . "/$sub_dir" . $template_file ) ) return STYLESHEETPATH . "/$sub_dir" . $template_file; // If there's a tpl in the parent of the current child theme else if ( file_exists( TEMPLATEPATH . "/$sub_dir" . $template_file ) ) return TEMPLATEPATH . "/$sub_dir" . $template_file; // Fall back on the bundled plugin template (N.B. no filtered subfolder involved) else if ( file_exists( $this->dir( "templates/$template_file" ) ) ) return $this->dir( "templates/$template_file" ); // Oh dear. We can't find the template. $msg = sprintf( __( "This plugin template could not be found, perhaps you need to hook `sil_plugins_dir` and `sil_plugins_url`: %s" ), $this->dir( "templates/$template_file" ) ); error_log( "Template error: $msg" ); echo "

$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[0-9\.]+)/#', $url, $matches ) ) { switch ( $matches['type'] ) { case 'plugins': return $this->plugin_request( $args, floatval( $matches['version'] ) ); break; case 'themes': return $this->theme_request( $args, floatval( $matches['version'] ) ); break; } } $query = parse_url( $url, PHP_URL_QUERY ); if ( empty( $query ) ) { return $args; } parse_str( $query, $query ); if ( !isset( $query['_euapi_type'] ) or !isset( $query['_euapi_file'] ) ) { return $args; } if ( !( $handler = $this->get_handler( $query['_euapi_type'], $query['_euapi_file'] ) ) ) { return $args; } $args = array_merge( $args, $handler->config['http'] ); return $args; } /** * Filters the arguments for HTTP requests to the plugin update check API. * * Here we loop over each plugin in the update check request and remove ones for which we're * handling or excluding updates. * * @author John Blackbourn * @param array $args HTTP request arguments. * @param float $version The API request version number. * @return array Updated array of arguments. */ protected function plugin_request( array $args, $version ) { switch ( $version ) { case 1.0: _doing_it_wrong( __METHOD__, sprintf( __( 'External Update API is not compatible with version %s of the WordPress Plugin API. Please update to the latest version of WordPress.', 'euapi' ), $version ), 0.4 ); return $args; break; case 1.1: default: $plugins = json_decode( $args['body']['plugins'] ); break; } if ( ! is_object( $plugins ) or empty( $plugins->plugins ) ) { return $args; } foreach ( $plugins->plugins as $plugin => $data ) { if ( !is_object( $data ) ) { continue; } $data = get_object_vars( $data ); $item = new EUAPI_Item_Plugin( $plugin, $data ); $handler = $this->get_handler( 'plugin', $plugin, $item ); if ( null === $handler ) { continue; } if ( is_a( $handler, 'EUAPI_Handler' ) ) { $handler->item = $item; } unset( $plugins->plugins->{$plugin} ); } $args['body']['plugins'] = json_encode( $plugins ); return $args; } /** * Filters the arguments for HTTP requests to the theme update check API. * * Here we loop over each theme in the update check request and remove ones for which we're * handling or excluding updates. * * @author John Blackbourn * @param array $args HTTP request arguments. * @param float $version The API request version number. * @return array Updated array of arguments. */ protected function theme_request( array $args, $version ) { switch ( $version ) { case 1.0: _doing_it_wrong( __METHOD__, sprintf( __( 'External Update API is not compatible with version %s of the WordPress Theme API. Please update to the latest version of WordPress.', 'euapi' ), $version ), 0.4 ); return $args; break; case 1.1: default: $themes = json_decode( $args['body']['themes'] ); break; } if ( ! is_object( $themes ) or empty( $themes->themes ) ) { return $args; } foreach ( $themes->themes as $theme => $data ) { if ( !is_object( $data ) ) { continue; } $data = get_object_vars( $data ); if ( !isset( $data['ThemeURI'] ) ) { # ThemeURI is missing from $data by default for some reason $data['ThemeURI'] = wp_get_theme( $data['Template'] )->get( 'ThemeURI' ); } $item = new EUAPI_Item_Theme( $theme, $data ); $handler = $this->get_handler( 'theme', $theme, $item ); if ( null === $handler ) { continue; } if ( is_a( $handler, 'EUAPI_Handler' ) ) { $handler->item = $item; } unset( $themes->themes->{$theme} ); } $args['body']['themes'] = json_encode( $themes ); return $args; } /** * Called immediately before the plugin update check results are saved in a transient. * * We use this to fire off update checks to each of the plugins we're handling updates * for, and populate the results in the update check object. * * @author John Blackbourn * @param object $update The plugin update check object. * @return object The updated update check object. */ public function filter_update_plugins( $update ) { if ( !isset( $this->handlers['plugin'] ) ) { return $update; } return self::check( $update, $this->handlers['plugin'] ); } /** * Called immediately before the theme update check results are saved in a transient. * * We use this to fire off update checks to each of the themes we're handling updates * for, and populate the results in the update check object. * * @author John Blackbourn * @param object $update Theme update check object. * @return object Updated update check object. */ public function filter_update_themes( $update ) { if ( !isset( $this->handlers['theme'] ) ) { return $update; } return self::check( $update, $this->handlers['theme'] ); } /** * Fire off update checks for each of the handlers specified and populate the results in * the update check object. * * @author John Blackbourn * @param object $update Update check object. * @param array $handlers Handlers that we're interested in. * @return object Updated update check object. */ public static function check( $update, array $handlers ) { if ( empty( $update->checked ) ) { return $update; } foreach ( array_filter( $handlers ) as $handler ) { $handler_update = $handler->get_update(); if ( $handler_update->get_new_version() and 1 === version_compare( $handler_update->get_new_version(), $handler->get_current_version() ) ) { if ( 'plugin' == $handler->get_type() ) { $update->response[ $handler->get_file() ] = (object) $handler_update->get_data_to_store(); } else { $update->response[ $handler->get_file() ] = $handler_update->get_data_to_store(); } } } return $update; } /** * Get the update handler for the given item, if one is present. * * @author John Blackbourn * @param string $type Handler type (either 'plugin' or 'theme'). * @param string $file Item base file name. * @param EUAPI_Item|null $item Item object for the plugin/theme. Optional. * @return EUAPI_Handler|null Update handler object, or null if no update handler is present. */ public function get_handler( $type, $file, $item = null ) { if ( isset( $this->handlers[$type] ) and array_key_exists( $file, $this->handlers[$type] ) ) { return $this->handlers[$type][$file]; } if ( !$item ) { $item = self::populate_item( $type, $file ); } if ( ! is_a( $item, 'EUAPI_Item' ) ) { $handler = null; } else { $handler = apply_filters( "euapi_{$type}_handler", null, $item ); } $this->handlers[$type][$file] = $handler; return $handler; } /** * Returns the item data for a given item, typically by reading the item file header * and populating its data. * * @author John Blackbourn * @param string $type Handler type (either 'plugin' or 'theme'). * @param string $file Item base file name. * @return EUAPI_Item|null Item object or null on failure. */ protected static function populate_item( $type, $file ) { switch ( $type ) { case 'plugin': if ( $data = self::get_plugin_data( $file ) ) { return new EUAPI_Item_Plugin( $file, $data ); } break; case 'theme': if ( $data = self::get_theme_data( $file ) ) { return new EUAPI_Item_Theme( $file, $data ); } break; } return null; } /** * Get data for a plugin by reading its file header. * * @param string $file Plugin base file name. * @return array|false Array of plugin data, or false on failure. */ public static function get_plugin_data( $file ) { require_once ABSPATH . '/wp-admin/includes/plugin.php'; if ( file_exists( $plugin = WP_PLUGIN_DIR . '/' . $file ) ) { return get_plugin_data( $plugin ); } return false; } /** * Get data for a theme by reading its file header. * * @param string $file Theme directory name. * @return array|false Array of theme data, or false on failure. */ public static function get_theme_data( $file ) { $theme = wp_get_theme( $file ); if ( !$theme->exists() ) { return false; } $data = array( 'Name' => '', 'ThemeURI' => '', 'Description' => '', 'Author' => '', 'AuthorURI' => '', 'Version' => '', 'Template' => '', 'Status' => '', 'Tags' => '', 'TextDomain' => '', 'DomainPath' => '', ); foreach ( $data as $k => $v ) { $data[$k] = $theme->get( $k ); } return $data; } /** * Before the Plugin API performs an action, this short-circuit callback is fired, allowing us to override the * API method for a given action. * * Here, we override the action which fetches plugin information from the wp.org API * and return our own plugin information if necessary. * * @param bool|object $default Default return value for this request. Usually boolean false. * @param string $action API function being performed. * @param object $plugin Plugin Info API object. * @return bool|WP_Error|EUAPI_Info EUAPI Info object, WP_Error object on failure, $default if we're not interfering. */ public function filter_plugins_api( $default, $action, $plugin ) { if ( 'plugin_information' != $action ) { return $default; } if ( false === strpos( $plugin->slug, '/' ) ) { return $default; } $handler = $this->get_handler( 'plugin', $plugin->slug ); if ( ! is_a( $handler, 'EUAPI_Handler' ) ) { return $default; } return $handler->get_info(); } /** * Before the Theme API performs an action, this short-circuit callback is fired, allowing us to override the * API method for a given action. * * Here, we override the action which fetches theme information from the wp.org API * and return our own theme information if necessary. * * @param bool|object $default Default return value for this request. Usually boolean false. * @param string $action API function being performed. * @param object $theme Theme Info API object. * @return bool|WP_Error|EUAPI_Info EUAPI Info object, WP_Error object on failure, $default if we're not interfering. */ public function filter_themes_api( $default, $action, $theme ) { if ( 'theme_information' != $action ) { return $default; } $handler = $this->get_handler( 'theme', $theme->slug ); if ( ! is_a( $handler, 'EUAPI_Handler' ) ) { return $default; } return $handler->get_info(); } /** * Fetch the contents of a URL. * * @author John Blackbourn * @param string $url URL to fetch. * @param array $args Array of arguments passed to wp_remote_get(). * @return WP_Error|string WP_Error object on failure, string contents of URL body on success. */ public static function fetch( $url, array $args = array() ) { $args = array_merge( array( 'timeout' => 5 ), $args ); $response = wp_remote_get( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $message = wp_remote_retrieve_response_message( $response ); if ( 200 != $code ) { return new WP_Error( 'fetch_failed', esc_html( $code . ' ' . $message ) ); } return wp_remote_retrieve_body( $response ); } /** * Parse a plugin or theme file to fetch its header values. * * Based on WordPress' `get_file_data()` function. * * @param string $content The file content. * @param array $all_headers The headers to return. * @return array The header values. */ public static function get_content_data( $content, array $all_headers ) { // Pull only the first 8kiB of the file in. if ( function_exists( 'mb_substr' ) ) { $file_data = mb_substr( $content, 0, 8192 ); } else { $file_data = substr( $content, 0, 8192 ); } // Make sure we catch CR-only line endings. $file_data = str_replace( "\r", "\n", $file_data ); foreach ( $all_headers as $field => $regex ) { if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) { $all_headers[ $field ] = _cleanup_header_comment( $match[1] ); } else { $all_headers[ $field ] = ''; } } return $all_headers; } /** * Pre-load our handlers so the plugin/theme update filters can function. * * @param bool|WP_Error $default Default return value for the update. Usually boolean true. * @param array $hook_extra Extra arguments passed to hooked filters. * @return bool|WP_Error Boolean true or a WP_Error object. */ public function filter_upgrader_pre_install( $default, array $hook_extra ) { if ( isset( $hook_extra['plugin'] ) ) { $this->get_handler( 'plugin', $hook_extra['plugin'] ); } else if ( isset( $hook_extra['theme'] ) ) { $this->get_handler( 'theme', $hook_extra['theme'] ); } return $default; } /** * If we have a handler for this update, do some post-processing after the update. * * @param bool|WP_Error $default Default return value for the update. Usually boolean true. * @param array $hook_extra Extra arguments passed to hooked filters. * @param array $result Installation result data. * @return bool|WP_Error Boolean true or a WP_Error object. */ public function filter_upgrader_post_install( $default, array $hook_extra, array $result ) { global $wp_filesystem; if ( isset( $hook_extra['plugin'] ) ) { $handler = $this->get_handler( 'plugin', $hook_extra['plugin'] ); } else if ( isset( $hook_extra['theme'] ) ) { $handler = $this->get_handler( 'theme', $hook_extra['theme'] ); } else { return $default; } if ( ! is_a( $handler, 'EUAPI_Handler' ) ) { return $default; } switch ( $handler->get_type() ) { case 'plugin': $proper_destination = WP_PLUGIN_DIR . '/' . $handler->config['folder_name']; break; case 'theme': $proper_destination = get_theme_root() . '/' . $handler->config['folder_name']; break; } // Move $wp_filesystem->move( $result['destination'], $proper_destination ); return $default; } /** * Singleton instantiator. * * @return EUAPI Our instance of the EUAPI class. */ public static function init() { static $instance = null; if ( !$instance ) $instance = new EUAPI; return $instance; } /** * Eat our own dog food. Handle updates to EUAPI through GitHub. * * @param EUAPI_Handler|null $handler The handler object for this item, or null if a handler isn't set. * @param EUAPI_Item $item The item in question. * @return EUAPI_Handler|null The handler for this item, or null. */ public function filter_euapi_plugin_handler( EUAPI_Handler $handler = null, EUAPI_Item $item ) { if ( 'https://github.com/cftp/external-update-api' == $item->url ) { $handler = new EUAPI_Handler_GitHub( array( 'type' => $item->type, 'file' => $item->file, 'github_url' => $item->url, 'http' => array( 'sslverify' => false, ), ) ); } return $handler; } } endif; // endif class exists ================================================ FILE: external-update-api/external-update-api/handler-files.php ================================================ array( 'timeout' => 5, ), ); // Back-compat with earlier versions where we had these values in the root of the $config array. if ( isset( $config['sslverify'] ) ) { $config['http']['sslverify'] = $config['sslverify']; } if ( isset( $config['timeout'] ) ) { $config['http']['timeout'] = $config['timeout']; } parent::__construct( array_merge( $defaults, $config ) ); } /** * Returns the URL of the plugin or theme file. * * @author John Blackbourn * @param string $file Optional file name. Defaults to base plugin file or theme stylesheet. * @return string URL of the plugin file. */ abstract public function get_file_url( $file = null ); /** * Fetch the latest version number. Does this by fetching the plugin * file and then parsing the header to get the version number. * * @author John Blackbourn * @return string|false Version number, or false on failure. */ final public function fetch_new_version() { $response = EUAPI::fetch( $this->get_file_url(), $this->config['http'] ); if ( is_wp_error( $response ) ) { return false; } $data = EUAPI::get_content_data( $response, array( 'version' => 'Version' ) ); if ( empty( $data['version'] ) ) { return false; } return $data['version']; } /** * Fetch info about the latest version of the item. * * @author John Blackbourn * @return EUAPI_Info|WP_Error An EUAPI_Info object, or a WP_Error object on failure. */ final public function fetch_info() { $fields = array( 'author' => 'Author', 'description' => 'Description' ); switch ( $this->get_type() ) { case 'plugin': $file = $this->get_file_url(); $fields['plugin_name'] = 'Plugin Name'; break; case 'theme': $file = $this->get_file_url( 'style.css' ); $fields['theme_name'] = 'Theme Name'; break; } $response = EUAPI::fetch( $file, $this->config['http'] ); if ( is_wp_error( $response ) ) { return $response; } $data = EUAPI::get_content_data( $response, $fields ); $info = array_merge( $data, array( 'slug' => $this->get_file(), 'version' => $this->get_new_version(), 'homepage' => $this->get_homepage_url(), 'download_link' => $this->get_package_url(), # 'requires' => '', # 'tested' => '', # 'last_updated' => '', 'downloaded' => 0, 'sections' => array( 'description' => $data['description'], ), ) ); return new EUAPI_Info( $info ); } } endif; // endif class exists ================================================ FILE: external-update-api/external-update-api/handler-github.php ================================================ null, ); $path = trim( parse_url( $config['github_url'], PHP_URL_PATH ), '/' ); list( $username, $repo ) = explode( '/', $path, 2 ); $defaults['base_url'] = sprintf( 'https://raw.githubusercontent.com/%1$s/%2$s/master', $username, $repo ); $defaults['package_url'] = sprintf( 'https://api.github.com/repos/%1$s/%2$s/zipball', $username, $repo ); parent::__construct( array_merge( $defaults, $config ) ); } /** * Returns the URL of the plugin or theme's homepage. * * @author John Blackbourn * @return string URL of the plugin or theme's homepage. */ public function get_homepage_url() { return $this->config['github_url']; } /** * Returns the URL of the plugin or theme file on GitHub, with access token appended if relevant. * * @author John Blackbourn * @param string $file Optional file name. Defaults to base plugin file or theme stylesheet. * @return string URL of the plugin file. */ public function get_file_url( $file = null ) { if ( empty( $file ) ) { $file = $this->config['file_name']; } $url = trailingslashit( $this->config['base_url'] ) . $file; if ( !empty( $this->config['access_token'] ) ) { $url = add_query_arg( array( 'access_token' => $this->config['access_token'] ), $url ); } return $url; } /** * Returns the URL of the plugin or theme's ZIP package on GitHub, with access token appended if relevant. * * @author John Blackbourn * @return string URL of the plugin or theme's ZIP package. */ public function get_package_url() { $url = $this->config['package_url']; if ( !empty( $this->config['access_token'] ) ) { $url = add_query_arg( array( 'access_token' => $this->config['access_token'] ), $url ); } return $url; } } endif; // endif class exists ================================================ FILE: external-update-api/external-update-api/handler.php ================================================ config = apply_filters( "euapi_{$config['type']}_handler_config", array_merge( $defaults, $config ) ); } /** * Return the URL of the item's homepage. * * @abstract * @return string URL of the item's homepage. */ abstract public function get_homepage_url(); /** * Return the URL of the item's ZIP package. * * @abstract * @return string URL of the item's ZIP package. */ abstract public function get_package_url(); /** * Fetch the latest version number of the item, typically from an external location. * * @abstract * @return string|false Version number, or false on failure. */ abstract public function fetch_new_version(); /** * Fetch info about the latest version of the item. * * @abstract * @return EUAPI_Info|WP_Error An EUAPI_Info object, or a WP_Error object on failure. */ abstract public function fetch_info(); /** * Fetch the upgrade notice for the item, typically from an external location. * * @return string|false Upgrade notice, or false on failure. */ public function fetch_upgrade_notice(){ return false; } /** * Get the current item's base file name (eg. my-plugin/my-plugin.php or my-theme/style.css). * * @author John Blackbourn * @return string File name */ final public function get_file() { return $this->config['file']; } /** * Get the current installed version number of the item. * * @author John Blackbourn * @return string|false Version number, or false on failure. */ final public function get_current_version() { if ( isset( $this->item ) ) { return $this->item->get_version(); } else { return false; } } /** * Get the latest version number of the item. * * @author John Blackbourn * @return string|false Version number, or false on failure. */ final public function get_new_version() { if ( !isset( $this->new_version ) ) { $this->new_version = $this->fetch_new_version(); } return $this->new_version; } /** * Get the upgrade notice for the item. * * @author John Blackbourn * @return string|false Upgrade notice, or false on failure. */ final public function get_upgrade_notice() { if ( !isset( $this->upgrade_notice ) ) { $this->upgrade_notice = $this->fetch_upgrade_notice(); } return $this->upgrade_notice; } /** * Get the update object for the item. * * @author John Blackbourn * @return EUAPI_Update Object containing various info about the latest update. */ final public function get_update() { if ( isset( $this->update ) ) { return $this->update; } $package = add_query_arg( array( '_euapi_type' => $this->get_type(), '_euapi_file' => $this->get_file() ), $this->get_package_url() ); return $this->update = new EUAPI_Update( array( 'slug' => $this->get_file(), 'new_version' => $this->get_new_version(), 'upgrade_notice' => $this->get_upgrade_notice(), 'url' => $this->get_homepage_url(), 'package' => $package, 'config' => $this->get_config(), ) ); } /** * Get the info object for the item. * * @author John Blackbourn * @return EUAPI_Info|WP_Error An EUAPI_Info object, or a WP_Error object on failure. */ final public function get_info() { if ( !isset( $this->info ) ) { $this->info = $this->fetch_info(); } return $this->info; } /** * Helper function to get the current item config. * * @author John Blackbourn * @return array Config array. */ final public function get_config() { return $this->config; } /** * Helper function to get the handler type (either 'plugin' or 'theme'). * * @author John Blackbourn * @return string Handler type. */ final public function get_type() { if ( !in_array( $this->config['type'], array( 'plugin', 'theme' ), true ) ) { return 'plugin'; } return $this->config['type']; } } endif; // endif class exists ================================================ FILE: external-update-api/external-update-api/info.php ================================================ $v ) { $this->$k = $v; } } } endif; ================================================ FILE: external-update-api/external-update-api/item-plugin.php ================================================ file = $plugin; $this->url = $data['PluginURI']; $this->version = $data['Version']; $this->data = $data; } } endif; ================================================ FILE: external-update-api/external-update-api/item-theme.php ================================================ file = $theme; $this->url = $data['ThemeURI']; $this->version = $data['Version']; $this->data = $data; } } endif; ================================================ FILE: external-update-api/external-update-api/item.php ================================================ version; } public function get_url() { return $this->url; } } endif; ================================================ FILE: external-update-api/external-update-api/update.php ================================================ slug = $args['slug']; $this->new_version = $args['new_version']; $this->upgrade_notice = $args['upgrade_notice']; $this->url = $args['url']; $this->package = $args['package']; } public function get_data_to_store() { return get_object_vars( $this ); } public function get_new_version() { return $this->new_version; } } endif; ================================================ FILE: external-update-api/external-update-api.php ================================================ file ) { $handler = new EUAPI_Handler_GitHub( array( 'type' => $item->type, 'file' => $item->file, 'github_url' => 'https://github.com/my-username/my-plugin', 'http' => array( 'sslverify' => false, ), ) ); } return $handler; } add_filter( 'euapi_plugin_handler', 'my_plugin_update_handler', 10, 2 ); ``` Theme Example: ``` function my_theme_update_handler( EUAPI_Handler $handler = null, EUAPI_Item_Theme $item ) { if ( 'my-theme/style.css' == $item->file ) { $handler = new EUAPI_Handler_GitHub( array( 'type' => $item->type, 'file' => $item->file, 'github_url' => 'https://github.com/my-username/my-theme', 'http' => array( 'sslverify' => false, ), ) ); } return $handler; } add_filter( 'euapi_theme_handler', 'my_theme_update_handler', 10, 2 ); ``` If your repo is private then you'll need to pass in an additional `access_token` parameter that contains your OAuth access token. You can see some more example handlers in our [CFTP Updater repo](https://github.com/cftp/cftp-updater). ### Writing a new Handler ### To write a new handler, your best bet is to copy the `EUAPI_Handler_GitHub` class included in the plugin and go from there. See the `EUAPI_Handler` class (and, optionally, the `EUAPI_Handler_Files` class) for the abstract methods which must be defined in your class. ## Frequently Asked Questions ## None yet. ## Upgrade Notice ## ### 0.5 ### * Fix integration with theme updates. * EUAPI is now a network-only plugin when used on Multisite. * Eat our own dog food. EUAPI now handles its own updates through GitHub. * At long-last fix the wonky compatibility with WordPress 3.7+. * Increase the minimum required WordPress version to 3.7. ## Changelog ## ### 0.5 ### * Fix integration with theme updates. * EUAPI is now a network-only plugin when used on Multisite. * Eat our own dog food. EUAPI now handles its own updates through GitHub. * Add support for an upgrade notice (not used by default). * Introduce an abstract `EUAPI_Handler_Files` class to simplify extension by other handlers. * Inline docs improvements. ### 0.4 ### * At long-last fix the wonky compatibility with WordPress 3.7+. * Increase the minimum required WordPress version to 3.7. ### 0.3.5 ### * Support JSON-encoded API requests in addition to serialisation. This is pre-emptive support for WordPress 3.7. ### 0.3.4 ### * Support the upcoming SSL communication with `api.wordpress.org` ### 0.3.3 ### * Correct a method name in the `EUAPI_Handler` class. ### 0.3.2 ### * Change a method name and inline docs to clarify that both plugins and themes are supported. ### 0.3.1 ### * Prevent false positives when reporting available updates. * Prevent multiple simultaneous updates breaking due to a variable name clash. ### 0.3 ### * Allow a handler to return boolean false to prevent update checks being performed altogether. ### 0.2.4 ### * First public release. ## Screenshots ## None yet. ================================================ FILE: js/post-public-admin.js ================================================ jQuery( function ( $ ) { 'use strict'; if ( ! bbl_post_public.is_default_lang ) { // Fixup the side admin menu, which is confused by our additional language post types. if ( bbl_post_public.menu_id ) { $( bbl_post_public.menu_id + ', ' + bbl_post_public.menu_id + '>a' ) .addClass( 'wp-has-current-submenu wp-menu-open' ) .removeClass( 'wp-not-current-submenu' ); } } if ( bbl_post_public.is_bbl_post_type || ! bbl_post_public.is_default_lang ) { // Remove the add button next to the title for non-default languages $( 'h2 .add-new-h2' ).remove(); // Remove Bulk Edit and Quick Edit options $( '#posts-filter option[value="edit"], #posts-filter td.column-title span.inline' ).remove(); } $( '#original_post_content' ).prop( 'readOnly', true ); } ); ================================================ FILE: languages/fa_IR.php ================================================ ================================================ FILE: languages/pt_BR.po ================================================ # Translation of Babble in Portuguese (Portugal) # This file is distributed under the same license as the Babble package. msgid "" msgstr "" "PO-Revision-Date: 2012-05-31 13:31:37+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: GlotPress/0.1\n" "Project-Id-Version: Babble\n" #: class-jobs.php:50 msgid "Status" msgstr "Status" #: class-jobs.php:51 msgid "Details" msgstr "Detalhes" #: class-jobs.php:63 msgctxt "translation jobs general name" msgid "Jobs" msgstr "Trabalhos" #: class-jobs.php:64 msgctxt "translation jobs singular name" msgid "Job" msgstr "Trabalho" #: class-jobs.php:65 msgctxt "translation job" msgid "Add New" msgstr "Adicione um novo" #: class-jobs.php:66 msgctxt "translation job" msgid "Create New Job" msgstr "Criar um novo trabalho" #: class-jobs.php:67 msgctxt "translation job" msgid "Edit Job" msgstr "Editar trabalho" #: class-jobs.php:68 msgctxt "translation job" msgid "New Job" msgstr "Novo trabalho" #: class-jobs.php:69 msgctxt "translation job" msgid "View Job" msgstr "Visualizar trabalho" #: class-jobs.php:70 msgctxt "translation job" msgid "Search Jobs" msgstr "Procurar trabalho" #: class-jobs.php:71 msgctxt "translation job" msgid "No jobs found." msgstr "Nenhum trabalho encontrado" #: class-jobs.php:72 msgctxt "translation job" msgid "No jobs found in Trash." msgstr "Nenhum trabalho encontrado na lixeira" #: class-jobs.php:73 msgctxt "translation job" msgid "All Jobs" msgstr "Todos os trabalhos" #: class-jobs.php:76 msgid "Content, both posts and taxonomy terms, which need to be translated." msgstr "Conteúdo que precisa ser traduzido (tanto postagens quanto termos de classificação)" #: class-jobs.php:88 msgctxt "statuses of translation jobs, general name" msgid "Statuses" msgstr "Status" #: class-jobs.php:89 msgctxt "statuses of translation jobs, singular name" msgid "Status" msgstr "Status" #: class-jobs.php:90 msgctxt "status of translation jobs" msgid "Status" msgstr "Status" #: class-jobs.php:91 msgctxt "status of translation jobs" msgid "Popular Statuses" msgstr "Status populares" #: class-jobs.php:92 msgctxt "status of translation jobs" msgid "All Statuses" msgstr "Todos os status" #: class-jobs.php:93 msgctxt "status of translation jobs" msgid "Edit Status" msgstr "Editar status" #: class-jobs.php:94 msgctxt "status of translation jobs" msgid "View Status" msgstr "Ver status" #: class-jobs.php:95 msgctxt "status of translation jobs" msgid "Update Status" msgstr "Atualizar status" #: class-jobs.php:96 msgctxt "status of translation jobs" msgid "Add New Status" msgstr "Adicionar status" #: class-jobs.php:97 msgctxt "status of translation jobs" msgid "New Status Name" msgstr "Novo nome para status" #: class-jobs.php:98 msgctxt "status of translation jobs" msgid "Separate statuses with commas" msgstr "Separar diferentes status com vírgulas" #: class-jobs.php:99 msgctxt "status of translation jobs" msgid "Add or remove statuses" msgstr "Adicionar ou remover status" #: class-jobs.php:100 msgctxt "status of translation jobs" msgid "Choose from the most used statuses" msgstr "Escolher status mais populares" #: class-jobs.php:110 msgctxt "language for translation jobs, general name" msgid "Languages" msgstr "Línguas" #: class-jobs.php:111 class-jobs.php:112 msgctxt "language for translation jobs, general name" msgid "Language" msgstr "Língua" #: class-jobs.php:113 msgctxt "language for translation jobs, general name" msgid "Popular Languages" msgstr "Línguas populares" #: class-jobs.php:114 msgctxt "language for translation jobs, general name" msgid "All Languages" msgstr "Todas as línguas" #: class-jobs.php:115 msgctxt "language for translation jobs, general name" msgid "Edit Language" msgstr "Editar língua" #: class-jobs.php:116 msgctxt "language for translation jobs, general name" msgid "View Language" msgstr "Ver língua" #: class-jobs.php:117 msgctxt "language for translation jobs, general name" msgid "Update Language" msgstr "Atualizar língua" #: class-jobs.php:118 msgctxt "language for translation jobs, general name" msgid "Add New Language" msgstr "Adicionar nova língua" #: class-jobs.php:119 msgctxt "language for translation jobs, general name" msgid "New Language Name" msgstr "Novo nome de língua" #: class-jobs.php:120 msgctxt "language for translation jobs, general name" msgid "Separate languages with commas" msgstr "Separar línguas com vírgulas" #: class-jobs.php:121 msgctxt "language for translation jobs, general name" msgid "Add or remove languages" msgstr "Adicionar ou remover línguas" #: class-jobs.php:122 msgctxt "language for translation jobs, general name" msgid "Choose from the most used languages" msgstr "Escolher a partir das línguas mais usadas" #: class-jobs.php:145 class-jobs.php:215 msgid "Save" msgstr "Salvar" #: class-languages.php:90 msgid "Babble problem: Please visit the Available Languages settings and setup your available languages and the default language." msgstr "Problema no Babble: Por favor, visite o Available Languages settings e configure suas línguas disponíveis e a língua padrão." #: class-languages.php:100 templates-admin/options-available-languages.php:11 msgid "Available Languages" msgstr "Línguas disponíveis" #: class-languages.php:290 msgid "The languages \"%1$s\" and \"%2$s\" are using the same URL Prefix. Each URL prefix should be unique." msgstr "As línguas \"%1$s\" e \"%2$s\" estão utilizando o mesmo prefixo URL. Cada prefixo URL deve ser único." #: class-languages.php:311 msgid "You must set at least two languages as active." msgstr "Você deve selecionar ao menos duas línguas como ativas." #: class-languages.php:332 msgid "Your language settings have been saved." msgstr "" #: class-locale.php:91 msgid "Babble problem: Fancy permalinks are disabled. Please enable them in order to have language prefixed URLs work correctly." msgstr "" #: class-plugin.php:311 msgid "This plugin admin template could not be found: %s" msgstr "" #: class-plugin.php:445 msgid "This plugin template could not be found, perhaps you need to hook `sil_plugins_dir` and `sil_plugins_url`: %s" msgstr "" #: class-post-public.php:142 class-taxonomy.php:83 msgid "Term Translation ID" msgstr "" #: class-post-public.php:157 msgid "Post Translation ID" msgstr "" #: class-post-public.php:988 msgid "Invalid Post" msgstr "" #: class-switcher-menu.php:149 class-switcher-menu.php:181 #: class-switcher-menu.php:221 class-switcher-menu.php:250 #: class-switcher-menu.php:295 class-switcher-menu.php:326 #: class-switcher-menu.php:366 class-switcher-menu.php:395 #: class-switcher-menu.php:424 class-switcher-menu.php:463 msgid "Switch to %s" msgstr "" #: class-switcher-menu.php:187 class-switcher-menu.php:257 #: class-switcher-menu.php:333 class-switcher-menu.php:431 msgid "Create for %s" msgstr "" #: class-taxonomy.php:375 msgid "Empty Term" msgstr "" #: templates-admin/options-available-languages.php:20 msgid "Please select the languages you wish to translate this site into, you should select at least two, and select \"Save Changes\" below the languages table." msgstr "" #: templates-admin/options-available-languages.php:23 msgid "Default language:" msgstr "" #: templates-admin/options-available-languages.php:36 #: templates-admin/options-available-languages.php:51 msgid "Active" msgstr "" #: templates-admin/options-available-languages.php:40 #: templates-admin/options-available-languages.php:55 msgid "Code" msgstr "" #: templates-admin/options-available-languages.php:41 #: templates-admin/options-available-languages.php:56 msgid "Name(s)" msgstr "" #: templates-admin/options-available-languages.php:42 #: templates-admin/options-available-languages.php:57 msgid "Display Name" msgstr "" #: templates-admin/options-available-languages.php:43 #: templates-admin/options-available-languages.php:58 msgid "URL Prefix" msgstr "" #: templates-admin/options-available-languages.php:44 #: templates-admin/options-available-languages.php:59 msgid "Text Direction" msgstr "" #: templates-admin/options-available-languages.php:68 #: templates-admin/options-available-languages.php:73 msgid "Enable \"%s\" on this site" msgstr "" #: templates-admin/options-available-languages.php:79 msgid "Display name for \"%s\"" msgstr "" #: templates-admin/options-available-languages.php:85 msgid "URL prefix for \"%s\"" msgstr "" #: templates-admin/options-available-languages.php:91 msgid "Left to right" msgstr "" #: templates-admin/options-available-languages.php:93 msgid "Right to left" msgstr "" msgid "Babble" msgstr "" msgid "http://simonwheatley.co.uk/wordpress/babble" msgstr "" msgid "Now with Taxonomies!" msgstr "" msgid "Simon Wheatley" msgstr "" msgid "http://simonwheatley.co.uk/wordpress/" msgstr "" ================================================ FILE: languages/tr.po ================================================ # Translation of Babble in Turkish # This file is distributed under the same license as the Babble package. msgid "" msgstr "" "PO-Revision-Date: 2012-05-31 13:26:47+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: GlotPress/0.1\n" "Project-Id-Version: Babble\n" #: class-jobs.php:50 msgid "Status" msgstr "Durum" #: class-jobs.php:51 msgid "Details" msgstr "Detaylar" #: class-jobs.php:63 msgctxt "translation jobs general name" msgid "Jobs" msgstr "İşler" #: class-jobs.php:64 msgctxt "translation jobs singular name" msgid "Job" msgstr "İş" #: class-jobs.php:65 msgctxt "translation job" msgid "Add New" msgstr "Yeni Ekle" #: class-jobs.php:66 msgctxt "translation job" msgid "Create New Job" msgstr "Yeni İş Ekle" #: class-jobs.php:67 msgctxt "translation job" msgid "Edit Job" msgstr "İşi Düzelt" #: class-jobs.php:68 msgctxt "translation job" msgid "New Job" msgstr "Yeni iş" #: class-jobs.php:69 msgctxt "translation job" msgid "View Job" msgstr "İşi Görüntüle" #: class-jobs.php:70 msgctxt "translation job" msgid "Search Jobs" msgstr "İşlerde Araştır" #: class-jobs.php:71 msgctxt "translation job" msgid "No jobs found." msgstr "İş bulunamadı." #: class-jobs.php:72 msgctxt "translation job" msgid "No jobs found in Trash." msgstr "Çöp kutusunda iş bulunamadı." #: class-jobs.php:73 msgctxt "translation job" msgid "All Jobs" msgstr "Bütün İşler" #: class-jobs.php:76 msgid "Content, both posts and taxonomy terms, which need to be translated." msgstr "İçeriklerin, hem iletilerin hem de sınıflandırmaların, çevrilmesi gerekmektedir." #: class-jobs.php:88 msgctxt "statuses of translation jobs, general name" msgid "Statuses" msgstr "Durumlar" #: class-jobs.php:89 msgctxt "statuses of translation jobs, singular name" msgid "Status" msgstr "Durum" #: class-jobs.php:90 msgctxt "status of translation jobs" msgid "Status" msgstr "Durum" #: class-jobs.php:91 msgctxt "status of translation jobs" msgid "Popular Statuses" msgstr "Popüler Durumlar" #: class-jobs.php:92 msgctxt "status of translation jobs" msgid "All Statuses" msgstr "Bütün Durumlar" #: class-jobs.php:93 msgctxt "status of translation jobs" msgid "Edit Status" msgstr "Durumu Düzenle" #: class-jobs.php:94 msgctxt "status of translation jobs" msgid "View Status" msgstr "Durumu Görüntüle" #: class-jobs.php:95 msgctxt "status of translation jobs" msgid "Update Status" msgstr "Durumu Güncelle" #: class-jobs.php:96 msgctxt "status of translation jobs" msgid "Add New Status" msgstr "Yeni Durum Ekle" #: class-jobs.php:97 msgctxt "status of translation jobs" msgid "New Status Name" msgstr "Yeni Durumun Adı" #: class-jobs.php:98 msgctxt "status of translation jobs" msgid "Separate statuses with commas" msgstr "Durumları virgülle ayırın" #: class-jobs.php:99 msgctxt "status of translation jobs" msgid "Add or remove statuses" msgstr "Durum ekleyin ya da çıkarın" #: class-jobs.php:100 msgctxt "status of translation jobs" msgid "Choose from the most used statuses" msgstr "En çok kullanılan durumlardan birini seç" #: class-jobs.php:110 msgctxt "language for translation jobs, general name" msgid "Languages" msgstr "Diller" #: class-jobs.php:111 class-jobs.php:112 msgctxt "language for translation jobs, general name" msgid "Language" msgstr "Dil" #: class-jobs.php:113 msgctxt "language for translation jobs, general name" msgid "Popular Languages" msgstr "Popüler Diller" #: class-jobs.php:114 msgctxt "language for translation jobs, general name" msgid "All Languages" msgstr "Bütün Diller" #: class-jobs.php:115 msgctxt "language for translation jobs, general name" msgid "Edit Language" msgstr "Dili Düzenle" #: class-jobs.php:116 msgctxt "language for translation jobs, general name" msgid "View Language" msgstr "Dili Görüntüle" #: class-jobs.php:117 msgctxt "language for translation jobs, general name" msgid "Update Language" msgstr "Dili Güncelle" #: class-jobs.php:118 msgctxt "language for translation jobs, general name" msgid "Add New Language" msgstr "Yeni bir Dil Ekle" #: class-jobs.php:119 msgctxt "language for translation jobs, general name" msgid "New Language Name" msgstr "Yeni Dilin İsmi" #: class-jobs.php:120 msgctxt "language for translation jobs, general name" msgid "Separate languages with commas" msgstr "Dilleri virgülle ayır" #: class-jobs.php:121 msgctxt "language for translation jobs, general name" msgid "Add or remove languages" msgstr "Yeni bir dil ekle ya da çıkar" #: class-jobs.php:122 msgctxt "language for translation jobs, general name" msgid "Choose from the most used languages" msgstr "En çok kullanılan dillerden birini seç" #: class-jobs.php:145 class-jobs.php:215 msgid "Save" msgstr "Kaydet" #: class-languages.php:90 msgid "Babble problem: Please visit the Available Languages settings and setup your available languages and the default language." msgstr "Babble problem: Lütfen Available Languages settings i ziyaret edin, ve kullanmak istediğiniz diller ile sitenizde otomatik olarak çalışmasını istediğiniz dili seçin. " #: class-languages.php:100 templates-admin/options-available-languages.php:11 msgid "Available Languages" msgstr "Dil Seçenekleri " #: class-languages.php:290 msgid "The languages \"%1$s\" and \"%2$s\" are using the same URL Prefix. Each URL prefix should be unique." msgstr " \"%1$s\" ve \"%2$s\" dilleri aynı internet adresi kodunu kullanmaktadır. Her internet adresinin kodu birbirinden farklı olmalıdır." #: class-languages.php:311 msgid "You must set at least two languages as active." msgstr "En az iki dili kullanılabilir dilleriniz olarak seçmelisiniz." #: class-languages.php:332 msgid "Your language settings have been saved." msgstr "Dil seçenekleriniz kaydedildi." #: class-locale.php:91 msgid "Babble problem: Fancy permalinks are disabled. Please enable them in order to have language prefixed URLs work correctly." msgstr " Anlaşılmazlık problemi : Düzenlenmiş linkler engellenmiş.Dil kodlarının bulunduğu internet adreslerinin düzgün çalışabilmesi için lütfen bu linkleri etkinleştirin." #: class-plugin.php:311 msgid "This plugin admin template could not be found: %s" msgstr "Bu eklenti paketi bulunamadı: %s" #: class-plugin.php:445 msgid "This plugin template could not be found, perhaps you need to hook `sil_plugins_dir` and `sil_plugins_url`: %s" msgstr "Bu eklenti paketi bulunamadı, `sil_plugins_dir`ve` sil_plugins_url adreslerini birbirine bağlamayı deneyin %s" #: class-post-public.php:142 class-taxonomy.php:83 msgid "Term Translation ID" msgstr "Terim Çeviri No" #: class-post-public.php:157 msgid "Post Translation ID" msgstr "İleti Çeviri No" #: class-post-public.php:988 msgid "Invalid Post" msgstr "Geçersiz İleti" #: class-switcher-menu.php:149 class-switcher-menu.php:181 #: class-switcher-menu.php:221 class-switcher-menu.php:250 #: class-switcher-menu.php:295 class-switcher-menu.php:326 #: class-switcher-menu.php:366 class-switcher-menu.php:395 #: class-switcher-menu.php:424 class-switcher-menu.php:463 msgid "Switch to %s" msgstr "%s e geçin" #: class-switcher-menu.php:187 class-switcher-menu.php:257 #: class-switcher-menu.php:333 class-switcher-menu.php:431 msgid "Create for %s" msgstr "%s için oluşturun" #: class-taxonomy.php:375 msgid "Empty Term" msgstr "Boş terim" #: templates-admin/options-available-languages.php:20 msgid "Please select the languages you wish to translate this site into, you should select at least two, and select \"Save Changes\" below the languages table." msgstr "Lütfen siteyi çevirmek istediğiniz dilleri seçin. En az iki dil belirtmelisiniz ve seçimlerinizi diller tablosunun altındaki \"Değişiklikleri Kaydet\" tuşuna basarak kaydediniz." #: templates-admin/options-available-languages.php:23 msgid "Default language:" msgstr "Otomatik dil:" #: templates-admin/options-available-languages.php:36 #: templates-admin/options-available-languages.php:51 msgid "Active" msgstr "Etkin" #: templates-admin/options-available-languages.php:40 #: templates-admin/options-available-languages.php:55 msgid "Code" msgstr "Kod" #: templates-admin/options-available-languages.php:41 #: templates-admin/options-available-languages.php:56 msgid "Name(s)" msgstr "Ad(lar)" #: templates-admin/options-available-languages.php:42 #: templates-admin/options-available-languages.php:57 msgid "Display Name" msgstr "Kullanıcı Adı" #: templates-admin/options-available-languages.php:43 #: templates-admin/options-available-languages.php:58 msgid "URL Prefix" msgstr "URL önkodu" #: templates-admin/options-available-languages.php:44 #: templates-admin/options-available-languages.php:59 msgid "Text Direction" msgstr "Yazı yönü" #: templates-admin/options-available-languages.php:68 #: templates-admin/options-available-languages.php:73 msgid "Enable \"%s\" on this site" msgstr "Bu sitede \"%s\"i etkinleştir" #: templates-admin/options-available-languages.php:79 msgid "Display name for \"%s\"" msgstr "\"%s\"in Kullanıcı adı" #: templates-admin/options-available-languages.php:85 msgid "URL prefix for \"%s\"" msgstr "\"%s\"in URL önkodu" #: templates-admin/options-available-languages.php:91 msgid "Left to right" msgstr "Left sağa doğru" #: templates-admin/options-available-languages.php:93 msgid "Right to left" msgstr "Right sola doğru" msgid "Babble" msgstr "Anlaşılmazlık" msgid "http://simonwheatley.co.uk/wordpress/babble" msgstr "http://simonwheatley.co.uk/wordpress/babble" msgid "Now with Taxonomies!" msgstr "Şimdi de sınıflandırmalar!" msgid "Simon Wheatley" msgstr "Simon Wheatley" msgid "http://simonwheatley.co.uk/wordpress/" msgstr "http://simonwheatley.co.uk/wordpress/" ================================================ FILE: miscellaneous.php ================================================ $new_taxonomy ) ); wp_redirect( $url ); exit; } } if ( $post_type ) { $new_post_type = bbl_get_post_type_in_lang( $post_type, $cur_lang_code ); if ( $post_type != $new_post_type ) { $url = add_query_arg( array( 'post_type' => $new_post_type ) ); wp_redirect( $url ); exit; } } } add_action( 'admin_init', 'bbl_admin_init' ); /** * Replicates the core comments_template function, but uses the API * to fetch the comments and includes more filters. * * Loads the comment template specified in $file. * * Will not display the comments template if not on single post or page, or if * the post does not have comments. * * Uses the WordPress database object to query for the comments. The comments * are passed through the 'comments_array' filter hook with the list of comments * and the post ID respectively. * * The $file path is passed through a filter hook called, 'comments_template' * which includes the TEMPLATEPATH and $file combined. Tries the $filtered path * first and if it fails it will require the default comment template from the * default theme. If either does not exist, then the WordPress process will be * halted. It is advised for that reason, that the default theme is not deleted. * * @since 1.5.0 * @global array $comment List of comment objects for the current post * @uses $wpdb * @uses $post * @uses $withcomments Will not try to get the comments if the post has none. * * @see comments_template() * * @param string $file Optional, default '/comments.php'. The file to load * @param bool $separate_comments Optional, whether to separate the comments by comment type. Default is false. * @return null Returns null if no comments appear */ function bbl_comments_template( $file = '/comments.php', $separate_comments = false ) { global $wp_query, $withcomments, $post, $wpdb, $id, $comment, $user_login, $user_ID, $user_identity, $overridden_cpage; if ( !(is_single() || is_page() || $withcomments) || empty($post) ) return; if ( empty($file) ) $file = '/comments.php'; $req = get_option('require_name_email'); /** * Comment author information fetched from the comment cookies. * * @uses wp_get_current_commenter() */ $commenter = wp_get_current_commenter(); /** * The name of the current comment author escaped for use in attributes. */ $comment_author = $commenter['comment_author']; // Escaped by sanitize_comment_cookies() /** * The email address of the current comment author escaped for use in attributes. */ $comment_author_email = $commenter['comment_author_email']; // Escaped by sanitize_comment_cookies() /** * The url of the current comment author escaped for use in attributes. */ $comment_author_url = esc_url($commenter['comment_author_url']); $query = new Bbl_Comment_Query; $args = array( 'order' => 'ASC', 'post_id' => $post->ID, 'status' => 'approve', 'status' => 'approve', ); if ( $user_ID) { $args[ 'unapproved_user_id' ] = $user_ID; } else if ( ! empty($comment_author) ) { $args[ 'unapproved_author' ] = wp_specialchars_decode($comment_author,ENT_QUOTES); $args[ 'unapproved_author_email' ] = $comment_author_email; } $args = apply_filters( 'comments_template_args', $args ); $comments = $query->query( $args ); // keep $comments for legacy's sake $wp_query->comments = apply_filters( 'comments_array', $comments, $post->ID ); $comments = &$wp_query->comments; $wp_query->comment_count = count($wp_query->comments); update_comment_cache($wp_query->comments); if ( $separate_comments ) { $wp_query->comments_by_type = &separate_comments($comments); $comments_by_type = &$wp_query->comments_by_type; } $overridden_cpage = FALSE; if ( '' == get_query_var('cpage') && get_option('page_comments') ) { set_query_var( 'cpage', 'newest' == get_option('default_comments_page') ? get_comment_pages_count() : 1 ); $overridden_cpage = TRUE; } if ( !defined('COMMENTS_TEMPLATE') || !COMMENTS_TEMPLATE) define('COMMENTS_TEMPLATE', true); $include = apply_filters('comments_template', STYLESHEETPATH . $file ); if ( file_exists( $include ) ) require $include; elseif ( file_exists( TEMPLATEPATH . $file ) ) require TEMPLATEPATH . $file; else // Backward compat code will be removed in a future release require ABSPATH . WPINC . '/theme-compat/comments.php'; } /** * WordPress Comment Query class. * * See Trac: http://core.trac.wordpress.org/ticket/19623 * * @since 3.1.0 */ class Bbl_Comment_Query { /** * Execute the query * * @since 3.1.0 * * @param string|array $query_vars * @return int|array */ function query( $query_vars ) { global $wpdb; $defaults = array( 'author_email' => '', 'ID' => '', 'karma' => '', 'number' => '', 'offset' => '', 'orderby' => '', 'order' => 'DESC', 'parent' => '', 'post_ID' => '', 'post_id' => '', 'post__in' => '', 'post_author' => '', 'post_name' => '', 'post_parent' => '', 'post_status' => '', 'post_type' => '', 'status' => '', 'type' => '', 'unapproved_author' => '', 'unapproved_author_email' => '', 'unapproved_user_id' => '', 'user_id' => '', 'search' => '', 'count' => false, ); $this->query_vars = wp_parse_args( $query_vars, $defaults ); do_action_ref_array( 'pre_get_comments', array( $this ) ); extract( $this->query_vars, EXTR_SKIP ); // $args can be whatever, only use the args defined in defaults to compute the key $key = md5( serialize( compact(array_keys($defaults)) ) ); $last_changed = wp_cache_get('last_changed', 'comment'); if ( !$last_changed ) { $last_changed = time(); wp_cache_set('last_changed', $last_changed, 'comment'); } $cache_key = "get_comments:$key:$last_changed"; if ( $cache = wp_cache_get( $cache_key, 'comment' ) ) { return $cache; } if ( empty( $post_id ) && empty( $post__in ) ) $post_id = 0; $post_id = absint($post_id); $where = ''; $show_unapproved = ( '' != $unapproved_user_id || '' !== $unapproved_author || '' != $unapproved_author_email ); if ( $show_unapproved ) { $where .= ' ( '; } if ( 'hold' == $status ) $where .= "comment_approved = '0'"; elseif ( 'approve' == $status ) $where .= "comment_approved = '1'"; elseif ( 'spam' == $status ) $where .= "comment_approved = 'spam'"; elseif ( 'trash' == $status ) $where .= "comment_approved = 'trash'"; else $where .= "( comment_approved = '0' OR comment_approved = '1' )"; if ( $show_unapproved ) { $where .= ' OR ( comment_approved = 0 '; if ( '' !== $unapproved_author ) $where .= $wpdb->prepare( ' AND comment_author = %s', $unapproved_author ); if ( '' !== $unapproved_author_email ) $where .= $wpdb->prepare( ' AND comment_author_email = %s', $unapproved_author_email ); if ( '' !== $unapproved_user_id ) $where .= $wpdb->prepare( ' AND user_id = %d', $unapproved_user_id ); $where .= ' ) ) '; } $order = ( 'ASC' == strtoupper($order) ) ? 'ASC' : 'DESC'; if ( ! empty( $orderby ) ) { $ordersby = is_array($orderby) ? $orderby : preg_split('/[,\s]/', $orderby); $ordersby = array_intersect( $ordersby, array( 'comment_agent', 'comment_approved', 'comment_author', 'comment_author_email', 'comment_author_IP', 'comment_author_url', 'comment_content', 'comment_date', 'comment_date_gmt', 'comment_ID', 'comment_karma', 'comment_parent', 'comment_post_ID', 'comment_type', 'user_id', ) ); $orderby = empty( $ordersby ) ? 'comment_date_gmt' : implode(', ', $ordersby); } else { $orderby = 'comment_date_gmt'; } $number = absint($number); $offset = absint($offset); if ( !empty($number) ) { if ( $offset ) $limits = 'LIMIT ' . $offset . ',' . $number; else $limits = 'LIMIT ' . $number; } else { $limits = ''; } if ( $count ) $fields = 'COUNT(*)'; else $fields = '*'; $join = ''; if ( ! empty($post_id) ) { $where .= $wpdb->prepare( ' AND comment_post_ID = %d', $post_id ); } else if ( '' != $post__in ) { $_post__in = implode(',', array_map( 'absint', $post__in )); $where .= " AND comment_post_ID IN ($_post__in)"; } if ( '' !== $author_email ) $where .= $wpdb->prepare( ' AND comment_author_email = %s', $author_email ); if ( '' !== $karma ) $where .= $wpdb->prepare( ' AND comment_karma = %d', $karma ); if ( 'comment' == $type ) { $where .= " AND comment_type = ''"; } elseif( 'pings' == $type ) { $where .= ' AND comment_type IN ("pingback", "trackback")'; } elseif ( ! empty( $type ) ) { $where .= $wpdb->prepare( ' AND comment_type = %s', $type ); } if ( '' !== $parent ) $where .= $wpdb->prepare( ' AND comment_parent = %d', $parent ); if ( '' !== $user_id ) $where .= $wpdb->prepare( ' AND user_id = %d', $user_id ); if ( '' !== $search ) $where .= $this->get_search_sql( $search, array( 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_author_IP', 'comment_content' ) ); $post_fields = array_filter( compact( array( 'post_author', 'post_name', 'post_parent', 'post_status', 'post_type', ) ) ); if ( ! empty( $post_fields ) ) { $join = "JOIN $wpdb->posts ON $wpdb->posts.ID = $wpdb->comments.comment_post_ID"; foreach( $post_fields as $field_name => $field_value ) $where .= $wpdb->prepare( " AND {$wpdb->posts}.{$field_name} = %s", $field_value ); } $pieces = array( 'fields', 'join', 'where', 'orderby', 'order', 'limits' ); $clauses = apply_filters_ref_array( 'comments_clauses', array( compact( $pieces ), $this ) ); foreach ( $pieces as $piece ) $$piece = isset( $clauses[ $piece ] ) ? $clauses[ $piece ] : ''; $query = "SELECT $fields FROM $wpdb->comments $join WHERE $where ORDER BY $orderby $order $limits"; if ( $count ) return $wpdb->get_var( $query ); $comments = $wpdb->get_results( $query ); $comments = apply_filters_ref_array( 'the_comments', array( $comments, $this ) ); wp_cache_add( $cache_key, $comments, 'comment' ); return $comments; } } ?> ================================================ FILE: readme.md ================================================ # Babble * Tags: translations, translation, multilingual, i18n, l10n, localisation * Requires at least: 3.5.1 * Tested up to: 4.1.1 * Stable tag: 1.5.1 * License: GPLv2 or later * License URI: http://www.gnu.org/licenses/gpl-2.0.html Multilingual WordPress done right. ## Description This plugin is at a beta stage for translating: * Posts * Pages * Custom post types * Categories * Tags * Custom taxonomies. It is powering a live site at http://freespeechdebate.com/. The plugin was built with an aversion to both additional database tables, additional columns or column changes and a desire to keep additional queries to a minimum. The plugin is (in theory) compatible with WordPress.com VIP and was built with this platform in mind. There are a **lot** of `@FIXME` comments, expressing doubts, fears, uncertainties and unknowns; feel free to weigh in on any of them. Please add bugs and contribute patches and pull requests to https://github.com/cftp/babble/issues **Pull requests on the *develop* branch, please, not the master branch.** Contributors: Simon Wheatley, John Blackbourn, Scott Evans, Simon Dickson, Marko Heijnen, Tom Nowell, Gary Jones, Emyr Thomas ## Installation Installation is fairly standard: 1. Upload the `babble` directory to the `/wp-content/plugins/` directory 2. Ensure pretty permalinks are activated in Settings -> Permalinks, not sure how it will cope without these! 3. Activate the plugin through the 'Plugins' menu in WordPress 4. You'll now be prompted to set the languages you want, you can pick from any of the language packs you've got installed 5. You'll notice the language switcher menu in the admin bar, use this to switch languages and (depending on context) to create new versions of the content you are looking at (from the front end) or editing (from the admin area) ## Screenshots ![Trigger a translation from the post edit screen](screenshot-1.png "Trigger a translation from the post edit screen") _Trigger a translation from the post edit screen_ ![View the jobs in the translation queue](screenshot-2.png "View the jobs in the translation queue") _View the jobs in the translation queue_ ![Add the translation on the translation editor](screenshot-3.png "Add the translation on the translation editor") _Add the translation on the translation editor_ ## Changelog ### 1.5.1 * Fix for not syncing when updating post meta (#261) ### 1.5 * New functionality for translating post meta data. * New option to immediately create blank translations when a translation job is open. * Use the default language text direction for translations which have empty content. * Visual tweaks to the translation editing screen. ### 1.4.4 * Load the interface textdomain when loading and displaying the admin toolbar on the front end. * Correctly set the interface language and content language separately from each other. * The WPLANG constant is deprecated since WP 4.0. We now give preference to the WPLANG option when setting the defaults. * Fix registration of 'term_translation' taxonomy. * Fix breaking rewrite rules starting with ^. * Various corrections to templates and body classes. * Various smaller bug fixes. ### 1.4.3 * Fix the single post template names * Update the bundled External Update API library ### 1.4.2 * Fix the language switcher widget so it correctly displays language names ### 1.4.1 * Add the External Update API library to serve updates to Babble from GitHub ### 1.4 * ENHANCEMENT: Translation job UI and workflow * ENHANCEMENT: Separation of admin language setting from the content language * Various fixes and enhancements *gap of time while we add various things* ### alpha 1.1 * Taxonomy terms. ### alpha 1 * Proof of concept concentrating on the translation of posts. Taxonomies and menus are not handled yet. Widgets are out of scope completely for this phase of work. ================================================ FILE: readme.txt ================================================ === Babble === Contributors: Automattic, simonwheatley Tags: translations, translation, multilingual, i18n, l10n, localisation Requires at least: 3.5.1 Tested up to: 4.1.1 Stable tag: 1.5.1 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html Multilingual WordPress done right. A plugin to facilitate translation of content within a single WordPress site. == Description == This plugin is at a beta stage for translating: * Posts * Pages * Custom post types * Categories * Tags * Custom taxonomies. It is powering a live site at http://freespeechdebate.com/. The plugin was built with an aversion to both additional database tables, additional columns or column changes and a desire to keep additional queries to a minimum. The plugin is (in theory) compatible with WordPress.com VIP and was built with this platform in mind. There are a **lot** of `@FIXME` comments, expressing doubts, fears, uncertainties and unknowns; feel free to weigh in on any of them. Please add bugs and contribute patches and pull requests to https://github.com/cftp/babble/issues **Pull requests on the *develop* branch, please, not the master branch.** Contributors: Simon Wheatley, John Blackbourn, Scott Evans, Simon Dickson, Marko Heijnen, Tom Nowell, Gary Jones, Emyr Thomas == Installation == Installation is fairly standard: 1. Upload the `babble` directory to the `/wp-content/plugins/` directory 2. Ensure pretty permalinks are activated in Settings -> Permalinks, not sure how it will cope without these! 3. Activate the plugin through the 'Plugins' menu in WordPress 4. You'll now be prompted to set the languages you want, you can pick from any of the language packs you've got installed 5. You'll notice the language switcher menu in the admin bar, use this to switch languages and (depending on context) to create new versions of the content you are looking at (from the front end) or editing (from the admin area) == Screenshots == 1. Trigger a translation from the post edit screen 2. View the jobs in the translation queue 3. Add the translation on the translation editor == Changelog == = 1.5.1 = * Fix for not syncing when updating post meta (#261) = 1.5 = * New functionality for translating post meta data. * New option to immediately create blank translations when a translation job is open. * Use the default language text direction for translations which have empty content. * Visual tweaks to the translation editing screen. = 1.4.4 = * Load the interface textdomain when loading and displaying the admin toolbar on the front end. * Correctly set the interface language and content language separately from each other. * The WPLANG constant is deprecated since WP 4.0. We now give preference to the WPLANG option when setting the defaults. * Fix registration of 'term_translation' taxonomy. * Fix breaking rewrite rules starting with ^. * Various corrections to templates and body classes. * Various smaller bug fixes. = 1.4.3 = * Fix the single post template names * Update the bundled External Update API library = 1.4.2 = * Fix the language switcher widget so it correctly displays language names = 1.4.1 = * Add the External Update API library to serve updates to Babble from GitHub = 1.4 = * ENHANCEMENT: Translation job UI and workflow * ENHANCEMENT: Separation of admin language setting from the content language * Various fixes and enhancements *gap of time while we add various things* = alpha 1.1 = * Taxonomy terms. = alpha 1 = * Proof of concept concentrating on the translation of posts. Taxonomies and menus are not handled yet. Widgets are out of scope completely for this phase of work. ================================================ FILE: templates-admin/options-available-languages.php ================================================

name ); ?> code ); ?> text_direction ) : ?> Left to right', 'babble' ); ?> Right to left', 'babble' ); ?>
================================================ FILE: templates-admin/switcher-interface.php ================================================ ================================================ FILE: templates-admin/translation-editor-meta.php ================================================ get_key(); ?>
get_input( "bbl_translation[meta][{$key}]", $translation ); ?>
get_output(); ?>
================================================ FILE: templates-admin/translation-editor-post-excerpt.php ================================================
================================================ FILE: templates-admin/translation-editor-terms.php ================================================

name ); ?>
slug ) or !empty( $translation->slug ) ) { ?>

slug ); ?>
description ) or !empty( $translation->description ) ) { ?>

================================================ FILE: templates-admin/translation-editor.php ================================================
ID}", '_bbl_translation_lang_code' ); echo ''; } ?> ID}", '_bbl_translation_origin_post' ); echo ''; } wp_nonce_field( "bbl_translation_edit_post_{$job->ID}", '_bbl_translation_edit_post' ); $original = $items['post']['original']; $translation = $items['post']['translation']; $original_cpto = get_post_type_object( $original->post_type ); do_action( 'bbl_translation_post_meta_boxes', 'bbl_translation_editor_post', $original, $translation ); ?>
post_title ) or !empty( $translation->post_title ) ) { ?>
post_title ); ?>
post_name ) or !empty( $translation->post_name ) ) { ?>
post_name ); ?>
post_content ) or !empty( $translation->post_content ) ) { ?> true argument in the quicktags settings is a hack to hide the Quicktags buttons but retain the Visual/Text tabs. ?>
post_content, 'translation_post_content', array( 'textarea_name' => 'bbl_translation[post][post_content]', ) ); ?>
post_content, 'original_post_content', array( 'textarea_name' => 'bbl_original[post][post_content]', 'media_buttons' => false, 'tinymce' => array( 'readonly' => 1, ), ) ); ?>
ID}", '_bbl_translation_origin_term' ); echo ''; echo ''; } wp_nonce_field( "bbl_translation_edit_terms_{$job->ID}", '_bbl_translation_edit_terms' ); do_action( 'bbl_translation_terms_meta_boxes', 'bbl_translation_editor_terms', $items['terms'] ); ?>
$terms ) do_meta_boxes( 'bbl_translation_editor_terms', $taxo, compact( 'taxo', 'terms' ) ); ?>
ID}", '_bbl_translation_edit_meta' ); do_action( 'bbl_translation_meta_meta_boxes', 'bbl_translation_editor_meta', $items['meta'] ); ?>
$meta ) { do_meta_boxes( 'bbl_translation_editor_meta', $meta_key, compact( 'meta_key', 'meta' ) ); } ?>
================================================ FILE: templates-admin/translation-groups.php ================================================

Translation Groups

$status_obj ) : ?>

term_id, 'post_translation' ); $posts = array(); foreach ( $post_ids as $post_id ) $posts[] = get_post( $post_id ); usort( $posts, array( 'SortPosts', 'post_type_descending' ) ); if ( $posts ) : ?> post_status, $selected_stati ) ) continue; ?>
ID Type Status Lang
ID Type Status Lang

Translation Group: term_id; ?>

WARNING: Post does not existremove 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.

================================================ FILE: translation-fields.php ================================================ __( 'Facebook', 'wordpress-seo' ), 'twitter' => __( 'Twitter', 'wordpress-seo' ), 'google-plus' => __( 'Google+', 'wordpress-seo' ), ) as $network => $label ) { $title = WPSEO_Meta::$meta_prefix . $network . '-title'; $desc = WPSEO_Meta::$meta_prefix . $network . '-description'; $fields[ $title ] = new Babble_Meta_Field_Text( $post, $title, sprintf( __( '%s Title', 'wordpress-seo' ), $label ) ); $fields[ $desc ] = new Babble_Meta_Field_Text( $post, $desc, sprintf( __( '%s Description', 'wordpress-seo' ), $label ) ); } } return $fields; } add_filter( 'bbl_translated_meta_fields', 'bbl_wpseo_meta_fields', 10, 2 ); ================================================ FILE: translation-group-tool-sorter.php ================================================ post_type == $n->post_type ) return 0; return ( $m->post_type < $n->post_type ) ? -1 : 1; } } ?> ================================================ FILE: translation-group-tool.php ================================================ Tools which allows you to see and edit Babble translation associations. Version: 0.1 Author: Simon Wheatley Author URI: http://simonwheatley.co.uk/wordpress/ */ /* Copyright 2012 Simon Wheatley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ require_once 'class-plugin.php'; /** * Handles the display and functionality of the translation group tool. * * @package BabbleTranslationGroupTool * @author Simon Wheatley **/ class BabbleTranslationGroupTool extends Babble_Plugin { /** * Initiate! * * @return void * @access public **/ function __construct() { $this->setup( 'babble-tgt', 'plugin' ); $this->add_action( 'admin_menu' ); $this->add_action( 'load-post-new.php', 'load_post' ); $this->add_action( 'load-post.php', 'load_post' ); $this->add_action( 'load-tools_page_btgt', 'load_tools_page' ); $this->add_action( 'save_post', null, null, 2 ); $this->add_filter( 'bbl_metaboxes_for_translators', 'metaboxes_for_translators' ); $this->add_filter( 'bbl_pre_sync_properties', 'pre_sync_properties', null, 2 ); } // HOOKS AND ALL THAT // ================== /** * Hooks the WP admin_menu action to add a menu to * the Tools section. * * @return void **/ public function admin_menu() { add_management_page( __( 'Translation Groups', 'babble-tgt' ), __( 'Translation Groups', 'babble-tgt' ), 'manage_options', 'btgt', array( $this, 'tools_page' ) ); } /** * Hooks the dynamic load-* action called when * this tools page loads. * * @return void **/ public function load_tools_page() { if ( ! $action = ( isset( $_GET[ 'btgt_action' ] ) ) ? $_GET[ 'btgt_action' ] : false ) return; $obj_id = ( isset( $_GET[ 'obj_id' ] ) ) ? $_GET[ 'obj_id' ] : false; $wp_nonce = ( isset( $_GET[ '_wpnonce' ] ) ) ? $_GET[ '_wpnonce' ] : false; switch ( $action ) { case 'delete_from_groups': if ( ! wp_verify_nonce( $wp_nonce, "btgt_delete_from_groups_$obj_id" ) ) { $this->set_admin_error( 'Sorry, went wrong. Please try again.' ); return; } wp_delete_object_term_relationships( $obj_id, 'post_translation' ); $this->set_admin_notice( "Deleted term relationships for $obj_id" ); break; case 'delete_post': wp_delete_object_term_relationships( $obj_id, 'post_translation' ); wp_delete_post( $obj_id, true ); break; case 'trash_post': wp_delete_object_term_relationships( $obj_id, 'post_translation' ); wp_trash_post( $obj_id ); break; } $args = array( 'page' => 'btgt', 'lang' => bbl_get_default_lang_code(), ); $url = add_query_arg( $args, admin_url( 'tools.php' ) ); $url .= '#' . $_GET[ 'anchor' ]; wp_redirect( $url ); } /** * Hooks the various dynamic actions fired when the edit post and * edit new post screens are loaded. Determines if the post to be * edited has become disconnected from it's translation group, * and shows the Reconnect metabox if it has. * * @return void **/ public function load_post() { $screen = get_current_screen(); if ( ! $post_id = isset( $_GET[ 'post' ] ) ? $_GET[ 'post' ] : false ) return; $post = get_post( $post_id ); if ( ! in_array( $post->post_status, array( 'draft', 'pending', 'publish' ) ) ) return; if ( bbl_get_post_lang_code( $post ) == bbl_get_default_lang_code() ) return; if ( $default_lang_post = bbl_get_post_in_lang( $post, bbl_get_default_lang_code() ) ) return; $this->add_meta_box( 'bbl_reconnect', 'Reconnect Translation', 'metabox_reconnect', $post->post_type, 'side' ); } /** * Hooks the WP save_post action * * @param int $post_id The ID of the post being saved * @param object $post The WordPress post object being saved * @return void **/ function save_post( $post_id, $post ) { if ( ! in_array( $post->post_status, array( 'draft', 'publish' ) ) ) return; if ( ! isset( $_POST[ '_bbl_reconnect_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_reconnect_translation_$post_id", '_bbl_reconnect_nonce' ); // Check the user has set a transid if ( ! $transid = isset( $_POST[ 'bbl_transid' ] ) ? (int) $_POST[ 'bbl_transid' ] : false ) return; // Check the transid the user has set actually exists if ( ! term_exists( $transid, 'post_translation' ) ) { $this->set_admin_error( __( 'The TransID you want to reconnect this content to does not exist. Please check the Translation Group information and try again.', 'babble' ) ); return; } global $bbl_post_public; $bbl_post_public->set_transid( $post, $transid ); } /** * Hooks the Babble bbl_pre_sync_properties filter to * log any changes to parent. We're not making changes * to the data, just logging significant changes for * debug purposes. * * @param array $postdata The data which will be applied to the post as part of the sync * @param int $origin_id The ID of the post we are syncing from * @return array The data which will be applied to the post as part of the sync **/ public function pre_sync_properties( $postdata, $origin_id ) { $current_post = get_post( $postdata[ 'ID' ] ); $origin_post = get_post( $origin_id ); if ( $current_post->post_parent != $postdata[ 'post_parent' ] ) { $user = wp_get_current_user(); $remote_ip = $_SERVER[ 'REMOTE_ADDR' ]; $referer = $_SERVER[ 'HTTP_REFERER' ]; $lang = bbl_get_current_lang_code(); $origin_lang = bbl_get_post_lang_code( $origin_id ); error_log( "Babble: $user->user_login has changed {$postdata[ 'ID' ]} parent from $current_post->post_parent ($current_post->post_type) to {$postdata[ 'post_parent' ]}. \tOrigin: $origin_id. Origin lang: $origin_lang. IP $remote_ip. User lang: $lang. Referer $referer." ); } return $postdata; } /** * Hooks the Babble bbl_metaboxes_for_translators filter to * add the bbl_reconnect metabox to the list of boxes allowed * on translator screens. * * @param array $boxes The array of box names which are allowed * @return array The array of box names which are allowed **/ function metaboxes_for_translators( $boxes ) { $boxes[] = 'bbl_reconnect'; return $boxes; } // CALLBACKS // ========= /** * The callback function which provides HTML for the Babble * Translation Reconnection metabox, which allows an admin * to reconnect a post to the equivalent post in the * default language. * * @param object $post The WP Post object being edited * @param array $metabox The args and params for this metabox * @return void (echoes HTML) **/ public function metabox_reconnect( $post, $metabox ) { wp_nonce_field( "bbl_reconnect_translation_{$post->ID}", '_bbl_reconnect_nonce' ); ?>


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.

render_admin( 'translation-groups.php', $vars ); } // UTILITIES // ========= /** * Get a link to trash a particular post. * * @param int $post_id The ID of the post to trash * @param string $action The action for this link * @return string A Nonced action URL **/ protected function get_action_link( $obj_id, $action, $anchor = null ) { $args = array( 'btgt_action' => $action, 'obj_id' => $obj_id, 'lang' => bbl_get_default_lang_code(), ); if ( ! is_null( $anchor ) ) $args[ 'anchor' ] = $anchor; return wp_nonce_url( add_query_arg( $args ), "btgt_{$action}_$obj_id" ); } } // END BabbleTranslationGroupTool class $bbl_translation_group_tool = new BabbleTranslationGroupTool(); ?> ================================================ FILE: widget.php ================================================ 'off', 'show_as' => 'dropdown', ); function __construct() { parent::__construct( 'bbl_widget', // Base ID __('Language Switcher','babble'), // Name array( 'description' => __('Displays a list of links to translations / equivalent pages.', 'babble') ) // Args ); } function widget( $args, $instance ) { $args = array_merge( array( 'show_as' => 'dropdown', ), $args ); echo $args['before_widget']; echo $args['before_title'] . __( 'Languages', 'babble' ) . $args['after_title']; $list = bbl_get_switcher_links(); switch ( $instance['show_as'] ) { case 'dropdown': echo ''; break; case 'list': echo ''; break; } echo $args['after_widget']; } function update( $new_instance, $old_instance ) { $new_instance = array_merge( $this->defaults, $new_instance ); $new_instance['show_as'] = strip_tags( $new_instance['show_as'] ); $new_instance['show_if_unavailable'] = strip_tags( $new_instance['show_if_unavailable'] ); return $new_instance; } function form( $instance ) { global $wpdb; $instance = wp_parse_args( $instance, $this->defaults ); ?>

/>