Script for Dual Currency Display in Bulgaria (BGN + EUR)

As part of the new legal requirement in Bulgaria to display both BGN and EUR prices, we have prepared a basic JavaScript script that automatically calculates and shows euro prices alongside existing BGN prices. The secondary price is wrapped in the span element with bgn__secondaryPrice class, so you can style it right away.

ℹ️ This is an early version — fully functional, but intended as a starting point. Developers can build on it, customize it to fit specific needs, and help us improve it further. Your feedback and suggestions are very welcome! ℹ️

You can see a live example here: https://732466.myshoptet.com.

You can insert the following code in to the footer script section.


(function () {
  // Conversion rate from BGN to EUR set by the EU
  const BGN_TO_EUR = 0.51129188;

  // Only run if currency is BGN
  if (typeof getShoptetDataLayer === 'function' && getShoptetDataLayer('currency') === 'BGN') {
    const config = window.shoptet && window.shoptet.config;
    if (!config) return;

    // Currency formatting config
    const symbol = config.currencySymbol;
    const decSeparator = config.decSeparator;
    const thousandSeparator = config.thousandSeparator;
    const decPlaces = config.decPlaces;
    const symbolLeft = config.currencySymbolLeft === '1';

    // Class for the secondary price span
    const secondaryPriceClass = 'bgn__secondaryPrice';
    // List of known wrappers that may split price across children
    const priceWrappers = ['from', 'to'];

    // Regex to match prices, supports both symbol on left and right
    let priceRegex;
    if (symbolLeft) {
      // Matches: [optional +][symbol][optional space][number][,decimals]
      priceRegex = new RegExp(
        '[+]?' + symbol + '\\s*([0-9' + thousandSeparator + ']+(?:\\' + decSeparator + '[0-9]{' + decPlaces + '})?)',
        'g'
      );
    } else {
      // Matches: [optional +][number][,decimals][optional space][symbol]
      priceRegex = new RegExp(
        '[+]?([0-9' + thousandSeparator + ']+(?:\\' + decSeparator + '[0-9]{' + decPlaces + '})?)\\s*' + symbol,
        'g'
      );
    }

    /**
     * Appends a secondary EUR price span to the given parent node.
     * @param {Node} parent - The node to append the span to.
     * @param {string} priceStr - The price string to convert.
     */
    function appendSecondaryPriceSpan(parent, priceStr) {
      let normalized = priceStr.replace(new RegExp('\\' + thousandSeparator, 'g'), '').replace(decSeparator, '.');
      let bgn = parseFloat(normalized);
      let eur = (bgn * BGN_TO_EUR).toFixed(2);
      let span = document.createElement('span');
      span.className = secondaryPriceClass;
      span.textContent = ' / ' + eur + ' EUR';
      parent.appendChild(span);
    }

    /**
     * Recursively traverses the DOM and adds a secondary EUR price after BGN prices.
     * Handles both text nodes and known wrappers that may split price across children.
     */
    function addSecondaryPrice(node) {
      // Handle known wrappers by flattening their text (e.g. .from, .to)
      if (
        node.nodeType === Node.ELEMENT_NODE &&
        node.classList &&
        priceWrappers.some(cls => node.classList.contains(cls))
      ) {
        // Skip if already has a secondary price
        if (
          Array.from(node.childNodes).some(
            n => n.nodeType === Node.ELEMENT_NODE && n.classList && n.classList.contains(secondaryPriceClass)
          )
        ) {
          return;
        }
        // Combine all text (including children) and match price
        let combinedText = node.textContent;
        priceRegex.lastIndex = 0;
        let match = priceRegex.exec(combinedText);
        if (match) {
          appendSecondaryPriceSpan(node, match[1]);
        }
      }
      // Standard text node logic
      else if (node.nodeType === Node.TEXT_NODE) {
        let parent = node.parentNode;
        let text = node.textContent;
        let match;
        let lastIndex = 0;
        let frag = document.createDocumentFragment();

        // Skip if parent already contains a secondary price span
        if (
          parent &&
          Array.from(parent.childNodes).some(
            n => n.nodeType === Node.ELEMENT_NODE && n.classList && n.classList.contains(secondaryPriceClass)
          )
        ) {
          return;
        }

        priceRegex.lastIndex = 0;
        while ((match = priceRegex.exec(text)) !== null) {
          // Add text before price
          frag.appendChild(document.createTextNode(text.slice(lastIndex, match.index + match[0].length)));
          // Append EUR price span after price
          appendSecondaryPriceSpan(frag, match[1]);
          lastIndex = priceRegex.lastIndex;
        }
        // Add remaining text
        if (lastIndex < text.length) {
          frag.appendChild(document.createTextNode(text.slice(lastIndex)));
        }
        // Replace original text node with new fragment if any changes
        if (frag.childNodes.length > 0) {
          parent.replaceChild(frag, node);
        }
      } else if (node.nodeType === Node.ELEMENT_NODE) {
        // Recursively process child nodes
        for (let child of Array.from(node.childNodes)) {
          addSecondaryPrice(child);
        }
      }
    }

    // The robustness is preferred over performance, so we traverse the entire body
    addSecondaryPrice(document.body);
    // This list of events might not be exhaustive, but covers common dynamic updates
    const dynamicEvents = ['ShoptetDOMContentLoaded', 'ShoptetSimpleVariantChange', 'ShoptetSurchargesPriceUpdated'];
    dynamicEvents.forEach(event => {
      document.addEventListener(event, function (e) {
        addSecondaryPrice(document.body);
      });
    });
  }
})();

Post navigation