734 lines
21 KiB
JavaScript
734 lines
21 KiB
JavaScript
/**
|
|
* DeepLX Translation App
|
|
* A modern web app for DeepL translation using the DeepLX API
|
|
*
|
|
* Features:
|
|
* - Auto-translation with configurable delay
|
|
* - Translation history with local storage
|
|
* - RTL/LTR text direction support
|
|
* - Responsive design with modern UI
|
|
* - Keyboard shortcuts and accessibility
|
|
*
|
|
* @author Xi Xu
|
|
* @version 1.0.0
|
|
* @license MIT
|
|
*/
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// Configuration constants
|
|
const CONFIG = {
|
|
MAX_CHARS: 5000,
|
|
DEFAULT_API_URL: "https://dplx.xi-xu.me/translate",
|
|
DEFAULT_DELAY: 1000,
|
|
MAX_HISTORY_ITEMS: 50,
|
|
CORS_PROXY: "https://corsproxy.io/?",
|
|
};
|
|
|
|
// Supported languages list - DeepL API compatible language codes
|
|
const languages = {
|
|
AR: "Arabic",
|
|
BG: "Bulgarian",
|
|
CS: "Czech",
|
|
DA: "Danish",
|
|
DE: "German",
|
|
EL: "Greek",
|
|
EN: "English",
|
|
ES: "Spanish",
|
|
ET: "Estonian",
|
|
FI: "Finnish",
|
|
FR: "French",
|
|
HU: "Hungarian",
|
|
ID: "Indonesian",
|
|
IT: "Italian",
|
|
JA: "Japanese",
|
|
KO: "Korean",
|
|
LT: "Lithuanian",
|
|
LV: "Latvian",
|
|
NB: "Norwegian (bokmål)",
|
|
NL: "Dutch",
|
|
PL: "Polish",
|
|
PT: "Portuguese",
|
|
RO: "Romanian",
|
|
RU: "Russian",
|
|
SK: "Slovak",
|
|
SL: "Slovenian",
|
|
SV: "Swedish",
|
|
TR: "Turkish",
|
|
UK: "Ukrainian",
|
|
ZH: "Chinese",
|
|
};
|
|
|
|
// Get DOM elements
|
|
// DOM element references - cached for performance
|
|
const elements = {
|
|
sourceLangSelect: document.getElementById("sourceLang"),
|
|
targetLangSelect: document.getElementById("targetLang"),
|
|
inputText: document.getElementById("inputText"),
|
|
outputText: document.getElementById("outputText"),
|
|
swapButton: document.getElementById("swapButton"),
|
|
copyButton: document.getElementById("copyButton"),
|
|
loadingSpinner: document.getElementById("loadingSpinner"),
|
|
statusMessage: document.getElementById("statusMessage"),
|
|
apiUrlInput: document.getElementById("apiUrlInput"),
|
|
historyButton: document.getElementById("historyButton"),
|
|
settingsButton: document.getElementById("settingsButton"),
|
|
historyPanel: document.getElementById("historyPanel"),
|
|
settingsPanel: document.getElementById("settingsPanel"),
|
|
closeHistoryButton: document.getElementById("closeHistoryButton"),
|
|
closeSettingsButton: document.getElementById("closeSettingsButton"),
|
|
historyList: document.getElementById("historyList"),
|
|
historyCount: document.getElementById("historyCount"),
|
|
clearHistoryButton: document.getElementById("clearHistoryButton"),
|
|
delayInput: document.getElementById("delayInput"),
|
|
autoTranslateToggle: document.getElementById("autoTranslateToggle"),
|
|
charCount: document.getElementById("charCount"),
|
|
};
|
|
|
|
// Application state
|
|
let translateTimeout;
|
|
let translationHistory = [];
|
|
|
|
// Function to detect Arabic text
|
|
/**
|
|
* Utility Functions
|
|
*/
|
|
|
|
/**
|
|
* Detects if text contains Arabic characters
|
|
* @param {string} text - Text to analyze
|
|
* @returns {boolean} True if text contains Arabic characters
|
|
*/
|
|
function isArabicText(text) {
|
|
const arabicRegex =
|
|
/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
|
return arabicRegex.test(text);
|
|
}
|
|
|
|
/**
|
|
* Applies appropriate text direction (RTL/LTR) based on text content
|
|
* @param {HTMLElement} element - Element to apply direction to
|
|
* @param {string} text - Text content to analyze
|
|
*/
|
|
function applyTextDirection(element, text) {
|
|
if (isArabicText(text)) {
|
|
element.classList.add("rtl-text");
|
|
element.classList.remove("ltr-text");
|
|
} else {
|
|
element.classList.add("ltr-text");
|
|
element.classList.remove("rtl-text");
|
|
}
|
|
}
|
|
|
|
// Character counter function
|
|
/**
|
|
* UI Update Functions
|
|
*/
|
|
|
|
/**
|
|
* Updates character counter and input field styling
|
|
* Handles RTL/LTR text direction and placeholder updates
|
|
*/
|
|
function updateCharCount() {
|
|
const count = elements.inputText.value.length;
|
|
elements.charCount.textContent = count;
|
|
|
|
// Apply RTL/LTR direction based on input text
|
|
applyTextDirection(elements.inputText, elements.inputText.value);
|
|
|
|
// Update color based on character count
|
|
elements.charCount.className = "transition-colors duration-200";
|
|
if (count > 4500) {
|
|
elements.charCount.classList.add("char-danger");
|
|
} else if (count > 4000) {
|
|
elements.charCount.classList.add("char-warning");
|
|
}
|
|
|
|
// Update placeholder based on auto-translate status
|
|
if (elements.autoTranslateToggle.checked) {
|
|
elements.inputText.placeholder =
|
|
count === 0
|
|
? "Enter text to translate here... (Auto-translate enabled)"
|
|
: "Auto-translate will start after you stop typing...";
|
|
} else {
|
|
elements.inputText.placeholder =
|
|
"Enter text to translate here... (Auto-translate disabled)";
|
|
}
|
|
}
|
|
|
|
// Populate language dropdown menus
|
|
/**
|
|
* Language Management Functions
|
|
*/
|
|
|
|
/**
|
|
* Populates language dropdown menus with sorted language options
|
|
* Sets default values for source (AUTO) and target (EN) languages
|
|
*/
|
|
function populateLanguages() {
|
|
elements.sourceLangSelect.innerHTML =
|
|
'<option value="AUTO">Detect language</option>';
|
|
|
|
// Sort languages alphabetically by name for better UX
|
|
const sortedLanguages = Object.entries(languages).sort(([, a], [, b]) =>
|
|
a.localeCompare(b)
|
|
);
|
|
|
|
sortedLanguages.forEach(([code, name]) => {
|
|
elements.sourceLangSelect.add(new Option(name, code));
|
|
});
|
|
|
|
elements.targetLangSelect.innerHTML = "";
|
|
sortedLanguages.forEach(([code, name]) => {
|
|
elements.targetLangSelect.add(new Option(name, code));
|
|
});
|
|
|
|
// Set default language selections
|
|
elements.sourceLangSelect.value = "AUTO";
|
|
elements.targetLangSelect.value = "EN";
|
|
}
|
|
|
|
// Load and save settings
|
|
/**
|
|
* Settings Management Functions
|
|
*/
|
|
|
|
/**
|
|
* Initializes app settings from localStorage
|
|
* Sets up event listeners for settings changes
|
|
*/
|
|
function setupSettings() {
|
|
// Load saved settings with fallback defaults
|
|
const savedUrl = localStorage.getItem("deeplxApiUrl");
|
|
const savedDelay =
|
|
localStorage.getItem("autoTranslateDelay") ||
|
|
CONFIG.DEFAULT_DELAY.toString();
|
|
const savedAutoTranslate =
|
|
localStorage.getItem("autoTranslateEnabled") !== "false";
|
|
|
|
elements.apiUrlInput.value = savedUrl || CONFIG.DEFAULT_API_URL;
|
|
elements.delayInput.value = savedDelay;
|
|
elements.autoTranslateToggle.checked = savedAutoTranslate;
|
|
|
|
// Set up event listeners for settings persistence
|
|
elements.apiUrlInput.addEventListener("input", () => {
|
|
const urlToSave = elements.apiUrlInput.value.trim();
|
|
if (urlToSave) {
|
|
localStorage.setItem("deeplxApiUrl", urlToSave);
|
|
showStatus("API endpoint saved.", "success");
|
|
}
|
|
});
|
|
|
|
elements.delayInput.addEventListener("input", () => {
|
|
const delay = elements.delayInput.value;
|
|
localStorage.setItem("autoTranslateDelay", delay);
|
|
showStatus("Delay setting saved.", "success");
|
|
});
|
|
|
|
elements.autoTranslateToggle.addEventListener("change", () => {
|
|
localStorage.setItem(
|
|
"autoTranslateEnabled",
|
|
elements.autoTranslateToggle.checked
|
|
);
|
|
showStatus(
|
|
`Auto-translate ${
|
|
elements.autoTranslateToggle.checked ? "enabled" : "disabled"
|
|
}.`,
|
|
"success"
|
|
);
|
|
updateCharCount(); // Update placeholder when toggle changes
|
|
});
|
|
}
|
|
|
|
// Load translation history
|
|
/**
|
|
* History Management Functions
|
|
*/
|
|
|
|
/**
|
|
* Loads translation history from localStorage
|
|
*/
|
|
function loadHistory() {
|
|
const saved = localStorage.getItem("translationHistory");
|
|
if (saved) {
|
|
translationHistory = JSON.parse(saved);
|
|
}
|
|
updateHistoryDisplay();
|
|
}
|
|
|
|
/**
|
|
* Saves translation history to localStorage
|
|
*/
|
|
function saveHistory() {
|
|
localStorage.setItem(
|
|
"translationHistory",
|
|
JSON.stringify(translationHistory)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Adds a new translation to history
|
|
* @param {string} sourceText - Original text
|
|
* @param {string} targetText - Translated text
|
|
* @param {string} sourceLang - Source language code
|
|
* @param {string} targetLang - Target language code
|
|
*/
|
|
function addToHistory(sourceText, targetText, sourceLang, targetLang) {
|
|
const historyItem = {
|
|
id: Date.now(),
|
|
sourceText,
|
|
targetText,
|
|
sourceLang,
|
|
targetLang,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
// Remove duplicate if exists
|
|
translationHistory = translationHistory.filter(
|
|
(item) =>
|
|
item.sourceText !== sourceText ||
|
|
item.sourceLang !== sourceLang ||
|
|
item.targetLang !== targetLang
|
|
);
|
|
|
|
// Add to beginning of array
|
|
translationHistory.unshift(historyItem);
|
|
|
|
// Keep only last items as per config
|
|
if (translationHistory.length > CONFIG.MAX_HISTORY_ITEMS) {
|
|
translationHistory = translationHistory.slice(
|
|
0,
|
|
CONFIG.MAX_HISTORY_ITEMS
|
|
);
|
|
}
|
|
|
|
saveHistory();
|
|
updateHistoryDisplay();
|
|
}
|
|
|
|
/**
|
|
* Updates the history display panel
|
|
*/
|
|
function updateHistoryDisplay() {
|
|
elements.historyCount.textContent = `${translationHistory.length} records`;
|
|
elements.historyList.innerHTML = "";
|
|
|
|
translationHistory.forEach((item) => {
|
|
const historyItem = document.createElement("div");
|
|
historyItem.className = "history-item";
|
|
|
|
const sourceTextClass = isArabicText(item.sourceText)
|
|
? "source-text rtl-text"
|
|
: "source-text";
|
|
const targetTextClass = isArabicText(item.targetText)
|
|
? "target-text rtl-text"
|
|
: "target-text";
|
|
|
|
historyItem.innerHTML = `
|
|
<div class="${sourceTextClass}">${item.sourceText.substring(0, 100)}${
|
|
item.sourceText.length > 100 ? "..." : ""
|
|
}</div>
|
|
<div class="${targetTextClass}">${item.targetText.substring(0, 100)}${
|
|
item.targetText.length > 100 ? "..." : ""
|
|
}</div>
|
|
<div class="lang-info">${
|
|
languages[item.sourceLang] || item.sourceLang
|
|
} → ${languages[item.targetLang] || item.targetLang}</div>
|
|
`;
|
|
|
|
historyItem.addEventListener("click", () => {
|
|
elements.inputText.value = item.sourceText;
|
|
elements.outputText.value = item.targetText;
|
|
elements.sourceLangSelect.value = item.sourceLang;
|
|
elements.targetLangSelect.value = item.targetLang;
|
|
|
|
// Apply text direction for loaded history items
|
|
applyTextDirection(elements.inputText, item.sourceText);
|
|
applyTextDirection(elements.outputText, item.targetText);
|
|
|
|
elements.historyPanel.classList.add("hidden");
|
|
showStatus("History item loaded.", "success");
|
|
});
|
|
|
|
elements.historyList.appendChild(historyItem);
|
|
});
|
|
}
|
|
|
|
// Display status message with enhanced animation
|
|
/**
|
|
* Status and Feedback Functions
|
|
*/
|
|
|
|
/**
|
|
* Displays status message with enhanced animation
|
|
* @param {string} message - Message to display
|
|
* @param {string} type - Message type ('error' or 'success')
|
|
*/
|
|
function showStatus(message, type = "error") {
|
|
elements.statusMessage.textContent = message;
|
|
elements.statusMessage.className = `text-sm h-5 text-right transition-all duration-300 ${
|
|
type === "error" ? "text-red-400" : "text-green-400"
|
|
}`;
|
|
|
|
// Add fade-in animation
|
|
elements.statusMessage.style.opacity = "0";
|
|
elements.statusMessage.style.transform = "translateY(10px)";
|
|
|
|
setTimeout(() => {
|
|
elements.statusMessage.style.opacity = "1";
|
|
elements.statusMessage.style.transform = "translateY(0)";
|
|
}, 50);
|
|
|
|
// Auto-hide message after 4 seconds
|
|
if (message) {
|
|
setTimeout(() => {
|
|
elements.statusMessage.style.opacity = "0";
|
|
setTimeout(() => {
|
|
elements.statusMessage.textContent = "";
|
|
elements.statusMessage.style.opacity = "1";
|
|
}, 300);
|
|
}, 4000);
|
|
}
|
|
}
|
|
|
|
// Translation function
|
|
/**
|
|
* Translation Functions
|
|
*/
|
|
|
|
/**
|
|
* Main translation function that handles API requests
|
|
* @param {boolean} isAutoTranslate - Whether this is an automatic translation
|
|
*/
|
|
async function translateText(isAutoTranslate = false) {
|
|
const originalApiUrl = elements.apiUrlInput.value.trim();
|
|
if (!originalApiUrl) {
|
|
showStatus("Please enter API endpoint.");
|
|
return;
|
|
}
|
|
|
|
// Using CORS proxy to handle POST requests correctly
|
|
const API_URL = `${CONFIG.CORS_PROXY}${encodeURIComponent(originalApiUrl)}`;
|
|
|
|
const text = elements.inputText.value.trim();
|
|
if (!text) {
|
|
if (!isAutoTranslate) {
|
|
showStatus("Please enter text to translate.");
|
|
}
|
|
elements.outputText.value = "";
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
showStatus("", "success"); // Clear status
|
|
|
|
try {
|
|
const payload = {
|
|
text: text,
|
|
source_lang:
|
|
elements.sourceLangSelect.value === "AUTO"
|
|
? undefined
|
|
: elements.sourceLangSelect.value,
|
|
target_lang: elements.targetLangSelect.value,
|
|
};
|
|
|
|
const response = await fetch(API_URL, {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
|
|
if (response.status === 404) {
|
|
throw new Error(
|
|
`Proxy cannot find API endpoint (404). Please check if URL is correct.`
|
|
);
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Network or proxy error: ${response.status} ${response.statusText}`
|
|
);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.code === 200) {
|
|
elements.outputText.value = result.data;
|
|
|
|
// Apply RTL/LTR direction to output text
|
|
applyTextDirection(elements.outputText, result.data);
|
|
|
|
// Add to history if translation was successful
|
|
if (result.data && text) {
|
|
addToHistory(
|
|
text,
|
|
result.data,
|
|
elements.sourceLangSelect.value,
|
|
elements.targetLangSelect.value
|
|
);
|
|
}
|
|
} else {
|
|
// Provide more specific error message from API response
|
|
throw new Error(`API error: ${result.message || "Unknown error"}`);
|
|
}
|
|
} catch (error) {
|
|
console.error("Translation request failed:", error);
|
|
let errorMessage = `Error: ${error.message}`;
|
|
if (error instanceof TypeError) {
|
|
// This often indicates a network-level failure
|
|
errorMessage =
|
|
"Error: Request failed. Please check network connection and browser console.";
|
|
}
|
|
showStatus(errorMessage);
|
|
elements.outputText.value = "";
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
// Auto-translate with debounce
|
|
/**
|
|
* Auto-translation Functions
|
|
*/
|
|
|
|
/**
|
|
* Schedules auto-translation with debounce
|
|
*/
|
|
function scheduleAutoTranslate() {
|
|
if (!elements.autoTranslateToggle.checked) return;
|
|
|
|
clearTimeout(translateTimeout);
|
|
const delay = parseInt(elements.delayInput.value) || CONFIG.DEFAULT_DELAY;
|
|
|
|
translateTimeout = setTimeout(() => {
|
|
translateText(true);
|
|
}, delay);
|
|
}
|
|
|
|
/**
|
|
* UI State Management Functions
|
|
*/
|
|
|
|
/**
|
|
* Sets loading state for the app
|
|
* @param {boolean} isLoading - Whether app is in loading state
|
|
*/
|
|
function setLoading(isLoading) {
|
|
elements.loadingSpinner.classList.toggle("hidden", !isLoading);
|
|
}
|
|
|
|
/**
|
|
* Language Management Functions
|
|
*/
|
|
|
|
/**
|
|
* Swaps source and target languages
|
|
*/
|
|
function swapLanguages() {
|
|
const source = elements.sourceLangSelect.value;
|
|
const target = elements.targetLangSelect.value;
|
|
|
|
// Don't swap if source is auto-detect
|
|
if (source === "AUTO") {
|
|
return;
|
|
}
|
|
|
|
elements.sourceLangSelect.value = target;
|
|
elements.targetLangSelect.value = source;
|
|
|
|
const inputTextValue = elements.inputText.value;
|
|
elements.inputText.value = elements.outputText.value;
|
|
elements.outputText.value = inputTextValue;
|
|
|
|
// Apply text direction after swapping
|
|
applyTextDirection(elements.inputText, elements.inputText.value);
|
|
applyTextDirection(elements.outputText, elements.outputText.value);
|
|
|
|
showStatus("", "success");
|
|
}
|
|
|
|
/**
|
|
* Updates swap button state based on source language selection
|
|
*/
|
|
function updateSwapButtonState() {
|
|
const isAutoDetect = elements.sourceLangSelect.value === "AUTO";
|
|
elements.swapButton.disabled = isAutoDetect;
|
|
|
|
if (isAutoDetect) {
|
|
elements.swapButton.classList.add("opacity-50", "cursor-not-allowed");
|
|
elements.swapButton.classList.remove("hover:bg-gray-600");
|
|
elements.swapButton.title = "Cannot swap when using Auto Detect";
|
|
} else {
|
|
elements.swapButton.classList.remove("opacity-50", "cursor-not-allowed");
|
|
elements.swapButton.classList.add("hover:bg-gray-600");
|
|
elements.swapButton.title = "Swap languages";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clipboard and UI Interaction Functions
|
|
*/
|
|
|
|
/**
|
|
* Copies translation result to clipboard with visual feedback
|
|
*/
|
|
function copyToClipboard() {
|
|
if (!elements.outputText.value) {
|
|
showStatus("No content to copy.");
|
|
return;
|
|
}
|
|
|
|
const textarea = document.createElement("textarea");
|
|
textarea.value = elements.outputText.value;
|
|
document.body.appendChild(textarea);
|
|
textarea.select();
|
|
try {
|
|
document.execCommand("copy");
|
|
showStatus("Copied to clipboard!", "success");
|
|
|
|
// Add pulse animation to copy button
|
|
elements.copyButton.classList.add("copy-success");
|
|
setTimeout(() => {
|
|
elements.copyButton.classList.remove("copy-success");
|
|
}, 300);
|
|
} catch (err) {
|
|
showStatus("Copy failed.");
|
|
console.error("Copy failed:", err);
|
|
}
|
|
document.body.removeChild(textarea);
|
|
}
|
|
|
|
/**
|
|
* Panel Management Functions
|
|
*/
|
|
|
|
/**
|
|
* Shows the history panel
|
|
*/
|
|
function showHistoryPanel() {
|
|
elements.historyPanel.classList.remove("hidden");
|
|
updateHistoryDisplay();
|
|
}
|
|
|
|
/**
|
|
* Hides the history panel
|
|
*/
|
|
function hideHistoryPanel() {
|
|
elements.historyPanel.classList.add("hidden");
|
|
}
|
|
|
|
/**
|
|
* Shows the settings panel
|
|
*/
|
|
function showSettingsPanel() {
|
|
elements.settingsPanel.classList.remove("hidden");
|
|
}
|
|
|
|
/**
|
|
* Hides the settings panel
|
|
*/
|
|
function hideSettingsPanel() {
|
|
elements.settingsPanel.classList.add("hidden");
|
|
}
|
|
|
|
/**
|
|
* Clears all translation history after confirmation
|
|
*/
|
|
function clearHistory() {
|
|
if (confirm("Are you sure you want to clear all translation history?")) {
|
|
translationHistory = [];
|
|
saveHistory();
|
|
updateHistoryDisplay();
|
|
showStatus("History cleared.", "success");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Animation and Visual Feedback Functions
|
|
*/
|
|
|
|
/**
|
|
* Adds visual feedback for successful operations
|
|
* @param {HTMLElement} element - Element to animate
|
|
*/
|
|
function addSuccessFeedback(element) {
|
|
element.style.transform = "scale(1.05)";
|
|
element.style.transition = "transform 0.2s ease";
|
|
setTimeout(() => {
|
|
element.style.transform = "scale(1)";
|
|
}, 200);
|
|
}
|
|
|
|
/**
|
|
* Enhanced swap function with animation
|
|
*/
|
|
function swapLanguagesWithAnimation() {
|
|
addSuccessFeedback(elements.swapButton);
|
|
swapLanguages();
|
|
}
|
|
|
|
// Initialize
|
|
/**
|
|
* Application Initialization
|
|
*/
|
|
|
|
// Initialize core app components
|
|
populateLanguages();
|
|
setupSettings();
|
|
loadHistory();
|
|
updateCharCount(); // Initialize character counter
|
|
updateSwapButtonState(); // Initialize swap button state
|
|
lucide.createIcons(); // Initialize Lucide icons
|
|
|
|
// Initial auto-translate if there's existing text
|
|
if (elements.inputText.value.trim()) {
|
|
scheduleAutoTranslate();
|
|
}
|
|
|
|
/**
|
|
* Event Listeners Setup
|
|
*/
|
|
|
|
// Button event listeners
|
|
elements.swapButton.addEventListener("click", swapLanguagesWithAnimation);
|
|
elements.copyButton.addEventListener("click", copyToClipboard);
|
|
elements.historyButton.addEventListener("click", showHistoryPanel);
|
|
elements.settingsButton.addEventListener("click", showSettingsPanel);
|
|
elements.closeHistoryButton.addEventListener("click", hideHistoryPanel);
|
|
elements.closeSettingsButton.addEventListener("click", hideSettingsPanel);
|
|
elements.clearHistoryButton.addEventListener("click", clearHistory);
|
|
|
|
// Auto-translate triggers
|
|
elements.inputText.addEventListener("input", () => {
|
|
updateCharCount();
|
|
scheduleAutoTranslate();
|
|
});
|
|
elements.sourceLangSelect.addEventListener("change", () => {
|
|
updateSwapButtonState();
|
|
scheduleAutoTranslate();
|
|
});
|
|
elements.targetLangSelect.addEventListener("change", scheduleAutoTranslate);
|
|
|
|
// Panel management - close when clicking outside
|
|
elements.historyPanel.addEventListener("click", (e) => {
|
|
if (e.target === elements.historyPanel) {
|
|
hideHistoryPanel();
|
|
}
|
|
});
|
|
|
|
elements.settingsPanel.addEventListener("click", (e) => {
|
|
if (e.target === elements.settingsPanel) {
|
|
hideSettingsPanel();
|
|
}
|
|
});
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.ctrlKey && e.key === "Enter") {
|
|
translateText();
|
|
}
|
|
if (e.key === "Escape") {
|
|
hideHistoryPanel();
|
|
hideSettingsPanel();
|
|
}
|
|
});
|
|
});
|