feat(ui): complete core translation infrastructure

- Update batman toolbelt action buttons to use getUIText()
- Translate all modal headers (AI Assistant, TTS, Translate)
- Convert all input placeholders to use translations
- Update critical alert messages with parameter substitution
- Add comprehensive translation keys for UI elements
- Translate Hermes intro message with proper HTML formatting
- Support both English and Chinese translations for all new keys
- Maintain fallback to English for missing translations
- Fix biome style issues (template literals, optional chains)
This commit is contained in:
Russell Ballestrini 2025-07-03 11:41:02 -04:00
parent eee33986cf
commit c263cb9d56
3 changed files with 100 additions and 51 deletions

View file

@ -169,11 +169,11 @@ export class SimpleBPETokenizer {
// Check token type and split if needed
if (this.isWord(trimmed)) {
return this.splitWord(trimmed);
} else if (this.isNumber(trimmed)) {
return this.splitNumber(trimmed);
} else {
return [token]; // Punctuation, symbols, etc.
}
if (this.isNumber(trimmed)) {
return this.splitNumber(trimmed);
}
return [token]; // Punctuation, symbols, etc.
}
isWord(token) {
@ -210,12 +210,11 @@ export class SimpleBPETokenizer {
if (remaining.length <= 4) {
tokens.push(remaining);
break;
} else {
// Take first 3-4 characters
const chunkSize = Math.min(4, Math.ceil(remaining.length / 2));
tokens.push(remaining.substring(0, chunkSize));
remaining = remaining.substring(chunkSize);
}
// Take first 3-4 characters
const chunkSize = Math.min(4, Math.ceil(remaining.length / 2));
tokens.push(remaining.substring(0, chunkSize));
remaining = remaining.substring(chunkSize);
}
}

View file

