Facing issue with RTL-Arabic mapping. Swatch dont show the variant as soon as I switch to Arabic from English. Using storefront api to inject remaining variants if above 250

I’m facing an issue with RTL/Arabic localization on Shopify storefront variant swatches.

Setup

  • Storefront uses Shopify Storefront API

  • Product has more than 250 variants

  • We inject/load the remaining variants separately through the Storefront API because of the variant limit

  • Swatches work correctly in English (LTR)

Problem

As soon as I switch the storefront language from English to Arabic (RTL):

  • Variant swatches stop showing correctly

  • Variant mapping seems broken

  • Selected options are not resolving to the correct variant

  • Some swatches disappear entirely

This only happens in Arabic/RTL. English works fine.

What I noticed

  • Option names/values may be getting translated differently in Arabic

  • Variant matching logic currently depends on option names/values from English

  • Injected variants from the Storefront API may not align with translated option labels

  • Direction change (LTR → RTL) might also affect swatch rendering/order

Current approach

  • Initial variants come from product payload

  • Remaining variants are fetched and merged through Storefront API

  • Swatches are generated dynamically from variant option.

Heres my code :

Product-variant-picker.liquid :

{% comment %}
 Renders product variant-picker

 Accepts:
 - product: {Object} product object.
 - block: {Object} passing the block information.
 - product_form_id: {String} Id of the product form to which the variant picker is associated.
 Usage:
 {% render 'product-variant-picker', product: product, block: block, product_form_id: product_form_id %}
{% endcomment %}
{%- unless product.has_only_default_variant -%}

 <style>
  .color-swatches-skeleton {
    display: flex;
    flex-wrap: wrap;
    gap: 26px;
    padding: 8px 0;
  }

  .color-swatches-skeleton .skeleton-swatch {
    width: 45px;
    height: 45px;
    border-radius: 0%;
    background: linear-gradient(90deg, #e8e8e8 25%, #f5f5f5 50%, #e8e8e8 75%);
    background-size: 200% 100%;
    animation: swatch-shimmer 1.4s infinite ease-in-out;
  }

  @keyframes swatch-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
  }

  /* Hide real swatches while loading */
  .color--option.swatches-loading .color-option {
    display: none !important;
  }

  /* Also hide direct labels in case there's no .color-option wrapper */
  .color--option.swatches-loading label.color-swatch {
    display: none !important;
  }

  /* Hide skeleton once loaded */
  .color--option:not(.swatches-loading) .color-swatches-skeleton {
    display: none !important;
  }
 </style>

 <script type="text/javascript" data-all-variants>
  window.ShopifyProduct = {
    variants: {{ product.variants | json }}
  };

  (function () {
    var DECLARED_COUNT = {{ product.variants_count }};
    var STOREFRONT_TOKEN = '768cddda9b2775eab70e7f5752590b60';
    var PRODUCT_GID = 'gid://shopify/Product/{{ product.id }}';
    var SHOP_DOMAIN = '{{ shop.permanent_domain }}';

    if (DECLARED_COUNT <= 250) return;

    var PAGE_SIZE = 250;
    var allVariants = window.ShopifyProduct.variants.slice();
    var existingIds = new Set(allVariants.map(function(v) { return v.id; }));

    function buildQuery(cursor) {
      var afterClause = cursor ? ', after: "' + cursor + '"' : '';
      return JSON.stringify({
        query: 'query { product(id: "' + PRODUCT_GID + '") { variants(first: ' + PAGE_SIZE + afterClause + ') { pageInfo { hasNextPage endCursor } edges { node { id title sku availableForSale quantityAvailable selectedOptions { name value } price { amount } image { url transformedSrc(maxWidth: 400, maxHeight: 400, crop: CENTER) } } } } } }'
      });
    }

    function mapVariant(node) {
      var opts = node.selectedOptions.map(function(o) { return o.value; });
      return {
        id:                 parseInt(node.id.replace('gid://shopify/ProductVariant/', '')),
        title:              node.title,
        sku:                node.sku,
        available:          node.availableForSale,
        options:            opts,
        option1:            opts[0] || null,
        option2:            opts[1] || null,
        option3:            opts[2] || null,
        price:              Math.round(parseFloat(node.price.amount) * 100),
        inventory_quantity: node.quantityAvailable,
        featured_image:     node.image ? { src: node.image.transformedSrc || node.image.url } : null
      };
    }

    function mergeBatch(edges) {
      edges.forEach(function(e) {
        var v = mapVariant(e.node);
        if (!existingIds.has(v.id)) {
          allVariants.push(v);
          existingIds.add(v.id);
        }
      });
      window.ShopifyProduct.variants = allVariants;
    }

    window.ShopifyProduct._loading = true;

    function fetchChain(cursor) {
      fetch('https://' + SHOP_DOMAIN + '/api/2024-01/graphql.json', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Shopify-Storefront-Access-Token': STOREFRONT_TOKEN
        },
        body: buildQuery(cursor)
      })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        var conn = data.data.product.variants;

        mergeBatch(conn.edges);

        // ✅ Fire partial event after every page so variant-selects can
        // progressively inject swatches without waiting for all pages
        document.dispatchEvent(new CustomEvent('shopify:variants:partial', {
          detail: { loaded: allVariants.length }
        }));
        console.log('[Variants] Progressive load: ' + allVariants.length);

        if (conn.pageInfo.hasNextPage) {
          // ✅ Immediately chain next page — zero idle time between pages
          fetchChain(conn.pageInfo.endCursor);
        } else {
          finalize();
        }
      })
      .catch(function(e) {
        console.warn('[Variants] Fetch failed, using what we have.', e);
        finalize();
      });
    }

    function finalize() {
      window.ShopifyProduct.variants = allVariants;
      window.ShopifyProduct._loading = false;

      injectMissingColorSwatches();

      document.dispatchEvent(new CustomEvent('shopify:variants:loaded', {
        detail: { total: allVariants.length }
      }));
      console.log('[Variants] Total loaded: ' + allVariants.length);
    }

    // Start the chain from the first page
    fetchChain(null);

    function injectMissingColorSwatches(skipFilter) {
      var variants = window.ShopifyProduct.variants;

      var productOptions = {{ product.options | json }};

function getOptionIndex(optionNames, possibleNames) {
  for (var i = 0; i < optionNames.length; i++) {
    var normalized = optionNames[i].toLowerCase();

    for (var j = 0; j < possibleNames.length; j++) {
      if (normalized === possibleNames[j].toLowerCase()) {
        return i;
      }
    }
  }
  return -1;
}

      console.log('[Inject] Total variants:', variants.length);

      var combosByGender = {};
      variants.forEach(function(v) {
        var g = v.option1;
        var c = v.option2;
        if (!g || !c) return;
        if (!combosByGender[g]) combosByGender[g] = new Set();
        combosByGender[g].add(c);
      });

      var allColors = new Set();
      Object.values(combosByGender).forEach(function(colorSet) {
        colorSet.forEach(function(c) { allColors.add(c); });
      });

      var variantSelects = document.querySelector('variant-selects');
      var colorFieldset = (variantSelects || document).querySelector('fieldset[data-name="Color"], fieldset[data-name="لون"]');
      if (!colorFieldset) {
        console.warn('[Inject] No color fieldset found');
        return;
      }

      var existingLabels = colorFieldset.querySelectorAll('label.color-swatch[data-value]');
      var existingColors = new Set();
      existingLabels.forEach(function(l) { existingColors.add(l.dataset.value); });

      var missingColors = [...allColors].filter(function(c) { return !existingColors.has(c); });
      console.log('[Inject] Missing colors to inject:', missingColors);
      if (missingColors.length === 0) return;

      var refLabel = colorFieldset.querySelector('label.color-swatch[data-value]');
      var refInput = refLabel ? colorFieldset.querySelector('input[id="' + refLabel.getAttribute('for') + '"]') : null;
      if (!refLabel || !refInput) return;

      var inputName = refInput.name;

      missingColors.forEach(function(color) {
        var matchVariant = variants.find(function(v) {
          return v.option2 === color && v.featured_image;
        }) || variants.find(function(v) {
          return v.option2 === color;
        });
        var imgSrc = matchVariant && matchVariant.featured_image ? matchVariant.featured_image.src : null;

        var newInput = document.createElement('input');
        newInput.type = 'radio';
        var safeId = 'injected-color-' + color.replace(/[\s|\/\\'"]/g, '-');
        newInput.id = safeId;
        newInput.name = inputName;
        newInput.value = color;
        newInput.dataset.originalValue = color;
        newInput.dataset.productUrl = refInput.dataset.productUrl || '';
        newInput.dataset.optionValueId = refInput.dataset.optionValueId || '';
        newInput.setAttribute('form', refInput.getAttribute('form') || '');
        newInput.hidden = true;

        var newLabel = document.createElement('label');
        newLabel.className = 'color-swatch';
        newLabel.setAttribute('for', safeId);
        newLabel.dataset.value = color;
        newLabel.hidden = true;

        if (imgSrc) {
          var img = document.createElement('img');
          img.src = imgSrc;
          img.alt = color;
          img.loading = 'lazy';
          img.style.display = 'block';
          img.style.maxWidth = '100%';
          img.style.maxHeight = '100%';
          img.style.margin = 'auto';
          newLabel.appendChild(img);
        }

        var hiddenSpan = document.createElement('span');
        hiddenSpan.className = 'visually-hidden label-unavailable';
        newLabel.appendChild(hiddenSpan);

        var colorDiv = colorFieldset.querySelector('.color-option');
        if (colorDiv) {
          colorDiv.appendChild(newInput);
          colorDiv.appendChild(newLabel);
        } else {
          colorFieldset.appendChild(newInput);
          colorFieldset.appendChild(newLabel);
        }

        console.log('[Inject] Injected:', color, '| img:', imgSrc || 'none');
      });

      document.querySelectorAll('variant-selects').forEach(function(vs) {
        vs.colorOptions = [...vs.querySelectorAll('fieldset[data-name="Color"] label.color-swatch, fieldset[data-name="لون"] label.color-swatch')];
        console.log('[Inject] Refreshed colorOptions count:', vs.colorOptions.length);
        if (!skipFilter && !vs.dataset.combined) vs.filterOptions();
      });
    }

    window.injectMissingColorSwatches = injectMissingColorSwatches;

  })();
