add remote URL translation tab: fetch and translate any website just-in-time

This commit is contained in:
Russell Ballestrini 2025-07-11 16:44:02 -04:00
parent 2587fcf050
commit 928f658328
21 changed files with 408 additions and 44 deletions

172
add_remote_url_keys.js Normal file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env node
// Script to add missing remote URL translation keys to all language files
import fs from 'fs';
import path from 'path';
const languagesDir = './src/languages';
// The new keys to add in English
const newKeys = {
remoteUrlTab: "🌐 Remote URL",
remoteUrlInfo: "Enter a URL to fetch and translate:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Fetch & Translate"
};
// Translations for each language
const translations = {
ar: {
remoteUrlTab: "🌐 رابط بعيد",
remoteUrlInfo: "أدخل رابط لجلبه وترجمته:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 جلب وترجمة"
},
bn: {
remoteUrlTab: "🌐 দূরবর্তী URL",
remoteUrlInfo: "একটি URL প্রবেশ করান যা আনতে এবং অনুবাদ করতে:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 আনুন এবং অনুবাদ করুন"
},
de: {
remoteUrlTab: "🌐 Externe URL",
remoteUrlInfo: "Geben Sie eine URL ein, um sie abzurufen und zu übersetzen:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Abrufen & Übersetzen"
},
es: {
remoteUrlTab: "🌐 URL Remota",
remoteUrlInfo: "Ingresa una URL para obtener y traducir:",
remoteUrlPlaceholder: "https://ejemplo.com",
fetchAndTranslate: "🌐 Obtener y Traducir"
},
fr: {
remoteUrlTab: "🌐 URL Distante",
remoteUrlInfo: "Entrez une URL à récupérer et traduire:",
remoteUrlPlaceholder: "https://exemple.com",
fetchAndTranslate: "🌐 Récupérer et Traduire"
},
hi: {
remoteUrlTab: "🌐 दूरस्थ URL",
remoteUrlInfo: "लाने और अनुवाद करने के लिए एक URL दर्ज करें:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 लाएं और अनुवाद करें"
},
id: {
remoteUrlTab: "🌐 URL Jarak Jauh",
remoteUrlInfo: "Masukkan URL untuk diambil dan diterjemahkan:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Ambil & Terjemahkan"
},
ja: {
remoteUrlTab: "🌐 リモートURL",
remoteUrlInfo: "取得して翻訳するURLを入力してください:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 取得して翻訳"
},
ko: {
remoteUrlTab: "🌐 원격 URL",
remoteUrlInfo: "가져와서 번역할 URL을 입력하세요:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 가져와서 번역"
},
mr: {
remoteUrlTab: "🌐 दूरस्थ URL",
remoteUrlInfo: "आणण्यासाठी आणि भाषांतर करण्यासाठी URL प्रविष्ट करा:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 आणा आणि भाषांतर करा"
},
pt: {
remoteUrlTab: "🌐 URL Remota",
remoteUrlInfo: "Digite uma URL para buscar e traduzir:",
remoteUrlPlaceholder: "https://exemplo.com",
fetchAndTranslate: "🌐 Buscar e Traduzir"
},
ru: {
remoteUrlTab: "🌐 Удаленный URL",
remoteUrlInfo: "Введите URL для загрузки и перевода:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Загрузить и Перевести"
},
sw: {
remoteUrlTab: "🌐 URL ya Mbali",
remoteUrlInfo: "Ingiza URL ya kuleta na kutafsiri:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Leta na Tafsiri"
},
te: {
remoteUrlTab: "🌐 రిమోట్ URL",
remoteUrlInfo: "తీసుకురావడానికి మరియు అనువదించడానికి URL ని నమోదు చేయండి:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 తీసుకురండి & అనువదించండి"
},
tr: {
remoteUrlTab: "🌐 Uzak URL",
remoteUrlInfo: "Getirmek ve çevirmek için bir URL girin:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Getir ve Çevir"
},
ur: {
remoteUrlTab: "🌐 ریموٹ URL",
remoteUrlInfo: "لانے اور ترجمہ کرنے کے لیے URL داخل کریں:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 لائیں اور ترجمہ کریں"
},
"zh-tw": {
remoteUrlTab: "🌐 遠端網址",
remoteUrlInfo: "輸入要擷取和翻譯的網址:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 擷取並翻譯"
},
zh: {
remoteUrlTab: "🌐 远程网址",
remoteUrlInfo: "输入要获取和翻译的网址:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 获取并翻译"
}
};
// Process each language file
for (const [langCode, langTranslations] of Object.entries(translations)) {
const filePath = path.join(languagesDir, `${langCode}.js`);
if (fs.existsSync(filePath)) {
let content = fs.readFileSync(filePath, 'utf8');
// Find the position to insert new keys (before the closing })
const insertPosition = content.lastIndexOf('};');
if (insertPosition === -1) {
console.error(`Could not find export structure in ${filePath}`);
continue;
}
// Find the last non-empty line before the closing }
const beforeClosing = content.substring(0, insertPosition).trimEnd();
// Check if any of the new keys already exist
const hasNewKeys = Object.keys(langTranslations).some(key =>
content.includes(`${key}:`));
if (hasNewKeys) {
console.log(`${langCode}: Keys already present, skipping`);
continue;
}
// Build the new keys string
const newKeysString = Object.entries(langTranslations)
.map(([key, value]) => `\t${key}: "${value}",`)
.join('\n');
// Insert the new keys
const newContent = beforeClosing + ',\n' + newKeysString + '\n};';
fs.writeFileSync(filePath, newContent, 'utf8');
console.log(`${langCode}: Added remote URL translation keys`);
} else {
console.error(`File not found: ${filePath}`);
}
}
console.log('\n🎉 Finished adding remote URL translation keys to all language files!');

