fix(web): prevent locale bootstrap from overriding user selection (#759)#924
Merged
fix(web): prevent locale bootstrap from overriding user selection (#759)#924
Conversation
The LocaleProvider runs an async bootstrap on mount that fetches the desktop preferences from the controller and then calls setLocaleState with either the server value (when present) or the locally detected default (when the server has no stored locale yet). On a fresh Windows zh-CN install the second branch always fires, and if the user clicks the language switcher before the GET resolves, the bootstrap reverts their selection back to "zh". Fix: - Track manual user interaction via a ref. setLocale flips it to true before any state update so the bootstrap can detect that a manual selection has happened during its in-flight fetch. - bootstrapLocale checks the ref after awaiting the GET and bails out early if the user has already interacted. - In the "server has no stored locale" branch, stop calling setLocaleState entirely. The useState initializer has already set React state to the detected default, so this call was redundant even in the non-racy path and only existed to fight the race window. We still persist to localStorage and POST to the controller so the credit-guard plugin and other locale-aware code see a consistent value. Closes #759
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
PerishCode
approved these changes
Apr 8, 2026
Merged
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.
What
Fix the welcome-screen language switcher being unable to switch from Chinese to English on a fresh Windows install (issue #759).
Why
LocaleProvider(apps/web/src/hooks/use-locale.tsx) runs an async bootstrap on mount that:useState(detectDefault)— on a Windows zh-CN system this returns"zh".setLocaleState(...)with either the server-stored locale (if present) or the locally detected default (if not).On a fresh install the controller has no stored locale yet, so step 3 takes the second branch and unconditionally calls
setLocaleState(localCandidate)— i.e.setLocaleState("zh"). If the user clicks English in the welcome screen language switcher before the GET resolves:setLocale("en")updates React state → UI briefly switches to EnglishbootstrapLocalecallssetLocaleState("zh")→ React state reverts to ChineseThe bug is Windows-specific in practice because Windows users overwhelmingly run zh-CN system locale. macOS users on en-US never see it:
localCandidateis"en", so the bootstrap override is effectively"en" → "en"and is invisible.How
Two changes in
apps/web/src/hooks/use-locale.tsx:Track manual interaction. New
userInteractedRefis flipped totrueinsidesetLocalebefore any state update.bootstrapLocalenow takes this ref as a parameter and bails out early if the user has already interacted while the GET was in flight.Don't call
setLocaleStatein the "server has no stored locale" branch. TheuseStateinitializer has already seeded React state tolocalCandidatesynchronously on first render, so callingsetLocaleState(localCandidate)here was redundant in the non-racy path and only ever existed to fight the race window. Removing it eliminates the override entirely. We still persist tolocalStorageand POST to the controller so the credit-guard plugin and other locale-aware code see a consistent value.The existing "server has a stored value" branch is unchanged: when the controller already has a locale, it continues to win on bootstrap (the typical "sync from server on app open" behavior), gated by the user-interaction check.
Affected areas
Checklist
pnpm typecheckpasses — not run locally (worktree without node_modules); change is type-stable: only adds auseRef<boolean>and a parameter of the same shape ({ current: boolean })pnpm lintpasses — not run locally for the same reasonpnpm testpasses — N/A, no tests cover this path todaypnpm generate-typesrun (if API routes/schemas changed) — N/Aanytypes introducedNotes for reviewers
setLocaleStatecall — same behavior as before for theresponse === nullcase.didBootstrapRefstill ensures bootstrap runs once.setLocaleruns normally, no race because bootstrap already returned.Closes #759