fix(scout-report): locale-lock thousands formatting to prevent hydration mismatch#76
Open
rgarciar1931 wants to merge 1 commit into
Conversation
…ion mismatch
DistributionPanel.tsx renders a <Tip text={tipText}> whose 4 unlocaled .toLocaleString() calls (DIST_N = 5000 and friends) produced locale-dependent output ("5,000" en-US vs "5.000" es-ES). Any /<username>?country=<locale> page viewed under a non-en-US browser locale crossed React's hydration-mismatch warning on first paint once the tooltip crossed the 1000s threshold. Add a locale-locked formatThousands(n: number): string helper in lib/format.ts that pins grouping to en-US so SSR (Node) and CSR (browser) render identical bytes regardless of host locale.
eeshsaxena
reviewed
Jul 18, 2026
eeshsaxena
left a comment
Contributor
There was a problem hiding this comment.
Looks correct to me. I checked the mechanism: (5000).toLocaleString() with no argument uses the runtime's default locale, so Node's SSR output and a non-en-US browser disagree (5,000 vs 5.000/5000) - which is exactly what trips React's hydration check. Pinning to en-US makes both sides deterministic.
I also looked for other call sites that could hit the same thing: the only other toLocaleString in the app is components/ScoutForm.tsx:134, and it already passes "en-US". So after this the app is consistent, and formatThousands is a good single home for that guarantee rather than repeating the locale string.
Tests cover the grouping. Deferring to the maintainer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Locale-lock the
tipTextthousands-grouping formatter inlib/format.tstoen-USso SSR (Node) and CSR (browser) always serialize identical bytes for the same number — eliminating React'sHydration failed because the server rendered text didn't match the clientwarning on real-scout pages under non-en-USbrowser locales once values cross the thousands threshold.Problem
components/DistributionPanel.tsxbuilds a<Tip text={tipText}>with four unlocaled.toLocaleString()calls:On Node SSR,
Intldefaults toen-US("5,000"). On a Spanish-locale browser the same call returns"5.000". Both sides are "correct" in their own locale — but React compares the rendered text node byte-for-byte at hydration, and only one matches. When the displayed numbers cross the 1000-threshold (DIST_N = 5000,DIST_ACTIVE_N = 2500), the mismatch is unavoidable unless the helper is locale-locked.This affected every real-scout page viewed under
es-ES,de-DE,fr-FR, etc. Tokenless sample pages (Linus Torvalds, ThePrimeagen, t3dotgg) didn't trigger it because their values stay below 1000 in the rendered copy; the bug only surfaced for our own scout flow once a hosted GitHub account was added.What changed
lib/format.ts— addedformatThousands(n: number): string => n.toLocaleString("en-US"). Co-located next toformatCount/round2/round1, with a comment that names the SSR/CSR parity invariant. Upstream'sformatCountrewrite (thecompacthelper + millions tier) is preserved verbatim and runs first; our locale-lock helper sits below.components/DistributionPanel.tsx— replaced all four.toLocaleString()calls intipTextwithformatThousands(...). A short comment near the call site notes the hydrate-at-risk consequence of unlocaled grouping, so a future reader doesn't reach for.toLocaleString()again in this neighborhood.tests/format.test.ts(new) — 4 cases pinning en-US comma grouping:0,7) rendered verbatim,1000→"1,000",1234→"1,234",12345→"12,345",1234567→"1,234,567"),-1234→"-1,234").Why this fix is minimal and centered on the helper
formatCountwas already locale-independent (toFixed+"k"/"M"), so the rest of the card didn't drift. The new helper covers the only remaining locale-dependent call site in the React tree, and the codebase has no other.toLocaleString()calls without an explicit locale argument. (Verified: onlycomponents/DistributionPanel.tsxhad unlocaled calls;components/ScoutForm.tsx:134already passes"en-US";formatCountusestoFixed.)Validation
npm run lint— clean.npm test— all tests pass onfix/scout-report-hid...tip62480e4. Test count grew from 165 → 241 after merging upstream'stests/format.test.tsadditions in the rebase.Error screenshot