How a UK paracetamol bottle changed Tempy’s dose UI

How a UK paracetamol bottle changed Tempy's dose UI
A reviewer asked an awkward question about Tempy's medication picker last week: which bottle are we calculating against? The answer was buried in a constant — 160 mg per 5 mL for acetaminophen, which is the common US/KR concentration. The trouble is, plenty of parents pour from a 250 mg/5 mL bottle instead. The same number of millilitres, a very different number of milligrams. So we changed the UI to say, on screen, what it assumes.
The problem
Tempy lets a parent log a dose by picking a medication, choosing a form (tablet, chewable, liquid), and entering an amount. When the form is liquid, the underlying calculator converts millilitres to milligrams using a fixed concentration baked into AppConstants. For acetaminophen, that constant is 160 mg/5 mL — the dominant retail concentration in the US and Korea, and the right default for our launch markets.
It is not the only concentration on the shelf, though. UK paracetamol regularly ships at 120 mg/5 mL (infant) or 250 mg/5 mL (older children). If a parent in London opens a 250 mg/5 mL bottle, pours 5 mL, and types 5 into Tempy, the app records the dose as 160 mg — under-reporting by roughly 36%. The next time the app considers whether enough time has passed for the next dose, it's reasoning about the wrong amount. That's a quiet kind of wrong, and the wrong direction.
We caught it in code review before the bottle reached anyone's child. The story is worth telling anyway, because the lesson is about how the fix landed rather than the bug itself.
Why it happened
The dose calculator has been single-purpose since the first version of Tempy: take a medication enum, a form, and a quantity, return milligrams. Concentration lived as a constant because we were optimising for fewer fields at 3 AM — asking "what does the bottle say?" felt like the wrong thing to put between a tired parent and "done".
That tradeoff was reasonable when our audience was Korean and US households. It stopped being reasonable the day someone in the UK installed the app. The constant didn't get wrong — it became regional. And there was no surface in the UI saying "we're assuming X" that would tip a parent off that the number in the field needs translating.
// lib/utils/dose_calculator.dart (paraphrased)
double mgFromMl(MedicationType med, double ml) {
final mgPer5ml = switch (med) {
MedicationType.acetaminophen => 160, // <-- US/KR retail
MedicationType.ibuprofen => 100,
MedicationType.dexibuprofen => 20,
};
return ml * mgPer5ml / 5;
}
Per-child, per-bottle concentration selection is the right long-term shape, and it ships in 1.0.6+59. We didn't want to leave the gap open while we built it.
What we tried first
The instinct was to fix it silently: detect locale, swap the constant. en-GB → 250 mg/5 mL. Done.
Two problems. First, UK retail isn't uniform — 120 mg/5 mL and 250 mg/5 mL are both on shelves, and parents have whichever they grabbed last. We'd be swapping one wrong default for another, with the same silent failure mode. Second, a calculator that quietly re-interprets your input is harder to trust than one that tells you what it's doing. The app should never claim to know what's in the bottle. The bottle does.
So the fix isn't a smarter default. It's to stop hiding the default.
The fix
Underneath the form selector, when "Liquid" is chosen, the picker now renders a single amber-tone hint:
Calculated for 160 mg/5 mL — verify your bottle
It's exactly twelve words in English, and exactly twelve corresponding phrases in the twelve locales Tempy ships in. No interstitial, no modal, no Did you know?. Just the assumption, in the parent's hand, before they enter the number.
if (_selectedForm == MedicationForm.liquid) ...[
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: _LiquidStrengthHint(
strength: _getLiquidStrength(), // "160" / "100" / "20"
),
),
],
The hint pulls its number from a per-medication helper, so ibuprofen reads 100 mg/5 mL, dexibuprofen reads 20 mg/5 mL. Wording stays put; the number tracks the active medication. The localised string is a single ARB key with a placeholder, landed across 12 locale files at once (en, en-US, de, es, fr, hi, ja, ko, pt, ru, vi, zh) plus the generated app_localizations_*.dart for each.
{
"liquidStrengthNote": "Calculated for {strength} mg/5 mL — verify your bottle",
"@liquidStrengthNote": {
"placeholders": { "strength": { "type": "String" } }
}
}
The same diff tidies a separator that was overflowing on the narrowest buttons: Paracetamol · Tylenol became Paracetamol/Tylenol, saving two characters and a FittedBox shrink. Small, but the medication picker is where every character of label width matters.
Before and after
| Before | After |
|---|---|
| Liquid concentration encoded as a constant, invisible to the user. | Liquid concentration printed under the form selector, in 12 locales. |
| 250 mg/5 mL UK bottle → ~36% under-reported mg silently. | Same UK parent now sees 160 mg/5 mL — verify your bottle and is invited to flag the mismatch before logging. |
Hint was first drafted inside the existing Reference box, which only renders when weight is set. |
Hint moved out of the weight-set branch (the M1 regression flagged in review) so weight-less logs see it too. |
The metric we cared about — can a parent log a dose without ever seeing what we assume? — went from yes to no. That is the entire success criterion for this change.
What we learned
A few notes we're carrying forward.
When you can't make a problem disappear, surface the assumption. The calculator can't know what bottle a parent is holding. It can refuse to pretend it does. Cheap UI move, real safety value, and it bought us time to ship the per-child concentration setting without leaving people in the dark.
Localisation is not just words — it's defaults. 160 mg/5 mL was a regional default encoded as if it were universal. Internationalising the strings without internationalising the defaults is a class of bug we'll be looking for again, especially in health-adjacent surfaces.
Put hints in the always-rendered branch. The first draft lived inside the Reference box, which only renders once weight is filled in. A parent logging a dose without entering a weight (common for repeat dosing) would never see it. Review caught it; the fix was to lift the hint up.
What's next
The proper version is per-child, per-bottle concentration selection, which lands in 1.0.6+59. It moves concentration out of AppConstants and into the child profile, with a "this bottle is" picker on first liquid log per child. The hint we just shipped stays — it'll just read whatever the parent set instead of the regional default.
Two more surfaces to revisit: tablet strengths (500 mg acetaminophen, 200 mg ibuprofen) have the same regional drift, and we owe ourselves an audit of every regional constant in the dose path.
The thing we did not do, and won't, is auto-detect locale and swap the constant. The app's job is to do the math you tell it to do. The bottle's job is to tell you what's in it. The UI's job is to make sure those two never drift apart silently.
Try Tempy
Tempy is a calm, offline-first fever log for parents — built so it survives 3 AM.
Frequently Asked Questions
Why did Tempy change how it displays the dose calculation for liquid medications?
Tempy changed its UI to explicitly show the assumed concentration (e.g., 160 mg/5 mL) because different regions use different bottle concentrations. This prevents parents from unknowingly logging incorrect doses when their bottle's concentration differs from the app's default.
What problem arises from using a fixed concentration constant for acetaminophen in Tempy?
Using a fixed concentration like 160 mg/5 mL can cause under-reporting or over-reporting of doses if the parent’s bottle has a different concentration, such as 250 mg/5 mL in the UK. This leads to inaccurate dose calculations and timing for the next dose.
Why didn’t Tempy implement automatic locale-based concentration detection?
Because UK bottles have multiple common concentrations (120 mg/5 mL and 250 mg/5 mL), auto-detecting by locale could swap one wrong default for another. Instead, Tempy chose to show the assumed concentration and let parents verify their bottle to avoid silent errors.
How does Tempy’s new UI help parents avoid dose calculation errors?
When a liquid form is selected, Tempy now displays a clear hint showing the concentration it assumes for calculations (e.g., 'Calculated for 160 mg/5 mL — verify your bottle'). This transparency encourages parents to check their bottle’s concentration before entering a dose.
What future improvements are planned for Tempy’s dose calculation system?
Tempy plans to implement per-child, per-bottle concentration selection, allowing parents to specify the exact concentration for each child’s medication. This will replace the fixed constants and improve accuracy across different regions and bottle types.
Continue reading

The removeConsole: true bug that hid every other bug
A Linkgo Railway memory pass surfaced a one-line next.config.js bug that had been silently stripping every console.error in production — and the three reliability fixes that surfaced once we could read the logs.

What happens to a connection pool when you fork a Celery worker (spoiler: nothing good)
Our admin dashboard started hitting Prisma P2037. pg_stat_activity showed 96 idle connections and 1 active. Here's how two SQLAlchemy engines, default pool settings, and a fork-unsafe Celery worker added up to 150 theoretical connections from one container — and the three-change fix.

Meet Linkgo: A Curated Directory of AI Tools, Agents, and MCPs
Linkgo is a curated AI tools directory at linkgo.dev — browse tools, agents, services, MCPs, and models across five operator-reviewed categories with screenshots, pricing, and FAQs.