Skip to content
48 changes: 46 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
<body>
<header class="tw-header">
<nav class="tw-nav">
<p> hello there i m vineet 😀</p>
<p> hello there i m vineet</p>
<a href="#home">Home</a>
<a href="#products">Product</a>
<a href="#about">About</a>
Expand All @@ -87,7 +87,7 @@ <h2>Latest News</h2>
</article>

<article class="article">
<h2>Company Updates</h2>
<h2>Company Updatess</h2>
<p>
Our company has expanded to three new locations this
year. We're now serving customers in over 50 countries
Expand Down Expand Up @@ -119,6 +119,50 @@ <h2>Customer Stories</h2>
})">Reset Translio</button>

</main>
<!-- <section style="height: 200vh; background-color: #e0e0e0;">
<h2>Extended Section</h2>
<p>This section is added to increase the page height to 200vh for testing purposes.</p>
<article>
<h3>Article 1</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum vestibulum.</p>
</article>
<article>
<h3>Article 2</h3>
<p>Curabitur sit amet massa nec sapien varius lacinia. Nulla facilisi. Donec vulputate interdum sollicitudin.</p>
</article>
<article>
<h3>Article 3</h3>
<p>Fusce vehicula dolor arcu, sit amet blandit dolor mollis nec. Donec viverra eleifend lacus, vitae ullamcorper metus.</p>
</article>
<article>
<h3>Article 4</h3>
<p>Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.</p>
</article>
<article>
<h3>Article 5</h3>
<p>Maecenas malesuada. Praesent congue erat at massa. Sed cursus turpis vitae tortor. Donec posuere vulputate arcu.</p>
</article>
<article>
<h3>Article 6</h3>
<p>Phasellus accumsan cursus velit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae.</p>
</article>
<article>
<h3>Article 7</h3>
<p>Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus.</p>
</article>
<article>
<h3>Article 8</h3>
<p>Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum.</p>
</article>
<article>
<h3>Article 9</h3>
<p>Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus.</p>
</article>
<article>
<h3>Article 10</h3>
<p>Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt.</p>
</article>
</section> -->
<script defer src="http://localhost:5173/dist/index.min.js"></script>
<script defer type="module">
TranslationWidget(import.meta.env.VITE_TRANSLATION_WIDGET_PUBLIC_KEY, {
Expand Down
4 changes: 3 additions & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Position } from "../types";

export const MAX_CACHE_SIZE = 1000;
export const BATCH_SIZE = 10;
export const CACHE_PREFIX = "jss-";

export const DEFAULT_CONFIG = {
pageLanguage: "en",
autoDetectLanguage: false,
position: "top-right" as const,
position: Position.TopRight,
};
39 changes: 19 additions & 20 deletions src/lib/dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,71 +7,71 @@ import { removeEmojis } from "../../utils/utils";
export type TranslatableContent = {
element: HTMLElement;
text: string;
}[]
}[];
export class DocumentNavigator {
/**
* Retrieves text nodes eligible for translation from the document
* @returns Collection of text nodes ready for translation
*/
static findTranslatableContent(): TranslatableContent {
if (typeof window === "undefined") return [];

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();

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 navigator = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, validator);
const groupedText = new Map<HTMLElement, Text[]>();

let currentNode: Node | null;
while ((currentNode = navigator.nextNode())) {
const parentElement = (currentNode as Text).parentElement;
if (!parentElement) continue;

if (!groupedText.has(parentElement)) {
groupedText.set(parentElement, []);
}
groupedText.get(parentElement)!.push(currentNode as Text);
}

const results: { element: HTMLElement; text: string }[] = [];

for (const [element, textNodes] of groupedText.entries()) {
let combinedText = "";

for (const node of textNodes) {
let text = node.textContent?.trim() || "";
const originalText = element.getAttribute("data-original-text");
if (originalText) text = originalText;

const textWithoutEmojis = removeEmojis(text);
if (text.length === 0 || text.length === 1 || textWithoutEmojis.length === 0) continue;

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

if (combinedText.length > 0) {
results.push({ element, text: combinedText });
}
}

return results;
}


/**
* Divides a collection into smaller groups
Expand All @@ -88,5 +88,4 @@ export class DocumentNavigator {

return groups;
}

}
9 changes: 3 additions & 6 deletions src/lib/storage/localstorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class LocalStorageWrapper {
}

getPageKey(targetLang: string): string {
return `${this.prefix}-${targetLang}`;
return `${this.prefix}${targetLang}`;
}

private shouldCompress(value: string): boolean {
Expand Down Expand Up @@ -79,13 +79,10 @@ export class LocalStorageWrapper {
const pageKey = this.getPageKey(targetLang);
let translations: TranslationContent = this.getItem(pageKey) || {};
translations[originalText] = translatedText;
this.setItem(pageKey, translations );
this.setItem(pageKey, translations);
}

setBatchNodeTranslationsArray(
targetLang: string,
batch: Array<{ originalText: string; translatedText: string }>
): void {
setBatchNodeTranslationsArray(targetLang: string, batch: Array<{ originalText: string; translatedText: string }>): void {
const pageKey = this.getPageKey(targetLang);
const existing: TranslationContent = this.getItem(pageKey) || {};

Expand Down
17 changes: 4 additions & 13 deletions src/lib/translation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,17 @@ interface TranslationError extends Error {
response?: Response;
}

interface CacheMetrics {
hits: number;
misses: number;
}

export class TranslationService {
private readonly publicKey: string;
private cacheMetrics: CacheMetrics = { hits: 0, misses: 0 };
private readonly apiUrl = "https://api.jigsawstack.com/v1/ai/translate";

constructor(publicKey: string) {
this.publicKey = publicKey;
}

getCacheMetrics(): CacheMetrics {
return { ...this.cacheMetrics };
}
async translateBatchText(texts: string[], targetLang: string, maxRetries = 2, retryDelay = 100): Promise<string[] | null> {


async translateBatchText(texts: string[], targetLang: string, maxRetries = 2, retryDelay = 100): Promise<string[]> {
let attempt = 0;
while (attempt < maxRetries) {
try {
Expand Down Expand Up @@ -59,12 +51,11 @@ export class TranslationService {
attempt++;
if (attempt >= maxRetries) {
console.error("Translation error after retries:", error);
return texts; // Return original texts on error
return null;
}
// Wait before retrying
await new Promise((res) => setTimeout(res, retryDelay));
}
}
return texts;
return null;
}
}
25 changes: 22 additions & 3 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface Language {

export interface TranslationConfig {
pageLanguage?: string;
position?: "top-right" | "top-left" | "bottom-right" | "bottom-left";
position?: Position;
autoDetectLanguage?: boolean;
theme?: {
baseColor?: string;
Expand All @@ -34,7 +34,6 @@ export interface TranslationWidgetOptions {
config?: TranslationConfig;
}


export interface WidgetElements {
trigger: HTMLDivElement | null;
dropdown: HTMLDivElement | null;
Expand All @@ -52,4 +51,24 @@ export interface TranslationResult {
duration?: number;
}

export interface TranslationContent { [key: string]: string }
export enum Position {
TopRight = "top-right",
TopLeft = "top-left",
BottomLeft = "bottom-left",
BottomRight = "bottom-right",
}

export enum LOCALSTORAGE_KEYS {
PREFERRED_LANGUAGE = "jss-pref",
}

export enum ATTRIBUTES {
TRANSLATED_LANG = "data-translated-lang",
ORIGINAL_TEXT = "data-original-text",
ORIGINAL_FONT_SIZE = "data-original-font-size",
}

export const LANG_PARAM = "lang";
export interface TranslationContent {
[key: string]: string;
}
31 changes: 29 additions & 2 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,37 @@ function generateNodeHash(text: string): string {

function generateChunkHash(texts: string[]): string {
const content = texts
.map(text => text.replace(/\s+/g, " ").trim().toLocaleLowerCase())
.map((text) => text.replace(/\s+/g, " ").trim().toLocaleLowerCase())
.join(" ")
.trim();
return murmurhash3_32_gc(content.toLowerCase(), 42).toString(16);
}

export { generateHashForContent, generateNodeHash, generateChunkHash, getVisibleTextContent, removeEmojis, getUserLanguage };
type ValidationResult = { isValid: true } | { isValid: false; message: string };

function validatePublicApiKey(publicKey: string): ValidationResult {
if (!publicKey) {
return {
isValid: false,
message: "Public key is required to initialize the translation widget",
};
}

if (publicKey.startsWith("sk_")) {
return {
isValid: false,
message: "Please use public api key for security reasons. You can get one from https://jigsawstack.com",
};
}

if (!publicKey.startsWith("pk_")) {
return {
isValid: false,
message: "Please use proper api key. You can get one from https://jigsawstack.com",
};
}

return { isValid: true };
}

export { generateHashForContent, generateNodeHash, generateChunkHash, getVisibleTextContent, removeEmojis, getUserLanguage, validatePublicApiKey };
Loading