xpay✦ Commerce
Directory
WooCommerce plugins
MHM Currency Switcher
MHM Currency Switcher
Multi-currency support for WooCommerce. Let your customers browse, shop, and checkout in their preferred currency with real-time exchange rates.
5.0
(1 ratings)Will this break my store?
What the WordPress.org registry says about keeping MHM Currency Switcher running.
Tested to 7.1.3
Tested against the WordPress branch in use today.1 month ago
7.4
Your host must be running at least this version.6.6
woocommerce
These must be installed and active first.1
A single maintainer. Worth knowing if the plugin is load-bearing for your store.Maintenance & trust
Scored on how MHM Currency Switcher is looked after — not on how many stores run it.
Maintenance
35 / 35WordPress compatibility
20 / 20Support responsiveness
Not enough dataMerchant satisfaction
10 / 15Listing transparency
10 / 10Ratings
5.0
1 ratingMHM Currency Switcher adds multi-currency support to your WooCommerce store. Customers can browse products, add items to their cart, and complete checkout in their preferred currency with real-time exchange rates.
Key Features
- Add multiple currencies with real-time exchange rates
- Cache compatibility mode — works behind a page cache without serving one visitor’s currency to everybody
- Currency switcher via shortcode — usable in a text widget, nav menu, or Elementor
- Product page price display widget with country flags
- Navigation menu integration — add switcher to any WordPress menu
- Automatic exchange rate fetching (ExchangeRate-API)
- Fee & rounding configuration per currency
- Cookie-based currency persistence
- WooCommerce HPOS compatible
- Elementor widgets included
- Every currency WooCommerce offers
- Scheduled automatic exchange rate updates
- Geolocation-based currency detection
- Fixed prices per product
- “How to use” tab in the settings screen, naming every way the switcher can be placed
Documentation and support
- Documentation — setup, shortcodes, cache compatibility and FAQ, in English and Turkish
- Support forum
- MaxHandMade on YouTube — WordPress and WooCommerce videos from the author
Cache compatibility mode
A page cache stores the HTML your server produced for whoever asked first. When
prices are converted on the server, that means the first visitor’s currency is
what every later visitor is served. Cache compatibility mode, which is on by
default, avoids this: anonymous shop, archive and product pages are rendered in
your base currency, so the same cached page is correct for everyone, and the
browser converts the prices it can see afterwards through a REST request to this
plugin.
The split is deliberate. Only displayed prices are converted in the browser.
Cart, checkout, order totals, order emails and the WooCommerce REST API are
always calculated on the server in the currency the customer actually chose, so
the amount charged cannot be altered from the browser.
You can switch the mode off under WooCommerce > MHM Currency >
Advanced, in which case prices are converted on the server as they were before
this feature existed. Read “Known limits” below before deciding either way —
both settings have consequences, and they are different ones.
Known limits
These are consequences of how cache compatibility mode works, not defects. They
are listed so you can decide with your eyes open.
Turning the mode off does not restore the exact 1.0.0 behaviour
Three fixes sit above the setting and stay in place whether it is on or off:
prices are no longer converted on admin screens or in admin AJAX (which used to
write a converted price into an order line item), wc/v3 REST reads are pinned
to the base currency, and scheduled tasks and WP-CLI no longer convert. Switching
the mode off restores the 1.0.0 display behaviour — prices converted on the
server — and nothing else.
Search engines and crawlers see your base prices
With the mode on, the page a crawler fetches has not been through the browser,
so it carries base-currency prices, and so does the machine-readable product data
in it. See the structured data question above; the mismatch is deliberate.
Converted prices replace the base ones a moment after the page appears
The page arrives with your base-currency prices already on screen — nothing is
hidden waiting for JavaScript — and the browser swaps in the converted ones as
soon as its request comes back, each price fading over 200ms as it changes. On a
slow connection the base price is readable for longer before the swap. Visitors
who have asked their system for reduced motion get the swap without the fade.
With JavaScript disabled, or the endpoint unreachable, base prices stay
Nothing breaks and no error is shown to the visitor — the page simply keeps the
base-currency prices it was rendered with, and the reason is written to the
browser console. Cart and checkout are unaffected, because they never depended
on the browser in the first place.
Variable products make one extra request, and some swatch plugins lose the price
On a cacheable page the plugin forces WooCommerce to fetch variation prices over
AJAX, because the variations JSON WooCommerce would otherwise embed in the page
carries base-currency prices that its own scripts write straight into the page.
The cost is one request when a visitor picks a variation, and that
data-product_variations is false: third-party colour or size swatch plugins
that read prices out of that JSON instead of asking WooCommerce may stop showing
a price. If you use such a plugin, check a variable product before going live.
The mini-cart relies on WooCommerce cart fragments
A mini-cart is rendered on every page, so it cannot be classified per request.
It is rendered in the base currency and then corrected by WooCommerce’s own cart
fragment refresh, which is a server-side conversion. If cart fragments are
disabled on your site — some themes and optimisation plugins dequeue them — the
cached mini-cart total stays in the base currency while the rest of the page
converts. The plugin watches for this and says so in the admin when it happens;
a site with no mini-cart at all is never warned about it.
A cart or checkout on a page WooCommerce does not know about must be excluded from your cache
Page caches exclude cart and checkout automatically because they recognise the
pages WooCommerce assigned. If you have put a cart or checkout shortcode or block
on some other page, exclude that page yourself. Two things go wrong otherwise:
the conversion decision flips to “convert” partway through the render, so the
rest of the page is printed already converted and without the markers the browser
looks for, and blocks-based cart and checkout embed their amounts in the page as
JSON while rendering. Either way the first visitor’s currency is what the cache
then hands to everyone. Cart contents are personal anyway; such a page should not
be cached.
`?currency=` multiplies your cache entries
A currency can be requested in the URL, and a cache treats every distinct URL as
a separate entry, so linking to ?currency=EUR and ?currency=GBP stores the
same page more than once. The switcher itself does not produce these URLs — it
writes a cookie and leaves the address alone. On a cached page it converts the
prices where they stand; on the cart page, for a logged-in visitor, or with
cache compatibility switched off, it reloads instead. Either way the URL is the
one the visitor was already on, so no extra cache entry is created.
A ?currency= link also applies to that page view only: it deliberately sets no
cookie, so the next page the visitor opens is back in your base currency unless
they use the switcher. That is not an oversight — a link that silently pinned a
currency could show one currency in the catalogue while the cart, which reads
the cookie, charged another. If you want a campaign link that sticks, send
visitors to a page carrying the switcher rather than relying on the parameter.
WooCommerce Analytics adds different currencies together
An order is stored in the currency the customer paid in, and WooCommerce
Analytics reports every order’s figures in your store currency without
converting them back. A 4.38 USD order is counted as 4.38 in your base
currency, so once you take orders in more than one currency the revenue
figures in Analytics, and the totals in the customer panel on the order
screen, are sums of unlike amounts. The orders themselves are correct — each
one keeps its own currency, total and the exchange rate it was placed at, and
this plugin stores that rate on the order. It is the aggregate reports that
cannot be read as money. Nothing in this plugin can fix that from the outside;
if you need accurate multi-currency reporting, export the orders and convert
them using the rate recorded on each one.
Logged-in visitors are converted on the server
Logged-in visitors take the server-side path, which is correct as long as your
cache does what nearly all of them do and never serves cached pages to logged-in
users. An edge cache or CDN configured to cache without looking at cookies is the
exception, and there a logged-in visitor’s converted page can be stored and
served on. If you cache at the edge, confirm it varies on the login cookie.
External services
This plugin connects to two third-party services to keep currency conversion
rates up to date. What each one is sent, and when, is described separately
below because the two are not the same. Both requests are made with PHP’s
WP_Http transport, which in WordPress’s default configuration sends a
User-Agent header of the form WordPress/{version}; {your site's URL} —
so the site’s own address leaves with every request to either service, not
just the data described below. That header is filterable
(http_headers_useragent, http_request_args), so a site that has changed
it will send something different.
ExchangeRate-API
What it is: a commercial exchange-rate API, used as the primary source of
exchange rates.
What is sent, and when: the three-letter base currency code you have
configured (for example USD), sent as part of the request URL —
https://api.exchangerate-api.com/v4/latest/{BASE_CURRENCY} — when you
press “Sync rates” in the admin panel, when you run wp mhmcs rates-sync
from the command line, and on the schedule you configure under automatic
rate updates (hourly, twice daily, or daily). No other data from your site
is included.
Terms of service and privacy policy: https://www.exchangerate-api.com/terms
(ExchangeRate-API publishes its privacy policy inside that same page rather
than on a separate one.)
European Central Bank (ECB) daily reference rates
What it is: the ECB’s public daily reference-rate feed, used as the fallback
when ExchangeRate-API cannot be reached.
What is sent, and when: nothing beyond the User-Agent described above. The
feed is a fixed, parameter-free address —
https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml — so no
currency code or other value is sent to the ECB; the same document is
returned to every requester. It is only requested when ExchangeRate-API’s
request has failed. The feed is EUR-based and covers roughly thirty
currencies rather than the hundreds ExchangeRate-API carries. If your base
currency is outside that set, this source returns nothing at all and your
existing rates are left unchanged until the next attempt; if only one of
your configured target currencies is outside that set, the other target
currencies still update and the unsupported one is simply left without a
rate from this source.
The ECB does not publish a document titled “Terms of Service.” Its terms of
use are stated on its Disclaimer & Copyright page, which is the closest
equivalent and is linked below.
Disclaimer & Copyright (terms of use): https://www.ecb.europa.eu/services/using-our-site/disclaimer/html/index.en.html
Privacy statement: https://www.ecb.europa.eu/services/data-protection/privacy-statements/html/ecb.privacy_statement_website.en.html
The ECB’s reference-rates page separately states that using these rates for
transaction purposes is strongly discouraged:
https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html
That page, not the two links above, is the source of that caution. This
plugin uses the feed to convert prices for display and checkout, which is
the kind of transactional use that notice is about; if that matters for your
shop, review that page before relying on this fallback.
Redirecting a source
If your network blocks the ECB feed, the mhmcs_fallback_rates_url filter
receives the URL, the base currency code and which source is being filtered
— always 'ecb', since ECB is now the only fallback — so the request can
be pointed elsewhere.
Visitor geolocation (through WooCommerce)
When “Enable geolocation-based currency detection” is on, the plugin asks
WooCommerce which country a visitor is in, through WooCommerce’s own
WC_Geolocation API. Since 2.2.0 that call explicitly switches WooCommerce’s
remote-API fallback OFF, so this plugin makes no third-party request for it:
- Behind CloudFlare — the country is read from the
CF-IPCountryrequest
header. Nothing leaves your server. - Otherwise — WooCommerce’s LOCAL MaxMind database file, if one is installed.
Again nothing leaves your server. - With neither — detection simply finds nothing, and the visitor keeps the
store’s base currency until they choose one. No third-party lookup is made.
Earlier versions called WC_Geolocation::geolocate_ip() with its bare defaults,
which let WooCommerce fall back to a remote geolocation service and send the
visitor’s IP address there. That fallback is now disabled, matching what
WooCommerce core itself does on the storefront.
The setting is off by default. Resolving a country from an IP address is
data processing a shop owner should switch on deliberately, so a fresh install
leaves it off and shops that already configured it keep their own choice.
To make detection work without CloudFlare, add a free MaxMind GeoLite2 license
key under WooCommerce > Settings > Integration > MaxMind Geolocation;
WooCommerce then downloads a local database file and answers from it.
WooCommerce geolocation documentation:
https://woocommerce.com/document/maxmind-geolocation-integration/
Source code
The settings screen is a React application, and what ships inside the plugin is
the compiled bundle at admin-app/build/index.js. The readable source it is
built from is not in the package, so here is where to find it and how to
reproduce the build.
Full source, including the unminified JavaScript:
https://github.com/MaxHandMade/mhm-currency-switcher
The source of the bundle is admin-app/src/. It is compiled with WordPress’s
own build tooling, @wordpress/scripts, and nothing else:
npm install
npm run build
That writes admin-app/build/index.js together with the index.asset.php
dependency map the plugin reads when enqueuing the script. No other build step,
minifier or bundler is involved, and no code is generated at install time or at
runtime.
[mhmcs_currency_switcher] renders the currency dropdown. It accepts one
attribute, size, which may be small, medium or large; leave it out to
use the size saved in Display Options.
[mhmcs_currency_prices] renders the same product price in several currencies.
Its attributes are all optional:
currencies— comma-separated codes, e.g.currencies="USD,EUR". Without
it, the currencies chosen in Display Options are used. Codes you have not
configured as currencies are ignored.product_id— price a specific product instead of the one being viewed.price— price a specific amount instead of a product’s.show_flags—trueorfalse, overriding the saved Display Options
setting.
Both are also available as Elementor widgets.
As many as your shop needs — every currency WooCommerce offers can be enabled,
and nothing is held back for a paid version. The REST API refuses a request
carrying more than 500 currency rows; that is a guard against oversized payloads
and is well above the number of codes WooCommerce itself offers, so the panel
cannot reach it.
Exchange rates are fetched from ExchangeRate-API in real time, either on demand or on a schedule you configure (hourly, twice daily, or daily) so your rates stay current without manual intervention.
If that source cannot be reached, the plugin falls back to the European Central Bank’s daily reference rate feed before giving up and leaving your existing rates in place. Both are named, with their terms, under “External services” below. If your network blocks the fallback, the mhmcs_fallback_rates_url filter can point it somewhere else.
Yes. MHM Currency Switcher fully supports WooCommerce High-Performance Order Storage (HPOS / Custom Order Tables).
No. A fixed price is stored per product and currency, not per price type, so the same amount is used for the regular price and the sale price. A product on sale in your base currency shows as not on sale in a currency you have given a fixed price to. If you need the sale to carry across, leave that currency to the exchange rate instead of fixing it.
Yes. When cache compatibility mode is on, prices on cached pages are converted through a public REST endpoint, and one address may call it 120 times a minute by default. Ordinary browsing is nowhere near that — a page makes one request. If your shop sits behind a reverse proxy or a CDN that makes every visitor look like the same address, raise or disable the limit with the mhmcs_convert_rate_limit filter. Note that the address is read from the proxy headers WooCommerce passes on, which it trusts unconditionally and which can be forged; the limit bounds accidental hammering rather than a determined attacker.
Some themes and plugins define WooCommerce’s cart constant on every page, usually to show a cart total in the header. When that happens the plugin treats every page as a checkout — the customer’s money is at stake — and converts prices on the server, which is exactly what cache compatibility mode exists to avoid. Nothing looks wrong on the site: prices are still correct for whoever loads the page first, and then a page cache can serve that person’s currency to everyone else. Because there is no visible symptom, the plugin says so in the admin instead. The notice clears itself as soon as a front-end page renders normally again.
With cache compatibility on, the page is generated in your base currency and the browser converts the prices afterwards, so the machine-readable product data a crawler reads stays in the base currency. This is deliberate and is not corrected: pinning it the other way would leave the data disagreeing with the page in the one case where the two currently agree — with cache compatibility switched off, where the page and the structured data are both converted on the server.
Only when the request asks for a currency: ?currency=EUR on a wc/v3 product request converts price, regular_price and sale_price and adds a currency_code field. Without the parameter the response is pinned to your base currency, so the answer never depends on the cookies of whoever is calling. A per-product fixed price takes precedence over the exchange rate here, exactly as it does on the shop page.
One known limit: the price_html field is not pinned in the same way. It cannot be reached in that state by a normal wc/v3 client — only by code that dispatches an internal REST request during a page render — so no integration sees it, but it is not consistent with the three numeric fields and is recorded here rather than left unsaid.
Categories
Plugin details
Tags on WordPress.org
