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' => '