/** * Makes React (and any other text-node-owning renderer) resilient to browser * page translation — Chrome / Google Translate and Edge, which merge and * replace Text nodes with `` wrappers, and Firefox, which empties and * refills translated elements, reusing only some of their Text nodes. React * keeps references to the original, now detached, Text nodes, so without this * shim: * * - unmounting translated conditional text throws NotFoundError (removeChild) * - mounting content before translated text throws NotFoundError (insertBefore) * - updating translated text writes to the detached node — the visible page * silently never updates again (see facebook/react#11538) * * Strategy — "re-adoption", not error swallowing: * * 1. A document-wide MutationObserver watches mutations. Translation displaces * text nodes in a recognizable pattern: adjacent Text nodes are merged * (`normalize()`), then the merged run is replaced by wrapper elements * inserted next to it in the same task. We track a DISPLACEMENT GROUP per * replaced run: the ordered renderer-owned originals (with their * pre-translation values) and the run of replacement nodes standing in for * them. React's own commits never look like this: React processes * deletions before placements, so a node it removes is never adjacent to a * same-batch insertion at removal time, and React never merges text nodes. * * 2. Patched Node.prototype.removeChild / insertBefore / appendChild and the * nodeValue / data setters detect operations on displaced Text nodes and * first RESTORE the group — original nodes go back into the replacement * run's position, replacements are removed — then let the native operation * proceed. This repairs the renderer's ownership invariant: updates become * visible, removals remove the right content, and the translator * re-translates the freshly restored text via its own observer. * * 3. Translation can also delete text nodes outright and move inline elements * (word-order changes — Chromium bug 872770). Deletions within a * translation batch get an empty group with position hints so the node can * come back if React updates it. Once translation activity has been * detected, unrecoverable parent mismatches degrade to guarded best-effort * operations instead of throwing. * * 4. Firefox applies a translation to an element by detaching every child and * appending the translation back, reusing Text nodes by position; all but * the first node of each run of text stay detached (see * recognizeChildrenMerges). A lossy merge gets a WHOLE-PARENT group — the * parent's original children and pre-translation text — restored before * any renderer operation on one of them, attached or not. Firefox then * re-translates the restored nodes one by one, in place. * * Arming, and why it cannot rely on the patches above: a browser's page * translator runs in the engine's own ISOLATED WORLD — it shares the DOM but * holds a separate copy of Node.prototype (Chromium runs the translate script * via ExecuteScriptInIsolatedWorld; see translate_agent.cc). None of the * patched methods here ever observe a translator's own mutations, so every * arming signal has to be one the shared DOM raises: * * - Chrome's translator adds a `translated-*` class to a few hundred * ms before it touches text: an attribute sentinel catches that, and * attributes are shared across worlds. * - Firefox's translator sets as it starts. Apps write that * attribute too, so only a write from outside the page counts — told apart * by wrappers on the instance that the page's own writes go through * and a translator's never do (see trackPageAttributeWrites). * - Edge's translator and the Google Translate extension mark nothing. For * those, a detection stylesheet (DETECTION_CSS) puts a CSS animation on the * translator's signature , and `animationstart` fires whichever world * inserted it — at a fraction of the cost of observing the document. * - Whatever still slips through lands on a patched method that is about to * throw NotFoundError, where a document sweep (translationEvident) tells a * translated page from a genuine renderer bug. * * Before any translation activity is detected, operations on untracked nodes * behave exactly as before — including throwing on genuine bugs. */ interface TranslationResilienceOptions { document?: Document; /** Observability hook: called with a short message on every non-native path taken. */ onEvent?: (message: string) => void; /** * Install the document-wide observer immediately instead of waiting for a * translation signal. The lazy default costs nothing until translation * starts, and arms on Chrome's translated-* class, on Firefox's * write from outside the page, and on the translator's own wrappers * for translators that mark nothing (Edge's built-in translator, the Google * Translate extension). eager remains as an escape hatch for a translator * that marks nothing and inserts no recognizable wrapper. */ eager?: boolean; } declare function installTranslationResilience(options?: TranslationResilienceOptions): () => void; export { type TranslationResilienceOptions, installTranslationResilience };