Open pub-trivia.app/pricing. If you are reading this from outside the UK, the price you see is in your own currency, and it was in the HTML before any JavaScript ran.
We sell quiz night software to pubs and bars, we bill in pounds, and a landlord in Dublin reading "£30" has to do the sum themselves before they know whether the plan is affordable. Showing an approximate local figure removes that guesswork at the exact moment somebody is deciding. Everything below is about making an approximate number safe to show.
Resolve on the server, or accept a flicker on the one number that matters
The easy version of this feature is an effect that looks up the country after mount and swaps the price. That paints the base price, then replaces it. The flicker lands on the single number the visitor came to the page to read, and it does not look like localisation, it looks like the price changing while they watch.
So the currency is decided during the server render and passed down as a prop into a context provider. The provider holds no effect, no fetch and no storage, because any of the three would reintroduce the post-load swap it exists to avoid. A client component reading that context renders the right figure on its first render, so there is nothing to correct on hydration.
The default value on the context is the currency we actually charge in. A surface that forgets to wrap itself in the provider shows the real price rather than a blank or a guess.
There is a cost and it is worth being honest about it: reading per-request information gives up the page's static cache.
curl -sI https://pub-trivia.app/pricing | grep -i cache
# cache-control: private, no-cache, no-store, max-age=0, must-revalidate
A page that renders instantly and shows the wrong number is worse than one that renders correctly.
The displayed figure has to be an upper bound
Whoever performs a currency conversion takes a cut of it, whether that is the payment processor or the customer's own card issuer. A straight mid-market conversion therefore quotes a figure slightly below what the customer is actually billed, which is the same unpleasant surprise this feature exists to remove, only smaller.
So the conversion is deliberately pessimistic, and then it rounds up to the next figure that looks like a price: the next .99 in a currency with decimals, the next whole unit in a currency without. One sentence is the whole contract of the module:
The number we display is never lower than the amount that will be asked for.
Quoting slightly high and charging slightly less is the only safe direction for this error to point. It also means the rounding rule is not cosmetic. It is the thing that absorbs small rate drift, which is why the rates can be a table in the repo with the date they were taken on it, rather than a network call in the render path of a price.
Two consequences fall out of the currency table rather than being special-cased:
- Zero-decimal currencies round to whole units, because ¥6,735.99 is not a price anybody in Tokyo has ever seen.
- The quoted rate is printed to the precision the currency deserves. "£1 ≈ ¥224.53" is false precision, "£1 ≈ ¥225" is a rate. Currencies near parity need their decimals, so the digit count follows the currency instead of being a constant.
Whole amounts drop their decimals on the way out too, because every list price is a round number and .00 on all of them reads as more precision than the number has.
The epsilon that stops one currency being a pound dearer
Rounding up to the next .99 is three lines of arithmetic, and binary floating point will quietly ruin it. An amount that lands exactly on a boundary gets nudged over it, and the round-up then takes the price to the next whole unit. Nothing throws. You simply find out later that one currency is a pound more expensive than all of its neighbours, for no reason a customer could ever guess at.
A small epsilon before the ceiling fixes it. It is the kind of line that looks like paranoia in a review and is obvious in hindsight.
Free has to stay free
This one shipped.
The retail round-up has no idea what a free plan is. The next .99 above zero is 0.99, so the trial, whose entire proposition is that it costs nothing, appeared to cost 99 cents to every visitor outside the UK. The rule that produced it was correct about every other number on the page.
Zero now returns before any of the arithmetic runs, and a test asserts it for every currency in the table. If you are outside the UK, the pricing page is the proof: the free plan reads as zero in your currency, not as a small amount.
One line of small print, and it shows its working
Three stacked grey notices under a price bury the one fact the customer needs. The conversion caveat, the rate behind it and the VAT line are composed into a single sentence instead:
Approximate EUR (£1 ≈ €1.22, incl. card fees). Charged in GBP.
Stripe shows the exact total at checkout. VAT added at checkout based on your country.
The three words "incl. card fees" are load-bearing. Without them the arithmetic does not reconcile, because the figure on screen is higher than the mid-market sum a curious reader will do on their phone, and somebody who checks your maths and finds it wrong trusts the price less, not more. Showing the rate that actually produced the number, rather than the prettier one, is the same instinct.
In our own currency the whole line collapses to the VAT sentence, because there is no conversion left to caveat.
See it yourself
- Open pub-trivia.app/pricing behind a VPN, with the exit node in Dublin, then Stockholm, then Tokyo. The price, the symbol and the rate note under it all change, and the free plan stays free in all three.
- Disable JavaScript and reload it. The localised price is still there, because it was rendered on the server.
- Compare it with pub-trivia.app/features to see what the plan actually includes. The first session is free and needs no card, which is also the quickest way to confirm that zero really is zero wherever you are.
Top comments (0)