PRODTABS_VERSION ); if ( defined( 'PRODTABS_PRO_VERSION' ) ) { $args['pv'] = PRODTABS_PRO_VERSION; } if ( $report ) { $args['si'] = System_Info::code(); } return ( self::french() ? 'https://refficience.com/prodtabs/support/' : 'https://refficience.com/en/prodtabs/support/' ) . '#' . http_build_query( $args, '', '&', PHP_QUERY_RFC3986 ); } /** * Products that have tabs of their own, one per group of translations when the site has languages. */ public static function products_with_tabs(): int { $cached = get_transient( 'prodtabs_products_with_tabs' ); if ( false !== $cached ) { return (int) $cached; } $args = array( 'post_type' => 'product', 'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future' ), 'fields' => 'ids', 'posts_per_page' => 1, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- one count, cached. 'meta_key' => Tabs::META_LAYOUT, ); $lang = Multilingual::default_language(); if ( '' !== $lang ) { $args['lang'] = $lang; // Polylang; WPML counts the admin language. } $count = (int) ( new \WP_Query( $args ) )->found_posts; set_transient( 'prodtabs_products_with_tabs', $count, 12 * HOUR_IN_SECONDS ); return $count; } /** * Tabs not translated yet, by language (Polylang and WPML): tabs of a product whose translation has no text of its * own, and shared tabs without a published translation. Counted when the overview opens, on the first * SCAN_LIMIT products of the default language that have tabs. * * @return int[] Number of tabs by language code; languages with none are left out. */ public static function untranslated(): array { $default = Multilingual::default_language(); $languages = Multilingual::languages(); if ( '' === $default || count( $languages ) < 2 || ! in_array( Multilingual::plugin(), array( 'polylang', 'wpml' ), true ) ) { return array(); } $counts = array(); $scanned = 0; // WPML ignores the language of get_posts(): pages are read until SCAN_LIMIT products of the default language are. for ( $page = 1; $scanned < self::SCAN_LIMIT; $page++ ) { $sources = get_posts( array( 'post_type' => 'product', 'post_status' => array( 'publish', 'future', 'private' ), 'fields' => 'ids', 'posts_per_page' => self::SCAN_LIMIT, 'paged' => $page, 'no_found_rows' => true, 'lang' => $default, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- overview screen only, limited. 'meta_key' => Tabs::META_LAYOUT, ) ); if ( ! $sources ) { break; } foreach ( array_map( 'intval', $sources ) as $id ) { if ( Multilingual::post_language( $id ) !== $default ) { continue; } if ( ++$scanned > self::SCAN_LIMIT ) { break 2; } $keys = array(); foreach ( (array) get_post_meta( $id, Tabs::META_LAYOUT, true ) as $entry ) { if ( is_array( $entry ) && 'local' === ( $entry['type'] ?? '' ) && ! empty( $entry['visible'] ) && isset( $entry['key'] ) ) { $keys[] = (string) $entry['key']; } } if ( ! $keys ) { continue; } foreach ( Multilingual::translation_ids( $id, 'product' ) as $translation ) { $lang = Multilingual::post_language( $translation ); if ( $translation === $id || '' === $lang || $default === $lang ) { continue; } $texts = get_post_meta( $translation, Tabs::META_TEXTS, true ); $missing = count( array_diff( $keys, array_keys( is_array( $texts ) ? $texts : array() ) ) ); if ( $missing ) { $counts[ $lang ] = ( $counts[ $lang ] ?? 0 ) + $missing; } } } } // Every shared tab: a cap would count rows of every language with WPML, and leave out the oldest tabs. $shared = get_posts( array( 'post_type' => Library::POST_TYPE, 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1, 'no_found_rows' => true, 'lang' => $default, ) ); foreach ( array_map( 'intval', $shared ) as $id ) { if ( Multilingual::post_language( $id ) !== $default ) { continue; } $published = array(); foreach ( Multilingual::translation_ids( $id, Library::POST_TYPE ) as $translation ) { if ( 'publish' === get_post_status( $translation ) ) { $published[ Multilingual::post_language( $translation ) ] = true; } } foreach ( $languages as $lang ) { if ( $default !== $lang && ! isset( $published[ $lang ] ) ) { $counts[ $lang ] = ( $counts[ $lang ] ?? 0 ) + 1; } } } return $counts; } /** * Shared tabs, one per group of translations. * * @return int[] published, and of these shown on their own by their "Show on" setting. */ private static function shared_tabs(): array { // Every language, then one tab per group: a filter on the default language works with Polylang but not with WPML, // whose get_posts() returns every language. No cap: it would count rows of every language, not groups. $args = array( 'post_type' => Library::POST_TYPE, 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1, 'no_found_rows' => true, 'lang' => '', ); $ids = array_map( 'intval', (array) Multilingual::all_languages( static function () use ( $args ) { return get_posts( $args ); } ) ); $seen = array(); $tabs = array(); foreach ( $ids as $id ) { if ( isset( $seen[ $id ] ) ) { continue; } foreach ( Multilingual::translation_ids( $id, Library::POST_TYPE ) as $member ) { $seen[ $member ] = true; } $tabs[] = $id; } $auto = 0; foreach ( $tabs as $id ) { $auto += '' !== (string) get_post_meta( $id, Library::META_SCOPE, true ) ? 1 : 0; } return array( 'published' => count( $tabs ), 'auto' => $auto, ); } /** * WooCommerce tabs in the order of the settings. * * @return array[] name and visible, by tab. */ private static function natives(): array { $labels = array( 'description' => __( 'Description', 'prodtabs' ), 'additional_information' => __( 'Additional information', 'prodtabs' ), 'reviews' => __( 'Reviews', 'prodtabs' ), ); $natives = Settings::get( 'natives' ); $out = array(); foreach ( Settings::get( 'native_order' ) as $key ) { $out[] = array( 'name' => '' !== $natives[ $key ]['title'] ? trim( str_replace( '{count}', '', $natives[ $key ]['title'] ) ) : $labels[ $key ], 'visible' => (bool) $natives[ $key ]['visible'], ); } return $out; } /** Name of the translation plugin. */ private static function translation_plugin(): string { $names = array( 'polylang' => 'Polylang', 'wpml' => 'WPML', 'translatepress' => 'TranslatePress', 'weglot' => 'Weglot', ); return $names[ Multilingual::plugin() ] ?? ''; } /** * Inline icon of a card, drawn with strokes in the current color. * * @param string $name tabs, woo-tabs, shared, languages or import. */ private static function icon( string $name ): string { $paths = array( 'tabs' => '', 'woo-tabs' => '', 'shared' => '', 'languages' => '', 'import' => '', 'check' => '', ); return ''; } /** * Allowed markup of the inline icons. */ private static function icon_tags(): array { $common = array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'd' => true, 'cx' => true, 'cy' => true, 'r' => true, ); return array( 'svg' => array( 'width' => true, 'height' => true, 'viewbox' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'aria-hidden' => true, 'focusable' => true, ), 'rect' => $common, 'path' => $common, 'circle' => $common, ); } /** Overview screen. */ public static function render(): void { $shared = self::shared_tabs(); $products = self::products_with_tabs(); $yikes = Import_Yikes::detect(); $plugin = self::translation_plugin(); $languages = Multilingual::languages(); $untranslated = self::untranslated(); $svg = self::icon_tags(); ?>

[prodtabs]
[prodtabs product="123"]
[prodtabs product="123" tab="shipping"]
[prodtabs shared="45"]