Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,4 @@ export default function Translation() {
}, []);

return null;
}

}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "translation-widget",
"version": "1.1.2",
"version": "1.1.3",
"description": "Translation widget to automatically translate any website with a single script",
"homepage": "https://github.com/JigsawStack/translation-widget",
"author": "JigsawStack",
Expand Down
16 changes: 16 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare global {

let widgetInstance: TranslationWidget | undefined;


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

(() => {
const originalRemoveChild = Node.prototype.removeChild;
Node.prototype.removeChild = function<T extends Node>(child: T): T {
try {
return originalRemoveChild.call(this, child) as T;
} catch (err) {
if (err instanceof DOMException && err.name === "NotFoundError") {
return child;
}
throw err;
}
};
})();


export default initializeTranslationWidget;
80 changes: 70 additions & 10 deletions src/lib/dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,51 @@ export class DocumentNavigator {
const validator: NodeProcessor = {
acceptNode(node: Node): number {
if (node.nodeType !== Node.TEXT_NODE) return NodeFilter.FILTER_REJECT;

const container = (node as Text).parentElement;
if (!container) return NodeFilter.FILTER_REJECT;

if (container.closest('[aria-hidden="true"]')) return NodeFilter.FILTER_REJECT;
if (container.classList.contains("sr-only")) return NodeFilter.FILTER_REJECT;

const shouldSkip =
container.closest(
"script, style, code, noscript, next-route-announcer, .jigts-translation-widget, .jigts-widget-trigger, .jigts-widget-dropdown, .notranslate"
) !== null || !node.textContent?.trim();

return shouldSkip ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;

const skipBySelector = container.closest(
"script, style, code, noscript, next-route-announcer, \
.jigts-translation-widget, .jigts-widget-trigger, \
.jigts-widget-dropdown, .notranslate"
);
if (skipBySelector) return NodeFilter.FILTER_REJECT;

// ✅ If the text is inside a clean wrapper like <span> → allow
const isInSpanWrapper =
container.tagName === "SPAN" &&
Array.from(container.childNodes).every((n) => n.nodeType === Node.TEXT_NODE);

const interactiveAncestor = container.closest(
"button, input, select, textarea, [role='button'], [role='link']"
) as HTMLElement | null;

if (interactiveAncestor && !isInSpanWrapper) {
const isTextSiblingToElement = Array.from(container.childNodes).some(
(n) =>
n.nodeType === Node.ELEMENT_NODE &&
node.parentNode === container // sibling to another element in same container
);

const isDirectChildOfInteractive =
interactiveAncestor === container;

if (isTextSiblingToElement && isDirectChildOfInteractive) {
// ⚠️ Text node is a sibling to other element nodes inside the button — unsafe
return NodeFilter.FILTER_REJECT;
}
}

if (!node.textContent?.trim()) return NodeFilter.FILTER_REJECT;

return NodeFilter.FILTER_ACCEPT;
},
};


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

if (isDescendantOfNested) continue;

const childNodes = Array.from(element.childNodes);
const hasText = childNodes.some(
(n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim()
);
const hasInteractiveElements = childNodes.some(
(n) =>
n.nodeType === Node.ELEMENT_NODE &&
["BUTTON", "INPUT", "TEXTAREA", "SELECT"].includes(
(n as HTMLElement).tagName
)
);

const isTextMixedWithInteractivity = hasText && hasInteractiveElements;


if (isTextMixedWithInteractivity) {
continue;
}

for (const node of textNodes) {
let text = node.textContent?.trim() || "";
const originalText = element.getAttribute("data-original-text");
Expand All @@ -81,9 +130,20 @@ export class DocumentNavigator {

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

if (element.children.length > 0) {
// if(isTextMixedWithInteractivity) {
// continue;
// }
const hasMixedContent = Array.from(element.childNodes).some(
(child) => child.nodeType !== Node.TEXT_NODE
);


if (hasMixedContent) {
isNested = true;
}
// if (element.children.length > 0) {
// isNested = true;
// }
}

if (combinedText.length > 0) {
Expand Down
6 changes: 3 additions & 3 deletions src/widget/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ export class TranslationWidget {
try {
// Find all translatable content nodes in the document
const nodes = DocumentNavigator.findTranslatableContent();

// get the visible nodes
const visibleNodes = nodes.filter((node) => {
const rect = node.element.getBoundingClientRect();
Expand Down Expand Up @@ -592,7 +593,6 @@ export class TranslationWidget {

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

console.log(successfulBatches);
// Process successful batches
successfulBatches.forEach(({ translations, nodes }) => {
nodes.forEach((node, nodeIndex) => {
Expand Down Expand Up @@ -1099,7 +1099,8 @@ export class TranslationWidget {
private setTranslatedContent(element: HTMLElement, translatedText: string): void {
// Check if the translated text contains HTML tags
const hasHtmlTags = /<[^>]*>/g.test(translatedText);



if (hasHtmlTags) {
// Create a temporary container to parse the HTML
const tempContainer = document.createElement('div');
Expand All @@ -1115,7 +1116,6 @@ export class TranslationWidget {
element.innerHTML = translatedText;
}
} else {
// No HTML tags, use textContent for safety
element.textContent = translatedText;
}
}
Expand Down