translation-resilience 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/simulator.ts
21
+ var simulator_exports = {};
22
+ __export(simulator_exports, {
23
+ pseudoTranslate: () => pseudoTranslate,
24
+ startTranslateObserver: () => startTranslateObserver,
25
+ translateSubtree: () => translateSubtree
26
+ });
27
+ module.exports = __toCommonJS(simulator_exports);
28
+ function pseudoTranslate(text) {
29
+ return `[${text}]`;
30
+ }
31
+ var simulatorOwnedTextNodes = /* @__PURE__ */ new WeakSet();
32
+ function isText(node) {
33
+ return node.nodeType === Node.TEXT_NODE;
34
+ }
35
+ function hasTranslatableContent(node) {
36
+ var _a;
37
+ return /\S/.test((_a = node.nodeValue) != null ? _a : "");
38
+ }
39
+ function isInsideFont(node) {
40
+ let current = node.parentNode;
41
+ while (current) {
42
+ if (current.nodeName === "FONT") return true;
43
+ current = current.parentNode;
44
+ }
45
+ return false;
46
+ }
47
+ function createFontWrapper(translatedText) {
48
+ const outer = document.createElement("font");
49
+ outer.setAttribute("style", "vertical-align: inherit;");
50
+ const inner = document.createElement("font");
51
+ inner.setAttribute("style", "vertical-align: inherit;");
52
+ const translatedNode = document.createTextNode(translatedText);
53
+ simulatorOwnedTextNodes.add(translatedNode);
54
+ inner.appendChild(translatedNode);
55
+ outer.appendChild(inner);
56
+ return outer;
57
+ }
58
+ function splitIntoSegments(text) {
59
+ return text.split(/(\d+)/).filter((segment) => segment !== "");
60
+ }
61
+ function translateTextNode(textNode, translate) {
62
+ var _a;
63
+ const parent = textNode.parentNode;
64
+ if (!parent) return;
65
+ for (const segment of splitIntoSegments((_a = textNode.nodeValue) != null ? _a : "")) {
66
+ parent.insertBefore(createFontWrapper(translate(segment)), textNode);
67
+ }
68
+ parent.removeChild(textNode);
69
+ }
70
+ function collectTranslatableTextNodes(root) {
71
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
72
+ const result = [];
73
+ let node = walker.nextNode();
74
+ while (node) {
75
+ if (isText(node) && hasTranslatableContent(node) && !isInsideFont(node) && !simulatorOwnedTextNodes.has(node)) {
76
+ result.push(node);
77
+ }
78
+ node = walker.nextNode();
79
+ }
80
+ return result;
81
+ }
82
+ function mergeAdjacentTextNodes(parent) {
83
+ var _a, _b;
84
+ let child = parent.firstChild;
85
+ while (child) {
86
+ if (isText(child)) {
87
+ let next = child.nextSibling;
88
+ while (next && isText(next)) {
89
+ const after = next.nextSibling;
90
+ child.nodeValue = ((_a = child.nodeValue) != null ? _a : "") + ((_b = next.nodeValue) != null ? _b : "");
91
+ parent.removeChild(next);
92
+ next = after;
93
+ }
94
+ }
95
+ child = child.nextSibling;
96
+ }
97
+ }
98
+ function normalizeSubtree(root) {
99
+ var _a;
100
+ const base = isText(root) ? (_a = root.parentNode) != null ? _a : root : root;
101
+ mergeAdjacentTextNodes(base);
102
+ const walker = document.createTreeWalker(base, NodeFilter.SHOW_ELEMENT);
103
+ let element = walker.nextNode();
104
+ while (element) {
105
+ mergeAdjacentTextNodes(element);
106
+ element = walker.nextNode();
107
+ }
108
+ }
109
+ function translateSubtree(root, translate = pseudoTranslate, options = {}) {
110
+ var _a, _b, _c, _d;
111
+ for (const textNode of (_a = options.deleteTextNodes) != null ? _a : []) {
112
+ (_b = textNode.parentNode) == null ? void 0 : _b.removeChild(textNode);
113
+ }
114
+ normalizeSubtree(root);
115
+ for (const textNode of collectTranslatableTextNodes(root)) {
116
+ translateTextNode(textNode, translate);
117
+ }
118
+ if (isText(root) && hasTranslatableContent(root) && !isInsideFont(root) && !simulatorOwnedTextNodes.has(root)) {
119
+ translateTextNode(root, translate);
120
+ }
121
+ for (const element of (_c = options.moveToParentEnd) != null ? _c : []) {
122
+ (_d = element.parentNode) == null ? void 0 : _d.appendChild(element);
123
+ }
124
+ }
125
+ function startTranslateObserver(root, translate = pseudoTranslate) {
126
+ const observer = new MutationObserver((records) => {
127
+ var _a, _b;
128
+ const parentsToTranslate = /* @__PURE__ */ new Set();
129
+ for (const record of records) {
130
+ if (record.type === "childList") {
131
+ for (const added of record.addedNodes) {
132
+ if (isText(added) && simulatorOwnedTextNodes.has(added)) continue;
133
+ if (isInsideFont(added)) {
134
+ if (isText(added) && hasTranslatableContent(added)) {
135
+ added.nodeValue = translate((_a = added.nodeValue) != null ? _a : "");
136
+ simulatorOwnedTextNodes.add(added);
137
+ }
138
+ } else if (isText(added)) {
139
+ if (hasTranslatableContent(added) && added.parentNode) parentsToTranslate.add(added.parentNode);
140
+ } else {
141
+ parentsToTranslate.add(added);
142
+ }
143
+ }
144
+ } else if (record.type === "characterData") {
145
+ const target = record.target;
146
+ if (isText(target) && !simulatorOwnedTextNodes.has(target) && isInsideFont(target) && hasTranslatableContent(target)) {
147
+ target.nodeValue = translate((_b = target.nodeValue) != null ? _b : "");
148
+ simulatorOwnedTextNodes.add(target);
149
+ }
150
+ }
151
+ }
152
+ for (const parent of parentsToTranslate) {
153
+ if (parent.isConnected) translateSubtree(parent, translate);
154
+ }
155
+ });
156
+ observer.observe(root, { childList: true, subtree: true, characterData: true });
157
+ return () => observer.disconnect();
158
+ }
159
+ // Annotate the CommonJS export names for ESM import in node:
160
+ 0 && (module.exports = {
161
+ pseudoTranslate,
162
+ startTranslateObserver,
163
+ translateSubtree
164
+ });
165
+ //# sourceMappingURL=simulator.cjs.map
@@ -0,0 +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":[]}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Simulates the DOM mutations Chrome / Google Translate performs when
3
+ * translating a page, so tests can reproduce the well-known class of React
4
+ * crashes and stale-text bugs without a real browser translation session.
5
+ *
6
+ * Mutation shape, cross-verified from facebook/react#11538 (incl. the verbatim
7
+ * Chromium bug 872770 analysis in comment 59) and
8
+ * https://martijnhols.nl/blog/everything-about-google-translate-crashing-react:
9
+ *
10
+ * 1. Adjacent Text nodes are merged first (`normalize()`), so several
11
+ * renderer-owned text nodes can collapse into one run.
12
+ * 2. Each run is split into segments (numbers get isolated into their own
13
+ * segment), and every segment becomes a nested double
14
+ * `<font style="vertical-align: inherit;">` wrapper around a NEW text
15
+ * node with the translated content.
16
+ * 3. The wrappers are inserted before the original text node, then the
17
+ * original is REMOVED — it stays alive in memory (renderers still hold
18
+ * references) but has parentNode === null.
19
+ * 4. Translation can also delete text nodes outright and move inline
20
+ * elements to match target-language word order (Chromium bug 872770).
21
+ * 5. Comment nodes are never touched.
22
+ * 6. A MutationObserver keeps watching the page and translates content that
23
+ * appears or changes later.
24
+ */
25
+ declare function pseudoTranslate(text: string): string;
26
+ type TranslateFn = (text: string) => string;
27
+ interface TranslateOptions {
28
+ /** Text nodes the "translation" deletes outright (no replacement). */
29
+ deleteTextNodes?: Text[];
30
+ /** Inline elements moved to the end of their parent (word-order change). */
31
+ moveToParentEnd?: Element[];
32
+ }
33
+ /** One-shot translation pass over a subtree, like the initial page translate. */
34
+ declare function translateSubtree(root: Node, translate?: TranslateFn, options?: TranslateOptions): void;
35
+ /**
36
+ * Models Google Translate's ongoing observation of the page: newly inserted
37
+ * text gets translated (including a fresh normalize pass over its parent,
38
+ * merging adjacent restored text nodes); text that changes inside an existing
39
+ * <font> wrapper is re-translated in place.
40
+ *
41
+ * Returns a stop function.
42
+ */
43
+ declare function startTranslateObserver(root: Node, translate?: TranslateFn): () => void;
44
+
45
+ export { type TranslateFn, type TranslateOptions, pseudoTranslate, startTranslateObserver, translateSubtree };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Simulates the DOM mutations Chrome / Google Translate performs when
3
+ * translating a page, so tests can reproduce the well-known class of React
4
+ * crashes and stale-text bugs without a real browser translation session.
5
+ *
6
+ * Mutation shape, cross-verified from facebook/react#11538 (incl. the verbatim
7
+ * Chromium bug 872770 analysis in comment 59) and
8
+ * https://martijnhols.nl/blog/everything-about-google-translate-crashing-react:
9
+ *
10
+ * 1. Adjacent Text nodes are merged first (`normalize()`), so several
11
+ * renderer-owned text nodes can collapse into one run.
12
+ * 2. Each run is split into segments (numbers get isolated into their own
13
+ * segment), and every segment becomes a nested double
14
+ * `<font style="vertical-align: inherit;">` wrapper around a NEW text
15
+ * node with the translated content.
16
+ * 3. The wrappers are inserted before the original text node, then the
17
+ * original is REMOVED — it stays alive in memory (renderers still hold
18
+ * references) but has parentNode === null.
19
+ * 4. Translation can also delete text nodes outright and move inline
20
+ * elements to match target-language word order (Chromium bug 872770).
21
+ * 5. Comment nodes are never touched.
22
+ * 6. A MutationObserver keeps watching the page and translates content that
23
+ * appears or changes later.
24
+ */
25
+ declare function pseudoTranslate(text: string): string;
26
+ type TranslateFn = (text: string) => string;
27
+ interface TranslateOptions {
28
+ /** Text nodes the "translation" deletes outright (no replacement). */
29
+ deleteTextNodes?: Text[];
30
+ /** Inline elements moved to the end of their parent (word-order change). */
31
+ moveToParentEnd?: Element[];
32
+ }
33
+ /** One-shot translation pass over a subtree, like the initial page translate. */
34
+ declare function translateSubtree(root: Node, translate?: TranslateFn, options?: TranslateOptions): void;
35
+ /**
36
+ * Models Google Translate's ongoing observation of the page: newly inserted
37
+ * text gets translated (including a fresh normalize pass over its parent,
38
+ * merging adjacent restored text nodes); text that changes inside an existing
39
+ * <font> wrapper is re-translated in place.
40
+ *
41
+ * Returns a stop function.
42
+ */
43
+ declare function startTranslateObserver(root: Node, translate?: TranslateFn): () => void;
44
+
45
+ export { type TranslateFn, type TranslateOptions, pseudoTranslate, startTranslateObserver, translateSubtree };
@@ -0,0 +1,138 @@
1
+ // src/simulator.ts
2
+ function pseudoTranslate(text) {
3
+ return `[${text}]`;
4
+ }
5
+ var simulatorOwnedTextNodes = /* @__PURE__ */ new WeakSet();
6
+ function isText(node) {
7
+ return node.nodeType === Node.TEXT_NODE;
8
+ }
9
+ function hasTranslatableContent(node) {
10
+ var _a;
11
+ return /\S/.test((_a = node.nodeValue) != null ? _a : "");
12
+ }
13
+ function isInsideFont(node) {
14
+ let current = node.parentNode;
15
+ while (current) {
16
+ if (current.nodeName === "FONT") return true;
17
+ current = current.parentNode;
18
+ }
19
+ return false;
20
+ }
21
+ function createFontWrapper(translatedText) {
22
+ const outer = document.createElement("font");
23
+ outer.setAttribute("style", "vertical-align: inherit;");
24
+ const inner = document.createElement("font");
25
+ inner.setAttribute("style", "vertical-align: inherit;");
26
+ const translatedNode = document.createTextNode(translatedText);
27
+ simulatorOwnedTextNodes.add(translatedNode);
28
+ inner.appendChild(translatedNode);
29
+ outer.appendChild(inner);
30
+ return outer;
31
+ }
32
+ function splitIntoSegments(text) {
33
+ return text.split(/(\d+)/).filter((segment) => segment !== "");
34
+ }
35
+ function translateTextNode(textNode, translate) {
36
+ var _a;
37
+ const parent = textNode.parentNode;
38
+ if (!parent) return;
39
+ for (const segment of splitIntoSegments((_a = textNode.nodeValue) != null ? _a : "")) {
40
+ parent.insertBefore(createFontWrapper(translate(segment)), textNode);
41
+ }
42
+ parent.removeChild(textNode);
43
+ }
44
+ function collectTranslatableTextNodes(root) {
45
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
46
+ const result = [];
47
+ let node = walker.nextNode();
48
+ while (node) {
49
+ if (isText(node) && hasTranslatableContent(node) && !isInsideFont(node) && !simulatorOwnedTextNodes.has(node)) {
50
+ result.push(node);
51
+ }
52
+ node = walker.nextNode();
53
+ }
54
+ return result;
55
+ }
56
+ function mergeAdjacentTextNodes(parent) {
57
+ var _a, _b;
58
+ let child = parent.firstChild;
59
+ while (child) {
60
+ if (isText(child)) {
61
+ let next = child.nextSibling;
62
+ while (next && isText(next)) {
63
+ const after = next.nextSibling;
64
+ child.nodeValue = ((_a = child.nodeValue) != null ? _a : "") + ((_b = next.nodeValue) != null ? _b : "");
65
+ parent.removeChild(next);
66
+ next = after;
67
+ }
68
+ }
69
+ child = child.nextSibling;
70
+ }
71
+ }
72
+ function normalizeSubtree(root) {
73
+ var _a;
74
+ const base = isText(root) ? (_a = root.parentNode) != null ? _a : root : root;
75
+ mergeAdjacentTextNodes(base);
76
+ const walker = document.createTreeWalker(base, NodeFilter.SHOW_ELEMENT);
77
+ let element = walker.nextNode();
78
+ while (element) {
79
+ mergeAdjacentTextNodes(element);
80
+ element = walker.nextNode();
81
+ }
82
+ }
83
+ function translateSubtree(root, translate = pseudoTranslate, options = {}) {
84
+ var _a, _b, _c, _d;
85
+ for (const textNode of (_a = options.deleteTextNodes) != null ? _a : []) {
86
+ (_b = textNode.parentNode) == null ? void 0 : _b.removeChild(textNode);
87
+ }
88
+ normalizeSubtree(root);
89
+ for (const textNode of collectTranslatableTextNodes(root)) {
90
+ translateTextNode(textNode, translate);
91
+ }
92
+ if (isText(root) && hasTranslatableContent(root) && !isInsideFont(root) && !simulatorOwnedTextNodes.has(root)) {
93
+ translateTextNode(root, translate);
94
+ }
95
+ for (const element of (_c = options.moveToParentEnd) != null ? _c : []) {
96
+ (_d = element.parentNode) == null ? void 0 : _d.appendChild(element);
97
+ }
98
+ }
99
+ function startTranslateObserver(root, translate = pseudoTranslate) {
100
+ const observer = new MutationObserver((records) => {
101
+ var _a, _b;
102
+ const parentsToTranslate = /* @__PURE__ */ new Set();
103
+ for (const record of records) {
104
+ if (record.type === "childList") {
105
+ for (const added of record.addedNodes) {
106
+ if (isText(added) && simulatorOwnedTextNodes.has(added)) continue;
107
+ if (isInsideFont(added)) {
108
+ if (isText(added) && hasTranslatableContent(added)) {
109
+ added.nodeValue = translate((_a = added.nodeValue) != null ? _a : "");
110
+ simulatorOwnedTextNodes.add(added);
111
+ }
112
+ } else if (isText(added)) {
113
+ if (hasTranslatableContent(added) && added.parentNode) parentsToTranslate.add(added.parentNode);
114
+ } else {
115
+ parentsToTranslate.add(added);
116
+ }
117
+ }
118
+ } else if (record.type === "characterData") {
119
+ const target = record.target;
120
+ if (isText(target) && !simulatorOwnedTextNodes.has(target) && isInsideFont(target) && hasTranslatableContent(target)) {
121
+ target.nodeValue = translate((_b = target.nodeValue) != null ? _b : "");
122
+ simulatorOwnedTextNodes.add(target);
123
+ }
124
+ }
125
+ }
126
+ for (const parent of parentsToTranslate) {
127
+ if (parent.isConnected) translateSubtree(parent, translate);
128
+ }
129
+ });
130
+ observer.observe(root, { childList: true, subtree: true, characterData: true });
131
+ return () => observer.disconnect();
132
+ }
133
+ export {
134
+ pseudoTranslate,
135
+ startTranslateObserver,
136
+ translateSubtree
137
+ };
138
+ //# sourceMappingURL=simulator.js.map
@@ -0,0 +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":[]}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "translation-resilience",
3
+ "version": "0.1.0",
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
+ "keywords": [
6
+ "google-translate",
7
+ "page-translation",
8
+ "react",
9
+ "notfounderror",
10
+ "removechild",
11
+ "insertbefore",
12
+ "mutationobserver",
13
+ "chrome",
14
+ "i18n",
15
+ "dom"
16
+ ],
17
+ "author": "Alex Speller <alex@alexspeller.com>",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/alexspeller/translation-resilience.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/alexspeller/translation-resilience/issues"
25
+ },
26
+ "type": "module",
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js",
34
+ "require": "./dist/index.cjs"
35
+ },
36
+ "./simulator": {
37
+ "types": "./dist/simulator.d.ts",
38
+ "import": "./dist/simulator.js",
39
+ "require": "./dist/simulator.cjs"
40
+ }
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "sideEffects": false,
46
+ "scripts": {
47
+ "build": "tsup",
48
+ "test": "vitest run",
49
+ "typecheck": "tsc --noEmit",
50
+ "lint": "biome check .",
51
+ "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build"
52
+ },
53
+ "devDependencies": {
54
+ "@biomejs/biome": "^2.5.3",
55
+ "@testing-library/dom": "^10.4.1",
56
+ "@testing-library/react": "^16.3.2",
57
+ "@types/react": "^18.3.31",
58
+ "@types/react-dom": "^18.3.7",
59
+ "jsdom": "^29.1.1",
60
+ "react": "^18.3.1",
61
+ "react-dom": "^18.3.1",
62
+ "tsup": "^8.5.1",
63
+ "typescript": "^5.9.3",
64
+ "vitest": "^4.1.10"
65
+ },
66
+ "allowScripts": {
67
+ "esbuild": true,
68
+ "fsevents": true
69
+ }
70
+ }