Skip to content

Commit 9c75b63

Browse files
fix: fixes made for nested interactive elements (#50)
* fix: test enviorment * feat: button interactivity fixed * Feat: added handling for unexpected errors * feat: update changes * feat: v1.1.3
1 parent 8e34240 commit 9c75b63

File tree

5 files changed

+91
-16
lines changed

5 files changed

+91
-16
lines changed

example/translation-demo-nextjs/components/translationWidget.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,4 @@ export default function Translation() {
1717
}, []);
1818

1919
return null;
20-
}
21-
20+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "translation-widget",
3-
"version": "1.1.2",
3+
"version": "1.1.3",
44
"description": "Translation widget to automatically translate any website with a single script",
55
"homepage": "https://github.com/JigsawStack/translation-widget",
66
"author": "JigsawStack",

src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ declare global {
1010

1111
let widgetInstance: TranslationWidget | undefined;
1212

13+
1314
const initializeTranslationWidget = (publicKey: string, config?: TranslationConfig): TranslationWidget => {
1415
if (typeof window === "undefined") {
1516
throw new Error("Translation widget can only be used in browser environment");
@@ -37,4 +38,19 @@ const initializeTranslationWidget = (publicKey: string, config?: TranslationConf
3738
}
3839
};
3940

41+
(() => {
42+
const originalRemoveChild = Node.prototype.removeChild;
43+
Node.prototype.removeChild = function<T extends Node>(child: T): T {
44+
try {
45+
return originalRemoveChild.call(this, child) as T;
46+
} catch (err) {
47+
if (err instanceof DOMException && err.name === "NotFoundError") {
48+
return child;
49+
}
50+
throw err;
51+
}
52+
};
53+
})();
54+
55+
4056
export default initializeTranslationWidget;

src/lib/dom/index.ts

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,51 @@ export class DocumentNavigator {
2020
const validator: NodeProcessor = {
2121
acceptNode(node: Node): number {
2222
if (node.nodeType !== Node.TEXT_NODE) return NodeFilter.FILTER_REJECT;
23-
23+
2424
const container = (node as Text).parentElement;
2525
if (!container) return NodeFilter.FILTER_REJECT;
26-
26+
2727
if (container.closest('[aria-hidden="true"]')) return NodeFilter.FILTER_REJECT;
2828
if (container.classList.contains("sr-only")) return NodeFilter.FILTER_REJECT;
29-
30-
const shouldSkip =
31-
container.closest(
32-
"script, style, code, noscript, next-route-announcer, .jigts-translation-widget, .jigts-widget-trigger, .jigts-widget-dropdown, .notranslate"
33-
) !== null || !node.textContent?.trim();
34-
35-
return shouldSkip ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
29+
30+
const skipBySelector = container.closest(
31+
"script, style, code, noscript, next-route-announcer, \
32+
.jigts-translation-widget, .jigts-widget-trigger, \
33+
.jigts-widget-dropdown, .notranslate"
34+
);
35+
if (skipBySelector) return NodeFilter.FILTER_REJECT;
36+
37+
// ✅ If the text is inside a clean wrapper like <span> → allow
38+
const isInSpanWrapper =
39+
container.tagName === "SPAN" &&
40+
Array.from(container.childNodes).every((n) => n.nodeType === Node.TEXT_NODE);
41+
42+
const interactiveAncestor = container.closest(
43+
"button, input, select, textarea, [role='button'], [role='link']"
44+
) as HTMLElement | null;
45+
46+
if (interactiveAncestor && !isInSpanWrapper) {
47+
const isTextSiblingToElement = Array.from(container.childNodes).some(
48+
(n) =>
49+
n.nodeType === Node.ELEMENT_NODE &&
50+
node.parentNode === container // sibling to another element in same container
51+
);
52+
53+
const isDirectChildOfInteractive =
54+
interactiveAncestor === container;
55+
56+
if (isTextSiblingToElement && isDirectChildOfInteractive) {
57+
// ⚠️ Text node is a sibling to other element nodes inside the button — unsafe
58+
return NodeFilter.FILTER_REJECT;
59+
}
60+
}
61+
62+
if (!node.textContent?.trim()) return NodeFilter.FILTER_REJECT;
63+
64+
return NodeFilter.FILTER_ACCEPT;
3665
},
3766
};
67+
3868

3969
const navigator = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, validator);
4070
const groupedText = new Map<HTMLElement, Text[]>();
@@ -71,6 +101,25 @@ export class DocumentNavigator {
71101

72102
if (isDescendantOfNested) continue;
73103

104+
const childNodes = Array.from(element.childNodes);
105+
const hasText = childNodes.some(
106+
(n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim()
107+
);
108+
const hasInteractiveElements = childNodes.some(
109+
(n) =>
110+
n.nodeType === Node.ELEMENT_NODE &&
111+
["BUTTON", "INPUT", "TEXTAREA", "SELECT"].includes(
112+
(n as HTMLElement).tagName
113+
)
114+
);
115+
116+
const isTextMixedWithInteractivity = hasText && hasInteractiveElements;
117+
118+
119+
if (isTextMixedWithInteractivity) {
120+
continue;
121+
}
122+
74123
for (const node of textNodes) {
75124
let text = node.textContent?.trim() || "";
76125
const originalText = element.getAttribute("data-original-text");
@@ -81,9 +130,20 @@ export class DocumentNavigator {
81130

82131
combinedText += (combinedText ? " " : "") + text;
83132

84-
if (element.children.length > 0) {
133+
// if(isTextMixedWithInteractivity) {
134+
// continue;
135+
// }
136+
const hasMixedContent = Array.from(element.childNodes).some(
137+
(child) => child.nodeType !== Node.TEXT_NODE
138+
);
139+
140+
141+
if (hasMixedContent) {
85142
isNested = true;
86143
}
144+
// if (element.children.length > 0) {
145+
// isNested = true;
146+
// }
87147
}
88148

89149
if (combinedText.length > 0) {

src/widget/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ export class TranslationWidget {
495495
try {
496496
// Find all translatable content nodes in the document
497497
const nodes = DocumentNavigator.findTranslatableContent();
498+
498499
// get the visible nodes
499500
const visibleNodes = nodes.filter((node) => {
500501
const rect = node.element.getBoundingClientRect();
@@ -592,7 +593,6 @@ export class TranslationWidget {
592593

593594
const batchArray: Array<{ originalText: string; translatedText: string }> = [];
594595

595-
console.log(successfulBatches);
596596
// Process successful batches
597597
successfulBatches.forEach(({ translations, nodes }) => {
598598
nodes.forEach((node, nodeIndex) => {
@@ -1099,7 +1099,8 @@ export class TranslationWidget {
10991099
private setTranslatedContent(element: HTMLElement, translatedText: string): void {
11001100
// Check if the translated text contains HTML tags
11011101
const hasHtmlTags = /<[^>]*>/g.test(translatedText);
1102-
1102+
1103+
11031104
if (hasHtmlTags) {
11041105
// Create a temporary container to parse the HTML
11051106
const tempContainer = document.createElement('div');
@@ -1115,7 +1116,6 @@ export class TranslationWidget {
11151116
element.innerHTML = translatedText;
11161117
}
11171118
} else {
1118-
// No HTML tags, use textContent for safety
11191119
element.textContent = translatedText;
11201120
}
11211121
}

0 commit comments

Comments
 (0)