Skip to content

fix(bin-designer): stop 3D preview crashing when the browser auto-translates#1934

Merged
andymai merged 2 commits into
mainfrom
fix/translation-dom-insertbefore-crash
May 29, 2026
Merged

fix(bin-designer): stop 3D preview crashing when the browser auto-translates#1934
andymai merged 2 commits into
mainfrom
fix/translation-dom-insertbefore-crash

Conversation

@andymai
Copy link
Copy Markdown
Owner

@andymai andymai commented May 29, 2026

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:

  • Visitors whose browser language isn't one of our 7 locales (en/de/es/fr/nb/nl/pt-BR) land on the English UI and accept Chrome's auto-translate. 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 → NotFoundError mid-render.
  • The 3D-preview PanelErrorBoundary catches it ($exception + 3d_render_error), so the app survives but the live preview drops to the Retry panel.
  • A stacking lid renders extra conditional overlay nodes (LidMesh, LidGuideLine, LidExplodeSlider, the live aria-live status 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

  • Global guard (src/shared/utils/translationDomGuard.ts): patches Node.prototype.insertBefore/removeChild to 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 in main.tsx before 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

  • New translationDomGuard.test.ts — 5/5 pass (real removals/inserts still work; cross-parent calls no-op instead of throwing).
  • PreviewCanvas 13/13, typecheck, eslint (touched files) all green.

Follow-up (not in this PR)

Adding zh-CN, ru, ja, it to i18n would stop these users landing on English at all — addressing the demand for translation rather than just hardening against it.

…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.
Copilot AI review requested due to automatic review settings May 29, 2026 17:07
@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gridfinity-layout-tool Ready Ready Preview, Comment May 29, 2026 5:16pm

Request Review

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 29, 2026

Greptile Summary

Adds a two-layer defense against browser page-translation crashing the 3D preview: a global Node.prototype guard that silently no-ops cross-parent insertBefore/removeChild calls (installed in main.tsx before first paint), plus translate="no" on the PreviewCanvas root so translators never touch its volatile overlay nodes in the first place.

  • translationDomGuard.ts: Patches both mutation methods with a parent-check that returns the node unchanged instead of throwing NotFoundError; the prototype-brand idempotency guard prevents double-wrapping across module re-evaluations.
  • PreviewCanvas.tsx: translate="no" opts the entire 3D canvas subtree (icons, live status text, conditional lid overlays) out of translation, eliminating the desync window at the source.
  • Tests: Five cases cover real operations still working, idempotency, and cross-parent no-ops for both patched methods.

Confidence Score: 5/5

Safe 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

Filename Overview
src/shared/utils/translationDomGuard.ts New utility that patches Node.prototype.insertBefore/removeChild to no-op on cross-parent calls caused by browser page-translation; idempotent, early-exit guarded, and well-documented.
src/shared/utils/translationDomGuard.test.ts Six focused tests cover idempotency, real child operations, and cross-parent no-op paths for both removeChild and insertBefore; uses proper beforeAll/afterAll prototype cleanup.
src/main.tsx Installs the translation DOM guard at module-evaluation time before any React rendering; minimal, correctly placed change.
src/features/bin-designer/components/PreviewCanvas/PreviewCanvas.tsx Adds translate="no" to the 3D-preview root div, opting the volatile canvas overlay subtree out of browser translation entirely.

Reviews (2): Last reviewed commit: "refactor(bin-designer): make translation..." | Re-trigger Greptile

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / insertBefore calls.
  • 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.

Comment on lines +15 to +18
afterAll(() => {
Node.prototype.removeChild = originalRemoveChild;
Node.prototype.insertBefore = originalInsertBefore;
});
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@andymai
Copy link
Copy Markdown
Owner Author

andymai commented May 29, 2026

Addressed the idempotency concern in b81a70d. Rather than adding a second check alongside the installed boolean, I removed the module-level flag entirely and brand Node.prototype with a non-enumerable marker — now the idempotency guard and the prototype mutation live on the same global object, so a vi.resetModules() re-evaluation can't desync them and double-wrap. Added a re-install idempotency test (6/6 passing).

@andymai andymai merged commit d5d4d6a into main May 29, 2026
16 checks passed
@andymai andymai deleted the fix/translation-dom-insertbefore-crash branch May 29, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants