| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>English to Malayalam Translator</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Manjari:wght@400;700&display=swap'); |
| |
| body { |
| font-family: 'Inter', sans-serif; |
| } |
| |
| .malayalam-text { |
| font-family: 'Manjari', sans-serif; |
| } |
| |
| |
| textarea::-webkit-scrollbar { |
| width: 8px; |
| } |
| textarea::-webkit-scrollbar-track { |
| background: #f1f1f1; |
| border-radius: 4px; |
| } |
| textarea::-webkit-scrollbar-thumb { |
| background: #cbd5e1; |
| border-radius: 4px; |
| } |
| textarea::-webkit-scrollbar-thumb:hover { |
| background: #94a3b8; |
| } |
| |
| .loader { |
| border: 3px solid #f3f3f3; |
| border-radius: 50%; |
| border-top: 3px solid #ffffff; |
| width: 20px; |
| height: 20px; |
| -webkit-animation: spin 1s linear infinite; |
| animation: spin 1s linear infinite; |
| display: inline-block; |
| vertical-align: middle; |
| margin-right: 8px; |
| } |
| |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| </style> |
| </head> |
| <body class="bg-slate-50 min-h-screen flex flex-col"> |
|
|
| |
| <nav class="bg-white shadow-sm border-b border-slate-200 sticky top-0 z-10"> |
| <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8"> |
| <div class="flex justify-between h-16 items-center"> |
| <div class="flex items-center gap-3"> |
| <div class="bg-blue-600 text-white p-2 rounded-lg flex items-center justify-center"> |
| <i class="fa-solid fa-language text-xl"></i> |
| </div> |
| <span class="font-bold text-xl text-slate-800 tracking-tight">MinT Translator</span> |
| </div> |
| <div class="text-sm text-slate-500 font-medium"> |
| English <i class="fa-solid fa-arrow-right mx-2 text-slate-300"></i> Malayalam |
| </div> |
| </div> |
| </div> |
| </nav> |
|
|
| |
| <main class="flex-grow flex flex-col items-center py-8 px-4 sm:px-6"> |
| |
| <div class="w-full max-w-5xl"> |
| |
| <div class="text-center mb-8"> |
| <h1 class="text-3xl font-bold text-slate-900 mb-3">Translate to Malayalam</h1> |
| <p class="text-slate-600 max-w-2xl mx-auto">Fast, open-source machine translation powered by Wikimedia's MinT API.</p> |
| </div> |
|
|
| |
| <div class="bg-white rounded-2xl shadow-xl shadow-slate-200/50 border border-slate-100 overflow-hidden flex flex-col md:flex-row"> |
| |
| |
| <div class="flex-1 flex flex-col border-b md:border-b-0 md:border-r border-slate-200 relative group"> |
| <div class="flex justify-between items-center px-4 py-3 border-b border-slate-100 bg-slate-50/50"> |
| <span class="font-semibold text-slate-700 text-sm uppercase tracking-wider">English</span> |
| <button id="clearBtn" class="text-slate-400 hover:text-red-500 transition-colors text-sm px-2 py-1 rounded hidden" title="Clear text"> |
| <i class="fa-solid fa-eraser"></i> Clear |
| </button> |
| </div> |
| <textarea id="sourceText" |
| class="flex-1 w-full p-5 resize-none outline-none text-slate-800 text-lg min-h-[250px] placeholder-slate-400 focus:bg-slate-50/30 transition-colors" |
| placeholder="Type or paste English text here..."></textarea> |
| |
| <div class="p-4 flex justify-between items-center bg-white border-t border-slate-50"> |
| <span id="charCount" class="text-xs font-medium text-slate-400">0 characters</span> |
| <button id="translateBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2.5 rounded-xl font-medium transition-all shadow-sm shadow-blue-600/20 active:scale-95 flex items-center"> |
| <span id="translateIcon"><i class="fa-solid fa-language mr-2"></i></span> |
| <span id="translateText">Translate</span> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="flex-1 flex flex-col relative bg-slate-50/30"> |
| <div class="flex justify-between items-center px-4 py-3 border-b border-slate-100 bg-slate-50/50"> |
| <span class="font-semibold text-slate-700 text-sm uppercase tracking-wider">Malayalam (മലയാളം)</span> |
| <button id="copyBtn" class="text-slate-400 hover:text-blue-600 transition-colors text-sm px-2 py-1 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled title="Copy to clipboard"> |
| <i class="fa-regular fa-copy"></i> Copy |
| </button> |
| </div> |
| |
| |
| <div class="flex-1 relative"> |
| |
| <div id="loadingOverlay" class="absolute inset-0 bg-white/80 backdrop-blur-sm z-10 flex flex-col items-center justify-center hidden"> |
| <div class="loader !border-blue-200 !border-t-blue-600 !w-10 !h-10 mb-3"></div> |
| <span class="text-blue-700 font-medium text-sm animate-pulse">Translating...</span> |
| </div> |
| |
| |
| <div id="errorMsg" class="absolute inset-0 bg-red-50/90 z-10 flex flex-col items-center justify-center hidden p-6 text-center"> |
| <i class="fa-solid fa-triangle-exclamation text-red-500 text-3xl mb-3"></i> |
| <p id="errorText" class="text-red-700 text-sm font-medium">Failed to translate. Please try again.</p> |
| <button id="dismissErrorBtn" class="mt-4 text-xs bg-red-100 text-red-700 px-3 py-1.5 rounded-md hover:bg-red-200 transition-colors">Dismiss</button> |
| </div> |
|
|
| <textarea id="targetText" |
| class="w-full h-full p-5 resize-none outline-none text-slate-800 text-lg min-h-[250px] bg-transparent malayalam-text" |
| placeholder="Translation will appear here..." readonly></textarea> |
| </div> |
| </div> |
|
|
| </div> |
|
|
| |
| <div id="toast" class="fixed bottom-5 right-5 transform translate-y-20 opacity-0 bg-slate-800 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 transition-all duration-300 pointer-events-none z-50"> |
| <i class="fa-solid fa-circle-check text-green-400"></i> |
| <span id="toastMsg" class="font-medium text-sm">Copied to clipboard!</span> |
| </div> |
|
|
| <div class="mt-8 text-center text-sm text-slate-500"> |
| <p>Press <kbd class="px-2 py-1 bg-slate-200 text-slate-700 rounded-md text-xs mx-1">Ctrl</kbd> + <kbd class="px-2 py-1 bg-slate-200 text-slate-700 rounded-md text-xs mx-1">Enter</kbd> to translate quickly.</p> |
| </div> |
| </div> |
|
|
| </main> |
|
|
| |
| <footer class="bg-white border-t border-slate-200 mt-auto py-6"> |
| <div class="max-w-6xl mx-auto px-4 flex flex-col sm:flex-row justify-between items-center gap-4"> |
| <div class="text-slate-500 text-sm"> |
| Powered by <a href="https://translate.wmcloud.org/" target="_blank" class="text-blue-600 hover:underline font-medium">Wikimedia MinT</a> |
| </div> |
| <div class="text-slate-400 text-xs"> |
| Built with Tailwind CSS |
| </div> |
| </div> |
| </footer> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| const sourceText = document.getElementById('sourceText'); |
| const targetText = document.getElementById('targetText'); |
| const translateBtn = document.getElementById('translateBtn'); |
| const clearBtn = document.getElementById('clearBtn'); |
| const copyBtn = document.getElementById('copyBtn'); |
| const charCount = document.getElementById('charCount'); |
| const loadingOverlay = document.getElementById('loadingOverlay'); |
| const errorMsg = document.getElementById('errorMsg'); |
| const errorText = document.getElementById('errorText'); |
| const dismissErrorBtn = document.getElementById('dismissErrorBtn'); |
| const translateIcon = document.getElementById('translateIcon'); |
| const translateTextEl = document.getElementById('translateText'); |
| const toast = document.getElementById('toast'); |
| const toastMsg = document.getElementById('toastMsg'); |
| |
| |
| const API_URL = 'https://translate.wmcloud.org/api/translate'; |
| |
| |
| function showToast(message) { |
| toastMsg.textContent = message; |
| toast.classList.remove('translate-y-20', 'opacity-0'); |
| |
| setTimeout(() => { |
| toast.classList.add('translate-y-20', 'opacity-0'); |
| }, 3000); |
| } |
| |
| |
| function updateInputState() { |
| const length = sourceText.value.length; |
| charCount.textContent = `${length} character${length !== 1 ? 's' : ''}`; |
| |
| if (length > 0) { |
| clearBtn.classList.remove('hidden'); |
| } else { |
| clearBtn.classList.add('hidden'); |
| } |
| } |
| |
| |
| function setLoading(isLoading) { |
| if (isLoading) { |
| loadingOverlay.classList.remove('hidden'); |
| translateBtn.disabled = true; |
| translateBtn.classList.add('opacity-80', 'cursor-not-allowed'); |
| translateIcon.innerHTML = '<div class="loader !w-4 !h-4 !border-2 !border-blue-300 !border-t-white mr-2"></div>'; |
| translateTextEl.textContent = 'Translating'; |
| } else { |
| loadingOverlay.classList.add('hidden'); |
| translateBtn.disabled = false; |
| translateBtn.classList.remove('opacity-80', 'cursor-not-allowed'); |
| translateIcon.innerHTML = '<i class="fa-solid fa-language mr-2"></i>'; |
| translateTextEl.textContent = 'Translate'; |
| } |
| } |
| |
| |
| function showError(message) { |
| errorText.textContent = message; |
| errorMsg.classList.remove('hidden'); |
| } |
| |
| dismissErrorBtn.addEventListener('click', () => { |
| errorMsg.classList.add('hidden'); |
| }); |
| |
| |
| async function performTranslation() { |
| const textToTranslate = sourceText.value.trim(); |
| |
| if (!textToTranslate) { |
| targetText.value = ''; |
| copyBtn.disabled = true; |
| return; |
| } |
| |
| errorMsg.classList.add('hidden'); |
| setLoading(true); |
| |
| try { |
| |
| const response = await fetch(API_URL, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Accept': 'application/json' |
| }, |
| body: JSON.stringify({ |
| content: textToTranslate, |
| source_language: 'en', |
| target_language: 'ml', |
| format: 'text' |
| }) |
| }); |
| |
| if (!response.ok) { |
| let errorDetail = ''; |
| try { |
| const errorData = await response.json(); |
| if (errorData.detail) { |
| errorDetail = typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail); |
| } else if (errorData.error) { |
| errorDetail = errorData.error; |
| } |
| } catch (e) { |
| |
| } |
| throw new Error(`API Error ${response.status}: ${errorDetail || response.statusText}`); |
| } |
| |
| const data = await response.json(); |
| |
| |
| let translatedResult = ''; |
| |
| if (typeof data === 'string') { |
| translatedResult = data; |
| } else if (data) { |
| |
| translatedResult = data.translation || |
| data.translated_content || |
| data.translatedText || |
| data.translated_text || |
| data.result || |
| data.text; |
| |
| |
| if (!translatedResult && Array.isArray(data.translations) && data.translations.length > 0) { |
| translatedResult = data.translations[0].translation || |
| data.translations[0].translatedText || |
| data.translations[0].text; |
| } |
| } |
| |
| if (translatedResult) { |
| targetText.value = translatedResult; |
| copyBtn.disabled = false; |
| } else if (data && data.error) { |
| throw new Error(data.error); |
| } else { |
| |
| throw new Error(`Unexpected response format: ${JSON.stringify(data)}`); |
| } |
| |
| } catch (error) { |
| console.error('Translation Error:', error); |
| showError(error.message || 'An error occurred while connecting to the translation service.'); |
| targetText.value = ''; |
| copyBtn.disabled = true; |
| } finally { |
| setLoading(false); |
| } |
| } |
| |
| |
| sourceText.addEventListener('input', updateInputState); |
| |
| |
| sourceText.addEventListener('keydown', (e) => { |
| if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { |
| e.preventDefault(); |
| performTranslation(); |
| } |
| }); |
| |
| translateBtn.addEventListener('click', performTranslation); |
| |
| clearBtn.addEventListener('click', () => { |
| sourceText.value = ''; |
| targetText.value = ''; |
| updateInputState(); |
| copyBtn.disabled = true; |
| errorMsg.classList.add('hidden'); |
| sourceText.focus(); |
| }); |
| |
| copyBtn.addEventListener('click', () => { |
| if (!targetText.value) return; |
| |
| |
| const tempTextArea = document.createElement('textarea'); |
| tempTextArea.value = targetText.value; |
| document.body.appendChild(tempTextArea); |
| tempTextArea.select(); |
| |
| try { |
| document.execCommand('copy'); |
| showToast('Translation copied to clipboard!'); |
| } catch (err) { |
| console.error('Failed to copy text: ', err); |
| showToast('Failed to copy. Please copy manually.'); |
| } |
| |
| document.body.removeChild(tempTextArea); |
| }); |
| |
| |
| updateInputState(); |
| }); |
| </script> |
| </body> |
| </html> |