is_plugin_active() ) { $languages = $integration->get_available_languages(); $all_languages = array_merge( $all_languages, $languages ); } } // Remove duplicates and default language $all_languages = array_unique( $all_languages ); $default_language = $this->get_default_language(); if ( $default_language ) { $all_languages = array_filter( $all_languages, function ( $lang ) use ( $default_language ) { return $lang !== $default_language; } ); } return array_values( $all_languages ); } /** * Get the default language from the first active localization plugin * * @return string|null Default language code or null if not available */ public function get_default_language(): ?string { $integration = IntegrationRegistry::get_active_localization_integration(); if ( $integration ) { return $integration->get_default_language(); } return null; } /** * Get products from the default language * * Uses the first active localization plugin to get products from the default language. * * @param int $limit Maximum number of products to return (-1 for all products, legacy feed behavior) * @param int $offset Offset for pagination * @return array Array of product IDs from the default language */ public function get_products_from_default_language( int $limit = 10, int $offset = 0 ): array { $integration = IntegrationRegistry::get_active_localization_integration(); if ( $integration ) { return $integration->get_products_from_default_language( $limit, $offset ); } // Fallback: get regular products if no localization plugin is active $args = [ 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => $limit, 'offset' => $offset, 'fields' => 'ids', ]; return get_posts( $args ); } /** * Get detailed translation information for a product * * Uses the first active localization plugin to get translation details. * * @param int $product_id Product ID (should be from default language) * @return array Detailed translation information */ public function get_product_translation_details( int $product_id ): array { $integration = IntegrationRegistry::get_active_localization_integration(); if ( $integration ) { return $integration->get_product_translation_details( $product_id ); } // Fallback: return basic structure if no localization plugin is active return [ 'product_id' => $product_id, 'default_language' => null, 'translations' => [], 'translation_status' => [], 'translated_fields' => [], ]; } // =========================================== // CSV FORMATTING AND GENERATION METHODS // =========================================== /** * Get all unique translated fields across all products for a language * * @param string $language_code Language code * @param int $limit Maximum number of products to check * @return array Array of unique field names that have translations */ public function get_translated_fields_for_language( string $language_code, int $limit = 100 ): array { if ( ! IntegrationRegistry::has_active_localization_plugin() ) { return []; } $product_ids = $this->get_products_from_default_language( $limit, 0 ); $all_translated_fields = []; foreach ( $product_ids as $product_id ) { $details = $this->get_product_translation_details( $product_id ); if ( isset( $details['translated_fields'][ $language_code ] ) ) { $translated_fields = $details['translated_fields'][ $language_code ]; $all_translated_fields = array_merge( $all_translated_fields, $translated_fields ); } } return array_unique( $all_translated_fields ); } /** * Map translated field names to Facebook CSV column names * * @param array $translated_fields Array of translated field names * @return array Array of Facebook CSV column names */ private function map_translated_fields_to_csv_columns( array $translated_fields ): array { // Mapping from WPML field names to Facebook CSV column names $field_mapping = [ 'name' => 'title', 'description' => 'description', 'short_description' => 'short_description', 'rich_text_description' => 'rich_text_description', 'image_id' => 'image_link', 'gallery_image_ids' => 'additional_image_link', 'link' => 'link', 'video' => 'video', ]; $csv_columns = []; foreach ( $translated_fields as $field ) { if ( isset( $field_mapping[ $field ] ) ) { $csv_columns[] = $field_mapping[ $field ]; } } // Remove duplicates and ensure required columns $csv_columns = array_unique( $csv_columns ); return $csv_columns; } /** * Extract translation data and convert to CSV format for a specific language * * This method processes products from the default language and generates CSV rows * for Facebook's language override feed. It handles both simple and variable products. * * **Variable Product Behavior:** * For variable products, localization plugins do NOT support direct 1:1 variation-level * translations. Instead, this method: * 1. Gets the translated parent product content (title, description, images) * 2. Applies that translated content to ALL variations of the original product * 3. Each variation gets its own CSV row with the same translated parent content * * Note: Multiple plugin handling is managed by IntegrationRegistry::get_active_localization_integration() * * @param string $language_code Language code (e.g., 'es_ES', 'fr_FR') * @param int $limit Maximum number of products to process * @param int $offset Offset for pagination * @return array CSV data ready for conversion to CSV string with dynamic columns */ public function get_language_csv_data( string $language_code, int $limit = 100, int $offset = 0 ): array { if ( ! IntegrationRegistry::has_active_localization_plugin() ) { return [ 'data' => [], 'columns' => [ 'id', 'override' ], 'translated_fields' => [], ]; } // First, determine which fields are translated for this language $translated_fields = $this->get_translated_fields_for_language( $language_code, $limit ); $csv_columns = $this->map_translated_fields_to_csv_columns( $translated_fields ); $product_ids = $this->get_products_from_default_language( $limit, $offset ); $csv_data = []; foreach ( $product_ids as $product_id ) { // Skip products that don't pass Facebook sync validation $original_product = wc_get_product( $product_id ); if ( ! $original_product ) { continue; } // Use Facebook's product sync validator if available if ( function_exists( 'facebook_for_woocommerce' ) ) { $sync_validator = facebook_for_woocommerce()->get_product_sync_validator( $original_product ); if ( ! $sync_validator->passes_all_checks() ) { continue; } } $details = $this->get_product_translation_details( $product_id ); if ( empty( $details['translations'] ) || ! isset( $details['translations'][ $language_code ] ) ) { continue; } $translated_id = $details['translations'][ $language_code ]; $product_translated_fields = $details['translated_fields'][ $language_code ] ?? []; // Only include products that have actual translated content if ( empty( $product_translated_fields ) ) { continue; } $translated_product = wc_get_product( $translated_id ); if ( ! $translated_product ) { continue; } // Create Facebook product instances for proper field extraction if ( ! class_exists( 'WC_Facebook_Product' ) ) { require_once WC_FACEBOOKCOMMERCE_PLUGIN_DIR . '/includes/fbproduct.php'; } $original_fb_product = new \WC_Facebook_Product( $original_product ); $translated_fb_product = new \WC_Facebook_Product( $translated_product ); // Determine the ID based on product type (matching catalog backend logic): // - Simple products: Use just the woo_id // - Variable products: Iterate through each variation and use the variation woo_id if ( $original_product->is_type( 'variable' ) ) { // For variable products, build the translated content once (it's the same for all variations) $translated_content = [ 'override' => \WooCommerce\Facebook\Locale::convert_to_facebook_language_code( $language_code ), ]; // Build translated field values once (they're shared across all variations) foreach ( $csv_columns as $column ) { $translated_content[ $column ] = $this->get_translated_field_value( $column, $original_fb_product, $translated_fb_product, $product_translated_fields, $language_code ); } // Check if at least one translatable field has content $has_content = false; foreach ( $csv_columns as $column ) { if ( ! empty( $translated_content[ $column ] ) ) { $has_content = true; break; } } // If there's no translated content, skip this entire variable product if ( ! $has_content ) { continue; } // Now iterate through variations and create a row for each with the same content $variations = $original_product->get_children(); foreach ( $variations as $variation_id ) { $variation = wc_get_product( $variation_id ); if ( ! $variation ) { continue; // Skip invalid variations } // Get product ID based on content ID migration rollout switch $variation_id = $this->get_product_id( $variation ); // Create row with the variation ID and reuse the translated content $csv_row = array_merge( [ 'id' => $variation_id ], $translated_content ); $csv_data[] = $csv_row; } } else { // Get product ID based on content ID migration rollout switch $product_id = $this->get_product_id( $original_product ); // Start with required columns $csv_row = [ 'id' => $product_id, 'override' => \WooCommerce\Facebook\Locale::convert_to_facebook_language_code( $language_code ), ]; // Add dynamic columns based on what's actually translated foreach ( $csv_columns as $column ) { $csv_row[ $column ] = $this->get_translated_field_value( $column, $original_fb_product, $translated_fb_product, $product_translated_fields, $language_code ); } // Only add row if at least one translatable field has content $has_content = false; foreach ( $csv_columns as $column ) { if ( ! empty( $csv_row[ $column ] ) ) { $has_content = true; break; } } if ( $has_content ) { $csv_data[] = $csv_row; } } } return [ 'data' => $csv_data, 'columns' => array_merge( [ 'id', 'override' ], $csv_columns ), 'translated_fields' => $translated_fields, ]; } /** * Get the value for a specific translated field using Facebook product methods * * @param string $column CSV column name * @param \WC_Facebook_Product $original_fb_product Original Facebook product * @param \WC_Facebook_Product $translated_fb_product Translated Facebook product * @param array $product_translated_fields Fields that are translated for this product * @param string $language_code Target language code for permalink translation * @return string Field value with proper validation and cleaning */ private function get_translated_field_value( string $column, \WC_Facebook_Product $original_fb_product, \WC_Facebook_Product $translated_fb_product, array $product_translated_fields, string $language_code = '' ): string { // Import required classes for validation if ( ! class_exists( '\WooCommerce\Facebook\Framework\Helper' ) ) { require_once WC_FACEBOOKCOMMERCE_PLUGIN_DIR . '/includes/Framework/Helper.php'; } $value = ''; switch ( $column ) { case 'title': if ( in_array( 'name', $product_translated_fields, true ) ) { $title = $translated_fb_product->get_name(); $value = \WooCommerce\Facebook\Framework\Helper::str_truncate( \WC_Facebookcommerce_Utils::clean_string( $title ), \WC_Facebook_Product::MAX_TITLE_LENGTH ); } break; case 'description': if ( in_array( 'description', $product_translated_fields, true ) ) { // Use get_fb_description() to match main feed behavior (strips HTML tags) $description = $translated_fb_product->get_fb_description(); $value = \WooCommerce\Facebook\Framework\Helper::str_truncate( $description, \WC_Facebook_Product::MAX_DESCRIPTION_LENGTH ); } break; case 'short_description': if ( in_array( 'short_description', $product_translated_fields, true ) ) { $short_description = $translated_fb_product->get_fb_short_description(); $value = \WooCommerce\Facebook\Framework\Helper::str_truncate( $short_description, \WC_Facebook_Product::MAX_DESCRIPTION_LENGTH ); } break; case 'rich_text_description': if ( in_array( 'rich_text_description', $product_translated_fields, true ) ) { $rich_text_description = $translated_fb_product->get_rich_text_description(); $value = \WooCommerce\Facebook\Framework\Helper::str_truncate( $rich_text_description, \WC_Facebook_Product::MAX_DESCRIPTION_LENGTH ); } break; case 'image_link': if ( in_array( 'image_id', $product_translated_fields, true ) ) { $image_urls = $translated_fb_product->get_all_image_urls(); $value = $image_urls[0] ?? ''; } break; case 'additional_image_link': if ( in_array( 'gallery_image_ids', $product_translated_fields, true ) ) { $image_urls = $translated_fb_product->get_all_image_urls(); $additional_images = array_slice( $image_urls, 1, 5 ); // Max 5 additional images $value = ! empty( $additional_images ) ? implode( ',', $additional_images ) : ''; } break; case 'video': if ( in_array( 'video', $product_translated_fields, true ) ) { $video_urls = $translated_fb_product->get_all_video_urls(); if ( ! empty( $video_urls ) ) { // Format video URLs as JSON array of objects with 'url' key, matching main feed format $value = wp_json_encode( $video_urls ); } } break; case 'link': if ( in_array( 'link', $product_translated_fields, true ) ) { // Use the translated product's built-in get_permalink() method // This automatically handles the correct URL structure and translated slugs $value = $translated_fb_product->get_permalink(); } break; } return $value; } /** * Format price for CSV using Facebook's approach * * @param int $price Price in cents * @param string $currency Currency code * @return string Formatted price */ private function format_price_for_csv( $price, $currency ): string { return (string) ( round( $price / 100.0, 2 ) ) . ' ' . $currency; } /** * Get the product ID based on the content ID migration rollout switch * * @param \WC_Product $product Product object * @return string Product ID (either simple ID or retailer ID based on GK) */ private function get_product_id( \WC_Product $product ): string { // Check if content ID migration is enabled if ( function_exists( 'facebook_for_woocommerce' ) ) { $rollout_switches = facebook_for_woocommerce()->get_rollout_switches(); if ( $rollout_switches && $rollout_switches->is_switch_enabled( \WooCommerce\Facebook\RolloutSwitches::SWITCH_CONTENT_ID_MIGRATION_ENABLED ) ) { // New behavior: Use simple WooCommerce ID return (string) $product->get_id(); } } // Old behavior: Use Facebook retailer ID format return \WC_Facebookcommerce_Utils::get_fb_retailer_id( $product ); } }