</script>
 {% if product.tags contains 'combined_listing' %}
  <script type="text/javascript" data-all-variants>
    window.ShopifyProduct = {
     variants: []
    }
    {% assign handles = '' %}
    {% assign colors = '' %}
    {% paginate collections['child-products'].products by 5000 %}
    {% for collection_product in collections['child-products'].products %}
     {% if collection_product.metafields.custom.parent_product.value.id == product.id %}
       window.ShopifyProduct.variants = [... window.ShopifyProduct.variants, ...{{ collection_product.variants | json }}]
       {% if collection_product.options_by_name["Gender"].selected_value == product.options_by_name.Gender.selected_value %}
       {% assign handles = handles | append: collection_product.handle | append: ',' %}
       {% assign colors = colors | append: collection_product.options_by_name["Color"].selected_value | append: ',' %}
       {% endif %}
      {% endif %}
    {% endfor %}
   {% endpaginate -%}
   {% assign handles = handles | split:"," %}
   {% assign colors = colors | split:"," %}
  </script>
 {% endif %}

 <variant-selects
  id="variant-selects-{{ section.id }}-{{ block.id }}"
  data-section="{{ section.id }}"
  {{ block.shopify_attributes }}
  {% if product.tags contains 'combined_listing' %}
   data-combined="true"
  {% endif %}
 >
  {% assign options = child_product.options_with_values %}
  {%- for option in options -%}
   {%- liquid
    assign swatch_count = option.values | map: 'swatch' | compact | size
    assign picker_type = block.settings.picker_type | default: 'button'

    if swatch_count > 0 and block.settings.swatch_shape != 'none'
     if block.settings.picker_type == 'dropdown'
      assign picker_type = 'swatch_dropdown'
     else
      assign picker_type = 'swatch'
     endif
    endif
   -%}
   {%- if picker_type == 'swatch' -%}
    <fieldset class="js product-form__input product-form__input--swatch">
     {% case option.name %}
      {% when 'Color' or 'لون' %}
       <legend class="form__label">
        {{- option.selected_value -}}
       </legend>
      {% when 'Size' or 'مقاس' %}
       {% capture gender %}{{ product.options_by_name['Gender'].selected_value}}'s{% endcapture %}
       <legend class="form__label desktop-standard-01 standard-02 regular">
        {{ 'sections.main-product.size.eu' | t: gender: gender }}
       </legend>
     {% endcase %}
     {% render 'product-variant-options',
      product: product,
      option: option,
      block: block,
      picker_type: picker_type,
      linked_to: linked_to
     %}
    </fieldset>
   {%- elsif picker_type == 'button' -%}
    <fieldset
    {% if option.name == 'Gender' or option.name == 'جنس' %}
      {% if option.values.size < 2 %}
        hidden
      {% endif %}
    {% endif %}
     data-name="{{ option.name }}"
     class="js product-form__input product-form__input--pill medium desktop-standard-02 standard-03{% if option.name == 'Gender' or option.name == 'جنس' %} gender-option toggle{% endif %}{% if option.name == 'Size' or option.name == 'مقاس' %} size--option{% endif %}{% if option.name == 'Color' or option.name == 'لون' %} color--option swatches-loading{% endif %}"
    >
     {% case option.name %}
      {% when 'Color' or 'لون' %}
       <legend class="form__label">
        {{- option.selected_value -}}
       </legend>
       <div class="color-swatches-skeleton" aria-hidden="true">
        {% for i in (1..12) %}
    <span class="skeleton-swatch"></span>
  {% endfor %}
       </div>
      {% when 'Size' or 'مقاس' %}
       {% capture gender %}{{ product.options_by_name['Gender'].selected_value}}'s{% endcapture %}
       {% comment %} <legend class="form__label desktop-standard-01 standard-02 regular">
        {{ 'sections.main-product.size.eu' | t: gender: gender }}
       </legend> {% endcomment %}
       {% comment %} <div class="btn-select-size"> Select a size</div> {% endcomment %}
        <div class="btn-select-size">
  {% if request.locale.iso_code == 'ar' %}
    اختر الحجم
  {% else %}
    Select a size
  {% endif %}
