yashdubey commited on
Commit
64af902
·
verified ·
1 Parent(s): 9d32856

Upload 14 files

Browse files
app.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import os
2
+ from translator.app import app
3
+
4
+ # For Hugging Face Spaces deployment
5
+ if __name__ == "__main__":
6
+ port = int(os.environ.get("PORT", 7860))
7
+ app.run(host="0.0.0.0", port=port, debug=False)
main.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gc
3
+ from translator.app import app
4
+
5
+ # For Hugging Face Spaces deployment
6
+ os.environ['MALLOC_TRIM_THRESHOLD_'] = '65536'
7
+
8
+ if __name__ == '__main__':
9
+ print("Starting LinguaSync Translator for Hugging Face Spaces...")
10
+
11
+ # Force garbage collection
12
+ gc.collect()
13
+
14
+ # Set environment for Hugging Face Spaces
15
+ if os.environ.get("SPACE_ID"):
16
+ print("Running in Hugging Face Spaces environment")
17
+
18
+ port = int(os.environ.get("PORT", 7860))
19
+ app.run(host="0.0.0.0", port=port, debug=False, threaded=True)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask>=2.3.2
2
+ torch>=2.0.1
3
+ transformers>=4.30.2
4
+ sentencepiece>=0.1.99
5
+ numpy>=1.24.3
6
+ tqdm>=4.65.0
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.10.11
static/style.css ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Additional custom styles for the translator app */
2
+
3
+ /* Fix for textarea and text input fields */
4
+ textarea.form-control {
5
+ white-space: normal !important;
6
+ word-wrap: break-word !important;
7
+ overflow-wrap: break-word !important;
8
+ resize: vertical !important;
9
+ }
10
+
11
+ #translationResult p {
12
+ white-space: pre-wrap !important;
13
+ word-wrap: break-word !important;
14
+ overflow-wrap: break-word !important;
15
+ }
16
+
17
+ /* Improve UI aesthetics */
18
+ .translator-card {
19
+ transition: all 0.3s ease;
20
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08) !important;
21
+ }
22
+
23
+ .translator-card:hover {
24
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12) !important;
25
+ transform: translateY(-2px);
26
+ }
27
+
28
+ /* Improve focus states for accessibility */
29
+ .form-control:focus, .form-select:focus, .btn:focus {
30
+ box-shadow: 0 0 0 0.25rem rgba(99, 102, 241, 0.25) !important;
31
+ }
32
+
33
+ /* Add subtle hover effects to buttons */
34
+ .btn:hover {
35
+ transform: translateY(-1px);
36
+ transition: all 0.2s ease;
37
+ }
38
+
39
+ /* Animated typing effect for the logo */
40
+ .typing-animation {
41
+ border-right: .15em solid var(--primary-color);
42
+ white-space: nowrap;
43
+ margin: 0 auto;
44
+ letter-spacing: .15em;
45
+ animation:
46
+ typing 3.5s steps(30, end),
47
+ blink-caret .75s step-end infinite;
48
+ display: inline-block;
49
+ }
50
+
51
+ @keyframes typing {
52
+ from { width: 0 }
53
+ to { width: 100% }
54
+ }
55
+
56
+ @keyframes blink-caret {
57
+ from, to { border-color: transparent }
58
+ 50% { border-color: var(--primary-color); }
59
+ }
60
+
61
+ /* Tooltip styling */
62
+ .custom-tooltip {
63
+ position: relative;
64
+ display: inline-block;
65
+ }
66
+
67
+ .custom-tooltip .tooltiptext {
68
+ visibility: hidden;
69
+ width: 120px;
70
+ background-color: #555;
71
+ color: #fff;
72
+ text-align: center;
73
+ border-radius: 6px;
74
+ padding: 5px;
75
+ position: absolute;
76
+ z-index: 1;
77
+ bottom: 125%;
78
+ left: 50%;
79
+ margin-left: -60px;
80
+ opacity: 0;
81
+ transition: opacity 0.3s;
82
+ }
83
+
84
+ .custom-tooltip:hover .tooltiptext {
85
+ visibility: visible;
86
+ opacity: 1;
87
+ }
88
+
89
+ /* Language badge styling */
90
+ .lang-badge {
91
+ border-radius: 50%;
92
+ width: 30px;
93
+ height: 30px;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ font-weight: bold;
98
+ margin-right: 5px;
99
+ }
100
+
101
+ /* History item hover effect */
102
+ .list-group-item {
103
+ transition: all 0.3s ease;
104
+ }
105
+
106
+ .list-group-item:hover {
107
+ transform: translateY(-2px);
108
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
109
+ }
110
+
111
+ /* Progress bar pulsating effect */
112
+ @keyframes pulse {
113
+ 0% { opacity: 0.6; }
114
+ 50% { opacity: 1; }
115
+ 100% { opacity: 0.6; }
116
+ }
117
+
118
+ .progress-bar {
119
+ animation: pulse 1.5s infinite;
120
+ }
121
+
122
+ /* Responsive adjustments */
123
+ @media (max-width: 992px) {
124
+ .container {
125
+ padding: 15px;
126
+ }
127
+
128
+ .typing-animation {
129
+ letter-spacing: 0.1em;
130
+ }
131
+ }
132
+
133
+ @media (max-width: 768px) {
134
+ .container {
135
+ padding: 12px;
136
+ }
137
+
138
+ h1 {
139
+ font-size: 1.5rem;
140
+ }
141
+
142
+ .form-control, .form-select {
143
+ font-size: 14px;
144
+ }
145
+
146
+ .typing-animation {
147
+ letter-spacing: 0.05em;
148
+ animation: none; /* Disable typing animation on mobile */
149
+ }
150
+
151
+ .custom-tooltip .tooltiptext {
152
+ width: 100px;
153
+ margin-left: -50px;
154
+ }
155
+
156
+ .lang-badge {
157
+ width: 24px;
158
+ height: 24px;
159
+ font-size: 12px;
160
+ }
161
+ }
162
+
163
+ @media (max-width: 576px) {
164
+ .container {
165
+ padding: 10px;
166
+ }
167
+
168
+ h1 {
169
+ font-size: 1.3rem;
170
+ }
171
+
172
+ .form-control, .form-select {
173
+ font-size: 13px;
174
+ }
175
+
176
+ .lang-badge {
177
+ width: 20px;
178
+ height: 20px;
179
+ font-size: 10px;
180
+ margin-right: 2px;
181
+ }
182
+
183
+ .custom-tooltip .tooltiptext {
184
+ display: none; /* Hide tooltips on very small screens */
185
+ }
186
+ }
187
+
188
+ /* Fix touch interactions on mobile devices */
189
+ @media (hover: none) {
190
+ .list-group-item:hover {
191
+ transform: none;
192
+ box-shadow: none;
193
+ }
194
+
195
+ .custom-tooltip:hover .tooltiptext {
196
+ visibility: hidden;
197
+ opacity: 0;
198
+ }
199
+ }
translator/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # This file makes the translator directory a package
translator/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (151 Bytes). View file
 
translator/__pycache__/app.cpython-313.pyc ADDED
Binary file (3.77 kB). View file
 
translator/__pycache__/translator.cpython-313.pyc ADDED
Binary file (7.95 kB). View file
 
