Add: Omni-directional translation feature with code block preservation
- Add translation.js with 20+ language support - Implement code block, URL, and HTML tag preservation during translation - Add translation modal with page translation and custom text modes - Integrate translation button in all interfaces - Add translation demo section with live examples - Support Russell's approach of preserving technical content during translation - Enable translation of current page content or custom text
This commit is contained in:
parent
061a04f1fe
commit
0fc73e4e33
3 changed files with 517 additions and 4 deletions
150
src/translation.js
Normal file
150
src/translation.js
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// Translation functionality with code block preservation
|
||||
import { sendMessage } from './chat.js';
|
||||
|
||||
// Top 20 languages commonly supported by translation models
|
||||
export const SUPPORTED_LANGUAGES = {
|
||||
'en': 'English',
|
||||
'es': 'Spanish',
|
||||
'fr': 'French',
|
||||
'de': 'German',
|
||||
'it': 'Italian',
|
||||
'pt': 'Portuguese',
|
||||
'ru': 'Russian',
|
||||
'ja': 'Japanese',
|
||||
'ko': 'Korean',
|
||||
'zh': 'Chinese (Simplified)',
|
||||
'ar': 'Arabic',
|
||||
'hi': 'Hindi',
|
||||
'tr': 'Turkish',
|
||||
'pl': 'Polish',
|
||||
'nl': 'Dutch',
|
||||
'sv': 'Swedish',
|
||||
'da': 'Danish',
|
||||
'no': 'Norwegian',
|
||||
'fi': 'Finnish',
|
||||
'he': 'Hebrew'
|
||||
};
|
||||
|
||||
// Preserve code blocks, URLs, and special formatting during translation
|
||||
export function preserveSpecialContent(text) {
|
||||
const preservations = [];
|
||||
let preservedText = text;
|
||||
|
||||
// Preserve code blocks (```code```, `inline`, <code>)
|
||||
const patterns = [
|
||||
// Triple backtick code blocks
|
||||
/```[\s\S]*?```/g,
|
||||
// Inline backticks
|
||||
/`[^`\n]+`/g,
|
||||
// HTML code tags
|
||||
/<code[\s\S]*?<\/code>/gi,
|
||||
// HTML pre tags
|
||||
/<pre[\s\S]*?<\/pre>/gi,
|
||||
// URLs
|
||||
/https?:\/\/[^\s<>"{}|\\^`\[\]]+/g,
|
||||
// Email addresses
|
||||
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
||||
// HTML tags
|
||||
/<[^>]+>/g
|
||||
];
|
||||
|
||||
patterns.forEach(pattern => {
|
||||
preservedText = preservedText.replace(pattern, (match) => {
|
||||
const placeholder = `__PRESERVE_${preservations.length}__`;
|
||||
preservations.push(match);
|
||||
return placeholder;
|
||||
});
|
||||
});
|
||||
|
||||
return { preservedText, preservations };
|
||||
}
|
||||
|
||||
// Restore preserved content after translation
|
||||
export function restoreSpecialContent(translatedText, preservations) {
|
||||
let restoredText = translatedText;
|
||||
|
||||
preservations.forEach((original, index) => {
|
||||
const placeholder = `__PRESERVE_${index}__`;
|
||||
restoredText = restoredText.replace(placeholder, original);
|
||||
});
|
||||
|
||||
return restoredText;
|
||||
}
|
||||
|
||||
// Translate text using Hermes AI
|
||||
export async function translateText(text, targetLanguage) {
|
||||
const { preservedText, preservations } = preserveSpecialContent(text);
|
||||
|
||||
const prompt = `Translate the following text to ${SUPPORTED_LANGUAGES[targetLanguage]}.
|
||||
|
||||
IMPORTANT: Do NOT translate any content that appears as __PRESERVE_X__ (where X is a number). Keep these placeholders exactly as they are.
|
||||
|
||||
Text to translate:
|
||||
${preservedText}
|
||||
|
||||
Please provide only the translation without any explanations or additional text.`;
|
||||
|
||||
let translatedText = '';
|
||||
|
||||
try {
|
||||
for await (const chunk of sendMessage(prompt)) {
|
||||
translatedText += chunk;
|
||||
}
|
||||
|
||||
// Clean up the response (remove any extra explanations)
|
||||
translatedText = translatedText.trim();
|
||||
|
||||
// Restore preserved content
|
||||
return restoreSpecialContent(translatedText, preservations);
|
||||
} catch (error) {
|
||||
console.error('Translation error:', error);
|
||||
throw new Error('Translation failed. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
// Extract page content for translation
|
||||
export function extractPageContent() {
|
||||
// Try to get main content from common selectors
|
||||
const contentSelectors = [
|
||||
'main',
|
||||
'article',
|
||||
'.content',
|
||||
'.post-content',
|
||||
'.entry-content',
|
||||
'#content',
|
||||
'body'
|
||||
];
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const selector of contentSelectors) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
// Get text content but preserve some structure
|
||||
content = element.innerText || element.textContent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up excessive whitespace
|
||||
content = content.replace(/\n\s*\n\s*\n/g, '\n\n').trim();
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Translate current page content
|
||||
export async function translateCurrentPage(targetLanguage) {
|
||||
const pageContent = extractPageContent();
|
||||
|
||||
if (!pageContent || pageContent.length < 10) {
|
||||
throw new Error('Unable to extract meaningful content from this page.');
|
||||
}
|
||||
|
||||
// Limit content length to avoid overwhelming the AI
|
||||
const maxLength = 8000;
|
||||
const contentToTranslate = pageContent.length > maxLength
|
||||
? pageContent.substring(0, maxLength) + '...'
|
||||
: pageContent;
|
||||
|
||||
return await translateText(contentToTranslate, targetLanguage);
|
||||
}
|
||||
262
src/ui.js
262
src/ui.js
|
|
@ -8,6 +8,7 @@ import { readPageWithHermes } from './page-reader.js';
|
|||
import { handleUserInput, chatHistory } from './chat.js';
|
||||
import { getPageSpecificKey } from './storage.js';
|
||||
import { fetchModelsFromEndpoints } from './models.js';
|
||||
import { SUPPORTED_LANGUAGES, translateText, translateCurrentPage } from './translation.js';
|
||||
|
||||
// Configuration flags
|
||||
const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false;
|
||||
|
|
@ -57,10 +58,12 @@ export function createFullInterface(container) {
|
|||
|
||||
const readBtn = createButton('📖 Read Page', () => readPageWithHermes());
|
||||
const ttsBtn = createButton('🔊 TTS Anything', () => openTTSModal());
|
||||
const translateBtn = createButton('🌐 Translate', () => openTranslateModal());
|
||||
const uploadBtn = createButton('📁 Upload File', () => document.querySelector('[data-uncloseai-file-input]')?.click());
|
||||
|
||||
controlsDiv.appendChild(readBtn);
|
||||
controlsDiv.appendChild(ttsBtn);
|
||||
controlsDiv.appendChild(translateBtn);
|
||||
controlsDiv.appendChild(uploadBtn);
|
||||
|
||||
// Hidden file input
|
||||
|
|
@ -1453,6 +1456,265 @@ export function openTTSModal() {
|
|||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// Open translation modal
|
||||
export function openTranslateModal() {
|
||||
// Check if Hermes modal is open to set appropriate z-index
|
||||
const hermesModal = document.getElementById('hermes-modal');
|
||||
const zIndex = hermesModal ? '2001' : '1001';
|
||||
|
||||
const modal = document.createElement('dialog');
|
||||
modal.open = true;
|
||||
modal.style.zIndex = zIndex;
|
||||
modal.style.position = 'fixed';
|
||||
|
||||
if (USE_CUSTOM_STYLING) {
|
||||
modal.style.cssText += `
|
||||
max-width: 720px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
background: white;
|
||||
`;
|
||||
}
|
||||
|
||||
const article = document.createElement('article');
|
||||
modal.appendChild(article);
|
||||
|
||||
const header = document.createElement('header');
|
||||
if (USE_CUSTOM_STYLING) {
|
||||
header.style.cssText = `
|
||||
background: linear-gradient(45deg, #4CAF50 0%, #45a049 100%);
|
||||
color: white;
|
||||
padding: 16px 20px;
|
||||
margin: -1em -1em 1em -1em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
} else {
|
||||
header.style.cssText = `
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
margin: -1em -1em 1em -1em;
|
||||
`;
|
||||
}
|
||||
article.appendChild(header);
|
||||
|
||||
const h1 = document.createElement('h1');
|
||||
h1.textContent = '🌐 Omni-Directional Translation';
|
||||
if (USE_CUSTOM_STYLING) {
|
||||
h1.style.cssText = `
|
||||
margin: 0;
|
||||
font-family: 'ChunkFiveRegular', monospace;
|
||||
font-size: 20px;
|
||||
`;
|
||||
}
|
||||
header.appendChild(h1);
|
||||
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.textContent = 'X';
|
||||
if (USE_CUSTOM_STYLING) {
|
||||
closeButton.style.cssText = `
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
} else {
|
||||
closeButton.style.cssText = `
|
||||
background: var(--background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--color);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.2s;
|
||||
`;
|
||||
closeButton.onmouseenter = () => closeButton.style.opacity = '0.7';
|
||||
closeButton.onmouseleave = () => closeButton.style.opacity = '1';
|
||||
}
|
||||
closeButton.onclick = () => document.body.removeChild(modal);
|
||||
header.appendChild(closeButton);
|
||||
|
||||
// Translation mode tabs
|
||||
const modeContainer = document.createElement('div');
|
||||
modeContainer.style.cssText = 'display: flex; gap: 10px; margin-bottom: 20px;';
|
||||
|
||||
const pageTab = document.createElement('button');
|
||||
pageTab.textContent = '📄 Translate Page';
|
||||
pageTab.style.cssText = 'flex: 1; padding: 10px; border: 2px solid #4CAF50; border-radius: 6px; background: #4CAF50; color: white; cursor: pointer;';
|
||||
|
||||
const customTab = document.createElement('button');
|
||||
customTab.textContent = '✏️ Custom Text';
|
||||
customTab.style.cssText = 'flex: 1; padding: 10px; border: 2px solid #4CAF50; border-radius: 6px; background: white; color: #4CAF50; cursor: pointer;';
|
||||
|
||||
modeContainer.appendChild(pageTab);
|
||||
modeContainer.appendChild(customTab);
|
||||
article.appendChild(modeContainer);
|
||||
|
||||
// Content areas
|
||||
const pageContent = document.createElement('div');
|
||||
const customContent = document.createElement('div');
|
||||
customContent.style.display = 'none';
|
||||
|
||||
// Page translation content
|
||||
const pageInfo = document.createElement('p');
|
||||
pageInfo.textContent = 'Translate the current page content into your chosen language:';
|
||||
pageContent.appendChild(pageInfo);
|
||||
|
||||
// Custom text content
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.style.width = '100%';
|
||||
textArea.style.height = '150px';
|
||||
textArea.placeholder = 'Enter any text to translate...';
|
||||
customContent.appendChild(textArea);
|
||||
|
||||
article.appendChild(pageContent);
|
||||
article.appendChild(customContent);
|
||||
|
||||
// Language selection
|
||||
const languageContainer = document.createElement('div');
|
||||
languageContainer.style.cssText = 'margin: 20px 0;';
|
||||
|
||||
const languageLabel = document.createElement('label');
|
||||
languageLabel.textContent = 'Translate to: ';
|
||||
languageLabel.style.display = 'block';
|
||||
languageLabel.style.marginBottom = '8px';
|
||||
|
||||
const languageSelect = document.createElement('select');
|
||||
languageSelect.style.cssText = 'width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;';
|
||||
|
||||
Object.entries(SUPPORTED_LANGUAGES).forEach(([code, name]) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = code;
|
||||
option.textContent = name;
|
||||
languageSelect.appendChild(option);
|
||||
});
|
||||
|
||||
languageContainer.appendChild(languageLabel);
|
||||
languageContainer.appendChild(languageSelect);
|
||||
article.appendChild(languageContainer);
|
||||
|
||||
// Translate button
|
||||
const translateButton = document.createElement('button');
|
||||
translateButton.textContent = '🌐 Translate';
|
||||
if (USE_CUSTOM_STYLING) {
|
||||
translateButton.style.cssText = `
|
||||
padding: 12px 24px;
|
||||
background: linear-gradient(45deg, #4CAF50 0%, #45a049 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
margin: 16px 10px 0 0;
|
||||
width: 100%;
|
||||
`;
|
||||
} else {
|
||||
translateButton.style.cssText = 'width: 100%; padding: 12px; margin: 16px 0;';
|
||||
}
|
||||
|
||||
// Result area
|
||||
const resultContainer = document.createElement('div');
|
||||
resultContainer.style.cssText = 'margin-top: 20px; display: none;';
|
||||
|
||||
const resultLabel = document.createElement('h3');
|
||||
resultLabel.textContent = 'Translation:';
|
||||
|
||||
const resultArea = document.createElement('div');
|
||||
resultArea.style.cssText = 'border: 1px solid #ccc; border-radius: 4px; padding: 15px; max-height: 400px; overflow-y: auto; background: #f9f9f9; white-space: pre-wrap;';
|
||||
|
||||
resultContainer.appendChild(resultLabel);
|
||||
resultContainer.appendChild(resultArea);
|
||||
article.appendChild(resultContainer);
|
||||
|
||||
article.appendChild(translateButton);
|
||||
|
||||
// Tab switching
|
||||
let isPageMode = true;
|
||||
|
||||
pageTab.onclick = () => {
|
||||
if (!isPageMode) {
|
||||
isPageMode = true;
|
||||
pageTab.style.background = '#4CAF50';
|
||||
pageTab.style.color = 'white';
|
||||
customTab.style.background = 'white';
|
||||
customTab.style.color = '#4CAF50';
|
||||
pageContent.style.display = 'block';
|
||||
customContent.style.display = 'none';
|
||||
translateButton.textContent = '🌐 Translate Page';
|
||||
}
|
||||
};
|
||||
|
||||
customTab.onclick = () => {
|
||||
if (isPageMode) {
|
||||
isPageMode = false;
|
||||
customTab.style.background = '#4CAF50';
|
||||
customTab.style.color = 'white';
|
||||
pageTab.style.background = 'white';
|
||||
pageTab.style.color = '#4CAF50';
|
||||
customContent.style.display = 'block';
|
||||
pageContent.style.display = 'none';
|
||||
translateButton.textContent = '🌐 Translate Text';
|
||||
}
|
||||
};
|
||||
|
||||
// Translation logic
|
||||
translateButton.onclick = async () => {
|
||||
const targetLanguage = languageSelect.value;
|
||||
const targetLanguageName = SUPPORTED_LANGUAGES[targetLanguage];
|
||||
|
||||
try {
|
||||
translateButton.textContent = 'Processing...';
|
||||
translateButton.disabled = true;
|
||||
resultContainer.style.display = 'none';
|
||||
|
||||
let translatedText;
|
||||
|
||||
if (isPageMode) {
|
||||
translatedText = await translateCurrentPage(targetLanguage);
|
||||
} else {
|
||||
const customText = textArea.value.trim();
|
||||
if (!customText) {
|
||||
alert('Please enter some text to translate!');
|
||||
return;
|
||||
}
|
||||
translatedText = await translateText(customText, targetLanguage);
|
||||
}
|
||||
|
||||
resultLabel.textContent = `Translation (${targetLanguageName}):`;
|
||||
resultArea.textContent = translatedText;
|
||||
resultContainer.style.display = 'block';
|
||||
|
||||
// Scroll to result
|
||||
resultContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
|
||||
} catch (error) {
|
||||
alert(`Translation failed: ${error.message}`);
|
||||
} finally {
|
||||
translateButton.textContent = isPageMode ? '🌐 Translate Page' : '🌐 Translate Text';
|
||||
translateButton.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// Initialize the system
|
||||
export function initializeSystem() {
|
||||
console.log('uncloseai.js: Initializing system');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue