trysem commited on
Commit
764fcf8
·
verified ·
1 Parent(s): c336cd1

Create Mint

Browse files
Files changed (1) hide show
  1. Mint +369 -0
Mint ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>English to Malayalam Translator</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Manjari:wght@400;700&display=swap');
11
+
12
+ body {
13
+ font-family: 'Inter', sans-serif;
14
+ }
15
+
16
+ .malayalam-text {
17
+ font-family: 'Manjari', sans-serif;
18
+ }
19
+
20
+ /* Custom scrollbar for textareas */
21
+ textarea::-webkit-scrollbar {
22
+ width: 8px;
23
+ }
24
+ textarea::-webkit-scrollbar-track {
25
+ background: #f1f1f1;
26
+ border-radius: 4px;
27
+ }
28
+ textarea::-webkit-scrollbar-thumb {
29
+ background: #cbd5e1;
30
+ border-radius: 4px;
31
+ }
32
+ textarea::-webkit-scrollbar-thumb:hover {
33
+ background: #94a3b8;
34
+ }
35
+
36
+ .loader {
37
+ border: 3px solid #f3f3f3;
38
+ border-radius: 50%;
39
+ border-top: 3px solid #ffffff;
40
+ width: 20px;
41
+ height: 20px;
42
+ -webkit-animation: spin 1s linear infinite; /* Safari */
43
+ animation: spin 1s linear infinite;
44
+ display: inline-block;
45
+ vertical-align: middle;
46
+ margin-right: 8px;
47
+ }
48
+
49
+ @keyframes spin {
50
+ 0% { transform: rotate(0deg); }
51
+ 100% { transform: rotate(360deg); }
52
+ }
53
+ </style>
54
+ </head>
55
+ <body class="bg-slate-50 min-h-screen flex flex-col">
56
+
57
+ <!-- Navbar -->
58
+ <nav class="bg-white shadow-sm border-b border-slate-200 sticky top-0 z-10">
59
+ <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
60
+ <div class="flex justify-between h-16 items-center">
61
+ <div class="flex items-center gap-3">
62
+ <div class="bg-blue-600 text-white p-2 rounded-lg flex items-center justify-center">
63
+ <i class="fa-solid fa-language text-xl"></i>
64
+ </div>
65
+ <span class="font-bold text-xl text-slate-800 tracking-tight">MinT Translator</span>
66
+ </div>
67
+ <div class="text-sm text-slate-500 font-medium">
68
+ English <i class="fa-solid fa-arrow-right mx-2 text-slate-300"></i> Malayalam
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </nav>
73
+
74
+ <!-- Main Content -->
75
+ <main class="flex-grow flex flex-col items-center py-8 px-4 sm:px-6">
76
+
77
+ <div class="w-full max-w-5xl">
78
+ <!-- Header section -->
79
+ <div class="text-center mb-8">
80
+ <h1 class="text-3xl font-bold text-slate-900 mb-3">Translate to Malayalam</h1>
81
+ <p class="text-slate-600 max-w-2xl mx-auto">Fast, open-source machine translation powered by Wikimedia's MinT API.</p>
82
+ </div>
83
+
84
+ <!-- Translation Container -->
85
+ <div class="bg-white rounded-2xl shadow-xl shadow-slate-200/50 border border-slate-100 overflow-hidden flex flex-col md:flex-row">
86
+
87
+ <!-- Input Panel (English) -->
88
+ <div class="flex-1 flex flex-col border-b md:border-b-0 md:border-r border-slate-200 relative group">
89
+ <div class="flex justify-between items-center px-4 py-3 border-b border-slate-100 bg-slate-50/50">
90
+ <span class="font-semibold text-slate-700 text-sm uppercase tracking-wider">English</span>
91
+ <button id="clearBtn" class="text-slate-400 hover:text-red-500 transition-colors text-sm px-2 py-1 rounded hidden" title="Clear text">
92
+ <i class="fa-solid fa-eraser"></i> Clear
93
+ </button>
94
+ </div>
95
+ <textarea id="sourceText"
96
+ 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"
97
+ placeholder="Type or paste English text here..."></textarea>
98
+
99
+ <div class="p-4 flex justify-between items-center bg-white border-t border-slate-50">
100
+ <span id="charCount" class="text-xs font-medium text-slate-400">0 characters</span>
101
+ <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">
102
+ <span id="translateIcon"><i class="fa-solid fa-language mr-2"></i></span>
103
+ <span id="translateText">Translate</span>
104
+ </button>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Output Panel (Malayalam) -->
109
+ <div class="flex-1 flex flex-col relative bg-slate-50/30">
110
+ <div class="flex justify-between items-center px-4 py-3 border-b border-slate-100 bg-slate-50/50">
111
+ <span class="font-semibold text-slate-700 text-sm uppercase tracking-wider">Malayalam (മലയാളം)</span>
112
+ <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">
113
+ <i class="fa-regular fa-copy"></i> Copy
114
+ </button>
115
+ </div>
116
+
117
+ <!-- Output Box -->
118
+ <div class="flex-1 relative">
119
+ <!-- Loading Overlay -->
120
+ <div id="loadingOverlay" class="absolute inset-0 bg-white/80 backdrop-blur-sm z-10 flex flex-col items-center justify-center hidden">
121
+ <div class="loader !border-blue-200 !border-t-blue-600 !w-10 !h-10 mb-3"></div>
122
+ <span class="text-blue-700 font-medium text-sm animate-pulse">Translating...</span>
123
+ </div>
124
+
125
+ <!-- Error Message -->
126
+ <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">
127
+ <i class="fa-solid fa-triangle-exclamation text-red-500 text-3xl mb-3"></i>
128
+ <p id="errorText" class="text-red-700 text-sm font-medium">Failed to translate. Please try again.</p>
129
+ <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>
130
+ </div>
131
+
132
+ <textarea id="targetText"
133
+ class="w-full h-full p-5 resize-none outline-none text-slate-800 text-lg min-h-[250px] bg-transparent malayalam-text"
134
+ placeholder="Translation will appear here..." readonly></textarea>
135
+ </div>
136
+ </div>
137
+
138
+ </div>
139
+
140
+ <!-- Toast Notification -->
141
+ <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">
142
+ <i class="fa-solid fa-circle-check text-green-400"></i>
143
+ <span id="toastMsg" class="font-medium text-sm">Copied to clipboard!</span>
144
+ </div>
145
+
146
+ <div class="mt-8 text-center text-sm text-slate-500">
147
+ <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>
148
+ </div>
149
+ </div>
150
+
151
+ </main>
152
+
153
+ <!-- Footer -->
154
+ <footer class="bg-white border-t border-slate-200 mt-auto py-6">
155
+ <div class="max-w-6xl mx-auto px-4 flex flex-col sm:flex-row justify-between items-center gap-4">
156
+ <div class="text-slate-500 text-sm">
157
+ Powered by <a href="https://translate.wmcloud.org/" target="_blank" class="text-blue-600 hover:underline font-medium">Wikimedia MinT</a>
158
+ </div>
159
+ <div class="text-slate-400 text-xs">
160
+ Built with Tailwind CSS
161
+ </div>
162
+ </div>
163
+ </footer>
164
+
165
+ <script>
166
+ document.addEventListener('DOMContentLoaded', () => {
167
+ const sourceText = document.getElementById('sourceText');
168
+ const targetText = document.getElementById('targetText');
169
+ const translateBtn = document.getElementById('translateBtn');
170
+ const clearBtn = document.getElementById('clearBtn');
171
+ const copyBtn = document.getElementById('copyBtn');
172
+ const charCount = document.getElementById('charCount');
173
+ const loadingOverlay = document.getElementById('loadingOverlay');
174
+ const errorMsg = document.getElementById('errorMsg');
175
+ const errorText = document.getElementById('errorText');
176
+ const dismissErrorBtn = document.getElementById('dismissErrorBtn');
177
+ const translateIcon = document.getElementById('translateIcon');
178
+ const translateTextEl = document.getElementById('translateText');
179
+ const toast = document.getElementById('toast');
180
+ const toastMsg = document.getElementById('toastMsg');
181
+
182
+ // API Endpoint for Wikimedia MinT
183
+ const API_URL = 'https://translate.wmcloud.org/api/translate';
184
+
185
+ // Function to show toast notification
186
+ function showToast(message) {
187
+ toastMsg.textContent = message;
188
+ toast.classList.remove('translate-y-20', 'opacity-0');
189
+
190
+ setTimeout(() => {
191
+ toast.classList.add('translate-y-20', 'opacity-0');
192
+ }, 3000);
193
+ }
194
+
195
+ // Function to update character count and toggle clear button
196
+ function updateInputState() {
197
+ const length = sourceText.value.length;
198
+ charCount.textContent = `${length} character${length !== 1 ? 's' : ''}`;
199
+
200
+ if (length > 0) {
201
+ clearBtn.classList.remove('hidden');
202
+ } else {
203
+ clearBtn.classList.add('hidden');
204
+ }
205
+ }
206
+
207
+ // Set loading state
208
+ function setLoading(isLoading) {
209
+ if (isLoading) {
210
+ loadingOverlay.classList.remove('hidden');
211
+ translateBtn.disabled = true;
212
+ translateBtn.classList.add('opacity-80', 'cursor-not-allowed');
213
+ translateIcon.innerHTML = '<div class="loader !w-4 !h-4 !border-2 !border-blue-300 !border-t-white mr-2"></div>';
214
+ translateTextEl.textContent = 'Translating';
215
+ } else {
216
+ loadingOverlay.classList.add('hidden');
217
+ translateBtn.disabled = false;
218
+ translateBtn.classList.remove('opacity-80', 'cursor-not-allowed');
219
+ translateIcon.innerHTML = '<i class="fa-solid fa-language mr-2"></i>';
220
+ translateTextEl.textContent = 'Translate';
221
+ }
222
+ }
223
+
224
+ // Show/Hide Error
225
+ function showError(message) {
226
+ errorText.textContent = message;
227
+ errorMsg.classList.remove('hidden');
228
+ }
229
+
230
+ dismissErrorBtn.addEventListener('click', () => {
231
+ errorMsg.classList.add('hidden');
232
+ });
233
+
234
+ // The main translate function
235
+ async function performTranslation() {
236
+ const textToTranslate = sourceText.value.trim();
237
+
238
+ if (!textToTranslate) {
239
+ targetText.value = '';
240
+ copyBtn.disabled = true;
241
+ return;
242
+ }
243
+
244
+ errorMsg.classList.add('hidden');
245
+ setLoading(true);
246
+
247
+ try {
248
+ // MinT API requires content, source_language, target_language, and format
249
+ const response = await fetch(API_URL, {
250
+ method: 'POST',
251
+ headers: {
252
+ 'Content-Type': 'application/json',
253
+ 'Accept': 'application/json'
254
+ },
255
+ body: JSON.stringify({
256
+ content: textToTranslate,
257
+ source_language: 'en',
258
+ target_language: 'ml',
259
+ format: 'text'
260
+ })
261
+ });
262
+
263
+ if (!response.ok) {
264
+ let errorDetail = '';
265
+ try {
266
+ const errorData = await response.json();
267
+ if (errorData.detail) {
268
+ errorDetail = typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail);
269
+ } else if (errorData.error) {
270
+ errorDetail = errorData.error;
271
+ }
272
+ } catch (e) {
273
+ // Ignored if JSON parsing fails on error response
274
+ }
275
+ throw new Error(`API Error ${response.status}: ${errorDetail || response.statusText}`);
276
+ }
277
+
278
+ const data = await response.json();
279
+
280
+ // MinT might return different structures. Let's check common keys:
281
+ let translatedResult = '';
282
+
283
+ if (typeof data === 'string') {
284
+ translatedResult = data;
285
+ } else if (data) {
286
+ // Check multiple common field names used by various translation APIs
287
+ translatedResult = data.translation ||
288
+ data.translated_content ||
289
+ data.translatedText ||
290
+ data.translated_text ||
291
+ data.result ||
292
+ data.text;
293
+
294
+ // Check for array-based nested structures (e.g., { translations: [{ translation: "..." }] })
295
+ if (!translatedResult && Array.isArray(data.translations) && data.translations.length > 0) {
296
+ translatedResult = data.translations[0].translation ||
297
+ data.translations[0].translatedText ||
298
+ data.translations[0].text;
299
+ }
300
+ }
301
+
302
+ if (translatedResult) {
303
+ targetText.value = translatedResult;
304
+ copyBtn.disabled = false;
305
+ } else if (data && data.error) {
306
+ throw new Error(data.error);
307
+ } else {
308
+ // Output the actual JSON response into the error string so we can see exactly what the API returned
309
+ throw new Error(`Unexpected response format: ${JSON.stringify(data)}`);
310
+ }
311
+
312
+ } catch (error) {
313
+ console.error('Translation Error:', error);
314
+ showError(error.message || 'An error occurred while connecting to the translation service.');
315
+ targetText.value = '';
316
+ copyBtn.disabled = true;
317
+ } finally {
318
+ setLoading(false);
319
+ }
320
+ }
321
+
322
+ // Event Listeners
323
+ sourceText.addEventListener('input', updateInputState);
324
+
325
+ // Handle Ctrl+Enter for quick translation
326
+ sourceText.addEventListener('keydown', (e) => {
327
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
328
+ e.preventDefault();
329
+ performTranslation();
330
+ }
331
+ });
332
+
333
+ translateBtn.addEventListener('click', performTranslation);
334
+
335
+ clearBtn.addEventListener('click', () => {
336
+ sourceText.value = '';
337
+ targetText.value = '';
338
+ updateInputState();
339
+ copyBtn.disabled = true;
340
+ errorMsg.classList.add('hidden');
341
+ sourceText.focus();
342
+ });
343
+
344
+ copyBtn.addEventListener('click', () => {
345
+ if (!targetText.value) return;
346
+
347
+ // Create a temporary textarea for broader compatibility
348
+ const tempTextArea = document.createElement('textarea');
349
+ tempTextArea.value = targetText.value;
350
+ document.body.appendChild(tempTextArea);
351
+ tempTextArea.select();
352
+
353
+ try {
354
+ document.execCommand('copy');
355
+ showToast('Translation copied to clipboard!');
356
+ } catch (err) {
357
+ console.error('Failed to copy text: ', err);
358
+ showToast('Failed to copy. Please copy manually.');
359
+ }
360
+
361
+ document.body.removeChild(tempTextArea);
362
+ });
363
+
364
+ // Initialize state
365
+ updateInputState();
366
+ });
367
+ </script>
368
+ </body>
369
+ </html>