</div>
     <div class="size-secection" style="display:none;">
      <div class="inner_size-secection">
        {% assign raw_gender = product.options_by_name['Gender'].selected_value %}

{% if request.locale.iso_code contains 'ar' %}
  {% assign gender = raw_gender %}
{% else %}
  {% assign gender = raw_gender | append: "'s" %}
{% endif %}
       <legend class="form__label desktop-standard-01 standard-02 regular">
        {{ 'sections.main-product.size.eu' | t: gender: gender }}
       </legend>
        <div class="close-size">
         <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg" class="_icon_1codi_279" aria-hidden="true"><path d="m5.64 5.64 12.72 12.72M5.64 18.36 18.36 5.64"></path></svg>
        </div>
       </div>
      {% comment %} </div> {% endcomment %}
     {% endcase %}
     {% if option.name != 'Gender' and option.name != 'جنس' %}
      <div
       class="medium desktop-semi-mono-02-regular desktop-semi-mono-02 semi-mono-03 semi-mono-03-regular {{ option.name | downcase }}-option 
      {% if option.name == 'Size' or option.name == 'مقاس' %} size-selection-item {% endif %} "
      >
     {% endif %}
     
     {% render 'product-variant-options',
      product: product,
      option: option,
      block: block,
      picker_type: picker_type,
      linked_to: linked_to,
      child_product: child_product,
      handles: handles,
      colors: colors
     %}
      
     {% if option.name != 'Gender' and option.name != 'جنس' %}
            </div>
          {% endif %}
          
            {% comment %} Avatar code start {% endcomment %}
          {% if option.name == 'Color' %}
            {% if avatraCheckbox == true %}
              {% assign avatar_label = product.selected_or_first_available_variant.metafields.custom.avatar_text %}
              {% assign avatar_image = product.selected_or_first_available_variant.metafields.custom.avatar_image %}
                <div class="avatar-desktop {% if avatar_label != blank %}avatar-model{% endif %}">
                  <div class="avatar-container {% if avatar_label == blank or avatar_image == blank %}hide-avatar{% endif %}">
                    <img src="{{avatar_image | img_url:'master'}}" alt="avatar">
                    <span>{{ avatar_label }}</span>
                  </div>
                </div>
            {% endif %}
            {%endif %}
            {% comment %} Avatar code end {% endcomment %} 
     {% case option.name %}
      {% when 'Size' or 'مقاس' %}
