' . esc_html__( 'Google Merchant feed settings. Leave fields empty to use WooCommerce data and automatic detection.', '3dl-product-feed-for-google' ) . '

'; }, 'tdl_gmc_page' ); add_settings_section( 'tdl_gmc_ids', __( 'Product identifiers', '3dl-product-feed-for-google' ), function () { echo '

' . esc_html__( 'The plugin checks the built-in WooCommerce GTIN first, then common meta keys, and finally your custom keys.', '3dl-product-feed-for-google' ) . '

'; }, 'tdl_gmc_page' ); add_settings_section( 'tdl_gmc_filters', __( 'Item filters', '3dl-product-feed-for-google' ), function () { echo '

' . esc_html__( 'An empty field means no restriction. These filters apply only to the Google feed.', '3dl-product-feed-for-google' ) . '

'; }, 'tdl_gmc_page' ); add_settings_field( 'tdl_gmc_brand_name', __( 'Default brand / manufacturer', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_brand' ], 'tdl_gmc_page', 'tdl_gmc_general' ); add_settings_field( 'tdl_gmc_brand_use_site_name', __( 'Use store name as fallback', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_site_brand' ], 'tdl_gmc_page', 'tdl_gmc_general' ); add_settings_field( 'tdl_gmc_feed_channel_description', __( 'Feed description', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_description' ], 'tdl_gmc_page', 'tdl_gmc_general' ); add_settings_field( 'tdl_gmc_gtin_keys', __( 'Custom GTIN/EAN meta keys', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_gtin' ], 'tdl_gmc_page', 'tdl_gmc_ids' ); add_settings_field( 'tdl_gmc_mpn_keys', __( 'Custom MPN meta keys', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_mpn' ], 'tdl_gmc_page', 'tdl_gmc_ids' ); add_settings_field( 'tdl_gmc_use_sku_as_mpn', __( 'Use SKU as MPN', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_sku_mpn' ], 'tdl_gmc_page', 'tdl_gmc_ids' ); add_settings_field( 'tdl_gmc_filter_flags', __( 'Basic exclusions', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_filter_flags' ], 'tdl_gmc_page', 'tdl_gmc_filters' ); add_settings_field( 'tdl_gmc_filter_weights', __( 'Allowed weights / volumes', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_weights' ], 'tdl_gmc_page', 'tdl_gmc_filters' ); add_settings_field( 'tdl_gmc_filter_cat_include', __( 'Include only categories', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_cat_include' ], 'tdl_gmc_page', 'tdl_gmc_filters' ); add_settings_field( 'tdl_gmc_filter_cat_exclude', __( 'Exclude categories', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_cat_exclude' ], 'tdl_gmc_page', 'tdl_gmc_filters' ); add_settings_field( 'tdl_gmc_filter_skus', __( 'Include only SKUs', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_skus' ], 'tdl_gmc_page', 'tdl_gmc_filters' ); add_settings_field( 'tdl_gmc_filter_price', __( 'Price including tax – min / max', '3dl-product-feed-for-google' ), [ __CLASS__, 'field_price' ], 'tdl_gmc_page', 'tdl_gmc_filters' ); } private static function register_text( string $name ): void { register_setting( 'tdl_gmc_group', $name, [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '' ] ); } private static function register_area( string $name ): void { register_setting( 'tdl_gmc_group', $name, [ 'type' => 'string', 'sanitize_callback' => static function ( $v ): string { return is_string( $v ) ? sanitize_textarea_field( $v ) : ''; }, 'default' => '' ] ); } private static function register_bool( string $name, bool $default ): void { register_setting( 'tdl_gmc_group', $name, [ 'type' => 'boolean', 'sanitize_callback' => static function ( $v ): bool { return ! empty( $v ); }, 'default' => $default ] ); } public static function field_brand(): void { echo ''; echo '

' . esc_html__( 'Fallback for products without their own brand. The plugin can read WooCommerce Brands and common brand attributes.', '3dl-product-feed-for-google' ) . '

'; } public static function field_site_brand(): void { echo ''; echo ''; } public static function field_description(): void { echo ''; } public static function field_gtin(): void { echo ''; echo '

' . esc_html__( 'Optional. Enter custom meta keys separated by commas or new lines.', '3dl-product-feed-for-google' ) . '

'; } public static function field_mpn(): void { echo ''; echo '

' . esc_html__( 'Optional. Enter custom meta keys separated by commas or new lines.', '3dl-product-feed-for-google' ) . '

'; } public static function field_sku_mpn(): void { echo ''; echo ''; } public static function field_filter_flags(): void { echo ''; echo '
'; echo ''; echo ''; } public static function field_weights(): void { echo ''; echo '

' . esc_html__( 'One allowed selling quantity per line, for example 50g, 250g, 1kg or 500ml.', '3dl-product-feed-for-google' ) . '

'; } public static function field_cat_include(): void { echo ''; echo '

' . esc_html__( 'Category slug or category ID, one per line.', '3dl-product-feed-for-google' ) . '

'; } public static function field_cat_exclude(): void { echo ''; echo '

' . esc_html__( 'Category slug or category ID, one per line.', '3dl-product-feed-for-google' ) . '

'; } public static function field_skus(): void { echo ''; echo '

' . esc_html__( 'One allowed SKU per line.', '3dl-product-feed-for-google' ) . '

'; } public static function field_price(): void { echo '   '; echo ''; } public static function menu(): void { add_menu_page( __( '3DL Google Feed', '3dl-product-feed-for-google' ), __( '3DL Google Feed', '3dl-product-feed-for-google' ), THREEDL_GMC_CAP, 'tdl-gmc-lite', [ __CLASS__, 'render' ], 'dashicons-media-spreadsheet', 56 ); } public static function render(): void { if ( ! current_user_can( THREEDL_GMC_CAP ) ) return; echo '

' . esc_html__( '3DL Product Feed for Google', '3dl-product-feed-for-google' ) . '

'; self::render_notice(); self::render_feed_rules(); echo '
'; settings_fields( 'tdl_gmc_group' ); do_settings_sections( 'tdl_gmc_page' ); submit_button(); echo '
'; self::render_status(); self::render_diagnostics(); echo '

' . esc_html__( 'Need more feeds?', '3dl-product-feed-for-google' ) . '

'; echo '

' . wp_kses_post( sprintf( __( 'The PRO version adds Zboží.cz, Heureka.cz, Google custom labels and licensed automatic updates. Learn more at %s.', '3dl-product-feed-for-google' ), '3DragonLabs.com' ) ) . '

'; echo '
'; } private static function render_notice(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only status from our own redirect. $notice = isset( $_GET['tdl_feed_notice'] ) ? sanitize_key( wp_unslash( $_GET['tdl_feed_notice'] ) ) : ''; if ( $notice === '' ) return; $class = 'notice notice-success inline'; $message = ''; if ( $notice === 'cache_cleared' ) $message = __( 'Feed cache and diagnostics were cleared.', '3dl-product-feed-for-google' ); if ( $notice === 'rebuilt' ) $message = __( 'Feed and diagnostics were regenerated successfully.', '3dl-product-feed-for-google' ); if ( $notice === 'rebuild_failed' ) { $class = 'notice notice-error inline'; $message = __( 'Feed regeneration failed. The previous cached feed, if available, was left untouched.', '3dl-product-feed-for-google' ); } if ( $message !== '' ) echo '

' . esc_html( $message ) . '

'; } private static function render_feed_rules(): void { echo '
'; echo '

' . esc_html__( 'How the feed works', '3dl-product-feed-for-google' ) . '

'; echo ''; echo '

' . esc_html__( 'These rules help prevent the feed from containing items that Google Merchant Center would reject immediately.', '3dl-product-feed-for-google' ) . '

'; echo '
'; } private static function render_status(): void { $info = Feed_Cache::instance()->cache_info( 'xml' ); $cache = esc_html__( 'not generated yet', '3dl-product-feed-for-google' ); if ( ! empty( $info['exists'] ) ) { $state = ! empty( $info['fresh'] ) ? __( 'fresh', '3dl-product-feed-for-google' ) : __( 'stale', '3dl-product-feed-for-google' ); $cache = sprintf( '%s / %s / %s', size_format( (int) $info['size'] ), wp_date( 'Y-m-d H:i:s', (int) $info['mtime'] ), $state ); } $url = site_url( '/3dl-gmc-feed.xml' ); $clear = add_query_arg( [ 'page' => 'tdl-gmc-lite', 'tdl_feed_action' => 'clear_cache', '_wpnonce' => wp_create_nonce( 'tdl_feed_admin_action' ) ], admin_url( 'admin.php' ) ); $rebuild = add_query_arg( [ 'page' => 'tdl-gmc-lite', 'tdl_feed_action' => 'rebuild_cache', '_wpnonce' => wp_create_nonce( 'tdl_feed_admin_action' ) ], admin_url( 'admin.php' ) ); echo '

' . esc_html__( 'Feed and cache', '3dl-product-feed-for-google' ) . '

'; echo '

Google: ' . esc_html( $url ) . '

'; echo '

' . esc_html__( 'Cache:', '3dl-product-feed-for-google' ) . ' ' . esc_html( $cache ) . '

'; echo '

' . esc_html__( 'Regenerate feed & diagnostics', '3dl-product-feed-for-google' ) . ' '; echo '' . esc_html__( 'Clear feed cache', '3dl-product-feed-for-google' ) . '

'; } private static function render_diagnostics(): void { $stats = Feed_Cache::instance()->stats_info( 'xml' ); echo '

' . esc_html__( 'Feed diagnostics', '3dl-product-feed-for-google' ) . '

'; if ( ! $stats ) { echo '

' . esc_html__( 'No diagnostics are available yet. Regenerate the feed to calculate included and skipped items.', '3dl-product-feed-for-google' ) . '

'; return; } $candidates = isset( $stats['candidates'] ) ? (int) $stats['candidates'] : 0; $included = isset( $stats['included'] ) ? (int) $stats['included'] : 0; $skipped = isset( $stats['skipped'] ) ? (int) $stats['skipped'] : max( 0, $candidates - $included ); echo '
'; self::stat_box( __( 'Candidates checked', '3dl-product-feed-for-google' ), $candidates ); self::stat_box( __( 'Included in feed', '3dl-product-feed-for-google' ), $included ); self::stat_box( __( 'Skipped', '3dl-product-feed-for-google' ), $skipped ); echo '
'; if ( ! empty( $stats['generated_at'] ) ) { echo '

' . esc_html( sprintf( __( 'Counts reflect the feed generated on %s.', '3dl-product-feed-for-google' ), wp_date( 'Y-m-d H:i:s', (int) $stats['generated_at'] ) ) ) . '

'; } $reasons = isset( $stats['reasons'] ) && is_array( $stats['reasons'] ) ? $stats['reasons'] : []; if ( ! $reasons ) return; $labels = self::diagnostic_labels(); arsort( $reasons ); echo '

' . esc_html__( 'Skipped items by reason', '3dl-product-feed-for-google' ) . '

'; echo ''; foreach ( $reasons as $reason => $count ) { if ( (int) $count <= 0 ) continue; $label = isset( $labels[ $reason ] ) ? $labels[ $reason ] : __( 'Other filter or validation rule', '3dl-product-feed-for-google' ); echo ''; } echo '
' . esc_html__( 'Reason', '3dl-product-feed-for-google' ) . '' . esc_html__( 'Items', '3dl-product-feed-for-google' ) . '
' . esc_html( $label ) . '' . esc_html( number_format_i18n( (int) $count ) ) . '
'; } private static function stat_box( string $label, int $value ): void { echo '
'; echo '
' . esc_html( number_format_i18n( $value ) ) . '
'; echo '
' . esc_html( $label ) . '
'; echo '
'; } private static function diagnostic_labels(): array { return [ 'invalid_variation' => __( 'Invalid or unreadable variation', '3dl-product-feed-for-google' ), 'not_published' => __( 'Variation is not published', '3dl-product-feed-for-google' ), 'not_purchasable' => __( 'Product or variation is not purchasable', '3dl-product-feed-for-google' ), 'out_of_stock' => __( 'Out of stock and excluded by settings', '3dl-product-feed-for-google' ), 'zero_price' => __( 'Zero price and excluded by settings', '3dl-product-feed-for-google' ), 'sku_filter' => __( 'Excluded by SKU filter', '3dl-product-feed-for-google' ), 'category_filter' => __( 'Excluded by category filter', '3dl-product-feed-for-google' ), 'price_filter' => __( 'Outside the configured price range', '3dl-product-feed-for-google' ), 'measure_filter' => __( 'Excluded by weight / volume filter', '3dl-product-feed-for-google' ), 'custom_filter' => __( 'Excluded by a developer filter', '3dl-product-feed-for-google' ), 'missing_title' => __( 'Missing usable title', '3dl-product-feed-for-google' ), 'missing_price' => __( 'Missing or invalid price', '3dl-product-feed-for-google' ), 'missing_image' => __( 'Missing usable product image', '3dl-product-feed-for-google' ), ]; } }