View file

@ -106,5 +106,9 @@ export const ar = {
copyRawMarkdown: "نسخ النص الخام (Markdown)",
copyHTML: "نسخ HTML",
aiModalNotAvailable: "نافذة الذكاء الاصطناعي غير متاحة في هذا السياق",
useCustomAPI: "استخدم نقطة نهاية مخصصة متوافقة مع OpenAI.",
useCustomAPI: "استخدم نقطة نهاية مخصصة متوافقة مع OpenAI.",,
remoteUrlTab: "🌐 رابط بعيد",
remoteUrlInfo: "أدخل رابط لجلبه وترجمته:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 جلب وترجمة",
};

View file

@ -106,5 +106,9 @@ export const bn = {
copyRawMarkdown: "কাঁচা টেক্সট কপি করুন (Markdown)",
copyHTML: "HTML কপি করুন",
aiModalNotAvailable: "AI মডেল এই প্রসঙ্গে উপলব্ধ নয়",
useCustomAPI: "একটি কাস্টম OpenAI সামঞ্জস্যপূর্ণ এন্ডপয়েন্ট ব্যবহার করুন।",
useCustomAPI: "একটি কাস্টম OpenAI সামঞ্জস্যপূর্ণ এন্ডপয়েন্ট ব্যবহার করুন।",,
remoteUrlTab: "🌐 দূরবর্তী URL",
remoteUrlInfo: "একটি URL প্রবেশ করান যা আনতে এবং অনুবাদ করতে:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 আনুন এবং অনুবাদ করুন",
};

View file

@ -106,5 +106,9 @@ export const de = {
copyRawMarkdown: "Rohen Text kopieren (Markdown)",
copyHTML: "HTML kopieren",
aiModalNotAvailable: "KI-Modal ist in diesem Kontext nicht verfügbar",
useCustomAPI: "Verwenden Sie einen benutzerdefinierten OpenAI-kompatiblen Endpunkt.",
useCustomAPI: "Verwenden Sie einen benutzerdefinierten OpenAI-kompatiblen Endpunkt.",,
remoteUrlTab: "🌐 Externe URL",
remoteUrlInfo: "Geben Sie eine URL ein, um sie abzurufen und zu übersetzen:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Abrufen & Übersetzen",
};

View file