<div class="size-info">
        <button class="open-size-chart regular standard-02 desktop-standard-01"> 
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="_sizeChartLinkRuler_10h4p_370">
         <path fill-rule="evenodd" clip-rule="evenodd" d="M22.5 7.5h-21v9h21v-9Zm-20 8v-7H5v4h1v-4h2.25v3h1v-3h2.25v4h1v-4h2.25v3h1v-3H18v4h1v-4h2.5v7h-19Z" fill="currentColor"></path>
        </svg> 
        {% if localization.language.iso_code == 'ar' %}
  {% assign size_chart_text = "جدول المقاسات" %}
{% else %}
  {% assign size_chart_text = "Size Chart" %}
{% endif %}

        <span>{{ size_chart_text }}</span>
        </button>
        <div class="regular standard-02 desktop-standard-01">
         {{ product.metafields.custom.sizing_recommendation.value.text.value | default: '' }}        
        </div>
        
       </div>
      </div>
       {% endcase %}
    </fieldset>
    {% assign option_name = option.name | downcase %}
   
    {% if request.locale.iso_code == 'ar' %}
  <div class="error-message">الرجاء اختيار المقاس</div>
{% else %}
   <div class="error-message">{{ 'products.product.option_error' | t: option_name: option_name }}</div>
{% endif %}
   {%- else -%}
    <div class="product-form__input product-form__input--dropdown">
     <label class="form__label" for="Option-{{ section.id }}-{{ forloop.index0 }}">
      {{ option.name }}
     </label>
     <div class="select">
      {%- if picker_type == 'swatch_dropdown' -%}
       <span
        data-selected-value
        class="dropdown-swatch"
       >
        {% render 'swatch', swatch: option.selected_value.swatch, shape: block.settings.swatch_shape %}
       </span>
      {%- endif -%}
      <select
       id="Option-{{ section.id }}-{{ forloop.index0 }}"
       class="select__select"
       name="options[{{ option.name | escape }}]"
       form="{{ product_form_id }}"
      >
       {% render 'product-variant-options',
        product: product,
        option: option,
        block: block,
        picker_type: picker_type,
        linked_to: linked_to,
        child_product: child_product,
        handles: handles,
        colors: colors
       %}
      </select>
      {% render 'icon-caret' %}
     </div>
    </div>
   {%- endif -%}
  {%- endfor -%}

  <script type="application/json" data-selected-variant>
   {{ product.selected_or_first_available_variant | json }}
  </script>

  <script>
   (function () {
     window.removeColorLoader = function removeColorLoader() {
  document.querySelectorAll('.color--option').forEach(function (el) {

    var realCount = el.querySelectorAll('label.color-swatch').length;
    var skeletonContainer = el.querySelector('.color-swatches-skeleton');

    if (skeletonContainer) {
      skeletonContainer.innerHTML = '';

      for (var i = 0; i < realCount; i++) {
        var span = document.createElement('span');
        span.className = 'skeleton-swatch';
        skeletonContainer.appendChild(span);
      }
    }

    el.classList.remove('swatches-loading');
  });
}

     // Large product (>250 variants): _loading is true, wait for fetch to finish
     if (window.ShopifyProduct && window.ShopifyProduct._loading) {
       document.addEventListener('shopify:variants:loaded', removeColorLoader, { once: true });
     } else {
       // Small product (≤250 variants): no async fetch, remove loader after
       // a short delay so the DOM has time to render the swatches first
       setTimeout(removeColorLoader, 800);
     }
   })();
  </script>

 </variant-selects>
{%- endunless -%}

