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 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.4 kB min+gzip.
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 activity detected',
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.** Before any translation activity is detected, every operation behaves natively — including throwing `NotFoundError` on genuine `removeChild`/`insertBefore` bugs in your code. The shim does not mask real bugs.
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
- Measured numbers, with an honest caveat up front: 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.
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
- While translation is **not** active (the common case):
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
- - **React-level overhead is small.** Re-rendering the table with three cells changing in every row, 50 commits: 27 ms → 41 ms total, ≈ 0.3 ms extra per full-table commit. Repeated mount+unmount of the whole table: +20%.
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
- Where the cost comes from: one document-wide `MutationObserver` (childList + characterData + oldValue, subtree) means the browser allocates a record per DOM mutation. The patched methods add a parent check per call; inserting a *detached* text node additionally drains and processes pending observer records. Correlation state expires after 100 ms and is purged incrementally (amortized O(1) per entry), so steady-state memory is effectively zero.
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
- While translation **is** active, each update to translated text pays the restore → re-translate cycle: ≈ 14 µs per updated text run, so a commit updating 1,500 translated text 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.
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 measured shim overhead (~0.3 ms on a full-table commit, +20% on mount/unmount cycles) would drop to a branch-predicted pointer compare — effectively unmeasurable. 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.
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
- observer = new MutationObserver((records) => {
309
- guarded("record processing", () => processRecords(records), void 0);
310
- });
311
- observer.observe(doc, { childList: true, subtree: true, characterData: true, characterDataOldValue: true });
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();
@@ -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
- observer = new MutationObserver((records) => {
283
- guarded("record processing", () => processRecords(records), void 0);
284
- });
285
- observer.observe(doc, { childList: true, subtree: true, characterData: true, characterDataOldValue: true });
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":[]}
@@ -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
  }
@@ -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":[]}
@@ -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;
@@ -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
  }
@@ -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.1.2",
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",