Spaces:
Sleeping
Sleeping
Upload 14 files
Browse files- app.py +7 -0
- main.py +19 -0
- requirements.txt +6 -0
- runtime.txt +1 -0
- static/style.css +199 -0
- translator/__init__.py +1 -0
- translator/__pycache__/__init__.cpython-313.pyc +0 -0
- translator/__pycache__/app.cpython-313.pyc +0 -0
- translator/__pycache__/translator.cpython-313.pyc +0 -0
- translator/app.py +72 -0
- translator/static/style.css +436 -0
- translator/templates/base.html +626 -0
- translator/templates/index.html +839 -0
- translator/translator.py +189 -0
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"}
|