@ -77,6 +77,7 @@ export const en = {
// Translation modal tabs
translatePageTab: "📄 Translate Page",
customTextTab: "✏️ Custom Text",
remoteUrlTab: "🌐 Remote URL",
// TTS modal components
speedLabel: "Speed:",
@ -88,6 +89,9 @@ export const en = {
translateButton: "🌐 Translate",
translatePageButton: "🌐 Translate Page",
translateTextButton: "🌐 Translate Text",
remoteUrlInfo: "Enter a URL to fetch and translate:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Fetch & Translate",
processingText: "Processing...",
processingTokens: "🤖 Processing {tokens} tokens...",

View file

@ -77,6 +77,7 @@ export const es = {
// Translation modal tabs
translatePageTab: "📄 Traducir Página",
customTextTab: "✏️ Texto Personalizado",
remoteUrlTab: "🌐 URL Remota",
// TTS modal components
speedLabel: "Velocidad:",
@ -88,6 +89,9 @@ export const es = {
translateButton: "🌐 Traducir",
translatePageButton: "🌐 Traducir Página",
translateTextButton: "🌐 Traducir Texto",
remoteUrlInfo: "Ingresa una URL para obtener y traducir:",
remoteUrlPlaceholder: "https://ejemplo.com",
fetchAndTranslate: "🌐 Obtener y Traducir",
processingText: "Procesando...",
// Button labels for UI

View file

@ -106,5 +106,9 @@ export const fr = {
pleaseSelectFile: "Veuillez d'abord sélectionner un fichier !",
pleaseEnterTranslateText: "Veuillez entrer du texte à traduire !",
aiModalNotAvailable: "Le modal IA n'est pas disponible dans ce contexte",
useCustomAPI: "Utiliser un point de terminaison personnalisé compatible OpenAI.",
useCustomAPI: "Utiliser un point de terminaison personnalisé compatible OpenAI.",,
remoteUrlTab: "🌐 URL Distante",
remoteUrlInfo: "Entrez une URL à récupérer et traduire:",
remoteUrlPlaceholder: "https://exemple.com",
fetchAndTranslate: "🌐 Récupérer et Traduire",
};

View file

@ -105,5 +105,9 @@ export const hi = {
copyRawMarkdown: "कच्चा टेक्स्ट कॉपी करें (Markdown)",
copyHTML: "HTML कॉपी करें",
aiModalNotAvailable: "AI मॉडल इस संदर्भ में उपलब्ध नहीं है",
useCustomAPI: "एक कस्टम OpenAI संगत एंडपॉइंट का उपयोग करें।",
useCustomAPI: "एक कस्टम OpenAI संगत एंडपॉइंट का उपयोग करें।",,
remoteUrlTab: "🌐 दूरस्थ URL",
remoteUrlInfo: "लाने और अनुवाद करने के लिए एक URL दर्ज करें:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 लाएं और अनुवाद करें",
};

View file

@ -106,5 +106,9 @@ export const id = {
copyRawMarkdown: "Salin teks mentah (Markdown)",
copyHTML: "Salin HTML",
aiModalNotAvailable: "Modal AI tidak tersedia dalam konteks ini",
useCustomAPI: "Gunakan endpoint kustom yang kompatibel dengan OpenAI.",
useCustomAPI: "Gunakan endpoint kustom yang kompatibel dengan OpenAI.",,
remoteUrlTab: "🌐 URL Jarak Jauh",
remoteUrlInfo: "Masukkan URL untuk diambil dan diterjemahkan:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Ambil & Terjemahkan",
};

View file

@ -106,5 +106,9 @@ export const ja = {
copyRawMarkdown: "生テキストをコピー (Markdown)",
copyHTML: "HTMLをコピー",
aiModalNotAvailable: "AIモーダルはこのコンテキストでは利用できません",
useCustomAPI: "カスタムのOpenAI互換エンドポイントを使用する。",
useCustomAPI: "カスタムのOpenAI互換エンドポイントを使用する。",,
remoteUrlTab: "🌐 リモートURL",
remoteUrlInfo: "取得して翻訳するURLを入力してください:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 取得して翻訳",
};

View file

@ -106,5 +106,9 @@ export const ko = {
copyRawMarkdown: "원시 텍스트 복사 (Markdown)",
copyHTML: "HTML 복사",
aiModalNotAvailable: "AI 모달이 이 컨텍스트에서 사용할 수 없습니다",
useCustomAPI: "사용자 정의 OpenAI 호환 엔드포인트를 사용하세요.",
useCustomAPI: "사용자 정의 OpenAI 호환 엔드포인트를 사용하세요.",,
remoteUrlTab: "🌐 원격 URL",
remoteUrlInfo: "가져와서 번역할 URL을 입력하세요:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 가져와서 번역",
};

View file

@ -106,5 +106,9 @@ export const mr = {
copyRawMarkdown: "कच्चा मजकूर कॉपी करा (Markdown)",
copyHTML: "HTML कॉपी करा",
aiModalNotAvailable: "AI मॉडल या संदर्भात उपलब्ध नाही",
useCustomAPI: "एक सानुकूल OpenAI सुसंगत एंडपॉइंट वापरा।",
useCustomAPI: "एक सानुकूल OpenAI सुसंगत एंडपॉइंट वापरा।",,
remoteUrlTab: "🌐 दूरस्थ URL",
remoteUrlInfo: "आणण्यासाठी आणि भाषांतर करण्यासाठी URL प्रविष्ट करा:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 आणा आणि भाषांतर करा",
};

View file

@ -106,5 +106,9 @@ export const pt = {
copyRawMarkdown: "Copiar texto bruto (Markdown)",
copyHTML: "Copiar HTML",
aiModalNotAvailable: "Modal de IA não disponível neste contexto",
useCustomAPI: "Use um endpoint personalizado compatível com OpenAI.",
useCustomAPI: "Use um endpoint personalizado compatível com OpenAI.",,
remoteUrlTab: "🌐 URL Remota",
remoteUrlInfo: "Digite uma URL para buscar e traduzir:",
remoteUrlPlaceholder: "https://exemplo.com",
fetchAndTranslate: "🌐 Buscar e Traduzir",
};

View file

@ -106,5 +106,9 @@ export const ru = {
copyRawMarkdown: "Копировать исходный текст (Markdown)",
copyHTML: "Копировать HTML",
aiModalNotAvailable: "Модальное окно ИИ недоступно в этом контексте",
useCustomAPI: "Используйте пользовательскую конечную точку, совместимую с OpenAI.",
useCustomAPI: "Используйте пользовательскую конечную точку, совместимую с OpenAI.",,
remoteUrlTab: "🌐 Удаленный URL",
remoteUrlInfo: "Введите URL для загрузки и перевода:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Загрузить и Перевести",
};

View file

@ -106,5 +106,9 @@ export const sw = {
copyRawMarkdown: "Nakili maandishi ghafi (Markdown)",
copyHTML: "Nakili HTML",
aiModalNotAvailable: "Modal ya AI haipatikani katika muktadha huu",
useCustomAPI: "Tumia mwisho wa desturi unaofanana na OpenAI.",
useCustomAPI: "Tumia mwisho wa desturi unaofanana na OpenAI.",,
remoteUrlTab: "🌐 URL ya Mbali",
remoteUrlInfo: "Ingiza URL ya kuleta na kutafsiri:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Leta na Tafsiri",
};

View file

@ -106,5 +106,9 @@ export const te = {
copyRawMarkdown: "ముడి వచనాన్ని కాపీ చేయండి (Markdown)",
copyHTML: "HTML కాపీ చేయండి",
aiModalNotAvailable: "AI మోడల్ ఈ సందర్భంలో అందుబాటులో లేదు",
useCustomAPI: "కస్టమ్ OpenAI అనుకూల ఎండ్‌పాయింట్‌ను ఉపయోగించండి.",
useCustomAPI: "కస్టమ్ OpenAI అనుకూల ఎండ్‌పాయింట్‌ను ఉపయోగించండి.",,
remoteUrlTab: "🌐 రిమోట్ URL",
remoteUrlInfo: "తీసుకురావడానికి మరియు అనువదించడానికి URL ని నమోదు చేయండి:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 తీసుకురండి & అనువదించండి",
};

View file

@ -106,5 +106,9 @@ export const tr = {
copyRawMarkdown: "Ham metni kopyala (Markdown)",
copyHTML: "HTML kopyala",
aiModalNotAvailable: "AI modalı bu bağlamda mevcut değil",
useCustomAPI: "Özel bir OpenAI uyumlu uç nokta kullanın.",
useCustomAPI: "Özel bir OpenAI uyumlu uç nokta kullanın.",,
remoteUrlTab: "🌐 Uzak URL",
remoteUrlInfo: "Getirmek ve çevirmek için bir URL girin:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 Getir ve Çevir",
};

View file

@ -106,5 +106,9 @@ export const ur = {
copyRawMarkdown: "خام متن کاپی کریں (Markdown)",
copyHTML: "HTML کاپی کریں",
aiModalNotAvailable: "AI ماڈل اس سیاق میں دستیاب نہیں ہے",
useCustomAPI: "ایک کسٹم OpenAI مطابقت پذیر اینڈ پوائنٹ استعمال کریں۔",
useCustomAPI: "ایک کسٹم OpenAI مطابقت پذیر اینڈ پوائنٹ استعمال کریں۔",,
remoteUrlTab: "🌐 ریموٹ URL",
remoteUrlInfo: "لانے اور ترجمہ کرنے کے لیے URL داخل کریں:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 لائیں اور ترجمہ کریں",
};

View file

@ -104,5 +104,9 @@ export const zhTw = {
copyRawMarkdown: "複製原始文字 (Markdown)",
copyHTML: "複製 HTML",
aiModalNotAvailable: "AI對話框在此上下文中不可用",
useCustomAPI: "使用自訂的OpenAI相容端點。",
useCustomAPI: "使用自訂的OpenAI相容端點。",,
remoteUrlTab: "🌐 遠端網址",
remoteUrlInfo: "輸入要擷取和翻譯的網址:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 擷取並翻譯",
};

View file

@ -104,5 +104,9 @@ export const zh = {
copyRawMarkdown: "复制原始文本 (Markdown)",
copyHTML: "复制 HTML",
aiModalNotAvailable: "AI模态框在此上下文中不可用",
useCustomAPI: "使用自定义的OpenAI兼容端点。",
useCustomAPI: "使用自定义的OpenAI兼容端点。",,
remoteUrlTab: "🌐 远程网址",
remoteUrlInfo: "输入要获取和翻译的网址:",
remoteUrlPlaceholder: "https://example.com",
fetchAndTranslate: "🌐 获取并翻译",
};

View file

@ -4,6 +4,55 @@ import { getUIText } from "./ui-translations.js";
import { extractWebpageContent } from "./content.js";
import { translateCurrentPage, translateText, SUPPORTED_LANGUAGES } from "./translation.js";
// Function to fetch and extract content from a remote URL
async function fetchRemotePageContent(url) {
try {
// Use a CORS proxy or fetch API with proper headers
// For now, we'll use a simple approach - in production you might want a proper CORS proxy
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
}
});
if (!response.ok) {
throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`);
}
const htmlContent = await response.text();
// Create a temporary DOM to extract content
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
// Extract meaningful content (similar to extractPageContent)
const title = doc.title || '';
const metaDescription = doc.querySelector('meta[name="description"]')?.content || '';
// Remove script and style elements
const scripts = doc.querySelectorAll('script, style, noscript');
scripts.forEach(el => el.remove());
// Get text content from body
const bodyContent = doc.body ? doc.body.innerText : doc.documentElement.innerText;
// Combine title, description, and body content
const extractedContent = [title, metaDescription, bodyContent]
.filter(content => content.trim())
.join('\n\n');
return extractedContent;
} catch (error) {
// If direct fetch fails due to CORS, suggest alternatives
if (error.name === 'TypeError' && error.message.includes('CORS')) {
throw new Error(`Cannot fetch "${url}" due to CORS restrictions. The website doesn't allow cross-origin requests.`);
}
throw new Error(`Failed to fetch remote content: ${error.message}`);
}
}
export function openTranslateModal() {
// Load appropriate CSS based on whether we're using PicoCSS or built-in styles
// Detect if PicoCSS is actually present on the page
@ -80,8 +129,20 @@ export function openTranslateModal() {
customTabLink.setAttribute("data-i18n", "customTextTab");
customTabItem.appendChild(customTabLink);
// Remote URL tab
const remoteTabItem = document.createElement("li");
const remoteTabLink = document.createElement("a");
remoteTabLink.setAttribute("role", "tab");
remoteTabLink.setAttribute("aria-selected", "false");
remoteTabLink.setAttribute("aria-controls", "remote-panel");
remoteTabLink.href = "#remote-panel";
remoteTabLink.textContent = getUIText("remoteUrlTab");
remoteTabLink.setAttribute("data-i18n", "remoteUrlTab");
remoteTabItem.appendChild(remoteTabLink);
tabList.appendChild(pageTabItem);
tabList.appendChild(customTabItem);
tabList.appendChild(remoteTabItem);
tabNav.appendChild(tabList);
article.appendChild(tabNav);
@ -97,6 +158,12 @@ export function openTranslateModal() {
customContent.setAttribute("aria-labelledby", "custom-tab");
customContent.className = "translate-tab-content";
const remoteContent = document.createElement("div");
remoteContent.id = "remote-panel";
remoteContent.setAttribute("role", "tabpanel");
remoteContent.setAttribute("aria-labelledby", "remote-tab");
remoteContent.className = "translate-tab-content";
// Page translation content
const pageInfo = document.createElement("p");
pageInfo.textContent = getUIText("translatePageInfo");
@ -109,8 +176,22 @@ export function openTranslateModal() {
textArea.placeholder = getUIText("translatePlaceholder");
customContent.appendChild(textArea);
// Remote URL content
const remoteInfo = document.createElement("p");
remoteInfo.textContent = getUIText("remoteUrlInfo");
remoteInfo.setAttribute("data-i18n", "remoteUrlInfo");
remoteContent.appendChild(remoteInfo);
const urlInput = document.createElement("input");
urlInput.type = "url";
urlInput.className = "translate-url-input";
urlInput.placeholder = getUIText("remoteUrlPlaceholder");
urlInput.setAttribute("data-i18n-placeholder", "remoteUrlPlaceholder");
remoteContent.appendChild(urlInput);
article.appendChild(pageContent);
article.appendChild(customContent);
article.appendChild(remoteContent);
// Language selection
const languageContainer = document.createElement("div");
@ -158,36 +239,49 @@ export function openTranslateModal() {
article.appendChild(translateButton);
// Tab switching using PicoCSS tab navigation
let isPageMode = true;
let activeTab = "page"; // "page", "custom", or "remote"
function switchToTab(targetTab) {
// Reset all tabs
pageTabLink.setAttribute("aria-selected", "false");
customTabLink.setAttribute("aria-selected", "false");
remoteTabLink.setAttribute("aria-selected", "false");
pageContent.classList.remove("active");
customContent.classList.remove("active");
remoteContent.classList.remove("active");
// Activate target tab
if (targetTab === "page") {
pageTabLink.setAttribute("aria-selected", "true");
pageContent.classList.add("active");
translateButton.textContent = getUIText("translatePageButton");
} else if (targetTab === "custom") {
customTabLink.setAttribute("aria-selected", "true");
customContent.classList.add("active");
translateButton.textContent = getUIText("translateTextButton");
} else if (targetTab === "remote") {
remoteTabLink.setAttribute("aria-selected", "true");
remoteContent.classList.add("active");
translateButton.textContent = getUIText("fetchAndTranslate");
}
activeTab = targetTab;
}
pageTabLink.onclick = (e) => {
e.preventDefault();
if (!isPageMode) {
isPageMode = true;
pageTabLink.setAttribute("aria-selected", "true");
customTabLink.setAttribute("aria-selected", "false");
// Visual styles are now handled by CSS classes
pageContent.classList.add("active");
customContent.classList.remove("active");
translateButton.textContent = getUIText("translatePageButton");
}
switchToTab("page");
};
customTabLink.onclick = (e) => {
e.preventDefault();
if (isPageMode) {
isPageMode = false;
customTabLink.setAttribute("aria-selected", "true");
pageTabLink.setAttribute("aria-selected", "false");
// Visual styles are now handled by CSS classes
customContent.classList.add("active");
pageContent.classList.remove("active");
translateButton.textContent = getUIText("translateTextButton");
}
switchToTab("custom");
};
remoteTabLink.onclick = (e) => {
e.preventDefault();
switchToTab("remote");
};
// Translation logic
@ -207,9 +301,19 @@ export function openTranslateModal() {
const { getSelectedModel } = await import("./models.js");
const { SUPPORTED_LANGUAGES } = await import("./translation.js");
const rawContent = isPageMode
? extractPageContent()
: textArea.value.trim();
let rawContent;
if (activeTab === "page") {
rawContent = extractPageContent();
} else if (activeTab === "custom") {
rawContent = textArea.value.trim();
} else if (activeTab === "remote") {
const url = urlInput.value.trim();
if (!url) {
throw new Error("Please enter a URL to translate");
}
// Fetch and extract content from remote URL
rawContent = await fetchRemotePageContent(url);
}
// Limit content length to avoid overwhelming the AI
const maxLength = 50000;
@ -350,7 +454,19 @@ ${preservedText}`;
previewWindow.document.write(htmlWithBase);
previewWindow.document.close();
previewWindow.document.title = `Translation Preview (${targetLanguageName}) - AI Enabled`;
// Set title based on translation source
let windowTitle;
if (activeTab === "remote") {
const urlInput = document.querySelector('.translate-url-input');
const sourceUrl = urlInput ? urlInput.value.trim() : 'Remote URL';
windowTitle = `${sourceUrl} - translated into ${targetLanguageName}`;
} else if (activeTab === "custom") {
windowTitle = `Custom Text - translated into ${targetLanguageName}`;
} else {
windowTitle = `${document.title} - translated into ${targetLanguageName}`;
}
previewWindow.document.title = windowTitle;
// Add a notice about AI functionality only in preview window (wait for body to be available)
const addNotice = () => {