fix: prevent black screen when switching languages mid-load#1
fix: prevent black screen when switching languages mid-load#1cat5inthecradle wants to merge 3 commits into
Conversation
Rapidly switching languages before the lab's message-box text finished
rendering could crash the widget to a black screen (reported on the iframe
embed; Chrome 149 / Win11). Three compounding causes:
- The OceansLab instance was hot-swapped with new strings/textToSpeechLocale
props mid-animation. The lab doesn't support live locale swaps — on
code.org a locale change is a full page reload. Add key={locale} so the
lab cleanly remounts instead of mutating in place.
- The loadStrings effect had no cancellation: two in-flight locale chunks
could resolve out of order and land stale strings mid-render. Guard with a
stale flag and only apply a locale once its strings chunk resolves.
- An uncaught error unmounted the whole React tree, leaving an empty #root
over the dark page background (the "black screen"). Add an ErrorBoundary
with a reload fallback so a crash degrades to a recoverable message.
Adds an e2e regression test for rapid back-to-back language switching.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Filed the upstream defense-in-depth issue: code-dot-org/code-dot-org#73241 (lab should tolerate live |
| componentDidCatch(error: unknown, info: React.ErrorInfo) { | ||
| console.error('AI for Oceans crashed:', error, info.componentStack); | ||
| } |
There was a problem hiding this comment.
This might not be the best UI experience, but it's better than a black screen, and there's no reason I can think of not to expose the error details to aid troubleshooting.
|
Converting back to a draft because there appear to be some issues after changing languages. |
The previous key-remount approach didn't actually fix the black screen and introduced new symptoms (lingering audio, missing text bubble/speaker). Root cause, confirmed by reading the oceans-lab bundle: - The lab keeps locale-dependent state in module-level singletons: a cached React root for the UI overlay (`let rc`, created once, never reset), a global mutable state object, the audio/TTS engine, and pending timers. - Its mount effect re-runs initAll when strings/textToSpeechLocale change, but the cleanup only cancels the animation frame (`cancelAnimationFrame`) — it never stops speechSynthesis/sounds, clears timers, or resets the overlay root. - So a React-level locale swap (key remount or in-place prop change) leaves the overlay root pointing at a torn-down node (no text bubble/speaker) and audio playing. The lab was built for code.org, where a locale change is a full page reload and these globals never survive a switch. Fix: make a language change a full iframe reload with the new ?lang= param, matching code.org. speechSynthesis is cancelled before reload; everything else is torn down by navigation and re-inits cleanly. Progress persists in sessionStorage, so the user stays on the same mode. Reverts the key/deferred-apply prop logic; keeps the ErrorBoundary. Updates the locale e2e tests for reload semantics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Updated the approach after manual testing showed the |
The lab overloads textToSpeechLocale: setting it (a) selects a spoken-TTS voice, (b) silences the guide "typing" sound effect — which only plays when the prop is unset — and (c) chooses the ICU plural-rule locale. The wrapper passed the active locale for every non-English language, which muted the typing sound for all of them and produced spoken audio only for Italian (the one locale with voice data); the other 21 EU locales got no guide audio at all. Stop passing textToSpeechLocale entirely so the typing sound plays in every locale and no spoken TTS runs. Side effect: the lab then compiles catalogs with English plural rules and its ICU compiler throws on `few`/`many` branches (e.g. ?lang=pl: "Invalid key `few`"). Strip plural categories English doesn't define (zero/two/few/many) from catalog strings on load, keeping one/other. Only fishshort/fishlong-pond-init1 use extra categories today; their plurals now follow English rules. Adds a ?lang=pl regression test (loads without the error screen). Upstream tracking of the prop overloading: code-dot-org/code-dot-org#73241 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up in this PR: guide audio across locales (commit c1facaf)Manual QA of the reload fix surfaced a second, pre-existing issue. The lab's Fix: stop passing Added a Verified manually in the iframe embed: typing sounds play in all languages and stop correctly on switch, no spoken Italian, mid-text language change reloads cleanly with no black screen. Upstream tracking of the prop overloading: code-dot-org/code-dot-org#73241 (comment). |
Problem
QA on the iframe embed reported (and reproduced) that switching languages before the previous language finishes rendering its message-box text crashes the widget to a black screen, with audio continuing to play. Reported on Chrome 149 / Windows 11; reproduced locally.
Root cause (from reading the
oceans-labbundle)OceansLabkeeps all locale-dependent state in module-level singletons, not React state:let rc— a cached React root for the UI overlay (text bubble, speaker, guide). Created once (rc || (rc = createRoot(...))), never reset.stateobject (getState/setState/resetState)Sounds.getSingleton(),speechSynthesis)The lab's mount effect does re-run
initAllwhenstrings/textToSpeechLocalechange, but its cleanup only callscancelAnimationFrame— it never stopsspeechSynthesis/sounds, clears timers, or resets the overlay root. So any React-level locale swap leaves the overlay root pointing at a torn-down node (→ missing text bubble/speaker) and audio still playing (→ lingering speech). On code.org the lab never hits this: a locale change there is a full page reload, so the globals are always rebuilt from scratch.Fix
Make a language change a full iframe reload with the new
?lang=param, matching code.org's model:reloadWithLang()cancelsspeechSynthesis, sets?lang=, and reloads.sessionStorage, so the user stays on the same mode; only the current mode's animation restarts (same as code.org).ErrorBoundary(defense-in-depth); reverts the earlierkey-remount / deferred-apply approach, which didn't fix the crash and added the audio/overlay symptoms.Testing
pnpm build(tsc + vite) — passespnpm exec playwright test e2e/locale.spec.ts— 8/8, including a regression test asserting a language switch reloads to the new?langand the lab renders in the new locale?lang=Xis identical to a fresh page load in X, which is the already-verified working pathNotes
deploy.yml) only runs on push tomain, not PRs, so checks were run locally.🤖 Generated with Claude Code