<script>
console.log('=== PRODUCT OPTIONS DEBUG ===');

console.log('Product options:', {{ product.options | json }});

{% for option in product.options_with_values %}
console.log(
  'Option {{ forloop.index0 }}:',
  {
    name: {{ option.name | json }},
    position: {{ forloop.index0 }},
    values: {{ option.values | json }}
  }
);
{% endfor %}

console.log('=== VARIANT STRUCTURE DEBUG ===');

window.ShopifyProduct.variants.forEach((variant, index) => {
  console.log('Variant #' + index, {
    id: variant.id,
    title: variant.title,
    option1: variant.option1,
    option2: variant.option2,
    option3: variant.option3,
    options: variant.options
  });
});
</script>

variant-select.js – is in comments, check it out.

NOTE : I found this code written by someone else so might be a bit spagetti , please help in solving .

class VariantSelects extends HTMLElement {
  constructor() {
    super();
    this.variant = this.variant ?? JSON.parse(this.querySelector('variant-selects [data-selected-variant]')?.innerHTML);

    this.sizeOptions  = [...this.querySelectorAll('fieldset[data-name="Size"] label, fieldset[data-name="مقاس"] label')];
    this.colorOptions = [...this.querySelectorAll('fieldset[data-name="Color"] label, fieldset[data-name="لون"] label')];
  }

