ASRRONNX / Mint
trysem's picture
Create Mint
764fcf8 verified
<!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;
}
/* Custom scrollbar for textareas */
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; /* Safari */
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">
<!-- Navbar -->
<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 Content -->
<main class="flex-grow flex flex-col items-center py-8 px-4 sm:px-6">
<div class="w-full max-w-5xl">
<!-- Header section -->
<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>
<!-- Translation Container -->
<div class="bg-white rounded-2xl shadow-xl shadow-slate-200/50 border border-slate-100 overflow-hidden flex flex-col md:flex-row">
<!-- Input Panel (English) -->
<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>
<!-- Output Panel (Malayalam) -->
<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>
<!-- Output Box -->
<div class="flex-1 relative">
<!-- Loading Overlay -->
<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>
<!-- Error Message -->
<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>
<!-- Toast Notification -->
<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 -->
<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');
// API Endpoint for Wikimedia MinT
const API_URL = 'https://translate.wmcloud.org/api/translate';
// Function to show toast notification
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 to update character count and toggle clear button
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');
}
}
// Set loading state
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';
}
}
// Show/Hide Error
function showError(message) {
errorText.textContent = message;
errorMsg.classList.remove('hidden');
}
dismissErrorBtn.addEventListener('click', () => {
errorMsg.classList.add('hidden');
});
// The main translate function
async function performTranslation() {
const textToTranslate = sourceText.value.trim();
if (!textToTranslate) {
targetText.value = '';
copyBtn.disabled = true;
return;
}
errorMsg.classList.add('hidden');
setLoading(true);
try {
// MinT API requires content, source_language, target_language, and format
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) {
// Ignored if JSON parsing fails on error response
}
throw new Error(`API Error ${response.status}: ${errorDetail || response.statusText}`);
}
const data = await response.json();
// MinT might return different structures. Let's check common keys:
let translatedResult = '';
if (typeof data === 'string') {
translatedResult = data;
} else if (data) {
// Check multiple common field names used by various translation APIs
translatedResult = data.translation ||
data.translated_content ||
data.translatedText ||
data.translated_text ||
data.result ||
data.text;
// Check for array-based nested structures (e.g., { translations: [{ translation: "..." }] })
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 {
// Output the actual JSON response into the error string so we can see exactly what the API returned
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);
}
}
// Event Listeners
sourceText.addEventListener('input', updateInputState);
// Handle Ctrl+Enter for quick translation
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;
// Create a temporary textarea for broader compatibility
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);
});
// Initialize state
updateInputState();
});
</script>
</body>
</html>