@ -245,7 +245,7 @@ export async function translateText(text, targetLanguage) {
console.log(`Input tokens: ${tokenInfo.totalTokens}`);
console.log(`Text length: ${tokenInfo.textLength} characters`);
console.log(`Estimated processing time: ${estimatedSeconds} seconds`);
console.log(`Token breakdown:`, tokenInfo.breakdown);
console.log("Token breakdown:", tokenInfo.breakdown);
const { preservedText, preservations } = preserveSpecialContent(text);

132
src/ui.js
View file

@ -76,10 +76,9 @@ async function detectPageLanguage() {
if (/^[a-z]{2,3}$/.test(detectedLang)) {
console.log("AI detected language:", detectedLang);
return detectedLang;
} else {
console.warn("Invalid language code from AI:", detectedLang);
return fallbackLanguageDetection();
}
console.warn("Invalid language code from AI:", detectedLang);
return fallbackLanguageDetection();
} catch (error) {
console.warn("Language detection error:", error);
return fallbackLanguageDetection();
@ -126,6 +125,8 @@ const UI_TRANSLATIONS = {
refreshModels: "🔄 Refresh Models",
clearChat: "🗑️ Clear Chat",
fullChatRaw: "📋 Full Chat Raw",
fullChatHTML: "📄 Full Chat HTML",
fullPageRaw: "📝 Full Page Raw",
downloadChat: "💾 Download Chat",
// Chat UI
@ -133,21 +134,51 @@ const UI_TRANSLATIONS = {
typePlaceholder: "Type your message...",
clearChatConfirm: "Clear all conversation history?",
// Modal headers
aiAssistantTitle: "AI Assistant",
ttsModalTitle: "Text-to-Speech",
translateModalTitle: "Translate Page",
// System messages
pageContentPrefix: "Here's the content of the webpage:",
additionalContext:
"You are embedded on this page. When answering questions, consider the page content and context.",
// Notifications
// Notifications and alerts
languageChanged:
"Language set to {lang}. Refresh the page to see all interface elements in your preferred language.",
translationFailed: "Translation failed: {error}",
ttsFailed: "TTS failed: {error}",
fullChatCopied: "Full chat copied as raw markdown!",
fullChatHTMLCopied: "Full chat copied as HTML!",
fullPageCopied: "Full page copied as markdown!",
failedToCopy: "Failed to copy: {error}",
failedToCopyPage: "Failed to copy page: {error}",
failedToCopyCode: "Failed to copy code: {error}",
errorOpeningModal: "Error opening AI modal: {error}",
failedToReadPage: "Failed to read page: {error}",
languageDetectionFailed: "Language detection failed: {error}",
// Dropdown labels
currentPage: "Current page: {lang}",
translatingTo: "Translating...",
detectingLanguage: "🔍 Detecting...",
// Intro/header text
hermesIntro:
'presents nous research\'s <a href="https://nousresearch.com/hermes3/" target="_blank" style="color: rgba(255,255,255,0.9); text-decoration: underline;">hermes</a> large language model',
discussingPage: "You are discussing",
// TTS placeholder
ttsPlaceholder:
"text-to-speech: write any message & have the artificial intelligence speak it!",
translatePlaceholder: "Enter any text to translate...",
// More alerts and messages
pleaseEnterText: "Please enter some text first!",
pleaseSelectFile: "Please select a file first!",
pleaseEnterTranslateText: "Please enter some text to translate!",
aiModalNotAvailable: "AI modal not available in this context",
},
// Chinese (Simplified)
@ -164,19 +195,42 @@ const UI_TRANSLATIONS = {
refreshModels: "🔄 刷新模型",
clearChat: "🗑️ 清除聊天",
fullChatRaw: "📋 完整聊天记录",
fullChatHTML: "📄 完整聊天HTML",
fullPageRaw: "📝 完整页面原始",
downloadChat: "💾 下载聊天",
sendButton: "发送",
typePlaceholder: "输入您的消息...",
clearChatConfirm: "清除所有对话历史记录?",
aiAssistantTitle: "AI助手",
ttsModalTitle: "文本转语音",
translateModalTitle: "翻译页面",
pageContentPrefix: "以下是网页内容:",
additionalContext: "您已嵌入此页面。回答问题时,请考虑页面内容和上下文。",
languageChanged:
"语言设置为 {lang}。刷新页面以查看所有界面元素的首选语言。",
translationFailed: "翻译失败:{error}",
ttsFailed: "语音合成失败:{error}",
fullChatCopied: "完整聊天记录已复制为markdown!",
fullChatHTMLCopied: "完整聊天记录已复制为HTML!",
fullPageCopied: "完整页面已复制为markdown!",
failedToCopy: "复制失败:{error}",
failedToCopyPage: "复制页面失败:{error}",
failedToCopyCode: "复制代码失败:{error}",
errorOpeningModal: "打开AI模态框时出错:{error}",
failedToReadPage: "读取页面失败:{error}",
languageDetectionFailed: "语言检测失败:{error}",
currentPage: "当前页面:{lang}",
translatingTo: "翻译中...",
detectingLanguage: "🔍 检测中...",
hermesIntro:
'呈现nous research的<a href="https://nousresearch.com/hermes3/" target="_blank" style="color: rgba(255,255,255,0.9); text-decoration: underline;">hermes</a>大型语言模型',
discussingPage: "您正在讨论",
ttsPlaceholder: "文本转语音:写任何消息,让人工智能朗读出来!",
translatePlaceholder: "输入任何要翻译的文本...",
pleaseEnterText: "请先输入一些文本!",
pleaseSelectFile: "请先选择一个文件!",
pleaseEnterTranslateText: "请输入一些要翻译的文本!",
aiModalNotAvailable: "AI模态框在此上下文中不可用",
},
// Hindi
@ -783,8 +837,7 @@ function refreshUILanguage() {
function detectCurrentTheme() {
const isDark =
document.documentElement.getAttribute("data-theme") === "dark" ||
(window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches &&
(window.matchMedia?.("(prefers-color-scheme: dark)").matches &&
!document.documentElement.getAttribute("data-theme"));
return isDark ? "dark" : "light";
@ -926,7 +979,7 @@ function addCodeBlockCopyButtons(element) {
copyBtn.textContent = originalText;
}, 1000);
} catch (error) {
alert("Failed to copy code: " + error.message);
alert(getUIText("failedToCopyCode", { error: error.message }));
}
};
@ -1241,7 +1294,7 @@ export async function handleTTSFromElement(button) {
const text = textarea?.value?.trim();
if (!text) {
alert("Please enter some text first!");
alert(getUIText("pleaseEnterText"));
return;
}
@ -1271,7 +1324,7 @@ export async function handleTTSFromElement(button) {
resultDiv.appendChild(result.audio);
resultDiv.appendChild(audioControls);
} catch (error) {
resultDiv.innerHTML = "<strong>Error:</strong> " + error.message;
resultDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
} finally {
button.disabled = false;
button.textContent = "🔊 Convert to Speech";
@ -1284,7 +1337,7 @@ export async function handleUploadFromElement(button) {
const resultDiv = container.querySelector("[data-upload-result]");
if (!fileInput.files[0]) {
alert("Please select a file first!");
alert(getUIText("pleaseSelectFile"));
return;
}
@ -1302,7 +1355,7 @@ export async function handleUploadFromElement(button) {
fileInput.value = "";
} catch (error) {
hideProgressIndicator();
resultDiv.innerHTML = "<strong>Error:</strong> " + error.message;
resultDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
} finally {
button.disabled = false;
button.textContent = "📁 Upload & Analyze";
@ -1463,7 +1516,7 @@ export async function handleSmartTranslate(button) {
} catch (error) {
button.textContent = getUIText("smartTranslate");
button.disabled = false;
alert(`Language detection failed: ${error.message}`);
alert(getUIText("languageDetectionFailed", { error: error.message }));
}
}
@ -1672,11 +1725,11 @@ function setupFloatingButtonHandler(floatingButton) {
console.error(
"toggleUncloseaiEmbeddedModal function not available in any form",
);
alert("AI modal not available in this context");
alert(getUIText("aiModalNotAvailable"));
}
} catch (error) {
console.error("Error opening modal:", error);
alert("Error opening AI modal: " + error.message);
alert(getUIText("errorOpeningModal", { error: error.message }));
} finally {
// Restore button state
floatingButton.textContent = originalText;
@ -1774,8 +1827,8 @@ async function openUncloseaiEmbeddedModalNew() {
const pageTitle = document.title || window.location.hostname;
subtitle.innerHTML = `
<div style="font-size: 12px; opacity: 0.9; font-weight: normal; line-height: 1.3;">
<span style="font-family: 'ChunkFiveRegular', monospace;">uncloseai.</span> presents nous research's <a href="https://nousresearch.com/hermes3/" target="_blank" style="color: rgba(255,255,255,0.9); text-decoration: underline;">hermes</a> large language model<br>
<span style="opacity: 0.8;">You are discussing: ${pageTitle}</span>
<span style="font-family: 'ChunkFiveRegular', monospace;">uncloseai.</span> ${getUIText("hermesIntro")}<br>
<span style="opacity: 0.8;">${getUIText("discussingPage")}: ${pageTitle}</span>
</div>
`;
@ -1874,7 +1927,7 @@ async function openUncloseaiEmbeddedModalNew() {
// Model selection
const modelSection = document.createElement("div");
modelSection.style.cssText = `margin-bottom: 16px;`;
modelSection.style.cssText = "margin-bottom: 16px;";
const modelLabel = document.createElement("label");
modelLabel.textContent = getUIText("modelLabel");
@ -1973,7 +2026,7 @@ async function openUncloseaiEmbeddedModalNew() {
// Voice selection for TTS
const voiceSection = document.createElement("div");
voiceSection.style.cssText = `margin-bottom: 16px;`;
voiceSection.style.cssText = "margin-bottom: 16px;";
const voiceLabel = document.createElement("label");
voiceLabel.textContent = getUIText("voiceLabel");
@ -2015,7 +2068,7 @@ async function openUncloseaiEmbeddedModalNew() {
// Language preference section
const languageSection = document.createElement("div");
languageSection.style.cssText = `margin-bottom: 16px;`;
languageSection.style.cssText = "margin-bottom: 16px;";
const languageLabel = document.createElement("label");
languageLabel.textContent = getUIText("languageLabel");
@ -2074,7 +2127,7 @@ async function openUncloseaiEmbeddedModalNew() {
// Action buttons
const actionsSection = document.createElement("div");
actionsSection.style.cssText = `margin-bottom: 16px;`;
actionsSection.style.cssText = "margin-bottom: 16px;";
const actionsLabel = document.createElement("div");
actionsLabel.textContent = getUIText("quickActions");
@ -2374,7 +2427,7 @@ async function openUncloseaiEmbeddedModalNew() {
isReadingPage = false;
}
} catch (error) {
alert("Failed to read page: " + error.message);
alert(getUIText("failedToReadPage", { error: error.message }));
button.textContent = "📖 Read Page";
isReadingPage = false;
} finally {
@ -2512,12 +2565,12 @@ async function openUncloseaiEmbeddedModalNew() {
} catch (error) {
btn.textContent = "🤖 Smart Translate";
btn.disabled = false;
alert(`Language detection failed: ${error.message}`);
alert(getUIText("languageDetectionFailed", { error: error.message }));
}
},
},
{
text: "📋 Full Chat Raw",
text: getUIText("fullChatRaw"),
action: async () => {
try {
const { getChatHistory } = await import("./chat.js");
@ -2531,14 +2584,14 @@ async function openUncloseaiEmbeddedModalNew() {
}
});
await navigator.clipboard.writeText(rawContent);
alert("Full chat copied as raw markdown!");
alert(getUIText("fullChatCopied"));
} catch (error) {
alert("Failed to copy: " + error.message);
alert(getUIText("failedToCopy", { error: error.message }));
}
},
},
{
text: "📄 Full Chat HTML",
text: getUIText("fullChatHTML"),
action: async () => {
try {
const { getChatHistory } = await import("./chat.js");
@ -2554,14 +2607,14 @@ async function openUncloseaiEmbeddedModalNew() {
});
htmlContent += "</div>";
await navigator.clipboard.writeText(htmlContent);
alert("Full chat copied as HTML!");
alert(getUIText("fullChatHTMLCopied"));
} catch (error) {
alert("Failed to copy: " + error.message);
alert(getUIText("failedToCopy", { error: error.message }));
}
},
},
{
text: "📝 Full Page Raw",
text: getUIText("fullPageRaw"),
action: async () => {
try {
// Get the entire page HTML
@ -2569,9 +2622,9 @@ async function openUncloseaiEmbeddedModalNew() {
// Convert to markdown
const markdown = htmlToMarkdown(pageHtml);
await navigator.clipboard.writeText(markdown);
alert("Full page copied as markdown!");
alert(getUIText("fullPageCopied"));
} catch (error) {
alert("Failed to copy page: " + error.message);
alert(getUIText("failedToCopyPage", { error: error.message }));
}
},
},
@ -2623,7 +2676,7 @@ async function openUncloseaiEmbeddedModalNew() {
const input = document.createElement("textarea");
input.id = "modal-user-input";
input.placeholder = "Ask about this page...";
input.placeholder = getUIText("typePlaceholder");
input.style.cssText = `
flex: 1;
min-height: 44px;
@ -2646,7 +2699,7 @@ async function openUncloseaiEmbeddedModalNew() {
// Auto-resize textarea
input.oninput = () => {
input.style.height = "auto";
input.style.height = Math.min(input.scrollHeight, 120) + "px";
input.style.height = `${Math.min(input.scrollHeight, 120)}px`;
};
const sendBtn = document.createElement("button");
@ -4588,7 +4641,7 @@ Format: Start with "Greetings! I'm Hermes..." and make it sound natural and enga
const userInput = document.createElement("textarea");
userInput.id = "modal-user-input";
userInput.placeholder = "Ask about this page...";
userInput.placeholder = getUIText("typePlaceholder");
if (USE_CUSTOM_STYLING) {
userInput.style.cssText = `
flex: 1;
@ -4780,8 +4833,7 @@ export function openTTSModal() {
article.appendChild(header);
const h1 = document.createElement("h1");
h1.innerHTML =
"<span style=\"font-family: 'ChunkFiveRegular', monospace;\">uncloseai.</span> TTS Anything!";
h1.innerHTML = `<span style="font-family: 'ChunkFiveRegular', monospace;">uncloseai.</span> ${getUIText("ttsModalTitle")}`;
if (USE_CUSTOM_STYLING) {
h1.style.cssText = `
margin: 0;
@ -4834,8 +4886,7 @@ export function openTTSModal() {
const textArea = document.createElement("textarea");
textArea.style.width = "100%";
textArea.style.height = "240px";
textArea.placeholder =
"text-to-speech: write any message & have the artificial intelligence speak it!";
textArea.placeholder = getUIText("ttsPlaceholder");
article.appendChild(textArea);
// Voice selection
@ -5077,8 +5128,7 @@ export function openTranslateModal() {
article.appendChild(header);
const h1 = document.createElement("h1");
h1.innerHTML =
"<span style=\"font-family: 'ChunkFiveRegular', monospace;\">uncloseai.</span> 🌐 Translation";
h1.innerHTML = `<span style="font-family: 'ChunkFiveRegular', monospace;">uncloseai.</span> ${getUIText("translateModalTitle")}`;
if (USE_CUSTOM_STYLING) {
h1.style.cssText = `
margin: 0;
@ -5164,7 +5214,7 @@ export function openTranslateModal() {
const textArea = document.createElement("textarea");
textArea.style.width = "100%";
textArea.style.height = "150px";
textArea.placeholder = "Enter any text to translate...";
textArea.placeholder = getUIText("translatePlaceholder");
customContent.appendChild(textArea);
article.appendChild(pageContent);