  connectedCallback() {
    const section = this.closest('section');
    const productInfo = this.closest('product-info');
    const selectBtn = productInfo?.querySelector('.btn-select-size');
    const sizePopup = productInfo?.querySelector('.size-secection');
    const closeBtn = productInfo?.querySelector('.close-size');

    // Restore last-picked size
    if (window.sizeSelected) {
      console.log(window.sizeSelected, "window.sizeSelected");
      const field = this.querySelector(
        `fieldset[data-name="Size"] input[value="${window.sizeSelected}"],
         fieldset[data-name="مقاس"] input[value="${window.sizeSelected}"]`
      );
      if (field) field.checked = true;
    }

    // Clone blocks for mobile
    this.linkedTo = [...productInfo.querySelectorAll('[data-linked-to]')];
    this.linkedTo.forEach(block => {
      const toClone = this.querySelector(`fieldset[data-name="${block.dataset.linkedTo}"]`).cloneNode(true);
      let name = block.dataset.linkedTo;
      if (htmlEl.lang === "ar" && htmlEl.dir === "rtl") {
        if (name === 'لون' || name === 'Color') {
          name = 'color';
        }
      } else {
        name = name.toLowerCase();
      }
      this[`${name}Options`] = [
        ...(this[`${name}Options`] || []),
        ...toClone.querySelectorAll('label')
      ];
      toClone.querySelectorAll('[id]').forEach(el => {
        el.id += 'cloned';
        el.name += 'cloned';
      });
      block.innerHTML = '';
      [...toClone.childNodes].reverse().forEach(n => toClone.append(n));
      block.appendChild(toClone);
    });

    // ✅ Assign loader early — showLoader() in the change handler needs it
    this.loader = productInfo.querySelector('.loader');

    const _selectedVariantData = JSON.parse(
      this.querySelector('[data-selected-variant]')?.innerHTML || 'null'
    );
    const _pendingColor = _selectedVariantData?.options?.[1];

    const runInitWithColorRestore = () => {
      if (!this.dataset.combined) {
        // ✅ skipFilter=true — we handle filterOptions ourselves after re-cloning
        window.injectMissingColorSwatches?.(true);

        // ✅ Re-clone mobile color fieldset AFTER injection so it includes the new swatches
        this.linkedTo?.forEach(linkedBlock => {
          const fieldsetName = linkedBlock.dataset.linkedTo;
          if (fieldsetName !== 'Color' && fieldsetName !== 'لون') return;
          const sourceFieldset = this.querySelector(`fieldset[data-name="${fieldsetName}"]`);
          if (!sourceFieldset) return;
          const toClone = sourceFieldset.cloneNode(true);
          toClone.querySelectorAll('[id]').forEach(el => {
            el.id += 'cloned';
            el.name += 'cloned';
          });
          linkedBlock.innerHTML = '';
          [...toClone.childNodes].reverse().forEach(n => toClone.append(n));
          linkedBlock.appendChild(toClone);
        });

        // Restore checked color before filterOptions runs
        if (_pendingColor) {
          const colorInput = this.querySelector(
            `fieldset[data-name="Color"] input[value="${_pendingColor}"],
             fieldset[data-name="لون"] input[value="${_pendingColor}"]`
          );
          if (colorInput && !colorInput.checked) {
            colorInput.checked = true;
          }
        }

        this.filterOptions();
      }

      // ✅ Section only becomes visible AFTER correct variant state is fully applied — no flicker
      if (!section.classList.contains('show')) {
        section.classList.add('show');
        window.scrollTo({ top: 0, behavior: 'smooth' });
      }
      this.removeLoader();
      this.applyFallback();
      requestAnimationFrame(() => {
        const submitBtn = this.closest('product-info')?.querySelector('button.product-form__submit');
        if (submitBtn) submitBtn.style.opacity = '1';
      });
    };

    if (window.ShopifyProduct._loading) {
      document.addEventListener('shopify:variants:loaded', runInitWithColorRestore, { once: true });
    } else {
      runInitWithColorRestore();
    }

    // Update size label if already selected
    if (window.sizeSelected && selectBtn) {
      selectBtn.textContent = window.sizeSelected;
      selectBtn.classList.add('selected');
    }

    // Size popup toggle
    if (selectBtn && !selectBtn.classList.contains('bound')) {
      selectBtn.addEventListener('click', () => {
        sizePopup.style.display = 'block';
      });
      selectBtn.classList.add('bound');
    }
    if (closeBtn && !closeBtn.classList.contains('bound')) {
      closeBtn.addEventListener('click', () => {
        sizePopup.style.display = 'none';
      });
      closeBtn.classList.add('bound');
    }

    this.addEventListener('change', event => {
      const t = this.getInputForEventTarget(event.target);
      const fs = t.closest('fieldset');
      const name = fs?.dataset.name;

      if (name === 'Gender' || name === 'جنس') {
        window.sizeSelected = '';
        this.querySelectorAll('fieldset[data-name="Size"] input[type="radio"], fieldset[data-name="مقاس"] input[type="radio"]').forEach(r => r.checked = false);
        if (selectBtn) {
          if (htmlEl.lang === "ar" && htmlEl.dir === "rtl") {
            selectBtn.textContent = 'اختر الحجم';
          } else {
            selectBtn.textContent = 'Select a size';
          }
          selectBtn.classList.remove('selected');
        }
        sizePopup.style.display = 'none';
        section.classList.remove('show');
        this.showLoader();
      }

      if (name === 'Color' || name === 'لون') {
        window.sizeSelected = '';
        section.classList.remove('show');
        window.removeColorLoader?.();
      }

      if (name === 'Size' || name === 'مقاس') {
        const headerATC = document.querySelector('.product-form-btn');
        const submitBton = document.querySelector('.product-form-btn button[type="submit"]');
        const spinLoad = document.querySelector('.spin-load');
        const spinner = document.querySelector(".product-form-btn .loading__spinner");
        if (submitBton) {
          submitBton.classList.add("hidden");
          spinLoad.classList.remove("hidden");
          spinner?.classList.remove("hidden");
          headerATC.style.textAlign = 'center';
          setTimeout(() => {
            headerATC.style.textAlign = '';
            submitBton.classList.remove("hidden");
            spinLoad.classList.add("hidden");
            spinner?.classList.add("hidden");
          }, 1200);
        }

        if (!t.classList.contains("disabled")) {
          window.sizeSelected = t.value;
          if (selectBtn) {
            selectBtn.textContent = t.value;
            selectBtn.classList.add('selected');
          }
          sizePopup.style.display = 'none';
        } else {
          window.sizeSelected = '';
          if (selectBtn) {
            if (htmlEl.lang === "ar" && htmlEl.dir === "rtl") {
              selectBtn.textContent = 'اختر الحجم';
            } else {
              selectBtn.textContent = 'Select a size';
            }
            selectBtn.classList.remove('selected');
          }
        }

        if (t.classList.contains("disabled")) {
          document.body.classList.add("loader-active");
          waitForElementAndClick(".gw-button-widget, .gw-float-widget");
        }

        function waitForElementAndClick(selector, maxAttempts = 10, interval = 200) {
          let attempts = 0;
          const checkAndClick = () => {
            const element = document.querySelector(selector);
            if (element && typeof element.click === 'function') {
              try {
                element.click();
                document.body.classList.remove("loader-active");
                return true;
              } catch (error) {
                console.warn("Click failed, retrying...", error);
              }
            }
            if (attempts < maxAttempts) {
              attempts++;
              setTimeout(checkAndClick, interval);
            } else {
              console.error("Failed to find or click the element after maximum attempts.");
            }
          };
          checkAndClick();
        }
      }

      // ✅ KEY FIX: if variants are still loading, wait then re-run filterOptions
      const runFilter = () => {
        if (!this.dataset.combined) this.filterOptions();
        this.applyFallback();
      };

      if (window.ShopifyProduct._loading) {
        document.addEventListener('shopify:variants:loaded', runFilter, { once: true });
      } else {
        runFilter();
      }

      this.updateSelectionMetadata(event);
      publish(PUB_SUB_EVENTS.optionValueSelectionChange, {
        data: {
          event,
          target: t,
          selectedOptionValues: this.selectedOptionValues,
          selectedOptions: this.selectedOptions,
        }
      });
    });

    // Observer for stock fallback
    const desktopBtn = productInfo.querySelector('button.product-form__submit');
    if (desktopBtn) new MutationObserver(() => {
      let { Gender, Color } = this.selectedOptions;
      let g = Gender || this.selectedOptions["جنس"];
      let c = Color  || this.selectedOptions["لون"];
      const inStock = (window.ShopifyProduct?.variants || []).filter(v => v.options[0] === g && v.options[1] === c && v.available);
      if (inStock.length > 0 && desktopBtn.disabled) this.applyFallback();
    }).observe(desktopBtn, { attributes: true, childList: true, subtree: true });

    document.querySelectorAll('form.add-to-cart-form button').forEach(btn => {
      new MutationObserver(() => {
        let { Gender, Color } = this.selectedOptions;
        let g = Gender || this.selectedOptions["جنس"];
        let c = Color  || this.selectedOptions["لون"];
        const inStock = (window.ShopifyProduct?.variants || []).filter(v => v.options[0] === g && v.options[1] === c && v.available);
        if (inStock.length > 0 && btn.disabled) this.applyFallback();
      }).observe(btn, { attributes: true, childList: true, subtree: true });
    });
  }