translator/app.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify, redirect, url_for
2
+ import os
3
+ import platform
4
+ import sys
5
+ from .translator import Translator
6
+
7
+ # Create Flask application
8
+ app = Flask(__name__, static_folder='static', static_url_path='/static')
9
+ translator = None # Initialize lazily to improve startup time
10
+
11
+ # Configure for Hugging Face Spaces
12
+ is_hf_spaces = os.environ.get('SPACE_ID') is not None
13
+ app.config['HF_SPACES'] = is_hf_spaces
14
+
15
+ # Print diagnostic information on startup
16
+ print(f"Starting LinguaSync Translator for {'Hugging Face Spaces' if is_hf_spaces else 'local development'}")
17
+ print(f"Python version: {platform.python_version()}")
18
+ print(f"Platform: {platform.system()} {platform.release()}")
19
+
20
+ # Lazy initialization of translator
21
+ def get_translator():
22
+ global translator
23
+ if translator is None:
24
+ print("Initializing translator...")
25
+ translator = Translator()
26
+ return translator
27
+
28
+ @app.route('/')
29
+ def index():
30
+ translator_instance = get_translator()
31
+ languages = translator_instance.get_available_languages()
32
+ return render_template('index.html', languages=languages)
33
+
34
+ @app.route('/translate', methods=['POST'])
35
+ def translate_text():
36
+ translator_instance = get_translator()
37
+ text = request.form.get('text', '')
38
+ source_lang = request.form.get('source_lang', 'hi')
39
+ target_lang = request.form.get('target_lang', 'en')
40
+
41
+ if not text:
42
+ return jsonify({'status': 'completed', 'translation': ''})
43
+
44
+ # Start the translation process asynchronously
45
+ result = translator_instance.translate(text, source_lang, target_lang)
46
+ return jsonify(result)
47
+
48
+ @app.route('/translation_status', methods=['GET'])
49
+ def translation_status():
50
+ translator_instance = get_translator()
51
+ task_id = request.args.get('task_id', '')
52
+
53
+ if not task_id:
54
+ return jsonify({'status': 'error', 'message': 'No task ID provided'})
55
+
56
+ # Check the status of the translation
57
+ result = translator_instance.get_translation_result(task_id)
58
+ return jsonify(result)
59
+
60
+ @app.route('/health', methods=['GET'])
61
+ def health_check():
62
+ """Health check endpoint for monitoring"""
63
+ translator_instance = get_translator()
64
+ health_data = {
65
+ 'status': 'ok',
66
+ 'languages_available': list(translator_instance.languages.keys()),
67
+ 'platform': 'Hugging Face Spaces' if app.config.get('HF_SPACES') else 'Local'
68
+ }
69
+ return jsonify(health_data)
70
+
71
+ if __name__ == '__main__':
72
+ app.run(debug=True)
translator/static/style.css ADDED
@@ -0,0 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Copy of main style.css for app-level static directory */
2
+
3
+ /* Magic UI Styles */
4
+ :root {
5
+ --gradient-start: #6366f1;
6
+ --gradient-mid: #8b5cf6;
7
+ --gradient-end: #d946ef;
8
+ --shadow-color: rgba(99, 102, 241, 0.2);
9
+ --glow-color: rgba(139, 92, 246, 0.6);
10
+ }
11
+
12
+ /* Enhanced translator card */
13
+ .magic-card {
14
+ border-radius: 20px;
15
+ overflow: hidden;
16
+ transition: all 0.5s ease;
17
+ box-shadow: 0 10px 40px var(--shadow-color) !important;
18
+ border: none;
19
+ margin: 20px 0;
20
+ }
21
+
22
+ .magic-card:hover {
23
+ box-shadow: 0 15px 60px var(--shadow-color) !important;
24
+ transform: translateY(-5px);
25
+ }
26
+
27
+ .card-header.bg-gradient {
28
+ background: linear-gradient(135deg, var(--gradient-start), var(--gradient-mid), var(--gradient-end));
29
+ padding: 20px;
30
+ border: none;
31
+ }
32
+
33
+ /* Professional buttons */
34
+ .btn-primary {
35
+ background-color: #4361ee;
36
+ color: white;
37
+ border: none;
38
+ position: relative;
39
+ transition: all 0.3s ease;
40
+ }
41
+
42
+ .btn-primary:hover, .btn-primary:focus {
43
+ background-color: #3a56d4;
44
+ color: white;
45
+ box-shadow: 0 2px 5px rgba(67, 97, 238, 0.2);
46
+ transform: translateY(-1px);
47
+ }
48
+
49
+ /* Translate button special effect */
50
+ .translate-btn {
51
+ font-weight: 500;
52
+ letter-spacing: 0.3px;
53
+ }
54
+
55
+ /* Swap button animation */
56
+ .swap-btn {
57
+ transition: all 0.3s ease;
58
+ background-color: #f8f9fa;
59
+ border: 1px solid #e9ecef;
60
+ color: #495057;
61
+ }
62
+
63
+ .swap-btn:hover {
64
+ background-color: #e9ecef;
65
+ transform: scale(1.05);
66
+ }
67
+
68
+ /* Enhanced form controls */
69
+ .enhanced-select {
70
+ border: 2px solid #e2e8f0;
71
+ padding: 12px 45px 12px 20px;
72
+ border-radius: 12px;
73
+ font-weight: 500;
74
+ background-image: none;
75
+ transition: all 0.3s ease;
76
+ }
77
+
78
+ .enhanced-select:focus {
79
+ border-color: var(--gradient-mid);
80
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.25);
81
+ }
82
+
83
+ .language-select-icon {
84
+ top: 50%;
85
+ right: 18px;
86
+ transform: translateY(-50%);
87
+ color: var(--gradient-mid);
88
+ }
89
+
90
+ /* Magic textarea */
91
+ .magic-textarea {
92
+ border: 2px solid #e2e8f0;
93
+ border-radius: 15px;
94
+ transition: all 0.3s ease;
95
+ font-size: 1.05rem;
96
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05) !important;
97
+ }
98
+
99
+ .magic-textarea:focus {
100
+ border-color: var(--gradient-mid);
101
+ box-shadow: 0 5px 25px rgba(139, 92, 246, 0.15) !important;
102
+ }
103
+
104
+ .text-input-decoration {
105
+ position: absolute;
106
+ top: 10px;
107
+ right: 10px;
108
+ width: 50px;
109
+ height: 50px;
110
+ border-radius: 50%;
111
+ background: radial-gradient(circle, rgba(139, 92, 246, 0.1) 0%, rgba(139, 92, 246, 0) 70%);
112
+ pointer-events: none;
113
+ }
114
+
115
+ /* Translation result container */
116
+ .result-container {
117
+ background: linear-gradient(to right bottom, #ffffff, #f9fafb);
118
+ border: none;
119
+ border-radius: 15px;
120
+ padding: 25px;
121
+ min-height: 150px;
122
+ position: relative;
123
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05) !important;
124
+ }
125
+
126
+ .translation-indicator {
127
+ position: absolute;
128
+ top: 10px;
129
+ right: 10px;
130
+ opacity: 0.7;
131
+ }
132
+
133
+ /* Progress bar */
134
+ .progress {
135
+ background-color: #f0f3f8;
136
+ }
137
+
138
+ .progress-bar {
139
+ animation: progress-pulse 1.5s infinite;
140
+ }
141
+
142
+ @keyframes progress-pulse {
143
+ 0% { opacity: 0.8; }
144
+ 50% { opacity: 1; }
145
+ 100% { opacity: 0.8; }
146
+ }
147
+
148
+ /* Feature cards */
149
+ .feature-card {
150
+ border-radius: 16px;
151
+ overflow: hidden;
152
+ transition: all 0.3s ease;
153
+ }
154
+
155
+ .feature-card:hover {
156
+ transform: translateY(-10px);
157
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1) !important;
158
+ }
159
+
160
+ .feature-icon {
161
+ width: 80px;
162
+ height: 80px;
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ margin-bottom: 20px;
167
+ transition: all 0.3s ease;
168
+ }
169
+
170
+ .feature-card:hover .feature-icon {
171
+ transform: scale(1.1);
172
+ }
173
+
174
+ /* Custom language select styling */
175
+ .language-select-wrapper {
176
+ display: block;
177
+ position: relative;
178
+ }
179
+
180
+ .language-select-icon {
181
+ top: 50%;
182
+ right: 12px;
183
+ transform: translateY(-50%);
184
+ pointer-events: none;
185
+ color: var(--primary-color);
186
+ }
187
+
188
+ .language-select-wrapper select {
189
+ appearance: none;
190
+ -webkit-appearance: none;
191
+ -moz-appearance: none;
192
+ padding-right: 30px; /* Space for the icon */
193
+ cursor: pointer;
194
+ background-color: white;
195
+ border-color: var(--border-color);
196
+ transition: all 0.2s;
197
+ }
198
+
199
+ .language-select-wrapper select:hover {
200
+ border-color: var(--primary-color);
201
+ }
202
+
203
+ .language-select-wrapper select:focus {
204
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25);
205
+ border-color: var(--primary-color);
206
+ outline: none;
207
+ }
208
+
209
+ .highlight-select {
210
+ animation: pulse-border 0.8s ease;
211
+ }
212
+
213
+ @keyframes pulse-border {
214
+ 0% { border-color: var(--border-color); }
215
+ 50% { border-color: var(--primary-color); }
216
+ 100% { border-color: var(--border-color); }
217
+ }
218
+
219
+ /* Fix select option styles */
220
+ .form-select option {
221
+ padding: 10px;
222
+ font-size: 0.95rem;
223
+ }
224
+
225
+ /* Fix for textarea and text input fields */
226
+ textarea.form-control {
227
+ white-space: normal !important;
228
+ word-wrap: break-word !important;
229
+ overflow-wrap: break-word !important;
230
+ resize: vertical !important;
231
+ }
232
+
233
+ #translationResult p {
234
+ white-space: pre-wrap !important;
235
+ word-wrap: break-word !important;
236
+ overflow-wrap: break-word !important;
237
+ }
238
+
239
+ /* Improve UI aesthetics */
240
+ .translator-card {
241
+ transition: all 0.3s ease;
242
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08) !important;
243
+ }
244
+
245
+ .translator-card:hover {
246
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12) !important;
247
+ transform: translateY(-2px);
248
+ }
249
+
250
+ /* Improve focus states for accessibility */
251
+ .form-control:focus, .form-select:focus, .btn:focus {
252
+ box-shadow: 0 0 0 0.25rem rgba(99, 102, 241, 0.25) !important;
253
+ }
254
+
255
+ /* Add subtle hover effects to buttons */
256
+ .btn:hover {
257
+ transform: translateY(-1px);
258
+ transition: all 0.2s ease;
259
+ }
260
+
261
+ /* Action buttons styling */
262
+ .action-btn {
263
+ background-color: #f8f9fa;
264
+ border: 1px solid #e9ecef;
265
+ color: #495057;
266
+ transition: all 0.2s ease;
267
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
268
+ }
269
+
270
+ .action-btn:hover {
271
+ background-color: #e9ecef;
272
+ color: #212529;
273
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08);
274
+ }
275
+
276
+ /* Animated typing effect for the logo */
277
+ .typing-animation {
278
+ border-right: .15em solid var(--primary-color);
279
+ white-space: nowrap;
280
+ margin: 0 auto;
281
+ letter-spacing: .15em;
282
+ animation:
283
+ typing 3.5s steps(30, end),
284
+ blink-caret .75s step-end infinite;
285
+ display: inline-block;
286
+ }
287
+
288
+ @keyframes typing {
289
+ from { width: 0 }
290
+ to { width: 100% }
291
+ }
292
+
293
+ @keyframes blink-caret {
294
+ from, to { border-color: transparent }
295
+ 50% { border-color: var(--primary-color); }
296
+ }
297
+
298
+ /* Tooltip styling */
299
+ .custom-tooltip {
300
+ position: relative;
301
+ display: inline-block;
302
+ }
303
+
304
+ .custom-tooltip .tooltiptext {
305
+ visibility: hidden;
306
+ width: 120px;
307
+ background-color: #555;
308
+ color: #fff;
309
+ text-align: center;
310
+ border-radius: 6px;
311
+ padding: 5px;
312
+ position: absolute;
313
+ z-index: 1;
314
+ bottom: 125%;
315
+ left: 50%;
316
+ margin-left: -60px;
317
+ opacity: 0;
318
+ transition: opacity 0.3s;
319
+ }
320
+
321
+ .custom-tooltip:hover .tooltiptext {
322
+ visibility: visible;
323
+ opacity: 1;
324
+ }
325
+
326
+ /* Language badge styling */
327
+ .lang-badge {
328
+ border-radius: 50%;
329
+ width: 30px;
330
+ height: 30px;
331
+ display: flex;
332
+ align-items: center;
333
+ justify-content: center;
334
+ font-weight: bold;
335
+ margin-right: 5px;
336
+ }
337
+
338
+ /* History item hover effect */
339
+ .list-group-item {
340
+ transition: all 0.3s ease;
341
+ }
342
+
343
+ .list-group-item:hover {
344
+ transform: translateY(-2px);
345
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
346
+ }
347
+
348
+ /* Progress bar pulsating effect */
349
+ @keyframes pulse {
350
+ 0% { opacity: 0.6; }
351
+ 50% { opacity: 1; }
352
+ 100% { opacity: 0.6; }
353
+ }
354
+
355
+ .progress-bar {
356
+ animation: pulse 1.5s infinite;
357
+ }
358
+
359
+ /* Responsive adjustments */
360
+ @media (max-width: 992px) {
361
+ .container {
362
+ padding: 15px;
363
+ }
364
+
365
+ .typing-animation {
366
+ letter-spacing: 0.1em;
367
+ }
368
+ }
369
+
370
+ @media (max-width: 768px) {
371
+ .container {
372
+ padding: 12px;
373
+ }
374
+
375
+ h1 {
376
+ font-size: 1.5rem;
377
+ }
378
+
379
+ .form-control, .form-select {
380
+ font-size: 14px;
381
+ }
382
+
383
+ .typing-animation {
384
+ letter-spacing: 0.05em;
385
+ animation: none; /* Disable typing animation on mobile */
386
+ }
387
+
388
+ .custom-tooltip .tooltiptext {
389
+ width: 100px;
390
+ margin-left: -50px;
391
+ }
392
+
393
+ .lang-badge {
394
+ width: 24px;
395
+ height: 24px;
396
+ font-size: 12px;
397
+ }
398
+ }
399
+
400
+ @media (max-width: 576px) {
401
+ .container {
402
+ padding: 10px;
403
+ }
404
+
405
+ h1 {
406
+ font-size: 1.3rem;
407
+ }
408
+
409
+ .form-control, .form-select {
410
+ font-size: 13px;
411
+ }
412
+
413
+ .lang-badge {
414
+ width: 20px;
415
+ height: 20px;
416
+ font-size: 10px;
417
+ margin-right: 2px;
418
+ }
419
+
420
+ .custom-tooltip .tooltiptext {
421
+ display: none; /* Hide tooltips on very small screens */
422
+ }
423
+ }
424
+
425
+ /* Fix touch interactions on mobile devices */
426
+ @media (hover: none) {
427
+ .list-group-item:hover {
428
+ transform: none;
429
+ box-shadow: none;
430
+ }
431
+
432
+ .custom-tooltip:hover .tooltiptext {
433
+ visibility: hidden;
434
+ opacity: 0;
435
+ }
436
+ }
translator/templates/base.html ADDED
@@ -0,0 +1,626 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, maximum-scale=1.0, user-scalable=no">
6
+ <title>TranslateNow - Professional Translation Platform</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
13
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
14
+ <style>
15
+ :root {
16
+ --primary-color: #6366f1;
17
+ --secondary-color: #8b5cf6;
18
+ --accent-color: #d946ef;
19
+ --text-color: #1e293b;
20
+ --light-bg: #f8fafc;
21
+ --border-color: #e2e8f0;
22
+ --sidebar-width: 320px;
23
+ --sidebar-width-mobile: 85vw;
24
+ --header-height: 64px;
25
+ --header-height-mobile: 56px;
26
+ --transition-speed: 0.3s;
27
+ }
28
+
29
+ body {
30
+ background: linear-gradient(135deg, #f1f5f9, #f8fafc);
31
+ min-height: 100vh;
32
+ font-family: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
33
+ color: var(--text-color);
34
+ margin: 0;
35
+ padding: 0;
36
+ overflow-x: hidden;
37
+ background-attachment: fixed;
38
+ }
39
+
40
+ .app-container {
41
+ display: flex;
42
+ width: 100%;
43
+ min-height: 100vh;
44
+ min-height: calc(var(--vh, 1vh) * 100); /* Mobile viewport height fix */
45
+ }
46
+
47
+ .main-content {
48
+ flex: 1;
49
+ padding: 20px;
50
+ padding-top: var(--header-height);
51
+ transition: margin var(--transition-speed) ease;
52
+ }
53
+
54
+ /* Sidebar */
55
+ .sidebar {
56
+ width: var(--sidebar-width);
57
+ background: white;
58
+ box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
59
+ height: 100vh;
60
+ position: fixed;
61
+ top: 0;
62
+ right: 0;
63
+ z-index: 1000;
64
+ transform: translateX(100%);
65
+ transition: transform var(--transition-speed) ease;
66
+ overflow-y: auto;
67
+ display: flex;
68
+ flex-direction: column;
69
+ }
70
+
71
+ .sidebar.open {
72
+ transform: translateX(0);
73
+ }
74
+
75
+ .sidebar-header {
76
+ display: flex;
77
+ justify-content: space-between;
78
+ align-items: center;
79
+ padding: 15px 20px;
80
+ background: var(--primary-color);
81
+ color: white;
82
+ height: var(--header-height);
83
+ }
84
+
85
+ .sidebar-header h5 {
86
+ margin: 0;
87
+ font-weight: 600;
88
+ }
89
+
90
+ .sidebar-content {
91
+ flex: 1;
92
+ padding: 20px;
93
+ overflow-y: auto;
94
+ }
95
+
96
+ .sidebar-footer {
97
+ padding: 15px 20px;
98
+ border-top: 1px solid var(--border-color);
99
+ background: white;
100
+ }
101
+
102
+ /* Header */
103
+ .app-header {
104
+ position: fixed;
105
+ top: 0;
106
+ left: 0;
107
+ right: 0;
108
+ height: var(--header-height);
109
+ background: linear-gradient(135deg, #ffffff, #f8fafc);
110
+ display: flex;
111
+ align-items: center;
112
+ padding: 0 20px;
113
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
114
+ z-index: 990;
115
+ justify-content: space-between;
116
+ border-bottom: 1px solid rgba(226, 232, 240, 0.8);
117
+ }
118
+
119
+ .app-title {
120
+ display: flex;
121
+ align-items: center;
122
+ margin: 0;
123
+ }
124
+
125
+ .logo-circle {
126
+ width: 40px;
127
+ height: 40px;
128
+ border-radius: 50%;
129
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color), var(--accent-color));
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ margin-right: 12px;
134
+ box-shadow: 0 4px 10px rgba(99, 102, 241, 0.3);
135
+ }
136
+
137
+ .logo-circle i {
138
+ font-size: 1.3rem;
139
+ color: white;
140
+ }
141
+
142
+ .logo-text {
143
+ background: linear-gradient(135deg, var(--primary-color), var(--accent-color));
144
+ -webkit-background-clip: text;
145
+ background-clip: text;
146
+ -webkit-text-fill-color: transparent;
147
+ color: transparent;
148
+ font-weight: 700;
149
+ letter-spacing: -0.5px;
150
+ font-size: 1.5rem;
151
+ }
152
+
153
+ .header-actions {
154
+ display: flex;
155
+ gap: 10px;
156
+ }
157
+
158
+ .magic-btn {
159
+ background: #4361ee;
160
+ color: white;
161
+ border-radius: 50%;
162
+ width: 42px;
163
+ height: 42px;
164
+ display: flex;
165
+ align-items: center;
166
+ justify-content: center;
167
+ box-shadow: 0 2px 6px rgba(67, 97, 238, 0.2);
168
+ transition: all 0.3s ease;
169
+ border: none;
170
+ }
171
+
172
+ .magic-btn:hover {
173
+ background: #3a56d4;
174
+ transform: translateY(-2px);
175
+ box-shadow: 0 4px 8px rgba(67, 97, 238, 0.3);
176
+ }
177
+
178
+ @keyframes pulse-glow {
179
+ 0% { box-shadow: 0 4px 10px rgba(99, 102, 241, 0.3); }
180
+ 50% { box-shadow: 0 4px 20px rgba(99, 102, 241, 0.5); }
181
+ 100% { box-shadow: 0 4px 10px rgba(99, 102, 241, 0.3); }
182
+ }
183
+
184
+ .pulse-animation {
185
+ animation: pulse-glow 1s ease;
186
+ }
187
+
188
+ /* Main card */
189
+ .translator-card {
190
+ background-color: white;
191
+ border-radius: 16px;
192
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
193
+ overflow: hidden;
194
+ margin-bottom: 20px;
195
+ border: none;
196
+ }
197
+
198
+ .form-group {
199
+ margin-bottom: 25px;
200
+ }
201
+
202
+ h1 {
203
+ color: var(--primary-color);
204
+ margin-bottom: 20px;
205
+ font-weight: 700;
206
+ letter-spacing: -0.5px;
207
+ }
208
+
209
+ #loadingSpinner {
210
+ display: none;
211
+ }
212
+
213
+ #translationResult {
214
+ margin-top: 15px;
215
+ padding: 20px;
216
+ border-radius: 12px;
217
+ background-color: var(--light-bg);
218
+ border-left: 4px solid var(--accent-color);
219
+ min-height: 100px;
220
+ transition: all 0.3s ease;
221
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
222
+ }
223
+
224
+ .btn {
225
+ transition: all 0.3s ease;
226
+ border-radius: 8px;
227
+ padding: 10px 24px;
228
+ font-weight: 500;
229
+ }
230
+
231
+ .btn-primary {
232
+ background: var(--primary-color);
233
+ border: none;
234
+ }
235
+
236
+ .btn-primary:hover {
237
+ background: var(--secondary-color);
238
+ transform: translateY(-1px);
239
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.25);
240
+ }
241
+
242
+ .form-select, .form-control {
243
+ border-radius: 8px;
244
+ border: 1px solid var(--border-color);
245
+ padding: 12px 16px;
246
+ transition: all 0.3s ease;
247
+ font-size: 0.95rem;
248
+ }
249
+
250
+ .form-select:focus, .form-control:focus {
251
+ border-color: var(--accent-color);
252
+ box-shadow: 0 0 0 3px rgba(165, 180, 252, 0.25);
253
+ }
254
+
255
+ /* Animation for text appearance */
256
+ @keyframes textAppear {
257
+ from { opacity: 0; transform: translateY(10px); }
258
+ to { opacity: 1; transform: translateY(0); }
259
+ }
260
+
261
+ #translatedText {
262
+ animation: textAppear 0.5s ease;
263
+ }
264
+
265
+ .card {
266
+ border: none;
267
+ border-radius: 12px;
268
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
269
+ overflow: hidden;
270
+ }
271
+
272
+ .card-header {
273
+ background-color: white;
274
+ border-bottom: 1px solid var(--border-color);
275
+ padding: 15px 20px;
276
+ }
277
+
278
+ .card-body {
279
+ padding: 25px;
280
+ }
281
+
282
+ .list-group-item {
283
+ border: 1px solid var(--border-color);
284
+ margin-bottom: 8px;
285
+ border-radius: 8px !important;
286
+ transition: all 0.2s ease;
287
+ }
288
+
289
+ .list-group-item:hover {
290
+ transform: translateY(-1px);
291
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
292
+ }
293
+
294
+ .badge {
295
+ font-weight: 500;
296
+ padding: 5px 10px;
297
+ }
298
+
299
+ /* Toggle sidebar button */
300
+ .toggle-sidebar {
301
+ background: none;
302
+ border: none;
303
+ cursor: pointer;
304
+ font-size: 1.5rem;
305
+ color: var(--primary-color);
306
+ padding: 5px;
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ border-radius: 8px;
311
+ transition: all 0.2s;
312
+ position: relative;
313
+ }
314
+
315
+ .toggle-sidebar:hover {
316
+ background: rgba(99, 102, 241, 0.1);
317
+ }
318
+
319
+ .history-badge {
320
+ position: absolute;
321
+ top: -5px;
322
+ right: -5px;
323
+ font-size: 0.7rem;
324
+ padding: 3px 6px;
325
+ background: var(--primary-color);
326
+ color: white;
327
+ border-radius: 10px;
328
+ }
329
+
330
+ /* Floating action button */
331
+ .fab {
332
+ position: fixed;
333
+ bottom: 20px;
334
+ right: 20px;
335
+ width: 56px;
336
+ height: 56px;
337
+ border-radius: 50%;
338
+ background: var(--primary-color);
339
+ color: white;
340
+ display: flex;
341
+ align-items: center;
342
+ justify-content: center;
343
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
344
+ cursor: pointer;
345
+ z-index: 900;
346
+ transition: all 0.3s;
347
+ }
348
+
349
+ .fab:hover {
350
+ background: var(--secondary-color);
351
+ transform: translateY(-3px) scale(1.05);
352
+ box-shadow: 0 6px 16px rgba(99, 102, 241, 0.5);
353
+ }
354
+
355
+ .fab i {
356
+ font-size: 1.5rem;
357
+ }
358
+
359
+ /* Responsive */
360
+ @media (max-width: 992px) {
361
+ .main-content {
362
+ padding: 15px;
363
+ padding-top: calc(var(--header-height) + 10px);
364
+ }
365
+
366
+ .card-body {
367
+ padding: 20px;
368
+ }
369
+
370
+ .form-group {
371
+ margin-bottom: 20px;
372
+ }
373
+ }
374
+
375
+ @media (max-width: 768px) {
376
+ :root {
377
+ --header-height: var(--header-height-mobile);
378
+ }
379
+
380
+ .main-content {
381
+ padding: 12px;
382
+ padding-top: calc(var(--header-height) + 10px);
383
+ }
384
+
385
+ .sidebar {
386
+ width: var(--sidebar-width-mobile);
387
+ }
388
+
389
+ .app-title span {
390
+ font-size: 1.1rem;
391
+ }
392
+
393
+ .app-title i {
394
+ font-size: 1.3rem;
395
+ }
396
+
397
+ .form-select, .form-control {
398
+ padding: 10px 14px;
399
+ font-size: 0.9rem;
400
+ }
401
+
402
+ .btn {
403
+ padding: 8px 16px;
404
+ font-size: 0.9rem;
405
+ }
406
+
407
+ h1 {
408
+ font-size: 1.5rem;
409
+ margin-bottom: 15px;
410
+ }
411
+
412
+ .card-body {
413
+ padding: 15px;
414
+ }
415
+
416
+ .form-group {
417
+ margin-bottom: 15px;
418
+ }
419
+
420
+ #translationResult {
421
+ min-height: 80px;
422
+ padding: 15px;
423
+ }
424
+
425
+ .app-header {
426
+ padding: 0 15px;
427
+ }
428
+ }
429
+
430
+ @media (max-width: 576px) {
431
+ .main-content {
432
+ padding: 10px;
433
+ padding-top: calc(var(--header-height) + 5px);
434
+ }
435
+
436
+ .sidebar {
437
+ width: 100%;
438
+ }
439
+
440
+ .translator-card {
441
+ border-radius: 12px;
442
+ margin-bottom: 15px;
443
+ }
444
+
445
+ .card-body {
446
+ padding: 12px;
447
+ }
448
+
449
+ .form-select, .form-control {
450
+ padding: 8px 12px;
451
+ font-size: 0.85rem;
452
+ }
453
+
454
+ .btn {
455
+ padding: 6px 12px;
456
+ font-size: 0.85rem;
457
+ }
458
+
459
+ #translationResult {
460
+ padding: 12px;
461
+ min-height: 60px;
462
+ }
463
+
464
+ .fab {
465
+ width: 48px;
466
+ height: 48px;
467
+ bottom: 15px;
468
+ right: 15px;
469
+ }
470
+
471
+ .fab i {
472
+ font-size: 1.2rem;
473
+ }
474
+
475
+ footer {
476
+ font-size: 0.75rem;
477
+ }
478
+ }
479
+
480
+ /* Fix for mobile viewport height issues */
481
+ @supports (-webkit-touch-callout: none) {
482
+ .app-container, .sidebar {
483
+ height: -webkit-fill-available;
484
+ }
485
+ }
486
+ </style>
487
+ </head>
488
+ <body>
489
+ <div class="app-container">
490
+ <!-- Header -->
491
+ <header class="app-header">
492
+ <h1 class="app-title">
493
+ <div class="logo-circle"><i class="bi bi-translate"></i></div>
494
+ <span class="d-none d-sm-inline logo-text">TranslateNow</span>
495
+ <span class="d-inline d-sm-none logo-text">TN</span>
496
+ </h1>
497
+ <div class="header-actions">
498
+ <button class="toggle-sidebar magic-btn" id="toggleSidebar" title="View translation history">
499
+ <i class="bi bi-clock-history"></i>
500
+ <span class="history-badge" id="historyCount">0</span>
501
+ </button>
502
+ </div>
503
+ </header>
504
+
505
+ <!-- Main Content Area -->
506
+ <div class="main-content animate__animated animate__fadeIn" style="transition: all 0.3s ease;">
507
+ {% block content %}{% endblock %}
508
+
509
+ <footer class="mt-4 pt-3 text-center">
510
+ <p class="text-muted">© 2025 TranslateNow <span class="d-none d-sm-inline">| <i class="bi bi-heart-fill text-danger"></i> Powered by Hugging Face</span></p>
511
+ </footer>
512
+ </div>
513
+
514
+ <!-- Sidebar for History -->
515
+ <aside class="sidebar" id="sidebar">
516
+ <div class="sidebar-header">
517
+ <h5><i class="bi bi-clock-history me-2"></i> Translation History</h5>
518
+ <button class="btn btn-sm text-white" id="closeSidebar">
519
+ <i class="bi bi-x-lg"></i>
520
+ </button>
521
+ </div>
522
+ <div class="sidebar-content">
523
+ <div id="translationHistory" class="list-group">
524
+ <!-- Translation history will be added here -->
525
+ <div class="text-center text-muted py-5">
526
+ <i class="bi bi-clock-history fs-1 d-block mb-3"></i>
527
+ <p>No translation history yet</p>
528
+ <p class="small">Your recent translations will appear here</p>
529
+ </div>
530
+ </div>
531
+ </div>
532
+ <div class="sidebar-footer">
533
+ <button class="btn btn-outline-danger btn-sm w-100" id="clearHistoryBtn">
534
+ <i class="bi bi-trash"></i> Clear All History
535
+ </button>
536
+ </div>
537
+ </aside>
538
+
539
+ <!-- Floating action button for mobile -->
540
+ <div class="fab d-md-none" id="mobileSidebarToggle">
541
+ <i class="bi bi-clock-history"></i>
542
+ </div>
543
+ </div>
544
+
545
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
546
+
547
+ <script>
548
+ // Sidebar toggle
549
+ document.addEventListener('DOMContentLoaded', function() {
550
+ const sidebar = document.getElementById('sidebar');
551
+ const toggleSidebar = document.getElementById('toggleSidebar');
552
+ const closeSidebar = document.getElementById('closeSidebar');
553
+ const mobileSidebarToggle = document.getElementById('mobileSidebarToggle');
554
+
555
+ // Add subtle animation to the logo
556
+ const logoCircle = document.querySelector('.logo-circle');
557
+ setInterval(() => {
558
+ logoCircle.classList.add('pulse-animation');
559
+ setTimeout(() => logoCircle.classList.remove('pulse-animation'), 1000);
560
+ }, 3000);
561
+
562
+ toggleSidebar.addEventListener('click', function() {
563
+ sidebar.classList.toggle('open');
564
+ }); mobileSidebarToggle.addEventListener('click', function() {
565
+ sidebar.classList.toggle('open');
566
+ });
567
+
568
+ closeSidebar.addEventListener('click', function() {
569
+ sidebar.classList.remove('open');
570
+ });
571
+
572
+ // Close sidebar when clicking outside
573
+ document.addEventListener('click', function(event) {
574
+ if (!sidebar.contains(event.target) &&
575
+ event.target !== toggleSidebar &&
576
+ event.target !== mobileSidebarToggle &&
577
+ !toggleSidebar.contains(event.target) &&
578
+ !mobileSidebarToggle.contains(event.target)) {
579
+ sidebar.classList.remove('open');
580
+ }
581
+ });
582
+
583
+ // Add touch swipe detection for mobile
584
+ let touchstartX = 0;
585
+ let touchendX = 0;
586
+
587
+ document.addEventListener('touchstart', e => {
588
+ touchstartX = e.changedTouches[0].screenX;
589
+ });
590
+
591
+ document.addEventListener('touchend', e => {
592
+ touchendX = e.changedTouches[0].screenX;
593
+ handleSwipe();
594
+ });
595
+
596
+ function handleSwipe() {
597
+ const swipeThreshold = 100; // Minimum pixels for swipe detection
598
+
599
+ // Right to left swipe (open sidebar)
600
+ if (touchendX < touchstartX - swipeThreshold && window.innerWidth <= 768) {
601
+ sidebar.classList.add('open');
602
+ }
603
+
604
+ // Left to right swipe (close sidebar)
605
+ if (touchendX > touchstartX + swipeThreshold && sidebar.classList.contains('open')) {
606
+ sidebar.classList.remove('open');
607
+ }
608
+ }
609
+
610
+ // Add viewport height fix for mobile browsers
611
+ function setAppHeight() {
612
+ const vh = window.innerHeight * 0.01;
613
+ document.documentElement.style.setProperty('--vh', `${vh}px`);
614
+ }
615
+
616
+ // Set initial height
617
+ setAppHeight();
618
+
619
+ // Reset on resize
620
+ window.addEventListener('resize', setAppHeight);
621
+ });
622
+ </script>
623
+
624
+ {% block scripts %}{% endblock %}
625
+ </body>
626
+ </html>
translator/templates/index.html ADDED
@@ -0,0 +1,839 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="translator-card magic-card">
5
+ <div class="card-header bg-gradient">
6
+ <div class="row align-items-center">
7
+ <div class="col">
8
+ <h5 class="mb-0 fw-bold text-white"><i class="bi bi-translate me-2"></i>TranslateNow</h5>
9
+ <p class="text-white-50 mb-0 small"><i class="bi bi-stars me-1"></i>Professional translation platform</p>
10
+ </div>
11
+ <div class="col-auto">
12
+ <button class="btn btn-sm btn-light px-3 shadow-sm action-btn" id="clearBtn" title="Clear all text">
13
+ <i class="bi bi-eraser"></i> Clear
14
+ </button>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ <div class="card-body">
19
+ <div class="row">
20
+ <div class="col-lg-5 col-md-12 mb-4 mb-lg-0">
21
+ <div class="form-group">
22
+ <div class="d-flex justify-content-between align-items-center">
23
+ <label for="sourceLanguage" class="form-label fw-medium">Source Language:</label>
24
+ <span class="badge bg-primary bg-opacity-10 text-primary" id="sourceCharCount">0 characters</span>
25
+ </div>
26
+ <div class="language-select-wrapper position-relative mb-3">
27
+ <select class="form-select shadow-sm enhanced-select" id="sourceLanguage">
28
+ {% for code, name in languages.items() %}
29
+ <option value="{{ code }}" {% if code == 'hi' %}selected{% endif %}>{{ name }}</option>
30
+ {% endfor %}
31
+ </select>
32
+ <div class="language-select-icon position-absolute">
33
+ <i class="bi bi-translate"></i>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ <div class="form-group">
38
+ <div class="d-flex justify-content-between align-items-center mb-2">
39
+ <label for="sourceText" class="form-label fw-medium">Enter text to translate:</label>
40
+ <div>
41
+ <button class="btn btn-sm btn-light me-1 px-3 action-btn" id="detectLanguageBtn" title="Auto detect language">
42
+ <i class="bi bi-magic me-1"></i>Auto-detect
43
+ </button>
44
+ <button class="btn btn-sm btn-light px-3 action-btn" id="clearSourceBtn" title="Clear text">
45
+ <i class="bi bi-x me-1"></i>Clear
46
+ </button>
47
+ </div>
48
+ </div>
49
+ <div class="position-relative">
50
+ <textarea class="form-control shadow-sm magic-textarea" id="sourceText" rows="8" placeholder="Type or paste your text here..." style="white-space: normal; word-wrap: break-word; resize: vertical; overflow-wrap: break-word; padding: 16px;"></textarea>
51
+ <div class="text-input-decoration"></div>
52
+ </div>
53
+ <div class="d-flex justify-content-end mt-2">
54
+ <button class="btn btn-sm btn-light me-2 px-3 action-btn" id="listenSourceBtn" title="Listen">
55
+ <i class="bi bi-volume-up"></i> <span class="d-none d-sm-inline">Listen</span>
56
+ </button>
57
+ <button class="btn btn-sm btn-light px-3 action-btn" id="copySourceBtn" title="Copy to clipboard">
58
+ <i class="bi bi-clipboard"></i> <span class="d-none d-sm-inline">Copy</span>
59
+ </button>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="col-lg-2 col-md-12 d-flex flex-lg-column flex-row align-items-center justify-content-center mb-4 mb-lg-0">
65
+ <button class="btn btn-light mb-lg-3 me-3 me-lg-0 shadow-sm rounded-circle p-2 p-sm-3 swap-btn" id="swapBtn" title="Swap languages">
66
+ <i class="bi bi-arrow-left-right"></i>
67
+ </button>
68
+ <button class="btn btn-primary shadow-sm px-4 px-sm-5 py-2 py-sm-3 rounded fw-medium translate-btn" id="translateBtn">
69
+ Translate <i class="bi bi-translate ms-1"></i>
70
+ </button>
71
+ <div class="spinner-border text-primary mt-lg-3 ms-3 ms-lg-0" id="loadingSpinner" role="status">
72
+ <span class="visually-hidden">Loading...</span>
73
+ </div>
74
+ <div class="progress mt-lg-3 mt-0 ms-3 ms-lg-0 w-100 d-none d-lg-block" style="height: 6px; border-radius: 10px; overflow: hidden;">
75
+ <div class="progress-bar bg-primary" id="translationProgress" role="progressbar" style="width: 0%"></div>
76
+ </div>
77
+ <!-- Mobile progress bar, horizontal orientation -->
78
+ <div class="progress mt-3 w-100 d-block d-lg-none" style="height: 6px; border-radius: 10px; overflow: hidden;">
79
+ <div class="progress-bar bg-primary" id="translationProgressMobile" role="progressbar" style="width: 0%"></div>
80
+ </div>
81
+ </div>
82
+
83
+ <div class="col-lg-5 col-md-12">
84
+ <div class="form-group">
85
+ <div class="d-flex justify-content-between align-items-center">
86
+ <label for="targetLanguage" class="form-label fw-medium">Target Language:</label>
87
+ <span class="badge bg-primary bg-opacity-10 text-primary" id="targetCharCount">0 characters</span>
88
+ </div>
89
+ <div class="language-select-wrapper position-relative mb-3">
90
+ <select class="form-select shadow-sm enhanced-select" id="targetLanguage">
91
+ {% for code, name in languages.items() %}
92
+ <option value="{{ code }}" {% if code == 'en' %}selected{% endif %}>{{ name }}</option>
93
+ {% endfor %}
94
+ </select>
95
+ <div class="language-select-icon position-absolute">
96
+ <i class="bi bi-translate"></i>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ <div class="form-group">
101
+ <label for="translatedText" class="form-label fw-medium">Translation:</label>
102
+ <div id="translationResult" class="animate__animated shadow-sm result-container">
103
+ <div class="translation-indicator"><i class="bi bi-stars text-primary"></i></div>
104
+ <p id="translatedText" style="white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word;"></p>
105
+ </div>
106
+ <div class="d-flex justify-content-end mt-2">
107
+ <button class="btn btn-sm btn-light me-2 px-3 action-btn" id="listenTargetBtn" title="Listen">
108
+ <i class="bi bi-volume-up"></i> <span class="d-none d-sm-inline">Listen</span>
109
+ </button>
110
+ <button class="btn btn-sm btn-light me-2 px-3 action-btn" id="copyTargetBtn" title="Copy to clipboard">
111
+ <i class="bi bi-clipboard"></i> <span class="d-none d-sm-inline">Copy</span>
112
+ </button>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Small credits section at the bottom -->
121
+ <div class="text-center mt-3 mb-2">
122
+ <small class="text-muted">TranslateNow | <i class="bi bi-shield-check text-success me-1"></i>Secure and accurate translations</small>
123
+ </div>
124
+ {% endblock %}
125
+
126
+ {% block scripts %}
127
+ <script>
128
+ let translationInProgress = false;
129
+ let currentTaskId = null;
130
+ let pollingInterval = null;
131
+ let progressInterval = null;
132
+ let typingTimer;
133
+ const doneTypingInterval = 800; // Wait time after typing stops (ms)
134
+ const translationHistory = [];
135
+ const maxHistoryItems = 15;
136
+
137
+ // Initialize all UI elements and event listeners
138
+ document.addEventListener('DOMContentLoaded', function() {
139
+ // Initialize character counters
140
+ updateCharCount('sourceText', 'sourceCharCount');
141
+
142
+ // Auto-translate after typing stops
143
+ const sourceTextArea = document.getElementById('sourceText');
144
+ sourceTextArea.addEventListener('keyup', function() {
145
+ clearTimeout(typingTimer);
146
+ updateCharCount('sourceText', 'sourceCharCount');
147
+ if (this.value) {
148
+ typingTimer = setTimeout(translateText, doneTypingInterval);
149
+ }
150
+ });
151
+
152
+ // Stop the timer if the user continues typing
153
+ sourceTextArea.addEventListener('keydown', function() {
154
+ clearTimeout(typingTimer);
155
+ });
156
+
157
+ // Main translation button
158
+ document.getElementById('translateBtn').addEventListener('click', translateText);
159
+
160
+ // Swap languages button
161
+ document.getElementById('swapBtn').addEventListener('click', swapLanguages);
162
+
163
+ // Add event listeners for language selects
164
+ document.getElementById('sourceLanguage').addEventListener('change', function() {
165
+ // Highlight the dropdown to indicate change
166
+ this.closest('.language-select-wrapper').classList.add('highlight-select');
167
+ setTimeout(() => {
168
+ this.closest('.language-select-wrapper').classList.remove('highlight-select');
169
+ }, 1000);
170
+
171
+ // Auto-translate if there's text
172
+ if (document.getElementById('sourceText').value.trim()) {
173
+ translateText();
174
+ }
175
+ });
176
+
177
+ document.getElementById('targetLanguage').addEventListener('change', function() {
178
+ // Highlight the dropdown to indicate change
179
+ this.closest('.language-select-wrapper').classList.add('highlight-select');
180
+ setTimeout(() => {
181
+ this.closest('.language-select-wrapper').classList.remove('highlight-select');
182
+ }, 1000);
183
+
184
+ // Auto-translate if there's text
185
+ if (document.getElementById('sourceText').value.trim()) {
186
+ translateText();
187
+ }
188
+ });
189
+
190
+ // Add event listeners for copy buttons
191
+ document.getElementById('copySourceBtn').addEventListener('click', function() {
192
+ copyToClipboard('sourceText');
193
+ });
194
+
195
+ document.getElementById('copyTargetBtn').addEventListener('click', function() {
196
+ copyToClipboard('translatedText', true);
197
+ });
198
+
199
+ // Add event listeners for listen buttons
200
+ document.getElementById('listenSourceBtn').addEventListener('click', function() {
201
+ const text = document.getElementById('sourceText').value;
202
+ const lang = document.getElementById('sourceLanguage').value;
203
+ speakText(text, lang);
204
+ });
205
+
206
+ document.getElementById('listenTargetBtn').addEventListener('click', function() {
207
+ const text = document.getElementById('translatedText').textContent;
208
+ const lang = document.getElementById('targetLanguage').value;
209
+ speakText(text, lang);
210
+ });
211
+
212
+ // Add event listener for clear button
213
+ document.getElementById('clearBtn').addEventListener('click', clearAll);
214
+
215
+ // Add event listener for clear history button
216
+ document.getElementById('clearHistoryBtn').addEventListener('click', function() {
217
+ translationHistory.length = 0;
218
+ updateHistoryUI();
219
+ updateHistoryCount();
220
+ // Show empty state and close sidebar
221
+ document.getElementById('sidebar').classList.remove('open');
222
+ });
223
+
224
+ // Clear source text button
225
+ document.getElementById('clearSourceBtn').addEventListener('click', function() {
226
+ const textarea = document.getElementById('sourceText');
227
+ if (!textarea.value.trim()) {
228
+ return;
229
+ }
230
+
231
+ textarea.value = '';
232
+ updateCharCount('sourceText', 'sourceCharCount');
233
+ pulseEffect('sourceText');
234
+
235
+ // Change button style briefly to show action performed
236
+ const originalClass = this.className;
237
+ this.className = 'btn btn-sm btn-danger rounded-pill px-2 py-1';
238
+
239
+ setTimeout(() => {
240
+ this.className = originalClass;
241
+ showToast('Text cleared', 'info');
242
+ }, 300);
243
+ });
244
+
245
+ // Auto-detect language button (uses basic language detection logic)
246
+ document.getElementById('detectLanguageBtn').addEventListener('click', function() {
247
+ const sourceText = document.getElementById('sourceText').value;
248
+ if (!sourceText.trim()) {
249
+ showToast('Please enter text to detect language', 'warning');
250
+ return;
251
+ }
252
+
253
+ // Show loading effect
254
+ const originalContent = this.innerHTML;
255
+ this.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Detecting...';
256
+ this.disabled = true;
257
+ this.classList.add('btn-secondary');
258
+ this.classList.remove('btn-outline-secondary');
259
+
260
+ // Basic language detection based on character sets and patterns
261
+ setTimeout(() => {
262
+ let detectedLang = 'en'; // Default to English
263
+
264
+ // Hindi detection - checks for Devanagari Unicode range
265
+ const hindiPattern = /[\u0900-\u097F]/;
266
+ if (hindiPattern.test(sourceText)) {
267
+ detectedLang = 'hi';
268
+ }
269
+ // Spanish detection - check for Spanish specific characters and common words
270
+ else if (/[áéíóúüñ¿¡]/.test(sourceText.toLowerCase()) ||
271
+ /\b(el|la|los|las|es|están|hola|gracias|buenos|días|cómo|por|qué)\b/.test(sourceText.toLowerCase())) {
272
+ detectedLang = 'es';
273
+ }
274
+ // German detection - check for German specific characters and common words
275
+ else if (/[äöüß]/.test(sourceText.toLowerCase()) ||
276
+ /\b(der|die|das|und|ist|sind|haben|ich|du|sie|wir|nicht|ein|eine)\b/.test(sourceText.toLowerCase())) {
277
+ detectedLang = 'de';
278
+ }
279
+ // French detection - check for French specific characters and common words
280
+ else if (/[éèêëàâäæçéèêëîïôœùûüÿ]/.test(sourceText.toLowerCase()) ||
281
+ /\b(le|la|les|un|une|des|et|est|sont|je|tu|il|elle|nous|vous|ils|elles|ne|pas)\b/.test(sourceText.toLowerCase())) {
282
+ detectedLang = 'fr';
283
+ }
284
+
285
+ document.getElementById('sourceLanguage').value = detectedLang;
286
+
287
+ // Highlight the dropdown to indicate change
288
+ const sourceSelect = document.getElementById('sourceLanguage').closest('.language-select-wrapper');
289
+ sourceSelect.classList.add('animate__animated', 'animate__flash');
290
+ setTimeout(() => sourceSelect.classList.remove('animate__animated', 'animate__flash'), 1000);
291
+
292
+ // Restore button
293
+ this.innerHTML = originalContent;
294
+ this.disabled = false;
295
+ this.classList.remove('btn-secondary');
296
+ this.classList.add('btn-light');
297
+
298
+ // Show feedback toast
299
+ const detectedLanguage = document.querySelector(`#sourceLanguage option[value="${detectedLang}"]`).textContent;
300
+ showToast('Language detected: ' + detectedLanguage, 'success');
301
+
302
+ // Translate if text exists
303
+ if (sourceText.trim()) {
304
+ translateText();
305
+ }
306
+ }, 800);
307
+ });
308
+
309
+ // Create toast container
310
+ createToastContainer();
311
+
312
+ // Load history from local storage if available
313
+ loadHistory();
314
+
315
+ // Update history count badge
316
+ updateHistoryCount();
317
+ });
318
+
319
+ function translateText() {
320
+ const sourceText = document.getElementById('sourceText').value;
321
+ const sourceLang = document.getElementById('sourceLanguage').value;
322
+ const targetLang = document.getElementById('targetLanguage').value;
323
+
324
+ if (!sourceText.trim()) {
325
+ return;
326
+ }
327
+
328
+ // Show loading spinner and progress bar
329
+ document.getElementById('loadingSpinner').style.display = 'inline-block';
330
+ document.getElementById('translateBtn').style.display = 'none';
331
+ startProgressAnimation();
332
+
333
+ // Cancel any ongoing translation
334
+ if (translationInProgress && pollingInterval) {
335
+ clearInterval(pollingInterval);
336
+ }
337
+
338
+ // Create form data
339
+ const formData = new FormData();
340
+ formData.append('text', sourceText);
341
+ formData.append('source_lang', sourceLang);
342
+ formData.append('target_lang', targetLang);
343
+
344
+ // Send translation request
345
+ fetch('/translate', {
346
+ method: 'POST',
347
+ body: formData
348
+ })
349
+ .then(response => response.json())
350
+ .then(data => {
351
+ translationInProgress = true;
352
+ currentTaskId = data.task_id;
353
+
354
+ // Set up polling for translation status
355
+ pollingInterval = setInterval(checkTranslationStatus, 500);
356
+ })
357
+ .catch(error => {
358
+ console.error('Error:', error);
359
+ document.getElementById('translatedText').textContent = 'An error occurred during translation.';
360
+
361
+ // Hide loading spinner
362
+ document.getElementById('loadingSpinner').style.display = 'none';
363
+ document.getElementById('translateBtn').style.display = 'inline-block';
364
+ stopProgressAnimation();
365
+
366
+ // Show error toast
367
+ showToast('Translation failed. Please try again.', 'error');
368
+ });
369
+ }
370
+
371
+ function checkTranslationStatus() {
372
+ if (!currentTaskId) return;
373
+
374
+ fetch(`/translation_status?task_id=${currentTaskId}`)
375
+ .then(response => response.json())
376
+ .then(data => {
377
+ if (data.status === 'completed') {
378
+ // Update the translation result with animation
379
+ const resultElement = document.getElementById('translationResult');
380
+ resultElement.classList.add('animate__animated', 'animate__fadeIn');
381
+
382
+ document.getElementById('translatedText').textContent = data.translation;
383
+ updateCharCount('translatedText', 'targetCharCount', true);
384
+
385
+ // Hide loading spinner
386
+ document.getElementById('loadingSpinner').style.display = 'none';
387
+ document.getElementById('translateBtn').style.display = 'inline-block';
388
+ stopProgressAnimation();
389
+
390
+ // Stop polling
391
+ clearInterval(pollingInterval);
392
+ translationInProgress = false;
393
+
394
+ // Add to history
395
+ const historyItem = {
396
+ source: document.getElementById('sourceText').value,
397
+ sourceLang: document.getElementById('sourceLanguage').value,
398
+ target: data.translation,
399
+ targetLang: document.getElementById('targetLanguage').value,
400
+ timestamp: new Date()
401
+ };
402
+
403
+ addToHistory(historyItem);
404
+ updateHistoryCount();
405
+ saveHistory();
406
+
407
+ // Show success toast
408
+ const sourceLang = document.querySelector(`#sourceLanguage option[value="${historyItem.sourceLang}"]`).textContent;
409
+ const targetLang = document.querySelector(`#targetLanguage option[value="${historyItem.targetLang}"]`).textContent;
410
+ showToast(`Translated from ${sourceLang} to ${targetLang}`);
411
+
412
+ // Remove animation classes after animation completes
413
+ setTimeout(() => {
414
+ resultElement.classList.remove('animate__animated', 'animate__fadeIn');
415
+ }, 1000);
416
+
417
+ currentTaskId = null;
418
+ }
419
+ })
420
+ .catch(error => {
421
+ console.error('Error checking translation status:', error);
422
+ });
423
+ }
424
+
425
+ // Add language swap functionality
426
+ function swapLanguages() {
427
+ const sourceSelect = document.getElementById('sourceLanguage');
428
+ const targetSelect = document.getElementById('targetLanguage');
429
+ const sourceText = document.getElementById('sourceText');
430
+ const translatedText = document.getElementById('translatedText');
431
+
432
+ // Animate the swap
433
+ const sourceSection = sourceSelect.closest('.col-md-5');
434
+ const targetSection = targetSelect.closest('.col-md-5');
435
+
436
+ sourceSection.classList.add('animate__animated', 'animate__fadeOut');
437
+ targetSection.classList.add('animate__animated', 'animate__fadeOut');
438
+
439
+ setTimeout(() => {
440
+ // Swap the selected languages
441
+ const tempLang = sourceSelect.value;
442
+ sourceSelect.value = targetSelect.value;
443
+ targetSelect.value = tempLang;
444
+
445
+ // Swap the text content
446
+ const tempText = sourceText.value;
447
+ sourceText.value = translatedText.textContent;
448
+ updateCharCount('sourceText', 'sourceCharCount');
449
+
450
+ sourceSection.classList.remove('animate__fadeOut');
451
+ targetSection.classList.remove('animate__fadeOut');
452
+ sourceSection.classList.add('animate__fadeIn');
453
+ targetSection.classList.add('animate__fadeIn');
454
+
455
+ setTimeout(() => {
456
+ sourceSection.classList.remove('animate__animated', 'animate__fadeIn');
457
+ targetSection.classList.remove('animate__animated', 'animate__fadeIn');
458
+
459
+ // If we have text to translate after swapping, trigger translation
460
+ if (sourceText.value.trim()) {
461
+ translateText();
462
+ }
463
+ }, 500);
464
+ }, 300);
465
+
466
+ // Show toast
467
+ showToast('Languages swapped');
468
+ }
469
+
470
+ // Helper function to copy text to clipboard
471
+ function copyToClipboard(elementId, isContent = false) {
472
+ const text = isContent
473
+ ? document.getElementById(elementId).textContent
474
+ : document.getElementById(elementId).value;
475
+
476
+ if (!text) return;
477
+
478
+ navigator.clipboard.writeText(text).then(() => {
479
+ // Show a tooltip or some indicator that text was copied
480
+ const btn = isContent ? document.getElementById('copyTargetBtn') : document.getElementById('copySourceBtn');
481
+ const originalHtml = btn.innerHTML;
482
+
483
+ btn.innerHTML = '<i class="bi bi-check2"></i> Copied';
484
+ btn.classList.add('btn-success');
485
+ btn.classList.remove('btn-light');
486
+ btn.classList.remove('btn-outline-primary');
487
+
488
+ setTimeout(() => {
489
+ btn.innerHTML = originalHtml;
490
+ btn.classList.remove('btn-success');
491
+ if (isContent) {
492
+ btn.classList.add('btn-light');
493
+ } else {
494
+ btn.classList.add('btn-light');
495
+ }
496
+ }, 1500);
497
+
498
+ // Show toast notification
499
+ showToast('Text copied to clipboard');
500
+ }).catch(err => {
501
+ showToast('Failed to copy text: ' + err, 'error');
502
+ });
503
+ }
504
+
505
+ // Helper function for text-to-speech
506
+ function speakText(text, lang) {
507
+ if (!text || !lang) return;
508
+
509
+ // Stop any ongoing speech
510
+ if (window.speechSynthesis) {
511
+ window.speechSynthesis.cancel();
512
+
513
+ const utterance = new SpeechSynthesisUtterance(text);
514
+ utterance.lang = lang === 'en' ? 'en-US' :
515
+ lang === 'fr' ? 'fr-FR' :
516
+ lang === 'es' ? 'es-ES' :
517
+ lang === 'de' ? 'de-DE' :
518
+ lang === 'hi' ? 'hi-IN' : 'en-US';
519
+
520
+ // Show toast notification
521
+ showToast('Playing audio', 'info');
522
+
523
+ window.speechSynthesis.speak(utterance);
524
+ } else {
525
+ showToast('Text-to-speech not supported in your browser', 'warning');
526
+ }
527
+ }
528
+
529
+ // Helper function to update character count
530
+ function updateCharCount(textElementId, countElementId, isContent = false) {
531
+ const text = isContent
532
+ ? document.getElementById(textElementId).textContent
533
+ : document.getElementById(textElementId).value;
534
+ const count = text ? text.length : 0;
535
+ const badge = document.getElementById(countElementId);
536
+ badge.textContent = `${count} characters`;
537
+
538
+ // Add visual feedback for long texts
539
+ if (count > 500) {
540
+ badge.classList.add('bg-warning', 'text-dark');
541
+ badge.classList.remove('bg-primary', 'bg-opacity-10', 'text-primary');
542
+ } else {
543
+ badge.classList.add('bg-primary', 'bg-opacity-10', 'text-primary');
544
+ badge.classList.remove('bg-warning', 'text-dark');
545
+ }
546
+ }
547
+
548
+ // Progress bar animation
549
+ function startProgressAnimation() {
550
+ const progressBar = document.getElementById('translationProgress');
551
+ const progressBarMobile = document.getElementById('translationProgressMobile');
552
+ let width = 0;
553
+
554
+ // Reset progress
555
+ progressBar.style.width = '0%';
556
+ progressBarMobile.style.width = '0%';
557
+
558
+ progressInterval = setInterval(() => {
559
+ if (width >= 90) {
560
+ clearInterval(progressInterval);
561
+ } else {
562
+ width += Math.random() * 3;
563
+ if (width > 90) width = 90;
564
+ progressBar.style.width = width + '%';
565
+ progressBarMobile.style.width = width + '%';
566
+ }
567
+ }, 100);
568
+ }
569
+
570
+ function stopProgressAnimation() {
571
+ if (progressInterval) {
572
+ clearInterval(progressInterval);
573
+ }
574
+
575
+ const progressBar = document.getElementById('translationProgress');
576
+ const progressBarMobile = document.getElementById('translationProgressMobile');
577
+ progressBar.style.width = '100%';
578
+ progressBarMobile.style.width = '100%';
579
+
580
+ setTimeout(() => {
581
+ progressBar.style.width = '0%';
582
+ progressBarMobile.style.width = '0%';
583
+ }, 500);
584
+ }
585
+
586
+ // Clear all text fields
587
+ function clearAll() {
588
+ // Animate the clear
589
+ const sourceText = document.getElementById('sourceText');
590
+ const translationResult = document.getElementById('translationResult');
591
+
592
+ if (sourceText.value || document.getElementById('translatedText').textContent) {
593
+ sourceText.classList.add('animate__animated', 'animate__fadeOut');
594
+ translationResult.classList.add('animate__animated', 'animate__fadeOut');
595
+
596
+ setTimeout(() => {
597
+ sourceText.value = '';
598
+ document.getElementById('translatedText').textContent = '';
599
+ updateCharCount('sourceText', 'sourceCharCount');
600
+ updateCharCount('translatedText', 'targetCharCount', true);
601
+
602
+ sourceText.classList.remove('animate__fadeOut');
603
+ translationResult.classList.remove('animate__fadeOut');
604
+ sourceText.classList.add('animate__fadeIn');
605
+ translationResult.classList.add('animate__fadeIn');
606
+
607
+ setTimeout(() => {
608
+ sourceText.classList.remove('animate__animated', 'animate__fadeIn');
609
+ translationResult.classList.remove('animate__animated', 'animate__fadeIn');
610
+ }, 500);
611
+ }, 300);
612
+
613
+ showToast('All text cleared');
614
+ }
615
+ }
616
+
617
+ // Translation history management
618
+ function addToHistory(item) {
619
+ // Add to the beginning of the array
620
+ translationHistory.unshift(item);
621
+
622
+ // Keep only the maximum number of items
623
+ if (translationHistory.length > maxHistoryItems) {
624
+ translationHistory.pop();
625
+ }
626
+
627
+ updateHistoryUI();
628
+
629
+ // Notify user about history
630
+ if (translationHistory.length === 1) {
631
+ // First translation added to history
632
+ const historyBadge = document.querySelector('.history-badge');
633
+ historyBadge.classList.add('animate__animated', 'animate__heartBeat');
634
+ setTimeout(() => {
635
+ historyBadge.classList.remove('animate__animated', 'animate__heartBeat');
636
+ }, 1500);
637
+ }
638
+ }
639
+
640
+ // Toast notification system
641
+ function createToastContainer() {
642
+ const toastContainer = document.createElement('div');
643
+ toastContainer.className = 'position-fixed bottom-0 end-0 p-3';
644
+ toastContainer.style.zIndex = '1080';
645
+ toastContainer.id = 'toastContainer';
646
+ document.body.appendChild(toastContainer);
647
+ }
648
+
649
+ function showToast(message, type = 'success') {
650
+ const toastContainer = document.getElementById('toastContainer');
651
+ const toastId = 'toast-' + Date.now();
652
+ const bgClass = type === 'success' ? 'bg-success' :
653
+ type === 'error' ? 'bg-danger' :
654
+ type === 'warning' ? 'bg-warning' : 'bg-primary';
655
+
656
+ const toast = document.createElement('div');
657
+ toast.className = `toast align-items-center ${bgClass} text-white border-0 animate__animated animate__fadeInUp`;
658
+ toast.id = toastId;
659
+ toast.setAttribute('role', 'alert');
660
+ toast.setAttribute('aria-live', 'assertive');
661
+ toast.setAttribute('aria-atomic', 'true');
662
+
663
+ toast.innerHTML = `
664
+ <div class="d-flex">
665
+ <div class="toast-body">
666
+ ${message}
667
+ </div>
668
+ <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
669
+ </div>
670
+ `;
671
+
672
+ toastContainer.appendChild(toast);
673
+
674
+ // Initialize and show toast
675
+ const bsToast = new bootstrap.Toast(toast, {
676
+ autohide: true,
677
+ delay: 3000
678
+ });
679
+ bsToast.show();
680
+
681
+ // Remove from DOM after hiding
682
+ toast.addEventListener('hidden.bs.toast', function() {
683
+ toast.remove();
684
+ });
685
+ }
686
+
687
+ // Apply pulse animation effect to element
688
+ function pulseEffect(elementId) {
689
+ const element = document.getElementById(elementId);
690
+ element.classList.add('animate__animated', 'animate__pulse');
691
+
692
+ setTimeout(() => {
693
+ element.classList.remove('animate__animated', 'animate__pulse');
694
+ }, 1000);
695
+ }
696
+
697
+ // Update history count badge
698
+ function updateHistoryCount() {
699
+ const historyCount = document.getElementById('historyCount');
700
+ historyCount.textContent = translationHistory.length;
701
+
702
+ if (translationHistory.length > 0) {
703
+ historyCount.style.display = 'inline-block';
704
+ } else {
705
+ historyCount.style.display = 'none';
706
+ }
707
+ }
708
+
709
+ // Save history to local storage
710
+ function saveHistory() {
711
+ try {
712
+ localStorage.setItem('translationHistory', JSON.stringify(translationHistory));
713
+ } catch (e) {
714
+ console.error('Failed to save history to local storage:', e);
715
+ }
716
+ }
717
+
718
+ // Load history from local storage
719
+ function loadHistory() {
720
+ try {
721
+ const savedHistory = localStorage.getItem('translationHistory');
722
+ if (savedHistory) {
723
+ // Parse the saved history and add timestamps back (as Date objects)
724
+ const parsedHistory = JSON.parse(savedHistory);
725
+ parsedHistory.forEach(item => {
726
+ item.timestamp = new Date(item.timestamp);
727
+ });
728
+
729
+ // Replace current history with saved history
730
+ translationHistory.length = 0;
731
+ translationHistory.push(...parsedHistory);
732
+
733
+ // Update UI
734
+ updateHistoryUI();
735
+ updateHistoryCount();
736
+ }
737
+ } catch (e) {
738
+ console.error('Failed to load history from local storage:', e);
739
+ }
740
+ }
741
+
742
+ function updateHistoryUI() {
743
+ const historyContainer = document.getElementById('translationHistory');
744
+
745
+ // Clear current history
746
+ historyContainer.innerHTML = '';
747
+
748
+ if (translationHistory.length === 0) {
749
+ historyContainer.innerHTML = `
750
+ <div class="text-center text-muted py-5">
751
+ <i class="bi bi-clock-history fs-1 d-block mb-3"></i>
752
+ <p>No translation history yet</p>
753
+ <p class="small">Your recent translations will appear here</p>
754
+ </div>
755
+ `;
756
+ return;
757
+ }
758
+
759
+ translationHistory.forEach((item, index) => {
760
+ const historyItem = document.createElement('div');
761
+ historyItem.classList.add('card', 'border-0', 'shadow-sm', 'mb-3', 'animate__animated', 'animate__fadeIn');
762
+
763
+ const sourceLanguageName = document.querySelector(`#sourceLanguage option[value="${item.sourceLang}"]`).textContent;
764
+ const targetLanguageName = document.querySelector(`#targetLanguage option[value="${item.targetLang}"]`).textContent;
765
+
766
+ const time = new Intl.DateTimeFormat('default', {
767
+ hour: 'numeric',
768
+ minute: 'numeric'
769
+ }).format(item.timestamp);
770
+
771
+ const date = new Intl.DateTimeFormat('default', {
772
+ year: 'numeric',
773
+ month: 'short',
774
+ day: 'numeric'
775
+ }).format(item.timestamp);
776
+
777
+ historyItem.innerHTML = `
778
+ <div class="card-header bg-transparent border-0 pb-0">
779
+ <div class="d-flex justify-content-between align-items-center">
780
+ <div>
781
+ <span class="badge bg-primary me-1">${sourceLanguageName}</span>
782
+ <i class="bi bi-arrow-right small"></i>
783
+ <span class="badge bg-primary ms-1">${targetLanguageName}</span>
784
+ </div>
785
+ <small class="text-muted" title="${date}">${time}</small>
786
+ </div>
787
+ </div>
788
+ <div class="card-body pt-2">
789
+ <div class="small mb-2">
790
+ <div class="text-truncate text-primary fw-medium" title="${item.source}">${item.source}</div>
791
+ <div class="text-truncate mt-1" title="${item.target}">${item.target}</div>
792
+ </div>
793
+ <div class="d-flex justify-content-between mt-2">
794
+ <button class="btn btn-sm btn-primary rounded-pill restore-btn">
795
+ <i class="bi bi-arrow-counterclockwise me-1"></i> Use
796
+ </button>
797
+ <button class="btn btn-sm btn-outline-danger rounded-circle remove-btn">
798
+ <i class="bi bi-trash"></i>
799
+ </button>
800
+ </div>
801
+ </div>
802
+ `;
803
+
804
+ // Add event listener to restore button
805
+ historyItem.querySelector('.restore-btn').addEventListener('click', function() {
806
+ document.getElementById('sourceText').value = item.source;
807
+ document.getElementById('translatedText').textContent = item.target;
808
+ document.getElementById('sourceLanguage').value = item.sourceLang;
809
+ document.getElementById('targetLanguage').value = item.targetLang;
810
+ updateCharCount('sourceText', 'sourceCharCount');
811
+ updateCharCount('translatedText', 'targetCharCount', true);
812
+
813
+ // Close sidebar
814
+ document.getElementById('sidebar').classList.remove('open');
815
+
816
+ // Add highlight effect
817
+ pulseEffect('sourceText');
818
+ pulseEffect('translationResult');
819
+
820
+ showToast('Translation restored');
821
+ });
822
+
823
+ // Add event listener to remove button
824
+ historyItem.querySelector('.remove-btn').addEventListener('click', function() {
825
+ historyItem.classList.add('animate__fadeOut');
826
+
827
+ setTimeout(() => {
828
+ translationHistory.splice(index, 1);
829
+ updateHistoryUI();
830
+ updateHistoryCount();
831
+ saveHistory();
832
+ }, 500);
833
+ });
834
+
835
+ historyContainer.appendChild(historyItem);
836
+ });
837
+ }
838
+ </script>
839
+ {% endblock %}
translator/translator.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import MarianMTModel, MarianTokenizer
2
+ import torch
3
+ import threading
4
+ import queue
5
+ import time
6
+ import uuid
7
+
8
+ class Translator:
9
+ def __init__(self):
10
+ # Dictionary mapping language pairs to model names
11
+ self.models = {
12
+ 'en-fr': 'Helsinki-NLP/opus-mt-en-fr',
13
+ 'en-es': 'Helsinki-NLP/opus-mt-en-es',
14
+ 'en-de': 'Helsinki-NLP/opus-mt-en-de',
15
+ 'en-hi': 'Helsinki-NLP/opus-mt-en-hi',
16
+ 'fr-en': 'Helsinki-NLP/opus-mt-fr-en',
17
+ 'es-en': 'Helsinki-NLP/opus-mt-es-en',
18
+ 'de-en': 'Helsinki-NLP/opus-mt-de-en',
19
+ 'hi-en': 'Helsinki-NLP/opus-mt-hi-en',
20
+ # Add more language pairs as needed
21
+ }
22
+ # Cache for loaded models to avoid reloading - limited to 1 model for memory constraints
23
+ self.loaded_models = {}
24
+ self.loaded_tokenizers = {}
25
+ self.max_models_in_memory = 1 # Only keep one model in memory at a time
26
+
27
+ # Available languages
28
+ self.languages = {
29
+ 'en': 'English',
30
+ 'fr': 'French',
31
+ 'es': 'Spanish',
32
+ 'de': 'German',
33
+ 'hi': 'Hindi',
34
+ # Add more languages as needed
35
+ }
36
+
37
+ # For optimized translation
38
+ self.translation_queue = queue.Queue()
39
+ self.translation_results = {}
40
+ self.worker_thread = threading.Thread(target=self._translation_worker, daemon=True)
41
+ self.worker_thread.start()
42
+
43
+ def get_available_languages(self):
44
+ """Return available languages"""
45
+ return self.languages
46
+
47
+ def get_model_name(self, source_lang, target_lang):
48
+ """Get the appropriate model name for the language pair"""
49
+ lang_pair = f"{source_lang}-{target_lang}"
50
+ return self.models.get(lang_pair)
51
+
52
+ def load_model(self, model_name):
53
+ """Load model and tokenizer if not already loaded, with memory management"""
54
+ # If the model is already loaded, return it
55
+ if model_name in self.loaded_models:
56
+ return self.loaded_models[model_name], self.loaded_tokenizers[model_name]
57
+
58
+ # If we've reached our model limit, clear the oldest model first
59
+ if len(self.loaded_models) >= self.max_models_in_memory:
60
+ # Clear all models to conserve memory
61
+ print(f"Memory limit reached. Clearing models...")
62
+ self.loaded_models = {}
63
+ self.loaded_tokenizers = {}
64
+ # Force garbage collection to free memory
65
+ import gc
66
+ gc.collect()
67
+ torch.cuda.empty_cache() if torch.cuda.is_available() else None
68
+
69
+ # Now load the new model
70
+ print(f"Loading model: {model_name}")
71
+ try:
72
+ # Use more memory-efficient loading options
73
+ self.loaded_tokenizers[model_name] = MarianTokenizer.from_pretrained(model_name)
74
+ self.loaded_models[model_name] = MarianMTModel.from_pretrained(
75
+ model_name,
76
+ low_cpu_mem_usage=True,
77
+ torch_dtype=torch.float16, # Use half-precision to save memory
78
+ local_files_only=False, # Allow downloading if needed
79
+ force_download=False # Don't force download if already cached
80
+ )
81
+
82
+ # Force garbage collection after loading to minimize memory impact
83
+ import gc
84
+ gc.collect()
85
+
86
+ return self.loaded_models[model_name], self.loaded_tokenizers[model_name]
87
+ except Exception as e:
88
+ print(f"Error loading model {model_name}: {e}")
89
+ # Make error message more descriptive for debugging
90
+ import traceback
91
+ print(f"Detailed error: {traceback.format_exc()}")
92
+ raise
93
+
94
+ def _translation_worker(self):
95
+ """Background worker that processes translation requests"""
96
+ while True:
97
+ try:
98
+ # Get a translation task from the queue
99
+ task_id, text, source_lang, target_lang = self.translation_queue.get()
100
+
101
+ # Process the translation
102
+ model_name = self.get_model_name(source_lang, target_lang)
103
+
104
+ if not model_name:
105
+ self.translation_results[task_id] = f"Translation not available for {source_lang} to {target_lang}"
106
+ else:
107
+ try:
108
+ model, tokenizer = self.load_model(model_name)
109
+
110
+ # Handle large inputs by chunking if needed
111
+ max_length = 512 # Maximum sequence length most models can handle
112
+ if len(text) > max_length * 2: # Rough character to token ratio
113
+ # Simple chunking for long texts
114
+ chunks = [text[i:i + max_length * 2] for i in range(0, len(text), max_length * 2)]
115
+ translated_chunks = []
116
+
117
+ for chunk in chunks:
118
+ # Process each chunk
119
+ inputs = tokenizer(chunk, return_tensors="pt", padding=True, truncation=True, max_length=max_length)
120
+
121
+ # Use memory-efficient settings
122
+ with torch.no_grad():
123
+ translated = model.generate(
124
+ **inputs,
125
+ num_beams=2, # Reduce beam size to save memory
126
+ early_stopping=True,
127
+ max_length=max_length
128
+ )
129
+
130
+ chunk_text = tokenizer.batch_decode(translated, skip_special_tokens=True)[0]
131
+ translated_chunks.append(chunk_text)
132
+
133
+ # Join the translated chunks
134
+ self.translation_results[task_id] = " ".join(translated_chunks)
135
+ else:
136
+ # Process as usual for smaller texts
137
+ inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=max_length)
138
+
139
+ # Generate translation with memory-efficient settings
140
+ with torch.no_grad():
141
+ translated = model.generate(
142
+ **inputs,
143
+ num_beams=2, # Reduce beam size to save memory
144
+ early_stopping=True,
145
+ max_length=max_length * 2
146
+ )
147
+
148
+ # Decode the generated tokens back to text
149
+ translated_text = tokenizer.batch_decode(translated, skip_special_tokens=True)
150
+ self.translation_results[task_id] = translated_text[0]
151
+
152
+ # Clear some memory after translation
153
+ import gc
154
+ gc.collect()
155
+ torch.cuda.empty_cache() if torch.cuda.is_available() else None
156
+
157
+ except Exception as e:
158
+ print(f"Translation error: {e}")
159
+ self.translation_results[task_id] = f"Error translating text: {str(e)[:100]}"
160
+
161
+ # Mark the task as done
162
+ self.translation_queue.task_done()
163
+ except Exception as e:
164
+ print(f"Error in translation worker: {e}")
165
+ self.translation_results[task_id] = "Server error occurred during translation"
166
+ self.translation_queue.task_done()
167
+ # Continue processing other items even if one fails
168
+ continue
169
+
170
+ def translate(self, text, source_lang, target_lang):
171
+ """Translate text from source language to target language"""
172
+ # Generate a unique ID for this translation request
173
+ task_id = str(uuid.uuid4())
174
+
175
+ # Submit the task to the background worker
176
+ self.translation_queue.put((task_id, text, source_lang, target_lang))
177
+
178
+ # Return the task ID immediately
179
+ return {"task_id": task_id, "status": "processing"}
180
+
181
+ def get_translation_result(self, task_id):
182
+ """Get the result of a translation task by its ID"""
183
+ if task_id in self.translation_results:
184
+ result = self.translation_results[task_id]
185
+ # Clean up after returning the result
186
+ del self.translation_results[task_id]
187
+ return {"status": "completed", "translation": result}
188
+ else:
189
+ return {"status": "processing"}