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);
});
});
}
})();