diff --git a/src/translate-modal.js b/src/translate-modal.js index 808db77..11482fe 100644 --- a/src/translate-modal.js +++ b/src/translate-modal.js @@ -2,7 +2,7 @@ import { initializeChunkFiveFont } from "./ui-themes.js"; import { getUIText } from "./ui-translations.js"; import { extractWebpageContent } from "./content.js"; -import { translateCurrentPage, translateText, SUPPORTED_LANGUAGES } from "./translation.js"; +import { translateCurrentPage, translateText, translateHTML, SUPPORTED_LANGUAGES } from "./translation.js"; // Function to fetch full HTML content from a remote URL using CORS proxy async function fetchRemotePageHTML(url) { @@ -54,93 +54,12 @@ async function fetchRemotePageHTML(url) { } } -// Function to translate remote HTML content (similar to translateCurrentPage) +// Function to translate remote HTML content - reuses translateCurrentPage logic with URL resolution async function translateRemoteHTML(htmlContent, targetLanguage, sourceUrl) { - const { preserveSpecialContent, restoreSpecialContent } = await import("./translation.js"); - const { translateText } = await import("./translation.js"); + // Use the same translateHTML function but with URL resolution for remote content + const translatedHTML = await translateHTML(htmlContent, targetLanguage, sourceUrl); - // Parse the HTML - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlContent, 'text/html'); - - // Extract just the body content for translation (preserve head intact) - const bodyHTML = doc.body ? doc.body.innerHTML : doc.documentElement.innerHTML; - - // Use the same content preservation as translateCurrentPage - const { preservedText, preservations } = preserveSpecialContent(bodyHTML); - - // Translate the preserved content - const translatedPreservedText = await translateText(preservedText, targetLanguage); - - // Restore the special content - const translatedContent = restoreSpecialContent(translatedPreservedText, preservations); - - // Replace the body content with translated content - if (doc.body) { - doc.body.innerHTML = translatedContent; - } else { - doc.documentElement.innerHTML = translatedContent; - } - - // Convert all relative URLs to absolute URLs - const baseUrl = new URL('.', sourceUrl).href; - - // Convert relative URLs in img src attributes - doc.querySelectorAll('img[src]').forEach(img => { - if (!img.src.startsWith('http') && !img.src.startsWith('//') && !img.src.startsWith('data:')) { - img.src = new URL(img.getAttribute('src'), sourceUrl).href; - } - }); - - // Convert relative URLs in link href attributes (CSS, etc.) - doc.querySelectorAll('link[href]').forEach(link => { - if (!link.href.startsWith('http') && !link.href.startsWith('//')) { - link.href = new URL(link.getAttribute('href'), sourceUrl).href; - } - }); - - // Convert relative URLs in script src attributes - doc.querySelectorAll('script[src]').forEach(script => { - if (!script.src.startsWith('http') && !script.src.startsWith('//')) { - script.src = new URL(script.getAttribute('src'), sourceUrl).href; - } - }); - - // Convert relative URLs in a href attributes - doc.querySelectorAll('a[href]').forEach(link => { - if (!link.href.startsWith('http') && !link.href.startsWith('//') && !link.href.startsWith('#') && !link.href.startsWith('mailto:') && !link.href.startsWith('tel:')) { - link.href = new URL(link.getAttribute('href'), sourceUrl).href; - } - }); - - // Convert background images in style attributes - doc.querySelectorAll('[style*="background"]').forEach(el => { - const style = el.getAttribute('style'); - if (style && style.includes('url(')) { - const updatedStyle = style.replace(/url\(['"]?([^'"]+)['"]?\)/g, (match, url) => { - if (!url.startsWith('http') && !url.startsWith('//') && !url.startsWith('data:')) { - return `url('${new URL(url, sourceUrl).href}')`; - } - return match; - }); - el.setAttribute('style', updatedStyle); - } - }); - - // Add base tag as fallback for any missed URLs - const existingBase = doc.querySelector('base'); - if (existingBase) { - existingBase.remove(); - } - - const baseTag = doc.createElement('base'); - baseTag.setAttribute('href', baseUrl); - if (doc.head) { - doc.head.insertBefore(baseTag, doc.head.firstChild); - } - - // Return the complete translated HTML document - return doc.documentElement.outerHTML; + return translatedHTML; } export function openTranslateModal() { diff --git a/src/translation.js b/src/translation.js index 8ddaafb..c4adbaf 100644 --- a/src/translation.js +++ b/src/translation.js @@ -382,34 +382,92 @@ export function extractPageContent() { return documentClone.documentElement.outerHTML; } -// Translate current page content -export async function translateCurrentPage(targetLanguage) { - const pageContent = extractPageContent(); +// Convert relative URLs to absolute URLs based on source URL +function convertRelativeToAbsolute(htmlContent, sourceUrl) { + if (!sourceUrl) { + return htmlContent; + } - if (!pageContent || pageContent.length < 10) { - throw new Error("Unable to extract meaningful content from this page."); + try { + const baseUrl = new URL(sourceUrl); + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlContent, 'text/html'); + + // Convert relative URLs in various attributes + const elementsWithUrls = [ + { selector: 'a[href]', attr: 'href' }, + { selector: 'img[src]', attr: 'src' }, + { selector: 'link[href]', attr: 'href' }, + { selector: 'script[src]', attr: 'src' }, + { selector: 'iframe[src]', attr: 'src' }, + { selector: 'source[src]', attr: 'src' }, + { selector: 'audio[src]', attr: 'src' }, + { selector: 'video[src]', attr: 'src' }, + { selector: 'embed[src]', attr: 'src' }, + { selector: 'object[data]', attr: 'data' }, + { selector: 'form[action]', attr: 'action' } + ]; + + elementsWithUrls.forEach(({ selector, attr }) => { + const elements = doc.querySelectorAll(selector); + elements.forEach(element => { + const url = element.getAttribute(attr); + if (url && !url.startsWith('http') && !url.startsWith('//') && !url.startsWith('#') && !url.startsWith('mailto:') && !url.startsWith('tel:')) { + try { + const absoluteUrl = new URL(url, baseUrl).href; + element.setAttribute(attr, absoluteUrl); + } catch (e) { + // If URL conversion fails, keep the original + console.warn('Failed to convert relative URL:', url, e); + } + } + }); + }); + + return doc.documentElement.outerHTML; + } catch (error) { + console.warn('Failed to convert relative URLs:', error); + return htmlContent; + } +} + +// Translate any HTML content with optional URL resolution +export async function translateHTML(htmlContent, targetLanguage, sourceUrl = null) { + if (!htmlContent || htmlContent.length < 10) { + throw new Error("Unable to extract meaningful content from provided HTML."); + } + + // Convert relative URLs to absolute if sourceUrl is provided + let processedContent = htmlContent; + if (sourceUrl) { + processedContent = convertRelativeToAbsolute(htmlContent, sourceUrl); + console.log('Converted relative URLs using base URL:', sourceUrl); } // Limit content length to avoid overwhelming the AI const maxLength = 50000; // Much higher limit for complete page translation const contentToTranslate = - pageContent.length > maxLength - ? `${pageContent.substring(0, maxLength)}...` - : pageContent; + processedContent.length > maxLength + ? `${processedContent.substring(0, maxLength)}...` + : processedContent; // Translate the content const translatedContent = await translateText(contentToTranslate, targetLanguage); - // Parse the translated content to replace the original content + // Parse the content to add base tag for URL resolution const parser = new DOMParser(); - const doc = parser.parseFromString(pageContent, 'text/html'); + const doc = parser.parseFromString(translatedContent, 'text/html'); - // Replace the body content with translated content - doc.body.innerHTML = translatedContent; - - // Add a base tag to resolve all relative URLs correctly - const currentUrl = window.location.href; - const baseUrl = new URL('.', currentUrl).href; // Get base URL (without filename) + // Add base tag for URL resolution + let baseUrl; + if (sourceUrl) { + // Use the source URL as base + baseUrl = new URL('.', sourceUrl).href; // Get base URL (without filename) + } else { + // Use current page URL as base (like current page translation) + const currentUrl = window.location.href; + baseUrl = new URL('.', currentUrl).href; + } // Remove any existing base tag const existingBase = doc.querySelector('base'); @@ -426,3 +484,11 @@ export async function translateCurrentPage(targetLanguage) { return doc.documentElement.outerHTML; } + +// Translate current page content +export async function translateCurrentPage(targetLanguage) { + const pageContent = extractPageContent(); + + // Use the new translateHTML function for current page translation + return await translateHTML(pageContent, targetLanguage); +} diff --git a/uncloseai.js b/uncloseai.js index 21a713c..d0e9386 100644 --- a/uncloseai.js +++ b/uncloseai.js @@ -67,6 +67,7 @@ export const openTranslateModal = UI.openTranslateModal; // Translation functionality export const translateText = Translation.translateText; export const translateCurrentPage = Translation.translateCurrentPage; +export const translateHTML = Translation.translateHTML; // ------------------------- // Configuration and State