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:
Russell Ballestrini 2025-07-01 12:35:09 -04:00
parent 061a04f1fe
commit 0fc73e4e33
3 changed files with 517 additions and 4 deletions

150
src/translation.js Normal file
View 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
View file

@ -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');