  filterOptions = (hide = true) => {
    let { Gender, Color } = this.selectedOptions;
    let g = Gender || this.selectedOptions["جنس"];
    let c = Color  || this.selectedOptions["لون"];
    const byG = ShopifyProduct.variants.filter(v => v.options[0] === g);

    if (hide) {
      const colors = [...new Set(byG.map(v => v.options[1]))];
      const productInfo = this.closest('product-info');

      // ✅ Query BOTH original fieldset labels AND cloned mobile fieldset labels
      this.colorOptions = [
        ...this.querySelectorAll('fieldset[data-name="Color"] label.color-swatch, fieldset[data-name="لون"] label.color-swatch'),
        ...(productInfo?.querySelectorAll('[data-linked-to="Color"] label.color-swatch, [data-linked-to="لون"] label.color-swatch') || [])
      ];
      this.colorOptions.forEach(el => el.hidden = !colors.includes(el.dataset.value));

      const sizes = [...new Set(byG.filter(v => v.options[1] === c).map(v => v.options[2]))];

      // ✅ Same for sizes
      this.sizeOptions = [
        ...this.querySelectorAll('fieldset[data-name="Size"] label, fieldset[data-name="مقاس"] label'),
        ...(productInfo?.querySelectorAll('[data-linked-to="Size"] label, [data-linked-to="مقاس"] label') || [])
      ];
      this.sizeOptions.forEach(el => el.hidden = !sizes.includes(el.dataset.value));
    }
    return true;
  }

  applyFallback() {
    const variants = window.ShopifyProduct?.variants || [];
    let { Gender, Color } = this.selectedOptions;
    let g = Gender || this.selectedOptions["جنس"];
    let c = Color  || this.selectedOptions["لون"];
    if (!g || !c) return;

    const inStock = variants.filter(v => v.options[0] === g && v.options[1] === c && v.available);
    const fallback = inStock[0] || null;

    const root = this.closest('product-info');
    if (root) {
      const desktopInput = root.querySelector('input.product-variant-id');
      const desktopBtn = root.querySelector('button.product-form__submit');
      if (desktopInput && desktopBtn) {
        if (fallback) {
          desktopInput.value = fallback.id;
          desktopInput.disabled = false;
          desktopBtn.disabled = false;
          if (htmlEl.lang === "ar" && htmlEl.dir === "rtl") {
            desktopBtn.querySelector('span').textContent = 'أضف إلى الحقيبة';
          } else {
            desktopBtn.querySelector('span').textContent = 'Add to bag';
          }
        } else {
          desktopInput.value = '';
          desktopInput.disabled = true;
          desktopBtn.disabled = true;
          if (htmlEl.lang === "ar" && htmlEl.dir === "rtl") {
            desktopBtn.querySelector('span').textContent = 'نفذ';
          } else {
            desktopBtn.querySelector('span').textContent = 'Sold out';
          }
        }
      }
    }

    document.querySelectorAll('form.add-to-cart-form').forEach(form => {
      const input = form.querySelector('input.product-variant-id');
      const btn = form.querySelector('button');
      if (!input || !btn) return;
      if (fallback) {
        input.value = fallback.id;
        input.disabled = false;
        btn.disabled = false;
        if (htmlEl.lang === "ar" && htmlEl.dir === "rtl") {
          btn.textContent = 'أضف إلى الحقيبة';
        } else {
          btn.textContent = 'Add to bag';
        }
      } else {
        input.value = '';
        input.disabled = true;
        btn.disabled = true;
        if (htmlEl.lang === "ar" && htmlEl.dir === "rtl") {
          btn.textContent = 'نفذ';
        } else {
          btn.textContent = 'Sold out';
        }
      }
    });
  }

  updateSelectionMetadata({ target }) {
    const { value, tagName } = target;
    if (tagName === 'SELECT' && target.selectedOptions.length) {
      target.querySelectorAll('option').forEach(o => o.selected = false);
      target.selectedOptions[0].selected = true;
      const sw = target.closest('.product-form__input').querySelector('[data-selected-value]>.swatch');
      const v = target.selectedOptions[0].dataset.optionSwatchValue;
      if (sw) {
        sw.style.setProperty('--swatch--background', v || 'unset');
        sw.classList.toggle('swatch--unavailable', !v);
      }
    } else if (tagName === 'INPUT' && target.type === 'radio') {
      const disp = target.closest('.product-form__input')?.querySelector('[data-selected-value]');
      if (disp) disp.textContent = value;
    }
  }

  showLoader() {
    this.loader?.classList.add('active');
  }

  removeLoader() {
    this.loader?.classList.remove('active');
  }

  getInputForEventTarget(t) {
    return t.tagName === 'SELECT' ? t.selectedOptions[0] : t;
  }

  get selectedOptionValues() {
    return Array.from(this.querySelectorAll('select option:checked, fieldset input:checked')).map(el => el.dataset.optionValueId);
  }

  get selectedOptions() {
    return Array.from(this.querySelectorAll('select option:checked, fieldset input:checked')).reduce((a, el) => {
      a[el.closest('fieldset').dataset.name] = el.value;
      return a;
    }, {});
  }
}

customElements.define('variant-selects', VariantSelects);



Found it — it’s right here in your code:

this.querySelectorAll('fieldset[data-name="Size"] ...')

this.querySelectorAll('fieldset[data-name="Color"] ...')

data-name holds the option name, and Shopify translates option names per locale — so in Arabic “Size” → “مقاس” and “Color” → “اللون”. Your selectors [data-name="Size"] / [data-name="Color"] then match nothing, and the swatches vanish. You already half-discovered this — you hardcoded a [data-name="مقاس"] fallback for Size, but not for Color, and hardcoding translations is fragile.

The robust fix: stop selecting by the translated name. Key off the option position, which is locale-independent. In the Liquid that renders each fieldset, output something like:

<fieldset data-option-position="{{ forloop.index }}" ...>

then change the JS to fieldset[data-option-position="1"], [data-option-position="2"], etc. Do the same for the 250+ variants you inject via the Storefront API — match them by variant ID or option position, never the value text. Use the translated values only for display, not for matching.

If you share the store URL I can point to the exact lines to change.

HI @dorame

I don’t think this is an RTL rendering issue. From the code you shared, it looks more like a variant matching issue that only becomes apparent after switching to Arabic.

A few things stood out:

  • Your filtering logic compares the selected option values directly against the variant option values, for example:
v.options[0] === g
v.options[1] === c

If the initial {{ product.variants | json }} data and the variants fetched from the Storefront API aren’t using the exact same localized option values after the language switch, those comparisons will fail.

  • When injecting new swatches, every injected input receives the same data-option-value-id from the reference input:
newInput.dataset.optionValueId = refInput.dataset.optionValueId;

If your variant resolution relies on selectedOptionValues, this could also lead to incorrect matching.

  • The code assumes:

  • options[0] = Gender

  • options[1] = Color

  • options[2] = Size

That works only if the option order never changes. It’s safer to determine the indexes dynamically from product.options instead of hardcoding them.

As a first debugging step, I’d log the option values from both the initial Liquid variants and the Storefront API response after switching to Arabic and verify they’re identical. If one dataset contains English values while the other contains Arabic values, the swatch filtering logic will never match correctly.

If you can also share the console output for one variant from both sources (before and after the language switch), it should be possible to pinpoint exactly where the mismatch is occurring.