_n('%1$s product', '%1$s products', 1, 'facetcore'), /* translators: %1$s: formatted number of products. */ 'productsMany' => _n('%1$s product', '%1$s products', 2, 'facetcore'), 'unitOne' => _n('product', 'products', 1, 'facetcore'), 'unitMany' => _n('product', 'products', 2, 'facetcore'), /* translators: 1: product count such as "12 products", 2: current page number, 3: total pages. */ 'statusFound' => __('%1$s found. Page %2$s of %3$s.', 'facetcore'), 'updating' => __('Updating products.', 'facetcore'), 'loadFailedTitle' => __('Products could not be loaded', 'facetcore'), 'loadFailedText' => __('Please try again in a moment.', 'facetcore'), 'productsUnavailable' => __('Products unavailable', 'facetcore'), 'updateFailed' => __('Unable to update products. Please try again.', 'facetcore'), 'updateFailedStatus' => __('Unable to update products.', 'facetcore'), 'tryAgain' => __('Try again', 'facetcore'), /* translators: %1$s: selected filter, e.g. "Color: Red". */ 'remove' => __('Remove %1$s', 'facetcore'), 'allSpecifications' => __('All specifications', 'facetcore'), 'view' => __('View', 'facetcore'), 'table' => __('Table', 'facetcore'), 'grid' => __('Grid', 'facetcore'), 'showLess' => __('Show less', 'facetcore'), /* translators: %1$s: number of filter values. */ 'showAll' => __('Show all %1$s', 'facetcore'), 'noMoreFacets' => __('No further specifications to refine.', 'facetcore'), 'emptyTitle' => __('No products match this selection', 'facetcore'), 'emptyText' => __('Try another search or clear your filters to see more products.', 'facetcore'), 'clearFilters' => __('Clear filters', 'facetcore'), 'sku' => __('SKU', 'facetcore'), 'product' => __('Product', 'facetcore'), 'price' => __('Price', 'facetcore'), 'availability' => __('Availability', 'facetcore'), 'action' => __('Action', 'facetcore'), 'notAvailable' => __('Not available', 'facetcore'), 'inStock' => __('In stock', 'facetcore'), 'outOfStock' => __('Out of stock', 'facetcore'), 'onBackorder' => __('On backorder', 'facetcore'), 'unavailable' => __('Unavailable', 'facetcore'), 'productPages' => __('Product pages', 'facetcore'), 'previous' => __('← Previous', 'facetcore'), 'next' => __('Next →', 'facetcore'), /* translators: 1: current page number, 2: total pages. */ 'pageOf' => __('Page %1$s of %2$s', 'facetcore'), 'tableRegion' => __('Matching products table. Scroll horizontally for more columns.', 'facetcore'), 'tableCaption' => __('Matching products and specifications', 'facetcore'), 'matchingProducts' => __('Matching products', 'facetcore'), /* translators: 1: first product number on the page, 2: last product number on the page, 3: total products. */ 'range' => __('%1$s–%2$s of %3$s', 'facetcore'), ]; } public function shortcode(array $atts = []): string { $atts = shortcode_atts(['per_page' => 20, 'id' => ''], $atts, 'facetcore'); $legacy = $atts['id'] === ''; $tableId = ''; $finderId = 0; $config = TableConfiguration::effective(array_diff_key(TableConfiguration::defaults(), array_flip(TableConfiguration::extensionKeys()))); if ($atts['id'] !== '') { $finderId = is_scalar($atts['id']) && preg_match('/^[1-9][0-9]*$/D', (string) $atts['id']) ? (int) $atts['id'] : 0; $config = $finderId ? TableConfiguration::get($finderId) : null; if (!$config) { return current_user_can('manage_woocommerce') ? '

' . esc_html__('FacetCore: this Finder is unavailable. Check the ID, publication status and password.', 'facetcore') . '

' : ''; } $tableId = ' data-table-id="' . $finderId . '"'; $atts['per_page'] = $config['per_page']; } $perPage = max(1, min(100, (int) $atts['per_page'])); wp_enqueue_script('facetcore-frontend'); wp_enqueue_style('facetcore-frontend'); $id = !$legacy && $finderId ? 'facetcore-' . $finderId : 'facetcore-' . wp_generate_uuid4(); $baseStateKey = $legacy ? 'legacy' : (string) $finderId; self::$stateKeys[$baseStateKey] = (self::$stateKeys[$baseStateKey] ?? 0) + 1; $stateKey = $baseStateKey . (self::$stateKeys[$baseStateKey] > 1 || $legacy ? '-' . self::$stateKeys[$baseStateKey] : ''); do_action('facetcore_frontend_assets', $config, $finderId); return '
' . '

' . esc_html__('Product finder', 'facetcore') . '

' . esc_html__('Choose specifications to find the right product.', 'facetcore') . '

' . esc_html__('Loading products…', 'facetcore') . '
' . ($config['enable_search'] ? '
' : '') . '
' . '
' . wp_kses(apply_filters('facetcore_frontend_controls', ''), self::controlHtml()) . '
' . '

' . esc_html__('Loading products.', 'facetcore') . '

' . '
' . '
'; } /** * Extension controls are HTML, not post content. Keep form controls and the * Pro data/ARIA hooks, but never scripts, event handlers or embedded content. * Extensions must still escape each dynamic value for its own context. */ private static function controlHtml(): array { $global = ['id' => true, 'class' => true, 'title' => true, 'role' => true, 'hidden' => true, 'tabindex' => true, 'data-*' => true]; // Unlike data-*, KSES does not support an aria-* wildcard. $global += array_fill_keys(['aria-atomic', 'aria-busy', 'aria-checked', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-disabled', 'aria-expanded', 'aria-haspopup', 'aria-hidden', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-multiselectable', 'aria-orientation', 'aria-pressed', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-selected', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext'], true); $allowed = []; foreach (['div', 'span', 'p', 'strong', 'em', 'small', 'ul', 'ol', 'li', 'legend'] as $tag) { $allowed[$tag] = $global; } $allowed['a'] = $global + ['href' => true, 'rel' => true]; $allowed['form'] = $global + ['action' => true, 'method' => true]; $allowed['fieldset'] = $global + ['disabled' => true, 'name' => true]; $allowed['label'] = $global + ['for' => true]; $allowed['input'] = $global + array_fill_keys(['type', 'name', 'value', 'placeholder', 'checked', 'disabled', 'readonly', 'required', 'min', 'max', 'step', 'minlength', 'maxlength', 'size', 'multiple', 'autocomplete'], true); $allowed['select'] = $global + ['name' => true, 'disabled' => true, 'required' => true, 'multiple' => true, 'size' => true]; $allowed['option'] = $global + ['value' => true, 'selected' => true, 'disabled' => true, 'label' => true]; $allowed['optgroup'] = $global + ['label' => true, 'disabled' => true]; $allowed['button'] = $global + ['type' => true, 'name' => true, 'value' => true, 'disabled' => true]; return $allowed; } }