translation-resilience 0.1.2 → 0.2.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/README.md +20 -10
- package/dist/index.cjs +47 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +47 -4
- package/dist/index.js.map +1 -1
- package/dist/simulator.cjs +8 -0
- package/dist/simulator.cjs.map +1 -1
- package/dist/simulator.d.cts +4 -0
- package/dist/simulator.d.ts +4 -0
- package/dist/simulator.js +8 -0
- package/dist/simulator.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ import { installTranslationResilience } from 'translation-resilience';
|
|
|
8
8
|
installTranslationResilience();
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Framework-agnostic (it patches the DOM layer, not React), dependency-free, ~2.
|
|
11
|
+
Framework-agnostic (it patches the DOM layer, not React), dependency-free, ~2.6 kB min+gzip, and lazily activated — near-zero cost until a translator actually touches the page.
|
|
12
12
|
|
|
13
13
|
## The problem
|
|
14
14
|
|
|
@@ -65,11 +65,18 @@ installTranslationResilience({
|
|
|
65
65
|
document,
|
|
66
66
|
|
|
67
67
|
// Observability hook: called with a short message on every non-native
|
|
68
|
-
// path taken, e.g. 'translation
|
|
68
|
+
// path taken, e.g. 'translation signal detected, observing document',
|
|
69
|
+
// 'translation activity detected',
|
|
69
70
|
// 'removeChild: displaced text already gone, removal skipped'.
|
|
70
71
|
onEvent: (message) => {
|
|
71
72
|
Bugsnag.leaveBreadcrumb('translation-resilience', { message }, 'log');
|
|
72
73
|
},
|
|
74
|
+
|
|
75
|
+
// Install the document-wide observer immediately instead of waiting for
|
|
76
|
+
// the translation signal on <html> (see "Performance cost"). Costs more
|
|
77
|
+
// on never-translated pages; covers hypothetical translators that
|
|
78
|
+
// displace text without marking the document first.
|
|
79
|
+
eager: false,
|
|
73
80
|
});
|
|
74
81
|
```
|
|
75
82
|
|
|
@@ -77,23 +84,24 @@ installTranslationResilience({
|
|
|
77
84
|
|
|
78
85
|
## Safety properties
|
|
79
86
|
|
|
80
|
-
- **Inert until translation happens.**
|
|
87
|
+
- **Inert until translation happens.** The document-wide observer isn't even created until the translator marks `<html>` (see "Performance cost"), and until 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
88
|
- **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
89
|
- **Reversible.** `installTranslationResilience()` returns an uninstall function that restores all prototypes and disconnects the observer. Calling install twice returns the same uninstall (idempotent).
|
|
83
90
|
- **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
91
|
|
|
85
92
|
## Performance cost
|
|
86
93
|
|
|
87
|
-
|
|
94
|
+
The shim is **lazily activated**. At install it patches the DOM methods and watches exactly one thing: the `lang` and `class` attributes of `<html>`. Chrome's translator announces itself there — it adds a `translated-ltr`/`translated-rtl` class and flips `lang` — a few hundred milliseconds *before* it touches any text (~275–500 ms measured against real Chrome). Only on that signal does the shim create the document-wide `MutationObserver` that does the real work. The class *value* is checked (`translated-…`), not merely "class changed", because browser extensions routinely add unrelated classes to `<html>`. Installing on an already-marked document activates immediately, and a synchronous fallback in the patched methods covers same-realm translators (like the bundled simulator) that signal and displace in the same task. A translator that displaces text without marking the document first would leave the shim dormant — stock browser behavior, never worse than not having it (`eager: true` trades the idle cost away to cover that hypothetical).
|
|
88
95
|
|
|
89
|
-
|
|
96
|
+
Measured numbers, with an honest caveat: these are microbenchmarks from one machine (Apple Silicon, headless Chrome, production React 18 build; a 500-row × 4-column table, medians of 7 runs). The shim has **not yet been benchmarked inside a large production app** — if you measure something different, please open an issue.
|
|
90
97
|
|
|
91
|
-
|
|
92
|
-
- **Per-operation costs are sub-microsecond.** Writes to an attached `text.data`: ~0.05 µs → ~0.3 µs each (setter indirection plus the observer allocating a characterData record). The worst case we could construct — a tight synchronous loop appending and removing individual detached text nodes, a pattern frameworks don't produce (they remove element subtrees as one operation) — costs ~1.8 µs per append+remove pair vs ~0.2 µs native.
|
|
98
|
+
**Before any translation signal (the overwhelmingly common case) the cost is near zero:**
|
|
93
99
|
|
|
94
|
-
|
|
100
|
+
- Re-rendering the table with three cells changing in every row, 50 commits: 26.9 ms → 29.1 ms (+8%, ≈ 40 µs per full-table commit). Repeated mount+unmount of the whole table: +3% (noise).
|
|
101
|
+
- Writes to an attached `text.data`: ~0.05 µs → ~0.08 µs each. The worst case we could construct — a tight synchronous loop appending and removing individual detached text nodes, a pattern frameworks don't produce — ~0.2 µs → ~0.26 µs per pair.
|
|
102
|
+
- What remains is the patched-method call indirection plus one attribute read per operation while dormant. There is no observer, so the browser allocates no mutation records.
|
|
95
103
|
|
|
96
|
-
|
|
104
|
+
**After translation starts**, the full machinery is live: one document-wide `MutationObserver` (childList + characterData + oldValue, subtree) means a record per DOM mutation, and inserting a *detached* text node drains and processes pending records. Correlation state expires after 100 ms and is purged incrementally (amortized O(1) per entry), so steady-state memory is effectively zero. On a translated page, operations on *untranslated* content cost roughly: ≈ 0.3 ms per pathological full-table commit, ~0.3 µs per text write, ~1.8 µs per worst-case churn op. Updates to *translated* text pay the restore → re-translate cycle: ≈ 14 µs per updated text run, so a commit updating 1,500 translated runs at once costs ≈ 20 ms extra — the alternative without the shim is those updates never becoming visible at all. Translated pages that are idle cost nothing beyond the observer.
|
|
97
105
|
|
|
98
106
|
### How React core could fix this (nearly for free)
|
|
99
107
|
|
|
@@ -104,7 +112,7 @@ Almost everything this package pays on the happy path is an *outsider tax*: the
|
|
|
104
112
|
- **Repair needs no archaeology.** This package tracks displacement groups through a correlation window because, from the outside, "which foreign nodes replaced mine, and what did mine say?" can only be answered by watching the mutations happen. React can instead rebuild the affected host element's children from fiber state — the moral equivalent of hydration-mismatch recovery — and let the translator re-translate the result. Same self-healing loop as this shim, none of the bookkeeping.
|
|
105
113
|
- **Nobody else pays.** The prototype patches here are page-global: every DOM user, React or not, pays the (small) toll. An in-core fix is scoped to React's own commit operations.
|
|
106
114
|
|
|
107
|
-
Against the numbers above: the
|
|
115
|
+
Against the numbers above: lazy activation already makes the never-translated case nearly free, but once translation is active the userland machinery has real costs that an in-core fix would not — and even the dormant-state residue (patched-call indirection, an attribute read per op) would drop to a branch-predicted pointer compare. A fix has been requested since 2017 ([facebook/react#11538](https://github.com/facebook/react/issues/11538)); the traditional objections — defensive checks don't belong in the commit hot path, and third-party DOM mutation is outside React's contract — are worth weighing against what userland has to do instead, which is this entire package. The same reasoning applies to any renderer that keeps references to the text nodes it creates (Vue, Ember, Svelte). We would be delighted for this package to be made obsolete.
|
|
108
116
|
|
|
109
117
|
## Testing your app against translation: the simulator
|
|
110
118
|
|
|
@@ -132,6 +140,8 @@ it('keeps counters updating on translated pages', async () => {
|
|
|
132
140
|
|
|
133
141
|
`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.
|
|
134
142
|
|
|
143
|
+
Like real Chrome, `translateSubtree` marks the document before touching any text — it adds `translated-ltr` and flips `lang` on `<html>` (that's what activates the lazily-installed shim). Remember to reset those attributes between tests if your assertions depend on them.
|
|
144
|
+
|
|
135
145
|
## Compatibility and limitations
|
|
136
146
|
|
|
137
147
|
- **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.
|
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,8 @@ var displaced = /* @__PURE__ */ new WeakMap();
|
|
|
29
29
|
var groupByReplacementNode = /* @__PURE__ */ new WeakMap();
|
|
30
30
|
var pendingCarrierOriginals = /* @__PURE__ */ new WeakMap();
|
|
31
31
|
var observer = null;
|
|
32
|
+
var sentinelObserver = null;
|
|
33
|
+
var sentinelActive = false;
|
|
32
34
|
var translationDetected = false;
|
|
33
35
|
var noopEvent = (_message) => void 0;
|
|
34
36
|
var emitEvent = noopEvent;
|
|
@@ -299,17 +301,51 @@ function restoreDisplaced(node, skipValue = false) {
|
|
|
299
301
|
return restoreGroup(group, skipValue ? node : void 0);
|
|
300
302
|
}
|
|
301
303
|
var uninstallCurrent = null;
|
|
304
|
+
function hasTranslatedClass(doc) {
|
|
305
|
+
return doc.documentElement.className.includes("translated-");
|
|
306
|
+
}
|
|
302
307
|
function installTranslationResilience(options = {}) {
|
|
303
308
|
var _a, _b;
|
|
304
309
|
if (uninstallCurrent) return uninstallCurrent;
|
|
305
310
|
const natives = domNatives();
|
|
306
311
|
const doc = (_a = options.document) != null ? _a : document;
|
|
307
312
|
emitEvent = (_b = options.onEvent) != null ? _b : noopEvent;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
313
|
+
const activateObserver = () => {
|
|
314
|
+
if (observer) return;
|
|
315
|
+
sentinelActive = false;
|
|
316
|
+
sentinelObserver == null ? void 0 : sentinelObserver.disconnect();
|
|
317
|
+
sentinelObserver = null;
|
|
318
|
+
observer = new MutationObserver((records) => {
|
|
319
|
+
guarded("record processing", () => processRecords(records), void 0);
|
|
320
|
+
});
|
|
321
|
+
observer.observe(doc, { childList: true, subtree: true, characterData: true, characterDataOldValue: true });
|
|
322
|
+
emitEvent("translation signal detected, observing document");
|
|
323
|
+
};
|
|
324
|
+
const sentinelSyncCheck = () => {
|
|
325
|
+
if (sentinelActive && hasTranslatedClass(doc)) activateObserver();
|
|
326
|
+
};
|
|
327
|
+
if (options.eager || hasTranslatedClass(doc)) {
|
|
328
|
+
activateObserver();
|
|
329
|
+
} else {
|
|
330
|
+
sentinelObserver = new MutationObserver((records) => {
|
|
331
|
+
guarded(
|
|
332
|
+
"sentinel processing",
|
|
333
|
+
() => {
|
|
334
|
+
for (const record of records) {
|
|
335
|
+
if (record.attributeName === "lang" || hasTranslatedClass(doc)) {
|
|
336
|
+
activateObserver();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
void 0
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
sentinelObserver.observe(doc.documentElement, { attributes: true, attributeFilter: ["lang", "class"] });
|
|
345
|
+
sentinelActive = true;
|
|
346
|
+
}
|
|
312
347
|
Node.prototype.removeChild = function removeChild(child) {
|
|
348
|
+
sentinelSyncCheck();
|
|
313
349
|
if (child.parentNode !== this) {
|
|
314
350
|
const outcome = guarded(
|
|
315
351
|
"removeChild repair",
|
|
@@ -334,6 +370,7 @@ function installTranslationResilience(options = {}) {
|
|
|
334
370
|
return child;
|
|
335
371
|
};
|
|
336
372
|
Node.prototype.insertBefore = function insertBefore(node, child) {
|
|
373
|
+
sentinelSyncCheck();
|
|
337
374
|
if (node instanceof Text && node.parentNode === null) {
|
|
338
375
|
guarded("insertBefore node repair", () => restoreDisplaced(node), "untracked");
|
|
339
376
|
}
|
|
@@ -357,6 +394,7 @@ function installTranslationResilience(options = {}) {
|
|
|
357
394
|
return node;
|
|
358
395
|
};
|
|
359
396
|
Node.prototype.appendChild = function appendChild(node) {
|
|
397
|
+
sentinelSyncCheck();
|
|
360
398
|
if (node instanceof Text && node.parentNode === null) {
|
|
361
399
|
guarded("appendChild repair", () => restoreDisplaced(node), "untracked");
|
|
362
400
|
}
|
|
@@ -373,6 +411,7 @@ function installTranslationResilience(options = {}) {
|
|
|
373
411
|
enumerable: natives.nodeValueDescriptor.enumerable,
|
|
374
412
|
get: natives.nodeValueDescriptor.get,
|
|
375
413
|
set(value) {
|
|
414
|
+
sentinelSyncCheck();
|
|
376
415
|
natives.setNodeValue(this, value);
|
|
377
416
|
restoreAfterWrite(this);
|
|
378
417
|
}
|
|
@@ -382,6 +421,7 @@ function installTranslationResilience(options = {}) {
|
|
|
382
421
|
enumerable: natives.dataDescriptor.enumerable,
|
|
383
422
|
get: natives.dataDescriptor.get,
|
|
384
423
|
set(value) {
|
|
424
|
+
sentinelSyncCheck();
|
|
385
425
|
natives.setData(this, value);
|
|
386
426
|
restoreAfterWrite(this);
|
|
387
427
|
}
|
|
@@ -389,6 +429,9 @@ function installTranslationResilience(options = {}) {
|
|
|
389
429
|
uninstallCurrent = () => {
|
|
390
430
|
observer == null ? void 0 : observer.disconnect();
|
|
391
431
|
observer = null;
|
|
432
|
+
sentinelObserver == null ? void 0 : sentinelObserver.disconnect();
|
|
433
|
+
sentinelObserver = null;
|
|
434
|
+
sentinelActive = false;
|
|
392
435
|
translationDetected = false;
|
|
393
436
|
emitEvent = noopEvent;
|
|
394
437
|
clearCorrelationState();
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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/**\n * Every correlation store below is kept in ascending-`at` order: new entries\n * are appended, and any refresh of an existing entry's timestamp must\n * delete-then-set so the entry moves to the end. purgeExpired relies on this\n * to drop only the expired prefix and stop at the first fresh entry —\n * processRecords runs on every synchronous drain (i.e. inside patched DOM\n * calls), so a full scan of the stores per drain is quadratic under heavy\n * synchronous DOM churn.\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) break;\n recentInsertedBefore.delete(key);\n }\n for (const [key, entry] of recentOldValues) {\n if (now - entry.at <= CORRELATION_WINDOW_MS) break;\n recentOldValues.delete(key);\n }\n for (const [key, entry] of recentCarrierAccumulated) {\n if (now - entry.at <= CORRELATION_WINDOW_MS) break;\n recentCarrierAccumulated.delete(key);\n }\n const firstFresh = pendingOrphans.findIndex((orphan) => now - orphan.at <= CORRELATION_WINDOW_MS);\n if (firstFresh === -1) {\n if (pendingOrphans.length > 0) pendingOrphans = [];\n } else if (firstFresh > 0) {\n pendingOrphans = pendingOrphans.slice(firstFresh);\n }\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 // delete-then-set keeps the store in ascending-`at` order (see purgeExpired)\n recentCarrierAccumulated.delete(carrier);\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 // delete-then-set keeps the store in ascending-`at` order (see purgeExpired)\n recentInsertedBefore.delete(record.nextSibling);\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":[]}
|
|
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 sentinelObserver: MutationObserver | null = null;\n/** True while installed but not yet observing — patched methods run a cheap synchronous signal check. */\nlet sentinelActive = false;\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/**\n * Every correlation store below is kept in ascending-`at` order: new entries\n * are appended, and any refresh of an existing entry's timestamp must\n * delete-then-set so the entry moves to the end. purgeExpired relies on this\n * to drop only the expired prefix and stop at the first fresh entry —\n * processRecords runs on every synchronous drain (i.e. inside patched DOM\n * calls), so a full scan of the stores per drain is quadratic under heavy\n * synchronous DOM churn.\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) break;\n recentInsertedBefore.delete(key);\n }\n for (const [key, entry] of recentOldValues) {\n if (now - entry.at <= CORRELATION_WINDOW_MS) break;\n recentOldValues.delete(key);\n }\n for (const [key, entry] of recentCarrierAccumulated) {\n if (now - entry.at <= CORRELATION_WINDOW_MS) break;\n recentCarrierAccumulated.delete(key);\n }\n const firstFresh = pendingOrphans.findIndex((orphan) => now - orphan.at <= CORRELATION_WINDOW_MS);\n if (firstFresh === -1) {\n if (pendingOrphans.length > 0) pendingOrphans = [];\n } else if (firstFresh > 0) {\n pendingOrphans = pendingOrphans.slice(firstFresh);\n }\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 // delete-then-set keeps the store in ascending-`at` order (see purgeExpired)\n recentCarrierAccumulated.delete(carrier);\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 // delete-then-set keeps the store in ascending-`at` order (see purgeExpired)\n recentInsertedBefore.delete(record.nextSibling);\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 * Install the document-wide observer immediately instead of waiting for a\n * translation signal on <html>. The lazy default costs nothing until\n * translation starts; eager also covers hypothetical translators that\n * displace text without marking the document first.\n */\n eager?: boolean;\n}\n\n/**\n * Chrome's translator marks the document before it displaces any text: it\n * adds a `translated-ltr`/`translated-rtl` class and flips `lang` on <html>,\n * measured ~275-500ms ahead of the first text mutation in real Chrome. The\n * class VALUE is checked (not just \"class changed\") because extensions add\n * unrelated classes to <html> on ordinary page loads.\n */\nfunction hasTranslatedClass(doc: Document): boolean {\n return doc.documentElement.className.includes('translated-');\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 const activateObserver = (): void => {\n if (observer) return;\n sentinelActive = false;\n sentinelObserver?.disconnect();\n sentinelObserver = null;\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 emitEvent('translation signal detected, observing document');\n };\n\n /**\n * Same-realm translators (the bundled simulator, or anything else calling\n * the patched DOM methods directly) can displace text in the same task that\n * sets the signal — before the sentinel's microtask callback runs. Real\n * Chrome translation runs in an isolated world with a large gap between\n * signal and displacement, so this synchronous fallback is only load\n * bearing for same-realm use: it reads one attribute while dormant and\n * costs a boolean check once active.\n */\n const sentinelSyncCheck = (): void => {\n if (sentinelActive && hasTranslatedClass(doc)) activateObserver();\n };\n\n if (options.eager || hasTranslatedClass(doc)) {\n activateObserver();\n } else {\n sentinelObserver = new MutationObserver((records) => {\n guarded(\n 'sentinel processing',\n () => {\n for (const record of records) {\n if (record.attributeName === 'lang' || hasTranslatedClass(doc)) {\n activateObserver();\n return;\n }\n }\n },\n undefined\n );\n });\n sentinelObserver.observe(doc.documentElement, { attributes: true, attributeFilter: ['lang', 'class'] });\n sentinelActive = true;\n }\n\n Node.prototype.removeChild = function removeChild<T extends Node>(this: Node, child: T): T {\n sentinelSyncCheck();\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 sentinelSyncCheck();\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 sentinelSyncCheck();\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 sentinelSyncCheck();\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 sentinelSyncCheck();\n natives.setData(this, value);\n restoreAfterWrite(this);\n },\n });\n\n uninstallCurrent = () => {\n observer?.disconnect();\n observer = null;\n sentinelObserver?.disconnect();\n sentinelObserver = null;\n sentinelActive = false;\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":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -43,6 +43,13 @@ interface TranslationResilienceOptions {
|
|
|
43
43
|
document?: Document;
|
|
44
44
|
/** Observability hook: called with a short message on every non-native path taken. */
|
|
45
45
|
onEvent?: (message: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Install the document-wide observer immediately instead of waiting for a
|
|
48
|
+
* translation signal on <html>. The lazy default costs nothing until
|
|
49
|
+
* translation starts; eager also covers hypothetical translators that
|
|
50
|
+
* displace text without marking the document first.
|
|
51
|
+
*/
|
|
52
|
+
eager?: boolean;
|
|
46
53
|
}
|
|
47
54
|
declare function installTranslationResilience(options?: TranslationResilienceOptions): () => void;
|
|
48
55
|
|
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,13 @@ interface TranslationResilienceOptions {
|
|
|
43
43
|
document?: Document;
|
|
44
44
|
/** Observability hook: called with a short message on every non-native path taken. */
|
|
45
45
|
onEvent?: (message: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Install the document-wide observer immediately instead of waiting for a
|
|
48
|
+
* translation signal on <html>. The lazy default costs nothing until
|
|
49
|
+
* translation starts; eager also covers hypothetical translators that
|
|
50
|
+
* displace text without marking the document first.
|
|
51
|
+
*/
|
|
52
|
+
eager?: boolean;
|
|
46
53
|
}
|
|
47
54
|
declare function installTranslationResilience(options?: TranslationResilienceOptions): () => void;
|
|
48
55
|
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ var displaced = /* @__PURE__ */ new WeakMap();
|
|
|
3
3
|
var groupByReplacementNode = /* @__PURE__ */ new WeakMap();
|
|
4
4
|
var pendingCarrierOriginals = /* @__PURE__ */ new WeakMap();
|
|
5
5
|
var observer = null;
|
|
6
|
+
var sentinelObserver = null;
|
|
7
|
+
var sentinelActive = false;
|
|
6
8
|
var translationDetected = false;
|
|
7
9
|
var noopEvent = (_message) => void 0;
|
|
8
10
|
var emitEvent = noopEvent;
|
|
@@ -273,17 +275,51 @@ function restoreDisplaced(node, skipValue = false) {
|
|
|
273
275
|
return restoreGroup(group, skipValue ? node : void 0);
|
|
274
276
|
}
|
|
275
277
|
var uninstallCurrent = null;
|
|
278
|
+
function hasTranslatedClass(doc) {
|
|
279
|
+
return doc.documentElement.className.includes("translated-");
|
|
280
|
+
}
|
|
276
281
|
function installTranslationResilience(options = {}) {
|
|
277
282
|
var _a, _b;
|
|
278
283
|
if (uninstallCurrent) return uninstallCurrent;
|
|
279
284
|
const natives = domNatives();
|
|
280
285
|
const doc = (_a = options.document) != null ? _a : document;
|
|
281
286
|
emitEvent = (_b = options.onEvent) != null ? _b : noopEvent;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
const activateObserver = () => {
|
|
288
|
+
if (observer) return;
|
|
289
|
+
sentinelActive = false;
|
|
290
|
+
sentinelObserver == null ? void 0 : sentinelObserver.disconnect();
|
|
291
|
+
sentinelObserver = null;
|
|
292
|
+
observer = new MutationObserver((records) => {
|
|
293
|
+
guarded("record processing", () => processRecords(records), void 0);
|
|
294
|
+
});
|
|
295
|
+
observer.observe(doc, { childList: true, subtree: true, characterData: true, characterDataOldValue: true });
|
|
296
|
+
emitEvent("translation signal detected, observing document");
|
|
297
|
+
};
|
|
298
|
+
const sentinelSyncCheck = () => {
|
|
299
|
+
if (sentinelActive && hasTranslatedClass(doc)) activateObserver();
|
|
300
|
+
};
|
|
301
|
+
if (options.eager || hasTranslatedClass(doc)) {
|
|
302
|
+
activateObserver();
|
|
303
|
+
} else {
|
|
304
|
+
sentinelObserver = new MutationObserver((records) => {
|
|
305
|
+
guarded(
|
|
306
|
+
"sentinel processing",
|
|
307
|
+
() => {
|
|
308
|
+
for (const record of records) {
|
|
309
|
+
if (record.attributeName === "lang" || hasTranslatedClass(doc)) {
|
|
310
|
+
activateObserver();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
void 0
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
sentinelObserver.observe(doc.documentElement, { attributes: true, attributeFilter: ["lang", "class"] });
|
|
319
|
+
sentinelActive = true;
|
|
320
|
+
}
|
|
286
321
|
Node.prototype.removeChild = function removeChild(child) {
|
|
322
|
+
sentinelSyncCheck();
|
|
287
323
|
if (child.parentNode !== this) {
|
|
288
324
|
const outcome = guarded(
|
|
289
325
|
"removeChild repair",
|
|
@@ -308,6 +344,7 @@ function installTranslationResilience(options = {}) {
|
|
|
308
344
|
return child;
|
|
309
345
|
};
|
|
310
346
|
Node.prototype.insertBefore = function insertBefore(node, child) {
|
|
347
|
+
sentinelSyncCheck();
|
|
311
348
|
if (node instanceof Text && node.parentNode === null) {
|
|
312
349
|
guarded("insertBefore node repair", () => restoreDisplaced(node), "untracked");
|
|
313
350
|
}
|
|
@@ -331,6 +368,7 @@ function installTranslationResilience(options = {}) {
|
|
|
331
368
|
return node;
|
|
332
369
|
};
|
|
333
370
|
Node.prototype.appendChild = function appendChild(node) {
|
|
371
|
+
sentinelSyncCheck();
|
|
334
372
|
if (node instanceof Text && node.parentNode === null) {
|
|
335
373
|
guarded("appendChild repair", () => restoreDisplaced(node), "untracked");
|
|
336
374
|
}
|
|
@@ -347,6 +385,7 @@ function installTranslationResilience(options = {}) {
|
|
|
347
385
|
enumerable: natives.nodeValueDescriptor.enumerable,
|
|
348
386
|
get: natives.nodeValueDescriptor.get,
|
|
349
387
|
set(value) {
|
|
388
|
+
sentinelSyncCheck();
|
|
350
389
|
natives.setNodeValue(this, value);
|
|
351
390
|
restoreAfterWrite(this);
|
|
352
391
|
}
|
|
@@ -356,6 +395,7 @@ function installTranslationResilience(options = {}) {
|
|
|
356
395
|
enumerable: natives.dataDescriptor.enumerable,
|
|
357
396
|
get: natives.dataDescriptor.get,
|
|
358
397
|
set(value) {
|
|
398
|
+
sentinelSyncCheck();
|
|
359
399
|
natives.setData(this, value);
|
|
360
400
|
restoreAfterWrite(this);
|
|
361
401
|
}
|
|
@@ -363,6 +403,9 @@ function installTranslationResilience(options = {}) {
|
|
|
363
403
|
uninstallCurrent = () => {
|
|
364
404
|
observer == null ? void 0 : observer.disconnect();
|
|
365
405
|
observer = null;
|
|
406
|
+
sentinelObserver == null ? void 0 : sentinelObserver.disconnect();
|
|
407
|
+
sentinelObserver = null;
|
|
408
|
+
sentinelActive = false;
|
|
366
409
|
translationDetected = false;
|
|
367
410
|
emitEvent = noopEvent;
|
|
368
411
|
clearCorrelationState();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resilience.ts"],"sourcesContent":["/**\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/**\n * Every correlation store below is kept in ascending-`at` order: new entries\n * are appended, and any refresh of an existing entry's timestamp must\n * delete-then-set so the entry moves to the end. purgeExpired relies on this\n * to drop only the expired prefix and stop at the first fresh entry —\n * processRecords runs on every synchronous drain (i.e. inside patched DOM\n * calls), so a full scan of the stores per drain is quadratic under heavy\n * synchronous DOM churn.\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) break;\n recentInsertedBefore.delete(key);\n }\n for (const [key, entry] of recentOldValues) {\n if (now - entry.at <= CORRELATION_WINDOW_MS) break;\n recentOldValues.delete(key);\n }\n for (const [key, entry] of recentCarrierAccumulated) {\n if (now - entry.at <= CORRELATION_WINDOW_MS) break;\n recentCarrierAccumulated.delete(key);\n }\n const firstFresh = pendingOrphans.findIndex((orphan) => now - orphan.at <= CORRELATION_WINDOW_MS);\n if (firstFresh === -1) {\n if (pendingOrphans.length > 0) pendingOrphans = [];\n } else if (firstFresh > 0) {\n pendingOrphans = pendingOrphans.slice(firstFresh);\n }\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 // delete-then-set keeps the store in ascending-`at` order (see purgeExpired)\n recentCarrierAccumulated.delete(carrier);\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 // delete-then-set keeps the store in ascending-`at` order (see purgeExpired)\n recentInsertedBefore.delete(record.nextSibling);\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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/resilience.ts"],"sourcesContent":["/**\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 sentinelObserver: MutationObserver | null = null;\n/** True while installed but not yet observing — patched methods run a cheap synchronous signal check. */\nlet sentinelActive = false;\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/**\n * Every correlation store below is kept in ascending-`at` order: new entries\n * are appended, and any refresh of an existing entry's timestamp must\n * delete-then-set so the entry moves to the end. purgeExpired relies on this\n * to drop only the expired prefix and stop at the first fresh entry —\n * processRecords runs on every synchronous drain (i.e. inside patched DOM\n * calls), so a full scan of the stores per drain is quadratic under heavy\n * synchronous DOM churn.\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) break;\n recentInsertedBefore.delete(key);\n }\n for (const [key, entry] of recentOldValues) {\n if (now - entry.at <= CORRELATION_WINDOW_MS) break;\n recentOldValues.delete(key);\n }\n for (const [key, entry] of recentCarrierAccumulated) {\n if (now - entry.at <= CORRELATION_WINDOW_MS) break;\n recentCarrierAccumulated.delete(key);\n }\n const firstFresh = pendingOrphans.findIndex((orphan) => now - orphan.at <= CORRELATION_WINDOW_MS);\n if (firstFresh === -1) {\n if (pendingOrphans.length > 0) pendingOrphans = [];\n } else if (firstFresh > 0) {\n pendingOrphans = pendingOrphans.slice(firstFresh);\n }\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 // delete-then-set keeps the store in ascending-`at` order (see purgeExpired)\n recentCarrierAccumulated.delete(carrier);\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 // delete-then-set keeps the store in ascending-`at` order (see purgeExpired)\n recentInsertedBefore.delete(record.nextSibling);\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 * Install the document-wide observer immediately instead of waiting for a\n * translation signal on <html>. The lazy default costs nothing until\n * translation starts; eager also covers hypothetical translators that\n * displace text without marking the document first.\n */\n eager?: boolean;\n}\n\n/**\n * Chrome's translator marks the document before it displaces any text: it\n * adds a `translated-ltr`/`translated-rtl` class and flips `lang` on <html>,\n * measured ~275-500ms ahead of the first text mutation in real Chrome. The\n * class VALUE is checked (not just \"class changed\") because extensions add\n * unrelated classes to <html> on ordinary page loads.\n */\nfunction hasTranslatedClass(doc: Document): boolean {\n return doc.documentElement.className.includes('translated-');\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 const activateObserver = (): void => {\n if (observer) return;\n sentinelActive = false;\n sentinelObserver?.disconnect();\n sentinelObserver = null;\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 emitEvent('translation signal detected, observing document');\n };\n\n /**\n * Same-realm translators (the bundled simulator, or anything else calling\n * the patched DOM methods directly) can displace text in the same task that\n * sets the signal — before the sentinel's microtask callback runs. Real\n * Chrome translation runs in an isolated world with a large gap between\n * signal and displacement, so this synchronous fallback is only load\n * bearing for same-realm use: it reads one attribute while dormant and\n * costs a boolean check once active.\n */\n const sentinelSyncCheck = (): void => {\n if (sentinelActive && hasTranslatedClass(doc)) activateObserver();\n };\n\n if (options.eager || hasTranslatedClass(doc)) {\n activateObserver();\n } else {\n sentinelObserver = new MutationObserver((records) => {\n guarded(\n 'sentinel processing',\n () => {\n for (const record of records) {\n if (record.attributeName === 'lang' || hasTranslatedClass(doc)) {\n activateObserver();\n return;\n }\n }\n },\n undefined\n );\n });\n sentinelObserver.observe(doc.documentElement, { attributes: true, attributeFilter: ['lang', 'class'] });\n sentinelActive = true;\n }\n\n Node.prototype.removeChild = function removeChild<T extends Node>(this: Node, child: T): T {\n sentinelSyncCheck();\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 sentinelSyncCheck();\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 sentinelSyncCheck();\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 sentinelSyncCheck();\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 sentinelSyncCheck();\n natives.setData(this, value);\n restoreAfterWrite(this);\n },\n });\n\n uninstallCurrent = () => {\n observer?.disconnect();\n observer = null;\n sentinelObserver?.disconnect();\n sentinelObserver = null;\n sentinelActive = false;\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":[]}
|
package/dist/simulator.cjs
CHANGED
|
@@ -106,8 +106,16 @@ function normalizeSubtree(root) {
|
|
|
106
106
|
element = walker.nextNode();
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
function emitTranslationSignals(root) {
|
|
110
|
+
var _a;
|
|
111
|
+
const doc = (_a = root.ownerDocument) != null ? _a : root instanceof Document ? root : document;
|
|
112
|
+
const html = doc.documentElement;
|
|
113
|
+
if (!html.classList.contains("translated-ltr")) html.classList.add("translated-ltr");
|
|
114
|
+
if (html.getAttribute("lang") !== "x-pseudo") html.setAttribute("lang", "x-pseudo");
|
|
115
|
+
}
|
|
109
116
|
function translateSubtree(root, translate = pseudoTranslate, options = {}) {
|
|
110
117
|
var _a, _b, _c, _d;
|
|
118
|
+
emitTranslationSignals(root);
|
|
111
119
|
for (const textNode of (_a = options.deleteTextNodes) != null ? _a : []) {
|
|
112
120
|
(_b = textNode.parentNode) == null ? void 0 : _b.removeChild(textNode);
|
|
113
121
|
}
|
package/dist/simulator.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/simulator.ts"],"sourcesContent":["/**\n * Simulates the DOM mutations Chrome / Google Translate performs when\n * translating a page, so tests can reproduce the well-known class of React\n * crashes and stale-text bugs without a real browser translation session.\n *\n * Mutation shape, cross-verified from facebook/react#11538 (incl. the verbatim\n * Chromium bug 872770 analysis in comment 59) and\n * https://martijnhols.nl/blog/everything-about-google-translate-crashing-react:\n *\n * 1. Adjacent Text nodes are merged first (`normalize()`), so several\n * renderer-owned text nodes can collapse into one run.\n * 2. Each run is split into segments (numbers get isolated into their own\n * segment), and every segment becomes a nested double\n * `<font style=\"vertical-align: inherit;\">` wrapper around a NEW text\n * node with the translated content.\n * 3. The wrappers are inserted before the original text node, then the\n * original is REMOVED — it stays alive in memory (renderers still hold\n * references) but has parentNode === null.\n * 4. Translation can also delete text nodes outright and move inline\n * elements to match target-language word order (Chromium bug 872770).\n * 5. Comment nodes are never touched.\n * 6. A MutationObserver keeps watching the page and translates content that\n * appears or changes later.\n */\n\nexport function pseudoTranslate(text: string): string {\n return `[${text}]`;\n}\n\nexport type TranslateFn = (text: string) => string;\n\nexport interface TranslateOptions {\n /** Text nodes the \"translation\" deletes outright (no replacement). */\n deleteTextNodes?: Text[];\n /** Inline elements moved to the end of their parent (word-order change). */\n moveToParentEnd?: Element[];\n}\n\n/** Inner text nodes created by the simulator (already-translated content). */\nconst simulatorOwnedTextNodes = new WeakSet<Text>();\n\nfunction isText(node: Node): node is Text {\n return node.nodeType === Node.TEXT_NODE;\n}\n\nfunction hasTranslatableContent(node: Text): boolean {\n return /\\S/.test(node.nodeValue ?? '');\n}\n\nfunction isInsideFont(node: Node): boolean {\n let current: Node | null = node.parentNode;\n while (current) {\n if (current.nodeName === 'FONT') return true;\n current = current.parentNode;\n }\n return false;\n}\n\nfunction createFontWrapper(translatedText: string): HTMLElement {\n const outer = document.createElement('font');\n outer.setAttribute('style', 'vertical-align: inherit;');\n const inner = document.createElement('font');\n inner.setAttribute('style', 'vertical-align: inherit;');\n const translatedNode = document.createTextNode(translatedText);\n simulatorOwnedTextNodes.add(translatedNode);\n inner.appendChild(translatedNode);\n outer.appendChild(inner);\n return outer;\n}\n\n/** Numbers are isolated into their own segment, like the real translator. */\nfunction splitIntoSegments(text: string): string[] {\n return text.split(/(\\d+)/).filter((segment) => segment !== '');\n}\n\n/**\n * Replaces a single text node with translated <font> wrappers, exactly the\n * way Google Translate does: insert the wrappers, then detach the original.\n */\nfunction translateTextNode(textNode: Text, translate: TranslateFn): void {\n const parent = textNode.parentNode;\n if (!parent) return;\n for (const segment of splitIntoSegments(textNode.nodeValue ?? '')) {\n parent.insertBefore(createFontWrapper(translate(segment)), textNode);\n }\n parent.removeChild(textNode);\n}\n\nfunction collectTranslatableTextNodes(root: Node): Text[] {\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);\n const result: Text[] = [];\n let node = walker.nextNode();\n while (node) {\n if (isText(node) && hasTranslatableContent(node) && !isInsideFont(node) && !simulatorOwnedTextNodes.has(node)) {\n result.push(node);\n }\n node = walker.nextNode();\n }\n return result;\n}\n\n/**\n * Spec-equivalent Node.normalize(): merge each run of adjacent Text siblings\n * into the first node of the run and remove the rest. Done with explicit DOM\n * operations (rather than calling normalize()) so the MutationRecords emitted\n * are deterministic across DOM implementations.\n */\nfunction mergeAdjacentTextNodes(parent: Node): void {\n let child = parent.firstChild;\n while (child) {\n if (isText(child)) {\n let next = child.nextSibling;\n while (next && isText(next)) {\n const after = next.nextSibling;\n child.nodeValue = (child.nodeValue ?? '') + (next.nodeValue ?? '');\n parent.removeChild(next);\n next = after;\n }\n }\n child = child.nextSibling;\n }\n}\n\nfunction normalizeSubtree(root: Node): void {\n const base = isText(root) ? (root.parentNode ?? root) : root;\n mergeAdjacentTextNodes(base);\n const walker = document.createTreeWalker(base, NodeFilter.SHOW_ELEMENT);\n let element = walker.nextNode();\n while (element) {\n mergeAdjacentTextNodes(element);\n element = walker.nextNode();\n }\n}\n\n/** One-shot translation pass over a subtree, like the initial page translate. */\nexport function translateSubtree(\n root: Node,\n translate: TranslateFn = pseudoTranslate,\n options: TranslateOptions = {}\n): void {\n for (const textNode of options.deleteTextNodes ?? []) {\n textNode.parentNode?.removeChild(textNode);\n }\n normalizeSubtree(root);\n for (const textNode of collectTranslatableTextNodes(root)) {\n translateTextNode(textNode, translate);\n }\n if (isText(root) && hasTranslatableContent(root) && !isInsideFont(root) && !simulatorOwnedTextNodes.has(root)) {\n translateTextNode(root, translate);\n }\n for (const element of options.moveToParentEnd ?? []) {\n element.parentNode?.appendChild(element);\n }\n}\n\n/**\n * Models Google Translate's ongoing observation of the page: newly inserted\n * text gets translated (including a fresh normalize pass over its parent,\n * merging adjacent restored text nodes); text that changes inside an existing\n * <font> wrapper is re-translated in place.\n *\n * Returns a stop function.\n */\nexport function startTranslateObserver(root: Node, translate: TranslateFn = pseudoTranslate): () => void {\n const observer = new MutationObserver((records) => {\n const parentsToTranslate = new Set<Node>();\n for (const record of records) {\n if (record.type === 'childList') {\n for (const added of record.addedNodes) {\n if (isText(added) && simulatorOwnedTextNodes.has(added)) continue;\n if (isInsideFont(added)) {\n if (isText(added) && hasTranslatableContent(added)) {\n added.nodeValue = translate(added.nodeValue ?? '');\n simulatorOwnedTextNodes.add(added);\n }\n } else if (isText(added)) {\n if (hasTranslatableContent(added) && added.parentNode) parentsToTranslate.add(added.parentNode);\n } else {\n parentsToTranslate.add(added);\n }\n }\n } else if (record.type === 'characterData') {\n const target = record.target;\n if (\n isText(target) &&\n !simulatorOwnedTextNodes.has(target) &&\n isInsideFont(target) &&\n hasTranslatableContent(target)\n ) {\n target.nodeValue = translate(target.nodeValue ?? '');\n simulatorOwnedTextNodes.add(target);\n }\n }\n }\n for (const parent of parentsToTranslate) {\n if (parent.isConnected) translateSubtree(parent, translate);\n }\n });\n observer.observe(root, { childList: true, subtree: true, characterData: true });\n return () => observer.disconnect();\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/simulator.ts"],"sourcesContent":["/**\n * Simulates the DOM mutations Chrome / Google Translate performs when\n * translating a page, so tests can reproduce the well-known class of React\n * crashes and stale-text bugs without a real browser translation session.\n *\n * Mutation shape, cross-verified from facebook/react#11538 (incl. the verbatim\n * Chromium bug 872770 analysis in comment 59) and\n * https://martijnhols.nl/blog/everything-about-google-translate-crashing-react:\n *\n * 1. Adjacent Text nodes are merged first (`normalize()`), so several\n * renderer-owned text nodes can collapse into one run.\n * 2. Each run is split into segments (numbers get isolated into their own\n * segment), and every segment becomes a nested double\n * `<font style=\"vertical-align: inherit;\">` wrapper around a NEW text\n * node with the translated content.\n * 3. The wrappers are inserted before the original text node, then the\n * original is REMOVED — it stays alive in memory (renderers still hold\n * references) but has parentNode === null.\n * 4. Translation can also delete text nodes outright and move inline\n * elements to match target-language word order (Chromium bug 872770).\n * 5. Comment nodes are never touched.\n * 6. A MutationObserver keeps watching the page and translates content that\n * appears or changes later.\n * 7. BEFORE any text is touched, the document is marked: a `translated-ltr`\n * class and a `lang` flip on <html> (class first, then lang — verified\n * empirically against real Chrome, where both land ~275-500ms ahead of\n * the first displacement mutation).\n */\n\nexport function pseudoTranslate(text: string): string {\n return `[${text}]`;\n}\n\nexport type TranslateFn = (text: string) => string;\n\nexport interface TranslateOptions {\n /** Text nodes the \"translation\" deletes outright (no replacement). */\n deleteTextNodes?: Text[];\n /** Inline elements moved to the end of their parent (word-order change). */\n moveToParentEnd?: Element[];\n}\n\n/** Inner text nodes created by the simulator (already-translated content). */\nconst simulatorOwnedTextNodes = new WeakSet<Text>();\n\nfunction isText(node: Node): node is Text {\n return node.nodeType === Node.TEXT_NODE;\n}\n\nfunction hasTranslatableContent(node: Text): boolean {\n return /\\S/.test(node.nodeValue ?? '');\n}\n\nfunction isInsideFont(node: Node): boolean {\n let current: Node | null = node.parentNode;\n while (current) {\n if (current.nodeName === 'FONT') return true;\n current = current.parentNode;\n }\n return false;\n}\n\nfunction createFontWrapper(translatedText: string): HTMLElement {\n const outer = document.createElement('font');\n outer.setAttribute('style', 'vertical-align: inherit;');\n const inner = document.createElement('font');\n inner.setAttribute('style', 'vertical-align: inherit;');\n const translatedNode = document.createTextNode(translatedText);\n simulatorOwnedTextNodes.add(translatedNode);\n inner.appendChild(translatedNode);\n outer.appendChild(inner);\n return outer;\n}\n\n/** Numbers are isolated into their own segment, like the real translator. */\nfunction splitIntoSegments(text: string): string[] {\n return text.split(/(\\d+)/).filter((segment) => segment !== '');\n}\n\n/**\n * Replaces a single text node with translated <font> wrappers, exactly the\n * way Google Translate does: insert the wrappers, then detach the original.\n */\nfunction translateTextNode(textNode: Text, translate: TranslateFn): void {\n const parent = textNode.parentNode;\n if (!parent) return;\n for (const segment of splitIntoSegments(textNode.nodeValue ?? '')) {\n parent.insertBefore(createFontWrapper(translate(segment)), textNode);\n }\n parent.removeChild(textNode);\n}\n\nfunction collectTranslatableTextNodes(root: Node): Text[] {\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);\n const result: Text[] = [];\n let node = walker.nextNode();\n while (node) {\n if (isText(node) && hasTranslatableContent(node) && !isInsideFont(node) && !simulatorOwnedTextNodes.has(node)) {\n result.push(node);\n }\n node = walker.nextNode();\n }\n return result;\n}\n\n/**\n * Spec-equivalent Node.normalize(): merge each run of adjacent Text siblings\n * into the first node of the run and remove the rest. Done with explicit DOM\n * operations (rather than calling normalize()) so the MutationRecords emitted\n * are deterministic across DOM implementations.\n */\nfunction mergeAdjacentTextNodes(parent: Node): void {\n let child = parent.firstChild;\n while (child) {\n if (isText(child)) {\n let next = child.nextSibling;\n while (next && isText(next)) {\n const after = next.nextSibling;\n child.nodeValue = (child.nodeValue ?? '') + (next.nodeValue ?? '');\n parent.removeChild(next);\n next = after;\n }\n }\n child = child.nextSibling;\n }\n}\n\nfunction normalizeSubtree(root: Node): void {\n const base = isText(root) ? (root.parentNode ?? root) : root;\n mergeAdjacentTextNodes(base);\n const walker = document.createTreeWalker(base, NodeFilter.SHOW_ELEMENT);\n let element = walker.nextNode();\n while (element) {\n mergeAdjacentTextNodes(element);\n element = walker.nextNode();\n }\n}\n\n/**\n * Marks the document the way Chrome's translator does before touching any\n * text: a `translated-ltr` class and a `lang` flip on <html>. Emitting these\n * first lets lazily-activating consumers (like translation-resilience\n * itself) arm before displacement begins.\n */\nfunction emitTranslationSignals(root: Node): void {\n const doc = root.ownerDocument ?? (root instanceof Document ? root : document);\n const html = doc.documentElement;\n if (!html.classList.contains('translated-ltr')) html.classList.add('translated-ltr');\n if (html.getAttribute('lang') !== 'x-pseudo') html.setAttribute('lang', 'x-pseudo');\n}\n\n/** One-shot translation pass over a subtree, like the initial page translate. */\nexport function translateSubtree(\n root: Node,\n translate: TranslateFn = pseudoTranslate,\n options: TranslateOptions = {}\n): void {\n emitTranslationSignals(root);\n for (const textNode of options.deleteTextNodes ?? []) {\n textNode.parentNode?.removeChild(textNode);\n }\n normalizeSubtree(root);\n for (const textNode of collectTranslatableTextNodes(root)) {\n translateTextNode(textNode, translate);\n }\n if (isText(root) && hasTranslatableContent(root) && !isInsideFont(root) && !simulatorOwnedTextNodes.has(root)) {\n translateTextNode(root, translate);\n }\n for (const element of options.moveToParentEnd ?? []) {\n element.parentNode?.appendChild(element);\n }\n}\n\n/**\n * Models Google Translate's ongoing observation of the page: newly inserted\n * text gets translated (including a fresh normalize pass over its parent,\n * merging adjacent restored text nodes); text that changes inside an existing\n * <font> wrapper is re-translated in place.\n *\n * Returns a stop function.\n */\nexport function startTranslateObserver(root: Node, translate: TranslateFn = pseudoTranslate): () => void {\n const observer = new MutationObserver((records) => {\n const parentsToTranslate = new Set<Node>();\n for (const record of records) {\n if (record.type === 'childList') {\n for (const added of record.addedNodes) {\n if (isText(added) && simulatorOwnedTextNodes.has(added)) continue;\n if (isInsideFont(added)) {\n if (isText(added) && hasTranslatableContent(added)) {\n added.nodeValue = translate(added.nodeValue ?? '');\n simulatorOwnedTextNodes.add(added);\n }\n } else if (isText(added)) {\n if (hasTranslatableContent(added) && added.parentNode) parentsToTranslate.add(added.parentNode);\n } else {\n parentsToTranslate.add(added);\n }\n }\n } else if (record.type === 'characterData') {\n const target = record.target;\n if (\n isText(target) &&\n !simulatorOwnedTextNodes.has(target) &&\n isInsideFont(target) &&\n hasTranslatableContent(target)\n ) {\n target.nodeValue = translate(target.nodeValue ?? '');\n simulatorOwnedTextNodes.add(target);\n }\n }\n }\n for (const parent of parentsToTranslate) {\n if (parent.isConnected) translateSubtree(parent, translate);\n }\n });\n observer.observe(root, { childList: true, subtree: true, characterData: true });\n return () => observer.disconnect();\n}\n"],"mappings":"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","names":[]}
|
package/dist/simulator.d.cts
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* 5. Comment nodes are never touched.
|
|
22
22
|
* 6. A MutationObserver keeps watching the page and translates content that
|
|
23
23
|
* appears or changes later.
|
|
24
|
+
* 7. BEFORE any text is touched, the document is marked: a `translated-ltr`
|
|
25
|
+
* class and a `lang` flip on <html> (class first, then lang — verified
|
|
26
|
+
* empirically against real Chrome, where both land ~275-500ms ahead of
|
|
27
|
+
* the first displacement mutation).
|
|
24
28
|
*/
|
|
25
29
|
declare function pseudoTranslate(text: string): string;
|
|
26
30
|
type TranslateFn = (text: string) => string;
|
package/dist/simulator.d.ts
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* 5. Comment nodes are never touched.
|
|
22
22
|
* 6. A MutationObserver keeps watching the page and translates content that
|
|
23
23
|
* appears or changes later.
|
|
24
|
+
* 7. BEFORE any text is touched, the document is marked: a `translated-ltr`
|
|
25
|
+
* class and a `lang` flip on <html> (class first, then lang — verified
|
|
26
|
+
* empirically against real Chrome, where both land ~275-500ms ahead of
|
|
27
|
+
* the first displacement mutation).
|
|
24
28
|
*/
|
|
25
29
|
declare function pseudoTranslate(text: string): string;
|
|
26
30
|
type TranslateFn = (text: string) => string;
|
package/dist/simulator.js
CHANGED
|
@@ -80,8 +80,16 @@ function normalizeSubtree(root) {
|
|
|
80
80
|
element = walker.nextNode();
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
function emitTranslationSignals(root) {
|
|
84
|
+
var _a;
|
|
85
|
+
const doc = (_a = root.ownerDocument) != null ? _a : root instanceof Document ? root : document;
|
|
86
|
+
const html = doc.documentElement;
|
|
87
|
+
if (!html.classList.contains("translated-ltr")) html.classList.add("translated-ltr");
|
|
88
|
+
if (html.getAttribute("lang") !== "x-pseudo") html.setAttribute("lang", "x-pseudo");
|
|
89
|
+
}
|
|
83
90
|
function translateSubtree(root, translate = pseudoTranslate, options = {}) {
|
|
84
91
|
var _a, _b, _c, _d;
|
|
92
|
+
emitTranslationSignals(root);
|
|
85
93
|
for (const textNode of (_a = options.deleteTextNodes) != null ? _a : []) {
|
|
86
94
|
(_b = textNode.parentNode) == null ? void 0 : _b.removeChild(textNode);
|
|
87
95
|
}
|
package/dist/simulator.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/simulator.ts"],"sourcesContent":["/**\n * Simulates the DOM mutations Chrome / Google Translate performs when\n * translating a page, so tests can reproduce the well-known class of React\n * crashes and stale-text bugs without a real browser translation session.\n *\n * Mutation shape, cross-verified from facebook/react#11538 (incl. the verbatim\n * Chromium bug 872770 analysis in comment 59) and\n * https://martijnhols.nl/blog/everything-about-google-translate-crashing-react:\n *\n * 1. Adjacent Text nodes are merged first (`normalize()`), so several\n * renderer-owned text nodes can collapse into one run.\n * 2. Each run is split into segments (numbers get isolated into their own\n * segment), and every segment becomes a nested double\n * `<font style=\"vertical-align: inherit;\">` wrapper around a NEW text\n * node with the translated content.\n * 3. The wrappers are inserted before the original text node, then the\n * original is REMOVED — it stays alive in memory (renderers still hold\n * references) but has parentNode === null.\n * 4. Translation can also delete text nodes outright and move inline\n * elements to match target-language word order (Chromium bug 872770).\n * 5. Comment nodes are never touched.\n * 6. A MutationObserver keeps watching the page and translates content that\n * appears or changes later.\n */\n\nexport function pseudoTranslate(text: string): string {\n return `[${text}]`;\n}\n\nexport type TranslateFn = (text: string) => string;\n\nexport interface TranslateOptions {\n /** Text nodes the \"translation\" deletes outright (no replacement). */\n deleteTextNodes?: Text[];\n /** Inline elements moved to the end of their parent (word-order change). */\n moveToParentEnd?: Element[];\n}\n\n/** Inner text nodes created by the simulator (already-translated content). */\nconst simulatorOwnedTextNodes = new WeakSet<Text>();\n\nfunction isText(node: Node): node is Text {\n return node.nodeType === Node.TEXT_NODE;\n}\n\nfunction hasTranslatableContent(node: Text): boolean {\n return /\\S/.test(node.nodeValue ?? '');\n}\n\nfunction isInsideFont(node: Node): boolean {\n let current: Node | null = node.parentNode;\n while (current) {\n if (current.nodeName === 'FONT') return true;\n current = current.parentNode;\n }\n return false;\n}\n\nfunction createFontWrapper(translatedText: string): HTMLElement {\n const outer = document.createElement('font');\n outer.setAttribute('style', 'vertical-align: inherit;');\n const inner = document.createElement('font');\n inner.setAttribute('style', 'vertical-align: inherit;');\n const translatedNode = document.createTextNode(translatedText);\n simulatorOwnedTextNodes.add(translatedNode);\n inner.appendChild(translatedNode);\n outer.appendChild(inner);\n return outer;\n}\n\n/** Numbers are isolated into their own segment, like the real translator. */\nfunction splitIntoSegments(text: string): string[] {\n return text.split(/(\\d+)/).filter((segment) => segment !== '');\n}\n\n/**\n * Replaces a single text node with translated <font> wrappers, exactly the\n * way Google Translate does: insert the wrappers, then detach the original.\n */\nfunction translateTextNode(textNode: Text, translate: TranslateFn): void {\n const parent = textNode.parentNode;\n if (!parent) return;\n for (const segment of splitIntoSegments(textNode.nodeValue ?? '')) {\n parent.insertBefore(createFontWrapper(translate(segment)), textNode);\n }\n parent.removeChild(textNode);\n}\n\nfunction collectTranslatableTextNodes(root: Node): Text[] {\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);\n const result: Text[] = [];\n let node = walker.nextNode();\n while (node) {\n if (isText(node) && hasTranslatableContent(node) && !isInsideFont(node) && !simulatorOwnedTextNodes.has(node)) {\n result.push(node);\n }\n node = walker.nextNode();\n }\n return result;\n}\n\n/**\n * Spec-equivalent Node.normalize(): merge each run of adjacent Text siblings\n * into the first node of the run and remove the rest. Done with explicit DOM\n * operations (rather than calling normalize()) so the MutationRecords emitted\n * are deterministic across DOM implementations.\n */\nfunction mergeAdjacentTextNodes(parent: Node): void {\n let child = parent.firstChild;\n while (child) {\n if (isText(child)) {\n let next = child.nextSibling;\n while (next && isText(next)) {\n const after = next.nextSibling;\n child.nodeValue = (child.nodeValue ?? '') + (next.nodeValue ?? '');\n parent.removeChild(next);\n next = after;\n }\n }\n child = child.nextSibling;\n }\n}\n\nfunction normalizeSubtree(root: Node): void {\n const base = isText(root) ? (root.parentNode ?? root) : root;\n mergeAdjacentTextNodes(base);\n const walker = document.createTreeWalker(base, NodeFilter.SHOW_ELEMENT);\n let element = walker.nextNode();\n while (element) {\n mergeAdjacentTextNodes(element);\n element = walker.nextNode();\n }\n}\n\n/** One-shot translation pass over a subtree, like the initial page translate. */\nexport function translateSubtree(\n root: Node,\n translate: TranslateFn = pseudoTranslate,\n options: TranslateOptions = {}\n): void {\n for (const textNode of options.deleteTextNodes ?? []) {\n textNode.parentNode?.removeChild(textNode);\n }\n normalizeSubtree(root);\n for (const textNode of collectTranslatableTextNodes(root)) {\n translateTextNode(textNode, translate);\n }\n if (isText(root) && hasTranslatableContent(root) && !isInsideFont(root) && !simulatorOwnedTextNodes.has(root)) {\n translateTextNode(root, translate);\n }\n for (const element of options.moveToParentEnd ?? []) {\n element.parentNode?.appendChild(element);\n }\n}\n\n/**\n * Models Google Translate's ongoing observation of the page: newly inserted\n * text gets translated (including a fresh normalize pass over its parent,\n * merging adjacent restored text nodes); text that changes inside an existing\n * <font> wrapper is re-translated in place.\n *\n * Returns a stop function.\n */\nexport function startTranslateObserver(root: Node, translate: TranslateFn = pseudoTranslate): () => void {\n const observer = new MutationObserver((records) => {\n const parentsToTranslate = new Set<Node>();\n for (const record of records) {\n if (record.type === 'childList') {\n for (const added of record.addedNodes) {\n if (isText(added) && simulatorOwnedTextNodes.has(added)) continue;\n if (isInsideFont(added)) {\n if (isText(added) && hasTranslatableContent(added)) {\n added.nodeValue = translate(added.nodeValue ?? '');\n simulatorOwnedTextNodes.add(added);\n }\n } else if (isText(added)) {\n if (hasTranslatableContent(added) && added.parentNode) parentsToTranslate.add(added.parentNode);\n } else {\n parentsToTranslate.add(added);\n }\n }\n } else if (record.type === 'characterData') {\n const target = record.target;\n if (\n isText(target) &&\n !simulatorOwnedTextNodes.has(target) &&\n isInsideFont(target) &&\n hasTranslatableContent(target)\n ) {\n target.nodeValue = translate(target.nodeValue ?? '');\n simulatorOwnedTextNodes.add(target);\n }\n }\n }\n for (const parent of parentsToTranslate) {\n if (parent.isConnected) translateSubtree(parent, translate);\n }\n });\n observer.observe(root, { childList: true, subtree: true, characterData: true });\n return () => observer.disconnect();\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/simulator.ts"],"sourcesContent":["/**\n * Simulates the DOM mutations Chrome / Google Translate performs when\n * translating a page, so tests can reproduce the well-known class of React\n * crashes and stale-text bugs without a real browser translation session.\n *\n * Mutation shape, cross-verified from facebook/react#11538 (incl. the verbatim\n * Chromium bug 872770 analysis in comment 59) and\n * https://martijnhols.nl/blog/everything-about-google-translate-crashing-react:\n *\n * 1. Adjacent Text nodes are merged first (`normalize()`), so several\n * renderer-owned text nodes can collapse into one run.\n * 2. Each run is split into segments (numbers get isolated into their own\n * segment), and every segment becomes a nested double\n * `<font style=\"vertical-align: inherit;\">` wrapper around a NEW text\n * node with the translated content.\n * 3. The wrappers are inserted before the original text node, then the\n * original is REMOVED — it stays alive in memory (renderers still hold\n * references) but has parentNode === null.\n * 4. Translation can also delete text nodes outright and move inline\n * elements to match target-language word order (Chromium bug 872770).\n * 5. Comment nodes are never touched.\n * 6. A MutationObserver keeps watching the page and translates content that\n * appears or changes later.\n * 7. BEFORE any text is touched, the document is marked: a `translated-ltr`\n * class and a `lang` flip on <html> (class first, then lang — verified\n * empirically against real Chrome, where both land ~275-500ms ahead of\n * the first displacement mutation).\n */\n\nexport function pseudoTranslate(text: string): string {\n return `[${text}]`;\n}\n\nexport type TranslateFn = (text: string) => string;\n\nexport interface TranslateOptions {\n /** Text nodes the \"translation\" deletes outright (no replacement). */\n deleteTextNodes?: Text[];\n /** Inline elements moved to the end of their parent (word-order change). */\n moveToParentEnd?: Element[];\n}\n\n/** Inner text nodes created by the simulator (already-translated content). */\nconst simulatorOwnedTextNodes = new WeakSet<Text>();\n\nfunction isText(node: Node): node is Text {\n return node.nodeType === Node.TEXT_NODE;\n}\n\nfunction hasTranslatableContent(node: Text): boolean {\n return /\\S/.test(node.nodeValue ?? '');\n}\n\nfunction isInsideFont(node: Node): boolean {\n let current: Node | null = node.parentNode;\n while (current) {\n if (current.nodeName === 'FONT') return true;\n current = current.parentNode;\n }\n return false;\n}\n\nfunction createFontWrapper(translatedText: string): HTMLElement {\n const outer = document.createElement('font');\n outer.setAttribute('style', 'vertical-align: inherit;');\n const inner = document.createElement('font');\n inner.setAttribute('style', 'vertical-align: inherit;');\n const translatedNode = document.createTextNode(translatedText);\n simulatorOwnedTextNodes.add(translatedNode);\n inner.appendChild(translatedNode);\n outer.appendChild(inner);\n return outer;\n}\n\n/** Numbers are isolated into their own segment, like the real translator. */\nfunction splitIntoSegments(text: string): string[] {\n return text.split(/(\\d+)/).filter((segment) => segment !== '');\n}\n\n/**\n * Replaces a single text node with translated <font> wrappers, exactly the\n * way Google Translate does: insert the wrappers, then detach the original.\n */\nfunction translateTextNode(textNode: Text, translate: TranslateFn): void {\n const parent = textNode.parentNode;\n if (!parent) return;\n for (const segment of splitIntoSegments(textNode.nodeValue ?? '')) {\n parent.insertBefore(createFontWrapper(translate(segment)), textNode);\n }\n parent.removeChild(textNode);\n}\n\nfunction collectTranslatableTextNodes(root: Node): Text[] {\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);\n const result: Text[] = [];\n let node = walker.nextNode();\n while (node) {\n if (isText(node) && hasTranslatableContent(node) && !isInsideFont(node) && !simulatorOwnedTextNodes.has(node)) {\n result.push(node);\n }\n node = walker.nextNode();\n }\n return result;\n}\n\n/**\n * Spec-equivalent Node.normalize(): merge each run of adjacent Text siblings\n * into the first node of the run and remove the rest. Done with explicit DOM\n * operations (rather than calling normalize()) so the MutationRecords emitted\n * are deterministic across DOM implementations.\n */\nfunction mergeAdjacentTextNodes(parent: Node): void {\n let child = parent.firstChild;\n while (child) {\n if (isText(child)) {\n let next = child.nextSibling;\n while (next && isText(next)) {\n const after = next.nextSibling;\n child.nodeValue = (child.nodeValue ?? '') + (next.nodeValue ?? '');\n parent.removeChild(next);\n next = after;\n }\n }\n child = child.nextSibling;\n }\n}\n\nfunction normalizeSubtree(root: Node): void {\n const base = isText(root) ? (root.parentNode ?? root) : root;\n mergeAdjacentTextNodes(base);\n const walker = document.createTreeWalker(base, NodeFilter.SHOW_ELEMENT);\n let element = walker.nextNode();\n while (element) {\n mergeAdjacentTextNodes(element);\n element = walker.nextNode();\n }\n}\n\n/**\n * Marks the document the way Chrome's translator does before touching any\n * text: a `translated-ltr` class and a `lang` flip on <html>. Emitting these\n * first lets lazily-activating consumers (like translation-resilience\n * itself) arm before displacement begins.\n */\nfunction emitTranslationSignals(root: Node): void {\n const doc = root.ownerDocument ?? (root instanceof Document ? root : document);\n const html = doc.documentElement;\n if (!html.classList.contains('translated-ltr')) html.classList.add('translated-ltr');\n if (html.getAttribute('lang') !== 'x-pseudo') html.setAttribute('lang', 'x-pseudo');\n}\n\n/** One-shot translation pass over a subtree, like the initial page translate. */\nexport function translateSubtree(\n root: Node,\n translate: TranslateFn = pseudoTranslate,\n options: TranslateOptions = {}\n): void {\n emitTranslationSignals(root);\n for (const textNode of options.deleteTextNodes ?? []) {\n textNode.parentNode?.removeChild(textNode);\n }\n normalizeSubtree(root);\n for (const textNode of collectTranslatableTextNodes(root)) {\n translateTextNode(textNode, translate);\n }\n if (isText(root) && hasTranslatableContent(root) && !isInsideFont(root) && !simulatorOwnedTextNodes.has(root)) {\n translateTextNode(root, translate);\n }\n for (const element of options.moveToParentEnd ?? []) {\n element.parentNode?.appendChild(element);\n }\n}\n\n/**\n * Models Google Translate's ongoing observation of the page: newly inserted\n * text gets translated (including a fresh normalize pass over its parent,\n * merging adjacent restored text nodes); text that changes inside an existing\n * <font> wrapper is re-translated in place.\n *\n * Returns a stop function.\n */\nexport function startTranslateObserver(root: Node, translate: TranslateFn = pseudoTranslate): () => void {\n const observer = new MutationObserver((records) => {\n const parentsToTranslate = new Set<Node>();\n for (const record of records) {\n if (record.type === 'childList') {\n for (const added of record.addedNodes) {\n if (isText(added) && simulatorOwnedTextNodes.has(added)) continue;\n if (isInsideFont(added)) {\n if (isText(added) && hasTranslatableContent(added)) {\n added.nodeValue = translate(added.nodeValue ?? '');\n simulatorOwnedTextNodes.add(added);\n }\n } else if (isText(added)) {\n if (hasTranslatableContent(added) && added.parentNode) parentsToTranslate.add(added.parentNode);\n } else {\n parentsToTranslate.add(added);\n }\n }\n } else if (record.type === 'characterData') {\n const target = record.target;\n if (\n isText(target) &&\n !simulatorOwnedTextNodes.has(target) &&\n isInsideFont(target) &&\n hasTranslatableContent(target)\n ) {\n target.nodeValue = translate(target.nodeValue ?? '');\n simulatorOwnedTextNodes.add(target);\n }\n }\n }\n for (const parent of parentsToTranslate) {\n if (parent.isConnected) translateSubtree(parent, translate);\n }\n });\n observer.observe(root, { childList: true, subtree: true, characterData: true });\n return () => observer.disconnect();\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "translation-resilience",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Stops browser page translation (Google Translate) from crashing React and silently freezing translated text, by re-adopting displaced text nodes instead of swallowing errors.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"google-translate",
|