fix(bin-designer): stop 3D preview crashing when the browser auto-translates#1934
Conversation
…nslates Non-English visitors (zh-CN, ru, ja, it) land on the English-only UI, accept Chrome's auto-translate, and Google Translate rewraps text nodes in <font> tags. That desyncs React's virtual DOM, so the reconciler later calls insertBefore/removeChild against a node the translator already moved and throws NotFoundError mid-render. The 3D-preview PanelErrorBoundary catches it and collapses the live preview to the Retry panel — most visibly while exporting a bin (extra lid/overlay nodes widen the re-render window). PostHog confirms 100% of affected users are non-English. - Add a global Node.prototype insertBefore/removeChild guard that no-ops on cross-parent calls instead of throwing, installed before first paint so every boot path is covered. - Mark the 3D-preview root translate="no" so translators never touch its volatile React-managed overlay/status text.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryAdds a two-layer defense against browser page-translation crashing the 3D preview: a global
Confidence Score: 5/5Safe to merge — the changes are narrowly scoped to translation resilience with no mutations to application logic, state, or rendering paths for normal users. Both the prototype patch and the translate=no attribute are established, well-understood techniques for this exact React/translator desync class. The patch correctly short-circuits only on provably wrong calls (child not in this parent / reference not in this parent), so real DOM operations are unaffected. Idempotency is guarded at the prototype level, installation happens before any React root is created, and the test suite verifies all meaningful code paths. No application data or state is touched. No files require special attention. Important Files Changed
Reviews (2): Last reviewed commit: "refactor(bin-designer): make translation..." | Re-trigger Greptile |
There was a problem hiding this comment.
Pull request overview
This PR hardens the React app against browser auto-translation mutating DOM nodes underneath React, which was causing the bin designer 3D preview to fall into its error boundary.
Changes:
- Adds a global DOM mutation guard for cross-parent
removeChild/insertBeforecalls. - Installs the guard during app startup before boot-path branching.
- Opts the bin designer preview surface out of browser translation.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/shared/utils/translationDomGuard.ts |
Adds the prototype guard for translation-induced DOM desyncs. |
src/shared/utils/translationDomGuard.test.ts |
Adds jsdom coverage for guarded and normal DOM operations. |
src/main.tsx |
Installs the guard at startup. |
src/features/bin-designer/components/PreviewCanvas/PreviewCanvas.tsx |
Adds translate="no" to the preview root. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| afterAll(() => { | ||
| Node.prototype.removeChild = originalRemoveChild; | ||
| Node.prototype.insertBefore = originalInsertBefore; | ||
| }); |
There was a problem hiding this comment.
This is resolved by b81a70d (the commit addressing Greptile's related point), which landed in the same revision this thread is on. There's no longer a module-scoped installed flag — idempotency is now branded on Node.prototype itself, and this test's afterAll deletes that brand (line 19, Reflect.deleteProperty) alongside restoring the native methods. So a later installTranslationDomGuard() in the same Vitest worker re-patches correctly; no order-dependence remains. Leaving the code as-is.
…totype brand Greptile review: the module-level `installed` flag and the Node.prototype mutation were independent state. vi.resetModules() could reset the flag while the prototype stayed patched, so a re-install would capture the already-wrapped function as "original" and double-wrap. Brand the prototype with a non-enumerable marker instead, tying the idempotency check to the same global object the patch mutates. Adds a re-install idempotency test.
|
Addressed the idempotency concern in b81a70d. Rather than adding a second check alongside the |
What
Non-English visitors hit a
NotFoundError: Failed to execute 'insertBefore' on 'Node'that collapses the bin designer's 3D preview to its "Retry" error panel — most visibly while exporting a bin (notably one with a stacking lid).Root cause
It's not the export pipeline — exports succeed every time. It's browser page-translation desyncing React:
<font>tags.insertBefore/removeChildagainst a node the translator already moved →NotFoundErrormid-render.PanelErrorBoundarycatches it ($exception+3d_render_error), so the app survives but the live preview drops to the Retry panel.LidMesh,LidGuideLine,LidExplodeSlider, the livearia-livestatus text), widening the export-time re-render window — hence the correlation.PostHog confirms 100% of affected users are non-English (
zh-CN,ru-RU,ru,ja,it) across all three issues; ~14 occurrences / ~9 users over ~12 days, still active.Fix
src/shared/utils/translationDomGuard.ts): patchesNode.prototype.insertBefore/removeChildto no-op on cross-parent calls instead of throwing. The translated text is already where the translator put it, so skipping React's redundant move is visually harmless. Installed inmain.tsxbefore first paint so every boot path is covered. (See Make React resilient to DOM mutations from Google Translate facebook/react#11538.)translate="no"on the 3D-preview root so translators never touch its volatile React-managed overlay/status text in the first place.Testing
translationDomGuard.test.ts— 5/5 pass (real removals/inserts still work; cross-parent calls no-op instead of throwing).PreviewCanvas13/13,typecheck,eslint(touched files) all green.Follow-up (not in this PR)
Adding
zh-CN,ru,ja,itto i18n would stop these users landing on English at all — addressing the demand for translation rather than just hardening against it.