translation-resilience 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/index.cjs +398 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +371 -0
- package/dist/index.js.map +1 -0
- package/dist/simulator.cjs +165 -0
- package/dist/simulator.cjs.map +1 -0
- package/dist/simulator.d.cts +45 -0
- package/dist/simulator.d.ts +45 -0
- package/dist/simulator.js +138 -0
- package/dist/simulator.js.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Speller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# translation-resilience
|
|
2
|
+
|
|
3
|
+
Stops browser page translation (Chrome's built-in Google Translate) from **crashing React apps** and **silently freezing translated text** — by repairing the DOM instead of swallowing errors.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { installTranslationResilience } from 'translation-resilience';
|
|
7
|
+
|
|
8
|
+
installTranslationResilience();
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Framework-agnostic (it patches the DOM layer, not React), dependency-free, ~2.3 kB min+gzip.
|
|
12
|
+
|
|
13
|
+
## The problem
|
|
14
|
+
|
|
15
|
+
When Chrome translates a page, it rewrites the DOM underneath your framework:
|
|
16
|
+
|
|
17
|
+
1. Adjacent `Text` nodes are merged (`normalize()`) — so `Lights: {count}` collapses into one run.
|
|
18
|
+
2. The merged run is split into segments (numbers isolated) and each segment is wrapped in nested `<font style="vertical-align: inherit;">` elements containing **new** text nodes.
|
|
19
|
+
3. The original text nodes are removed from the document — but React still owns and references them.
|
|
20
|
+
4. Translation can also delete text nodes outright and reorder inline elements to match target-language word order ([Chromium bug 872770](https://issues.chromium.org/issues/41410214)).
|
|
21
|
+
|
|
22
|
+
React keeps operating on the original, now-detached nodes:
|
|
23
|
+
|
|
24
|
+
- **Crash**: unmounting translated conditional text throws `NotFoundError: Failed to execute 'removeChild' on 'Node'`. On React ≤ 18 this takes down the whole tree; React 19 catches it and tears down to the nearest error boundary — still broken, just politer.
|
|
25
|
+
- **Crash**: mounting an element before translated text throws the same from `insertBefore`.
|
|
26
|
+
- **Silent freeze**: updating translated text (counters, timers, live data) writes into the detached node. The visible page never updates again, with no error anywhere.
|
|
27
|
+
|
|
28
|
+
Background reading: [facebook/react#11538](https://github.com/facebook/react/issues/11538), [Everything about Google Translate crashing React](https://martijnhols.nl/blog/everything-about-google-translate-crashing-react). The same mechanism breaks Vue ([vuejs/core#14810](https://github.com/vuejs/core/issues/14810)) and Ember/Glimmer ([glimmerjs/glimmer-vm#1372](https://github.com/glimmerjs/glimmer-vm/issues/1372)).
|
|
29
|
+
|
|
30
|
+
## Why not the well-known monkey-patch?
|
|
31
|
+
|
|
32
|
+
The widely copied [error-swallowing patch](https://github.com/facebook/react/issues/11538#issuecomment-417504600) catches the `removeChild`/`insertBefore` exceptions. That stops the crash but **not the freeze**: text React updates still never reaches the screen, and "removed" text stays visible. Other mitigations — wrapping every text interpolation in a `<span>`, or `<meta name="google" content="notranslate">` — are invasive or hostile to users who genuinely need translation.
|
|
33
|
+
|
|
34
|
+
## How this is different: re-adoption
|
|
35
|
+
|
|
36
|
+
Instead of swallowing errors, this shim puts the original text nodes **back** the moment the renderer touches them:
|
|
37
|
+
|
|
38
|
+
1. A document-wide `MutationObserver` recognizes translation's displacement pattern (merge, wrap, remove — a pattern renderer commits never produce) and tracks each replaced text run as a *displacement group*: the ordered renderer-owned originals with their pre-translation values, plus the wrapper nodes currently standing in for them.
|
|
39
|
+
2. Patched `Node.prototype.removeChild` / `insertBefore` / `appendChild` and the `nodeValue` / `data` setters detect operations on displaced text nodes and first **restore the group** — originals go back into the wrappers' position, wrappers are removed — then let the native operation proceed on a consistent tree.
|
|
40
|
+
3. The translator's own observer notices the restored (now updated) text and re-translates it, so the user sees fresh, translated content. The loop is self-healing: update → restore → re-translate.
|
|
41
|
+
|
|
42
|
+
The result: no crashes, **and** live data keeps updating on translated pages — in the visitor's language.
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
Install:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm install translation-resilience
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Call once in your client entrypoint, before your framework renders:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { installTranslationResilience } from 'translation-resilience';
|
|
56
|
+
|
|
57
|
+
const uninstall = installTranslationResilience();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Options
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
installTranslationResilience({
|
|
64
|
+
// Which document to observe (defaults to the global `document`).
|
|
65
|
+
document,
|
|
66
|
+
|
|
67
|
+
// Observability hook: called with a short message on every non-native
|
|
68
|
+
// path taken, e.g. 'translation activity detected',
|
|
69
|
+
// 'removeChild: displaced text already gone, removal skipped'.
|
|
70
|
+
onEvent: (message) => {
|
|
71
|
+
Bugsnag.leaveBreadcrumb('translation-resilience', { message }, 'log');
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`onEvent` is the recommended way to learn how often translation actually happens to your users — the `'translation activity detected'` message fires once per page load, only when a translator starts displacing text.
|
|
77
|
+
|
|
78
|
+
## Safety properties
|
|
79
|
+
|
|
80
|
+
- **Inert until translation happens.** Before any translation activity is detected, every operation behaves natively — including throwing `NotFoundError` on genuine `removeChild`/`insertBefore` bugs in your code. The shim does not mask real bugs.
|
|
81
|
+
- **Fault-contained.** Every non-native code path runs inside a guard; an internal error in the shim falls back to stock browser behavior and reports through `onEvent` (`internal error in …`). A bug in the shim can never make things worse than not having it.
|
|
82
|
+
- **Reversible.** `installTranslationResilience()` returns an uninstall function that restores all prototypes and disconnects the observer. Calling install twice returns the same uninstall (idempotent).
|
|
83
|
+
- **SSR-safe to import.** Native DOM entry points are captured lazily, so importing the module in Node is fine; only *calling* install requires a DOM.
|
|
84
|
+
|
|
85
|
+
### Cost
|
|
86
|
+
|
|
87
|
+
One document-wide `MutationObserver` (childList + characterData, subtree). Record processing is linear in the number of mutations with small constants, and correlation state expires after 100 ms, so steady-state memory is effectively zero. The prototype patches add one `parentNode` comparison to `removeChild`/`insertBefore`/`appendChild` calls on the happy path.
|
|
88
|
+
|
|
89
|
+
## Testing your app against translation: the simulator
|
|
90
|
+
|
|
91
|
+
The package ships the mutation simulator used to test the shim itself — it reproduces Google Translate's exact DOM mutations (merge, segment-split, nested `<font>` wrappers, detached originals, deletions, word-order moves, and the ongoing re-translation observer) so you can write deterministic tests without a real browser translation session:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { render } from '@testing-library/react';
|
|
95
|
+
import { installTranslationResilience } from 'translation-resilience';
|
|
96
|
+
import { startTranslateObserver, translateSubtree } from 'translation-resilience/simulator';
|
|
97
|
+
|
|
98
|
+
it('keeps counters updating on translated pages', async () => {
|
|
99
|
+
const uninstall = installTranslationResilience();
|
|
100
|
+
const { container, rerender } = render(<Counter count={4} />);
|
|
101
|
+
const stopTranslator = startTranslateObserver(container);
|
|
102
|
+
translateSubtree(container);
|
|
103
|
+
|
|
104
|
+
rerender(<Counter count={5} />);
|
|
105
|
+
await Promise.resolve();
|
|
106
|
+
|
|
107
|
+
expect(container.textContent).toContain('5');
|
|
108
|
+
stopTranslator();
|
|
109
|
+
uninstall();
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`translateSubtree(root, translate?, options?)` performs the initial translation pass (the default `translate` wraps text as `[text]` so assertions are easy); `options` can simulate the nastier behaviors: `deleteTextNodes` (translation dropping nodes) and `moveToParentEnd` (word-order element moves). `startTranslateObserver(root)` keeps re-translating changed content, like the real thing.
|
|
114
|
+
|
|
115
|
+
## Compatibility and limitations
|
|
116
|
+
|
|
117
|
+
- **Renderers**: developed and tested against React 18 (the tests render real components with `react-dom` and assert both crash-avoidance and update-visibility). React 19 benefits equally — its error-boundary teardown and the silent freeze both disappear. The patch layer is framework-agnostic, so Vue/Svelte/Ember apps should benefit too, but the test suite currently covers React.
|
|
118
|
+
- **Translators**: built against Chrome's translation mutations (the merge/wrap/remove pattern, `<font>` wrappers). A translator that mutates differently simply isn't recognized — unrecognized mutations degrade to stock behavior, never worse than not having the shim.
|
|
119
|
+
- **`event.target` inside translated text is a `<font>` element.** That's inherent to page translation, not this shim. React's synthetic event dispatch is unaffected (handlers fire on the right components with the right `currentTarget`), but avoid comparing `event.target` by identity or tag — use `closest()`/`contains()`.
|
|
120
|
+
- **Word-order moves are cosmetically imperfect**: text the translator deleted for a word-order change is restored at the best position still known, which may differ from the translator's chosen ordering until re-translation catches up.
|
|
121
|
+
- **Same-realm only**: iframes have their own prototypes and documents; install the shim inside each frame that renders your app.
|
|
122
|
+
|
|
123
|
+
## Demo
|
|
124
|
+
|
|
125
|
+
`npm run build`, serve the repo root over HTTP (`npx serve .`), and open `/demo/`. Translate the page via Chrome's right-click → "Translate", or use the "Simulate Google Translate" button in browsers without it. `?shim=off` shows stock behavior: the toggles crash the app and the counter freezes.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT © Alex Speller
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
installTranslationResilience: () => installTranslationResilience
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(src_exports);
|
|
26
|
+
|
|
27
|
+
// src/resilience.ts
|
|
28
|
+
var displaced = /* @__PURE__ */ new WeakMap();
|
|
29
|
+
var groupByReplacementNode = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
var pendingCarrierOriginals = /* @__PURE__ */ new WeakMap();
|
|
31
|
+
var observer = null;
|
|
32
|
+
var translationDetected = false;
|
|
33
|
+
var noopEvent = (_message) => void 0;
|
|
34
|
+
var emitEvent = noopEvent;
|
|
35
|
+
var capturedNatives = null;
|
|
36
|
+
function domNatives() {
|
|
37
|
+
if (capturedNatives) return capturedNatives;
|
|
38
|
+
if (typeof Node === "undefined" || typeof CharacterData === "undefined") {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"translation-resilience requires a DOM. Call installTranslationResilience() from client-side code only."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const nodeValueDescriptor = Object.getOwnPropertyDescriptor(Node.prototype, "nodeValue");
|
|
44
|
+
const dataDescriptor = Object.getOwnPropertyDescriptor(CharacterData.prototype, "data");
|
|
45
|
+
const nodeValueSet = nodeValueDescriptor == null ? void 0 : nodeValueDescriptor.set;
|
|
46
|
+
const dataSet = dataDescriptor == null ? void 0 : dataDescriptor.set;
|
|
47
|
+
if (!(nodeValueDescriptor == null ? void 0 : nodeValueDescriptor.get) || !nodeValueSet || !(dataDescriptor == null ? void 0 : dataDescriptor.get) || !dataSet) {
|
|
48
|
+
throw new Error("translation-resilience: expected accessor descriptors for nodeValue and data");
|
|
49
|
+
}
|
|
50
|
+
capturedNatives = {
|
|
51
|
+
insertBefore: Node.prototype.insertBefore,
|
|
52
|
+
removeChild: Node.prototype.removeChild,
|
|
53
|
+
appendChild: Node.prototype.appendChild,
|
|
54
|
+
nodeValueDescriptor,
|
|
55
|
+
dataDescriptor,
|
|
56
|
+
setNodeValue: (node, value) => nodeValueSet.call(node, value),
|
|
57
|
+
setData: (node, value) => dataSet.call(node, value)
|
|
58
|
+
};
|
|
59
|
+
return capturedNatives;
|
|
60
|
+
}
|
|
61
|
+
function guarded(operation, run, fallback) {
|
|
62
|
+
try {
|
|
63
|
+
return run();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
emitEvent(`internal error in ${operation}: ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
+
return fallback;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function registerGroup(group) {
|
|
70
|
+
if (!translationDetected) {
|
|
71
|
+
translationDetected = true;
|
|
72
|
+
emitEvent("translation activity detected");
|
|
73
|
+
}
|
|
74
|
+
for (const original of group.originals) displaced.set(original.node, group);
|
|
75
|
+
for (const node of group.replacement) groupByReplacementNode.set(node, group);
|
|
76
|
+
}
|
|
77
|
+
function unregisterGroup(group) {
|
|
78
|
+
for (const original of group.originals) displaced.delete(original.node);
|
|
79
|
+
for (const node of group.replacement) groupByReplacementNode.delete(node);
|
|
80
|
+
}
|
|
81
|
+
var CORRELATION_WINDOW_MS = 100;
|
|
82
|
+
var recentInsertedBefore = /* @__PURE__ */ new Map();
|
|
83
|
+
var recentOldValues = /* @__PURE__ */ new Map();
|
|
84
|
+
var recentCarrierAccumulated = /* @__PURE__ */ new Map();
|
|
85
|
+
var pendingOrphans = [];
|
|
86
|
+
function purgeExpired(now) {
|
|
87
|
+
for (const [key, entry] of recentInsertedBefore) {
|
|
88
|
+
if (now - entry.at > CORRELATION_WINDOW_MS) recentInsertedBefore.delete(key);
|
|
89
|
+
}
|
|
90
|
+
for (const [key, entry] of recentOldValues) {
|
|
91
|
+
if (now - entry.at > CORRELATION_WINDOW_MS) recentOldValues.delete(key);
|
|
92
|
+
}
|
|
93
|
+
for (const [key, entry] of recentCarrierAccumulated) {
|
|
94
|
+
if (now - entry.at > CORRELATION_WINDOW_MS) recentCarrierAccumulated.delete(key);
|
|
95
|
+
}
|
|
96
|
+
pendingOrphans = pendingOrphans.filter((orphan) => now - orphan.at <= CORRELATION_WINDOW_MS);
|
|
97
|
+
}
|
|
98
|
+
function clearCorrelationState() {
|
|
99
|
+
recentInsertedBefore.clear();
|
|
100
|
+
recentOldValues.clear();
|
|
101
|
+
recentCarrierAccumulated.clear();
|
|
102
|
+
pendingOrphans = [];
|
|
103
|
+
}
|
|
104
|
+
function looksLikeTranslatorReplacement(node) {
|
|
105
|
+
return node instanceof Text || node.nodeName === "FONT";
|
|
106
|
+
}
|
|
107
|
+
function replacementRunFor(removed, record) {
|
|
108
|
+
if (record.removedNodes.length === 1 && record.addedNodes.length === 1) {
|
|
109
|
+
const added = record.addedNodes[0];
|
|
110
|
+
return added ? [added] : [];
|
|
111
|
+
}
|
|
112
|
+
const entry = recentInsertedBefore.get(removed);
|
|
113
|
+
if (!entry) return [];
|
|
114
|
+
recentInsertedBefore.delete(removed);
|
|
115
|
+
return entry.nodes.filter(looksLikeTranslatorReplacement);
|
|
116
|
+
}
|
|
117
|
+
function mergeCarrierFor(removed, record, now) {
|
|
118
|
+
var _a, _b, _c, _d;
|
|
119
|
+
const carrier = record.previousSibling;
|
|
120
|
+
if (!carrier || !(carrier instanceof Text)) return null;
|
|
121
|
+
const carrierOriginalValue = (_a = recentOldValues.get(carrier)) == null ? void 0 : _a.value;
|
|
122
|
+
if (carrierOriginalValue === void 0) return null;
|
|
123
|
+
const accumulated = ((_c = (_b = recentCarrierAccumulated.get(carrier)) == null ? void 0 : _b.value) != null ? _c : "") + snapshotValue(removed);
|
|
124
|
+
if (!((_d = carrier.nodeValue) != null ? _d : "").startsWith(carrierOriginalValue + accumulated)) return null;
|
|
125
|
+
recentCarrierAccumulated.set(carrier, { at: now, value: accumulated });
|
|
126
|
+
return carrier;
|
|
127
|
+
}
|
|
128
|
+
function snapshotValue(node) {
|
|
129
|
+
var _a, _b, _c;
|
|
130
|
+
return (_c = (_b = (_a = recentOldValues.get(node)) == null ? void 0 : _a.value) != null ? _b : node.nodeValue) != null ? _c : "";
|
|
131
|
+
}
|
|
132
|
+
function handleDisplacedText(removed, record, now) {
|
|
133
|
+
var _a, _b, _c;
|
|
134
|
+
const run = replacementRunFor(removed, record);
|
|
135
|
+
if (run.length > 0) {
|
|
136
|
+
const group = {
|
|
137
|
+
parent: record.target,
|
|
138
|
+
originals: [{ node: removed, value: snapshotValue(removed) }, ...(_a = pendingCarrierOriginals.get(removed)) != null ? _a : []],
|
|
139
|
+
replacement: run,
|
|
140
|
+
previousSiblingHint: record.previousSibling,
|
|
141
|
+
nextSiblingHint: record.nextSibling
|
|
142
|
+
};
|
|
143
|
+
pendingCarrierOriginals.delete(removed);
|
|
144
|
+
registerGroup(group);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
const carrier = mergeCarrierFor(removed, record, now);
|
|
148
|
+
if (carrier) {
|
|
149
|
+
const carried = (_b = pendingCarrierOriginals.get(carrier)) != null ? _b : [];
|
|
150
|
+
carried.push({ node: removed, value: snapshotValue(removed) }, ...(_c = pendingCarrierOriginals.get(removed)) != null ? _c : []);
|
|
151
|
+
pendingCarrierOriginals.delete(removed);
|
|
152
|
+
pendingCarrierOriginals.set(carrier, carried);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
pendingOrphans.push({
|
|
156
|
+
at: now,
|
|
157
|
+
parent: record.target,
|
|
158
|
+
removed,
|
|
159
|
+
previousSibling: record.previousSibling,
|
|
160
|
+
nextSibling: record.nextSibling
|
|
161
|
+
});
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
function handleReplacementRemoved(group, removed, record) {
|
|
165
|
+
groupByReplacementNode.delete(removed);
|
|
166
|
+
const index = group.replacement.indexOf(removed);
|
|
167
|
+
const run = replacementRunFor(removed, record).filter((node) => {
|
|
168
|
+
return !group.originals.some((original) => original.node === node);
|
|
169
|
+
});
|
|
170
|
+
if (index >= 0) {
|
|
171
|
+
group.replacement.splice(index, 1, ...run);
|
|
172
|
+
} else {
|
|
173
|
+
group.replacement.push(...run);
|
|
174
|
+
}
|
|
175
|
+
for (const node of run) groupByReplacementNode.set(node, group);
|
|
176
|
+
if (group.replacement.length === 0 && group.originals.every((original) => original.node.parentNode !== null)) {
|
|
177
|
+
unregisterGroup(group);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (group.replacement.length === 0) {
|
|
181
|
+
group.previousSiblingHint = record.previousSibling;
|
|
182
|
+
group.nextSiblingHint = record.nextSibling;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function flushPendingOrphans() {
|
|
186
|
+
var _a;
|
|
187
|
+
for (const orphan of pendingOrphans) {
|
|
188
|
+
if (displaced.has(orphan.removed)) continue;
|
|
189
|
+
const group = {
|
|
190
|
+
parent: orphan.parent,
|
|
191
|
+
originals: [
|
|
192
|
+
{ node: orphan.removed, value: snapshotValue(orphan.removed) },
|
|
193
|
+
...(_a = pendingCarrierOriginals.get(orphan.removed)) != null ? _a : []
|
|
194
|
+
],
|
|
195
|
+
replacement: [],
|
|
196
|
+
previousSiblingHint: orphan.previousSibling,
|
|
197
|
+
nextSiblingHint: orphan.nextSibling
|
|
198
|
+
};
|
|
199
|
+
pendingCarrierOriginals.delete(orphan.removed);
|
|
200
|
+
registerGroup(group);
|
|
201
|
+
}
|
|
202
|
+
pendingOrphans = [];
|
|
203
|
+
}
|
|
204
|
+
function processRecords(records) {
|
|
205
|
+
var _a;
|
|
206
|
+
if (records.length === 0) return;
|
|
207
|
+
const now = performance.now();
|
|
208
|
+
purgeExpired(now);
|
|
209
|
+
let sawTranslatorActivity = false;
|
|
210
|
+
for (const record of records) {
|
|
211
|
+
if (record.type === "childList") {
|
|
212
|
+
for (const added of record.addedNodes) {
|
|
213
|
+
if (added.nodeName === "FONT") sawTranslatorActivity = true;
|
|
214
|
+
if (record.nextSibling) {
|
|
215
|
+
const entry = (_a = recentInsertedBefore.get(record.nextSibling)) != null ? _a : { at: now, nodes: [] };
|
|
216
|
+
entry.at = now;
|
|
217
|
+
entry.nodes.push(added);
|
|
218
|
+
recentInsertedBefore.set(record.nextSibling, entry);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} else if (record.type === "characterData" && record.oldValue !== null && !recentOldValues.has(record.target)) {
|
|
222
|
+
recentOldValues.set(record.target, { at: now, value: record.oldValue });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const record of records) {
|
|
226
|
+
if (record.type !== "childList") continue;
|
|
227
|
+
for (const removed of record.removedNodes) {
|
|
228
|
+
const group = groupByReplacementNode.get(removed);
|
|
229
|
+
if (group) {
|
|
230
|
+
handleReplacementRemoved(group, removed, record);
|
|
231
|
+
} else if (removed instanceof Text && !displaced.has(removed)) {
|
|
232
|
+
if (handleDisplacedText(removed, record, now)) sawTranslatorActivity = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (sawTranslatorActivity) flushPendingOrphans();
|
|
237
|
+
}
|
|
238
|
+
function drainPendingRecords() {
|
|
239
|
+
if (observer) {
|
|
240
|
+
const records = observer.takeRecords();
|
|
241
|
+
guarded("record processing", () => processRecords(records), void 0);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function setTextValue(node, value) {
|
|
245
|
+
domNatives().setData(node, value);
|
|
246
|
+
}
|
|
247
|
+
function restoreGroup(group, skipValueFor) {
|
|
248
|
+
var _a, _b, _c;
|
|
249
|
+
const natives = domNatives();
|
|
250
|
+
unregisterGroup(group);
|
|
251
|
+
for (const original of group.originals) {
|
|
252
|
+
recentOldValues.delete(original.node);
|
|
253
|
+
recentCarrierAccumulated.delete(original.node);
|
|
254
|
+
pendingCarrierOriginals.delete(original.node);
|
|
255
|
+
}
|
|
256
|
+
const attached = group.replacement.filter((node) => node.parentNode === group.parent);
|
|
257
|
+
let cursor;
|
|
258
|
+
if (attached.length > 0) {
|
|
259
|
+
cursor = (_a = attached[0]) != null ? _a : null;
|
|
260
|
+
} else if (((_b = group.previousSiblingHint) == null ? void 0 : _b.parentNode) === group.parent) {
|
|
261
|
+
cursor = group.previousSiblingHint.nextSibling;
|
|
262
|
+
} else if (((_c = group.nextSiblingHint) == null ? void 0 : _c.parentNode) === group.parent) {
|
|
263
|
+
cursor = group.nextSiblingHint;
|
|
264
|
+
} else if (group.replacement.length === 0 && group.parent.isConnected) {
|
|
265
|
+
cursor = null;
|
|
266
|
+
} else {
|
|
267
|
+
return "gone";
|
|
268
|
+
}
|
|
269
|
+
for (const original of group.originals) {
|
|
270
|
+
if (original.node !== skipValueFor) setTextValue(original.node, original.value);
|
|
271
|
+
if (original.node === cursor) {
|
|
272
|
+
cursor = original.node.nextSibling;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (original.node.parentNode !== null && original.node.parentNode !== group.parent) continue;
|
|
276
|
+
natives.insertBefore.call(group.parent, original.node, cursor);
|
|
277
|
+
}
|
|
278
|
+
for (const node of attached) {
|
|
279
|
+
if (node.parentNode === group.parent && !group.originals.some((original) => original.node === node)) {
|
|
280
|
+
natives.removeChild.call(group.parent, node);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return "restored";
|
|
284
|
+
}
|
|
285
|
+
function restoreDisplaced(node, skipValue = false) {
|
|
286
|
+
drainPendingRecords();
|
|
287
|
+
const group = displaced.get(node);
|
|
288
|
+
if (!group) return "untracked";
|
|
289
|
+
return restoreGroup(group, skipValue ? node : void 0);
|
|
290
|
+
}
|
|
291
|
+
var uninstallCurrent = null;
|
|
292
|
+
function installTranslationResilience(options = {}) {
|
|
293
|
+
var _a, _b;
|
|
294
|
+
if (uninstallCurrent) return uninstallCurrent;
|
|
295
|
+
const natives = domNatives();
|
|
296
|
+
const doc = (_a = options.document) != null ? _a : document;
|
|
297
|
+
emitEvent = (_b = options.onEvent) != null ? _b : noopEvent;
|
|
298
|
+
observer = new MutationObserver((records) => {
|
|
299
|
+
guarded("record processing", () => processRecords(records), void 0);
|
|
300
|
+
});
|
|
301
|
+
observer.observe(doc, { childList: true, subtree: true, characterData: true, characterDataOldValue: true });
|
|
302
|
+
Node.prototype.removeChild = function removeChild(child) {
|
|
303
|
+
if (child.parentNode !== this) {
|
|
304
|
+
const outcome = guarded(
|
|
305
|
+
"removeChild repair",
|
|
306
|
+
() => {
|
|
307
|
+
const result = child instanceof Text ? restoreDisplaced(child) : "untracked";
|
|
308
|
+
if (result === "gone") {
|
|
309
|
+
emitEvent("removeChild: displaced text already gone, removal skipped");
|
|
310
|
+
return "handled";
|
|
311
|
+
}
|
|
312
|
+
if (result === "untracked" && translationDetected) {
|
|
313
|
+
emitEvent("removeChild: removing node from its actual parent");
|
|
314
|
+
if (child.parentNode) natives.removeChild.call(child.parentNode, child);
|
|
315
|
+
return "handled";
|
|
316
|
+
}
|
|
317
|
+
return "native";
|
|
318
|
+
},
|
|
319
|
+
"native"
|
|
320
|
+
);
|
|
321
|
+
if (outcome === "handled") return child;
|
|
322
|
+
}
|
|
323
|
+
natives.removeChild.call(this, child);
|
|
324
|
+
return child;
|
|
325
|
+
};
|
|
326
|
+
Node.prototype.insertBefore = function insertBefore(node, child) {
|
|
327
|
+
if (node instanceof Text && node.parentNode === null) {
|
|
328
|
+
guarded("insertBefore node repair", () => restoreDisplaced(node), "untracked");
|
|
329
|
+
}
|
|
330
|
+
if (child && child.parentNode !== this) {
|
|
331
|
+
const outcome = guarded(
|
|
332
|
+
"insertBefore reference repair",
|
|
333
|
+
() => {
|
|
334
|
+
const result = child instanceof Text ? restoreDisplaced(child) : "untracked";
|
|
335
|
+
if (result !== "restored" && translationDetected) {
|
|
336
|
+
emitEvent("insertBefore: reference gone, appending instead");
|
|
337
|
+
natives.appendChild.call(this, node);
|
|
338
|
+
return "handled";
|
|
339
|
+
}
|
|
340
|
+
return "native";
|
|
341
|
+
},
|
|
342
|
+
"native"
|
|
343
|
+
);
|
|
344
|
+
if (outcome === "handled") return node;
|
|
345
|
+
}
|
|
346
|
+
natives.insertBefore.call(this, node, child);
|
|
347
|
+
return node;
|
|
348
|
+
};
|
|
349
|
+
Node.prototype.appendChild = function appendChild(node) {
|
|
350
|
+
if (node instanceof Text && node.parentNode === null) {
|
|
351
|
+
guarded("appendChild repair", () => restoreDisplaced(node), "untracked");
|
|
352
|
+
}
|
|
353
|
+
natives.appendChild.call(this, node);
|
|
354
|
+
return node;
|
|
355
|
+
};
|
|
356
|
+
const restoreAfterWrite = (node) => {
|
|
357
|
+
if (node instanceof Text && node.parentNode === null) {
|
|
358
|
+
guarded("text write repair", () => restoreDisplaced(node, true), "untracked");
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
Object.defineProperty(Node.prototype, "nodeValue", {
|
|
362
|
+
configurable: true,
|
|
363
|
+
enumerable: natives.nodeValueDescriptor.enumerable,
|
|
364
|
+
get: natives.nodeValueDescriptor.get,
|
|
365
|
+
set(value) {
|
|
366
|
+
natives.setNodeValue(this, value);
|
|
367
|
+
restoreAfterWrite(this);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
Object.defineProperty(CharacterData.prototype, "data", {
|
|
371
|
+
configurable: true,
|
|
372
|
+
enumerable: natives.dataDescriptor.enumerable,
|
|
373
|
+
get: natives.dataDescriptor.get,
|
|
374
|
+
set(value) {
|
|
375
|
+
natives.setData(this, value);
|
|
376
|
+
restoreAfterWrite(this);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
uninstallCurrent = () => {
|
|
380
|
+
observer == null ? void 0 : observer.disconnect();
|
|
381
|
+
observer = null;
|
|
382
|
+
translationDetected = false;
|
|
383
|
+
emitEvent = noopEvent;
|
|
384
|
+
clearCorrelationState();
|
|
385
|
+
Node.prototype.removeChild = natives.removeChild;
|
|
386
|
+
Node.prototype.insertBefore = natives.insertBefore;
|
|
387
|
+
Node.prototype.appendChild = natives.appendChild;
|
|
388
|
+
Object.defineProperty(Node.prototype, "nodeValue", natives.nodeValueDescriptor);
|
|
389
|
+
Object.defineProperty(CharacterData.prototype, "data", natives.dataDescriptor);
|
|
390
|
+
uninstallCurrent = null;
|
|
391
|
+
};
|
|
392
|
+
return uninstallCurrent;
|
|
393
|
+
}
|
|
394
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
395
|
+
0 && (module.exports = {
|
|
396
|
+
installTranslationResilience
|
|
397
|
+
});
|
|
398
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/resilience.ts"],"sourcesContent":["export type { TranslationResilienceOptions } from './resilience';\nexport { installTranslationResilience } from './resilience';\n","/**\n * Makes React (and any other text-node-owning renderer) resilient to browser\n * page translation (Chrome / Google Translate), which merges and replaces\n * Text nodes with `<font>` wrappers. React keeps references to the original,\n * now detached, Text nodes, so without this shim:\n *\n * - unmounting translated conditional text throws NotFoundError (removeChild)\n * - mounting content before translated text throws NotFoundError (insertBefore)\n * - updating translated text writes to the detached node — the visible page\n * silently never updates again (see facebook/react#11538)\n *\n * Strategy — \"re-adoption\", not error swallowing:\n *\n * 1. A document-wide MutationObserver watches mutations. Translation displaces\n * text nodes in a recognizable pattern: adjacent Text nodes are merged\n * (`normalize()`), then the merged run is replaced by wrapper elements\n * inserted next to it in the same task. We track a DISPLACEMENT GROUP per\n * replaced run: the ordered renderer-owned originals (with their\n * pre-translation values) and the run of replacement nodes standing in for\n * them. React's own commits never look like this: React processes\n * deletions before placements, so a node it removes is never adjacent to a\n * same-batch insertion at removal time, and React never merges text nodes.\n *\n * 2. Patched Node.prototype.removeChild / insertBefore / appendChild and the\n * nodeValue / data setters detect operations on displaced Text nodes and\n * first RESTORE the group — original nodes go back into the replacement\n * run's position, replacements are removed — then let the native operation\n * proceed. This repairs the renderer's ownership invariant: updates become\n * visible, removals remove the right content, and the translator\n * re-translates the freshly restored text via its own observer.\n *\n * 3. Translation can also delete text nodes outright and move inline elements\n * (word-order changes — Chromium bug 872770). Deletions within a\n * translation batch get an empty group with position hints so the node can\n * come back if React updates it. Once translation activity has been\n * detected, unrecoverable parent mismatches degrade to guarded best-effort\n * operations instead of throwing.\n *\n * Before any translation activity is detected, operations on untracked nodes\n * behave exactly as before — including throwing on genuine bugs.\n */\n\ninterface DisplacedOriginal {\n node: Text;\n /** The node's value before translation touched it (normalize mutates the merge target). */\n value: string;\n}\n\ninterface DisplacementGroup {\n parent: Node;\n /** Renderer-owned text nodes this group stands in for, in document order. */\n originals: DisplacedOriginal[];\n /** Nodes currently displaying the originals' content (empty if translation deleted the run). */\n replacement: Node[];\n /** Position hints captured at removal time, used when `replacement` is empty. */\n previousSiblingHint: Node | null;\n nextSiblingHint: Node | null;\n}\n\nconst displaced = new WeakMap<Text, DisplacementGroup>();\nconst groupByReplacementNode = new WeakMap<Node, DisplacementGroup>();\n/** Live merge targets (normalize) carrying content of already-detached originals. */\nconst pendingCarrierOriginals = new WeakMap<Text, DisplacedOriginal[]>();\n\nlet observer: MutationObserver | null = null;\nlet translationDetected = false;\nconst noopEvent = (_message: string): void => undefined;\nlet emitEvent: (message: string) => void = noopEvent;\n\ninterface DomNatives {\n insertBefore: typeof Node.prototype.insertBefore;\n removeChild: typeof Node.prototype.removeChild;\n appendChild: typeof Node.prototype.appendChild;\n nodeValueDescriptor: PropertyDescriptor;\n dataDescriptor: PropertyDescriptor;\n setNodeValue(node: Node, value: string | null): void;\n setData(node: CharacterData, value: string): void;\n}\n\nlet capturedNatives: DomNatives | null = null;\n\n/**\n * Native DOM entry points, captured once on first use rather than at module\n * load so that importing this module is safe in non-DOM environments (SSR);\n * only calling install requires a browser.\n */\nfunction domNatives(): DomNatives {\n if (capturedNatives) return capturedNatives;\n if (typeof Node === 'undefined' || typeof CharacterData === 'undefined') {\n throw new Error(\n 'translation-resilience requires a DOM. Call installTranslationResilience() from client-side code only.'\n );\n }\n const nodeValueDescriptor = Object.getOwnPropertyDescriptor(Node.prototype, 'nodeValue');\n const dataDescriptor = Object.getOwnPropertyDescriptor(CharacterData.prototype, 'data');\n const nodeValueSet = nodeValueDescriptor?.set;\n const dataSet = dataDescriptor?.set;\n if (!nodeValueDescriptor?.get || !nodeValueSet || !dataDescriptor?.get || !dataSet) {\n throw new Error('translation-resilience: expected accessor descriptors for nodeValue and data');\n }\n capturedNatives = {\n insertBefore: Node.prototype.insertBefore,\n removeChild: Node.prototype.removeChild,\n appendChild: Node.prototype.appendChild,\n nodeValueDescriptor,\n dataDescriptor,\n setNodeValue: (node, value) => nodeValueSet.call(node, value),\n setData: (node, value) => dataSet.call(node, value),\n };\n return capturedNatives;\n}\n\n/**\n * A fault in the shim itself must never make things worse than stock\n * behavior: every non-native code path runs through this guard, and on an\n * internal error the caller falls back to the native operation.\n */\nfunction guarded<T>(operation: string, run: () => T, fallback: T): T {\n try {\n return run();\n } catch (error) {\n emitEvent(`internal error in ${operation}: ${error instanceof Error ? error.message : String(error)}`);\n return fallback;\n }\n}\n\nfunction registerGroup(group: DisplacementGroup): void {\n if (!translationDetected) {\n translationDetected = true;\n emitEvent('translation activity detected');\n }\n for (const original of group.originals) displaced.set(original.node, group);\n for (const node of group.replacement) groupByReplacementNode.set(node, group);\n}\n\nfunction unregisterGroup(group: DisplacementGroup): void {\n for (const original of group.originals) displaced.delete(original.node);\n for (const node of group.replacement) groupByReplacementNode.delete(node);\n}\n\n/**\n * Correlation state must survive batch boundaries: the record stream can be\n * split at arbitrary points — the shim's own synchronous drains inside\n * patched DOM methods split a translator's mutation sequence across several\n * processRecords calls, and translators themselves spread work across tasks.\n * Entries are consumed on use and expire after a short wall-clock window so\n * that old insertions are never attributed to unrelated removals later.\n */\nconst CORRELATION_WINDOW_MS = 100;\n\ninterface TimedRun {\n at: number;\n nodes: Node[];\n}\ninterface TimedValue {\n at: number;\n value: string;\n}\ninterface PendingOrphan {\n at: number;\n parent: Node;\n removed: Text;\n previousSibling: Node | null;\n nextSibling: Node | null;\n}\n\n/** Nodes recently inserted, indexed by their at-insertion-time nextSibling. */\nconst recentInsertedBefore = new Map<Node, TimedRun>();\n/** First recently-seen characterData oldValue per node = value before the mutation sequence. */\nconst recentOldValues = new Map<Node, TimedValue>();\n/** Accumulated merged-away content per normalize target, for validation. */\nconst recentCarrierAccumulated = new Map<Text, TimedValue>();\n/** Text removals with no replacement — become deletion groups once translator activity is confirmed. */\nlet pendingOrphans: PendingOrphan[] = [];\n\nfunction purgeExpired(now: number): void {\n for (const [key, entry] of recentInsertedBefore) {\n if (now - entry.at > CORRELATION_WINDOW_MS) recentInsertedBefore.delete(key);\n }\n for (const [key, entry] of recentOldValues) {\n if (now - entry.at > CORRELATION_WINDOW_MS) recentOldValues.delete(key);\n }\n for (const [key, entry] of recentCarrierAccumulated) {\n if (now - entry.at > CORRELATION_WINDOW_MS) recentCarrierAccumulated.delete(key);\n }\n pendingOrphans = pendingOrphans.filter((orphan) => now - orphan.at <= CORRELATION_WINDOW_MS);\n}\n\nfunction clearCorrelationState(): void {\n recentInsertedBefore.clear();\n recentOldValues.clear();\n recentCarrierAccumulated.clear();\n pendingOrphans = [];\n}\n\n/** Replacement nodes a translator produces: <font> wrappers or plain text (revert). */\nfunction looksLikeTranslatorReplacement(node: Node): boolean {\n return node instanceof Text || node.nodeName === 'FONT';\n}\n\n/**\n * The run of nodes that took a removed node's place. A replaceChild-style\n * swap queues a single record carrying both sides — correlate those directly.\n * Translation instead inserts the wrapper(s) directly before the original and\n * then removes it as separate operations, so each wrapper's insertion record\n * has the original as its at-insertion-time nextSibling. Sibling pointers are\n * NOT walked at processing time — later mutations may already have\n * invalidated them.\n */\nfunction replacementRunFor(removed: Node, record: MutationRecord): Node[] {\n if (record.removedNodes.length === 1 && record.addedNodes.length === 1) {\n const added = record.addedNodes[0];\n return added ? [added] : [];\n }\n const entry = recentInsertedBefore.get(removed);\n if (!entry) return [];\n recentInsertedBefore.delete(removed);\n return entry.nodes.filter(looksLikeTranslatorReplacement);\n}\n\n/**\n * Detects a normalize() merge: the removed text's content was appended onto\n * the preceding Text sibling. The carrier must have a recent characterData\n * record (a renderer removing a node never mutates its neighbor) and its\n * content must be consistent with concatenation. Pre-mutation values are\n * used throughout — the caller (e.g. React) may already have written a new\n * value into a node before these records were processed.\n */\nfunction mergeCarrierFor(removed: Text, record: MutationRecord, now: number): Text | null {\n const carrier = record.previousSibling;\n if (!carrier || !(carrier instanceof Text)) return null;\n const carrierOriginalValue = recentOldValues.get(carrier)?.value;\n if (carrierOriginalValue === undefined) return null;\n const accumulated = (recentCarrierAccumulated.get(carrier)?.value ?? '') + snapshotValue(removed);\n if (!(carrier.nodeValue ?? '').startsWith(carrierOriginalValue + accumulated)) return null;\n recentCarrierAccumulated.set(carrier, { at: now, value: accumulated });\n return carrier;\n}\n\nfunction snapshotValue(node: Text): string {\n return recentOldValues.get(node)?.value ?? node.nodeValue ?? '';\n}\n\n/** Returns true if a displacement group was registered. */\nfunction handleDisplacedText(removed: Text, record: MutationRecord, now: number): boolean {\n const run = replacementRunFor(removed, record);\n if (run.length > 0) {\n const group: DisplacementGroup = {\n parent: record.target,\n originals: [{ node: removed, value: snapshotValue(removed) }, ...(pendingCarrierOriginals.get(removed) ?? [])],\n replacement: run,\n previousSiblingHint: record.previousSibling,\n nextSiblingHint: record.nextSibling,\n };\n pendingCarrierOriginals.delete(removed);\n registerGroup(group);\n return true;\n }\n\n const carrier = mergeCarrierFor(removed, record, now);\n if (carrier) {\n const carried = pendingCarrierOriginals.get(carrier) ?? [];\n carried.push({ node: removed, value: snapshotValue(removed) }, ...(pendingCarrierOriginals.get(removed) ?? []));\n pendingCarrierOriginals.delete(removed);\n pendingCarrierOriginals.set(carrier, carried);\n return false;\n }\n\n pendingOrphans.push({\n at: now,\n parent: record.target,\n removed,\n previousSibling: record.previousSibling,\n nextSibling: record.nextSibling,\n });\n return false;\n}\n\nfunction handleReplacementRemoved(group: DisplacementGroup, removed: Node, record: MutationRecord): void {\n groupByReplacementNode.delete(removed);\n const index = group.replacement.indexOf(removed);\n const run = replacementRunFor(removed, record).filter((node) => {\n // The translator may revert by re-inserting an original itself; an\n // original never doubles as its own group's replacement.\n return !group.originals.some((original) => original.node === node);\n });\n if (index >= 0) {\n group.replacement.splice(index, 1, ...run);\n } else {\n group.replacement.push(...run);\n }\n for (const node of run) groupByReplacementNode.set(node, group);\n\n // Full revert: every original is back in the document — the group is moot.\n if (group.replacement.length === 0 && group.originals.every((original) => original.node.parentNode !== null)) {\n unregisterGroup(group);\n return;\n }\n if (group.replacement.length === 0) {\n group.previousSiblingHint = record.previousSibling;\n group.nextSiblingHint = record.nextSibling;\n }\n}\n\n/**\n * Text removals with no replacement, seen around confirmed translator\n * activity, are translation deletions (word-order changes drop text nodes —\n * Chromium bug 872770). Track them so React can bring the text back.\n */\nfunction flushPendingOrphans(): void {\n for (const orphan of pendingOrphans) {\n if (displaced.has(orphan.removed)) continue;\n const group: DisplacementGroup = {\n parent: orphan.parent,\n originals: [\n { node: orphan.removed, value: snapshotValue(orphan.removed) },\n ...(pendingCarrierOriginals.get(orphan.removed) ?? []),\n ],\n replacement: [],\n previousSiblingHint: orphan.previousSibling,\n nextSiblingHint: orphan.nextSibling,\n };\n pendingCarrierOriginals.delete(orphan.removed);\n registerGroup(group);\n }\n pendingOrphans = [];\n}\n\nfunction processRecords(records: MutationRecord[]): void {\n if (records.length === 0) return;\n const now = performance.now();\n purgeExpired(now);\n\n let sawTranslatorActivity = false;\n for (const record of records) {\n if (record.type === 'childList') {\n for (const added of record.addedNodes) {\n if (added.nodeName === 'FONT') sawTranslatorActivity = true;\n if (record.nextSibling) {\n const entry = recentInsertedBefore.get(record.nextSibling) ?? { at: now, nodes: [] };\n entry.at = now;\n entry.nodes.push(added);\n recentInsertedBefore.set(record.nextSibling, entry);\n }\n }\n } else if (record.type === 'characterData' && record.oldValue !== null && !recentOldValues.has(record.target)) {\n recentOldValues.set(record.target, { at: now, value: record.oldValue });\n }\n }\n\n for (const record of records) {\n if (record.type !== 'childList') continue;\n for (const removed of record.removedNodes) {\n const group = groupByReplacementNode.get(removed);\n if (group) {\n handleReplacementRemoved(group, removed, record);\n } else if (removed instanceof Text && !displaced.has(removed)) {\n if (handleDisplacedText(removed, record, now)) sawTranslatorActivity = true;\n }\n }\n }\n\n if (sawTranslatorActivity) flushPendingOrphans();\n}\n\n/** Synchronously fold in records the observer hasn't delivered yet. */\nfunction drainPendingRecords(): void {\n if (observer) {\n const records = observer.takeRecords();\n guarded('record processing', () => processRecords(records), undefined);\n }\n}\n\ntype RestoreResult = 'restored' | 'gone' | 'untracked';\n\nfunction setTextValue(node: Text, value: string): void {\n domNatives().setData(node, value);\n}\n\n/**\n * Puts a displaced group's original text nodes back into the position its\n * replacement run occupies (removing the replacements), so the caller's\n * native DOM operation can proceed on a consistent tree.\n */\nfunction restoreGroup(group: DisplacementGroup, skipValueFor?: Text): RestoreResult {\n const natives = domNatives();\n unregisterGroup(group);\n for (const original of group.originals) {\n // Re-adoption makes the node's DOM state authoritative again; correlation\n // entries recorded while it was displaced would poison future sequences.\n recentOldValues.delete(original.node);\n recentCarrierAccumulated.delete(original.node);\n pendingCarrierOriginals.delete(original.node);\n }\n\n const attached = group.replacement.filter((node) => node.parentNode === group.parent);\n let cursor: Node | null;\n if (attached.length > 0) {\n cursor = attached[0] ?? null;\n } else if (group.previousSiblingHint?.parentNode === group.parent) {\n cursor = group.previousSiblingHint.nextSibling;\n } else if (group.nextSiblingHint?.parentNode === group.parent) {\n cursor = group.nextSiblingHint;\n } else if (group.replacement.length === 0 && group.parent.isConnected) {\n cursor = null; // deleted run with dead hints: append to the parent\n } else {\n return 'gone';\n }\n\n for (const original of group.originals) {\n if (original.node !== skipValueFor) setTextValue(original.node, original.value);\n if (original.node === cursor) {\n cursor = original.node.nextSibling;\n continue;\n }\n if (original.node.parentNode !== null && original.node.parentNode !== group.parent) continue;\n natives.insertBefore.call(group.parent, original.node, cursor);\n }\n for (const node of attached) {\n if (node.parentNode === group.parent && !group.originals.some((original) => original.node === node)) {\n natives.removeChild.call(group.parent, node);\n }\n }\n return 'restored';\n}\n\nfunction restoreDisplaced(node: Text, skipValue = false): RestoreResult {\n drainPendingRecords();\n const group = displaced.get(node);\n if (!group) return 'untracked';\n return restoreGroup(group, skipValue ? node : undefined);\n}\n\nlet uninstallCurrent: (() => void) | null = null;\n\nexport interface TranslationResilienceOptions {\n document?: Document;\n /** Observability hook: called with a short message on every non-native path taken. */\n onEvent?: (message: string) => void;\n}\n\nexport function installTranslationResilience(options: TranslationResilienceOptions = {}): () => void {\n if (uninstallCurrent) return uninstallCurrent;\n const natives = domNatives();\n const doc = options.document ?? document;\n emitEvent = options.onEvent ?? noopEvent;\n\n observer = new MutationObserver((records) => {\n guarded('record processing', () => processRecords(records), undefined);\n });\n observer.observe(doc, { childList: true, subtree: true, characterData: true, characterDataOldValue: true });\n\n Node.prototype.removeChild = function removeChild<T extends Node>(this: Node, child: T): T {\n if (child.parentNode !== this) {\n const outcome = guarded(\n 'removeChild repair',\n (): 'native' | 'handled' => {\n const result = child instanceof Text ? restoreDisplaced(child) : 'untracked';\n if (result === 'gone') {\n // The displaced content is already absent, which is what this removal wanted.\n emitEvent('removeChild: displaced text already gone, removal skipped');\n return 'handled';\n }\n if (result === 'untracked' && translationDetected) {\n // Translation moved this node somewhere we could not track (e.g. a\n // word-order change). Remove it from wherever it actually is.\n emitEvent('removeChild: removing node from its actual parent');\n if (child.parentNode) natives.removeChild.call(child.parentNode, child);\n return 'handled';\n }\n return 'native';\n },\n 'native'\n );\n if (outcome === 'handled') return child;\n }\n natives.removeChild.call(this, child);\n return child;\n };\n\n Node.prototype.insertBefore = function insertBefore<T extends Node>(this: Node, node: T, child: Node | null): T {\n if (node instanceof Text && node.parentNode === null) {\n guarded('insertBefore node repair', () => restoreDisplaced(node), 'untracked');\n }\n if (child && child.parentNode !== this) {\n const outcome = guarded(\n 'insertBefore reference repair',\n (): 'native' | 'handled' => {\n const result = child instanceof Text ? restoreDisplaced(child) : 'untracked';\n if (result !== 'restored' && translationDetected) {\n // The reference node is unrecoverable; appending keeps the new node\n // in the right parent, which is the best position still guaranteed.\n emitEvent('insertBefore: reference gone, appending instead');\n natives.appendChild.call(this, node);\n return 'handled';\n }\n return 'native';\n },\n 'native'\n );\n if (outcome === 'handled') return node;\n }\n natives.insertBefore.call(this, node, child);\n return node;\n };\n\n Node.prototype.appendChild = function appendChild<T extends Node>(this: Node, node: T): T {\n if (node instanceof Text && node.parentNode === null) {\n guarded('appendChild repair', () => restoreDisplaced(node), 'untracked');\n }\n natives.appendChild.call(this, node);\n return node;\n };\n\n const restoreAfterWrite = (node: Node): void => {\n if (node instanceof Text && node.parentNode === null) {\n guarded('text write repair', () => restoreDisplaced(node, true), 'untracked');\n }\n };\n\n Object.defineProperty(Node.prototype, 'nodeValue', {\n configurable: true,\n enumerable: natives.nodeValueDescriptor.enumerable,\n get: natives.nodeValueDescriptor.get,\n set(this: Node, value: string | null) {\n natives.setNodeValue(this, value);\n restoreAfterWrite(this);\n },\n });\n\n Object.defineProperty(CharacterData.prototype, 'data', {\n configurable: true,\n enumerable: natives.dataDescriptor.enumerable,\n get: natives.dataDescriptor.get,\n set(this: CharacterData, value: string) {\n natives.setData(this, value);\n restoreAfterWrite(this);\n },\n });\n\n uninstallCurrent = () => {\n observer?.disconnect();\n observer = null;\n translationDetected = false;\n emitEvent = noopEvent;\n clearCorrelationState();\n Node.prototype.removeChild = natives.removeChild;\n Node.prototype.insertBefore = natives.insertBefore;\n Node.prototype.appendChild = natives.appendChild;\n Object.defineProperty(Node.prototype, 'nodeValue', natives.nodeValueDescriptor);\n Object.defineProperty(CharacterData.prototype, 'data', natives.dataDescriptor);\n uninstallCurrent = null;\n };\n return uninstallCurrent;\n}\n"],"mappings":"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","names":[]}
|